Ruby on Rails | Screencasts | Download | Documentation | Weblog | Community | Source

Ticket #5608 (reopened defect)

Opened 2 years ago

Last modified 1 year ago

ValidatesUniqueness with Scoping doesn't play with "type" (and probably other magic field names.

Reported by: rubyonrails@datanomisch.de Assigned to: David
Priority: normal Milestone: 1.2
Component: ActiveRecord Version: 1.1.1
Severity: normal Keywords: inheritance
Cc: bitsweat, masterkain, chewi

Description (Last modified by bitsweat)

I have two classes:

class Relationship < ActiveRecord::Base

  belongs_to :source,
    :class_name => "User",
    :foreign_key => "source_id"

  belongs_to :target,
    :class_name => "User",
    :foreign_key => "target_id"

end


class Friendship < Relationship

  belongs_to :user,
    :class_name => "User",
    :foreign_key => "source_id"

  belongs_to :friend,
    :class_name => "User",
    :foreign_key => "target_id"

end

I want to ensure that only one combination of type, source_id, target_id exists which I try to achieve through

validates_uniqueness_of :target_id, :scope => [:type, :source_id]

and that produces the following messages

can't dump anonymous class Class

RAILS_ROOT: ./script/../config/..
Application Trace | Framework Trace | Full Trace

