diff options
56 files changed, 1538 insertions, 353 deletions
diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG index 92848c8a7d..f82bd0da92 100644 --- a/activerecord/CHANGELOG +++ b/activerecord/CHANGELOG @@ -1,5 +1,11 @@ *Rails 3.1.0 (unreleased)* +* Associations with a :through option can now use *any* association as the + through or source association, including other associations which have a + :through option and has_and_belongs_to_many associations + + [Jon Leighton] + * limits and offsets are removed from COUNT queries unless both are supplied. For example: diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index e91cbd7f33..ec5b41a3e7 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -52,14 +52,6 @@ module ActiveRecord end end - class HasManyThroughSourceAssociationMacroError < ActiveRecordError #:nodoc: - def initialize(reflection) - through_reflection = reflection.through_reflection - source_reflection = reflection.source_reflection - super("Invalid source reflection macro :#{source_reflection.macro}#{" :through" if source_reflection.options[:through]} for has_many #{reflection.name.inspect}, :through => #{through_reflection.name.inspect}. Use :source to specify the source reflection.") - end - end - class HasManyThroughCantAssociateThroughHasOneOrManyReflection < ActiveRecordError #:nodoc: def initialize(owner, reflection) super("Cannot modify association '#{owner.class.name}##{reflection.name}' because the source reflection class '#{reflection.source_reflection.class_name}' is associated to '#{reflection.through_reflection.class_name}' via :#{reflection.source_reflection.macro}.") @@ -78,6 +70,12 @@ module ActiveRecord end end + class HasManyThroughNestedAssociationsAreReadonly < ActiveRecordError #:nodoc + def initialize(owner, reflection) + super("Cannot modify association '#{owner.class.name}##{reflection.name}' because it goes through more than one other association.") + end + end + class HasAndBelongsToManyAssociationWithPrimaryKeyError < ActiveRecordError #:nodoc: def initialize(reflection) super("Primary key is not allowed in a has_and_belongs_to_many join table (#{reflection.options[:join_table]}).") @@ -144,6 +142,7 @@ module ActiveRecord autoload :Preloader, 'active_record/associations/preloader' autoload :JoinDependency, 'active_record/associations/join_dependency' + autoload :AliasTracker, 'active_record/associations/alias_tracker' # Clears out the association cache. def clear_association_cache #:nodoc: @@ -548,6 +547,49 @@ module ActiveRecord # belongs_to :tag, :inverse_of => :taggings # end # + # === Nested Associations + # + # You can actually specify *any* association with the <tt>:through</tt> option, including an + # association which has a <tt>:through</tt> option itself. For example: + # + # class Author < ActiveRecord::Base + # has_many :posts + # has_many :comments, :through => :posts + # has_many :commenters, :through => :comments + # end + # + # class Post < ActiveRecord::Base + # has_many :comments + # end + # + # class Comment < ActiveRecord::Base + # belongs_to :commenter + # end + # + # @author = Author.first + # @author.commenters # => People who commented on posts written by the author + # + # An equivalent way of setting up this association this would be: + # + # class Author < ActiveRecord::Base + # has_many :posts + # has_many :commenters, :through => :posts + # end + # + # class Post < ActiveRecord::Base + # has_many :comments + # has_many :commenters, :through => :comments + # end + # + # class Comment < ActiveRecord::Base + # belongs_to :commenter + # end + # + # When using nested association, you will not be able to modify the association because there + # is not enough information to know what modification to make. For example, if you tried to + # add a <tt>Commenter</tt> in the example above, there would be no way to tell how to set up the + # intermediate <tt>Post</tt> and <tt>Comment</tt> objects. + # # === Polymorphic Associations # # Polymorphic associations on models are not restricted on what types of models they @@ -1068,10 +1110,10 @@ module ActiveRecord # [:as] # Specifies a polymorphic interface (See <tt>belongs_to</tt>). # [:through] - # Specifies a join model through which to perform the query. Options for <tt>:class_name</tt>, + # Specifies an association through which to perform the query. This can be any other type + # of association, including other <tt>:through</tt> associations. Options for <tt>:class_name</tt>, # <tt>:primary_key</tt> and <tt>:foreign_key</tt> are ignored, as the association uses the - # source reflection. You can only use a <tt>:through</tt> query through a <tt>belongs_to</tt>, - # <tt>has_one</tt> or <tt>has_many</tt> association on the join model. + # source reflection. # # If the association on the join model is a +belongs_to+, the collection can be modified # and the records on the <tt>:through</tt> model will be automatically created and removed @@ -1198,10 +1240,10 @@ module ActiveRecord # you want to do a join but not include the joined columns. Do not forget to include the # primary and foreign keys, otherwise it will raise an error. # [:through] - # Specifies a Join Model through which to perform the query. Options for <tt>:class_name</tt> - # and <tt>:foreign_key</tt> are ignored, as the association uses the source reflection. You - # can only use a <tt>:through</tt> query through a <tt>has_one</tt> or <tt>belongs_to</tt> - # association on the join model. + # Specifies a Join Model through which to perform the query. Options for <tt>:class_name</tt>, + # <tt>:primary_key</tt>, and <tt>:foreign_key</tt> are ignored, as the association uses the + # source reflection. You can only use a <tt>:through</tt> query through a <tt>has_one</tt> + # or <tt>belongs_to</tt> association on the join model. # [:source] # Specifies the source association name used by <tt>has_one :through</tt> queries. # Only use it if the name cannot be inferred from the association. diff --git a/activerecord/lib/active_record/associations/alias_tracker.rb b/activerecord/lib/active_record/associations/alias_tracker.rb new file mode 100644 index 0000000000..6fc2bfdb31 --- /dev/null +++ b/activerecord/lib/active_record/associations/alias_tracker.rb @@ -0,0 +1,85 @@ +require 'active_support/core_ext/string/conversions' + +module ActiveRecord + module Associations + # Keeps track of table aliases for ActiveRecord::Associations::ClassMethods::JoinDependency and + # ActiveRecord::Associations::ThroughAssociationScope + class AliasTracker # :nodoc: + attr_reader :aliases, :table_joins + + # table_joins is an array of arel joins which might conflict with the aliases we assign here + def initialize(table_joins = []) + @aliases = Hash.new + @table_joins = table_joins + end + + def aliased_table_for(table_name, aliased_name = nil) + table_alias = aliased_name_for(table_name, aliased_name) + + if table_alias == table_name # TODO: Is this conditional necessary? + Arel::Table.new(table_name) + else + Arel::Table.new(table_name).alias(table_alias) + end + end + + def aliased_name_for(table_name, aliased_name = nil) + aliased_name ||= table_name + + initialize_count_for(table_name) if aliases[table_name].nil? + + if aliases[table_name].zero? + # If it's zero, we can have our table_name + aliases[table_name] = 1 + table_name + else + # Otherwise, we need to use an alias + aliased_name = connection.table_alias_for(aliased_name) + + initialize_count_for(aliased_name) if aliases[aliased_name].nil? + + # Update the count + aliases[aliased_name] += 1 + + if aliases[aliased_name] > 1 + "#{truncate(aliased_name)}_#{aliases[aliased_name]}" + else + aliased_name + end + end + end + + def pluralize(table_name) + ActiveRecord::Base.pluralize_table_names ? table_name.to_s.pluralize : table_name + end + + private + + def initialize_count_for(name) + aliases[name] = 0 + + unless Arel::Table === table_joins + # quoted_name should be downcased as some database adapters (Oracle) return quoted name in uppercase + quoted_name = connection.quote_table_name(name).downcase + + aliases[name] += table_joins.map { |join| + # Table names + table aliases + join.left.downcase.scan( + /join(?:\s+\w+)?\s+(\S+\s+)?#{quoted_name}\son/ + ).size + }.sum + end + + aliases[name] + end + + def truncate(name) + name[0..connection.table_alias_length-3] + end + + def connection + ActiveRecord::Base.connection + end + end + end +end diff --git a/activerecord/lib/active_record/associations/has_many_through_association.rb b/activerecord/lib/active_record/associations/has_many_through_association.rb index acac68fda5..9d2b29685b 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -34,7 +34,9 @@ module ActiveRecord end def insert_record(record, validate = true) + ensure_not_nested return if record.new_record? && !record.save(:validate => validate) + through_record(record).save! update_counter(1) record @@ -59,6 +61,8 @@ module ActiveRecord end def build_record(attributes) + ensure_not_nested + record = super(attributes) inverse = source_reflection.inverse_of @@ -93,6 +97,8 @@ module ActiveRecord end def delete_records(records, method) + ensure_not_nested + through = owner.association(through_reflection.name) scope = through.scoped.where(construct_join_attributes(*records)) diff --git a/activerecord/lib/active_record/associations/has_one_through_association.rb b/activerecord/lib/active_record/associations/has_one_through_association.rb index d76d729303..fdf8ae1453 100644 --- a/activerecord/lib/active_record/associations/has_one_through_association.rb +++ b/activerecord/lib/active_record/associations/has_one_through_association.rb @@ -12,6 +12,8 @@ module ActiveRecord private def create_through_record(record) + ensure_not_nested + through_proxy = owner.association(through_reflection.name) through_record = through_proxy.send(:load_target) diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb index c7c3cf521c..504f25271c 100644 --- a/activerecord/lib/active_record/associations/join_dependency.rb +++ b/activerecord/lib/active_record/associations/join_dependency.rb @@ -5,18 +5,16 @@ module ActiveRecord autoload :JoinBase, 'active_record/associations/join_dependency/join_base' autoload :JoinAssociation, 'active_record/associations/join_dependency/join_association' - attr_reader :join_parts, :reflections, :table_aliases, :active_record + attr_reader :join_parts, :reflections, :alias_tracker, :active_record def initialize(base, associations, joins) - @active_record = base - @table_joins = joins - @join_parts = [JoinBase.new(base)] - @associations = {} - @reflections = [] - @table_aliases = Hash.new do |h,name| - h[name] = count_aliases_from_table_joins(name.downcase) - end - @table_aliases[base.table_name] = 1 + @active_record = base + @table_joins = joins + @join_parts = [JoinBase.new(base)] + @associations = {} + @reflections = [] + @alias_tracker = AliasTracker.new(joins) + @alias_tracker.aliased_name_for(base.table_name) # Updates the count for base.table_name to 1 build(associations) end @@ -45,20 +43,6 @@ module ActiveRecord }.flatten end - def count_aliases_from_table_joins(name) - return 0 if Arel::Table === @table_joins - - # quoted_name should be downcased as some database adapters (Oracle) return quoted name in uppercase - quoted_name = active_record.connection.quote_table_name(name).downcase - - @table_joins.map { |join| - # Table names + table aliases - join.left.downcase.scan( - /join(?:\s+\w+)?\s+(\S+\s+)?#{quoted_name}\son/ - ).size - }.sum - end - def instantiate(rows) primary_key = join_base.aliased_primary_key parents = {} diff --git a/activerecord/lib/active_record/associations/join_dependency/join_association.rb b/activerecord/lib/active_record/associations/join_dependency/join_association.rb index ebe39c35fe..890e77fca9 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb @@ -18,10 +18,13 @@ module ActiveRecord attr_accessor :join_type # These implement abstract methods from the superclass - attr_reader :aliased_prefix, :aliased_table_name + attr_reader :aliased_prefix - delegate :options, :through_reflection, :source_reflection, :to => :reflection + attr_reader :tables + + delegate :options, :through_reflection, :source_reflection, :through_reflection_chain, :to => :reflection delegate :table, :table_name, :to => :parent, :prefix => :parent + delegate :alias_tracker, :to => :join_dependency def initialize(reflection, join_dependency, parent = nil) reflection.check_validity! @@ -38,13 +41,7 @@ module ActiveRecord @join_type = Arel::InnerJoin @aliased_prefix = "t#{ join_dependency.join_parts.size }" - # This must be done eagerly upon initialisation because the alias which is produced - # depends on the state of the join dependency, but we want it to work the same way - # every time. - allocate_aliases - @table = Arel::Table.new( - table_name, :as => aliased_table_name, :engine => arel_engine - ) + setup_tables end def ==(other) @@ -60,7 +57,95 @@ module ActiveRecord end def join_to(relation) - send("join_#{reflection.macro}_to", relation) + # The chain starts with the target table, but we want to end with it here (makes + # more sense in this context) + chain = through_reflection_chain.reverse + + foreign_table = parent_table + index = 0 + + chain.each do |reflection| + table = tables[index] + conditions = [] + + if reflection.source_reflection.nil? + case reflection.macro + when :belongs_to + key = reflection.association_primary_key + foreign_key = reflection.foreign_key + when :has_many, :has_one + key = reflection.foreign_key + foreign_key = reflection.active_record_primary_key + + conditions << polymorphic_conditions(reflection, table) + when :has_and_belongs_to_many + # For habtm, we need to deal with the join table at the same time as the + # target table (because unlike a :through association, there is no reflection + # to represent the join table) + table, join_table = table + + join_key = reflection.foreign_key + join_foreign_key = reflection.active_record.primary_key + + relation = relation.join(join_table, join_type).on( + join_table[join_key]. + eq(foreign_table[join_foreign_key]) + ) + + # We've done the first join now, so update the foreign_table for the second + foreign_table = join_table + + key = reflection.klass.primary_key + foreign_key = reflection.association_foreign_key + end + else + case reflection.source_reflection.macro + when :belongs_to + key = reflection.association_primary_key + foreign_key = reflection.foreign_key + + conditions << source_type_conditions(reflection, foreign_table) + when :has_many, :has_one + key = reflection.foreign_key + foreign_key = reflection.source_reflection.active_record_primary_key + when :has_and_belongs_to_many + table, join_table = table + + join_key = reflection.foreign_key + join_foreign_key = reflection.klass.primary_key + + relation = relation.join(join_table, join_type).on( + join_table[join_key]. + eq(foreign_table[join_foreign_key]) + ) + + foreign_table = join_table + + key = reflection.klass.primary_key + foreign_key = reflection.association_foreign_key + end + end + + conditions << table[key].eq(foreign_table[foreign_key]) + + conditions << reflection_conditions(index, table) + conditions << sti_conditions(reflection, table) + + ands = relation.create_and(conditions.flatten.compact) + + join = relation.create_join( + table, + relation.create_on(ands), + join_type) + + relation = relation.from(join) + + # The current table in this iteration becomes the foreign table in the next + foreign_table = table + index += 1 + end + + relation end def join_relation(joining_relation) @@ -68,42 +153,59 @@ module ActiveRecord joining_relation.joins(self) end - attr_reader :table - # More semantic name given we are talking about associations - alias_method :target_table, :table - - protected - - def aliased_table_name_for(name, suffix = nil) - aliases = @join_dependency.table_aliases - - if aliases[name] != 0 # We need an alias - connection = active_record.connection - - name = connection.table_alias_for "#{pluralize(reflection.name)}_#{parent_table_name}#{suffix}" - aliases[name] += 1 - name = name[0, connection.table_alias_length-3] + "_#{aliases[name]}" if aliases[name] > 1 + def table + if tables.last.is_a?(Array) + tables.last.first else - aliases[name] += 1 + tables.last end + end - name + def aliased_table_name + table.table_alias || table.name end - def pluralize(table_name) - ActiveRecord::Base.pluralize_table_names ? table_name.to_s.pluralize : table_name + protected + + def table_alias_for(reflection, join = false) + name = alias_tracker.pluralize(reflection.name) + name << "_#{parent_table_name}" + name << "_join" if join + name end private - def allocate_aliases - @aliased_table_name = aliased_table_name_for(table_name) + # Generate aliases and Arel::Table instances for each of the tables which we will + # later generate joins for. We must do this in advance in order to correctly allocate + # the proper alias. + def setup_tables + @tables = through_reflection_chain.map do |reflection| + table = alias_tracker.aliased_table_for( + reflection.table_name, + table_alias_for(reflection, reflection != self.reflection) + ) + + # For habtm, we have two Arel::Table instances related to a single reflection, so + # we just store them as a pair in the array. + if reflection.macro == :has_and_belongs_to_many || + (reflection.source_reflection && reflection.source_reflection.macro == :has_and_belongs_to_many) + + join_table = alias_tracker.aliased_table_for( + (reflection.source_reflection || reflection).options[:join_table], + table_alias_for(reflection, true) + ) - if reflection.macro == :has_and_belongs_to_many - @aliased_join_table_name = aliased_table_name_for(reflection.options[:join_table], "_join") - elsif [:has_many, :has_one].include?(reflection.macro) && reflection.options[:through] - @aliased_join_table_name = aliased_table_name_for(reflection.through_reflection.klass.table_name, "_join") + [table, join_table] + else + table + end end + + # The joins are generated from the through_reflection_chain in reverse order, so + # reverse the tables too (but it's important to generate the aliases in the 'forward' + # order, which is why we only do the reversal now. + @tables.reverse! end def process_conditions(conditions, table_name) @@ -118,161 +220,39 @@ module ActiveRecord active_record.send(:sanitize_sql, condition, table_name) end - def join_target_table(relation, condition) - conditions = [condition] + def reflection_conditions(index, table) + reflection.through_conditions.reverse[index].map do |condition| + process_conditions(condition, table.table_alias || table.name) + end + end - # If the target table is an STI model then we must be sure to only include records of - # its type and its sub-types. - unless active_record.descends_from_active_record? - sti_column = target_table[active_record.inheritance_column] - subclasses = active_record.descendants - sti_condition = sti_column.eq(active_record.sti_name) + def sti_conditions(reflection, table) + unless reflection.klass.descends_from_active_record? + sti_column = table[reflection.klass.inheritance_column] + sti_condition = sti_column.eq(reflection.klass.sti_name) + subclasses = reflection.klass.descendants - conditions << subclasses.inject(sti_condition) { |attr,subclass| + # TODO: use IN (...), or possibly AR::Base#type_condition + subclasses.inject(sti_condition) { |attr,subclass| attr.or(sti_column.eq(subclass.sti_name)) } end - - # If the reflection has conditions, add them - if options[:conditions] - conditions << process_conditions(options[:conditions], aliased_table_name) - end - - ands = relation.create_and(conditions) - - join = relation.create_join( - target_table, - relation.create_on(ands), - join_type) - - relation.from join - end - - def join_has_and_belongs_to_many_to(relation) - join_table = Arel::Table.new( - options[:join_table] - ).alias(@aliased_join_table_name) - - fk = options[:foreign_key] || reflection.active_record.to_s.foreign_key - klass_fk = options[:association_foreign_key] || reflection.klass.to_s.foreign_key - - relation = relation.join(join_table, join_type) - relation = relation.on( - join_table[fk]. - eq(parent_table[reflection.active_record.primary_key]) - ) - - join_target_table( - relation, - target_table[reflection.klass.primary_key]. - eq(join_table[klass_fk]) - ) end - def join_has_many_to(relation) - if reflection.options[:through] - join_has_many_through_to(relation) - elsif reflection.options[:as] - join_has_many_polymorphic_to(relation) - else - foreign_key = options[:foreign_key] || reflection.active_record.name.foreign_key - primary_key = options[:primary_key] || parent.primary_key - - join_target_table( - relation, - target_table[foreign_key]. - eq(parent_table[primary_key]) - ) + def source_type_conditions(reflection, foreign_table) + if reflection.options[:source_type] + foreign_table[reflection.source_reflection.foreign_type]. + eq(reflection.options[:source_type]) end end - alias :join_has_one_to :join_has_many_to - - def join_has_many_through_to(relation) - join_table = Arel::Table.new( - through_reflection.klass.table_name - ).alias @aliased_join_table_name - - jt_conditions = [] - first_key = second_key = nil - - if through_reflection.macro == :belongs_to - jt_primary_key = through_reflection.foreign_key - jt_foreign_key = through_reflection.association_primary_key - else - jt_primary_key = through_reflection.active_record_primary_key - jt_foreign_key = through_reflection.foreign_key - - if through_reflection.options[:as] # has_many :through against a polymorphic join - jt_conditions << - join_table["#{through_reflection.options[:as]}_type"]. - eq(parent.active_record.base_class.name) - end - end - - case source_reflection.macro - when :has_many - second_key = options[:foreign_key] || primary_key - - if source_reflection.options[:as] - first_key = "#{source_reflection.options[:as]}_id" - else - first_key = through_reflection.klass.base_class.to_s.foreign_key - end - - unless through_reflection.klass.descends_from_active_record? - jt_conditions << - join_table[through_reflection.active_record.inheritance_column]. - eq(through_reflection.klass.sti_name) - end - when :belongs_to - first_key = primary_key - - if reflection.options[:source_type] - second_key = source_reflection.association_foreign_key - - jt_conditions << - join_table[reflection.source_reflection.foreign_type]. - eq(reflection.options[:source_type]) - else - second_key = source_reflection.foreign_key - end - end - - jt_conditions << - parent_table[jt_primary_key]. - eq(join_table[jt_foreign_key]) - if through_reflection.options[:conditions] - jt_conditions << process_conditions(through_reflection.options[:conditions], aliased_table_name) + def polymorphic_conditions(reflection, table) + if reflection.options[:as] + table[reflection.type]. + eq(reflection.active_record.base_class.name) end - - relation = relation.join(join_table, join_type).on(*jt_conditions) - - join_target_table( - relation, - target_table[first_key].eq(join_table[second_key]) - ) end - def join_has_many_polymorphic_to(relation) - join_target_table( - relation, - target_table["#{reflection.options[:as]}_id"]. - eq(parent_table[parent.primary_key]).and( - target_table["#{reflection.options[:as]}_type"]. - eq(parent.active_record.base_class.name)) - ) - end - - def join_belongs_to_to(relation) - foreign_key = options[:foreign_key] || reflection.foreign_key - primary_key = options[:primary_key] || reflection.klass.primary_key - - join_target_table( - relation, - target_table[primary_key].eq(parent_table[foreign_key]) - ) - end end end end diff --git a/activerecord/lib/active_record/associations/preloader/through_association.rb b/activerecord/lib/active_record/associations/preloader/through_association.rb index d630fc4c63..30558ae29c 100644 --- a/activerecord/lib/active_record/associations/preloader/through_association.rb +++ b/activerecord/lib/active_record/associations/preloader/through_association.rb @@ -19,8 +19,9 @@ module ActiveRecord source_reflection.name, options ).run - through_records.each do |owner, owner_through_records| - owner_through_records.map! { |r| r.send(source_reflection.name) }.flatten! + through_records.each do |owner, records| + records.map! { |r| r.send(source_reflection.name) }.flatten! + records.compact! end end @@ -32,8 +33,13 @@ module ActiveRecord through_options ).run + # TODO: Verify that this is actually necessary and not just a symptom of an + # underlying inefficiency + identity_map = {} + Hash[owners.map do |owner| through_records = Array.wrap(owner.send(through_reflection.name)) + through_records.map! { |record| identity_map[record] ||= record } # Dont cache the association - we would only be caching a subset if reflection.options[:source_type] && through_reflection.collection? diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb index e1d60ccb17..ed24373cba 100644 --- a/activerecord/lib/active_record/associations/through_association.rb +++ b/activerecord/lib/active_record/associations/through_association.rb @@ -1,9 +1,12 @@ +require 'enumerator' + module ActiveRecord # = Active Record Through Association module Associations module ThroughAssociation #:nodoc: - delegate :source_options, :through_options, :source_reflection, :through_reflection, :to => :reflection + delegate :source_options, :through_options, :source_reflection, :through_reflection, + :through_reflection_chain, :through_conditions, :to => :reflection protected @@ -13,10 +16,12 @@ module ActiveRecord def association_scope scope = super.joins(construct_joins) - scope = add_conditions(scope) + scope = scope.where(reflection_conditions(0)) + unless options[:include] scope = scope.includes(source_options[:include]) end + scope end @@ -30,6 +35,7 @@ module ActiveRecord { } end + # TODO: Needed? def aliased_through_table name = through_reflection.table_name @@ -39,41 +45,101 @@ module ActiveRecord end def construct_owner_conditions - super(aliased_through_table, through_reflection) + reflection = through_reflection_chain.last + + if reflection.macro == :has_and_belongs_to_many + table = tables[reflection].first + else + table = Array.wrap(tables[reflection]).first + end + + super(table, reflection) end def construct_joins - right = aliased_through_table - left = reflection.klass.arel_table + joins, right_index = [], 1 - conditions = [] + # Iterate over each pair in the through reflection chain, joining them together + through_reflection_chain.each_cons(2) do |left, right| + left_table, right_table = tables[left], tables[right] - if source_reflection.macro == :belongs_to - reflection_primary_key = source_reflection.association_primary_key - source_primary_key = source_reflection.foreign_key + if left.source_reflection.nil? + case left.macro + when :belongs_to + joins << inner_join( + right_table, + left_table[left.association_primary_key], + right_table[left.foreign_key], + reflection_conditions(right_index) + ) + when :has_many, :has_one + joins << inner_join( + right_table, + left_table[left.foreign_key], + right_table[right.association_primary_key], + polymorphic_conditions(left, left), + reflection_conditions(right_index) + ) + when :has_and_belongs_to_many + joins << inner_join( + right_table, + left_table.first[left.foreign_key], + right_table[right.klass.primary_key], + reflection_conditions(right_index) + ) + end + else + case left.source_reflection.macro + when :belongs_to + joins << inner_join( + right_table, + left_table[left.association_primary_key], + right_table[left.foreign_key], + source_type_conditions(left), + reflection_conditions(right_index) + ) + when :has_many, :has_one + if right.macro == :has_and_belongs_to_many + join_table, right_table = tables[right] + end - if options[:source_type] - column = source_reflection.foreign_type - conditions << - right[column].eq(options[:source_type]) - end - else - reflection_primary_key = source_reflection.foreign_key - source_primary_key = source_reflection.active_record_primary_key + joins << inner_join( + right_table, + left_table[left.foreign_key], + right_table[left.source_reflection.active_record_primary_key], + polymorphic_conditions(left, left.source_reflection), + reflection_conditions(right_index) + ) - if source_options[:as] - column = "#{source_options[:as]}_type" - conditions << - left[column].eq(through_reflection.klass.name) + if right.macro == :has_and_belongs_to_many + joins << inner_join( + join_table, + right_table[right.klass.primary_key], + join_table[right.association_foreign_key] + ) + end + when :has_and_belongs_to_many + join_table, left_table = tables[left] + + joins << inner_join( + join_table, + left_table[left.klass.primary_key], + join_table[left.association_foreign_key] + ) + + joins << inner_join( + right_table, + join_table[left.foreign_key], + right_table[right.klass.primary_key], + reflection_conditions(right_index) + ) + end end - end - conditions << - left[reflection_primary_key].eq(right[source_primary_key]) + right_index += 1 + end - right.create_join( - right, - right.create_on(right.create_and(conditions))) + joins end # Construct attributes for :through pointing to owner and associate. This is used by the @@ -112,37 +178,88 @@ module ActiveRecord end end - # The reason that we are operating directly on the scope here (rather than passing - # back some arel conditions to be added to the scope) is because scope.where([x, y]) - # has a different meaning to scope.where(x).where(y) - the first version might - # perform some substitution if x is a string. - def add_conditions(scope) - unless through_reflection.klass.descends_from_active_record? - scope = scope.where(through_reflection.klass.send(:type_condition)) + def alias_tracker + @alias_tracker ||= AliasTracker.new + end + + # TODO: It is decidedly icky to have an array for habtm entries, and no array for others + def tables + @tables ||= begin + Hash[ + through_reflection_chain.map do |reflection| + table = alias_tracker.aliased_table_for( + reflection.table_name, + table_alias_for(reflection, reflection != self.reflection) + ) + + if reflection.macro == :has_and_belongs_to_many || + (reflection.source_reflection && + reflection.source_reflection.macro == :has_and_belongs_to_many) + + join_table = alias_tracker.aliased_table_for( + (reflection.source_reflection || reflection).options[:join_table], + table_alias_for(reflection, true) + ) + + [reflection, [join_table, table]] + else + [reflection, table] + end + end + ] end + end - scope = scope.where(interpolate(source_options[:conditions])) - scope.where(through_conditions) + def table_alias_for(reflection, join = false) + name = alias_tracker.pluralize(reflection.name) + name << "_#{self.reflection.name}" + name << "_join" if join + name end - # If there is a hash of conditions then we make sure the keys are scoped to the - # through table name if left ambiguous. - def through_conditions - conditions = interpolate(through_options[:conditions]) + def inner_join(table, left_column, right_column, *conditions) + conditions << left_column.eq(right_column) - if conditions.is_a?(Hash) - Hash[conditions.map { |key, value| - unless value.is_a?(Hash) || key.to_s.include?('.') - key = aliased_through_table.name + '.' + key.to_s - end + table.create_join( + table, + table.create_on(table.create_and(conditions.flatten.compact))) + end - [key, value] - }] - else - conditions + def reflection_conditions(index) + reflection = through_reflection_chain[index] + conditions = through_conditions[index].dup + + # TODO: maybe this should go in Reflection#through_conditions directly? + unless reflection.klass.descends_from_active_record? + conditions << reflection.klass.send(:type_condition) + end + + unless conditions.empty? + conditions.map! do |condition| + condition = reflection.klass.send(:sanitize_sql, interpolate(condition), reflection.table_name) + condition = Arel.sql(condition) unless condition.is_a?(Arel::Node) + condition + end + + Arel::Nodes::And.new(conditions) end end + def polymorphic_conditions(reflection, polymorphic_reflection) + if polymorphic_reflection.options[:as] + tables[reflection][polymorphic_reflection.type]. + eq(polymorphic_reflection.active_record.base_class.name) + end + end + + def source_type_conditions(reflection) + if reflection.options[:source_type] + tables[reflection.through_reflection][reflection.foreign_type]. + eq(reflection.options[:source_type]) + end + end + + # TODO: Think about this in the context of nested associations def stale_state if through_reflection.macro == :belongs_to owner[through_reflection.foreign_key].to_s @@ -153,6 +270,12 @@ module ActiveRecord through_reflection.macro == :belongs_to && !owner[through_reflection.foreign_key].nil? end + + def ensure_not_nested + if reflection.nested? + raise HasManyThroughNestedAssociationsAreReadonly.new(owner, reflection) + end + end end end end diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 5de08953f9..e3e2cac042 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -265,7 +265,12 @@ module ActiveRecord false end - def through_reflection_foreign_key + def through_reflection_chain + [self] + end + + def through_conditions + [Array.wrap(options[:conditions])] end def source_reflection @@ -363,7 +368,7 @@ module ActiveRecord # Holds all the meta-data about a :through association as it was specified # in the Active Record class. class ThroughReflection < AssociationReflection #:nodoc: - delegate :association_primary_key, :foreign_type, :to => :source_reflection + delegate :foreign_key, :foreign_type, :association_foreign_key, :to => :source_reflection # Gets the source of the through reflection. It checks both a singularized # and pluralized form for <tt>:belongs_to</tt> or <tt>:has_many</tt>. @@ -392,6 +397,101 @@ module ActiveRecord @through_reflection ||= active_record.reflect_on_association(options[:through]) end + # Returns an array of AssociationReflection objects which are involved in this through + # association. Each item in the array corresponds to a table which will be part of the + # query for this association. + # + # If the source reflection is itself a ThroughReflection, then we don't include self in + # the chain, but just defer to the source reflection. + # + # The chain is built by recursively calling through_reflection_chain on the source + # reflection and the through reflection. The base case for the recursion is a normal + # association, which just returns [self] for its through_reflection_chain. + def through_reflection_chain + @through_reflection_chain ||= begin + if source_reflection.source_reflection + # If the source reflection has its own source reflection, then the chain must start + # by getting us to that source reflection. + chain = source_reflection.through_reflection_chain + else + # If the source reflection does not go through another reflection, then we can get + # to this reflection directly, and so start the chain here + chain = [self] + end + + # Recursively build the rest of the chain + chain += through_reflection.through_reflection_chain + + # Finally return the completed chain + chain + end + end + + # Consider the following example: + # + # class Person + # has_many :articles + # has_many :comment_tags, :through => :articles + # end + # + # class Article + # has_many :comments + # has_many :comment_tags, :through => :comments, :source => :tags + # end + # + # class Comment + # has_many :tags + # end + # + # There may be conditions on Person.comment_tags, Article.comment_tags and/or Comment.tags, + # but only Comment.tags will be represented in the through_reflection_chain. So this method + # creates an array of conditions corresponding to the through_reflection_chain. Each item in + # the through_conditions array corresponds to an item in the through_reflection_chain, and is + # itself an array of conditions from an arbitrary number of relevant reflections. + def through_conditions + @through_conditions ||= begin + # Initialize the first item - which corresponds to this reflection - either by recursing + # into the souce reflection (if it is itself a through reflection), or by grabbing the + # source reflection conditions. + if source_reflection.source_reflection + conditions = source_reflection.through_conditions + else + conditions = [Array.wrap(source_reflection.options[:conditions])] + end + + # Add to it the conditions from this reflection if necessary. + conditions.first << options[:conditions] if options[:conditions] + + # Recursively fill out the rest of the array from the through reflection + conditions += through_reflection.through_conditions + + # And return + conditions + end + end + + # A through association is nested iff there would be more than one join table + def nested? + through_reflection_chain.length > 2 || + through_reflection.macro == :has_and_belongs_to_many + end + + # We want to use the klass from this reflection, rather than just delegate straight to + # the source_reflection, because the source_reflection may be polymorphic. We still + # need to respect the source_reflection's :primary_key option, though. + def association_primary_key + @association_primary_key ||= begin + # Get the "actual" source reflection if the immediate source reflection has a + # source reflection itself + source_reflection = self.source_reflection + while source_reflection.source_reflection + source_reflection = source_reflection.source_reflection + end + + source_reflection.options[:primary_key] || klass.primary_key + end + end + # Gets an array of possible <tt>:through</tt> source reflection names: # # [:singularized, :pluralized] @@ -429,10 +529,6 @@ module ActiveRecord raise HasManyThroughAssociationPolymorphicSourceError.new(active_record.name, self, source_reflection) end - unless [:belongs_to, :has_many, :has_one].include?(source_reflection.macro) && source_reflection.options[:through].nil? - raise HasManyThroughSourceAssociationMacroError.new(self) - end - if macro == :has_one && through_reflection.collection? raise HasOneThroughCantAssociateThroughCollection.new(active_record.name, self, through_reflection) end @@ -440,14 +536,6 @@ module ActiveRecord check_validity_of_inverse! end - def through_reflection_primary_key - through_reflection.belongs_to? ? through_reflection.klass.primary_key : through_reflection.foreign_key - end - - def through_reflection_foreign_key - through_reflection.foreign_key if through_reflection.belongs_to? - end - private def derive_class_name # get the class_name of the belongs_to association of the through reflection diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index cd1d7108b3..0c7a9ec56d 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -260,8 +260,9 @@ module ActiveRecord join_list ) + # TODO: Necessary? join_nodes.each do |join| - join_dependency.table_aliases[join.left.name.downcase] = 1 + join_dependency.alias_tracker.aliased_name_for(join.left.name.downcase) end join_dependency.graft(*stashed_association_joins) diff --git a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb index 0742e311d9..39e8a7960a 100644 --- a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb +++ b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb @@ -15,17 +15,17 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase def test_eager_association_loading_with_cascaded_two_levels authors = Author.find(:all, :include=>{:posts=>:comments}, :order=>"authors.id") - assert_equal 2, authors.size + assert_equal 3, authors.size assert_equal 5, authors[0].posts.size - assert_equal 1, authors[1].posts.size + assert_equal 3, authors[1].posts.size assert_equal 10, authors[0].posts.collect{|post| post.comments.size }.inject(0){|sum,i| sum+i} end def test_eager_association_loading_with_cascaded_two_levels_and_one_level authors = Author.find(:all, :include=>[{:posts=>:comments}, :categorizations], :order=>"authors.id") - assert_equal 2, authors.size + assert_equal 3, authors.size assert_equal 5, authors[0].posts.size - assert_equal 1, authors[1].posts.size + assert_equal 3, authors[1].posts.size assert_equal 10, authors[0].posts.collect{|post| post.comments.size }.inject(0){|sum,i| sum+i} assert_equal 1, authors[0].categorizations.size assert_equal 2, authors[1].categorizations.size @@ -51,24 +51,24 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase categories = Category.joins(:categorizations).includes([{:posts=>:comments}, :authors]) assert_nothing_raised do - assert_equal 2, categories.count - assert_equal 2, categories.all.uniq.size # Must uniq since instantiating with inner joins will get dupes + assert_equal 3, categories.count + assert_equal 3, categories.all.uniq.size # Must uniq since instantiating with inner joins will get dupes end end def test_cascaded_eager_association_loading_with_duplicated_includes categories = Category.includes(:categorizations).includes(:categorizations => :author).where("categorizations.id is not null") assert_nothing_raised do - assert_equal 2, categories.count - assert_equal 2, categories.all.size + assert_equal 3, categories.count + assert_equal 3, categories.all.size end end def test_cascaded_eager_association_loading_with_twice_includes_edge_cases categories = Category.includes(:categorizations => :author).includes(:categorizations => :post).where("posts.id is not null") assert_nothing_raised do - assert_equal 2, categories.count - assert_equal 2, categories.all.size + assert_equal 3, categories.count + assert_equal 3, categories.all.size end end @@ -81,15 +81,15 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase def test_eager_association_loading_with_cascaded_two_levels_with_two_has_many_associations authors = Author.find(:all, :include=>{:posts=>[:comments, :categorizations]}, :order=>"authors.id") - assert_equal 2, authors.size + assert_equal 3, authors.size assert_equal 5, authors[0].posts.size - assert_equal 1, authors[1].posts.size + assert_equal 3, authors[1].posts.size assert_equal 10, authors[0].posts.collect{|post| post.comments.size }.inject(0){|sum,i| sum+i} end def test_eager_association_loading_with_cascaded_two_levels_and_self_table_reference authors = Author.find(:all, :include=>{:posts=>[:comments, :author]}, :order=>"authors.id") - assert_equal 2, authors.size + assert_equal 3, authors.size assert_equal 5, authors[0].posts.size assert_equal authors(:david).name, authors[0].name assert_equal [authors(:david).name], authors[0].posts.collect{|post| post.author.name}.uniq @@ -157,9 +157,9 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase def test_eager_association_loading_where_first_level_returns_nil authors = Author.find(:all, :include => {:post_about_thinking => :comments}, :order => 'authors.id DESC') - assert_equal [authors(:mary), authors(:david)], authors + assert_equal [authors(:bob), authors(:mary), authors(:david)], authors assert_no_queries do - authors[1].post_about_thinking.comments.first + authors[2].post_about_thinking.comments.first end end end diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index 26808ae931..ed6337b596 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -68,8 +68,8 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_with_ordering list = Post.find(:all, :include => :comments, :order => "posts.id DESC") - [:eager_other, :sti_habtm, :sti_post_and_comments, :sti_comments, - :authorless, :thinking, :welcome + [:other_by_mary, :other_by_bob, :misc_by_mary, :misc_by_bob, :eager_other, + :sti_habtm, :sti_post_and_comments, :sti_comments, :authorless, :thinking, :welcome ].each_with_index do |post, index| assert_equal posts(post), list[index] end @@ -97,25 +97,25 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_preloading_has_many_in_multiple_queries_with_more_ids_than_database_can_handle Post.connection.expects(:in_clause_length).at_least_once.returns(5) posts = Post.find(:all, :include=>:comments) - assert_equal 7, posts.size + assert_equal 11, posts.size end def test_preloading_has_many_in_one_queries_when_database_has_no_limit_on_ids_it_can_handle Post.connection.expects(:in_clause_length).at_least_once.returns(nil) posts = Post.find(:all, :include=>:comments) - assert_equal 7, posts.size + assert_equal 11, posts.size end def test_preloading_habtm_in_multiple_queries_with_more_ids_than_database_can_handle Post.connection.expects(:in_clause_length).at_least_once.returns(5) posts = Post.find(:all, :include=>:categories) - assert_equal 7, posts.size + assert_equal 11, posts.size end def test_preloading_habtm_in_one_queries_when_database_has_no_limit_on_ids_it_can_handle Post.connection.expects(:in_clause_length).at_least_once.returns(nil) posts = Post.find(:all, :include=>:categories) - assert_equal 7, posts.size + assert_equal 11, posts.size end def test_load_associated_records_in_one_query_when_adapter_has_no_limit diff --git a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb index dc382c3007..73d02c9676 100644 --- a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb @@ -642,12 +642,12 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase def test_find_grouped all_posts_from_category1 = Post.find(:all, :conditions => "category_id = 1", :joins => :categories) grouped_posts_of_category1 = Post.find(:all, :conditions => "category_id = 1", :group => "author_id", :select => 'count(posts.id) as posts_count', :joins => :categories) - assert_equal 4, all_posts_from_category1.size - assert_equal 1, grouped_posts_of_category1.size + assert_equal 5, all_posts_from_category1.size + assert_equal 2, grouped_posts_of_category1.size end def test_find_scoped_grouped - assert_equal 4, categories(:general).posts_grouped_by_title.size + assert_equal 5, categories(:general).posts_grouped_by_title.size assert_equal 1, categories(:technology).posts_grouped_by_title.size end diff --git a/activerecord/test/cases/associations/has_many_through_associations_test.rb b/activerecord/test/cases/associations/has_many_through_associations_test.rb index efdecd4b09..9adaebe924 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -17,9 +17,10 @@ require 'models/developer' require 'models/subscriber' require 'models/book' require 'models/subscription' -require 'models/categorization' -require 'models/category' require 'models/essay' +require 'models/category' +require 'models/owner' +require 'models/categorization' require 'models/member' require 'models/membership' require 'models/club' @@ -27,7 +28,7 @@ require 'models/club' class HasManyThroughAssociationsTest < ActiveRecord::TestCase fixtures :posts, :readers, :people, :comments, :authors, :categories, :taggings, :tags, :owners, :pets, :toys, :jobs, :references, :companies, :members, :author_addresses, - :subscribers, :books, :subscriptions, :developers, :categorizations + :subscribers, :books, :subscriptions, :developers, :categorizations, :essays # Dummies to force column loads so query counts are clean. def setup @@ -656,6 +657,25 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert author.comments.include?(comment) end + def test_has_many_through_polymorphic_with_primary_key_option + assert_equal [categories(:general)], authors(:david).essay_categories + + authors = Author.joins(:essay_categories).where('categories.id' => categories(:general).id) + assert_equal authors(:david), authors.first + + assert_equal [owners(:blackbeard)], authors(:david).essay_owners + + authors = Author.joins(:essay_owners).where("owners.name = 'blackbeard'") + assert_equal authors(:david), authors.first + end + + def test_has_many_through_with_primary_key_option + assert_equal [categories(:general)], authors(:david).essay_categories_2 + + authors = Author.joins(:essay_categories_2).where('categories.id' => categories(:general).id) + assert_equal authors(:david), authors.first + end + def test_size_of_through_association_should_increase_correctly_when_has_many_association_is_added post = posts(:thinking) readers = post.readers.size @@ -679,10 +699,10 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase end def test_joining_has_many_through_belongs_to - posts = Post.joins(:author_categorizations). + posts = Post.joins(:author_categorizations).order('posts.id'). where('categorizations.id' => categorizations(:mary_thinking_sti).id) - assert_equal [posts(:eager_other)], posts + assert_equal [posts(:eager_other), posts(:misc_by_mary), posts(:other_by_mary)], posts end def test_select_chosen_fields_only diff --git a/activerecord/test/cases/associations/has_one_through_associations_test.rb b/activerecord/test/cases/associations/has_one_through_associations_test.rb index bfc5ddc747..9ba5549277 100644 --- a/activerecord/test/cases/associations/has_one_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_through_associations_test.rb @@ -9,13 +9,16 @@ require 'models/member_detail' require 'models/minivan' require 'models/dashboard' require 'models/speedometer' +require 'models/category' require 'models/author' +require 'models/essay' +require 'models/owner' require 'models/post' require 'models/comment' class HasOneThroughAssociationsTest < ActiveRecord::TestCase fixtures :member_types, :members, :clubs, :memberships, :sponsors, :organizations, :minivans, - :dashboards, :speedometers, :authors, :posts, :comments + :dashboards, :speedometers, :authors, :posts, :comments, :categories, :essays, :owners def setup @member = members(:groucho) @@ -242,6 +245,25 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase end end + def test_has_one_through_polymorphic_with_primary_key_option + assert_equal categories(:general), authors(:david).essay_category + + authors = Author.joins(:essay_category).where('categories.id' => categories(:general).id) + assert_equal authors(:david), authors.first + + assert_equal owners(:blackbeard), authors(:david).essay_owner + + authors = Author.joins(:essay_owner).where("owners.name = 'blackbeard'") + assert_equal authors(:david), authors.first + end + + def test_has_one_through_with_primary_key_option + assert_equal categories(:general), authors(:david).essay_category_2 + + authors = Author.joins(:essay_category_2).where('categories.id' => categories(:general).id) + assert_equal authors(:david), authors.first + end + def test_has_one_through_with_default_scope_on_join_model assert_equal posts(:welcome).comments.order('id').first, authors(:david).comment_on_first_post end diff --git a/activerecord/test/cases/associations/join_model_test.rb b/activerecord/test/cases/associations/join_model_test.rb index 19303fef9f..543eff7d8b 100644 --- a/activerecord/test/cases/associations/join_model_test.rb +++ b/activerecord/test/cases/associations/join_model_test.rb @@ -288,7 +288,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end def test_has_many_going_through_join_model_with_custom_foreign_key - assert_equal [], posts(:thinking).authors + assert_equal [authors(:bob)], posts(:thinking).authors assert_equal [authors(:mary)], posts(:authorless).authors end @@ -305,7 +305,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end def test_has_many_through_with_custom_primary_key_on_has_many_source - assert_equal [authors(:david)], posts(:thinking).authors_using_custom_pk + assert_equal [authors(:david), authors(:bob)], posts(:thinking).authors_using_custom_pk.order(:id) end def test_both_scoped_and_explicit_joins_should_be_respected @@ -399,14 +399,6 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end end - def test_has_many_through_has_many_through - assert_raise(ActiveRecord::HasManyThroughSourceAssociationMacroError) { authors(:david).tags } - end - - def test_has_many_through_habtm - assert_raise(ActiveRecord::HasManyThroughSourceAssociationMacroError) { authors(:david).post_categories } - end - def test_eager_load_has_many_through_has_many author = Author.find :first, :conditions => ['name = ?', 'David'], :include => :comments, :order => 'comments.id' SpecialComment.new; VerySpecialComment.new diff --git a/activerecord/test/cases/associations/nested_through_associations_test.rb b/activerecord/test/cases/associations/nested_through_associations_test.rb new file mode 100644 index 0000000000..a4ac69782a --- /dev/null +++ b/activerecord/test/cases/associations/nested_through_associations_test.rb @@ -0,0 +1,537 @@ +require "cases/helper" +require 'models/author' +require 'models/post' +require 'models/person' +require 'models/reference' +require 'models/job' +require 'models/reader' +require 'models/comment' +require 'models/tag' +require 'models/tagging' +require 'models/subscriber' +require 'models/book' +require 'models/subscription' +require 'models/rating' +require 'models/member' +require 'models/member_detail' +require 'models/member_type' +require 'models/sponsor' +require 'models/club' +require 'models/organization' +require 'models/category' +require 'models/categorization' +require 'models/membership' +require 'models/essay' + +class NestedThroughAssociationsTest < ActiveRecord::TestCase + fixtures :authors, :books, :posts, :subscriptions, :subscribers, :tags, :taggings, + :people, :readers, :references, :jobs, :ratings, :comments, :members, :member_details, + :member_types, :sponsors, :clubs, :organizations, :categories, :categories_posts, + :categorizations, :memberships, :essays + + # Through associations can either use the has_many or has_one macros. + # + # has_many + # - Source reflection can be has_many, has_one, belongs_to or has_and_belongs_to_many + # - Through reflection can be has_many, has_one, belongs_to or has_and_belongs_to_many + # + # has_one + # - Source reflection can be has_one or belongs_to + # - Through reflection can be has_one or belongs_to + # + # Additionally, the source reflection and/or through reflection may be subject to + # polymorphism and/or STI. + # + # When testing these, we need to make sure it works via loading the association directly, or + # joining the association, or including the association. We also need to ensure that associations + # are readonly where relevant. + + # has_many through + # Source: has_many through + # Through: has_many + def test_has_many_through_has_many_with_has_many_through_source_reflection + general = tags(:general) + assert_equal [general, general], authors(:david).tags + end + + def test_has_many_through_has_many_with_has_many_through_source_reflection_preload + authors = assert_queries(5) { Author.includes(:tags).to_a } + general = tags(:general) + + assert_no_queries do + assert_equal [general, general], authors.first.tags + end + end + + def test_has_many_through_has_many_with_has_many_through_source_reflection_preload_via_joins + assert_includes_and_joins_equal( + Author.where('tags.id' => tags(:general).id), + [authors(:david)], :tags + ) + + # This ensures that the polymorphism of taggings is being observed correctly + authors = Author.joins(:tags).where('taggings.taggable_type' => 'FakeModel') + assert authors.empty? + end + + # has_many through + # Source: has_many + # Through: has_many through + def test_has_many_through_has_many_through_with_has_many_source_reflection + luke, david = subscribers(:first), subscribers(:second) + assert_equal [luke, david, david], authors(:david).subscribers.order('subscribers.nick') + end + + def test_has_many_through_has_many_through_with_has_many_source_reflection_preload + luke, david = subscribers(:first), subscribers(:second) + authors = assert_queries(4) { Author.includes(:subscribers).to_a } + assert_no_queries do + assert_equal [luke, david, david], authors.first.subscribers.sort_by(&:nick) + end + end + + def test_has_many_through_has_many_through_with_has_many_source_reflection_preload_via_joins + # All authors with subscribers where one of the subscribers' nick is 'alterself' + assert_includes_and_joins_equal( + Author.where('subscribers.nick' => 'alterself'), + [authors(:david)], :subscribers + ) + end + + # has_many through + # Source: has_one through + # Through: has_one + def test_has_many_through_has_one_with_has_one_through_source_reflection + assert_equal [member_types(:founding)], members(:groucho).nested_member_types + end + + def test_has_many_through_has_one_with_has_one_through_source_reflection_preload + members = assert_queries(4) { Member.includes(:nested_member_types).to_a } + founding = member_types(:founding) + assert_no_queries do + assert_equal [founding], members.first.nested_member_types + end + end + + def test_has_many_through_has_one_with_has_one_through_source_reflection_preload_via_joins + assert_includes_and_joins_equal( + Member.where('member_types.id' => member_types(:founding).id), + [members(:groucho)], :nested_member_types + ) + end + + # has_many through + # Source: has_one + # Through: has_one through + def test_has_many_through_has_one_through_with_has_one_source_reflection + assert_equal [sponsors(:moustache_club_sponsor_for_groucho)], members(:groucho).nested_sponsors + end + + def test_has_many_through_has_one_through_with_has_one_source_reflection_preload + members = assert_queries(4) { Member.includes(:nested_sponsors).to_a } + mustache = sponsors(:moustache_club_sponsor_for_groucho) + assert_no_queries do + assert_equal [mustache], members.first.nested_sponsors + end + end + + def test_has_many_through_has_one_through_with_has_one_source_reflection_preload_via_joins + assert_includes_and_joins_equal( + Member.where('sponsors.id' => sponsors(:moustache_club_sponsor_for_groucho).id), + [members(:groucho)], :nested_sponsors + ) + end + + # has_many through + # Source: has_many through + # Through: has_one + def test_has_many_through_has_one_with_has_many_through_source_reflection + groucho_details, other_details = member_details(:groucho), member_details(:some_other_guy) + + assert_equal [groucho_details, other_details], + members(:groucho).organization_member_details.order('member_details.id') + end + + def test_has_many_through_has_one_with_has_many_through_source_reflection_preload + members = assert_queries(4) { Member.includes(:organization_member_details).to_a.sort_by(&:id) } + groucho_details, other_details = member_details(:groucho), member_details(:some_other_guy) + + assert_no_queries do + assert_equal [groucho_details, other_details], members.first.organization_member_details.sort_by(&:id) + end + end + + def test_has_many_through_has_one_with_has_many_through_source_reflection_preload_via_joins + assert_includes_and_joins_equal( + Member.where('member_details.id' => member_details(:groucho).id).order('member_details.id'), + [members(:groucho), members(:some_other_guy)], :organization_member_details + ) + + members = Member.joins(:organization_member_details). + where('member_details.id' => 9) + assert members.empty? + end + + # has_many through + # Source: has_many + # Through: has_one through + def test_has_many_through_has_one_through_with_has_many_source_reflection + groucho_details, other_details = member_details(:groucho), member_details(:some_other_guy) + + assert_equal [groucho_details, other_details], + members(:groucho).organization_member_details_2.order('member_details.id') + end + + def test_has_many_through_has_one_through_with_has_many_source_reflection_preload + members = assert_queries(4) { Member.includes(:organization_member_details_2).to_a.sort_by(&:id) } + groucho_details, other_details = member_details(:groucho), member_details(:some_other_guy) + + assert_no_queries do + assert_equal [groucho_details, other_details], members.first.organization_member_details_2.sort_by(&:id) + end + end + + def test_has_many_through_has_one_through_with_has_many_source_reflection_preload_via_joins + assert_includes_and_joins_equal( + Member.where('member_details.id' => member_details(:groucho).id).order('member_details.id'), + [members(:groucho), members(:some_other_guy)], :organization_member_details_2 + ) + + members = Member.joins(:organization_member_details_2). + where('member_details.id' => 9) + assert members.empty? + end + + # has_many through + # Source: has_and_belongs_to_many + # Through: has_many + def test_has_many_through_has_many_with_has_and_belongs_to_many_source_reflection + general, cooking = categories(:general), categories(:cooking) + + assert_equal [general, cooking], authors(:bob).post_categories.order('categories.id') + end + + def test_has_many_through_has_many_with_has_and_belongs_to_many_source_reflection_preload + authors = assert_queries(3) { Author.includes(:post_categories).to_a.sort_by(&:id) } + general, cooking = categories(:general), categories(:cooking) + + assert_no_queries do + assert_equal [general, cooking], authors[2].post_categories.sort_by(&:id) + end + end + + def test_has_many_through_has_many_with_has_and_belongs_to_many_source_reflection_preload_via_joins + assert_includes_and_joins_equal( + Author.where('categories.id' => categories(:cooking).id), + [authors(:bob)], :post_categories + ) + end + + # has_many through + # Source: has_many + # Through: has_and_belongs_to_many + def test_has_many_through_has_and_belongs_to_many_with_has_many_source_reflection + greetings, more = comments(:greetings), comments(:more_greetings) + + assert_equal [greetings, more], categories(:technology).post_comments.order('comments.id') + end + + def test_has_many_through_has_and_belongs_to_many_with_has_many_source_reflection_preload + categories = assert_queries(3) { Category.includes(:post_comments).to_a.sort_by(&:id) } + greetings, more = comments(:greetings), comments(:more_greetings) + + assert_no_queries do + assert_equal [greetings, more], categories[1].post_comments.sort_by(&:id) + end + end + + def test_has_many_through_has_and_belongs_to_many_with_has_many_source_reflection_preload_via_joins + assert_includes_and_joins_equal( + Category.where('comments.id' => comments(:more_greetings).id).order('comments.id'), + [categories(:general), categories(:technology)], :post_comments + ) + end + + # has_many through + # Source: has_many through a habtm + # Through: has_many through + def test_has_many_through_has_many_with_has_many_through_habtm_source_reflection + greetings, more = comments(:greetings), comments(:more_greetings) + + assert_equal [greetings, more], authors(:bob).category_post_comments.order('comments.id') + end + + def test_has_many_through_has_many_with_has_many_through_habtm_source_reflection_preload + authors = assert_queries(5) { Author.includes(:category_post_comments).to_a.sort_by(&:id) } + greetings, more = comments(:greetings), comments(:more_greetings) + + assert_no_queries do + assert_equal [greetings, more], authors[2].category_post_comments.sort_by(&:id) + end + end + + def test_has_many_through_has_many_with_has_many_through_habtm_source_reflection_preload_via_joins + assert_includes_and_joins_equal( + Author.where('comments.id' => comments(:does_it_hurt).id).order('comments.id'), + [authors(:david), authors(:mary)], :category_post_comments + ) + end + + # has_many through + # Source: belongs_to + # Through: has_many through + def test_has_many_through_has_many_through_with_belongs_to_source_reflection + assert_equal [tags(:general), tags(:general)], authors(:david).tagging_tags + end + + def test_has_many_through_has_many_through_with_belongs_to_source_reflection_preload + authors = assert_queries(5) { Author.includes(:tagging_tags).to_a } + general = tags(:general) + + assert_no_queries do + assert_equal [general, general], authors.first.tagging_tags + end + end + + def test_has_many_through_has_many_through_with_belongs_to_source_reflection_preload_via_joins + assert_includes_and_joins_equal( + Author.where('tags.id' => tags(:general).id), + [authors(:david)], :tagging_tags + ) + end + + # has_many through + # Source: has_many through + # Through: belongs_to + def test_has_many_through_belongs_to_with_has_many_through_source_reflection + welcome_general, thinking_general = taggings(:welcome_general), taggings(:thinking_general) + + assert_equal [welcome_general, thinking_general], + categorizations(:david_welcome_general).post_taggings.order('taggings.id') + end + + def test_has_many_through_belongs_to_with_has_many_through_source_reflection_preload + categorizations = assert_queries(4) { Categorization.includes(:post_taggings).to_a.sort_by(&:id) } + welcome_general, thinking_general = taggings(:welcome_general), taggings(:thinking_general) + + assert_no_queries do + assert_equal [welcome_general, thinking_general], categorizations.first.post_taggings.sort_by(&:id) + end + end + + def test_has_many_through_belongs_to_with_has_many_through_source_reflection_preload_via_joins + assert_includes_and_joins_equal( + Categorization.where('taggings.id' => taggings(:welcome_general).id).order('taggings.id'), + [categorizations(:david_welcome_general)], :post_taggings + ) + end + + # has_one through + # Source: has_one through + # Through: has_one + def test_has_one_through_has_one_with_has_one_through_source_reflection + assert_equal member_types(:founding), members(:groucho).nested_member_type + end + + def test_has_one_through_has_one_with_has_one_through_source_reflection_preload + members = assert_queries(4) { Member.includes(:nested_member_type).to_a.sort_by(&:id) } + founding = member_types(:founding) + + assert_no_queries do + assert_equal founding, members.first.nested_member_type + end + end + + def test_has_one_through_has_one_with_has_one_through_source_reflection_preload_via_joins + assert_includes_and_joins_equal( + Member.where('member_types.id' => member_types(:founding).id), + [members(:groucho)], :nested_member_type + ) + end + + # has_one through + # Source: belongs_to + # Through: has_one through + def test_has_one_through_has_one_through_with_belongs_to_source_reflection + assert_equal categories(:general), members(:groucho).club_category + end + + def test_has_one_through_has_one_through_with_belongs_to_source_reflection_preload + members = assert_queries(4) { Member.includes(:club_category).to_a.sort_by(&:id) } + general = categories(:general) + + assert_no_queries do + assert_equal general, members.first.club_category + end + end + + def test_has_one_through_has_one_through_with_belongs_to_source_reflection_preload_via_joins + assert_includes_and_joins_equal( + Member.where('categories.id' => categories(:technology).id), + [members(:blarpy_winkup)], :club_category + ) + end + + def test_distinct_has_many_through_a_has_many_through_association_on_source_reflection + author = authors(:david) + assert_equal [tags(:general)], author.distinct_tags + end + + def test_distinct_has_many_through_a_has_many_through_association_on_through_reflection + author = authors(:david) + assert_equal [subscribers(:first), subscribers(:second)], + author.distinct_subscribers.order('subscribers.nick') + end + + def test_nested_has_many_through_with_a_table_referenced_multiple_times + author = authors(:bob) + assert_equal [posts(:misc_by_bob), posts(:misc_by_mary), posts(:other_by_bob), posts(:other_by_mary)], + author.similar_posts.sort_by(&:id) + + # Mary and Bob both have posts in misc, but they are the only ones. + authors = Author.joins(:similar_posts).where('posts.id' => posts(:misc_by_bob).id) + assert_equal [authors(:mary), authors(:bob)], authors.uniq.sort_by(&:id) + + # Check the polymorphism of taggings is being observed correctly (in both joins) + authors = Author.joins(:similar_posts).where('taggings.taggable_type' => 'FakeModel') + assert authors.empty? + authors = Author.joins(:similar_posts).where('taggings_authors_join.taggable_type' => 'FakeModel') + assert authors.empty? + end + + def test_has_many_through_with_foreign_key_option_on_through_reflection + assert_equal [posts(:welcome), posts(:authorless)], people(:david).agents_posts.order('posts.id') + assert_equal [authors(:david)], references(:david_unicyclist).agents_posts_authors + + references = Reference.joins(:agents_posts_authors).where('authors.id' => authors(:david).id) + assert_equal [references(:david_unicyclist)], references + end + + def test_has_many_through_with_foreign_key_option_on_source_reflection + assert_equal [people(:michael), people(:susan)], jobs(:unicyclist).agents.order('people.id') + + jobs = Job.joins(:agents) + assert_equal [jobs(:unicyclist), jobs(:unicyclist)], jobs + end + + def test_has_many_through_with_sti_on_through_reflection + ratings = posts(:sti_comments).special_comments_ratings.sort_by(&:id) + assert_equal [ratings(:special_comment_rating), ratings(:sub_special_comment_rating)], ratings + + # Ensure STI is respected in the join + scope = Post.joins(:special_comments_ratings).where(:id => posts(:sti_comments).id) + assert scope.where("comments.type" => "Comment").empty? + assert !scope.where("comments.type" => "SpecialComment").empty? + assert !scope.where("comments.type" => "SubSpecialComment").empty? + end + + def test_nested_has_many_through_writers_should_raise_error + david = authors(:david) + subscriber = subscribers(:first) + + assert_raises(ActiveRecord::HasManyThroughNestedAssociationsAreReadonly) do + david.subscribers = [subscriber] + end + + assert_raises(ActiveRecord::HasManyThroughNestedAssociationsAreReadonly) do + david.subscriber_ids = [subscriber.id] + end + + assert_raises(ActiveRecord::HasManyThroughNestedAssociationsAreReadonly) do + david.subscribers << subscriber + end + + assert_raises(ActiveRecord::HasManyThroughNestedAssociationsAreReadonly) do + david.subscribers.delete(subscriber) + end + + assert_raises(ActiveRecord::HasManyThroughNestedAssociationsAreReadonly) do + david.subscribers.clear + end + + assert_raises(ActiveRecord::HasManyThroughNestedAssociationsAreReadonly) do + david.subscribers.build + end + + assert_raises(ActiveRecord::HasManyThroughNestedAssociationsAreReadonly) do + david.subscribers.create + end + end + + def test_nested_has_one_through_writers_should_raise_error + groucho = members(:groucho) + founding = member_types(:founding) + + assert_raises(ActiveRecord::HasManyThroughNestedAssociationsAreReadonly) do + groucho.nested_member_type = founding + end + end + + def test_nested_has_many_through_with_conditions_on_through_associations + assert_equal [tags(:blue)], authors(:bob).misc_post_first_blue_tags + end + + def test_nested_has_many_through_with_conditions_on_through_associations_preload + assert Author.where('tags.id' => 100).joins(:misc_post_first_blue_tags).empty? + + authors = assert_queries(3) { Author.includes(:misc_post_first_blue_tags).to_a.sort_by(&:id) } + blue = tags(:blue) + + assert_no_queries do + assert_equal [blue], authors[2].misc_post_first_blue_tags + end + end + + def test_nested_has_many_through_with_conditions_on_through_associations_preload_via_joins + # Pointless condition to force single-query loading + assert_includes_and_joins_equal( + Author.where('tags.id = tags.id'), + [authors(:bob)], :misc_post_first_blue_tags + ) + end + + def test_nested_has_many_through_with_conditions_on_source_associations + assert_equal [tags(:blue)], authors(:bob).misc_post_first_blue_tags_2 + end + + def test_nested_has_many_through_with_conditions_on_source_associations_preload + authors = assert_queries(4) { Author.includes(:misc_post_first_blue_tags_2).to_a.sort_by(&:id) } + blue = tags(:blue) + + assert_no_queries do + assert_equal [blue], authors[2].misc_post_first_blue_tags_2 + end + end + + def test_nested_has_many_through_with_conditions_on_source_associations_preload_via_joins + # Pointless condition to force single-query loading + assert_includes_and_joins_equal( + Author.where('tags.id = tags.id'), + [authors(:bob)], :misc_post_first_blue_tags_2 + ) + end + + def test_nested_has_many_through_with_foreign_key_option_on_the_source_reflection_through_reflection + assert_equal [categories(:general)], organizations(:nsa).author_essay_categories + + organizations = Organization.joins(:author_essay_categories). + where('categories.id' => categories(:general).id) + assert_equal [organizations(:nsa)], organizations + + assert_equal categories(:general), organizations(:nsa).author_owned_essay_category + + organizations = Organization.joins(:author_owned_essay_category). + where('categories.id' => categories(:general).id) + assert_equal [organizations(:nsa)], organizations + end + + private + + def assert_includes_and_joins_equal(query, expected, association) + actual = assert_queries(1) { query.joins(association).to_a.uniq } + assert_equal expected, actual + + actual = assert_queries(1) { query.includes(association).to_a.uniq } + assert_equal expected, actual + end +end diff --git a/activerecord/test/cases/batches_test.rb b/activerecord/test/cases/batches_test.rb index 6f65fb96d1..dc0e0da4c5 100644 --- a/activerecord/test/cases/batches_test.rb +++ b/activerecord/test/cases/batches_test.rb @@ -25,7 +25,7 @@ class EachTest < ActiveRecord::TestCase end def test_each_should_execute_if_id_is_in_select - assert_queries(4) do + assert_queries(6) do Post.find_each(:select => "id, title, type", :batch_size => 2) do |post| assert_kind_of Post, post end diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index c35590b84b..2e620d8b03 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -124,11 +124,13 @@ class FinderTest < ActiveRecord::TestCase def test_find_all_with_limit_and_offset_and_multiple_order_clauses first_three_posts = Post.find :all, :order => 'author_id, id', :limit => 3, :offset => 0 second_three_posts = Post.find :all, :order => ' author_id,id ', :limit => 3, :offset => 3 - last_posts = Post.find :all, :order => ' author_id, id ', :limit => 3, :offset => 6 + third_three_posts = Post.find :all, :order => ' author_id, id ', :limit => 3, :offset => 6 + last_posts = Post.find :all, :order => ' author_id, id ', :limit => 3, :offset => 9 assert_equal [[0,3],[1,1],[1,2]], first_three_posts.map { |p| [p.author_id, p.id] } assert_equal [[1,4],[1,5],[1,6]], second_three_posts.map { |p| [p.author_id, p.id] } - assert_equal [[2,7]], last_posts.map { |p| [p.author_id, p.id] } + assert_equal [[2,7],[2,9],[2,11]], third_three_posts.map { |p| [p.author_id, p.id] } + assert_equal [[3,8],[3,10]], last_posts.map { |p| [p.author_id, p.id] } end diff --git a/activerecord/test/cases/json_serialization_test.rb b/activerecord/test/cases/json_serialization_test.rb index 5da7f9e1b9..8664d63e8f 100644 --- a/activerecord/test/cases/json_serialization_test.rb +++ b/activerecord/test/cases/json_serialization_test.rb @@ -181,7 +181,11 @@ class DatabaseConnectedJsonEncodingTest < ActiveRecord::TestCase def test_should_allow_except_option_for_list_of_authors ActiveRecord::Base.include_root_in_json = false authors = [@david, @mary] - assert_equal %([{"id":1},{"id":2}]), ActiveSupport::JSON.encode(authors, :except => [:name, :author_address_id, :author_address_extra_id]) + encoded = ActiveSupport::JSON.encode(authors, :except => [ + :name, :author_address_id, :author_address_extra_id, + :organization_id, :owned_essay_id + ]) + assert_equal %([{"id":1},{"id":2}]), encoded ensure ActiveRecord::Base.include_root_in_json = true end @@ -196,7 +200,7 @@ class DatabaseConnectedJsonEncodingTest < ActiveRecord::TestCase ) ['"name":"David"', '"posts":[', '{"id":1}', '{"id":2}', '{"id":4}', - '{"id":5}', '{"id":6}', '"name":"Mary"', '"posts":[{"id":7}]'].each do |fragment| + '{"id":5}', '{"id":6}', '"name":"Mary"', '"posts":[', '{"id":7}', '{"id":9}'].each do |fragment| assert json.include?(fragment), json end end diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb index eb580928ba..9529ae56b8 100644 --- a/activerecord/test/cases/reflection_test.rb +++ b/activerecord/test/cases/reflection_test.rb @@ -7,9 +7,16 @@ require 'models/subscriber' require 'models/ship' require 'models/pirate' require 'models/price_estimate' -require 'models/tagging' +require 'models/essay' require 'models/author' +require 'models/organization' require 'models/post' +require 'models/tagging' +require 'models/category' +require 'models/book' +require 'models/subscriber' +require 'models/subscription' +require 'models/tag' require 'models/sponsor' class ReflectionTest < ActiveRecord::TestCase @@ -195,10 +202,54 @@ class ReflectionTest < ActiveRecord::TestCase assert_kind_of ThroughReflection, Subscriber.reflect_on_association(:books) end + def test_through_reflection_chain + expected = [ + Author.reflect_on_association(:essay_categories), + Author.reflect_on_association(:essays), + Organization.reflect_on_association(:authors) + ] + actual = Organization.reflect_on_association(:author_essay_categories).through_reflection_chain + + assert_equal expected, actual + end + + def test_through_conditions + expected = [ + ["tags.name = 'Blue'"], + ["taggings.comment = 'first'"], + ["posts.title LIKE 'misc post%'"] + ] + actual = Author.reflect_on_association(:misc_post_first_blue_tags).through_conditions + assert_equal expected, actual + + expected = [ + ["tags.name = 'Blue'", "taggings.comment = 'first'", "posts.title LIKE 'misc post%'"], + [], + [] + ] + actual = Author.reflect_on_association(:misc_post_first_blue_tags_2).through_conditions + assert_equal expected, actual + end + + def test_nested? + assert !Author.reflect_on_association(:comments).nested? + assert Author.reflect_on_association(:tags).nested? + + # Only goes :through once, but the through_reflection is a has_and_belongs_to_many, so this is + # a nested through association + assert Category.reflect_on_association(:post_comments).nested? + end + def test_association_primary_key - assert_equal "id", Author.reflect_on_association(:posts).association_primary_key.to_s + # Normal association + assert_equal "id", Author.reflect_on_association(:posts).association_primary_key.to_s assert_equal "name", Author.reflect_on_association(:essay).association_primary_key.to_s - assert_equal "id", Tagging.reflect_on_association(:taggable).association_primary_key.to_s + assert_equal "id", Tagging.reflect_on_association(:taggable).association_primary_key.to_s + + # Through association (uses the :primary_key option from the source reflection) + assert_equal "nick", Author.reflect_on_association(:subscribers).association_primary_key.to_s + assert_equal "name", Author.reflect_on_association(:essay_category).association_primary_key.to_s + assert_equal "custom_primary_key", Author.reflect_on_association(:tags_with_primary_key).association_primary_key.to_s # nested end def test_active_record_primary_key diff --git a/activerecord/test/cases/relation_scoping_test.rb b/activerecord/test/cases/relation_scoping_test.rb index cda2850b02..7369aaea1d 100644 --- a/activerecord/test/cases/relation_scoping_test.rb +++ b/activerecord/test/cases/relation_scoping_test.rb @@ -335,7 +335,7 @@ class DefaultScopingTest < ActiveRecord::TestCase end records = klass.all - assert_equal 1, records.length + assert_equal 3, records.length assert_equal 2, records.first.author_id end diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index 37bbb17e74..54537f11a8 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -165,7 +165,7 @@ class RelationTest < ActiveRecord::TestCase def test_finding_with_complex_order tags = Tag.includes(:taggings).order("REPLACE('abc', taggings.taggable_type, taggings.taggable_type)").to_a - assert_equal 2, tags.length + assert_equal 3, tags.length end def test_finding_with_order_limit_and_offset @@ -281,7 +281,7 @@ class RelationTest < ActiveRecord::TestCase def test_find_with_preloaded_associations assert_queries(2) do - posts = Post.preload(:comments) + posts = Post.preload(:comments).order('posts.id') assert posts.first.comments.first end @@ -291,7 +291,7 @@ class RelationTest < ActiveRecord::TestCase end assert_queries(2) do - posts = Post.preload(:author) + posts = Post.preload(:author).order('posts.id') assert posts.first.author end @@ -309,7 +309,7 @@ class RelationTest < ActiveRecord::TestCase def test_find_with_included_associations assert_queries(2) do - posts = Post.includes(:comments) + posts = Post.includes(:comments).order('posts.id') assert posts.first.comments.first end @@ -319,7 +319,7 @@ class RelationTest < ActiveRecord::TestCase end assert_queries(2) do - posts = Post.includes(:author) + posts = Post.includes(:author).order('posts.id') assert posts.first.author end @@ -538,7 +538,7 @@ class RelationTest < ActiveRecord::TestCase def test_last authors = Author.scoped - assert_equal authors(:mary), authors.last + assert_equal authors(:bob), authors.last end def test_destroy_all @@ -618,22 +618,22 @@ class RelationTest < ActiveRecord::TestCase def test_count posts = Post.scoped - assert_equal 7, posts.count - assert_equal 7, posts.count(:all) - assert_equal 7, posts.count(:id) + assert_equal 11, posts.count + assert_equal 11, posts.count(:all) + assert_equal 11, posts.count(:id) assert_equal 1, posts.where('comments_count > 1').count - assert_equal 5, posts.where(:comments_count => 0).count + assert_equal 9, posts.where(:comments_count => 0).count end def test_count_with_distinct posts = Post.scoped assert_equal 3, posts.count(:comments_count, :distinct => true) - assert_equal 7, posts.count(:comments_count, :distinct => false) + assert_equal 11, posts.count(:comments_count, :distinct => false) assert_equal 3, posts.select(:comments_count).count(:distinct => true) - assert_equal 7, posts.select(:comments_count).count(:distinct => false) + assert_equal 11, posts.select(:comments_count).count(:distinct => false) end def test_count_explicit_columns @@ -643,7 +643,7 @@ class RelationTest < ActiveRecord::TestCase assert_equal [0], posts.select('comments_count').where('id is not null').group('id').order('id').count.values.uniq assert_equal 0, posts.where('id is not null').select('comments_count').count - assert_equal 7, posts.select('comments_count').count('id') + assert_equal 11, posts.select('comments_count').count('id') assert_equal 0, posts.select('comments_count').count assert_equal 0, posts.count(:comments_count) assert_equal 0, posts.count('comments_count') @@ -658,12 +658,12 @@ class RelationTest < ActiveRecord::TestCase def test_size posts = Post.scoped - assert_queries(1) { assert_equal 7, posts.size } + assert_queries(1) { assert_equal 11, posts.size } assert ! posts.loaded? best_posts = posts.where(:comments_count => 0) best_posts.to_a # force load - assert_no_queries { assert_equal 5, best_posts.size } + assert_no_queries { assert_equal 9, best_posts.size } end def test_count_complex_chained_relations diff --git a/activerecord/test/fixtures/authors.yml b/activerecord/test/fixtures/authors.yml index de2ec7d38b..832236a486 100644 --- a/activerecord/test/fixtures/authors.yml +++ b/activerecord/test/fixtures/authors.yml @@ -3,7 +3,13 @@ david: name: David author_address_id: 1 author_address_extra_id: 2 + organization_id: No Such Agency + owned_essay_id: A Modest Proposal mary: id: 2 name: Mary + +bob: + id: 3 + name: Bob diff --git a/activerecord/test/fixtures/books.yml b/activerecord/test/fixtures/books.yml index 473663ff5b..fb48645456 100644 --- a/activerecord/test/fixtures/books.yml +++ b/activerecord/test/fixtures/books.yml @@ -1,7 +1,9 @@ awdr: + author_id: 1 id: 1 name: "Agile Web Development with Rails" rfr: + author_id: 1 id: 2 name: "Ruby for Rails" diff --git a/activerecord/test/fixtures/categories.yml b/activerecord/test/fixtures/categories.yml index b0770a093d..3e75e733a6 100644 --- a/activerecord/test/fixtures/categories.yml +++ b/activerecord/test/fixtures/categories.yml @@ -12,3 +12,8 @@ sti_test: id: 3 name: Special category type: SpecialCategory + +cooking: + id: 4 + name: Cooking + type: Category diff --git a/activerecord/test/fixtures/categories_posts.yml b/activerecord/test/fixtures/categories_posts.yml index 9b67ab4fa4..c6f0d885f5 100644 --- a/activerecord/test/fixtures/categories_posts.yml +++ b/activerecord/test/fixtures/categories_posts.yml @@ -21,3 +21,11 @@ sti_test_sti_habtm: general_hello: category_id: 1 post_id: 4 + +general_misc_by_bob: + category_id: 1 + post_id: 8 + +cooking_misc_by_bob: + category_id: 4 + post_id: 8 diff --git a/activerecord/test/fixtures/categorizations.yml b/activerecord/test/fixtures/categorizations.yml index c5b6fc9a51..62e5bd111a 100644 --- a/activerecord/test/fixtures/categorizations.yml +++ b/activerecord/test/fixtures/categorizations.yml @@ -15,3 +15,9 @@ mary_thinking_general: author_id: 2 post_id: 2 category_id: 1 + +bob_misc_by_bob_technology: + id: 4 + author_id: 3 + post_id: 8 + category_id: 2 diff --git a/activerecord/test/fixtures/clubs.yml b/activerecord/test/fixtures/clubs.yml index 1986d28229..82e439e8e5 100644 --- a/activerecord/test/fixtures/clubs.yml +++ b/activerecord/test/fixtures/clubs.yml @@ -1,6 +1,8 @@ boring_club: name: Banana appreciation society + category_id: 1 moustache_club: name: Moustache and Eyebrow Fancier Club crazy_club: - name: Skull and bones
\ No newline at end of file + name: Skull and bones + category_id: 2 diff --git a/activerecord/test/fixtures/essays.yml b/activerecord/test/fixtures/essays.yml new file mode 100644 index 0000000000..9d15d82359 --- /dev/null +++ b/activerecord/test/fixtures/essays.yml @@ -0,0 +1,6 @@ +david_modest_proposal: + name: A Modest Proposal + writer_type: Author + writer_id: David + category_id: General + author_id: David diff --git a/activerecord/test/fixtures/member_details.yml b/activerecord/test/fixtures/member_details.yml new file mode 100644 index 0000000000..e1fe695a9b --- /dev/null +++ b/activerecord/test/fixtures/member_details.yml @@ -0,0 +1,8 @@ +groucho: + id: 1 + member_id: 1 + organization: nsa +some_other_guy: + id: 2 + member_id: 2 + organization: nsa diff --git a/activerecord/test/fixtures/members.yml b/activerecord/test/fixtures/members.yml index 824840b7e5..f3bbf0dac6 100644 --- a/activerecord/test/fixtures/members.yml +++ b/activerecord/test/fixtures/members.yml @@ -6,3 +6,6 @@ some_other_guy: id: 2 name: Englebert Humperdink member_type_id: 2 +blarpy_winkup: + id: 3 + name: Blarpy Winkup diff --git a/activerecord/test/fixtures/memberships.yml b/activerecord/test/fixtures/memberships.yml index eed8b22af8..60eb641054 100644 --- a/activerecord/test/fixtures/memberships.yml +++ b/activerecord/test/fixtures/memberships.yml @@ -18,3 +18,10 @@ other_guys_membership: member_id: 2 favourite: false type: CurrentMembership + +blarpy_winkup_crazy_club: + joined_on: <%= 4.weeks.ago.to_s(:db) %> + club: crazy_club + member_id: 3 + favourite: false + type: CurrentMembership diff --git a/activerecord/test/fixtures/owners.yml b/activerecord/test/fixtures/owners.yml index d5493a84b7..2d21ce433c 100644 --- a/activerecord/test/fixtures/owners.yml +++ b/activerecord/test/fixtures/owners.yml @@ -1,6 +1,7 @@ blackbeard: owner_id: 1 name: blackbeard + essay_id: A Modest Proposal ashley: owner_id: 2 diff --git a/activerecord/test/fixtures/posts.yml b/activerecord/test/fixtures/posts.yml index 07069a064f..7298096025 100644 --- a/activerecord/test/fixtures/posts.yml +++ b/activerecord/test/fixtures/posts.yml @@ -52,3 +52,31 @@ eager_other: title: eager loading with OR'd conditions body: hello type: Post + +misc_by_bob: + id: 8 + author_id: 3 + title: misc post by bob + body: hello + type: Post + +misc_by_mary: + id: 9 + author_id: 2 + title: misc post by mary + body: hello + type: Post + +other_by_bob: + id: 10 + author_id: 3 + title: other post by bob + body: hello + type: Post + +other_by_mary: + id: 11 + author_id: 2 + title: other post by mary + body: hello + type: Post diff --git a/activerecord/test/fixtures/ratings.yml b/activerecord/test/fixtures/ratings.yml new file mode 100644 index 0000000000..34e208efa3 --- /dev/null +++ b/activerecord/test/fixtures/ratings.yml @@ -0,0 +1,14 @@ +normal_comment_rating: + id: 1 + comment_id: 8 + value: 1 + +special_comment_rating: + id: 2 + comment_id: 6 + value: 1 + +sub_special_comment_rating: + id: 3 + comment_id: 12 + value: 1 diff --git a/activerecord/test/fixtures/taggings.yml b/activerecord/test/fixtures/taggings.yml index 3db6a4c079..a337cce019 100644 --- a/activerecord/test/fixtures/taggings.yml +++ b/activerecord/test/fixtures/taggings.yml @@ -26,3 +26,43 @@ godfather: orphaned: id: 5 tag_id: 1 + +misc_post_by_bob: + id: 6 + tag_id: 2 + taggable_id: 8 + taggable_type: Post + +misc_post_by_mary: + id: 7 + tag_id: 2 + taggable_id: 9 + taggable_type: Post + +misc_by_bob_blue_first: + id: 8 + tag_id: 3 + taggable_id: 8 + taggable_type: Post + comment: first + +misc_by_bob_blue_second: + id: 9 + tag_id: 3 + taggable_id: 8 + taggable_type: Post + comment: second + +other_by_bob_blue: + id: 10 + tag_id: 3 + taggable_id: 10 + taggable_type: Post + comment: first + +other_by_mary_blue: + id: 11 + tag_id: 3 + taggable_id: 11 + taggable_type: Post + comment: first diff --git a/activerecord/test/fixtures/tags.yml b/activerecord/test/fixtures/tags.yml index 7610fd38b9..d4b7c9a4d5 100644 --- a/activerecord/test/fixtures/tags.yml +++ b/activerecord/test/fixtures/tags.yml @@ -4,4 +4,8 @@ general: misc: id: 2 - name: Misc
\ No newline at end of file + name: Misc + +blue: + id: 3 + name: Blue diff --git a/activerecord/test/models/author.rb b/activerecord/test/models/author.rb index 83a6f5d926..f362f70dec 100644 --- a/activerecord/test/models/author.rb +++ b/activerecord/test/models/author.rb @@ -94,16 +94,48 @@ class Author < ActiveRecord::Base has_many :author_favorites has_many :favorite_authors, :through => :author_favorites, :order => 'name' - has_many :tagging, :through => :posts # through polymorphic has_one - has_many :taggings, :through => :posts, :source => :taggings # through polymorphic has_many - has_many :tags, :through => :posts # through has_many :through + has_many :tagging, :through => :posts + has_many :taggings, :through => :posts + has_many :tags, :through => :posts + has_many :similar_posts, :through => :tags, :source => :tagged_posts, :uniq => true + has_many :distinct_tags, :through => :posts, :source => :tags, :select => "DISTINCT tags.*", :order => "tags.name" has_many :post_categories, :through => :posts, :source => :categories + has_many :tagging_tags, :through => :taggings, :source => :tag + has_many :tags_with_primary_key, :through => :posts + + has_many :books + has_many :subscriptions, :through => :books + has_many :subscribers, :through => :subscriptions, :order => "subscribers.nick" # through has_many :through (on through reflection) + has_many :distinct_subscribers, :through => :subscriptions, :source => :subscriber, :select => "DISTINCT subscribers.*", :order => "subscribers.nick" has_one :essay, :primary_key => :name, :as => :writer + has_one :essay_category, :through => :essay, :source => :category + has_one :essay_owner, :through => :essay, :source => :owner + + has_one :essay_2, :primary_key => :name, :class_name => 'Essay', :foreign_key => :author_id + has_one :essay_category_2, :through => :essay_2, :source => :category + + has_many :essays, :primary_key => :name, :as => :writer + has_many :essay_categories, :through => :essays, :source => :category + has_many :essay_owners, :through => :essays, :source => :owner + + has_many :essays_2, :primary_key => :name, :class_name => 'Essay', :foreign_key => :author_id + has_many :essay_categories_2, :through => :essays_2, :source => :category + + belongs_to :owned_essay, :primary_key => :name, :class_name => 'Essay' + has_one :owned_essay_category, :through => :owned_essay, :source => :category - belongs_to :author_address, :dependent => :destroy + belongs_to :author_address, :dependent => :destroy belongs_to :author_address_extra, :dependent => :delete, :class_name => "AuthorAddress" + has_many :post_categories, :through => :posts, :source => :categories + has_many :category_post_comments, :through => :categories, :source => :post_comments + + has_many :misc_posts, :class_name => 'Post', :conditions => "posts.title LIKE 'misc post%'" + has_many :misc_post_first_blue_tags, :through => :misc_posts, :source => :first_blue_tags + + has_many :misc_post_first_blue_tags_2, :through => :posts, :source => :first_blue_tags_2, :conditions => "posts.title LIKE 'misc post%'" + scope :relation_include_posts, includes(:posts) scope :relation_include_tags, includes(:tags) diff --git a/activerecord/test/models/book.rb b/activerecord/test/models/book.rb index 1e030b4f59..d27d0af77c 100644 --- a/activerecord/test/models/book.rb +++ b/activerecord/test/models/book.rb @@ -1,4 +1,6 @@ class Book < ActiveRecord::Base + has_many :authors + has_many :citations, :foreign_key => 'book1_id' has_many :references, :through => :citations, :source => :reference_of, :uniq => true diff --git a/activerecord/test/models/categorization.rb b/activerecord/test/models/categorization.rb index 45f50e4af3..09489b8ea4 100644 --- a/activerecord/test/models/categorization.rb +++ b/activerecord/test/models/categorization.rb @@ -4,6 +4,8 @@ class Categorization < ActiveRecord::Base belongs_to :named_category, :class_name => 'Category', :foreign_key => :named_category_name, :primary_key => :name belongs_to :author + has_many :post_taggings, :through => :author, :source => :taggings + belongs_to :author_using_custom_pk, :class_name => 'Author', :foreign_key => :author_id, :primary_key => :author_address_extra_id has_many :authors_using_custom_pk, :class_name => 'Author', :foreign_key => :id, :primary_key => :category_id end diff --git a/activerecord/test/models/category.rb b/activerecord/test/models/category.rb index 8f37433ec6..02b85fd38a 100644 --- a/activerecord/test/models/category.rb +++ b/activerecord/test/models/category.rb @@ -22,6 +22,8 @@ class Category < ActiveRecord::Base end has_many :categorizations + has_many :post_comments, :through => :posts, :source => :comments + has_many :authors, :through => :categorizations has_many :authors_with_select, :through => :categorizations, :source => :author, :select => 'authors.*, categorizations.post_id' diff --git a/activerecord/test/models/club.rb b/activerecord/test/models/club.rb index c432a6ace8..24a65b0f2f 100644 --- a/activerecord/test/models/club.rb +++ b/activerecord/test/models/club.rb @@ -5,6 +5,7 @@ class Club < ActiveRecord::Base has_many :current_memberships has_one :sponsor has_one :sponsored_member, :through => :sponsor, :source => :sponsorable, :source_type => "Member" + belongs_to :category private diff --git a/activerecord/test/models/comment.rb b/activerecord/test/models/comment.rb index ff533717cc..2a4c37089a 100644 --- a/activerecord/test/models/comment.rb +++ b/activerecord/test/models/comment.rb @@ -8,6 +8,7 @@ class Comment < ActiveRecord::Base :conditions => { "posts.author_id" => 1 } belongs_to :post, :counter_cache => true + has_many :ratings def self.what_are_you 'a comment...' diff --git a/activerecord/test/models/essay.rb b/activerecord/test/models/essay.rb index 6c28f5e49b..ec4b982b5b 100644 --- a/activerecord/test/models/essay.rb +++ b/activerecord/test/models/essay.rb @@ -1,3 +1,5 @@ class Essay < ActiveRecord::Base belongs_to :writer, :primary_key => :name, :polymorphic => true + belongs_to :category, :primary_key => :name + has_one :owner, :primary_key => :name end diff --git a/activerecord/test/models/job.rb b/activerecord/test/models/job.rb index 3333a02e27..f7b0e787b1 100644 --- a/activerecord/test/models/job.rb +++ b/activerecord/test/models/job.rb @@ -2,4 +2,6 @@ class Job < ActiveRecord::Base has_many :references has_many :people, :through => :references belongs_to :ideal_reference, :class_name => 'Reference' + + has_many :agents, :through => :people end diff --git a/activerecord/test/models/member.rb b/activerecord/test/models/member.rb index e6e78f9e45..991e0e051f 100644 --- a/activerecord/test/models/member.rb +++ b/activerecord/test/models/member.rb @@ -11,6 +11,17 @@ class Member < ActiveRecord::Base has_one :organization, :through => :member_detail belongs_to :member_type + has_many :nested_member_types, :through => :member_detail, :source => :member_type + has_one :nested_member_type, :through => :member_detail, :source => :member_type + + has_many :nested_sponsors, :through => :sponsor_club, :source => :sponsor + has_one :nested_sponsor, :through => :sponsor_club, :source => :sponsor + + has_many :organization_member_details, :through => :member_detail + has_many :organization_member_details_2, :through => :organization, :source => :member_details + + has_one :club_category, :through => :club, :source => :category + has_many :current_memberships has_one :club_through_many, :through => :current_memberships, :source => :club diff --git a/activerecord/test/models/member_detail.rb b/activerecord/test/models/member_detail.rb index 94f59e5794..fe619f8732 100644 --- a/activerecord/test/models/member_detail.rb +++ b/activerecord/test/models/member_detail.rb @@ -2,4 +2,6 @@ class MemberDetail < ActiveRecord::Base belongs_to :member belongs_to :organization has_one :member_type, :through => :member + + has_many :organization_member_details, :through => :organization, :source => :member_details end diff --git a/activerecord/test/models/organization.rb b/activerecord/test/models/organization.rb index 1da342a0bd..4a4111833f 100644 --- a/activerecord/test/models/organization.rb +++ b/activerecord/test/models/organization.rb @@ -2,5 +2,11 @@ class Organization < ActiveRecord::Base has_many :member_details has_many :members, :through => :member_details + has_many :authors, :primary_key => :name + has_many :author_essay_categories, :through => :authors, :source => :essay_categories + + has_one :author, :primary_key => :name + has_one :author_owned_essay_category, :through => :author, :source => :owned_essay_category + scope :clubs, { :from => 'clubs' } -end
\ No newline at end of file +end diff --git a/activerecord/test/models/person.rb b/activerecord/test/models/person.rb index cc3a4f5f9d..ad59d12672 100644 --- a/activerecord/test/models/person.rb +++ b/activerecord/test/models/person.rb @@ -21,6 +21,9 @@ class Person < ActiveRecord::Base has_many :agents_of_agents, :through => :agents, :source => :agents belongs_to :number1_fan, :class_name => 'Person' + has_many :agents_posts, :through => :agents, :source => :posts + has_many :agents_posts_authors, :through => :agents_posts, :source => :author + scope :males, :conditions => { :gender => 'M' } scope :females, :conditions => { :gender => 'F' } end diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb index a342aaf60b..b39325f949 100644 --- a/activerecord/test/models/post.rb +++ b/activerecord/test/models/post.rb @@ -49,6 +49,8 @@ class Post < ActiveRecord::Base has_many :special_comments has_many :nonexistant_comments, :class_name => 'Comment', :conditions => 'comments.id < 0' + has_many :special_comments_ratings, :through => :special_comments, :source => :ratings + has_and_belongs_to_many :categories has_and_belongs_to_many :special_categories, :join_table => "categories_posts", :association_foreign_key => 'category_id' @@ -73,8 +75,14 @@ class Post < ActiveRecord::Base has_many :misc_tags, :through => :taggings, :source => :tag, :conditions => "tags.name = 'Misc'" has_many :funky_tags, :through => :taggings, :source => :tag has_many :super_tags, :through => :taggings + has_many :tags_with_primary_key, :through => :taggings, :source => :tag_with_primary_key has_one :tagging, :as => :taggable + has_many :first_taggings, :as => :taggable, :class_name => 'Tagging', :conditions => "taggings.comment = 'first'" + has_many :first_blue_tags, :through => :first_taggings, :source => :tag, :conditions => "tags.name = 'Blue'" + + has_many :first_blue_tags_2, :through => :taggings, :source => :blue_tag, :conditions => "taggings.comment = 'first'" + has_many :invalid_taggings, :as => :taggable, :class_name => "Tagging", :conditions => 'taggings.id < 0' has_many :invalid_tags, :through => :invalid_taggings, :source => :tag diff --git a/activerecord/test/models/rating.rb b/activerecord/test/models/rating.rb new file mode 100644 index 0000000000..12c4b5affa --- /dev/null +++ b/activerecord/test/models/rating.rb @@ -0,0 +1,3 @@ +class Rating < ActiveRecord::Base + belongs_to :comment +end diff --git a/activerecord/test/models/reference.rb b/activerecord/test/models/reference.rb index 06c4f79ef3..e33a0f2acc 100644 --- a/activerecord/test/models/reference.rb +++ b/activerecord/test/models/reference.rb @@ -2,6 +2,8 @@ class Reference < ActiveRecord::Base belongs_to :person belongs_to :job + has_many :agents_posts_authors, :through => :person + class << self attr_accessor :make_comments end diff --git a/activerecord/test/models/tagging.rb b/activerecord/test/models/tagging.rb index 231d2b5890..17e627a60e 100644 --- a/activerecord/test/models/tagging.rb +++ b/activerecord/test/models/tagging.rb @@ -6,6 +6,8 @@ class Tagging < ActiveRecord::Base belongs_to :tag, :include => :tagging belongs_to :super_tag, :class_name => 'Tag', :foreign_key => 'super_tag_id' belongs_to :invalid_tag, :class_name => 'Tag', :foreign_key => 'tag_id' + belongs_to :blue_tag, :class_name => 'Tag', :foreign_key => :tag_id, :conditions => "tags.name = 'Blue'" + belongs_to :tag_with_primary_key, :class_name => 'Tag', :foreign_key => :tag_id, :primary_key => :custom_primary_key belongs_to :interpolated_tag, :class_name => 'Tag', :foreign_key => :tag_id, :conditions => proc { "1 = #{1}" } belongs_to :taggable, :polymorphic => true, :counter_cache => true has_many :things, :through => :taggable diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 0b3865fc78..bdadd0698b 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -49,6 +49,8 @@ ActiveRecord::Schema.define do t.string :name, :null => false t.integer :author_address_id t.integer :author_address_extra_id + t.string :organization_id + t.string :owned_essay_id end create_table :author_addresses, :force => true do |t| @@ -75,6 +77,7 @@ ActiveRecord::Schema.define do end create_table :books, :force => true do |t| + t.integer :author_id t.column :name, :string end @@ -123,6 +126,7 @@ ActiveRecord::Schema.define do create_table :clubs, :force => true do |t| t.string :name + t.integer :category_id end create_table :collections, :force => true do |t| @@ -216,6 +220,8 @@ ActiveRecord::Schema.define do t.string :name t.string :writer_id t.string :writer_type + t.string :category_id + t.string :author_id end create_table :events, :force => true do |t| @@ -393,6 +399,7 @@ ActiveRecord::Schema.define do t.string :name t.column :updated_at, :datetime t.column :happy_at, :datetime + t.string :essay_id end create_table :paint_colors, :force => true do |t| @@ -482,6 +489,11 @@ ActiveRecord::Schema.define do t.string :type end + create_table :ratings, :force => true do |t| + t.integer :comment_id + t.integer :value + end + create_table :readers, :force => true do |t| t.integer :post_id, :null => false t.integer :person_id, :null => false @@ -553,6 +565,7 @@ ActiveRecord::Schema.define do t.column :super_tag_id, :integer t.column :taggable_type, :string t.column :taggable_id, :integer + t.string :comment end create_table :tasks, :force => true do |t| |