aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--activerecord/lib/active_record/associations.rb385
-rw-r--r--activerecord/lib/active_record/associations/alias_tracker.rb73
-rw-r--r--activerecord/lib/active_record/associations/has_many_through_association.rb1
-rw-r--r--activerecord/lib/active_record/associations/nested_has_many_through.rb158
-rw-r--r--activerecord/lib/active_record/associations/through_association_scope.rb200
-rw-r--r--activerecord/lib/active_record/reflection.rb51
-rw-r--r--activerecord/test/cases/associations/cascaded_eager_loading_test.rb18
-rw-r--r--activerecord/test/cases/associations/eager_test.rb4
-rw-r--r--activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb6
-rw-r--r--activerecord/test/cases/associations/join_model_test.rb8
-rw-r--r--activerecord/test/cases/associations/nested_has_many_through_associations_test.rb261
-rw-r--r--activerecord/test/cases/batches_test.rb2
-rw-r--r--activerecord/test/cases/finder_test.rb2
-rw-r--r--activerecord/test/cases/json_serialization_test.rb2
-rw-r--r--activerecord/test/cases/relations_test.rb20
-rw-r--r--activerecord/test/fixtures/authors.yml4
-rw-r--r--activerecord/test/fixtures/books.yml2
-rw-r--r--activerecord/test/fixtures/categories.yml5
-rw-r--r--activerecord/test/fixtures/categories_posts.yml8
-rw-r--r--activerecord/test/fixtures/member_details.yml8
-rw-r--r--activerecord/test/fixtures/members.yml2
-rw-r--r--activerecord/test/fixtures/memberships.yml6
-rw-r--r--activerecord/test/fixtures/posts.yml14
-rw-r--r--activerecord/test/fixtures/ratings.yml14
-rw-r--r--activerecord/test/fixtures/sponsors.yml9
-rw-r--r--activerecord/test/fixtures/taggings.yml12
-rw-r--r--activerecord/test/fixtures/tags.yml2
-rw-r--r--activerecord/test/models/author.rb17
-rw-r--r--activerecord/test/models/book.rb2
-rw-r--r--activerecord/test/models/category.rb2
-rw-r--r--activerecord/test/models/comment.rb1
-rw-r--r--activerecord/test/models/job.rb2
-rw-r--r--activerecord/test/models/member.rb11
-rw-r--r--activerecord/test/models/member_detail.rb2
-rw-r--r--activerecord/test/models/person.rb3
-rw-r--r--activerecord/test/models/post.rb2
-rw-r--r--activerecord/test/models/rating.rb3
-rw-r--r--activerecord/test/models/reference.rb2
-rw-r--r--activerecord/test/schema/schema.rb6
39 files changed, 1049 insertions, 281 deletions
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb
index affa2fbcaf..2a72fa95c9 100644
--- a/activerecord/lib/active_record/associations.rb
+++ b/activerecord/lib/active_record/associations.rb
@@ -111,8 +111,10 @@ module ActiveRecord
autoload :HasAndBelongsToManyAssociation, 'active_record/associations/has_and_belongs_to_many_association'
autoload :HasManyAssociation, 'active_record/associations/has_many_association'
autoload :HasManyThroughAssociation, 'active_record/associations/has_many_through_association'
+ autoload :NestedHasManyThroughAssociation, 'active_record/associations/nested_has_many_through_association'
autoload :HasOneAssociation, 'active_record/associations/has_one_association'
autoload :HasOneThroughAssociation, 'active_record/associations/has_one_through_association'
+ autoload :AliasTracker, 'active_record/associations/alias_tracker'
# Clears out the association cache.
def clear_association_cache #:nodoc:
@@ -1833,7 +1835,7 @@ module ActiveRecord
end
class JoinDependency # :nodoc:
- attr_reader :join_parts, :reflections, :table_aliases
+ attr_reader :join_parts, :reflections, :alias_tracker
def initialize(base, associations, joins)
@join_parts = [JoinBase.new(base, joins)]
@@ -1841,8 +1843,8 @@ module ActiveRecord
@reflections = []
@base_records_hash = {}
@base_records_in_order = []
- @table_aliases = Hash.new(0)
- @table_aliases[base.table_name] = 1
+ @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
@@ -1862,17 +1864,6 @@ module ActiveRecord
join_parts.first
end
- def count_aliases_from_table_joins(name)
- # quoted_name should be downcased as some database adapters (Oracle) return quoted name in uppercase
- quoted_name = join_base.active_record.connection.quote_table_name(name.downcase).downcase
- join_sql = join_base.table_joins.to_s.downcase
- join_sql.blank? ? 0 :
- # Table names
- join_sql.scan(/join(?:\s+\w+)?\s+#{quoted_name}\son/).size +
- # Table aliases
- join_sql.scan(/join(?:\s+\w+)?\s+\S+\s+#{quoted_name}\son/).size
- end
-
def instantiate(rows)
rows.each_with_index do |row, i|
primary_id = join_base.record_id(row)
@@ -2125,11 +2116,11 @@ module ActiveRecord
# What type of join will be generated, either Arel::InnerJoin (default) or Arel::OuterJoin
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
+ delegate :options, :through_reflection, :source_reflection, :through_reflection_chain, :to => :reflection
delegate :table, :table_name, :to => :parent, :prefix => true
+ delegate :alias_tracker, :to => :join_dependency
def initialize(reflection, join_dependency, parent = nil)
reflection.check_validity!
@@ -2140,15 +2131,13 @@ module ActiveRecord
super(reflection.klass)
- @reflection = reflection
- @join_dependency = join_dependency
- @parent = parent
- @join_type = Arel::InnerJoin
+ @reflection = reflection
+ @join_dependency = join_dependency
+ @parent = parent
+ @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
+ setup_tables
end
def ==(other)
@@ -2164,7 +2153,91 @@ 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
+
+ chain.zip(@tables).each do |reflection, table|
+ conditions = []
+
+ if reflection.source_reflection.nil?
+ case reflection.macro
+ when :belongs_to
+ key = reflection.options[:primary_key] ||
+ reflection.klass.primary_key
+ foreign_key = reflection.primary_key_name
+ when :has_many, :has_one
+ key = reflection.primary_key_name
+ foreign_key = reflection.options[:primary_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
+
+ # TODO: Can join_key just be reflection.primary_key_name ?
+ join_key = reflection.options[:foreign_key] ||
+ reflection.active_record.to_s.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
+
+ # TODO: Can foreign_key be reflection.association_foreign_key?
+ key = reflection.klass.primary_key
+ foreign_key = reflection.options[:association_foreign_key] ||
+ reflection.klass.to_s.foreign_key
+ end
+ else
+ case reflection.source_reflection.macro
+ when :belongs_to
+ key = reflection.klass.primary_key
+ foreign_key = reflection.source_reflection.primary_key_name
+
+ conditions << source_type_conditions(reflection, foreign_table)
+ when :has_many, :has_one
+ key = reflection.source_reflection.primary_key_name
+ foreign_key = reflection.source_reflection.klass.primary_key
+ when :has_and_belongs_to_many
+ table, join_table = table
+
+ join_key = reflection.source_reflection.primary_key_name
+ join_foreign_key = reflection.source_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.source_reflection.association_foreign_key
+ end
+ end
+
+ conditions << table[key].eq(foreign_table[foreign_key])
+
+ conditions << reflection_conditions(reflection, table)
+ conditions << sti_conditions(reflection, table)
+
+ relation = relation.join(table, join_type).on(*conditions.compact)
+
+ # The current table in this iteration becomes the foreign table in the next
+ foreign_table = table
+ end
+
+ relation
end
def join_relation(joining_relation)
@@ -2173,213 +2246,117 @@ module ActiveRecord
end
def table
- @table ||= Arel::Table.new(
- table_name, :as => aliased_table_name,
- :engine => arel_engine, :columns => active_record.columns
- )
+ if reflection.macro == :has_and_belongs_to_many
+ @tables.last.first
+ else
+ @tables.last
+ end
+ end
+
+ def aliased_table_name
+ table.table_alias || table.name
end
- # More semantic name given we are talking about associations
- alias_method :target_table, :table
-
protected
- def aliased_table_name_for(name, suffix = nil)
- if @join_dependency.table_aliases[name].zero?
- @join_dependency.table_aliases[name] = @join_dependency.count_aliases_from_table_joins(name)
- end
-
- if !@join_dependency.table_aliases[name].zero? # We need an alias
- name = active_record.connection.table_alias_for "#{pluralize(reflection.name)}_#{parent_table_name}#{suffix}"
- @join_dependency.table_aliases[name] += 1
- if @join_dependency.table_aliases[name] == 1 # First time we've seen this name
- # Also need to count the aliases from the table_aliases to avoid incorrect count
- @join_dependency.table_aliases[name] += @join_dependency.count_aliases_from_table_joins(name)
- end
- table_index = @join_dependency.table_aliases[name]
- name = name[0..active_record.connection.table_alias_length-3] + "_#{table_index}" if table_index > 1
- else
- @join_dependency.table_aliases[name] += 1
- end
-
+ def table_alias_for(reflection, join = false)
+ name = alias_tracker.pluralize(reflection.name)
+ name << "_#{parent_table_name}"
+ name << "_join" if join
name
end
- def pluralize(table_name)
- ActiveRecord::Base.pluralize_table_names ? table_name.to_s.pluralize : table_name
- end
-
def interpolate_sql(sql)
instance_eval("%@#{sql.gsub('@', '\@')}@", __FILE__, __LINE__)
end
private
- def allocate_aliases
- @aliased_prefix = "t#{ join_dependency.join_parts.size }"
- @aliased_table_name = aliased_table_name_for(table_name)
-
- 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")
- end
- end
-
- def process_conditions(conditions, table_name)
- Arel.sql(interpolate_sql(sanitize_sql(conditions, table_name)))
- end
-
- def join_target_table(relation, *conditions)
- relation = relation.join(target_table, join_type)
-
- # 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]
+ # 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|
+ aliased_table_name = alias_tracker.aliased_name_for(
+ reflection.table_name,
+ table_alias_for(reflection, reflection != self.reflection)
+ )
- sti_condition = sti_column.eq(active_record.sti_name)
- active_record.descendants.each do |subclass|
- sti_condition = sti_condition.or(sti_column.eq(subclass.sti_name))
- end
+ table = Arel::Table.new(
+ reflection.table_name, :engine => arel_engine,
+ :as => aliased_table_name, :columns => reflection.klass.columns
+ )
- conditions << sti_condition
+ # 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_name = (reflection.source_reflection || reflection).options[:join_table]
+
+ aliased_join_table_name = alias_tracker.aliased_name_for(
+ join_table_name,
+ table_alias_for(reflection, true)
+ )
+
+ join_table = Arel::Table.new(
+ join_table_name, :engine => arel_engine,
+ :as => aliased_join_table_name
+ )
+
+ [table, join_table]
+ else
+ table
+ end
end
- # If the reflection has conditions, add them
- if options[:conditions]
- conditions << process_conditions(options[:conditions], aliased_table_name)
- 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!
- relation = relation.on(*conditions)
+ @tables
end
-
- def join_has_and_belongs_to_many_to(relation)
- join_table = Arel::Table.new(
- options[:join_table], :engine => arel_engine,
- :as => @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 reflection_conditions(reflection, table)
+ if reflection.options[:conditions]
+ Arel.sql(interpolate_sql(sanitize_sql(
+ reflection.options[:conditions],
+ table.table_alias || table.name
+ )))
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, :engine => arel_engine,
- :as => @aliased_join_table_name
- )
-
- jt_conditions = []
- jt_foreign_key = first_key = second_key = nil
-
- if through_reflection.options[:as] # has_many :through against a polymorphic join
- as_key = through_reflection.options[:as].to_s
- jt_foreign_key = as_key + '_id'
+ def sti_conditions(reflection, table)
+ unless reflection.klass.descends_from_active_record?
+ sti_column = table[reflection.klass.inheritance_column]
- jt_conditions <<
- join_table[as_key + '_type'].
- eq(parent.active_record.base_class.name)
- else
- jt_foreign_key = through_reflection.primary_key_name
- 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
+ condition = sti_column.eq(reflection.klass.sti_name)
- if reflection.options[:source_type]
- second_key = source_reflection.association_foreign_key
-
- jt_conditions <<
- join_table[reflection.source_reflection.options[:foreign_type]].
- eq(reflection.options[:source_type])
- else
- second_key = source_reflection.primary_key_name
+ reflection.klass.descendants.each do |subclass|
+ condition = condition.or(sti_column.eq(subclass.sti_name))
end
+
+ condition
end
-
- jt_conditions <<
- parent_table[parent.primary_key].
- eq(join_table[jt_foreign_key])
-
- if through_reflection.options[:conditions]
- jt_conditions << process_conditions(through_reflection.options[:conditions], aliased_table_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]),
- 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.primary_key_name
- primary_key = options[:primary_key] || reflection.klass.primary_key
-
- join_target_table(
- relation,
- target_table[primary_key].eq(parent_table[foreign_key])
- )
- end
- end
+ def source_type_conditions(reflection, foreign_table)
+ if reflection.options[:source_type]
+ foreign_table[reflection.source_reflection.options[:foreign_type]].
+ eq(reflection.options[:source_type])
+ end
+ end
+
+ def polymorphic_conditions(reflection, table)
+ if reflection.options[:as]
+ table["#{reflection.options[:as]}_type"].
+ eq(reflection.active_record.base_class.name)
+ end
+ end
end
+ end
end
end
end
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..10e90ec117
--- /dev/null
+++ b/activerecord/lib/active_record/associations/alias_tracker.rb
@@ -0,0 +1,73 @@
+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:
+ # other_sql is some other sql which might conflict with the aliases we assign here. Therefore
+ # we store other_sql so that we can scan it before assigning a specific name.
+ def initialize(other_sql = nil)
+ @aliases = Hash.new
+ @other_sql = other_sql.to_s.downcase
+ 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 @other_sql.blank?
+ # quoted_name should be downcased as some database adapters (Oracle) return quoted name in uppercase
+ quoted_name = connection.quote_table_name(name.downcase).downcase
+
+ # Table names
+ @aliases[name] += @other_sql.scan(/join(?:\s+\w+)?\s+#{quoted_name}\son/).size
+
+ # Table aliases
+ @aliases[name] += @other_sql.scan(/join(?:\s+\w+)?\s+\S+\s+#{quoted_name}\son/).size
+ 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 97883d8393..313d9da621 100644
--- a/activerecord/lib/active_record/associations/has_many_through_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_through_association.rb
@@ -1,4 +1,5 @@
require "active_record/associations/through_association_scope"
+require "active_record/associations/nested_has_many_through"
require 'active_support/core_ext/object/blank'
module ActiveRecord
diff --git a/activerecord/lib/active_record/associations/nested_has_many_through.rb b/activerecord/lib/active_record/associations/nested_has_many_through.rb
new file mode 100644
index 0000000000..d699a60edb
--- /dev/null
+++ b/activerecord/lib/active_record/associations/nested_has_many_through.rb
@@ -0,0 +1,158 @@
+# TODO: Remove in the end, when its functionality is fully integrated in ThroughAssociationScope.
+
+module ActiveRecord
+ module Associations
+ module NestedHasManyThrough
+ def self.included(klass)
+ klass.alias_method_chain :construct_conditions, :nesting
+ klass.alias_method_chain :construct_joins, :nesting
+ end
+
+ def construct_joins_with_nesting(custom_joins = nil)
+ if nested?
+ @nested_join_attributes ||= construct_nested_join_attributes
+ "#{construct_nested_join_attributes[:joins]} #{@reflection.options[:joins]} #{custom_joins}"
+ else
+ construct_joins_without_nesting(custom_joins)
+ end
+ end
+
+ def construct_conditions_with_nesting
+ if nested?
+ @nested_join_attributes ||= construct_nested_join_attributes
+ if @reflection.through_reflection && @reflection.through_reflection.macro == :belongs_to
+ "#{@nested_join_attributes[:remote_key]} = #{belongs_to_quoted_key} #{@nested_join_attributes[:conditions]}"
+ else
+ "#{@nested_join_attributes[:remote_key]} = #{@owner.quoted_id} #{@nested_join_attributes[:conditions]}"
+ end
+ else
+ construct_conditions_without_nesting
+ end
+ end
+
+ protected
+
+ # Given any belongs_to or has_many (including has_many :through) association,
+ # return the essential components of a join corresponding to that association, namely:
+ #
+ # * <tt>:joins</tt>: any additional joins required to get from the association's table
+ # (reflection.table_name) to the table that's actually joining to the active record's table
+ # * <tt>:remote_key</tt>: the name of the key in the join table (qualified by table name) which will join
+ # to a field of the active record's table
+ # * <tt>:local_key</tt>: the name of the key in the local table (not qualified by table name) which will
+ # take part in the join
+ # * <tt>:conditions</tt>: any additional conditions (e.g. filtering by type for a polymorphic association,
+ # or a :conditions clause explicitly given in the association), including a leading AND
+ def construct_nested_join_attributes(reflection = @reflection, association_class = reflection.klass, table_ids = {association_class.table_name => 1})
+ if (reflection.macro == :has_many || reflection.macro == :has_one) && reflection.through_reflection
+ construct_has_many_through_attributes(reflection, table_ids)
+ else
+ construct_has_many_or_belongs_to_attributes(reflection, association_class, table_ids)
+ end
+ end
+
+ def construct_has_many_through_attributes(reflection, table_ids)
+ # Construct the join components of the source association, so that we have a path from
+ # the eventual target table of the association up to the table named in :through, and
+ # all tables involved are allocated table IDs.
+ source_attrs = construct_nested_join_attributes(reflection.source_reflection, reflection.klass, table_ids)
+
+ # Determine the alias of the :through table; this will be the last table assigned
+ # when constructing the source join components above.
+ through_table_alias = through_table_name = reflection.through_reflection.table_name
+ through_table_alias += "_#{table_ids[through_table_name]}" unless table_ids[through_table_name] == 1
+
+ # Construct the join components of the through association, so that we have a path to
+ # the active record's table.
+ through_attrs = construct_nested_join_attributes(reflection.through_reflection, reflection.through_reflection.klass, table_ids)
+
+ # Any subsequent joins / filters on owner attributes will act on the through association,
+ # so that's what we return for the conditions/keys of the overall association.
+ conditions = through_attrs[:conditions]
+ conditions += " AND #{interpolate_sql(reflection.klass.send(:sanitize_sql, reflection.options[:conditions]))}" if reflection.options[:conditions]
+
+ {
+ :joins => "%s INNER JOIN %s ON ( %s = %s.%s %s) %s %s" % [
+ source_attrs[:joins],
+ through_table_name == through_table_alias ? through_table_name : "#{through_table_name} #{through_table_alias}",
+ source_attrs[:remote_key],
+ through_table_alias, source_attrs[:local_key],
+ source_attrs[:conditions],
+ through_attrs[:joins],
+ reflection.options[:joins]
+ ],
+ :remote_key => through_attrs[:remote_key],
+ :local_key => through_attrs[:local_key],
+ :conditions => conditions
+ }
+ end
+
+ # reflection is not has_many :through; it's a standard has_many / belongs_to instead
+ # TODO: see if we can defer to rails code here a bit more
+ def construct_has_many_or_belongs_to_attributes(reflection, association_class, table_ids)
+ # Determine the alias used for remote_table_name, if any. In all cases this will already
+ # have been assigned an ID in table_ids (either through being involved in a previous join,
+ # or - if it's the first table in the query - as the default value of table_ids)
+ remote_table_alias = remote_table_name = association_class.table_name
+ remote_table_alias += "_#{table_ids[remote_table_name]}" unless table_ids[remote_table_name] == 1
+
+ # Assign a new alias for the local table.
+ local_table_alias = local_table_name = reflection.active_record.table_name
+ if table_ids[local_table_name]
+ table_id = table_ids[local_table_name] += 1
+ local_table_alias += "_#{table_id}"
+ else
+ table_ids[local_table_name] = 1
+ end
+
+ conditions = ''
+ # Add type_condition, if applicable
+ conditions += " AND #{association_class.send(:type_condition).to_sql}" if association_class.finder_needs_type_condition?
+ # Add custom conditions
+ conditions += " AND (#{interpolate_sql(association_class.send(:sanitize_sql, reflection.options[:conditions]))})" if reflection.options[:conditions]
+
+ if reflection.macro == :belongs_to
+ if reflection.options[:polymorphic]
+ conditions += " AND #{local_table_alias}.#{reflection.options[:foreign_type]} = #{reflection.active_record.quote_value(association_class.base_class.name.to_s)}"
+ end
+ {
+ :joins => reflection.options[:joins],
+ :remote_key => "#{remote_table_alias}.#{association_class.primary_key}",
+ :local_key => reflection.primary_key_name,
+ :conditions => conditions
+ }
+ else
+ # Association is has_many (without :through)
+ if reflection.options[:as]
+ conditions += " AND #{remote_table_alias}.#{reflection.options[:as]}_type = #{reflection.active_record.quote_value(reflection.active_record.base_class.name.to_s)}"
+ end
+ {
+ :joins => "#{reflection.options[:joins]}",
+ :remote_key => "#{remote_table_alias}.#{reflection.primary_key_name}",
+ :local_key => reflection.klass.primary_key,
+ :conditions => conditions
+ }
+ end
+ end
+
+ def belongs_to_quoted_key
+ attribute = @reflection.through_reflection.primary_key_name
+ column = @owner.column_for_attribute attribute
+
+ @owner.send(:quote_value, @owner.send(attribute), column)
+ end
+
+ def nested?
+ through_source_reflection? || through_through_reflection?
+ end
+
+ def through_source_reflection?
+ @reflection.source_reflection && @reflection.source_reflection.options[:through]
+ end
+
+ def through_through_reflection?
+ @reflection.through_reflection && @reflection.through_reflection.options[:through]
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/associations/through_association_scope.rb b/activerecord/lib/active_record/associations/through_association_scope.rb
index cabb33c4a8..582474355e 100644
--- a/activerecord/lib/active_record/associations/through_association_scope.rb
+++ b/activerecord/lib/active_record/associations/through_association_scope.rb
@@ -1,3 +1,5 @@
+require 'enumerator'
+
module ActiveRecord
# = Active Record Through Association Scope
module Associations
@@ -18,10 +20,18 @@ module ActiveRecord
end
# Build SQL conditions from attributes, qualified by table name.
+ # TODO: Conditions on joins
def construct_conditions
- table_name = @reflection.through_reflection.quoted_table_name
- conditions = construct_quoted_owner_attributes(@reflection.through_reflection).map do |attr, value|
- "#{table_name}.#{attr} = #{value}"
+ reflection = @reflection.through_reflection_chain.last
+
+ if reflection.macro == :has_and_belongs_to_many
+ table_alias = table_aliases[reflection].first
+ else
+ table_alias = table_aliases[reflection]
+ end
+
+ conditions = construct_quoted_owner_attributes(reflection).map do |attr, value|
+ "#{table_alias}.#{attr} = #{value}"
end
conditions << sql_conditions if sql_conditions
"(" + conditions.join(') AND (') + ")"
@@ -49,36 +59,172 @@ module ActiveRecord
distinct = "DISTINCT " if @reflection.options[:uniq]
selected = custom_select || @reflection.options[:select] || "#{distinct}#{@reflection.quoted_table_name}.*"
end
-
+
def construct_joins(custom_joins = nil)
- polymorphic_join = nil
- if @reflection.source_reflection.macro == :belongs_to
- reflection_primary_key = @reflection.klass.primary_key
- source_primary_key = @reflection.source_reflection.primary_key_name
- if @reflection.options[:source_type]
- polymorphic_join = "AND %s.%s = %s" % [
- @reflection.through_reflection.quoted_table_name, "#{@reflection.source_reflection.options[:foreign_type]}",
- @owner.class.quote_value(@reflection.options[:source_type])
- ]
- end
- else
- reflection_primary_key = @reflection.source_reflection.primary_key_name
- source_primary_key = @reflection.through_reflection.klass.primary_key
- if @reflection.source_reflection.options[:as]
- polymorphic_join = "AND %s.%s = %s" % [
- @reflection.quoted_table_name, "#{@reflection.source_reflection.options[:as]}_type",
- @owner.class.quote_value(@reflection.through_reflection.klass.name)
- ]
+ # p @reflection.through_reflection_chain
+
+ "#{construct_through_joins} #{@reflection.options[:joins]} #{custom_joins}"
+ end
+
+ def construct_through_joins
+ joins = []
+
+ # Iterate over each pair in the through reflection chain, joining them together
+ @reflection.through_reflection_chain.each_cons(2) do |left, right|
+ right_table_and_alias = table_name_and_alias(right.quoted_table_name, table_aliases[right])
+
+ if left.source_reflection.nil?
+ # TODO: Perhaps need to pay attention to left.options[:primary_key] and
+ # left.options[:foreign_key] in places here
+
+ case left.macro
+ when :belongs_to
+ joins << inner_join_sql(
+ right_table_and_alias,
+ table_aliases[left], left.klass.primary_key,
+ table_aliases[right], left.primary_key_name
+ )
+ when :has_many, :has_one
+ joins << inner_join_sql(
+ right_table_and_alias,
+ table_aliases[left], left.primary_key_name,
+ table_aliases[right], right.klass.primary_key,
+ polymorphic_conditions(left, left.options[:as])
+ )
+ when :has_and_belongs_to_many
+ raise NotImplementedError
+ end
+ else
+ case left.source_reflection.macro
+ when :belongs_to
+ joins << inner_join_sql(
+ right_table_and_alias,
+ table_aliases[left], left.klass.primary_key,
+ table_aliases[right], left.source_reflection.primary_key_name,
+ source_type_conditions(left)
+ )
+ when :has_many, :has_one
+ if right.macro == :has_and_belongs_to_many
+ join_table, right_table = table_aliases[right]
+ right_table_and_alias = table_name_and_alias(right.quoted_table_name, right_table)
+ else
+ right_table = table_aliases[right]
+ end
+
+ joins << inner_join_sql(
+ right_table_and_alias,
+ table_aliases[left], left.source_reflection.primary_key_name,
+ right_table, right.klass.primary_key,
+ polymorphic_conditions(left, left.source_reflection.options[:as])
+ )
+
+ if right.macro == :has_and_belongs_to_many
+ joins << inner_join_sql(
+ table_name_and_alias(
+ quote_table_name(right.options[:join_table]),
+ 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 = table_aliases[left]
+
+ joins << inner_join_sql(
+ table_name_and_alias(
+ quote_table_name(left.source_reflection.options[:join_table]),
+ join_table
+ ),
+ left_table, left.klass.primary_key,
+ join_table, left.source_reflection.association_foreign_key
+ )
+
+ joins << inner_join_sql(
+ right_table_and_alias,
+ join_table, left.source_reflection.primary_key_name,
+ table_aliases[right], right.klass.primary_key
+ )
+ end
end
end
+
+ joins.join(" ")
+ end
+
+ def alias_tracker
+ @alias_tracker ||= AliasTracker.new
+ end
- "INNER JOIN %s ON %s.%s = %s.%s %s #{@reflection.options[:joins]} #{custom_joins}" % [
- @reflection.through_reflection.quoted_table_name,
- @reflection.quoted_table_name, reflection_primary_key,
- @reflection.through_reflection.quoted_table_name, source_primary_key,
- polymorphic_join
+ def table_aliases
+ @table_aliases ||= begin
+ @reflection.through_reflection_chain.inject({}) do |aliases, reflection|
+ table_alias = quote_table_name(alias_tracker.aliased_name_for(
+ reflection.table_name,
+ table_alias_for(reflection, reflection != @reflection)
+ ))
+
+ if reflection.macro == :has_and_belongs_to_many ||
+ (reflection.source_reflection &&
+ reflection.source_reflection.macro == :has_and_belongs_to_many)
+
+ join_table_alias = quote_table_name(alias_tracker.aliased_name_for(
+ (reflection.source_reflection || reflection).options[:join_table],
+ table_alias_for(reflection, true)
+ ))
+
+ aliases[reflection] = [join_table_alias, table_alias]
+ else
+ aliases[reflection] = table_alias
+ end
+
+ aliases
+ end
+ end
+ end
+
+ def table_alias_for(reflection, join = false)
+ name = alias_tracker.pluralize(reflection.name)
+ name << "_#{@reflection.name}"
+ name << "_join" if join
+ name
+ end
+
+ def quote_table_name(table_name)
+ @reflection.klass.connection.quote_table_name(table_name)
+ end
+
+ def table_name_and_alias(table_name, table_alias)
+ "#{table_name} #{table_alias if table_alias != table_name}".strip
+ end
+
+ def inner_join_sql(table, on_left_table, on_left_key, on_right_table, on_right_key, conds = nil)
+ "INNER JOIN %s ON %s.%s = %s.%s %s" % [
+ table,
+ on_left_table, on_left_key,
+ on_right_table, on_right_key,
+ conds
]
end
+
+ def polymorphic_conditions(reflection, interface_name)
+ if interface_name
+ "AND %s.%s = %s" % [
+ table_aliases[reflection], "#{interface_name}_type",
+ @owner.class.quote_value(reflection.active_record.base_class.name)
+ ]
+ end
+ end
+
+ def source_type_conditions(reflection)
+ if reflection.options[:source_type]
+ "AND %s.%s = %s" % [
+ table_aliases[reflection.through_reflection],
+ reflection.source_reflection.options[:foreign_type].to_s,
+ @owner.class.quote_value(reflection.options[:source_type])
+ ]
+ end
+ end
# Construct attributes for associate pointing to owner.
def construct_owner_attributes(reflection)
diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb
index db18fb7c0f..b7cd466e13 100644
--- a/activerecord/lib/active_record/reflection.rb
+++ b/activerecord/lib/active_record/reflection.rb
@@ -131,6 +131,14 @@ module ActiveRecord
@sanitized_conditions ||= klass.send(:sanitize_sql, options[:conditions]) if options[:conditions]
end
+ # TODO: Remove these in the final patch. I am just using them for debugging etc.
+ def inspect
+ "#<#{code_name}>"
+ end
+ def code_name
+ "#{active_record.name}.#{macro} :#{name}"
+ end
+
private
def derive_class_name
name.to_s.camelize
@@ -241,6 +249,10 @@ module ActiveRecord
def through_reflection
false
end
+
+ def through_reflection_chain
+ [self]
+ end
def through_reflection_primary_key_name
end
@@ -304,6 +316,16 @@ module ActiveRecord
def belongs_to?
macro == :belongs_to
end
+
+ # TODO: Remove for final patch. Just here for debugging.
+ def inspect
+ str = "#<#{code_name}, @source_reflection="
+ str << (source_reflection.respond_to?(:code_name) ? source_reflection.code_name : source_reflection.inspect)
+ str << ", @through_reflection="
+ str << (through_reflection.respond_to?(:code_name) ? through_reflection.code_name : through_reflection.inspect)
+ str << ">"
+ str
+ end
private
def derive_class_name
@@ -352,6 +374,27 @@ module ActiveRecord
def through_reflection
@through_reflection ||= active_record.reflect_on_association(options[:through])
end
+
+ # TODO: Documentation
+ def through_reflection_chain
+ @through_reflection_chain ||= begin
+ if source_reflection.through_reflection
+ # If the source reflection goes through another reflection, then the chain must start
+ # by getting us to the 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
# Gets an array of possible <tt>:through</tt> source reflection names:
#
@@ -378,9 +421,11 @@ module ActiveRecord
raise HasManyThroughAssociationPolymorphicError.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
+ # TODO: Presumably remove the HasManyThroughSourceAssociationMacroError class and delete these lines.
+ # Think about whether there are any cases which should still be disallowed.
+ # unless [:belongs_to, :has_many, :has_one].include?(source_reflection.macro) && source_reflection.options[:through].nil?
+ # raise HasManyThroughSourceAssociationMacroError.new(self)
+ # end
check_validity_of_inverse!
end
diff --git a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb
index c7c32da9f3..0e9c8a2639 100644
--- a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb
+++ b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb
@@ -13,17 +13,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 2, 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 2, 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
@@ -54,15 +54,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 2, 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
@@ -130,9 +130,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 2d8e02e398..2ff0714e9f 100644
--- a/activerecord/test/cases/associations/eager_test.rb
+++ b/activerecord/test/cases/associations/eager_test.rb
@@ -53,8 +53,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
+ [: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
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 7e070e1746..c6777d0cb3 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
@@ -713,12 +713,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/join_model_test.rb b/activerecord/test/cases/associations/join_model_test.rb
index 7a22ce4dad..4b7a8b494d 100644
--- a/activerecord/test/cases/associations/join_model_test.rb
+++ b/activerecord/test/cases/associations/join_model_test.rb
@@ -394,14 +394,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_has_many_through_associations_test.rb b/activerecord/test/cases/associations/nested_has_many_through_associations_test.rb
new file mode 100644
index 0000000000..4e7e766b14
--- /dev/null
+++ b/activerecord/test/cases/associations/nested_has_many_through_associations_test.rb
@@ -0,0 +1,261 @@
+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/owner'
+require 'models/pet'
+require 'models/toy'
+require 'models/contract'
+require 'models/company'
+require 'models/developer'
+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'
+
+# NOTE: Some of these tests might not really test "nested" HMT associations, as opposed to ones which
+# are just one level deep. But it's all the same thing really, as the "nested" code is being
+# written in a generic way which applies to "non-nested" HMT associations too. So let's just shove
+# all useful tests in here for now and then work out where they ought to live properly later.
+
+class NestedHasManyThroughAssociationsTest < 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
+
+ # 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
+ author = authors(:david)
+ assert_equal [tags(:general), tags(:general)], author.tags
+
+ # Only David has a Post tagged with General
+ authors = Author.joins(:tags).where('tags.id' => tags(:general).id)
+ assert_equal [authors(:david)], authors.uniq
+
+ authors = Author.includes(:tags)
+ assert_equal [tags(:general), tags(:general)], authors.first.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
+ author = authors(:david)
+ assert_equal [subscribers(:first), subscribers(:second), subscribers(:second)], author.subscribers
+
+ # All authors with subscribers where one of the subscribers' nick is 'alterself'
+ authors = Author.joins(:subscribers).where('subscribers.nick' => 'alterself')
+ assert_equal [authors(:david)], authors
+
+ # TODO: Make this work
+ # authors = Author.includes(:subscribers)
+ # assert_equal [subscribers(:first), subscribers(:second), subscribers(:second)], authors.first.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
+
+ members = Member.joins(:nested_member_types).where('member_types.id' => member_types(:founding).id)
+ assert_equal [members(:groucho)], members
+
+ members = Member.includes(:nested_member_types)
+ assert_equal [member_types(:founding)], members.first.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
+
+ members = Member.joins(:nested_sponsors).where('sponsors.id' => sponsors(:moustache_club_sponsor_for_groucho).id)
+ assert_equal [members(:groucho)], members
+
+ # TODO: Make this work
+ # members = Member.includes(:nested_sponsors)
+ # assert_equal [sponsors(:moustache_club_sponsor_for_groucho)], members.first.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
+ assert_equal [member_details(:groucho), member_details(:some_other_guy)],
+ members(:groucho).organization_member_details
+
+ members = Member.joins(:organization_member_details).
+ where('member_details.id' => member_details(:groucho).id)
+ assert_equal [members(:groucho), members(:some_other_guy)], members
+
+ members = Member.joins(:organization_member_details).
+ where('member_details.id' => 9)
+ assert members.empty?
+
+ members = Member.includes(:organization_member_details)
+ assert_equal [member_details(:groucho), member_details(:some_other_guy)],
+ members.first.organization_member_details
+ end
+
+ # has_many through
+ # Source: has_many
+ # Through: has_one through
+ def test_has_many_through_has_one_through_with_has_many_source_reflection
+ assert_equal [member_details(:groucho), member_details(:some_other_guy)],
+ members(:groucho).organization_member_details_2
+
+ members = Member.joins(:organization_member_details_2).
+ where('member_details.id' => member_details(:groucho).id)
+ assert_equal [members(:groucho), members(:some_other_guy)], members
+
+ members = Member.joins(:organization_member_details_2).
+ where('member_details.id' => 9)
+ assert members.empty?
+
+ # TODO: Make this work
+ # members = Member.includes(:organization_member_details_2)
+ # assert_equal [member_details(:groucho), member_details(:some_other_guy)],
+ # members.first.organization_member_details_2
+ 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
+ assert_equal [categories(:general), categories(:cooking)], authors(:bob).post_categories
+
+ authors = Author.joins(:post_categories).where('categories.id' => categories(:cooking).id)
+ assert_equal [authors(:bob)], authors
+
+ authors = Author.includes(:post_categories)
+ assert_equal [categories(:general), categories(:cooking)], authors[2].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
+ assert_equal [comments(:greetings), comments(:more_greetings)], categories(:technology).post_comments
+
+ categories = Category.joins(:post_comments).where('comments.id' => comments(:more_greetings).id)
+ assert_equal [categories(:general), categories(:technology)], categories
+
+ # TODO: Make this work
+ # categories = Category.includes(:post_comments)
+ # assert_equal [comments(:greetings), comments(:more_greetings)], categories[1].post_comments
+ end
+
+ # TODO: has_many through
+ # Source: belongs_to
+ # Through: has_many through
+
+ # TODO: has_many through
+ # Source: has_many through
+ # Through: belongs_to
+
+ # 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
+
+ members = Member.joins(:nested_member_type).where('member_types.id' => member_types(:founding).id)
+ assert_equal [members(:groucho)], members
+
+ members = Member.includes(:nested_member_type)
+ assert_equal member_types(:founding), members.first.nested_member_type
+ end
+
+ # TODO: has_one through
+ # Source: belongs_to
+ # Through: has_one through
+
+ 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
+ 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)], 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
+ 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
+
+ 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
+end
diff --git a/activerecord/test/cases/batches_test.rb b/activerecord/test/cases/batches_test.rb
index dcc49e12ca..70883ad30f 100644
--- a/activerecord/test/cases/batches_test.rb
+++ b/activerecord/test/cases/batches_test.rb
@@ -24,7 +24,7 @@ class EachTest < ActiveRecord::TestCase
end
def test_each_should_execute_if_id_is_in_select
- assert_queries(4) do
+ assert_queries(5) 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 c058196078..0476fc94df 100644
--- a/activerecord/test/cases/finder_test.rb
+++ b/activerecord/test/cases/finder_test.rb
@@ -127,7 +127,7 @@ class FinderTest < ActiveRecord::TestCase
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],[3,8]], 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..430be003ac 100644
--- a/activerecord/test/cases/json_serialization_test.rb
+++ b/activerecord/test/cases/json_serialization_test.rb
@@ -196,7 +196,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/relations_test.rb b/activerecord/test/cases/relations_test.rb
index 222819cbe5..df4e84ca29 100644
--- a/activerecord/test/cases/relations_test.rb
+++ b/activerecord/test/cases/relations_test.rb
@@ -428,7 +428,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
@@ -501,22 +501,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 9, posts.count
+ assert_equal 9, posts.count(:all)
+ assert_equal 9, posts.count(:id)
assert_equal 1, posts.where('comments_count > 1').count
- assert_equal 5, posts.where(:comments_count => 0).count
+ assert_equal 7, 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 9, 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 9, posts.select(:comments_count).count(:distinct => false)
end
def test_count_explicit_columns
@@ -526,7 +526,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 9, 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')
@@ -541,12 +541,12 @@ class RelationTest < ActiveRecord::TestCase
def test_size
posts = Post.scoped
- assert_queries(1) { assert_equal 7, posts.size }
+ assert_queries(1) { assert_equal 9, 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 7, 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..6f13ec4dac 100644
--- a/activerecord/test/fixtures/authors.yml
+++ b/activerecord/test/fixtures/authors.yml
@@ -7,3 +7,7 @@ david:
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/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 6db945e61d..824840b7e5 100644
--- a/activerecord/test/fixtures/members.yml
+++ b/activerecord/test/fixtures/members.yml
@@ -1,6 +1,8 @@
groucho:
+ id: 1
name: Groucho Marx
member_type_id: 1
some_other_guy:
+ id: 2
name: Englebert Humperdink
member_type_id: 2
diff --git a/activerecord/test/fixtures/memberships.yml b/activerecord/test/fixtures/memberships.yml
index b9722dbc8a..eed8b22af8 100644
--- a/activerecord/test/fixtures/memberships.yml
+++ b/activerecord/test/fixtures/memberships.yml
@@ -1,20 +1,20 @@
membership_of_boring_club:
joined_on: <%= 3.weeks.ago.to_s(:db) %>
club: boring_club
- member: groucho
+ member_id: 1
favourite: false
type: CurrentMembership
membership_of_favourite_club:
joined_on: <%= 3.weeks.ago.to_s(:db) %>
club: moustache_club
- member: groucho
+ member_id: 1
favourite: true
type: Membership
other_guys_membership:
joined_on: <%= 4.weeks.ago.to_s(:db) %>
club: boring_club
- member: some_other_guy
+ member_id: 2
favourite: false
type: CurrentMembership
diff --git a/activerecord/test/fixtures/posts.yml b/activerecord/test/fixtures/posts.yml
index f817493190..ca6d4c2fe1 100644
--- a/activerecord/test/fixtures/posts.yml
+++ b/activerecord/test/fixtures/posts.yml
@@ -50,3 +50,17 @@ 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
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/sponsors.yml b/activerecord/test/fixtures/sponsors.yml
index 42df8957d1..bfc6b238b1 100644
--- a/activerecord/test/fixtures/sponsors.yml
+++ b/activerecord/test/fixtures/sponsors.yml
@@ -1,9 +1,12 @@
moustache_club_sponsor_for_groucho:
sponsor_club: moustache_club
- sponsorable: groucho (Member)
+ sponsorable_id: 1
+ sponsorable_type: Member
boring_club_sponsor_for_groucho:
sponsor_club: boring_club
- sponsorable: some_other_guy (Member)
+ sponsorable_id: 2
+ sponsorable_type: Member
crazy_club_sponsor_for_groucho:
sponsor_club: crazy_club
- sponsorable: some_other_guy (Member) \ No newline at end of file
+ sponsorable_id: 2
+ sponsorable_type: Member
diff --git a/activerecord/test/fixtures/taggings.yml b/activerecord/test/fixtures/taggings.yml
index 3db6a4c079..7cc7198ded 100644
--- a/activerecord/test/fixtures/taggings.yml
+++ b/activerecord/test/fixtures/taggings.yml
@@ -26,3 +26,15 @@ 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
diff --git a/activerecord/test/fixtures/tags.yml b/activerecord/test/fixtures/tags.yml
index 7610fd38b9..6cb886dc46 100644
--- a/activerecord/test/fixtures/tags.yml
+++ b/activerecord/test/fixtures/tags.yml
@@ -4,4 +4,4 @@ general:
misc:
id: 2
- name: Misc \ No newline at end of file
+ name: Misc
diff --git a/activerecord/test/models/author.rb b/activerecord/test/models/author.rb
index 34bfd2d881..584164f19a 100644
--- a/activerecord/test/models/author.rb
+++ b/activerecord/test/models/author.rb
@@ -83,16 +83,25 @@ 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
+ has_many :distinct_tags, :through => :posts, :source => :tags, :select => "DISTINCT tags.*", :order => "tags.name"
has_many :post_categories, :through => :posts, :source => :categories
+ 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
- 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
+
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/category.rb b/activerecord/test/models/category.rb
index 48415846dd..c933943813 100644
--- a/activerecord/test/models/category.rb
+++ b/activerecord/test/models/category.rb
@@ -23,6 +23,8 @@ class Category < ActiveRecord::Base
has_many :categorizations
has_many :authors, :through => :categorizations, :select => 'authors.*, categorizations.post_id'
+
+ has_many :post_comments, :through => :posts, :source => :comments
end
class SpecialCategory < Category
diff --git a/activerecord/test/models/comment.rb b/activerecord/test/models/comment.rb
index 88061b2145..1a3fb42b66 100644
--- a/activerecord/test/models/comment.rb
+++ b/activerecord/test/models/comment.rb
@@ -7,6 +7,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/job.rb b/activerecord/test/models/job.rb
index 3333a02e27..46b1d87aa1 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 255fb569d7..44c10cc4a4 100644
--- a/activerecord/test/models/member.rb
+++ b/activerecord/test/models/member.rb
@@ -9,4 +9,13 @@ class Member < ActiveRecord::Base
has_one :member_detail
has_one :organization, :through => :member_detail
belongs_to :member_type
-end \ No newline at end of file
+
+ 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
+end
diff --git a/activerecord/test/models/member_detail.rb b/activerecord/test/models/member_detail.rb
index 94f59e5794..0f53b69ced 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/person.rb b/activerecord/test/models/person.rb
index 951ec93c53..d35c51b660 100644
--- a/activerecord/test/models/person.rb
+++ b/activerecord/test/models/person.rb
@@ -13,6 +13,9 @@ class Person < ActiveRecord::Base
belongs_to :primary_contact, :class_name => 'Person'
has_many :agents, :class_name => 'Person', :foreign_key => 'primary_contact_id'
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' }
diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb
index a3cb9c724a..f3b78c3647 100644
--- a/activerecord/test/models/post.rb
+++ b/activerecord/test/models/post.rb
@@ -46,6 +46,8 @@ class Post < ActiveRecord::Base
has_one :very_special_comment_with_post, :class_name => "VerySpecialComment", :include => :post
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'
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 4a17c936f5..2feb15d706 100644
--- a/activerecord/test/models/reference.rb
+++ b/activerecord/test/models/reference.rb
@@ -1,6 +1,8 @@
class Reference < ActiveRecord::Base
belongs_to :person
belongs_to :job
+
+ has_many :agents_posts_authors, :through => :person
end
class BadReference < ActiveRecord::Base
diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb
index ea62833d81..2fa9a4521e 100644
--- a/activerecord/test/schema/schema.rb
+++ b/activerecord/test/schema/schema.rb
@@ -71,6 +71,7 @@ ActiveRecord::Schema.define do
end
create_table :books, :force => true do |t|
+ t.integer :author_id
t.column :name, :string
end
@@ -448,6 +449,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