C:/PROGRAMME/RUBY-1.82/lib/ruby/1.8/yaml/rubytypes.rb:9:in `to_yaml'
C:/PROGRAMME/RUBY-1.82/lib/ruby/gems/1.8/gems/activerecord-1.14.2/lib/active_record/connection_adapters/abstract/quoting.rb:22:in `quote'
C:/PROGRAMME/RUBY-1.82/lib/ruby/gems/1.8/gems/activerecord-1.14.2/lib/active_record/connection_adapters/postgresql_adapter.rb:117:in `quote'
C:/PROGRAMME/RUBY-1.82/lib/ruby/gems/1.8/gems/activerecord-1.14.2/lib/active_record/base.rb:1305:in `quote_bound_value'
C:/PROGRAMME/RUBY-1.82/lib/ruby/gems/1.8/gems/activerecord-1.14.2/lib/active_record/base.rb:1287:in `replace_bind_variables'
C:/PROGRAMME/RUBY-1.82/lib/ruby/gems/1.8/gems/activerecord-1.14.2/lib/active_record/base.rb:1287:in `gsub'
C:/PROGRAMME/RUBY-1.82/lib/ruby/gems/1.8/gems/activerecord-1.14.2/lib/active_record/base.rb:1287:in `replace_bind_variables'
C:/PROGRAMME/RUBY-1.82/lib/ruby/gems/1.8/gems/activerecord-1.14.2/lib/active_record/base.rb:1276:in `sanitize_sql'
C:/PROGRAMME/RUBY-1.82/lib/ruby/gems/1.8/gems/activerecord-1.14.2/lib/active_record/base.rb:1062:in `add_conditions!'
C:/PROGRAMME/RUBY-1.82/lib/ruby/gems/1.8/gems/activerecord-1.14.2/lib/active_record/base.rb:1012:in `construct_finder_sql'
C:/PROGRAMME/RUBY-1.82/lib/ruby/gems/1.8/gems/activerecord-1.14.2/lib/active_record/base.rb:924:in `find_every'
C:/PROGRAMME/RUBY-1.82/lib/ruby/gems/1.8/gems/activerecord-1.14.2/lib/active_record/base.rb:918:in `find_initial'
C:/PROGRAMME/RUBY-1.82/lib/ruby/gems/1.8/gems/activerecord-1.14.2/lib/active_record/base.rb:380:in `find'
C:/PROGRAMME/RUBY-1.82/lib/ruby/gems/1.8/gems/activerecord-1.14.2/lib/active_record/validations.rb:528:in `validates_uniqueness_of'
C:/PROGRAMME/RUBY-1.82/lib/ruby/gems/1.8/gems/activerecord-1.14.2/lib/active_record/validations.rb:514:in `validates_each'
C:/PROGRAMME/RUBY-1.82/lib/ruby/gems/1.8/gems/activerecord-1.14.2/lib/active_record/validations.rb:299:in `each'
C:/PROGRAMME/RUBY-1.82/lib/ruby/gems/1.8/gems/activerecord-1.14.2/lib/active_record/validations.rb:299:in `validates_each'
C:/PROGRAMME/RUBY-1.82/lib/ruby/gems/1.8/gems/activerecord-1.14.2/lib/active_record/validations.rb:296:in `call'
C:/PROGRAMME/RUBY-1.82/lib/ruby/gems/1.8/gems/activerecord-1.14.2/lib/active_record/validations.rb:794:in `run_validations'
C:/PROGRAMME/RUBY-1.82/lib/ruby/gems/1.8/gems/activerecord-1.14.2/lib/active_record/validations.rb:788:in `each'
C:/PROGRAMME/RUBY-1.82/lib/ruby/gems/1.8/gems/activerecord-1.14.2/lib/active_record/validations.rb:788:in `run_validations'
C:/PROGRAMME/RUBY-1.82/lib/ruby/gems/1.8/gems/activerecord-1.14.2/lib/active_record/validations.rb:752:in `valid_without_callbacks'
C:/PROGRAMME/RUBY-1.82/lib/ruby/gems/1.8/gems/activerecord-1.14.2/lib/active_record/callbacks.rb:306:in `valid?'
C:/PROGRAMME/RUBY-1.82/lib/ruby/gems/1.8/gems/activerecord-1.14.2/lib/active_record/validations.rb:723:in `save_without_transactions'
C:/PROGRAMME/RUBY-1.82/lib/ruby/gems/1.8/gems/activerecord-1.14.2/lib/active_record/transactions.rb:126:in `save'
C:/PROGRAMME/RUBY-1.82/lib/ruby/gems/1.8/gems/activerecord-1.14.2/lib/active_record/transactions.rb:126:in `transaction'
C:/PROGRAMME/RUBY-1.82/lib/ruby/gems/1.8/gems/activerecord-1.14.2/lib/active_record/transactions.rb:91:in `transaction'
C:/PROGRAMME/RUBY-1.82/lib/ruby/gems/1.8/gems/activerecord-1.14.2/lib/active_record/transactions.rb:118:in `transaction'
C:/PROGRAMME/RUBY-1.82/lib/ruby/gems/1.8/gems/activerecord-1.14.2/lib/active_record/transactions.rb:126:in `save'
#{RAILS_ROOT}/app/controllers/account_controller.rb:74:in `add_friend'
-e:3:in `load'
-e:3

Change History

07/05/06 17:41:06 changed by bitsweat

  • cc set to bitsweat.
  • description changed.

11/17/06 05:24:34 changed by bitsweat

Scoping to :type means you call the type method on the record, which returns its class.

Should we call read_attribute(:type) instead? What about folks who want to scope to a method which has no corresponding attribute, then?

A workaround:

def ruby_type() read_attribute('type') end
validates_uniqueness_of :target_id, :scope => [:ruby_type, :source_id]

(in reply to: ↑ description ; follow-up: ↓ 4 ) 11/18/06 05:52:55 changed by mmerlin

Replying to rubyonrails@datanomisch.de:

On windows XP professional with:
InstantRails 1.4
Ruby 1.8.5
RoR 1.1.6

I encountered the same problem, and reduced it to a minimal case before I found this bug entry. For testing, I created a new rails project using a MySQL database with single (real) table, and 2 models. The table only has 3 fields; id, name, type. I did not even bother creating a controller. Below is the initial mimimal failing sample, followed by a variation to attempt to get past the error, PLUS a different work around.

First failing case

schema.rb

ActiveRecord::Schema.define(:version => 1) do
  create_table "parents", :force => true do |t|
    t.column "name", :string
    t.column "type", :string
  end
end

parent.rb

class Parent < ActiveRecord::Base
  validates_uniqueness_of :name, :scope => :type
end

child.rb

class Child < Parent
end

parent_test.rb

require File.dirname(__FILE__) + '/../test_helper'

class ParentTest < Test::Unit::TestCase
  fixtures :parents

  def test_sti
    assert Child.new( :name => "Peter" ).valid?, "fatal"
  end
end

The test run

C:\InstantRails\InstantRails-1.4-win\rails_apps\sti>ruby test/unit/parent_test.rb
Loaded suite test/unit/parent_test
Started
C:/InstantRails/InstantRails-1.4-win/ruby/lib/ruby/gems/1.8/gems/activerecord-1.14.4/lib/active_record/validations.rb:531: warning: Object#type is deprecated; use Object#class
E
Finished in 0.328 seconds.

  1) Error:
test_sti(ParentTest):
TypeError: can't dump anonymous class Class
C:/InstantRails/InstantRails-1.4-win/ruby/lib/ruby/1.8/yaml/rubytypes.rb:6:in `to_yaml'
C:/InstantRails/InstantRails-1.4-win/ruby/lib/ruby/gems/1.8/gems/activerecord-1.14.4/lib/active_record/connection_adapters/abstract/quoting.rb:22:in `quote'
C:/InstantRails/InstantRails-1.4-win/ruby/lib/ruby/gems/1.8/gems/activerecord-1.14.4/lib/active_record/connection_adapters/mysql_adapter.rb:122:in `quote'
C:/InstantRails/InstantRails-1.4-win/ruby/lib/ruby/gems/1.8/gems/activerecord-1.14.4/lib/active_record/base.rb:1305:in `quote_bound_value'
C:/InstantRails/InstantRails-1.4-win/ruby/lib/ruby/gems/1.8/gems/activerecord-1.14.4/lib/active_record/base.rb:1287:in `replace_bind_variables'
C:/InstantRails/InstantRails-1.4-win/ruby/lib/ruby/gems/1.8/gems/activerecord-1.14.4/lib/active_record/base.rb:1287:in `gsub'
C:/InstantRails/InstantRails-1.4-win/ruby/lib/ruby/gems/1.8/gems/activerecord-1.14.4/lib/active_record/base.rb:1287:in `replace_bind_variables'
C:/InstantRails/InstantRails-1.4-win/ruby/lib/ruby/gems/1.8/gems/activerecord-1.14.4/lib/active_record/base.rb:1276:in `sanitize_sql'
C:/InstantRails/InstantRails-1.4-win/ruby/lib/ruby/gems/1.8/gems/activerecord-1.14.4/lib/active_record/base.rb:1062:in `add_conditions!'
C:/InstantRails/InstantRails-1.4-win/ruby/lib/ruby/gems/1.8/gems/activerecord-1.14.4/lib/active_record/base.rb:1012:in `construct_finder_sql'
C:/InstantRails/InstantRails-1.4-win/ruby/lib/ruby/gems/1.8/gems/activerecord-1.14.4/lib/active_record/base.rb:924:in `find_every'
C:/InstantRails/InstantRails-1.4-win/ruby/lib/ruby/gems/1.8/gems/activerecord-1.14.4/lib/active_record/base.rb:918:in `find_initial'
C:/InstantRails/InstantRails-1.4-win/ruby/lib/ruby/gems/1.8/gems/activerecord-1.14.4/lib/active_record/base.rb:380:in `find'
C:/InstantRails/InstantRails-1.4-win/ruby/lib/ruby/gems/1.8/gems/activerecord-1.14.4/lib/active_record/validations.rb:540:in `validates_uniqueness_of'
C:/InstantRails/InstantRails-1.4-win/ruby/lib/ruby/gems/1.8/gems/activerecord-1.14.4/lib/active_record/validations.rb:302:in `validates_each'
C:/InstantRails/InstantRails-1.4-win/ruby/lib/ruby/gems/1.8/gems/activerecord-1.14.4/lib/active_record/validations.rb:299:in `each'
C:/InstantRails/InstantRails-1.4-win/ruby/lib/ruby/gems/1.8/gems/activerecord-1.14.4/lib/active_record/validations.rb:299:in `validates_each'
C:/InstantRails/InstantRails-1.4-win/ruby/lib/ruby/gems/1.8/gems/activerecord-1.14.4/lib/active_record/validations.rb:806:in `call'
C:/InstantRails/InstantRails-1.4-win/ruby/lib/ruby/gems/1.8/gems/activerecord-1.14.4/lib/active_record/validations.rb:806:in `run_validations'
C:/InstantRails/InstantRails-1.4-win/ruby/lib/ruby/gems/1.8/gems/activerecord-1.14.4/lib/active_record/validations.rb:800:in `each'
C:/InstantRails/InstantRails-1.4-win/ruby/lib/ruby/gems/1.8/gems/activerecord-1.14.4/lib/active_record/validations.rb:800:in `run_validations'
C:/InstantRails/InstantRails-1.4-win/ruby/lib/ruby/gems/1.8/gems/activerecord-1.14.4/lib/active_record/validations.rb:764:in `valid_without_callbacks'
C:/InstantRails/InstantRails-1.4-win/ruby/lib/ruby/gems/1.8/gems/activerecord-1.14.4/lib/active_record/callbacks.rb:310:in `valid?'
test/unit/parent_test.rb:7:in `test_sti'

The very start of the error messages above showed a warning about a deprecated Object:type reference. I tried just changing the :scope => :type to :scope => :class, but that failed in (almost) the same way. The trace shows all of the same modules and line numbers, but the deprecated warning went away.

Second failing case

parent.rb

class Parent < ActiveRecord::Base
  validates_uniqueness_of :name, :scope => :class
end

The test run

    C:\InstantRails\InstantRails-1.4-win\rails_apps\sti>ruby test/unit/parent_test.rb
Loaded suite test/unit/parent_test
Started
E
Finished in 0.734 seconds.

  1) Error:
test_sti(ParentTest):
TypeError: can't dump anonymous class Class
C:/InstantRails/InstantRails-1.4-win/ruby/lib/ruby/1.8/yaml/rubytypes.rb:6:in `to_yaml'
C:/InstantRails/InstantRails-1.4-win/ruby/lib/ruby/gems/1.8/gems/activerecord-1.14.4/lib/active_record/connection_adapters/abstract/quoting.rb:22:in `quote'
. . .
C:/InstantRails/InstantRails-1.4-win/ruby/lib/ruby/gems/1.8/gems/activerecord-1.14.4/lib/active_record/callbacks.rb:310:in `valid?'
test/unit/parent_test.rb:7:in `test_sti'

Here is (one way) to get this work, as long as you can change the type field in the database to something else. I used class_name then override the inheritance_column to point to the alternate name, and no more complaints. I expect that one of those each or every references in the traceback is getting the field names (database columns), and building an 'executable' .<field> (.type here) reference from it. The complete fix might be as simple as changing that to a [:<field>] reference. Would that impact other fields or performance?

Working adjustment (workaround)

parent.rb

class Parent < ActiveRecord::Base
  validates_uniqueness_of :name, :scope => :class_name
  def self.inheritance_column() :class_name end
end

schema.rb

ActiveRecord::Schema.define(:version => 1) do
  create_table "parents", :force => true do |t|
    t.column "name", :string
    t.column "class_name", :string
  end
end

The test run

C:\InstantRails\InstantRails-1.4-win\rails_apps\sti>ruby test/unit/parent_test.b
Loaded suite test/unit/parent_test
Started
.
Finished in 0.281 seconds.

1 tests, 1 assertions, 0 failures, 0 errors

(in reply to: ↑ 3 ) 11/18/06 22:16:48 changed by mmerlin

Replying to mmerlin:

I tried to make the :scope reference generic, so that it would not be dependent on the specific inheritance column name. Rails does not like anything I tried.

self.inheritance_column() :class_name end

valididates_uniqueness_of ... :scope => :class_name #WORKS
valididates_uniqueness_of ... :scope => :class #FAILS
valididates_uniqueness_of ... :scope => inheritance_column #FAILS

(follow-up: ↓ 6 ) 11/18/06 22:36:50 changed by bitsweat

  • keywords set to inheritance.
  • status changed from new to closed.
  • resolution set to wontfix.

mmerlin, did you see the workaround I proposed?

Simply create a method that reads the inheritance column, and scope on that method.

We have three choices in this scenario: 1. change the inheritance column name in the database 2. create a method that returns the value of the inheritance column and scope on that 3. change uniqueness scoping to read the given *attribute* rather than call the given method

I think the fact that (2) is a very simple workaround for this pathological scenario is reason enough to leave the implementation as-is.

(in reply to: ↑ 5 ) 11/19/06 05:26:08 changed by mmerlin

Replying to bitsweat:

mmerlin, did you see the workaround I proposed? Simply create a method that reads the inheritance column, and scope on that method. We have three choices in this scenario: 1. change the inheritance column name in the database 2. create a method that returns the value of the inheritance column and scope on that 3. change uniqueness scoping to read the given *attribute* rather than call the given method I think the fact that (2) is a very simple workaround for this pathological scenario is reason enough to leave the implementation as-is.

I saw the workaround, but to (noob) me, option 1 looked cleaner [at least with a new project / database]. It did get rid of the initial error, but I have been trying to 'enhance' it to learn more about how RoR works. Just found one more problem with my implementation of option 1. I had:

def self.inheritance_column() :class_name end

It turns out that Rails does not 'see' that as a real inheritance column. When doing Child.new(), the class_name was being left blank / nil. Changing that to:

def self.inheritance_column() "class_name" end

and it is being populated properly again.

Option 2 is also viable (although I did not test that case [yet]). Rails is still 'broken', because using :scope => :class or :scope => :type should either work, or be documented as invalid, with the [preferred] alternative. That dump / trace is a bit extreme for using what appears perfectly reasonable, especially since the error only occurs [for me at least] when doing .valid?, with nothing pointing to what the real problem is [at least when using :scope => :class]

05/07/07 19:14:28 changed by stouset

  • status changed from closed to reopened.
  • resolution deleted.

Recommended fix by bitsweat does not work. Thus, I'm reopening.

If you scope on :ruby_type where ruby_type is a method you've defined in the model, this simply causes the database query for the "ruby_type" column to fail. Remember, the scoping is done at a database level -- it's not loading every database row into a n ActiveRecord object just to compare their type.

mmerlin's workaround of using a different name for the inheritance column does work, but changing the default name of the inheritance column just to get unique scoping seems a bit unreasonable.

06/08/07 15:42:15 changed by zaydana

I would say changing the line in validates_each from: [code]value = record.send(attr)/code to [code]value = record.attributes[attr.to_s]/code would be the best fix.

Since the validates is based on SQL anyway, I can't imagine there would be many scenarios where it is going to fail?

The other possibilities I see would be:

  1. Letting us choose between whether it should use .send (which would be default), or .attributes.
  2. Letting the user specify a different name for the database column to how the value for that column is found (.send(:ruby_type), and 'type' column in the database

06/15/07 03:48:33 changed by masterkain

  • cc changed from bitsweat to bitsweat, masterkain.

08/01/07 14:27:43 changed by chewi

  • cc changed from bitsweat, masterkain to bitsweat, masterkain, chewi.

10/06/07 20:30:00 changed by chewi

This may not be a great idea but I've found that redefining type works as a workaround. I haven't seen any ill effects of doing this so far. Ruby always emits a warning when you call type so it's unlikely that Rails calls it anywhere intentionally.

  def type
    read_attribute(:type)
  end