From b689834bcf2730353d066277f43047f10abb8d30 Mon Sep 17 00:00:00 2001 From: Bodaniel Jeanes Date: Sun, 26 Sep 2010 22:17:18 +1000 Subject: [PATCH 001/100] Initial nested_has_many_through support [#1152] --- .../lib/active_record/associations.rb | 1 + .../has_many_through_association.rb | 2 + .../associations/nested_has_many_through.rb | 156 ++++++++++++++++++ activerecord/lib/active_record/reflection.rb | 6 +- .../cases/associations/join_model_test.rb | 8 - ...sted_has_many_through_associations_test.rb | 43 +++++ activerecord/test/fixtures/books.yml | 2 + activerecord/test/models/author.rb | 14 +- activerecord/test/models/book.rb | 2 + activerecord/test/schema/schema.rb | 1 + 10 files changed, 220 insertions(+), 15 deletions(-) create mode 100644 activerecord/lib/active_record/associations/nested_has_many_through.rb create mode 100644 activerecord/test/cases/associations/nested_has_many_through_associations_test.rb diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 565ebf8197..812abf5a55 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -111,6 +111,7 @@ 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' 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..964c381c0d 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 @@ -6,6 +7,7 @@ module ActiveRecord module Associations class HasManyThroughAssociation < HasManyAssociation #:nodoc: include ThroughAssociationScope + include NestedHasManyThrough alias_method :new, :build 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..2d03b81128 --- /dev/null +++ b/activerecord/lib/active_record/associations/nested_has_many_through.rb @@ -0,0 +1,156 @@ +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: + # + # * :joins: 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 + # * :remote_key: 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 + # * :local_key: the name of the key in the local table (not qualified by table name) which will + # take part in the join + # * :conditions: 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/reflection.rb b/activerecord/lib/active_record/reflection.rb index db18fb7c0f..ae90d30b42 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -378,9 +378,9 @@ 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 + # 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/join_model_test.rb b/activerecord/test/cases/associations/join_model_test.rb index f131dc01f6..0b1a3db1e4 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..36de709ffc --- /dev/null +++ b/activerecord/test/cases/associations/nested_has_many_through_associations_test.rb @@ -0,0 +1,43 @@ +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' + +class NestedHasManyThroughAssociationsTest < ActiveRecord::TestCase + fixtures :authors, :books, :posts, :subscriptions, :subscribers, :tags, :taggings + + def test_has_many_through_a_has_many_through_association_on_source_reflection + author = authors(:david) + assert_equal [tags(:general), tags(:general)], author.tags + end + + def test_has_many_through_a_has_many_through_association_on_through_reflection + author = authors(:david) + assert_equal [subscribers(:first), subscribers(:second), subscribers(:second)], author.subscribers + 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 + end +end \ No newline at end of file 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/models/author.rb b/activerecord/test/models/author.rb index 34bfd2d881..94810e2f34 100644 --- a/activerecord/test/models/author.rb +++ b/activerecord/test/models/author.rb @@ -83,14 +83,20 @@ 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 # through polymorphic has_one + has_many :taggings, :through => :posts, :source => :taggings # through polymorphic has_many + has_many :tags, :through => :posts # through has_many :through (on source reflection + polymorphic) + 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 # 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" scope :relation_include_posts, includes(:posts) 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/schema/schema.rb b/activerecord/test/schema/schema.rb index ea62833d81..dbd5da45eb 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 From 14c4881f9c7bf4eae61e548542ee309c013e1fca Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Thu, 30 Sep 2010 22:01:03 +0100 Subject: [PATCH 002/100] Prevent test_has_many_through_a_has_many_through_association_on_through_reflection failing for me due to ordering of the results --- activerecord/test/models/author.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/activerecord/test/models/author.rb b/activerecord/test/models/author.rb index 94810e2f34..1efb4fc095 100644 --- a/activerecord/test/models/author.rb +++ b/activerecord/test/models/author.rb @@ -84,14 +84,14 @@ class Author < ActiveRecord::Base 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 :taggings, :through => :posts, :source => :taggings # through polymorphic has_many TODO: Why is the :source needed? has_many :tags, :through => :posts # through has_many :through (on source reflection + polymorphic) 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 # through has_many :through (on through reflection) + 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 From 4f69a61107d9d59f96bf249ef077483e90babe72 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Fri, 1 Oct 2010 13:10:41 +0100 Subject: [PATCH 003/100] Started implementing nested :through associations by using the existing structure of ThroughAssociationScope rather than layering a module over the top --- .../has_many_through_association.rb | 2 +- .../associations/through_association_scope.rb | 49 ++++++++++++------- activerecord/lib/active_record/reflection.rb | 15 ++++++ ...sted_has_many_through_associations_test.rb | 26 +++++----- 4 files changed, 60 insertions(+), 32 deletions(-) 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 964c381c0d..ee892d373c 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -7,7 +7,7 @@ module ActiveRecord module Associations class HasManyThroughAssociation < HasManyAssociation #:nodoc: include ThroughAssociationScope - include NestedHasManyThrough + # include NestedHasManyThrough alias_method :new, :build diff --git a/activerecord/lib/active_record/associations/through_association_scope.rb b/activerecord/lib/active_record/associations/through_association_scope.rb index cabb33c4a8..c433c9e66e 100644 --- a/activerecord/lib/active_record/associations/through_association_scope.rb +++ b/activerecord/lib/active_record/associations/through_association_scope.rb @@ -19,8 +19,8 @@ module ActiveRecord # Build SQL conditions from attributes, qualified by table name. 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 = @reflection.final_through_reflection.quoted_table_name + conditions = construct_quoted_owner_attributes(@reflection.final_through_reflection).map do |attr, value| "#{table_name}.#{attr} = #{value}" end conditions << sql_conditions if sql_conditions @@ -49,35 +49,48 @@ 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) + "#{construct_through_joins(@reflection)} #{@reflection.options[:joins]} #{custom_joins}" + end + + def construct_through_joins(reflection) 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] + 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]) + 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] + 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) + reflection.quoted_table_name, "#{@reflection.source_reflection.options[:as]}_type", + @owner.class.quote_value(reflection.through_reflection.klass.name) ] end 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, + joins = "INNER JOIN %s ON %s.%s = %s.%s %s" % [ + reflection.through_reflection.quoted_table_name, + reflection.quoted_table_name, reflection_primary_key, + reflection.through_reflection.quoted_table_name, source_primary_key, polymorphic_join ] + + # If the reflection we are going :through goes itself :through another reflection, then + # we must recursively get the joins to make that happen too. + if reflection.through_reflection.through_reflection + joins << " " + joins << construct_through_joins(reflection.through_reflection) + end + + joins end # Construct attributes for associate pointing to owner. diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index ae90d30b42..888ddcdd5b 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -352,6 +352,21 @@ module ActiveRecord def through_reflection @through_reflection ||= active_record.reflect_on_association(options[:through]) end + + # A :through reflection may have a :through reflection itself. This method returns the through + # reflection which is furthest away, i.e. the last in the chain, so the first which does not + # have its own :through reflection. + def final_through_reflection + @final_through_reflection ||= begin + reflection = through_reflection + + while reflection.through_reflection + reflection = reflection.through_reflection + end + + reflection + end + end # Gets an array of possible :through source reflection names: # 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 index 36de709ffc..539e6e000a 100644 --- a/activerecord/test/cases/associations/nested_has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/nested_has_many_through_associations_test.rb @@ -21,23 +21,23 @@ require 'models/subscription' class NestedHasManyThroughAssociationsTest < ActiveRecord::TestCase fixtures :authors, :books, :posts, :subscriptions, :subscribers, :tags, :taggings - def test_has_many_through_a_has_many_through_association_on_source_reflection - author = authors(:david) - assert_equal [tags(:general), tags(:general)], author.tags - end +# def test_has_many_through_a_has_many_through_association_on_source_reflection +# author = authors(:david) +# assert_equal [tags(:general), tags(:general)], author.tags +# end def test_has_many_through_a_has_many_through_association_on_through_reflection author = authors(:david) assert_equal [subscribers(:first), subscribers(:second), subscribers(:second)], author.subscribers 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_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 -end \ No newline at end of file +# 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 +end From 34ee586e993ad9e466b81f376fa92feb5d312b4c Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Sat, 2 Oct 2010 18:50:17 +0100 Subject: [PATCH 004/100] Integrate nested support into ThroughAssociationScope, using my concept of generating a 'chain' of reflections to be joined. It seems to work at the moment, all existing tests are passing. There may be further complications as we add more test cases for nested associations, though. --- .../has_many_through_association.rb | 1 - .../associations/nested_has_many_through.rb | 2 + .../associations/through_association_scope.rb | 89 +++++++++++-------- activerecord/lib/active_record/reflection.rb | 50 ++++++++--- ...sted_has_many_through_associations_test.rb | 24 ++--- 5 files changed, 107 insertions(+), 59 deletions(-) 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 ee892d373c..313d9da621 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -7,7 +7,6 @@ module ActiveRecord module Associations class HasManyThroughAssociation < HasManyAssociation #:nodoc: include ThroughAssociationScope - # include NestedHasManyThrough alias_method :new, :build diff --git a/activerecord/lib/active_record/associations/nested_has_many_through.rb b/activerecord/lib/active_record/associations/nested_has_many_through.rb index 2d03b81128..d699a60edb 100644 --- a/activerecord/lib/active_record/associations/nested_has_many_through.rb +++ b/activerecord/lib/active_record/associations/nested_has_many_through.rb @@ -1,3 +1,5 @@ +# TODO: Remove in the end, when its functionality is fully integrated in ThroughAssociationScope. + module ActiveRecord module Associations module NestedHasManyThrough diff --git a/activerecord/lib/active_record/associations/through_association_scope.rb b/activerecord/lib/active_record/associations/through_association_scope.rb index c433c9e66e..c5453fa79f 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 @@ -19,8 +21,9 @@ module ActiveRecord # Build SQL conditions from attributes, qualified by table name. def construct_conditions - table_name = @reflection.final_through_reflection.quoted_table_name - conditions = construct_quoted_owner_attributes(@reflection.final_through_reflection).map do |attr, value| + reflection = @reflection.through_reflection_chain.last + table_name = reflection.quoted_table_name + conditions = construct_quoted_owner_attributes(reflection).map do |attr, value| "#{table_name}.#{attr} = #{value}" end conditions << sql_conditions if sql_conditions @@ -51,43 +54,57 @@ module ActiveRecord end def construct_joins(custom_joins = nil) - "#{construct_through_joins(@reflection)} #{@reflection.options[:joins]} #{custom_joins}" + # puts @reflection.through_reflection_chain.map(&:inspect) + + "#{construct_through_joins} #{@reflection.options[:joins]} #{custom_joins}" end - def construct_through_joins(reflection) - 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) - ] - end - end - - joins = "INNER JOIN %s ON %s.%s = %s.%s %s" % [ - 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 construct_through_joins + joins = [] - # If the reflection we are going :through goes itself :through another reflection, then - # we must recursively get the joins to make that happen too. - if reflection.through_reflection.through_reflection - joins << " " - joins << construct_through_joins(reflection.through_reflection) + # Iterate over each pair in the through reflection chain, joining them together + @reflection.through_reflection_chain.each_cons(2) do |left, right| + polymorphic_join = nil + + case + when left.options[:as] + left_primary_key = left.primary_key_name + right_primary_key = right.klass.primary_key + + polymorphic_join = "AND %s.%s = %s" % [ + left.quoted_table_name, "#{left.options[:as]}_type", + @owner.class.quote_value(right.klass.name) + ] + when left.source_reflection.macro == :belongs_to + left_primary_key = left.klass.primary_key + right_primary_key = left.source_reflection.primary_key_name + + if left.options[:source_type] + polymorphic_join = "AND %s.%s = %s" % [ + right.quoted_table_name, + left.source_reflection.options[:foreign_type].to_s, + @owner.class.quote_value(left.options[:source_type]) + ] + end + else + left_primary_key = left.source_reflection.primary_key_name + right_primary_key = right.klass.primary_key + + if left.source_reflection.options[:as] + polymorphic_join = "AND %s.%s = %s" % [ + left.quoted_table_name, + "#{left.source_reflection.options[:as]}_type", + @owner.class.quote_value(right.klass.name) + ] + end + end + + joins << "INNER JOIN %s ON %s.%s = %s.%s %s" % [ + right.quoted_table_name, + left.quoted_table_name, left_primary_key, + right.quoted_table_name, right_primary_key, + polymorphic_join + ] end joins diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 888ddcdd5b..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 @@ -353,18 +375,24 @@ module ActiveRecord @through_reflection ||= active_record.reflect_on_association(options[:through]) end - # A :through reflection may have a :through reflection itself. This method returns the through - # reflection which is furthest away, i.e. the last in the chain, so the first which does not - # have its own :through reflection. - def final_through_reflection - @final_through_reflection ||= begin - reflection = through_reflection - - while reflection.through_reflection - reflection = reflection.through_reflection + # 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 - reflection + # Recursively build the rest of the chain + chain += through_reflection.through_reflection_chain + + # Finally return the completed chain + chain end end @@ -393,6 +421,8 @@ module ActiveRecord raise HasManyThroughAssociationPolymorphicError.new(active_record.name, self, source_reflection) 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 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 index 539e6e000a..938643b1b3 100644 --- a/activerecord/test/cases/associations/nested_has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/nested_has_many_through_associations_test.rb @@ -21,23 +21,23 @@ require 'models/subscription' class NestedHasManyThroughAssociationsTest < ActiveRecord::TestCase fixtures :authors, :books, :posts, :subscriptions, :subscribers, :tags, :taggings -# def test_has_many_through_a_has_many_through_association_on_source_reflection -# author = authors(:david) -# assert_equal [tags(:general), tags(:general)], author.tags -# end + def test_has_many_through_a_has_many_through_association_on_source_reflection + author = authors(:david) + assert_equal [tags(:general), tags(:general)], author.tags + end def test_has_many_through_a_has_many_through_association_on_through_reflection author = authors(:david) assert_equal [subscribers(:first), subscribers(:second), subscribers(:second)], author.subscribers 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_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_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 end From a34391c3b495bad268204bdf4f6b3483a61abcd5 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Sat, 2 Oct 2010 21:45:46 +0100 Subject: [PATCH 005/100] Add support for table aliasing, with a test that needs aliasing in order to work correctly. This test incidentally provides a more complicated test case (4 inner joins, 2 using polymorphism). --- .../associations/through_association_scope.rb | 50 ++++++++++++++----- ...sted_has_many_through_associations_test.rb | 5 ++ activerecord/test/fixtures/authors.yml | 4 ++ activerecord/test/fixtures/posts.yml | 14 ++++++ activerecord/test/fixtures/taggings.yml | 12 +++++ activerecord/test/fixtures/tags.yml | 2 +- activerecord/test/models/author.rb | 3 +- 7 files changed, 75 insertions(+), 15 deletions(-) diff --git a/activerecord/lib/active_record/associations/through_association_scope.rb b/activerecord/lib/active_record/associations/through_association_scope.rb index c5453fa79f..90ebadda89 100644 --- a/activerecord/lib/active_record/associations/through_association_scope.rb +++ b/activerecord/lib/active_record/associations/through_association_scope.rb @@ -22,9 +22,8 @@ module ActiveRecord # Build SQL conditions from attributes, qualified by table name. def construct_conditions reflection = @reflection.through_reflection_chain.last - table_name = reflection.quoted_table_name conditions = construct_quoted_owner_attributes(reflection).map do |attr, value| - "#{table_name}.#{attr} = #{value}" + "#{table_aliases[reflection]}.#{attr} = #{value}" end conditions << sql_conditions if sql_conditions "(" + conditions.join(') AND (') + ")" @@ -67,21 +66,23 @@ module ActiveRecord polymorphic_join = nil case - when left.options[:as] + when left.source_reflection.nil? left_primary_key = left.primary_key_name right_primary_key = right.klass.primary_key - polymorphic_join = "AND %s.%s = %s" % [ - left.quoted_table_name, "#{left.options[:as]}_type", - @owner.class.quote_value(right.klass.name) - ] + if left.options[:as] + polymorphic_join = "AND %s.%s = %s" % [ + table_aliases[left], "#{left.options[:as]}_type", + @owner.class.quote_value(right.klass.name) + ] + end when left.source_reflection.macro == :belongs_to left_primary_key = left.klass.primary_key right_primary_key = left.source_reflection.primary_key_name if left.options[:source_type] polymorphic_join = "AND %s.%s = %s" % [ - right.quoted_table_name, + table_aliases[right], left.source_reflection.options[:foreign_type].to_s, @owner.class.quote_value(left.options[:source_type]) ] @@ -92,22 +93,45 @@ module ActiveRecord if left.source_reflection.options[:as] polymorphic_join = "AND %s.%s = %s" % [ - left.quoted_table_name, + table_aliases[left], "#{left.source_reflection.options[:as]}_type", @owner.class.quote_value(right.klass.name) ] end end + if right.quoted_table_name == table_aliases[right] + table = right.quoted_table_name + else + table = "#{right.quoted_table_name} #{table_aliases[right]}" + end + joins << "INNER JOIN %s ON %s.%s = %s.%s %s" % [ - right.quoted_table_name, - left.quoted_table_name, left_primary_key, - right.quoted_table_name, right_primary_key, + table, + table_aliases[left], left_primary_key, + table_aliases[right], right_primary_key, polymorphic_join ] end - joins + joins.join(" ") + end + + def table_aliases + @table_aliases ||= begin + tally = {} + @reflection.through_reflection_chain.inject({}) do |aliases, reflection| + if tally[reflection.table_name].nil? + tally[reflection.table_name] = 1 + aliases[reflection] = reflection.quoted_table_name + else + tally[reflection.table_name] += 1 + aliased_table_name = reflection.table_name + "_#{tally[reflection.table_name]}" + aliases[reflection] = reflection.klass.connection.quote_table_name(aliased_table_name) + end + aliases + end + end end # Construct attributes for associate pointing to owner. 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 index 938643b1b3..925a9598fb 100644 --- a/activerecord/test/cases/associations/nested_has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/nested_has_many_through_associations_test.rb @@ -40,4 +40,9 @@ class NestedHasManyThroughAssociationsTest < ActiveRecord::TestCase 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) + end end 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/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/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 1efb4fc095..1fbd729b60 100644 --- a/activerecord/test/models/author.rb +++ b/activerecord/test/models/author.rb @@ -84,8 +84,9 @@ class Author < ActiveRecord::Base 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 TODO: Why is the :source needed? + has_many :taggings, :through => :posts # through polymorphic has_many has_many :tags, :through => :posts # through has_many :through (on source reflection + polymorphic) + 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 From 43711083dd34252877bab9df43d3db0fd42feeb2 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Sun, 3 Oct 2010 11:56:32 +0100 Subject: [PATCH 006/100] Fix the tests (I have actually verified that these are also the 'right' fixes, rather than just making the tests pass again) --- .../cascaded_eager_loading_test.rb | 18 ++++++++--------- .../test/cases/associations/eager_test.rb | 4 ++-- activerecord/test/cases/batches_test.rb | 2 +- activerecord/test/cases/finder_test.rb | 2 +- .../test/cases/json_serialization_test.rb | 2 +- activerecord/test/cases/relations_test.rb | 20 +++++++++---------- 6 files changed, 24 insertions(+), 24 deletions(-) diff --git a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb index b93e49613d..8bb8f3e359 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 9, 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 9, 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 9, 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 40859d425f..1669c4d5f4 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/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 26b5096255..e73f58fdc7 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 d642aeed8b..fec5e6731f 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -434,7 +434,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 @@ -507,22 +507,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 @@ -532,7 +532,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') @@ -547,12 +547,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 From 3cc35633872a0072764d8edb20f1fc4e14adf729 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Tue, 5 Oct 2010 20:20:27 +0100 Subject: [PATCH 007/100] A failing test for a nested has many through association loaded via Foo.joins(:bar) --- .../nested_has_many_through_associations_test.rb | 6 ++++++ 1 file changed, 6 insertions(+) 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 index 925a9598fb..a5d3f27702 100644 --- a/activerecord/test/cases/associations/nested_has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/nested_has_many_through_associations_test.rb @@ -45,4 +45,10 @@ class NestedHasManyThroughAssociationsTest < ActiveRecord::TestCase author = authors(:bob) assert_equal [posts(:misc_by_bob), posts(:misc_by_mary)], author.similar_posts.sort_by(&:id) end + + def test_nested_has_many_through_as_a_join + # 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 + end end From f2b41914d6be935182d37e0c0d491352ac3de043 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Wed, 6 Oct 2010 12:06:51 +0100 Subject: [PATCH 008/100] Refactoring JoinDependency and friends. This improves the code (IMO) including adding some explanatory comments, but more importantly structures it in such a way as to allow a JoinAssociation to produce an arbitrary number of actual joins, which will be necessary for nested has many through support. Also added 3 tests covering functionality which existed but was not previously covered. --- .../lib/active_record/associations.rb | 504 +++++++++++------- .../active_record/relation/finder_methods.rb | 7 +- .../active_record/relation/query_methods.rb | 13 +- .../cascaded_eager_loading_test.rb | 8 +- .../test/cases/associations/eager_test.rb | 8 +- .../inner_join_association_test.rb | 24 +- .../cases/associations/join_model_test.rb | 4 +- activerecord/test/cases/finder_test.rb | 2 +- activerecord/test/fixtures/comments.yml | 6 + activerecord/test/models/comment.rb | 3 + 10 files changed, 363 insertions(+), 216 deletions(-) diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 812abf5a55..67c204f154 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1834,10 +1834,10 @@ module ActiveRecord end class JoinDependency # :nodoc: - attr_reader :joins, :reflections, :table_aliases + attr_reader :join_parts, :reflections, :table_aliases def initialize(base, associations, joins) - @joins = [JoinBase.new(base, joins)] + @join_parts = [JoinBase.new(base, joins)] @associations = associations @reflections = [] @base_records_hash = {} @@ -1850,17 +1850,17 @@ module ActiveRecord def graft(*associations) associations.each do |association| join_associations.detect {|a| association == a} || - build(association.reflection.name, association.find_parent_in(self) || join_base, association.join_class) + build(association.reflection.name, association.find_parent_in(self) || join_base, association.join_type) end self end def join_associations - @joins.last(@joins.length - 1) + join_parts.last(join_parts.length - 1) end def join_base - @joins[0] + join_parts.first end def count_aliases_from_table_joins(name) @@ -1918,22 +1918,24 @@ module ActiveRecord protected - def build(associations, parent = nil, join_class = Arel::InnerJoin) - parent ||= @joins.last + def build(associations, parent = nil, join_type = Arel::InnerJoin) + parent ||= join_parts.last case associations when Symbol, String reflection = parent.reflections[associations.to_s.intern] or raise ConfigurationError, "Association named '#{ associations }' was not found; perhaps you misspelled it?" @reflections << reflection - @joins << build_join_association(reflection, parent).with_join_class(join_class) + join_association = build_join_association(reflection, parent) + join_association.join_type = join_type + @join_parts << join_association when Array associations.each do |association| - build(association, parent, join_class) + build(association, parent, join_type) end when Hash associations.keys.sort{|a,b|a.to_s<=>b.to_s}.each do |name| - build(name, parent, join_class) - build(associations[name], nil, join_class) + build(name, parent, join_type) + build(associations[name], nil, join_type) end else raise ConfigurationError, associations.inspect @@ -1950,91 +1952,111 @@ module ActiveRecord JoinAssociation.new(reflection, self, parent) end - def construct(parent, associations, joins, row) + def construct(parent, associations, join_parts, row) case associations when Symbol, String - join = joins.detect{|j| j.reflection.name.to_s == associations.to_s && j.parent_table_name == parent.class.table_name } - raise(ConfigurationError, "No such association") if join.nil? + join_part = join_parts.detect { |j| + j.reflection.name.to_s == associations.to_s && + j.parent_table_name == parent.class.table_name } + raise(ConfigurationError, "No such association") if join_part.nil? - joins.delete(join) - construct_association(parent, join, row) + join_parts.delete(join_part) + construct_association(parent, join_part, row) when Array associations.each do |association| - construct(parent, association, joins, row) + construct(parent, association, join_parts, row) end when Hash associations.sort_by { |k,_| k.to_s }.each do |name, assoc| - join = joins.detect{|j| j.reflection.name.to_s == name.to_s && j.parent_table_name == parent.class.table_name } - raise(ConfigurationError, "No such association") if join.nil? + join_part = join_parts.detect{ |j| + j.reflection.name.to_s == name.to_s && + j.parent_table_name == parent.class.table_name } + raise(ConfigurationError, "No such association") if join_part.nil? - association = construct_association(parent, join, row) - joins.delete(join) - construct(association, assoc, joins, row) if association + association = construct_association(parent, join_part, row) + join_parts.delete(join_part) + construct(association, assoc, join_parts, row) if association end else raise ConfigurationError, associations.inspect end end - def construct_association(record, join, row) - return if record.id.to_s != join.parent.record_id(row).to_s + def construct_association(record, join_part, row) + return if record.id.to_s != join_part.parent.record_id(row).to_s - macro = join.reflection.macro + macro = join_part.reflection.macro if macro == :has_one - return if record.instance_variable_defined?("@#{join.reflection.name}") - association = join.instantiate(row) unless row[join.aliased_primary_key].nil? - set_target_and_inverse(join, association, record) + return if record.instance_variable_defined?("@#{join_part.reflection.name}") + association = join_part.instantiate(row) unless row[join_part.aliased_primary_key].nil? + set_target_and_inverse(join_part, association, record) else - return if row[join.aliased_primary_key].nil? - association = join.instantiate(row) + return if row[join_part.aliased_primary_key].nil? + association = join_part.instantiate(row) case macro when :has_many, :has_and_belongs_to_many - collection = record.send(join.reflection.name) + collection = record.send(join_part.reflection.name) collection.loaded collection.target.push(association) collection.__send__(:set_inverse_instance, association, record) when :belongs_to - set_target_and_inverse(join, association, record) + set_target_and_inverse(join_part, association, record) else - raise ConfigurationError, "unknown macro: #{join.reflection.macro}" + raise ConfigurationError, "unknown macro: #{join_part.reflection.macro}" end end association end - def set_target_and_inverse(join, association, record) - association_proxy = record.send("set_#{join.reflection.name}_target", association) + def set_target_and_inverse(join_part, association, record) + association_proxy = record.send("set_#{join_part.reflection.name}_target", association) association_proxy.__send__(:set_inverse_instance, association, record) end - - class JoinBase # :nodoc: - attr_reader :active_record, :table_joins - delegate :table_name, :column_names, :primary_key, :reflections, :sanitize_sql, :arel_engine, :to => :active_record - - def initialize(active_record, joins = nil) + + # A JoinPart represents a part of a JoinDependency. It is an abstract class, inherited + # by JoinBase and JoinAssociation. A JoinBase represents the Active Record which + # everything else is being joined onto. A JoinAssociation represents an association which + # is joining to the base. A JoinAssociation may result in more than one actual join + # operations (for example a has_and_belongs_to_many JoinAssociation would result in + # two; one for the join table and one for the target table). + class JoinPart # :nodoc: + # The Active Record class which this join part is associated 'about'; for a JoinBase + # this is the actual base model, for a JoinAssociation this is the target model of the + # association. + attr_reader :active_record + + delegate :table_name, :column_names, :primary_key, :reflections, :sanitize_sql, :arel_engine, :to => :active_record + + def initialize(active_record) @active_record = active_record @cached_record = {} - @table_joins = joins end - + def ==(other) - other.class == self.class && - other.active_record == active_record && - other.table_joins == table_joins + raise NotImplementedError end - + + # An Arel::Table for the active_record + def table + raise NotImplementedError + end + + # The prefix to be used when aliasing columns in the active_record's table def aliased_prefix - "t0" + raise NotImplementedError end - + + # The alias for the active_record's table + def aliased_table_name + raise NotImplementedError + end + + # The alias for the primary key of the active_record's table def aliased_primary_key "#{aliased_prefix}_r0" end - - def aliased_table_name - active_record.table_name - end - + + # An array of [column_name, alias] pairs for the table def column_names_with_alias unless defined?(@column_names_with_alias) @column_names_with_alias = [] @@ -2060,33 +2082,74 @@ module ActiveRecord end end - class JoinAssociation < JoinBase # :nodoc: - attr_reader :reflection, :parent, :aliased_table_name, :aliased_prefix, :aliased_join_table_name, :parent_table_name, :join_class - delegate :options, :klass, :through_reflection, :source_reflection, :to => :reflection + class JoinBase < JoinPart # :nodoc: + # Extra joins provided when the JoinDependency was created + attr_reader :table_joins + + def initialize(active_record, joins = nil) + super(active_record) + @table_joins = joins + end + + def ==(other) + other.class == self.class && + other.active_record == active_record && + other.table_joins == table_joins + end + + def aliased_prefix + "t0" + end + + def table + Arel::Table.new(table_name, :engine => arel_engine, :columns => active_record.columns) + end + + def aliased_table_name + active_record.table_name + end + end + + class JoinAssociation < JoinPart # :nodoc: + # The reflection of the association represented + attr_reader :reflection + + # The JoinDependency object which this JoinAssociation exists within. This is mainly + # relevant for generating aliases which do not conflict with other joins which are + # part of the query. + attr_reader :join_dependency + + # A JoinBase instance representing the active record we are joining onto. + # (So in Author.has_many :posts, the Author would be that base record.) + attr_reader :parent + + # 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 + + delegate :options, :through_reflection, :source_reflection, :to => :reflection + delegate :table, :table_name, :to => :parent, :prefix => true def initialize(reflection, join_dependency, parent = nil) reflection.check_validity! + if reflection.options[:polymorphic] raise EagerLoadPolymorphicError.new(reflection) end super(reflection.klass) - @join_dependency = join_dependency - @parent = parent - @reflection = reflection - @aliased_prefix = "t#{ join_dependency.joins.size }" - @parent_table_name = parent.active_record.table_name - @aliased_table_name = aliased_table_name_for(table_name) - @join = nil - @join_class = Arel::InnerJoin - - if reflection.macro == :has_and_belongs_to_many - @aliased_join_table_name = aliased_table_name_for(reflection.options[:join_table], "_join") - end - - if [: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 + + @reflection = reflection + @join_dependency = join_dependency + @parent = parent + @join_type = Arel::InnerJoin + + # 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 end def ==(other) @@ -2096,63 +2159,29 @@ module ActiveRecord end def find_parent_in(other_join_dependency) - other_join_dependency.joins.detect do |join| - self.parent == join + other_join_dependency.join_parts.detect do |join_part| + self.parent == join_part end end - - def with_join_class(join_class) - @join_class = join_class - self + + def join_to(relation) + send("join_#{reflection.macro}_to", relation) end - def association_join - return @join if @join - - aliased_table = Arel::Table.new(table_name, :as => @aliased_table_name, - :engine => arel_engine, - :columns => klass.columns) - - parent_table = Arel::Table.new(parent.table_name, :as => parent.aliased_table_name, - :engine => arel_engine, - :columns => parent.active_record.columns) - - @join = send("build_#{reflection.macro}", aliased_table, parent_table) - - unless klass.descends_from_active_record? - sti_column = aliased_table[klass.inheritance_column] - sti_condition = sti_column.eq(klass.sti_name) - klass.descendants.each {|subclass| sti_condition = sti_condition.or(sti_column.eq(subclass.sti_name)) } - - @join << sti_condition - end - - [through_reflection, reflection].each do |ref| - if ref && ref.options[:conditions] - @join << interpolate_sql(sanitize_sql(ref.options[:conditions], aliased_table_name)) - end - end - - @join + def join_relation(joining_relation) + self.join_type = Arel::OuterJoin + joining_relation.joins(self) end - - def relation - aliased = Arel::Table.new(table_name, :as => @aliased_table_name, - :engine => arel_engine, - :columns => klass.columns) - - if reflection.macro == :has_and_belongs_to_many - [Arel::Table.new(options[:join_table], :as => aliased_join_table_name, :engine => arel_engine), aliased] - elsif reflection.options[:through] - [Arel::Table.new(through_reflection.klass.table_name, :as => aliased_join_table_name, :engine => arel_engine), aliased] - else - aliased - end - end - - def join_relation(joining_relation, join = nil) - joining_relation.joins(self.with_join_class(Arel::OuterJoin)) + + def table + @table ||= Arel::Table.new( + table_name, :as => aliased_table_name, + :engine => arel_engine, :columns => active_record.columns + ) end + + # More semantic name given we are talking about associations + alias_method :target_table, :table protected @@ -2186,7 +2215,7 @@ module ActiveRecord end def table_name_and_alias - table_alias_for table_name, @aliased_table_name + table_alias_for table_name, aliased_table_name end def interpolate_sql(sql) @@ -2194,74 +2223,169 @@ module ActiveRecord end private - - def build_has_and_belongs_to_many(aliased_table, parent_table) - join_table = Arel::Table.new(options[:join_table], :as => aliased_join_table_name, :engine => arel_engine) - fk = options[:foreign_key] || reflection.active_record.to_s.foreign_key - klass_fk = options[:association_foreign_key] || klass.to_s.foreign_key - - [ - join_table[fk].eq(parent_table[reflection.active_record.primary_key]), - aliased_table[klass.primary_key].eq(join_table[klass_fk]) - ] - end - - def build_has_many(aliased_table, parent_table) - if reflection.options[:through] - join_table = Arel::Table.new(through_reflection.klass.table_name, - :as => aliased_join_table_name, - :engine => arel_engine) - jt_foreign_key = jt_as_extra = jt_source_extra = jt_sti_extra = nil - 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' - jt_as_extra = 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_sti_extra = 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_source_extra = join_table[reflection.source_reflection.options[:foreign_type]].eq(reflection.options[:source_type]) - else - second_key = source_reflection.primary_key_name - end - end - - [ - [parent_table[parent.primary_key].eq(join_table[jt_foreign_key]), jt_as_extra, jt_source_extra, jt_sti_extra].compact, - aliased_table[first_key].eq(join_table[second_key]) - ] - elsif reflection.options[:as] - id_rel = aliased_table["#{reflection.options[:as]}_id"].eq(parent_table[parent.primary_key]) - type_rel = aliased_table["#{reflection.options[:as]}_type"].eq(parent.active_record.base_class.name) - [id_rel, type_rel] - else - foreign_key = options[:foreign_key] || reflection.active_record.name.foreign_key - [aliased_table[foreign_key].eq(parent_table[reflection.options[:primary_key] || parent.primary_key])] + + 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 - alias :build_has_one :build_has_many + + 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] + + 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 + + conditions << sti_condition + end + + # If the reflection has conditions, add them + if options[:conditions] + conditions << process_conditions(options[:conditions], aliased_table_name) + end + + relation = relation.on(*conditions) + end - def build_belongs_to(aliased_table, parent_table) - [aliased_table[options[:primary_key] || reflection.klass.primary_key].eq(parent_table[options[:foreign_key] || reflection.primary_key_name])] + 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]) + ) + 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' + + 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 + + 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 + end + 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 end diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index ede1c8821e..5034caf084 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -343,8 +343,11 @@ module ActiveRecord end def column_aliases(join_dependency) - join_dependency.joins.collect{|join| join.column_names_with_alias.collect{|column_name, aliased_name| - "#{connection.quote_table_name join.aliased_table_name}.#{connection.quote_column_name column_name} AS #{aliased_name}"}}.flatten.join(", ") + join_dependency.join_parts.collect { |join_part| + join_part.column_names_with_alias.collect{ |column_name, aliased_name| + "#{connection.quote_table_name join_part.aliased_table_name}.#{connection.quote_column_name column_name} AS #{aliased_name}" + } + }.flatten.join(", ") end def using_limitable_reflections?(reflections) diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 2e0a2effc2..acc42faf7d 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -230,19 +230,8 @@ module ActiveRecord @implicit_readonly = true unless association_joins.empty? && stashed_association_joins.empty? - to_join = [] - join_dependency.join_associations.each do |association| - if (association_relation = association.relation).is_a?(Array) - to_join << [association_relation.first, association.join_class, association.association_join.first] - to_join << [association_relation.last, association.join_class, association.association_join.last] - else - to_join << [association_relation, association.join_class, association.association_join] - end - end - - to_join.uniq.each do |left, join_class, right| - relation = relation.join(left, join_class).on(*right) + relation = association.join_to(relation) end relation.join(custom_joins) diff --git a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb index 8bb8f3e359..0e9c8a2639 100644 --- a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb +++ b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb @@ -16,7 +16,7 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase assert_equal 3, authors.size assert_equal 5, authors[0].posts.size assert_equal 2, authors[1].posts.size - assert_equal 9, authors[0].posts.collect{|post| post.comments.size }.inject(0){|sum,i| sum+i} + 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 @@ -24,7 +24,7 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase assert_equal 3, authors.size assert_equal 5, authors[0].posts.size assert_equal 2, authors[1].posts.size - assert_equal 9, authors[0].posts.collect{|post| post.comments.size }.inject(0){|sum,i| sum+i} + 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 end @@ -35,7 +35,7 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase end authors = Author.joins(:posts).eager_load(:comments).where(:posts => {:taggings_count => 1}).all assert_equal 1, assert_no_queries { authors.size } - assert_equal 9, assert_no_queries { authors[0].comments.size } + assert_equal 10, assert_no_queries { authors[0].comments.size } end def test_eager_association_loading_grafts_stashed_associations_to_correct_parent @@ -57,7 +57,7 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase assert_equal 3, authors.size assert_equal 5, authors[0].posts.size assert_equal 2, authors[1].posts.size - assert_equal 9, authors[0].posts.collect{|post| post.comments.size }.inject(0){|sum,i| sum+i} + 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 diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index 1669c4d5f4..2ff0714e9f 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -174,7 +174,7 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_eager_association_loading_with_belongs_to comments = Comment.find(:all, :include => :post) - assert_equal 10, comments.length + assert_equal 11, comments.length titles = comments.map { |c| c.post.title } assert titles.include?(posts(:welcome).title) assert titles.include?(posts(:sti_post_and_comments).title) @@ -532,7 +532,7 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_eager_has_many_with_association_inheritance post = Post.find(4, :include => [ :special_comments ]) post.special_comments.each do |special_comment| - assert_equal "SpecialComment", special_comment.class.to_s + assert special_comment.is_a?(SpecialComment) end end @@ -726,8 +726,8 @@ class EagerAssociationTest < ActiveRecord::TestCase posts = assert_queries(2) do Post.find(:all, :joins => :comments, :include => :author, :order => 'comments.id DESC') end - assert_equal posts(:eager_other), posts[0] - assert_equal authors(:mary), assert_no_queries { posts[0].author} + assert_equal posts(:eager_other), posts[1] + assert_equal authors(:mary), assert_no_queries { posts[1].author} end def test_eager_loading_with_conditions_on_joined_table_preloads diff --git a/activerecord/test/cases/associations/inner_join_association_test.rb b/activerecord/test/cases/associations/inner_join_association_test.rb index 4ba867dc7c..780eabc443 100644 --- a/activerecord/test/cases/associations/inner_join_association_test.rb +++ b/activerecord/test/cases/associations/inner_join_association_test.rb @@ -4,9 +4,12 @@ require 'models/comment' require 'models/author' require 'models/category' require 'models/categorization' +require 'models/tagging' +require 'models/tag' class InnerJoinAssociationTest < ActiveRecord::TestCase - fixtures :authors, :posts, :comments, :categories, :categories_posts, :categorizations + fixtures :authors, :posts, :comments, :categories, :categories_posts, :categorizations, + :taggings, :tags def test_construct_finder_sql_applies_aliases_tables_on_association_conditions result = Author.joins(:thinking_posts, :welcome_posts).to_a @@ -62,4 +65,23 @@ class InnerJoinAssociationTest < ActiveRecord::TestCase authors_with_welcoming_post_titles = Author.calculate(:count, 'authors.id', :joins => :posts, :distinct => true, :conditions => "posts.title like 'Welcome%'") assert_equal real_count, authors_with_welcoming_post_titles, "inner join and conditions should have only returned authors posting titles starting with 'Welcome'" end + + def test_find_with_sti_join + scope = Post.joins(:special_comments).where(:id => posts(:sti_comments).id) + + # The join should match SpecialComment and its subclasses only + assert scope.where("comments.type" => "Comment").empty? + assert !scope.where("comments.type" => "SpecialComment").empty? + assert !scope.where("comments.type" => "SubSpecialComment").empty? + end + + def test_find_with_conditions_on_reflection + assert !posts(:welcome).comments.empty? + assert Post.joins(:nonexistant_comments).where(:id => posts(:welcome).id).empty? # [sic!] + end + + def test_find_with_conditions_on_through_reflection + assert !posts(:welcome).tags.empty? + assert Post.joins(:misc_tags).where(:id => posts(:welcome).id).empty? + end end diff --git a/activerecord/test/cases/associations/join_model_test.rb b/activerecord/test/cases/associations/join_model_test.rb index 0b1a3db1e4..4b7a8b494d 100644 --- a/activerecord/test/cases/associations/join_model_test.rb +++ b/activerecord/test/cases/associations/join_model_test.rb @@ -398,7 +398,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase author = Author.find :first, :conditions => ['name = ?', 'David'], :include => :comments, :order => 'comments.id' SpecialComment.new; VerySpecialComment.new assert_no_queries do - assert_equal [1,2,3,5,6,7,8,9,10], author.comments.collect(&:id) + assert_equal [1,2,3,5,6,7,8,9,10,12], author.comments.collect(&:id) end end @@ -500,7 +500,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase def test_has_many_through_collection_size_doesnt_load_target_if_not_loaded author = authors(:david) - assert_equal 9, author.comments.size + assert_equal 10, author.comments.size assert !author.comments.loaded? end diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index e73f58fdc7..0476fc94df 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -259,7 +259,7 @@ class FinderTest < ActiveRecord::TestCase end def test_find_on_association_proxy_conditions - assert_equal [1, 2, 3, 5, 6, 7, 8, 9, 10], Comment.find_all_by_post_id(authors(:david).posts).map(&:id).sort + assert_equal [1, 2, 3, 5, 6, 7, 8, 9, 10, 12], Comment.find_all_by_post_id(authors(:david).posts).map(&:id).sort end def test_find_on_hash_conditions_with_range diff --git a/activerecord/test/fixtures/comments.yml b/activerecord/test/fixtures/comments.yml index 97d77f8b9a..ddbb823c49 100644 --- a/activerecord/test/fixtures/comments.yml +++ b/activerecord/test/fixtures/comments.yml @@ -57,3 +57,9 @@ eager_other_comment1: post_id: 7 body: go crazy type: SpecialComment + +sub_special_comment: + id: 12 + post_id: 4 + body: Sub special comment + type: SubSpecialComment diff --git a/activerecord/test/models/comment.rb b/activerecord/test/models/comment.rb index 9f6e2d3b71..88061b2145 100644 --- a/activerecord/test/models/comment.rb +++ b/activerecord/test/models/comment.rb @@ -23,6 +23,9 @@ class SpecialComment < Comment end end +class SubSpecialComment < SpecialComment +end + class VerySpecialComment < Comment def self.what_are_you 'a very special comment...' From ab5a9335020eff0da35b62b86a62ed8587a4d598 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Sat, 9 Oct 2010 22:00:33 +0100 Subject: [PATCH 009/100] Add support for nested through associations in JoinAssociation. Hence Foo.joins(:bar) will work for through associations. There is some duplicated code now, which will be refactored. --- .../lib/active_record/associations.rb | 188 +++++++++--------- .../associations/through_association_scope.rb | 4 + ...sted_has_many_through_associations_test.rb | 59 +++++- activerecord/test/fixtures/ratings.yml | 14 ++ activerecord/test/models/comment.rb | 1 + activerecord/test/models/job.rb | 2 + activerecord/test/models/person.rb | 3 + activerecord/test/models/post.rb | 2 + activerecord/test/models/rating.rb | 3 + activerecord/test/models/reference.rb | 2 + activerecord/test/schema/schema.rb | 5 + 11 files changed, 182 insertions(+), 101 deletions(-) create mode 100644 activerecord/test/fixtures/ratings.yml create mode 100644 activerecord/test/models/rating.rb diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 67c204f154..688c05c545 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -2129,7 +2129,7 @@ module ActiveRecord # These implement abstract methods from the superclass attr_reader :aliased_prefix, :aliased_table_name - 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 def initialize(reflection, join_dependency, parent = nil) @@ -2185,13 +2185,13 @@ module ActiveRecord protected - def aliased_table_name_for(name, suffix = nil) + def aliased_table_name_for(name, aliased_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}" + name = active_record.connection.table_alias_for "#{aliased_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 @@ -2226,12 +2226,26 @@ module ActiveRecord def allocate_aliases @aliased_prefix = "t#{ join_dependency.join_parts.size }" - @aliased_table_name = aliased_table_name_for(table_name) + @aliased_table_name = aliased_table_name_for(table_name, pluralize(reflection.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") + case reflection.macro + when :has_and_belongs_to_many + @aliased_join_table_name = aliased_table_name_for( + reflection.options[:join_table], + pluralize(reflection.name), "_join" + ) + when :has_many, :has_one + # Add the target table name which was already generated. We don't want to generate + # it again as that would lead to an unnecessary alias. + @aliased_through_table_names = [@aliased_table_name] + + # Generate the rest in the original order + @aliased_through_table_names += through_reflection_chain[1..-1].map do |reflection| + aliased_table_name_for(reflection.table_name, pluralize(reflection.name), "_join") + end + + # Now reverse the list, as we will use it in that order + @aliased_through_table_names.reverse! end end @@ -2284,99 +2298,81 @@ module ActiveRecord 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]) + # Chain usually starts with target, but we want to end with it here (just makes it + # easier to understand the joins that are generated) + chain = through_reflection_chain.reverse + + foreign_table = parent_table + + chain.zip(@aliased_through_table_names).each do |reflection, aliased_table_name| + table = Arel::Table.new( + reflection.table_name, :engine => arel_engine, + :as => aliased_table_name, :columns => reflection.klass.columns ) + + 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 + + if reflection.options[:as] + conditions << + table["#{reflection.options[:as]}_type"]. + eq(reflection.active_record.base_class.name) + end + end + elsif reflection.source_reflection.macro == :belongs_to + key = reflection.klass.primary_key + foreign_key = reflection.source_reflection.primary_key_name + + if reflection.options[:source_type] + conditions << + foreign_table[reflection.source_reflection.options[:foreign_type]]. + eq(reflection.options[:source_type]) + end + else + key = reflection.source_reflection.primary_key_name + foreign_key = reflection.source_reflection.klass.primary_key + end + + conditions << table[key].eq(foreign_table[foreign_key]) + + if reflection.options[:conditions] + conditions << process_conditions(reflection.options[:conditions], aliased_table_name) + 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 reflection.klass.descends_from_active_record? + sti_column = table[reflection.klass.inheritance_column] + + sti_condition = sti_column.eq(reflection.klass.sti_name) + reflection.klass.descendants.each do |subclass| + sti_condition = sti_condition.or(sti_column.eq(subclass.sti_name)) + end + + conditions << sti_condition + end + + relation = relation.join(table, join_type).on(*conditions) + + # The current table in this iteration becomes the foreign table in the next + foreign_table = table end + + relation 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' - - 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 - - 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 - end - 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 diff --git a/activerecord/lib/active_record/associations/through_association_scope.rb b/activerecord/lib/active_record/associations/through_association_scope.rb index 90ebadda89..8406f5fd20 100644 --- a/activerecord/lib/active_record/associations/through_association_scope.rb +++ b/activerecord/lib/active_record/associations/through_association_scope.rb @@ -73,6 +73,8 @@ module ActiveRecord if left.options[:as] polymorphic_join = "AND %s.%s = %s" % [ table_aliases[left], "#{left.options[:as]}_type", + # TODO: Why right.klass.name? Rather than left.active_record.name? + # TODO: Also should maybe use the base_class (see related code in JoinAssociation) @owner.class.quote_value(right.klass.name) ] end @@ -117,6 +119,8 @@ module ActiveRecord joins.join(" ") end + # TODO: Use the same aliasing strategy (and code?) as JoinAssociation (as this is the + # documented behaviour) def table_aliases @table_aliases ||= begin tally = {} 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 index a5d3f27702..ba75b70941 100644 --- a/activerecord/test/cases/associations/nested_has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/nested_has_many_through_associations_test.rb @@ -17,18 +17,37 @@ require 'models/developer' require 'models/subscriber' require 'models/book' require 'models/subscription' +require 'models/rating' + +# 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 + fixtures :authors, :books, :posts, :subscriptions, :subscribers, :tags, :taggings, + :people, :readers, :references, :jobs, :ratings, :comments def test_has_many_through_a_has_many_through_association_on_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 + + # This ensures that the polymorphism of taggings is being observed correctly + authors = Author.joins(:tags).where('taggings.taggable_type' => 'FakeModel') + assert authors.empty? end def test_has_many_through_a_has_many_through_association_on_through_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 end def test_distinct_has_many_through_a_has_many_through_association_on_source_reflection @@ -44,11 +63,41 @@ class NestedHasManyThroughAssociationsTest < ActiveRecord::TestCase 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_nested_has_many_through_as_a_join - # 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 + 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/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/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/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 dbd5da45eb..2fa9a4521e 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -449,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 From 3aba73fed1afc93cf64f9da90d5ad2d51c99df9a Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Sun, 10 Oct 2010 00:53:04 +0100 Subject: [PATCH 010/100] Refactoring to remove duplication introduced by the last commit --- .../lib/active_record/associations.rb | 313 +++++++++--------- 1 file changed, 149 insertions(+), 164 deletions(-) diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 688c05c545..397159d35e 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -2126,8 +2126,7 @@ 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, :through_reflection_chain, :to => :reflection delegate :table, :table_name, :to => :parent, :prefix => true @@ -2141,15 +2140,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) @@ -2165,7 +2162,71 @@ 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 + + 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 + + key = reflection.klass.primary_key + foreign_key = reflection.options[:association_foreign_key] || + reflection.klass.to_s.foreign_key + end + elsif reflection.source_reflection.macro == :belongs_to + key = reflection.klass.primary_key + foreign_key = reflection.source_reflection.primary_key_name + + conditions << source_type_conditions(reflection, foreign_table) + else + key = reflection.source_reflection.primary_key_name + foreign_key = reflection.source_reflection.klass.primary_key + 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) @@ -2174,15 +2235,17 @@ 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, aliased_name, suffix = nil) @@ -2224,167 +2287,89 @@ module ActiveRecord private - def allocate_aliases - @aliased_prefix = "t#{ join_dependency.join_parts.size }" - @aliased_table_name = aliased_table_name_for(table_name, pluralize(reflection.name)) - - case reflection.macro - when :has_and_belongs_to_many - @aliased_join_table_name = aliased_table_name_for( - reflection.options[:join_table], - pluralize(reflection.name), "_join" - ) - when :has_many, :has_one - # Add the target table name which was already generated. We don't want to generate - # it again as that would lead to an unnecessary alias. - @aliased_through_table_names = [@aliased_table_name] - - # Generate the rest in the original order - @aliased_through_table_names += through_reflection_chain[1..-1].map do |reflection| - aliased_table_name_for(reflection.table_name, pluralize(reflection.name), "_join") - end - - # Now reverse the list, as we will use it in that order - @aliased_through_table_names.reverse! - 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| + suffix = reflection == self.reflection ? nil : '_join' - 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 + aliased_table_name = aliased_table_name_for( + reflection.table_name, + pluralize(reflection.name), + suffix + ) - conditions << sti_condition - end - - # If the reflection has conditions, add them - if options[:conditions] - conditions << process_conditions(options[:conditions], aliased_table_name) - end - - relation = relation.on(*conditions) - 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) - # Chain usually starts with target, but we want to end with it here (just makes it - # easier to understand the joins that are generated) - chain = through_reflection_chain.reverse - - foreign_table = parent_table - - chain.zip(@aliased_through_table_names).each do |reflection, aliased_table_name| table = Arel::Table.new( reflection.table_name, :engine => arel_engine, :as => aliased_table_name, :columns => reflection.klass.columns ) - 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 - - if reflection.options[:as] - conditions << - table["#{reflection.options[:as]}_type"]. - eq(reflection.active_record.base_class.name) - end - end - elsif reflection.source_reflection.macro == :belongs_to - key = reflection.klass.primary_key - foreign_key = reflection.source_reflection.primary_key_name + # 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 + aliased_join_table_name = aliased_table_name_for( + reflection.options[:join_table], + pluralize(reflection.name), "_join" + ) - if reflection.options[:source_type] - conditions << - foreign_table[reflection.source_reflection.options[:foreign_type]]. - eq(reflection.options[:source_type]) - end + join_table = Arel::Table.new( + reflection.options[:join_table], :engine => arel_engine, + :as => aliased_join_table_name + ) + + [table, join_table] else - key = reflection.source_reflection.primary_key_name - foreign_key = reflection.source_reflection.klass.primary_key + table end - - conditions << table[key].eq(foreign_table[foreign_key]) - - if reflection.options[:conditions] - conditions << process_conditions(reflection.options[:conditions], aliased_table_name) - 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 reflection.klass.descends_from_active_record? - sti_column = table[reflection.klass.inheritance_column] - - sti_condition = sti_column.eq(reflection.klass.sti_name) - reflection.klass.descendants.each do |subclass| - sti_condition = sti_condition.or(sti_column.eq(subclass.sti_name)) - end - - conditions << sti_condition - end - - relation = relation.join(table, join_type).on(*conditions) - - # The current table in this iteration becomes the foreign table in the next - foreign_table = table end - relation - end - alias :join_has_one_to :join_has_many_to - - def join_belongs_to_to(relation) - foreign_key = options[:foreign_key] || reflection.primary_key_name - primary_key = options[:primary_key] || reflection.klass.primary_key + # 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! - join_target_table( - relation, - target_table[primary_key].eq(parent_table[foreign_key]) - ) + @tables end - end + + 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 + + def sti_conditions(reflection, table) + unless reflection.klass.descends_from_active_record? + sti_column = table[reflection.klass.inheritance_column] + + condition = sti_column.eq(reflection.klass.sti_name) + + reflection.klass.descendants.each do |subclass| + condition = condition.or(sti_column.eq(subclass.sti_name)) + end + + condition + 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 From 7aea695815821df332913bae4b47714a525009a3 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Tue, 12 Oct 2010 13:33:51 +0100 Subject: [PATCH 011/100] A load of tests that need to be written --- ...sted_has_many_through_associations_test.rb | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) 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 index ba75b70941..8a4ab627bb 100644 --- a/activerecord/test/cases/associations/nested_has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/nested_has_many_through_associations_test.rb @@ -28,6 +28,26 @@ class NestedHasManyThroughAssociationsTest < ActiveRecord::TestCase fixtures :authors, :books, :posts, :subscriptions, :subscribers, :tags, :taggings, :people, :readers, :references, :jobs, :ratings, :comments + # 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_a_has_many_through_association_on_source_reflection author = authors(:david) assert_equal [tags(:general), tags(:general)], author.tags @@ -41,6 +61,9 @@ class NestedHasManyThroughAssociationsTest < ActiveRecord::TestCase assert authors.empty? end + # has_many through + # Source: has_many + # Through: has_many through def test_has_many_through_a_has_many_through_association_on_through_reflection author = authors(:david) assert_equal [subscribers(:first), subscribers(:second), subscribers(:second)], author.subscribers @@ -49,6 +72,46 @@ class NestedHasManyThroughAssociationsTest < ActiveRecord::TestCase authors = Author.joins(:subscribers).where('subscribers.nick' => 'alterself') assert_equal [authors(:david)], authors end + + # TODO: has_many through + # Source: has_one through + # Through: has_one + + # TODO: has_many through + # Source: has_one + # Through: has_one through + + # TODO: has_many through + # Source: has_many through + # Through: has_one + + # TODO: has_many through + # Source: has_many + # Through: has_one through + + # TODO: has_many through + # Source: has_and_belongs_to_many + # Through: has_many + + # TODO: has_many through + # Source: has_many + # Through: has_and_belongs_to_many + + # TODO: has_many through + # Source: belongs_to + # Through: has_many through + + # TODO: has_many through + # Source: has_many through + # Through: belongs_to + + # TODO: has_one through + # Source: has_one through + # Through: has_one + + # 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) From 1777600e6e11e553ad97b7bc89e4b19e992eb3d3 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Tue, 12 Oct 2010 16:40:24 +0100 Subject: [PATCH 012/100] Support has_one through assocs as the source association --- .../associations/through_association_scope.rb | 31 ++++++++++------ ...sted_has_many_through_associations_test.rb | 35 +++++++++++++++++-- activerecord/test/fixtures/member_details.yml | 3 ++ activerecord/test/fixtures/members.yml | 2 ++ activerecord/test/fixtures/memberships.yml | 6 ++-- activerecord/test/fixtures/sponsors.yml | 9 +++-- activerecord/test/models/member.rb | 5 ++- 7 files changed, 71 insertions(+), 20 deletions(-) create mode 100644 activerecord/test/fixtures/member_details.yml diff --git a/activerecord/lib/active_record/associations/through_association_scope.rb b/activerecord/lib/active_record/associations/through_association_scope.rb index 8406f5fd20..81e29f047b 100644 --- a/activerecord/lib/active_record/associations/through_association_scope.rb +++ b/activerecord/lib/active_record/associations/through_association_scope.rb @@ -53,7 +53,7 @@ module ActiveRecord end def construct_joins(custom_joins = nil) - # puts @reflection.through_reflection_chain.map(&:inspect) + # p @reflection.through_reflection_chain "#{construct_through_joins} #{@reflection.options[:joins]} #{custom_joins}" end @@ -67,16 +67,27 @@ module ActiveRecord case when left.source_reflection.nil? - left_primary_key = left.primary_key_name - right_primary_key = right.klass.primary_key + # TODO: Perhaps need to pay attention to left.options[:primary_key] and + # left.options[:foreign_key] in places here - if left.options[:as] - polymorphic_join = "AND %s.%s = %s" % [ - table_aliases[left], "#{left.options[:as]}_type", - # TODO: Why right.klass.name? Rather than left.active_record.name? - # TODO: Also should maybe use the base_class (see related code in JoinAssociation) - @owner.class.quote_value(right.klass.name) - ] + case left.macro + when :belongs_to + left_primary_key = left.klass.primary_key + right_primary_key = right.primary_key_name + when :has_many, :has_one + left_primary_key = left.primary_key_name + right_primary_key = right.klass.primary_key + + if left.options[:as] + polymorphic_join = "AND %s.%s = %s" % [ + table_aliases[left], "#{left.options[:as]}_type", + # TODO: Why right.klass.name? Rather than left.active_record.name? + # TODO: Also should maybe use the base_class (see related code in JoinAssociation) + @owner.class.quote_value(right.klass.name) + ] + end + when :has_and_belongs_to_many + raise NotImplementedError end when left.source_reflection.macro == :belongs_to left_primary_key = left.klass.primary_key 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 index 8a4ab627bb..4fab426696 100644 --- a/activerecord/test/cases/associations/nested_has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/nested_has_many_through_associations_test.rb @@ -18,6 +18,9 @@ require 'models/subscriber' require 'models/book' require 'models/subscription' require 'models/rating' +require 'models/member' +require 'models/member_detail' +require 'models/member_type' # 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 @@ -26,7 +29,8 @@ require 'models/rating' class NestedHasManyThroughAssociationsTest < ActiveRecord::TestCase fixtures :authors, :books, :posts, :subscriptions, :subscribers, :tags, :taggings, - :people, :readers, :references, :jobs, :ratings, :comments + :people, :readers, :references, :jobs, :ratings, :comments, :members, :member_details, + :member_types # Through associations can either use the has_many or has_one macros. # @@ -56,6 +60,9 @@ class NestedHasManyThroughAssociationsTest < ActiveRecord::TestCase 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? @@ -71,11 +78,24 @@ class NestedHasManyThroughAssociationsTest < ActiveRecord::TestCase # 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 - # TODO: has_many through + # 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 # TODO: has_many through # Source: has_one @@ -105,9 +125,18 @@ class NestedHasManyThroughAssociationsTest < ActiveRecord::TestCase # Source: has_many through # Through: belongs_to - # TODO: has_one through + # 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 diff --git a/activerecord/test/fixtures/member_details.yml b/activerecord/test/fixtures/member_details.yml new file mode 100644 index 0000000000..020932246a --- /dev/null +++ b/activerecord/test/fixtures/member_details.yml @@ -0,0 +1,3 @@ +groucho: + id: 1 + member_id: 1 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/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/models/member.rb b/activerecord/test/models/member.rb index 255fb569d7..b8b22d0fde 100644 --- a/activerecord/test/models/member.rb +++ b/activerecord/test/models/member.rb @@ -9,4 +9,7 @@ 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 +end From 6a016a551109ed2ef78fff8f74aef6b1f4ae96a9 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Tue, 12 Oct 2010 16:53:22 +0100 Subject: [PATCH 013/100] Add test_has_many_through_has_one_through --- .../nested_has_many_through_associations_test.rb | 16 ++++++++++++++-- activerecord/test/models/member.rb | 3 +++ 2 files changed, 17 insertions(+), 2 deletions(-) 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 index 4fab426696..c1e8a4b1bf 100644 --- a/activerecord/test/cases/associations/nested_has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/nested_has_many_through_associations_test.rb @@ -21,6 +21,8 @@ require 'models/rating' require 'models/member' require 'models/member_detail' require 'models/member_type' +require 'models/sponsor' +require 'models/club' # 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 @@ -30,7 +32,7 @@ require 'models/member_type' class NestedHasManyThroughAssociationsTest < ActiveRecord::TestCase fixtures :authors, :books, :posts, :subscriptions, :subscribers, :tags, :taggings, :people, :readers, :references, :jobs, :ratings, :comments, :members, :member_details, - :member_types + :member_types, :sponsors, :clubs # Through associations can either use the has_many or has_one macros. # @@ -97,9 +99,19 @@ class NestedHasManyThroughAssociationsTest < ActiveRecord::TestCase assert_equal [member_types(:founding)], members.first.nested_member_types end - # TODO: has_many through + # has_many through # Source: has_one # Through: has_one through + def test_has_many_through_has_one_through + 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 # TODO: has_many through # Source: has_many through diff --git a/activerecord/test/models/member.rb b/activerecord/test/models/member.rb index b8b22d0fde..c2dd9efe3b 100644 --- a/activerecord/test/models/member.rb +++ b/activerecord/test/models/member.rb @@ -12,4 +12,7 @@ class Member < ActiveRecord::Base 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 end From 61073861856110b4a842a4d5e1033698fd52901f Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Tue, 12 Oct 2010 16:54:43 +0100 Subject: [PATCH 014/100] Rename some tests for consistency --- .../nested_has_many_through_associations_test.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 index c1e8a4b1bf..bc0fb8582d 100644 --- a/activerecord/test/cases/associations/nested_has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/nested_has_many_through_associations_test.rb @@ -54,7 +54,7 @@ class NestedHasManyThroughAssociationsTest < ActiveRecord::TestCase # has_many through # Source: has_many through # Through: has_many - def test_has_many_through_a_has_many_through_association_on_source_reflection + def test_has_many_through_has_many_with_has_many_through_source_reflection author = authors(:david) assert_equal [tags(:general), tags(:general)], author.tags @@ -73,7 +73,7 @@ class NestedHasManyThroughAssociationsTest < ActiveRecord::TestCase # has_many through # Source: has_many # Through: has_many through - def test_has_many_through_a_has_many_through_association_on_through_reflection + 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 @@ -102,7 +102,7 @@ class NestedHasManyThroughAssociationsTest < ActiveRecord::TestCase # has_many through # Source: has_one # Through: has_one through - def test_has_many_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) From dc39aceb94fa810f8d7e263c0293f325fbf9a109 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Tue, 12 Oct 2010 17:27:10 +0100 Subject: [PATCH 015/100] Adding test_has_many_through_has_one_with_has_many_through_source_reflection and modifying ThroughAssociationScope to make it work correctly. --- .../associations/through_association_scope.rb | 2 +- ...sted_has_many_through_associations_test.rb | 21 +++++++++++++++++-- activerecord/test/fixtures/member_details.yml | 5 +++++ activerecord/test/models/member.rb | 2 ++ activerecord/test/models/member_detail.rb | 2 ++ 5 files changed, 29 insertions(+), 3 deletions(-) diff --git a/activerecord/lib/active_record/associations/through_association_scope.rb b/activerecord/lib/active_record/associations/through_association_scope.rb index 81e29f047b..09f92332cf 100644 --- a/activerecord/lib/active_record/associations/through_association_scope.rb +++ b/activerecord/lib/active_record/associations/through_association_scope.rb @@ -73,7 +73,7 @@ module ActiveRecord case left.macro when :belongs_to left_primary_key = left.klass.primary_key - right_primary_key = right.primary_key_name + right_primary_key = left.primary_key_name when :has_many, :has_one left_primary_key = left.primary_key_name right_primary_key = right.klass.primary_key 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 index bc0fb8582d..4b5ce6313a 100644 --- a/activerecord/test/cases/associations/nested_has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/nested_has_many_through_associations_test.rb @@ -23,6 +23,7 @@ require 'models/member_detail' require 'models/member_type' require 'models/sponsor' require 'models/club' +require 'models/organization' # 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 @@ -32,7 +33,7 @@ require 'models/club' 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 + :member_types, :sponsors, :clubs, :organizations # Through associations can either use the has_many or has_one macros. # @@ -113,9 +114,25 @@ class NestedHasManyThroughAssociationsTest < ActiveRecord::TestCase # assert_equal [sponsors(:moustache_club_sponsor_for_groucho)], members.first.nested_sponsors end - # TODO: has_many through + # 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 # TODO: has_many through # Source: has_many diff --git a/activerecord/test/fixtures/member_details.yml b/activerecord/test/fixtures/member_details.yml index 020932246a..e1fe695a9b 100644 --- a/activerecord/test/fixtures/member_details.yml +++ b/activerecord/test/fixtures/member_details.yml @@ -1,3 +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/models/member.rb b/activerecord/test/models/member.rb index c2dd9efe3b..1113ef3e28 100644 --- a/activerecord/test/models/member.rb +++ b/activerecord/test/models/member.rb @@ -15,4 +15,6 @@ class Member < ActiveRecord::Base 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 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 From 56064aa4b014233ae54413628679b7f7fa5d6f77 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Tue, 12 Oct 2010 17:32:52 +0100 Subject: [PATCH 016/100] Add test_has_many_through_has_one_through_with_has_many_source_reflection --- ...sted_has_many_through_associations_test.rb | 19 ++++++++++++++++++- activerecord/test/models/member.rb | 1 + 2 files changed, 19 insertions(+), 1 deletion(-) 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 index 4b5ce6313a..7c4bffaddd 100644 --- a/activerecord/test/cases/associations/nested_has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/nested_has_many_through_associations_test.rb @@ -134,9 +134,26 @@ class NestedHasManyThroughAssociationsTest < ActiveRecord::TestCase members.first.organization_member_details end - # TODO: has_many through + # 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 # TODO: has_many through # Source: has_and_belongs_to_many diff --git a/activerecord/test/models/member.rb b/activerecord/test/models/member.rb index 1113ef3e28..44c10cc4a4 100644 --- a/activerecord/test/models/member.rb +++ b/activerecord/test/models/member.rb @@ -17,4 +17,5 @@ class Member < ActiveRecord::Base 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 From c37a5e7acde436b359043a67b7daace8be6f08c6 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Tue, 12 Oct 2010 18:16:31 +0100 Subject: [PATCH 017/100] Add a commented, failing test for using a habtm in a has many through association. I want to refactor how aliasing works first. --- .../associations/through_association_scope.rb | 96 ++++++++++--------- ...s_and_belongs_to_many_associations_test.rb | 8 +- ...sted_has_many_through_associations_test.rb | 9 +- .../test/fixtures/categories_posts.yml | 8 ++ activerecord/test/models/author.rb | 8 +- 5 files changed, 74 insertions(+), 55 deletions(-) diff --git a/activerecord/lib/active_record/associations/through_association_scope.rb b/activerecord/lib/active_record/associations/through_association_scope.rb index 09f92332cf..8c5b95439e 100644 --- a/activerecord/lib/active_record/associations/through_association_scope.rb +++ b/activerecord/lib/active_record/associations/through_association_scope.rb @@ -65,52 +65,56 @@ module ActiveRecord @reflection.through_reflection_chain.each_cons(2) do |left, right| polymorphic_join = nil - case - when 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 - left_primary_key = left.klass.primary_key - right_primary_key = left.primary_key_name - when :has_many, :has_one - left_primary_key = left.primary_key_name - right_primary_key = right.klass.primary_key - - if left.options[:as] - polymorphic_join = "AND %s.%s = %s" % [ - table_aliases[left], "#{left.options[:as]}_type", - # TODO: Why right.klass.name? Rather than left.active_record.name? - # TODO: Also should maybe use the base_class (see related code in JoinAssociation) - @owner.class.quote_value(right.klass.name) - ] - end - when :has_and_belongs_to_many - raise NotImplementedError - end - when left.source_reflection.macro == :belongs_to - left_primary_key = left.klass.primary_key - right_primary_key = left.source_reflection.primary_key_name - - if left.options[:source_type] - polymorphic_join = "AND %s.%s = %s" % [ - table_aliases[right], - left.source_reflection.options[:foreign_type].to_s, - @owner.class.quote_value(left.options[:source_type]) - ] - end - else - left_primary_key = left.source_reflection.primary_key_name - right_primary_key = right.klass.primary_key - - if left.source_reflection.options[:as] - polymorphic_join = "AND %s.%s = %s" % [ - table_aliases[left], - "#{left.source_reflection.options[:as]}_type", - @owner.class.quote_value(right.klass.name) - ] - end + 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 + left_primary_key = left.klass.primary_key + right_primary_key = left.primary_key_name + when :has_many, :has_one + left_primary_key = left.primary_key_name + right_primary_key = right.klass.primary_key + + if left.options[:as] + polymorphic_join = "AND %s.%s = %s" % [ + table_aliases[left], "#{left.options[:as]}_type", + # TODO: Why right.klass.name? Rather than left.active_record.name? + # TODO: Also should maybe use the base_class (see related code in JoinAssociation) + @owner.class.quote_value(right.klass.name) + ] + end + when :has_and_belongs_to_many + raise NotImplementedError + end + else + case left.source_reflection.macro + when :belongs_to + left_primary_key = left.klass.primary_key + right_primary_key = left.source_reflection.primary_key_name + + if left.options[:source_type] + polymorphic_join = "AND %s.%s = %s" % [ + table_aliases[right], + left.source_reflection.options[:foreign_type].to_s, + @owner.class.quote_value(left.options[:source_type]) + ] + end + when :has_many, :has_one + left_primary_key = left.source_reflection.primary_key_name + right_primary_key = right.klass.primary_key + + if left.source_reflection.options[:as] + polymorphic_join = "AND %s.%s = %s" % [ + table_aliases[left], + "#{left.source_reflection.options[:as]}_type", + @owner.class.quote_value(right.klass.name) + ] + end + when :has_and_belongs_to_many + raise NotImplementedError + end end if right.quoted_table_name == table_aliases[right] 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..e67cbcc1a8 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,13 +713,13 @@ 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 1, categories(:technology).posts_grouped_by_title.size + assert_equal 5, categories(:general).posts_grouped_by_title.size + assert_equal 2, categories(:technology).posts_grouped_by_title.size end def test_find_scoped_grouped_having 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 index 7c4bffaddd..835a573978 100644 --- a/activerecord/test/cases/associations/nested_has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/nested_has_many_through_associations_test.rb @@ -24,6 +24,7 @@ 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 @@ -33,7 +34,7 @@ require 'models/organization' 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 + :member_types, :sponsors, :clubs, :organizations, :categories, :categories_posts # Through associations can either use the has_many or has_one macros. # @@ -155,9 +156,13 @@ class NestedHasManyThroughAssociationsTest < ActiveRecord::TestCase # members.first.organization_member_details_2 end - # TODO: has_many through + # has_many through # Source: has_and_belongs_to_many # Through: has_many + # TODO: Enable and implement this, and finish off the test + # def test_has_many_through_has_many_with_has_and_belongs_to_many_source_reflection + # assert_equal [categories(:general), categories(:technology)], authors(:bob).post_categories + # end # TODO: has_many through # Source: has_many diff --git a/activerecord/test/fixtures/categories_posts.yml b/activerecord/test/fixtures/categories_posts.yml index 9b67ab4fa4..3b41510cb1 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 + +technology_misc_by_bob: + category_id: 2 + post_id: 8 diff --git a/activerecord/test/models/author.rb b/activerecord/test/models/author.rb index 1fbd729b60..584164f19a 100644 --- a/activerecord/test/models/author.rb +++ b/activerecord/test/models/author.rb @@ -83,9 +83,9 @@ 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 # through polymorphic has_many - has_many :tags, :through => :posts # through has_many :through (on source reflection + polymorphic) + 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 @@ -100,6 +100,8 @@ class Author < ActiveRecord::Base 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) From e8874318b7a025ffd30df1a53c403eb9d8912c9f Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Tue, 12 Oct 2010 19:49:32 +0100 Subject: [PATCH 018/100] Extract aliasing code from JoinDependency and JoinAssociation into a separate AliasTracker class. This can then be used by ThroughAssociationScope as well. --- .../lib/active_record/associations.rb | 56 ++++----------- .../associations/alias_tracker.rb | 68 +++++++++++++++++++ 2 files changed, 83 insertions(+), 41 deletions(-) create mode 100644 activerecord/lib/active_record/associations/alias_tracker.rb diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 397159d35e..d0d1eeec45 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -114,6 +114,7 @@ module ActiveRecord 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: @@ -1834,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)] @@ -1842,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 @@ -1863,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) @@ -2130,6 +2120,7 @@ module ActiveRecord 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! @@ -2248,24 +2239,10 @@ module ActiveRecord protected - def aliased_table_name_for(name, aliased_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 "#{aliased_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) + name = pluralize(reflection.name) + name << "_#{parent_table_name}" + name << "_join" if reflection != self.reflection name end @@ -2273,12 +2250,12 @@ module ActiveRecord ActiveRecord::Base.pluralize_table_names ? table_name.to_s.pluralize : table_name end - def table_alias_for(table_name, table_alias) + def table_name_and_alias_for(table_name, table_alias) "#{table_name} #{table_alias if table_name != table_alias}".strip end def table_name_and_alias - table_alias_for table_name, aliased_table_name + table_name_and_alias_for(table_name, aliased_table_name) end def interpolate_sql(sql) @@ -2292,12 +2269,9 @@ module ActiveRecord # the proper alias. def setup_tables @tables = through_reflection_chain.map do |reflection| - suffix = reflection == self.reflection ? nil : '_join' - - aliased_table_name = aliased_table_name_for( + aliased_table_name = alias_tracker.aliased_name_for( reflection.table_name, - pluralize(reflection.name), - suffix + table_alias_for(reflection) ) table = Arel::Table.new( @@ -2308,9 +2282,9 @@ module ActiveRecord # 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 - aliased_join_table_name = aliased_table_name_for( + aliased_join_table_name = alias_tracker.aliased_name_for( reflection.options[:join_table], - pluralize(reflection.name), "_join" + table_alias_for(reflection) ) join_table = Arel::Table.new( 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..f48efabec2 --- /dev/null +++ b/activerecord/lib/active_record/associations/alias_tracker.rb @@ -0,0 +1,68 @@ +require 'active_support/core_ext/string/conversions' + +module ActiveRecord + module Associations + # Keeps track of table aliases for ActiveRecord::Associations::ClassMethods::JoinDependency + 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) + @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 + + 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 From 3f2e25805d56440a4ef2a7a9ae6b99be04e6357b Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Tue, 12 Oct 2010 23:42:30 +0100 Subject: [PATCH 019/100] Some small tweaks on the last commit --- .../lib/active_record/associations.rb | 22 +++++-------------- .../associations/alias_tracker.rb | 9 ++++++-- 2 files changed, 12 insertions(+), 19 deletions(-) diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index d0d1eeec45..41f882743c 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -2239,25 +2239,13 @@ module ActiveRecord protected - def table_alias_for(reflection) - name = pluralize(reflection.name) + def table_alias_for(reflection, join = false) + name = alias_tracker.pluralize(reflection.name) name << "_#{parent_table_name}" - name << "_join" if reflection != self.reflection + name << "_join" if join name end - def pluralize(table_name) - ActiveRecord::Base.pluralize_table_names ? table_name.to_s.pluralize : table_name - end - - def table_name_and_alias_for(table_name, table_alias) - "#{table_name} #{table_alias if table_name != table_alias}".strip - end - - def table_name_and_alias - table_name_and_alias_for(table_name, aliased_table_name) - end - def interpolate_sql(sql) instance_eval("%@#{sql.gsub('@', '\@')}@", __FILE__, __LINE__) end @@ -2271,7 +2259,7 @@ module ActiveRecord @tables = through_reflection_chain.map do |reflection| aliased_table_name = alias_tracker.aliased_name_for( reflection.table_name, - table_alias_for(reflection) + table_alias_for(reflection, reflection != self.reflection) ) table = Arel::Table.new( @@ -2284,7 +2272,7 @@ module ActiveRecord if reflection.macro == :has_and_belongs_to_many aliased_join_table_name = alias_tracker.aliased_name_for( reflection.options[:join_table], - table_alias_for(reflection) + table_alias_for(reflection, true) ) join_table = Arel::Table.new( diff --git a/activerecord/lib/active_record/associations/alias_tracker.rb b/activerecord/lib/active_record/associations/alias_tracker.rb index f48efabec2..10e90ec117 100644 --- a/activerecord/lib/active_record/associations/alias_tracker.rb +++ b/activerecord/lib/active_record/associations/alias_tracker.rb @@ -2,11 +2,12 @@ require 'active_support/core_ext/string/conversions' module ActiveRecord module Associations - # Keeps track of table aliases for ActiveRecord::Associations::ClassMethods::JoinDependency + # 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) + def initialize(other_sql = nil) @aliases = Hash.new @other_sql = other_sql.to_s.downcase end @@ -36,6 +37,10 @@ module ActiveRecord end end end + + def pluralize(table_name) + ActiveRecord::Base.pluralize_table_names ? table_name.to_s.pluralize : table_name + end private From 199db8c8c006a5f3bcbbe2a32d39444a741c5843 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Wed, 13 Oct 2010 00:05:04 +0100 Subject: [PATCH 020/100] Hook ThroughAssociationScope up to use the AliasTracker class --- .../associations/through_association_scope.rb | 39 ++++++++++++++----- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/activerecord/lib/active_record/associations/through_association_scope.rb b/activerecord/lib/active_record/associations/through_association_scope.rb index 8c5b95439e..d73f35c2db 100644 --- a/activerecord/lib/active_record/associations/through_association_scope.rb +++ b/activerecord/lib/active_record/associations/through_association_scope.rb @@ -20,6 +20,7 @@ module ActiveRecord end # Build SQL conditions from attributes, qualified by table name. + # TODO: Conditions on joins def construct_conditions reflection = @reflection.through_reflection_chain.last conditions = construct_quoted_owner_attributes(reflection).map do |attr, value| @@ -134,24 +135,44 @@ module ActiveRecord joins.join(" ") end - # TODO: Use the same aliasing strategy (and code?) as JoinAssociation (as this is the - # documented behaviour) + def alias_tracker + @alias_tracker ||= AliasTracker.new + end + def table_aliases @table_aliases ||= begin - tally = {} @reflection.through_reflection_chain.inject({}) do |aliases, reflection| - if tally[reflection.table_name].nil? - tally[reflection.table_name] = 1 - aliases[reflection] = reflection.quoted_table_name + 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 + join_table_alias = quote_table_name(alias_tracker.aliased_name_for( + reflection.options[:join_table], + table_alias_for(reflection, true) + )) + + aliases[reflection] = [table_alias, join_table_alias] else - tally[reflection.table_name] += 1 - aliased_table_name = reflection.table_name + "_#{tally[reflection.table_name]}" - aliases[reflection] = reflection.klass.connection.quote_table_name(aliased_table_name) + 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 # Construct attributes for associate pointing to owner. def construct_owner_attributes(reflection) From 781ad0f8fee209bcf10c5e52daae246477d49ea7 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Wed, 13 Oct 2010 01:29:09 +0100 Subject: [PATCH 021/100] First bit of support for habtm in through assocs - test_has_many_through_has_many_with_has_and_belongs_to_many_source_reflection now passes --- .../lib/active_record/associations.rb | 45 ++++++++++++++----- .../associations/through_association_scope.rb | 45 +++++++++++++------ ...s_and_belongs_to_many_associations_test.rb | 2 +- ...sted_has_many_through_associations_test.rb | 13 ++++-- activerecord/test/fixtures/categories.yml | 5 +++ .../test/fixtures/categories_posts.yml | 4 +- 6 files changed, 84 insertions(+), 30 deletions(-) diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 41f882743c..2a72fa95c9 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -2180,6 +2180,7 @@ module ActiveRecord # 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 @@ -2192,18 +2193,37 @@ module ActiveRecord # 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 - elsif reflection.source_reflection.macro == :belongs_to - key = reflection.klass.primary_key - foreign_key = reflection.source_reflection.primary_key_name - - conditions << source_type_conditions(reflection, foreign_table) else - key = reflection.source_reflection.primary_key_name - foreign_key = reflection.source_reflection.klass.primary_key + 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]) @@ -2269,14 +2289,19 @@ module ActiveRecord # 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 + 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( - reflection.options[:join_table], + join_table_name, table_alias_for(reflection, true) ) join_table = Arel::Table.new( - reflection.options[:join_table], :engine => arel_engine, + join_table_name, :engine => arel_engine, :as => aliased_join_table_name ) diff --git a/activerecord/lib/active_record/associations/through_association_scope.rb b/activerecord/lib/active_record/associations/through_association_scope.rb index d73f35c2db..6cc2fe2559 100644 --- a/activerecord/lib/active_record/associations/through_association_scope.rb +++ b/activerecord/lib/active_record/associations/through_association_scope.rb @@ -65,6 +65,7 @@ module ActiveRecord # Iterate over each pair in the through reflection chain, joining them together @reflection.through_reflection_chain.each_cons(2) do |left, right| polymorphic_join = nil + left_table, right_table = table_aliases[left], table_aliases[right] if left.source_reflection.nil? # TODO: Perhaps need to pay attention to left.options[:primary_key] and @@ -114,20 +115,31 @@ module ActiveRecord ] end when :has_and_belongs_to_many - raise NotImplementedError + join_table, left_table = left_table + + left_primary_key = left.klass.primary_key + join_primary_key = left.source_reflection.association_foreign_key + + joins << "INNER JOIN %s ON %s.%s = %s.%s" % [ + table_name_and_alias( + quote_table_name(left.source_reflection.options[:join_table]), + join_table + ), + left_table, left_primary_key, + join_table, join_primary_key + ] + + left_table = join_table + + left_primary_key = left.source_reflection.primary_key_name + right_primary_key = right.klass.primary_key end end - if right.quoted_table_name == table_aliases[right] - table = right.quoted_table_name - else - table = "#{right.quoted_table_name} #{table_aliases[right]}" - end - joins << "INNER JOIN %s ON %s.%s = %s.%s %s" % [ - table, - table_aliases[left], left_primary_key, - table_aliases[right], right_primary_key, + table_name_and_alias(right.quoted_table_name, right_table), + left_table, left_primary_key, + right_table, right_primary_key, polymorphic_join ] end @@ -147,13 +159,16 @@ module ActiveRecord table_alias_for(reflection, reflection != @reflection) )) - if reflection.macro == :has_and_belongs_to_many + 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.options[:join_table], + (reflection.source_reflection || reflection).options[:join_table], table_alias_for(reflection, true) )) - aliases[reflection] = [table_alias, join_table_alias] + aliases[reflection] = [join_table_alias, table_alias] else aliases[reflection] = table_alias end @@ -173,6 +188,10 @@ module ActiveRecord 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 # Construct attributes for associate pointing to owner. def construct_owner_attributes(reflection) 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 e67cbcc1a8..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 @@ -719,7 +719,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase def test_find_scoped_grouped assert_equal 5, categories(:general).posts_grouped_by_title.size - assert_equal 2, categories(:technology).posts_grouped_by_title.size + assert_equal 1, categories(:technology).posts_grouped_by_title.size end def test_find_scoped_grouped_having 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 index 835a573978..964112b006 100644 --- a/activerecord/test/cases/associations/nested_has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/nested_has_many_through_associations_test.rb @@ -159,10 +159,15 @@ class NestedHasManyThroughAssociationsTest < ActiveRecord::TestCase # has_many through # Source: has_and_belongs_to_many # Through: has_many - # TODO: Enable and implement this, and finish off the test - # def test_has_many_through_has_many_with_has_and_belongs_to_many_source_reflection - # assert_equal [categories(:general), categories(:technology)], authors(:bob).post_categories - # end + 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 # TODO: has_many through # Source: has_many 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 3b41510cb1..c6f0d885f5 100644 --- a/activerecord/test/fixtures/categories_posts.yml +++ b/activerecord/test/fixtures/categories_posts.yml @@ -26,6 +26,6 @@ general_misc_by_bob: category_id: 1 post_id: 8 -technology_misc_by_bob: - category_id: 2 +cooking_misc_by_bob: + category_id: 4 post_id: 8 From 5d8bb060909339d858151ca24bf764c642bf2b12 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Wed, 13 Oct 2010 17:55:41 +0100 Subject: [PATCH 022/100] Refactoring ThroughAssociationScope#construct_through_joins --- .../associations/through_association_scope.rb | 116 ++++++++++-------- 1 file changed, 62 insertions(+), 54 deletions(-) diff --git a/activerecord/lib/active_record/associations/through_association_scope.rb b/activerecord/lib/active_record/associations/through_association_scope.rb index 6cc2fe2559..25fde49650 100644 --- a/activerecord/lib/active_record/associations/through_association_scope.rb +++ b/activerecord/lib/active_record/associations/through_association_scope.rb @@ -64,8 +64,7 @@ module ActiveRecord # Iterate over each pair in the through reflection chain, joining them together @reflection.through_reflection_chain.each_cons(2) do |left, right| - polymorphic_join = nil - left_table, right_table = table_aliases[left], table_aliases[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 @@ -73,75 +72,56 @@ module ActiveRecord case left.macro when :belongs_to - left_primary_key = left.klass.primary_key - right_primary_key = left.primary_key_name + 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 - left_primary_key = left.primary_key_name - right_primary_key = right.klass.primary_key - - if left.options[:as] - polymorphic_join = "AND %s.%s = %s" % [ - table_aliases[left], "#{left.options[:as]}_type", - # TODO: Why right.klass.name? Rather than left.active_record.name? - # TODO: Also should maybe use the base_class (see related code in JoinAssociation) - @owner.class.quote_value(right.klass.name) - ] - end + 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 - left_primary_key = left.klass.primary_key - right_primary_key = left.source_reflection.primary_key_name - - if left.options[:source_type] - polymorphic_join = "AND %s.%s = %s" % [ - table_aliases[right], - left.source_reflection.options[:foreign_type].to_s, - @owner.class.quote_value(left.options[:source_type]) - ] - end + 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 - left_primary_key = left.source_reflection.primary_key_name - right_primary_key = right.klass.primary_key - - if left.source_reflection.options[:as] - polymorphic_join = "AND %s.%s = %s" % [ - table_aliases[left], - "#{left.source_reflection.options[:as]}_type", - @owner.class.quote_value(right.klass.name) - ] - end + joins << inner_join_sql( + right_table_and_alias, + table_aliases[left], left.source_reflection.primary_key_name, + table_aliases[right], right.klass.primary_key, + polymorphic_conditions(left, left.source_reflection.options[:as]) + ) when :has_and_belongs_to_many - join_table, left_table = left_table + join_table, left_table = table_aliases[left] - left_primary_key = left.klass.primary_key - join_primary_key = left.source_reflection.association_foreign_key - - joins << "INNER JOIN %s ON %s.%s = %s.%s" % [ + joins << inner_join_sql( table_name_and_alias( quote_table_name(left.source_reflection.options[:join_table]), join_table ), - left_table, left_primary_key, - join_table, join_primary_key - ] + left_table, left.klass.primary_key, + join_table, left.source_reflection.association_foreign_key + ) - left_table = join_table - - left_primary_key = left.source_reflection.primary_key_name - right_primary_key = right.klass.primary_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 - - joins << "INNER JOIN %s ON %s.%s = %s.%s %s" % [ - table_name_and_alias(right.quoted_table_name, right_table), - left_table, left_primary_key, - right_table, right_primary_key, - polymorphic_join - ] end joins.join(" ") @@ -192,6 +172,34 @@ module ActiveRecord 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) From 212fdd8ba9624f61421a7a950283537a3d39ac18 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Wed, 13 Oct 2010 18:36:51 +0100 Subject: [PATCH 023/100] Add test_has_many_through_has_and_belongs_to_many_with_has_many_source_reflection and make it work --- .../associations/through_association_scope.rb | 31 +++++++++++++++++-- ...sted_has_many_through_associations_test.rb | 12 ++++++- activerecord/test/models/category.rb | 2 ++ 3 files changed, 41 insertions(+), 4 deletions(-) diff --git a/activerecord/lib/active_record/associations/through_association_scope.rb b/activerecord/lib/active_record/associations/through_association_scope.rb index 25fde49650..582474355e 100644 --- a/activerecord/lib/active_record/associations/through_association_scope.rb +++ b/activerecord/lib/active_record/associations/through_association_scope.rb @@ -23,8 +23,15 @@ module ActiveRecord # TODO: Conditions on joins def construct_conditions 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_aliases[reflection]}.#{attr} = #{value}" + "#{table_alias}.#{attr} = #{value}" end conditions << sql_conditions if sql_conditions "(" + conditions.join(') AND (') + ")" @@ -97,12 +104,30 @@ module ActiveRecord 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, - table_aliases[right], right.klass.primary_key, + 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] 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 index 964112b006..4e7e766b14 100644 --- a/activerecord/test/cases/associations/nested_has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/nested_has_many_through_associations_test.rb @@ -169,9 +169,19 @@ class NestedHasManyThroughAssociationsTest < ActiveRecord::TestCase assert_equal [categories(:general), categories(:cooking)], authors[2].post_categories end - # TODO: has_many through + # 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 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 From 22782e2cc131863b72e457636f9a995a6ae50136 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Thu, 14 Oct 2010 12:31:35 +0100 Subject: [PATCH 024/100] Fix bug in previous refactoring --- .../associations/through_association_scope.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/activerecord/lib/active_record/associations/through_association_scope.rb b/activerecord/lib/active_record/associations/through_association_scope.rb index 582474355e..c3f12fee2b 100644 --- a/activerecord/lib/active_record/associations/through_association_scope.rb +++ b/activerecord/lib/active_record/associations/through_association_scope.rb @@ -89,7 +89,7 @@ module ActiveRecord right_table_and_alias, table_aliases[left], left.primary_key_name, table_aliases[right], right.klass.primary_key, - polymorphic_conditions(left, left.options[:as]) + polymorphic_conditions(left, left) ) when :has_and_belongs_to_many raise NotImplementedError @@ -115,7 +115,7 @@ module ActiveRecord 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]) + polymorphic_conditions(left, left.source_reflection) ) if right.macro == :has_and_belongs_to_many @@ -207,11 +207,11 @@ module ActiveRecord ] end - def polymorphic_conditions(reflection, interface_name) - if interface_name + def polymorphic_conditions(reflection, polymorphic_reflection) + if polymorphic_reflection.options[:as] "AND %s.%s = %s" % [ - table_aliases[reflection], "#{interface_name}_type", - @owner.class.quote_value(reflection.active_record.base_class.name) + table_aliases[reflection], "#{polymorphic_reflection.options[:as]}_type", + @owner.class.quote_value(polymorphic_reflection.active_record.base_class.name) ] end end From bc821a56114ae6f6d0b595475ad9e71f01f46f35 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Thu, 14 Oct 2010 12:59:16 +0100 Subject: [PATCH 025/100] Added test_has_many_through_has_many_with_has_many_through_habtm_source_reflection and make it pass --- .../associations/through_association_scope.rb | 8 ++++++-- .../test/cases/associations/join_model_test.rb | 2 +- ...nested_has_many_through_associations_test.rb | 17 ++++++++++++++++- activerecord/test/fixtures/categorizations.yml | 6 ++++++ activerecord/test/models/author.rb | 1 + 5 files changed, 30 insertions(+), 4 deletions(-) diff --git a/activerecord/lib/active_record/associations/through_association_scope.rb b/activerecord/lib/active_record/associations/through_association_scope.rb index c3f12fee2b..a52672eecd 100644 --- a/activerecord/lib/active_record/associations/through_association_scope.rb +++ b/activerecord/lib/active_record/associations/through_association_scope.rb @@ -92,7 +92,11 @@ module ActiveRecord polymorphic_conditions(left, left) ) when :has_and_belongs_to_many - raise NotImplementedError + joins << inner_join_sql( + right_table_and_alias, + table_aliases[left].first, left.primary_key_name, + table_aliases[right], right.klass.primary_key + ) end else case left.source_reflection.macro @@ -106,7 +110,7 @@ module ActiveRecord 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) + right_table_and_alias = table_name_and_alias(right.quoted_table_name, right_table) else right_table = table_aliases[right] end diff --git a/activerecord/test/cases/associations/join_model_test.rb b/activerecord/test/cases/associations/join_model_test.rb index 4b7a8b494d..385505b109 100644 --- a/activerecord/test/cases/associations/join_model_test.rb +++ b/activerecord/test/cases/associations/join_model_test.rb @@ -304,7 +304,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 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 index 4e7e766b14..26c31ef761 100644 --- a/activerecord/test/cases/associations/nested_has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/nested_has_many_through_associations_test.rb @@ -25,6 +25,7 @@ require 'models/sponsor' require 'models/club' require 'models/organization' require 'models/category' +require 'models/categorization' # 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 @@ -34,7 +35,8 @@ require 'models/category' 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 + :member_types, :sponsors, :clubs, :organizations, :categories, :categories_posts, + :categorizations # Through associations can either use the has_many or has_one macros. # @@ -183,6 +185,19 @@ class NestedHasManyThroughAssociationsTest < ActiveRecord::TestCase # assert_equal [comments(:greetings), comments(:more_greetings)], categories[1].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 + assert_equal [comments(:greetings), comments(:more_greetings)], authors(:bob).category_post_comments + + authors = Author.joins(:category_post_comments).where('comments.id' => comments(:does_it_hurt).id) + assert_equal [authors(:david), authors(:mary)], authors + + comments = Author.joins(:category_post_comments) + assert_equal [comments(:greetings), comments(:more_greetings)], comments[2].category_post_comments + end + # TODO: has_many through # Source: belongs_to # Through: has_many through 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/models/author.rb b/activerecord/test/models/author.rb index 584164f19a..f2f373af8c 100644 --- a/activerecord/test/models/author.rb +++ b/activerecord/test/models/author.rb @@ -101,6 +101,7 @@ class Author < ActiveRecord::Base 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 scope :relation_include_posts, includes(:posts) scope :relation_include_tags, includes(:tags) From 7963c30ebaeb511f7ddacc99ae2c7a530059ae6b Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Thu, 14 Oct 2010 13:07:28 +0100 Subject: [PATCH 026/100] Add test_has_many_through_has_many_through_with_belongs_to_source_reflection (which already works) --- .../nested_has_many_through_associations_test.rb | 13 ++++++++++++- activerecord/test/models/author.rb | 1 + 2 files changed, 13 insertions(+), 1 deletion(-) 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 index 26c31ef761..7e1fc60cb9 100644 --- a/activerecord/test/cases/associations/nested_has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/nested_has_many_through_associations_test.rb @@ -198,9 +198,20 @@ class NestedHasManyThroughAssociationsTest < ActiveRecord::TestCase assert_equal [comments(:greetings), comments(:more_greetings)], comments[2].category_post_comments end - # TODO: has_many through + # has_many through # Source: belongs_to # Through: has_many through + def test_has_many_through_has_many_through_with_belongs_to_source_reflection + author = authors(:david) + assert_equal [tags(:general), tags(:general)], author.tagging_tags + + authors = Author.joins(:tagging_tags).where('tags.id' => tags(:general).id) + assert_equal [authors(:david)], authors.uniq + + # TODO: Make this work + # authors = Author.includes(:tagging_tags) + # assert_equal [tags(:general), tags(:general)], authors.first.tagging_tags + end # TODO: has_many through # Source: has_many through diff --git a/activerecord/test/models/author.rb b/activerecord/test/models/author.rb index f2f373af8c..b5f702018a 100644 --- a/activerecord/test/models/author.rb +++ b/activerecord/test/models/author.rb @@ -89,6 +89,7 @@ class Author < ActiveRecord::Base 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 :tagging_tags, :through => :taggings, :source => :tag has_many :books has_many :subscriptions, :through => :books From 25acd19da5f75a425218740fbb187b18bbb060ce Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Thu, 14 Oct 2010 13:16:47 +0100 Subject: [PATCH 027/100] Add test_has_many_through_belongs_to_with_has_many_through_source_reflection (which already passes) --- .../nested_has_many_through_associations_test.rb | 13 ++++++++++++- activerecord/test/models/categorization.rb | 4 +++- 2 files changed, 15 insertions(+), 2 deletions(-) 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 index 7e1fc60cb9..6212eed0eb 100644 --- a/activerecord/test/cases/associations/nested_has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/nested_has_many_through_associations_test.rb @@ -213,9 +213,20 @@ class NestedHasManyThroughAssociationsTest < ActiveRecord::TestCase # assert_equal [tags(:general), tags(:general)], authors.first.tagging_tags end - # TODO: has_many through + # has_many through # Source: has_many through # Through: belongs_to + def test_has_many_through_belongs_to_with_has_many_through_source_reflection + assert_equal [taggings(:welcome_general), taggings(:thinking_general)], + categorizations(:david_welcome_general).post_taggings + + categorizations = Categorization.joins(:post_taggings).where('taggings.id' => taggings(:welcome_general).id) + assert_equal [categorizations(:david_welcome_general)], categorizations + + categorizations = Categorization.includes(:post_taggings) + assert_equal [taggings(:welcome_general), taggings(:thinking_general)], + categorizations.first.post_taggings + end # has_one through # Source: has_one through diff --git a/activerecord/test/models/categorization.rb b/activerecord/test/models/categorization.rb index 10594323ff..bddc1e5f0c 100644 --- a/activerecord/test/models/categorization.rb +++ b/activerecord/test/models/categorization.rb @@ -2,4 +2,6 @@ class Categorization < ActiveRecord::Base belongs_to :post belongs_to :category belongs_to :author -end \ No newline at end of file + + has_many :post_taggings, :through => :author, :source => :taggings +end From 002985fb66ae63f157db84f83520c3c256c04f77 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Thu, 14 Oct 2010 13:44:32 +0100 Subject: [PATCH 028/100] Add test_has_one_through_has_one_through_with_belongs_to_source_reflection --- .../nested_has_many_through_associations_test.rb | 15 +++++++++++++-- activerecord/test/fixtures/clubs.yml | 4 +++- activerecord/test/fixtures/members.yml | 3 +++ activerecord/test/fixtures/memberships.yml | 7 +++++++ activerecord/test/models/club.rb | 3 ++- activerecord/test/models/member.rb | 2 ++ activerecord/test/schema/schema.rb | 1 + 7 files changed, 31 insertions(+), 4 deletions(-) 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 index 6212eed0eb..0bd19c10e0 100644 --- a/activerecord/test/cases/associations/nested_has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/nested_has_many_through_associations_test.rb @@ -26,6 +26,7 @@ require 'models/club' require 'models/organization' require 'models/category' require 'models/categorization' +require 'models/membership' # 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 @@ -36,7 +37,7 @@ 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, - :categorizations + :categorizations, :memberships # Through associations can either use the has_many or has_one macros. # @@ -241,9 +242,19 @@ class NestedHasManyThroughAssociationsTest < ActiveRecord::TestCase assert_equal member_types(:founding), members.first.nested_member_type end - # TODO: has_one through + # 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 + + members = Member.joins(:club_category).where('categories.id' => categories(:technology).id) + assert_equal [members(:blarpy_winkup)], members + + # TODO: Make this work + # members = Member.includes(:club_category) + # assert_equal categories(:general), members.first.club_category + end def test_distinct_has_many_through_a_has_many_through_association_on_source_reflection author = authors(:david) 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/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/models/club.rb b/activerecord/test/models/club.rb index 6e7cdd643a..83d6b1b15a 100644 --- a/activerecord/test/models/club.rb +++ b/activerecord/test/models/club.rb @@ -4,10 +4,11 @@ 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 def private_method "I'm sorry sir, this is a *private* club, not a *pirate* club" end -end \ No newline at end of file +end diff --git a/activerecord/test/models/member.rb b/activerecord/test/models/member.rb index 44c10cc4a4..bed62f8b7f 100644 --- a/activerecord/test/models/member.rb +++ b/activerecord/test/models/member.rb @@ -18,4 +18,6 @@ class Member < ActiveRecord::Base 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 end diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 2fa9a4521e..8b9c56b895 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -113,6 +113,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| From 11508db1be0016bb1a9893c7b2062845233f78e0 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Thu, 14 Oct 2010 13:52:38 +0100 Subject: [PATCH 029/100] Remove unnecessary requires from nested_has_many_through_associations_test.rb --- .../nested_has_many_through_associations_test.rb | 6 ------ 1 file changed, 6 deletions(-) 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 index 0bd19c10e0..eea1c4e54c 100644 --- a/activerecord/test/cases/associations/nested_has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/nested_has_many_through_associations_test.rb @@ -8,12 +8,6 @@ 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' From 06c64eb60611bdeeb55e35a4819ba65d74dbadc3 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Fri, 15 Oct 2010 15:46:19 +0100 Subject: [PATCH 030/100] Support preloading nested through associations (using the default multi-query strategy) --- .../lib/active_record/association_preload.rb | 137 ++++++------ ...sted_has_many_through_associations_test.rb | 201 ++++++++++++------ 2 files changed, 216 insertions(+), 122 deletions(-) diff --git a/activerecord/lib/active_record/association_preload.rb b/activerecord/lib/active_record/association_preload.rb index e6b367790b..664b0a7d59 100644 --- a/activerecord/lib/active_record/association_preload.rb +++ b/activerecord/lib/active_record/association_preload.rb @@ -202,93 +202,108 @@ module ActiveRecord set_association_collection_records(id_to_record_map, reflection.name, associated_records, 'the_parent_record_id') end - def preload_has_one_association(records, reflection, preload_options={}) - return if records.first.send("loaded_#{reflection.name}?") - id_to_record_map, ids = construct_id_map(records, reflection.options[:primary_key]) + def preload_has_one_or_has_many_association(records, reflection, preload_options={}) + if reflection.macro == :has_many + return if records.first.send(reflection.name).loaded? + records.each { |record| record.send(reflection.name).loaded } + else + return if records.first.send("loaded_#{reflection.name}?") + records.each {|record| record.send("set_#{reflection.name}_target", nil)} + end + options = reflection.options - records.each {|record| record.send("set_#{reflection.name}_target", nil)} + if options[:through] - through_records = preload_through_records(records, reflection, options[:through]) + records_with_through_records = preload_through_records(records, reflection, options[:through]) + all_through_records = records_with_through_records.map(&:last).flatten - unless through_records.empty? - through_reflection = reflections[options[:through]] - through_primary_key = through_reflection.primary_key_name + unless all_through_records.empty? source = reflection.source_reflection.name - through_records.first.class.preload_associations(through_records, source) - if through_reflection.macro == :belongs_to - id_to_record_map = construct_id_map(records, through_primary_key).first - through_primary_key = through_reflection.klass.primary_key - end - - through_records.each do |through_record| - add_preloaded_record_to_collection(id_to_record_map[through_record[through_primary_key].to_s], - reflection.name, through_record.send(source)) + all_through_records.first.class.preload_associations(all_through_records, source, options) + + records_with_through_records.each do |record, through_records| + source_records = through_records.map(&source).flatten.compact + + case reflection.macro + when :has_many, :has_and_belongs_to_many + add_preloaded_records_to_collection([record], reflection.name, source_records) + when :has_one, :belongs_to + add_preloaded_record_to_collection([record], reflection.name, source_records.first) + end end end else - set_association_single_records(id_to_record_map, reflection.name, find_associated_records(ids, reflection, preload_options), reflection.primary_key_name) - end - end - - def preload_has_many_association(records, reflection, preload_options={}) - return if records.first.send(reflection.name).loaded? - options = reflection.options - - primary_key_name = reflection.through_reflection_primary_key_name - id_to_record_map, ids = construct_id_map(records, primary_key_name || reflection.options[:primary_key]) - records.each {|record| record.send(reflection.name).loaded} - - if options[:through] - through_records = preload_through_records(records, reflection, options[:through]) - unless through_records.empty? - source = reflection.source_reflection.name - through_records.first.class.preload_associations(through_records, source, options) - through_records.each do |through_record| - through_record_id = through_record[reflection.through_reflection_primary_key].to_s - add_preloaded_records_to_collection(id_to_record_map[through_record_id], reflection.name, through_record.send(source)) - end + id_to_record_map, ids = construct_id_map(records, reflection.options[:primary_key]) + associated_records = find_associated_records(ids, reflection, preload_options) + + if reflection.macro == :has_many + set_association_collection_records( + id_to_record_map, reflection.name, + associated_records, reflection.primary_key_name + ) + else + set_association_single_records( + id_to_record_map, reflection.name, + associated_records, reflection.primary_key_name + ) end - - else - set_association_collection_records(id_to_record_map, reflection.name, find_associated_records(ids, reflection, preload_options), - reflection.primary_key_name) end end + + alias_method :preload_has_one_association, :preload_has_one_or_has_many_association + alias_method :preload_has_many_association, :preload_has_one_or_has_many_association def preload_through_records(records, reflection, through_association) through_reflection = reflections[through_association] - through_records = [] + # If the same through record is loaded twice, we want to return exactly the same + # object in the result, rather than two separate instances representing the same + # record. This is so that we can preload the source association for each record, + # and always be able to access the preloaded association regardless of where we + # refer to the record. + # + # Suffices to say, if AR had an identity map built in then this would be unnecessary. + identity_map = {} + + options = {} + if reflection.options[:source_type] interface = reflection.source_reflection.options[:foreign_type] - preload_options = {:conditions => ["#{connection.quote_column_name interface} = ?", reflection.options[:source_type]]} - + options[:conditions] = ["#{connection.quote_column_name interface} = ?", reflection.options[:source_type]] records.compact! - records.first.class.preload_associations(records, through_association, preload_options) + else + if reflection.options[:conditions] + options[:include] = reflection.options[:include] || + reflection.options[:source] + options[:conditions] = reflection.options[:conditions] + end + + options[:order] = reflection.options[:order] + end + + records.first.class.preload_associations(records, through_association, options) - # Dont cache the association - we would only be caching a subset - records.each do |record| + records.map do |record| + if reflection.options[:source_type] + # Dont cache the association - we would only be caching a subset proxy = record.send(through_association) - + if proxy.respond_to?(:target) - through_records.concat Array.wrap(proxy.target) + through_records = proxy.target proxy.reset else # this is a has_one :through reflection - through_records << proxy if proxy + through_records = proxy end + else + through_records = record.send(through_association) end - else - options = {} - options[:include] = reflection.options[:include] || reflection.options[:source] if reflection.options[:conditions] - options[:order] = reflection.options[:order] - options[:conditions] = reflection.options[:conditions] - records.first.class.preload_associations(records, through_association, options) - - records.each do |record| - through_records.concat Array.wrap(record.send(through_association)) + + through_records = Array.wrap(through_records).map do |through_record| + identity_map[through_record] ||= through_record end + + [record, through_records] end - through_records end def preload_belongs_to_association(records, reflection, preload_options={}) 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 index eea1c4e54c..32b03bf076 100644 --- a/activerecord/test/cases/associations/nested_has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/nested_has_many_through_associations_test.rb @@ -54,70 +54,98 @@ class NestedHasManyThroughAssociationsTest < ActiveRecord::TestCase # 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 + general = tags(:general) + + assert_equal [general, general], authors(:david).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? + + assert_queries(5) do + authors = Author.includes(:tags).to_a + end + + assert_no_queries do + assert_equal [general, general], authors.first.tags + end 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) + author = authors(:david) - assert_equal [subscribers(:first), subscribers(:second), subscribers(:second)], author.subscribers + assert_equal [luke, david, david], 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 + assert_queries(4) do + authors = Author.includes(:subscribers).to_a + end + + assert_no_queries do + assert_equal [luke, david, david], authors.first.subscribers.sort_by(&:nick) + end + + # TODO: Add eager loading test using LEFT OUTER JOIN 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 + founding = member_types(:founding) + + assert_equal [founding], members(:groucho).nested_member_types - members = Member.joins(:nested_member_types).where('member_types.id' => member_types(:founding).id) + members = Member.joins(:nested_member_types).where('member_types.id' => founding.id) assert_equal [members(:groucho)], members - members = Member.includes(:nested_member_types) - assert_equal [member_types(:founding)], members.first.nested_member_types + assert_queries(4) do + members = Member.includes(:nested_member_types).to_a + end + + assert_no_queries do + assert_equal [founding], members.first.nested_member_types + end 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 + mustache = sponsors(:moustache_club_sponsor_for_groucho) - members = Member.joins(:nested_sponsors).where('sponsors.id' => sponsors(:moustache_club_sponsor_for_groucho).id) + assert_equal [mustache], members(:groucho).nested_sponsors + + members = Member.joins(:nested_sponsors).where('sponsors.id' => mustache.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 + assert_queries(4) do + members = Member.includes(:nested_sponsors).to_a + end + + assert_no_queries do + assert_equal [mustache], members.first.nested_sponsors + end 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 + groucho_details, other_details = member_details(:groucho), member_details(:some_other_guy) + + assert_equal [groucho_details, other_details], members(:groucho).organization_member_details members = Member.joins(:organization_member_details). where('member_details.id' => member_details(:groucho).id) @@ -127,127 +155,178 @@ class NestedHasManyThroughAssociationsTest < ActiveRecord::TestCase 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 + assert_queries(4) do + members = Member.includes(:organization_member_details).to_a + end + + assert_no_queries do + assert_equal [groucho_details, other_details], members.first.organization_member_details + end 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 + groucho_details, other_details = member_details(:groucho), member_details(:some_other_guy) + + assert_equal [groucho_details, other_details], members(:groucho).organization_member_details_2 members = Member.joins(:organization_member_details_2). - where('member_details.id' => member_details(:groucho).id) + where('member_details.id' => groucho_details.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 + assert_queries(4) do + members = Member.includes(:organization_member_details_2).to_a + end + + assert_no_queries do + assert_equal [groucho_details, other_details], members.first.organization_member_details_2 + end 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 + general, cooking = categories(:general), categories(:cooking) + + assert_equal [general, cooking], authors(:bob).post_categories - authors = Author.joins(:post_categories).where('categories.id' => categories(:cooking).id) + authors = Author.joins(:post_categories).where('categories.id' => cooking.id) assert_equal [authors(:bob)], authors - authors = Author.includes(:post_categories) - assert_equal [categories(:general), categories(:cooking)], authors[2].post_categories + assert_queries(3) do + authors = Author.includes(:post_categories).to_a + end + + assert_no_queries do + assert_equal [general, cooking], authors[2].post_categories + end 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 + greetings, more = comments(:greetings), comments(:more_greetings) + + assert_equal [greetings, more], categories(:technology).post_comments - categories = Category.joins(:post_comments).where('comments.id' => comments(:more_greetings).id) + categories = Category.joins(:post_comments).where('comments.id' => more.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 + assert_queries(3) do + categories = Category.includes(:post_comments).to_a + end + + assert_no_queries do + assert_equal [greetings, more], categories[1].post_comments + end 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 - assert_equal [comments(:greetings), comments(:more_greetings)], authors(:bob).category_post_comments + greetings, more = comments(:greetings), comments(:more_greetings) + + assert_equal [greetings, more], authors(:bob).category_post_comments authors = Author.joins(:category_post_comments).where('comments.id' => comments(:does_it_hurt).id) assert_equal [authors(:david), authors(:mary)], authors - comments = Author.joins(:category_post_comments) - assert_equal [comments(:greetings), comments(:more_greetings)], comments[2].category_post_comments + assert_queries(5) do + authors = Author.includes(:category_post_comments).to_a + end + + assert_no_queries do + assert_equal [greetings, more], authors[2].category_post_comments + end end # has_many through # Source: belongs_to # Through: has_many through def test_has_many_through_has_many_through_with_belongs_to_source_reflection - author = authors(:david) - assert_equal [tags(:general), tags(:general)], author.tagging_tags + general = tags(:general) + + assert_equal [general, general], authors(:david).tagging_tags authors = Author.joins(:tagging_tags).where('tags.id' => tags(:general).id) assert_equal [authors(:david)], authors.uniq - # TODO: Make this work - # authors = Author.includes(:tagging_tags) - # assert_equal [tags(:general), tags(:general)], authors.first.tagging_tags + assert_queries(5) do + authors = Author.includes(:tagging_tags).to_a + end + + assert_no_queries do + assert_equal [general, general], authors.first.tagging_tags + end end # has_many through # Source: has_many through # Through: belongs_to def test_has_many_through_belongs_to_with_has_many_through_source_reflection - assert_equal [taggings(:welcome_general), taggings(:thinking_general)], - categorizations(:david_welcome_general).post_taggings + welcome_general, thinking_general = taggings(:welcome_general), taggings(:thinking_general) + + assert_equal [welcome_general, thinking_general], categorizations(:david_welcome_general).post_taggings - categorizations = Categorization.joins(:post_taggings).where('taggings.id' => taggings(:welcome_general).id) + categorizations = Categorization.joins(:post_taggings).where('taggings.id' => welcome_general.id) assert_equal [categorizations(:david_welcome_general)], categorizations - categorizations = Categorization.includes(:post_taggings) - assert_equal [taggings(:welcome_general), taggings(:thinking_general)], - categorizations.first.post_taggings + assert_queries(4) do + categorizations = Categorization.includes(:post_taggings).to_a + end + + assert_no_queries do + assert_equal [welcome_general, thinking_general], categorizations.first.post_taggings + end 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 + founding = member_types(:founding) + + assert_equal founding, members(:groucho).nested_member_type - members = Member.joins(:nested_member_type).where('member_types.id' => member_types(:founding).id) + members = Member.joins(:nested_member_type).where('member_types.id' => founding.id) assert_equal [members(:groucho)], members - members = Member.includes(:nested_member_type) - assert_equal member_types(:founding), members.first.nested_member_type + assert_queries(4) do + members = Member.includes(:nested_member_type).to_a + end + + assert_no_queries do + assert_equal founding, members.first.nested_member_type + end 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 + general = categories(:general) + + assert_equal general, members(:groucho).club_category members = Member.joins(:club_category).where('categories.id' => categories(:technology).id) assert_equal [members(:blarpy_winkup)], members - # TODO: Make this work - # members = Member.includes(:club_category) - # assert_equal categories(:general), members.first.club_category + assert_queries(4) do + members = Member.includes(:club_category).to_a + end + + assert_no_queries do + assert_equal general, members.first.club_category + end end def test_distinct_has_many_through_a_has_many_through_association_on_source_reflection From 1e2525bfe0248d873d6d6026f45102853a1c95cd Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Fri, 15 Oct 2010 16:21:42 +0100 Subject: [PATCH 031/100] Add assertions for nested through associations loaded by includes with conditions (uses the single-query strategy). Currently one failure to fix. --- ...sted_has_many_through_associations_test.rb | 156 +++++++++--------- 1 file changed, 75 insertions(+), 81 deletions(-) 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 index 32b03bf076..4d5152ed5d 100644 --- a/activerecord/test/cases/associations/nested_has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/nested_has_many_through_associations_test.rb @@ -58,18 +58,16 @@ class NestedHasManyThroughAssociationsTest < ActiveRecord::TestCase assert_equal [general, general], authors(:david).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 + 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? - assert_queries(5) do - authors = Author.includes(:tags).to_a - end - + authors = assert_queries(5) { Author.includes(:tags).to_a } assert_no_queries do assert_equal [general, general], authors.first.tags end @@ -85,13 +83,12 @@ class NestedHasManyThroughAssociationsTest < ActiveRecord::TestCase assert_equal [luke, david, david], 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 - - assert_queries(4) do - authors = Author.includes(:subscribers).to_a - end + assert_includes_and_joins_equal( + Author.where('subscribers.nick' => 'alterself'), + [authors(:david)], :subscribers + ) + 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 @@ -107,13 +104,12 @@ class NestedHasManyThroughAssociationsTest < ActiveRecord::TestCase assert_equal [founding], members(:groucho).nested_member_types - members = Member.joins(:nested_member_types).where('member_types.id' => founding.id) - assert_equal [members(:groucho)], members - - assert_queries(4) do - members = Member.includes(:nested_member_types).to_a - end + assert_includes_and_joins_equal( + Member.where('member_types.id' => founding.id), + [members(:groucho)], :nested_member_types + ) + members = assert_queries(4) { Member.includes(:nested_member_types).to_a } assert_no_queries do assert_equal [founding], members.first.nested_member_types end @@ -127,13 +123,12 @@ class NestedHasManyThroughAssociationsTest < ActiveRecord::TestCase assert_equal [mustache], members(:groucho).nested_sponsors - members = Member.joins(:nested_sponsors).where('sponsors.id' => mustache.id) - assert_equal [members(:groucho)], members - - assert_queries(4) do - members = Member.includes(:nested_sponsors).to_a - end + assert_includes_and_joins_equal( + Member.where('sponsors.id' => mustache.id), + [members(:groucho)], :nested_sponsors + ) + members = assert_queries(4) { Member.includes(:nested_sponsors).to_a } assert_no_queries do assert_equal [mustache], members.first.nested_sponsors end @@ -147,18 +142,16 @@ class NestedHasManyThroughAssociationsTest < ActiveRecord::TestCase assert_equal [groucho_details, other_details], 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 + assert_includes_and_joins_equal( + Member.where('member_details.id' => member_details(:groucho).id), + [members(:groucho), members(:some_other_guy)], :organization_member_details + ) members = Member.joins(:organization_member_details). where('member_details.id' => 9) assert members.empty? - assert_queries(4) do - members = Member.includes(:organization_member_details).to_a - end - + members = assert_queries(4) { Member.includes(:organization_member_details).to_a } assert_no_queries do assert_equal [groucho_details, other_details], members.first.organization_member_details end @@ -172,18 +165,16 @@ class NestedHasManyThroughAssociationsTest < ActiveRecord::TestCase assert_equal [groucho_details, other_details], members(:groucho).organization_member_details_2 - members = Member.joins(:organization_member_details_2). - where('member_details.id' => groucho_details.id) - assert_equal [members(:groucho), members(:some_other_guy)], members + assert_includes_and_joins_equal( + Member.where('member_details.id' => groucho_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? - assert_queries(4) do - members = Member.includes(:organization_member_details_2).to_a - end - + members = assert_queries(4) { Member.includes(:organization_member_details_2).to_a } assert_no_queries do assert_equal [groucho_details, other_details], members.first.organization_member_details_2 end @@ -197,13 +188,12 @@ class NestedHasManyThroughAssociationsTest < ActiveRecord::TestCase assert_equal [general, cooking], authors(:bob).post_categories - authors = Author.joins(:post_categories).where('categories.id' => cooking.id) - assert_equal [authors(:bob)], authors - - assert_queries(3) do - authors = Author.includes(:post_categories).to_a - end + assert_includes_and_joins_equal( + Author.where('categories.id' => cooking.id), + [authors(:bob)], :post_categories + ) + authors = assert_queries(3) { Author.includes(:post_categories).to_a } assert_no_queries do assert_equal [general, cooking], authors[2].post_categories end @@ -217,13 +207,12 @@ class NestedHasManyThroughAssociationsTest < ActiveRecord::TestCase assert_equal [greetings, more], categories(:technology).post_comments - categories = Category.joins(:post_comments).where('comments.id' => more.id) - assert_equal [categories(:general), categories(:technology)], categories - - assert_queries(3) do - categories = Category.includes(:post_comments).to_a - end + assert_includes_and_joins_equal( + Category.where('comments.id' => more.id), + [categories(:general), categories(:technology)], :post_comments + ) + categories = assert_queries(3) { Category.includes(:post_comments).to_a } assert_no_queries do assert_equal [greetings, more], categories[1].post_comments end @@ -237,13 +226,12 @@ class NestedHasManyThroughAssociationsTest < ActiveRecord::TestCase assert_equal [greetings, more], authors(:bob).category_post_comments - authors = Author.joins(:category_post_comments).where('comments.id' => comments(:does_it_hurt).id) - assert_equal [authors(:david), authors(:mary)], authors - - assert_queries(5) do - authors = Author.includes(:category_post_comments).to_a - end + assert_includes_and_joins_equal( + Author.where('comments.id' => comments(:does_it_hurt).id), + [authors(:david), authors(:mary)], :category_post_comments + ) + authors = assert_queries(5) { Author.includes(:category_post_comments).to_a } assert_no_queries do assert_equal [greetings, more], authors[2].category_post_comments end @@ -257,13 +245,12 @@ class NestedHasManyThroughAssociationsTest < ActiveRecord::TestCase assert_equal [general, general], authors(:david).tagging_tags - authors = Author.joins(:tagging_tags).where('tags.id' => tags(:general).id) - assert_equal [authors(:david)], authors.uniq - - assert_queries(5) do - authors = Author.includes(:tagging_tags).to_a - end + assert_includes_and_joins_equal( + Author.where('tags.id' => tags(:general).id), + [authors(:david)], :tagging_tags + ) + authors = assert_queries(5) { Author.includes(:tagging_tags).to_a } assert_no_queries do assert_equal [general, general], authors.first.tagging_tags end @@ -277,13 +264,12 @@ class NestedHasManyThroughAssociationsTest < ActiveRecord::TestCase assert_equal [welcome_general, thinking_general], categorizations(:david_welcome_general).post_taggings - categorizations = Categorization.joins(:post_taggings).where('taggings.id' => welcome_general.id) - assert_equal [categorizations(:david_welcome_general)], categorizations - - assert_queries(4) do - categorizations = Categorization.includes(:post_taggings).to_a - end + assert_includes_and_joins_equal( + Categorization.where('taggings.id' => welcome_general.id), + [categorizations(:david_welcome_general)], :post_taggings + ) + categorizations = assert_queries(4) { Categorization.includes(:post_taggings).to_a } assert_no_queries do assert_equal [welcome_general, thinking_general], categorizations.first.post_taggings end @@ -297,13 +283,12 @@ class NestedHasManyThroughAssociationsTest < ActiveRecord::TestCase assert_equal founding, members(:groucho).nested_member_type - members = Member.joins(:nested_member_type).where('member_types.id' => founding.id) - assert_equal [members(:groucho)], members - - assert_queries(4) do - members = Member.includes(:nested_member_type).to_a - end + assert_includes_and_joins_equal( + Member.where('member_types.id' => founding.id), + [members(:groucho)], :nested_member_type + ) + members = assert_queries(4) { Member.includes(:nested_member_type).to_a } assert_no_queries do assert_equal founding, members.first.nested_member_type end @@ -317,13 +302,12 @@ class NestedHasManyThroughAssociationsTest < ActiveRecord::TestCase assert_equal general, members(:groucho).club_category - members = Member.joins(:club_category).where('categories.id' => categories(:technology).id) - assert_equal [members(:blarpy_winkup)], members - - assert_queries(4) do - members = Member.includes(:club_category).to_a - end + assert_includes_and_joins_equal( + Member.where('categories.id' => categories(:technology).id), + [members(:blarpy_winkup)], :club_category + ) + members = assert_queries(4) { Member.includes(:club_category).to_a } assert_no_queries do assert_equal general, members.first.club_category end @@ -379,4 +363,14 @@ class NestedHasManyThroughAssociationsTest < ActiveRecord::TestCase assert !scope.where("comments.type" => "SpecialComment").empty? assert !scope.where("comments.type" => "SubSpecialComment").empty? 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 From d619e399380cd840f9f5ec88bb3d823fbb1f4d08 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Fri, 15 Oct 2010 16:27:13 +0100 Subject: [PATCH 032/100] Fix small bug which was shown by the last commit --- activerecord/lib/active_record/associations.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 2a72fa95c9..22a693540e 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -2246,7 +2246,7 @@ module ActiveRecord end def table - if reflection.macro == :has_and_belongs_to_many + if @tables.last.is_a?(Array) @tables.last.first else @tables.last From edc176d33be9499f4c096779c5b4711b5daf0c06 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Fri, 15 Oct 2010 17:46:09 +0100 Subject: [PATCH 033/100] Make sure nested through associations are read only --- .../lib/active_record/associations.rb | 6 +++ .../has_many_through_association.rb | 10 +++++ .../has_one_through_association.rb | 2 + .../associations/through_association_scope.rb | 27 ++++++++---- activerecord/lib/active_record/reflection.rb | 4 ++ ...sted_has_many_through_associations_test.rb | 42 +++++++++++++++++++ 6 files changed, 82 insertions(+), 9 deletions(-) diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 22a693540e..1111033435 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -64,6 +64,12 @@ module ActiveRecord super("Cannot dissociate new records through '#{owner.class.name}##{reflection.name}' on '#{reflection.source_reflection.class_name rescue nil}##{reflection.source_reflection.name rescue nil}'. Both records must have an id in order to delete the has_many :through record associating them.") 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) 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 313d9da621..f0ad166802 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -8,6 +8,11 @@ module ActiveRecord class HasManyThroughAssociation < HasManyAssociation #:nodoc: include ThroughAssociationScope + def build(attributes = {}, &block) + ensure_not_nested + super + end + alias_method :new, :build def create!(attrs = nil) @@ -37,6 +42,7 @@ module ActiveRecord protected def create_record(attrs, force = true) + ensure_not_nested ensure_owner_is_not_new transaction do @@ -60,6 +66,8 @@ module ActiveRecord end def insert_record(record, force = true, validate = true) + ensure_not_nested + if record.new_record? if force record.save! @@ -75,6 +83,8 @@ module ActiveRecord # TODO - add dependent option support def delete_records(records) + ensure_not_nested + klass = @reflection.through_reflection.klass records.each do |associate| klass.delete_all(construct_join_attributes(associate)) 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 fba0a2bfcc..8153eb7c57 100644 --- a/activerecord/lib/active_record/associations/has_one_through_association.rb +++ b/activerecord/lib/active_record/associations/has_one_through_association.rb @@ -14,6 +14,8 @@ module ActiveRecord private def create_through_record(new_value) #nodoc: + ensure_not_nested + klass = @reflection.through_reflection.klass current_object = @owner.send(@reflection.through_reflection.name) diff --git a/activerecord/lib/active_record/associations/through_association_scope.rb b/activerecord/lib/active_record/associations/through_association_scope.rb index a52672eecd..51ab8869ed 100644 --- a/activerecord/lib/active_record/associations/through_association_scope.rb +++ b/activerecord/lib/active_record/associations/through_association_scope.rb @@ -8,15 +8,18 @@ module ActiveRecord protected def construct_scope - { :create => construct_owner_attributes(@reflection), - :find => { :conditions => construct_conditions, - :joins => construct_joins, - :include => @reflection.options[:include] || @reflection.source_reflection.options[:include], - :select => construct_select, - :order => @reflection.options[:order], - :limit => @reflection.options[:limit], - :readonly => @reflection.options[:readonly], - } } + scope = {} + scope[:find] = { + :conditions => construct_conditions, + :joins => construct_joins, + :include => @reflection.options[:include] || @reflection.source_reflection.options[:include], + :select => construct_select, + :order => @reflection.options[:order], + :limit => @reflection.options[:limit], + :readonly => @reflection.options[:readonly] + } + scope[:create] = construct_owner_attributes(@reflection) unless @reflection.nested? + scope end # Build SQL conditions from attributes, qualified by table name. @@ -299,6 +302,12 @@ module ActiveRecord end alias_method :sql_conditions, :conditions + + 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 b7cd466e13..ee63fcfce2 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -395,6 +395,10 @@ module ActiveRecord chain end end + + def nested? + through_reflection_chain.length > 2 + end # Gets an array of possible :through source reflection names: # 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 index 4d5152ed5d..03ec4281d8 100644 --- a/activerecord/test/cases/associations/nested_has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/nested_has_many_through_associations_test.rb @@ -363,6 +363,48 @@ class NestedHasManyThroughAssociationsTest < ActiveRecord::TestCase 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 private From 78b8c51cb3b0c629152f3bbaf6d8bcf988cc936e Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Sun, 17 Oct 2010 23:29:56 +0100 Subject: [PATCH 034/100] Refactoring: replace the mix of variables like @finder_sql, @counter_sql, etc with just a single scope hash (created on initialization of the proxy). This is now used consistently across all associations. Therefore, all you have to do to ensure finding/counting etc is done correctly is implement the scope correctly. --- .../associations/association_collection.rb | 66 ++++++++----------- .../associations/association_proxy.rb | 19 ++++++ .../associations/belongs_to_association.rb | 20 +++--- .../belongs_to_polymorphic_association.rb | 22 +++---- .../has_and_belongs_to_many_association.rb | 39 ++++++----- .../associations/has_many_association.rb | 58 +++++++--------- .../has_many_through_association.rb | 16 +---- .../associations/has_one_association.rb | 39 +++++------ .../has_one_through_association.rb | 2 +- .../associations/through_association_scope.rb | 11 ++-- .../lib/active_record/autosave_association.rb | 4 +- 11 files changed, 140 insertions(+), 156 deletions(-) diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index cb2d9e0a79..896e18af01 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -19,11 +19,6 @@ module ActiveRecord # If you need to work on all current children, new and existing records, # +load_target+ and the +loaded+ flag are your friends. class AssociationCollection < AssociationProxy #:nodoc: - def initialize(owner, reflection) - super - construct_sql - end - delegate :group, :order, :limit, :joins, :where, :preload, :eager_load, :includes, :from, :lock, :readonly, :having, :to => :scoped def select(select = nil) @@ -36,7 +31,7 @@ module ActiveRecord end def scoped - with_scope(construct_scope) { @reflection.klass.scoped } + with_scope(@scope) { @reflection.klass.scoped } end def find(*args) @@ -58,9 +53,7 @@ module ActiveRecord merge_options_from_reflection!(options) construct_find_options!(options) - find_scope = construct_scope[:find].slice(:conditions, :order) - - with_scope(:find => find_scope) do + with_scope(:find => @scope[:find].slice(:conditions, :order)) do relation = @reflection.klass.send(:construct_finder_arel, options, @reflection.klass.send(:current_scoped_methods)) case args.first @@ -178,17 +171,18 @@ module ActiveRecord end end - # Count all records using SQL. If the +:counter_sql+ option is set for the association, it will - # be used for the query. If no +:counter_sql+ was supplied, but +:finder_sql+ was set, the - # descendant's +construct_sql+ method will have set :counter_sql automatically. - # Otherwise, construct options and pass them with scope to the target class's +count+. + # Count all records using SQL. If the +:counter_sql+ or +:finder_sql+ option is set for the + # association, it will be used for the query. Otherwise, construct options and pass them with + # scope to the target class's +count+. def count(column_name = nil, options = {}) column_name, options = nil, column_name if column_name.is_a?(Hash) - if @reflection.options[:counter_sql] && !options.blank? - raise ArgumentError, "If finder_sql/counter_sql is used then options cannot be passed" - elsif @reflection.options[:counter_sql] - @reflection.klass.count_by_sql(@counter_sql) + if @reflection.options[:counter_sql] || @reflection.options[:finder_sql] + unless options.blank? + raise ArgumentError, "If finder_sql/counter_sql is used then options cannot be passed" + end + + @reflection.klass.count_by_sql(custom_counter_sql) else if @reflection.options[:uniq] @@ -197,7 +191,7 @@ module ActiveRecord options.merge!(:distinct => true) end - value = @reflection.klass.send(:with_scope, construct_scope) { @reflection.klass.count(column_name, options) } + value = @reflection.klass.send(:with_scope, @scope) { @reflection.klass.count(column_name, options) } limit = @reflection.options[:limit] offset = @reflection.options[:offset] @@ -377,18 +371,6 @@ module ActiveRecord def construct_find_options!(options) end - def construct_counter_sql - if @reflection.options[:counter_sql] - @counter_sql = interpolate_sql(@reflection.options[:counter_sql]) - elsif @reflection.options[:finder_sql] - # replace the SELECT clause with COUNT(*), preserving any hints within /* ... */ - @reflection.options[:counter_sql] = @reflection.options[:finder_sql].sub(/SELECT\b(\/\*.*?\*\/ )?(.*)\bFROM\b/im) { "SELECT #{$1}COUNT(*) FROM" } - @counter_sql = interpolate_sql(@reflection.options[:counter_sql]) - else - @counter_sql = @finder_sql - end - end - def load_target if !@owner.new_record? || foreign_key_present begin @@ -434,9 +416,9 @@ module ActiveRecord elsif @reflection.klass.scopes[method] @_named_scopes_cache ||= {} @_named_scopes_cache[method] ||= {} - @_named_scopes_cache[method][args] ||= with_scope(construct_scope) { @reflection.klass.send(method, *args) } + @_named_scopes_cache[method][args] ||= with_scope(@scope) { @reflection.klass.send(method, *args) } else - with_scope(construct_scope) do + with_scope(@scope) do if block_given? @reflection.klass.send(method, *args) { |*block_args| yield(*block_args) } else @@ -446,9 +428,19 @@ module ActiveRecord end end - # overloaded in derived Association classes to provide useful scoping depending on association type. - def construct_scope - {} + def custom_counter_sql + if @reflection.options[:counter_sql] + counter_sql = @reflection.options[:counter_sql] + else + # replace the SELECT clause with COUNT(*), preserving any hints within /* ... */ + counter_sql = @reflection.options[:finder_sql].sub(/SELECT\b(\/\*.*?\*\/ )?(.*)\bFROM\b/im) { "SELECT #{$1}COUNT(*) FROM" } + end + + interpolate_sql(counter_sql) + end + + def custom_finder_sql + interpolate_sql(@reflection.options[:finder_sql]) end def reset_target! @@ -462,7 +454,7 @@ module ActiveRecord def find_target records = if @reflection.options[:finder_sql] - @reflection.klass.find_by_sql(@finder_sql) + @reflection.klass.find_by_sql(custom_finder_sql) else find(:all) end @@ -494,7 +486,7 @@ module ActiveRecord ensure_owner_is_not_new scoped_where = scoped.where_values_hash - create_scope = scoped_where ? construct_scope[:create].merge(scoped_where) : construct_scope[:create] + create_scope = scoped_where ? @scope[:create].merge(scoped_where) : @scope[:create] record = @reflection.klass.send(:with_scope, :create => create_scope) do @reflection.build_association(attrs) end diff --git a/activerecord/lib/active_record/associations/association_proxy.rb b/activerecord/lib/active_record/associations/association_proxy.rb index f333f4d603..0c12c3737d 100644 --- a/activerecord/lib/active_record/associations/association_proxy.rb +++ b/activerecord/lib/active_record/associations/association_proxy.rb @@ -61,6 +61,7 @@ module ActiveRecord reflection.check_validity! Array.wrap(reflection.options[:extend]).each { |ext| proxy_extend(ext) } reset + construct_scope end # Returns the owner of the proxy. @@ -203,6 +204,24 @@ module ActiveRecord @reflection.klass.send :with_scope, *args, &block end + # Construct the scope used for find/create queries on the target + def construct_scope + @scope = { + :find => construct_find_scope, + :create => construct_create_scope + } + end + + # Implemented by subclasses + def construct_find_scope + raise NotImplementedError + end + + # Implemented by (some) subclasses + def construct_create_scope + {} + end + private # Forwards any missing method call to the \target. def method_missing(method, *args) diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb index 2eb56e5cd3..34b6cd5576 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -50,19 +50,21 @@ module ActiveRecord "find" end - options = @reflection.options.dup - (options.keys - [:select, :include, :readonly]).each do |key| - options.delete key - end - options[:conditions] = conditions + options = @reflection.options.dup.slice(:select, :include, :readonly) - the_target = @reflection.klass.send(find_method, - @owner[@reflection.primary_key_name], - options - ) if @owner[@reflection.primary_key_name] + the_target = with_scope(:find => @scope[:find]) do + @reflection.klass.send(find_method, + @owner[@reflection.primary_key_name], + options + ) if @owner[@reflection.primary_key_name] + end set_inverse_instance(the_target, @owner) the_target end + + def construct_find_scope + { :conditions => conditions } + end def foreign_key_present !@owner[@reflection.primary_key_name].nil? diff --git a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb index e429806b0c..a0df860623 100644 --- a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb @@ -44,20 +44,20 @@ module ActiveRecord end end + def construct_find_scope + { :conditions => conditions } + end + def find_target return nil if association_class.nil? - target = - if @reflection.options[:conditions] - association_class.find( - @owner[@reflection.primary_key_name], - :select => @reflection.options[:select], - :conditions => conditions, - :include => @reflection.options[:include] - ) - else - association_class.find(@owner[@reflection.primary_key_name], :select => @reflection.options[:select], :include => @reflection.options[:include]) - end + target = association_class.send(:with_scope, :find => @scope[:find]) do + association_class.find( + @owner[@reflection.primary_key_name], + :select => @reflection.options[:select], + :include => @reflection.options[:include] + ) + end set_inverse_instance(target, @owner) target end diff --git a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb index eb65234dfb..1fc9aba5cf 100644 --- a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb +++ b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb @@ -24,7 +24,7 @@ module ActiveRecord protected def construct_find_options!(options) - options[:joins] = Arel::SqlLiteral.new @join_sql + options[:joins] = Arel::SqlLiteral.new(@scope[:find][:joins]) options[:readonly] = finding_with_ambiguous_select?(options[:select] || @reflection.options[:select]) options[:select] ||= (@reflection.options[:select] || Arel::SqlLiteral.new('*')) end @@ -80,27 +80,26 @@ module ActiveRecord ).delete end end - - def construct_sql - if @reflection.options[:finder_sql] - @finder_sql = interpolate_sql(@reflection.options[:finder_sql]) - else - @finder_sql = "#{@owner.connection.quote_table_name @reflection.options[:join_table]}.#{@reflection.primary_key_name} = #{owner_quoted_id} " - @finder_sql << " AND (#{conditions})" if conditions - end - - @join_sql = "INNER JOIN #{@owner.connection.quote_table_name @reflection.options[:join_table]} ON #{@reflection.quoted_table_name}.#{@reflection.klass.primary_key} = #{@owner.connection.quote_table_name @reflection.options[:join_table]}.#{@reflection.association_foreign_key}" - - construct_counter_sql + + def construct_joins + "INNER JOIN #{@owner.connection.quote_table_name @reflection.options[:join_table]} ON #{@reflection.quoted_table_name}.#{@reflection.klass.primary_key} = #{@owner.connection.quote_table_name @reflection.options[:join_table]}.#{@reflection.association_foreign_key}" end - def construct_scope - { :find => { :conditions => @finder_sql, - :joins => @join_sql, - :readonly => false, - :order => @reflection.options[:order], - :include => @reflection.options[:include], - :limit => @reflection.options[:limit] } } + def construct_conditions + sql = "#{@owner.connection.quote_table_name @reflection.options[:join_table]}.#{@reflection.primary_key_name} = #{owner_quoted_id} " + sql << " AND (#{conditions})" if conditions + sql + end + + def construct_find_scope + { + :conditions => construct_conditions, + :joins => construct_joins, + :readonly => false, + :order => @reflection.options[:order], + :include => @reflection.options[:include], + :limit => @reflection.options[:limit] + } end # Join tables with additional columns on top of the two foreign keys must be considered diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index 978fc74560..830a82980d 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -6,14 +6,10 @@ module ActiveRecord # If the association has a :through option further specialization # is provided by its child HasManyThroughAssociation. class HasManyAssociation < AssociationCollection #:nodoc: - def initialize(owner, reflection) - @finder_sql = nil - super - end protected def owner_quoted_id if @reflection.options[:primary_key] - quote_value(@owner.send(@reflection.options[:primary_key])) + @owner.class.quote_value(@owner.send(@reflection.options[:primary_key])) else @owner.quoted_id end @@ -35,10 +31,10 @@ module ActiveRecord def count_records count = if has_cached_counter? @owner.send(:read_attribute, cached_counter_attribute_name) - elsif @reflection.options[:counter_sql] - @reflection.klass.count_by_sql(@counter_sql) + elsif @reflection.options[:counter_sql] || @reflection.options[:finder_sql] + @reflection.klass.count_by_sql(custom_counter_sql) else - @reflection.klass.count(:conditions => @counter_sql, :include => @reflection.options[:include]) + @reflection.klass.count(@scope[:find].slice(:conditions, :joins, :include)) end # If there's nothing in the database and @target has no new records @@ -87,36 +83,32 @@ module ActiveRecord false end - def construct_sql - case - when @reflection.options[:finder_sql] - @finder_sql = interpolate_sql(@reflection.options[:finder_sql]) - - when @reflection.options[:as] - @finder_sql = - "#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_id = #{owner_quoted_id} AND " + - "#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_type = #{@owner.class.quote_value(@owner.class.base_class.name.to_s)}" - @finder_sql << " AND (#{conditions})" if conditions - - else - @finder_sql = "#{@reflection.quoted_table_name}.#{@reflection.primary_key_name} = #{owner_quoted_id}" - @finder_sql << " AND (#{conditions})" if conditions + def construct_conditions + if @reflection.options[:as] + sql = + "#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_id = #{owner_quoted_id} AND " + + "#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_type = #{@owner.class.quote_value(@owner.class.base_class.name.to_s)}" + else + sql = "#{@reflection.quoted_table_name}.#{@reflection.primary_key_name} = #{owner_quoted_id}" end - - construct_counter_sql + sql << " AND (#{conditions})" if conditions + sql end - def construct_scope + def construct_find_scope + { + :conditions => construct_conditions, + :readonly => false, + :order => @reflection.options[:order], + :limit => @reflection.options[:limit], + :include => @reflection.options[:include] + } + end + + def construct_create_scope create_scoping = {} set_belongs_to_association_for(create_scoping) - { - :find => { :conditions => @finder_sql, - :readonly => false, - :order => @reflection.options[:order], - :limit => @reflection.options[:limit], - :include => @reflection.options[:include]}, - :create => create_scoping - } + create_scoping end def we_can_set_the_inverse_on_this?(record) 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 f0ad166802..419a3d385e 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -93,21 +93,7 @@ module ActiveRecord def find_target return [] unless target_reflection_has_associated_record? - with_scope(construct_scope) { @reflection.klass.find(:all) } - end - - def construct_sql - case - when @reflection.options[:finder_sql] - @finder_sql = interpolate_sql(@reflection.options[:finder_sql]) - - @finder_sql = "#{@reflection.quoted_table_name}.#{@reflection.primary_key_name} = #{owner_quoted_id}" - @finder_sql << " AND (#{conditions})" if conditions - else - @finder_sql = construct_conditions - end - - construct_counter_sql + with_scope(@scope) { @reflection.klass.find(:all) } end def has_cached_counter? diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index a6e6bfa356..17901387e9 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -2,11 +2,6 @@ module ActiveRecord # = Active Record Belongs To Has One Association module Associations class HasOneAssociation < AssociationProxy #:nodoc: - def initialize(owner, reflection) - super - construct_sql - end - def create(attrs = {}, replace_existing = true) new_record(replace_existing) do |reflection| attrs = merge_with_conditions(attrs) @@ -79,33 +74,31 @@ module ActiveRecord private def find_target - options = @reflection.options.dup - (options.keys - [:select, :order, :include, :readonly]).each do |key| - options.delete key - end - options[:conditions] = @finder_sql + options = @reflection.options.dup.slice(:select, :order, :include, :readonly) - the_target = @reflection.klass.find(:first, options) + the_target = with_scope(:find => @scope[:find]) do + @reflection.klass.find(:first, options) + end set_inverse_instance(the_target, @owner) the_target end - def construct_sql - case - when @reflection.options[:as] - @finder_sql = - "#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_id = #{owner_quoted_id} AND " + - "#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_type = #{@owner.class.quote_value(@owner.class.base_class.name.to_s)}" - else - @finder_sql = "#{@reflection.quoted_table_name}.#{@reflection.primary_key_name} = #{owner_quoted_id}" + def construct_find_scope + if @reflection.options[:as] + sql = + "#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_id = #{owner_quoted_id} AND " + + "#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_type = #{@owner.class.quote_value(@owner.class.base_class.name.to_s)}" + else + sql = "#{@reflection.quoted_table_name}.#{@reflection.primary_key_name} = #{owner_quoted_id}" end - @finder_sql << " AND (#{conditions})" if conditions + sql << " AND (#{conditions})" if conditions + { :conditions => sql } end - def construct_scope + def construct_create_scope create_scoping = {} set_belongs_to_association_for(create_scoping) - { :create => create_scoping } + create_scoping end def new_record(replace_existing) @@ -113,7 +106,7 @@ module ActiveRecord # instance. Otherwise, if the target has not previously been loaded # elsewhere, the instance we create will get orphaned. load_target if replace_existing - record = @reflection.klass.send(:with_scope, :create => construct_scope[:create]) do + record = @reflection.klass.send(:with_scope, :create => @scope[:create]) do yield @reflection end 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 8153eb7c57..de962e01b6 100644 --- a/activerecord/lib/active_record/associations/has_one_through_association.rb +++ b/activerecord/lib/active_record/associations/has_one_through_association.rb @@ -35,7 +35,7 @@ module ActiveRecord private def find_target - with_scope(construct_scope) { @reflection.klass.find(:first) } + with_scope(@scope) { @reflection.klass.find(:first) } 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 51ab8869ed..261b4037a3 100644 --- a/activerecord/lib/active_record/associations/through_association_scope.rb +++ b/activerecord/lib/active_record/associations/through_association_scope.rb @@ -7,9 +7,8 @@ module ActiveRecord protected - def construct_scope - scope = {} - scope[:find] = { + def construct_find_scope + { :conditions => construct_conditions, :joins => construct_joins, :include => @reflection.options[:include] || @reflection.source_reflection.options[:include], @@ -18,8 +17,10 @@ module ActiveRecord :limit => @reflection.options[:limit], :readonly => @reflection.options[:readonly] } - scope[:create] = construct_owner_attributes(@reflection) unless @reflection.nested? - scope + end + + def construct_create_scope + @reflection.nested? ? {} : construct_owner_attributes(@reflection) end # Build SQL conditions from attributes, qualified by table name. diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index 21a9a1f2cb..f3f89fe7c3 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -313,8 +313,8 @@ module ActiveRecord end end - # reconstruct the SQL queries now that we know the owner's id - association.send(:construct_sql) if association.respond_to?(:construct_sql) + # reconstruct the scope now that we know the owner's id + association.send(:construct_scope) if association.respond_to?(:construct_scope) end end From 9ec07348749675110843c44f680da79223218db2 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Tue, 19 Oct 2010 00:27:40 +0100 Subject: [PATCH 035/100] Properly support conditions on any of the reflections involved in a nested through association --- .../lib/active_record/associations.rb | 15 ++- .../associations/through_association_scope.rb | 127 +++++++++--------- activerecord/lib/active_record/reflection.rb | 53 +++++++- .../cascaded_eager_loading_test.rb | 6 +- .../test/cases/associations/eager_test.rb | 4 +- ...sted_has_many_through_associations_test.rb | 40 +++++- activerecord/test/cases/batches_test.rb | 2 +- activerecord/test/cases/finder_test.rb | 6 +- activerecord/test/cases/relations_test.rb | 18 +-- activerecord/test/fixtures/posts.yml | 14 ++ activerecord/test/fixtures/taggings.yml | 28 ++++ activerecord/test/fixtures/tags.yml | 4 + activerecord/test/models/author.rb | 7 +- activerecord/test/models/post.rb | 5 + activerecord/test/models/tagging.rb | 3 +- activerecord/test/schema/schema.rb | 1 + 16 files changed, 235 insertions(+), 98 deletions(-) diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 1111033435..75e5eb8ee4 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -2164,8 +2164,10 @@ module ActiveRecord chain = through_reflection_chain.reverse foreign_table = parent_table + index = 0 - chain.zip(@tables).each do |reflection, table| + chain.each do |reflection| + table = @tables[index] conditions = [] if reflection.source_reflection.nil? @@ -2234,13 +2236,14 @@ module ActiveRecord conditions << table[key].eq(foreign_table[foreign_key]) - conditions << reflection_conditions(reflection, table) + conditions << reflection_conditions(index, table) conditions << sti_conditions(reflection, table) - relation = relation.join(table, join_type).on(*conditions.compact) + relation = relation.join(table, join_type).on(*conditions.flatten.compact) # The current table in this iteration becomes the foreign table in the next foreign_table = table + index += 1 end relation @@ -2325,10 +2328,10 @@ module ActiveRecord @tables end - def reflection_conditions(reflection, table) - if reflection.options[:conditions] + def reflection_conditions(index, table) + @reflection.through_conditions.reverse[index].map do |condition| Arel.sql(interpolate_sql(sanitize_sql( - reflection.options[:conditions], + condition, table.table_alias || table.name ))) end diff --git a/activerecord/lib/active_record/associations/through_association_scope.rb b/activerecord/lib/active_record/associations/through_association_scope.rb index 261b4037a3..feb0a93360 100644 --- a/activerecord/lib/active_record/associations/through_association_scope.rb +++ b/activerecord/lib/active_record/associations/through_association_scope.rb @@ -24,7 +24,6 @@ module ActiveRecord end # Build SQL conditions from attributes, qualified by table name. - # TODO: Conditions on joins def construct_conditions reflection = @reflection.through_reflection_chain.last @@ -34,11 +33,12 @@ module ActiveRecord table_alias = table_aliases[reflection] end - conditions = construct_quoted_owner_attributes(reflection).map do |attr, value| + parts = construct_quoted_owner_attributes(reflection).map do |attr, value| "#{table_alias}.#{attr} = #{value}" end - conditions << sql_conditions if sql_conditions - "(" + conditions.join(') AND (') + ")" + parts += reflection_conditions(0) + + "(" + parts.join(') AND (') + ")" end # Associate attributes pointing to owner, quoted. @@ -55,23 +55,21 @@ module ActiveRecord end end - def construct_from - @reflection.table_name - end - def construct_select(custom_select = nil) distinct = "DISTINCT " if @reflection.options[:uniq] selected = custom_select || @reflection.options[:select] || "#{distinct}#{@reflection.quoted_table_name}.*" end def construct_joins(custom_joins = nil) - # p @reflection.through_reflection_chain + # TODO: Remove this at the end + #p @reflection.through_reflection_chain + #p @reflection.through_conditions "#{construct_through_joins} #{@reflection.options[:joins]} #{custom_joins}" end def construct_through_joins - joins = [] + joins, right_index = [], 1 # Iterate over each pair in the through reflection chain, joining them together @reflection.through_reflection_chain.each_cons(2) do |left, right| @@ -86,20 +84,23 @@ module ActiveRecord joins << inner_join_sql( right_table_and_alias, table_aliases[left], left.klass.primary_key, - table_aliases[right], left.primary_key_name + table_aliases[right], left.primary_key_name, + reflection_conditions(right_index) ) 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) + polymorphic_conditions(left, left), + reflection_conditions(right_index) ) when :has_and_belongs_to_many joins << inner_join_sql( right_table_and_alias, table_aliases[left].first, left.primary_key_name, - table_aliases[right], right.klass.primary_key + table_aliases[right], right.klass.primary_key, + reflection_conditions(right_index) ) end else @@ -109,7 +110,8 @@ module ActiveRecord right_table_and_alias, table_aliases[left], left.klass.primary_key, table_aliases[right], left.source_reflection.primary_key_name, - source_type_conditions(left) + source_type_conditions(left), + reflection_conditions(right_index) ) when :has_many, :has_one if right.macro == :has_and_belongs_to_many @@ -123,7 +125,8 @@ module ActiveRecord 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) + polymorphic_conditions(left, left.source_reflection), + reflection_conditions(right_index) ) if right.macro == :has_and_belongs_to_many @@ -151,10 +154,13 @@ module ActiveRecord joins << inner_join_sql( right_table_and_alias, join_table, left.source_reflection.primary_key_name, - table_aliases[right], right.klass.primary_key + table_aliases[right], right.klass.primary_key, + reflection_conditions(right_index) ) end end + + right_index += 1 end joins.join(" ") @@ -206,18 +212,45 @@ module ActiveRecord "#{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 - ] + def inner_join_sql(table, on_left_table, on_left_key, on_right_table, on_right_key, *conditions) + conditions << "#{on_left_table}.#{on_left_key} = #{on_right_table}.#{on_right_key}" + conditions = conditions.flatten.compact + conditions = conditions.map { |sql| "(#{sql})" } * ' AND ' + + "INNER JOIN #{table} ON #{conditions}" + end + + def reflection_conditions(index) + reflection = @reflection.through_reflection_chain[index] + reflection_conditions = @reflection.through_conditions[index] + + conditions = [] + + if reflection.options[:as].nil? && # reflection.klass is a Module if :as is used + reflection.klass.finder_needs_type_condition? + conditions << reflection.klass.send(:type_condition).to_sql + end + + reflection_conditions.each do |condition| + sanitized_condition = reflection.klass.send(:sanitize_sql, condition) + interpolated_condition = interpolate_sql(sanitized_condition) + + if condition.is_a?(Hash) + interpolated_condition.gsub!( + @reflection.quoted_table_name, + reflection.quoted_table_name + ) + end + + conditions << interpolated_condition + end + + conditions end def polymorphic_conditions(reflection, polymorphic_reflection) if polymorphic_reflection.options[:as] - "AND %s.%s = %s" % [ + "%s.%s = %s" % [ table_aliases[reflection], "#{polymorphic_reflection.options[:as]}_type", @owner.class.quote_value(polymorphic_reflection.active_record.base_class.name) ] @@ -226,7 +259,7 @@ module ActiveRecord def source_type_conditions(reflection) if reflection.options[:source_type] - "AND %s.%s = %s" % [ + "%s.%s = %s" % [ table_aliases[reflection.through_reflection], reflection.source_reflection.options[:foreign_type].to_s, @owner.class.quote_value(reflection.options[:source_type]) @@ -245,6 +278,8 @@ module ActiveRecord end # Construct attributes for :through pointing to owner and associate. + # This method is used when adding records to the association. Since this only makes sense for + # non-nested through associations, that's the only case we have to worry about here. def construct_join_attributes(associate) # TODO: revisit this to allow it for deletion, supposing dependent option is supported raise ActiveRecord::HasManyThroughCantAssociateThroughHasOneOrManyReflection.new(@owner, @reflection) if [:has_one, :has_many].include?(@reflection.source_reflection.macro) @@ -261,48 +296,6 @@ module ActiveRecord join_attributes end - - def conditions - @conditions = build_conditions unless defined?(@conditions) - @conditions - end - - def build_conditions - association_conditions = @reflection.options[:conditions] - through_conditions = build_through_conditions - source_conditions = @reflection.source_reflection.options[:conditions] - uses_sti = !@reflection.through_reflection.klass.descends_from_active_record? - - if association_conditions || through_conditions || source_conditions || uses_sti - all = [] - - [association_conditions, source_conditions].each do |conditions| - all << interpolate_sql(sanitize_sql(conditions)) if conditions - end - - all << through_conditions if through_conditions - all << build_sti_condition if uses_sti - - all.map { |sql| "(#{sql})" } * ' AND ' - end - end - - def build_through_conditions - conditions = @reflection.through_reflection.options[:conditions] - if conditions.is_a?(Hash) - interpolate_sql(@reflection.through_reflection.klass.send(:sanitize_sql, conditions)).gsub( - @reflection.quoted_table_name, - @reflection.through_reflection.quoted_table_name) - elsif conditions - interpolate_sql(sanitize_sql(conditions)) - end - end - - def build_sti_condition - @reflection.through_reflection.klass.send(:type_condition).to_sql - end - - alias_method :sql_conditions, :conditions def ensure_not_nested if @reflection.nested? diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index ee63fcfce2..d8bd6c9873 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -253,6 +253,10 @@ module ActiveRecord def through_reflection_chain [self] end + + def through_conditions + [Array.wrap(options[:conditions])] + end def through_reflection_primary_key_name end @@ -378,9 +382,9 @@ module ActiveRecord # 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. + 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 @@ -396,6 +400,49 @@ module ActiveRecord 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 + def nested? through_reflection_chain.length > 2 end diff --git a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb index 0e9c8a2639..5b24d49a7d 100644 --- a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb +++ b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb @@ -15,7 +15,7 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase authors = Author.find(:all, :include=>{:posts=>:comments}, :order=>"authors.id") assert_equal 3, authors.size assert_equal 5, authors[0].posts.size - assert_equal 2, 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 @@ -23,7 +23,7 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase authors = Author.find(:all, :include=>[{:posts=>:comments}, :categorizations], :order=>"authors.id") assert_equal 3, authors.size assert_equal 5, authors[0].posts.size - assert_equal 2, 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 @@ -56,7 +56,7 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase authors = Author.find(:all, :include=>{:posts=>[:comments, :categorizations]}, :order=>"authors.id") assert_equal 3, authors.size assert_equal 5, authors[0].posts.size - assert_equal 2, 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 diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index 2ff0714e9f..6b910ae2a0 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") - [:misc_by_mary, :misc_by_bob, :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 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 index 03ec4281d8..c39ec5d139 100644 --- a/activerecord/test/cases/associations/nested_has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/nested_has_many_through_associations_test.rb @@ -92,8 +92,6 @@ class NestedHasManyThroughAssociationsTest < ActiveRecord::TestCase assert_no_queries do assert_equal [luke, david, david], authors.first.subscribers.sort_by(&:nick) end - - # TODO: Add eager loading test using LEFT OUTER JOIN end # has_many through @@ -325,7 +323,7 @@ class NestedHasManyThroughAssociationsTest < ActiveRecord::TestCase 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) + 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) @@ -406,6 +404,42 @@ class NestedHasManyThroughAssociationsTest < ActiveRecord::TestCase end end + def test_nested_has_many_through_with_conditions_on_through_associations + blue, bob = tags(:blue), authors(:bob) + + assert_equal [blue], bob.misc_post_first_blue_tags + + # Pointless condition to force single-query loading + assert_includes_and_joins_equal( + Author.where('tags.id = tags.id'), + [bob], :misc_post_first_blue_tags + ) + + 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 } + 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_source_associations + blue, bob = tags(:blue), authors(:bob) + + assert_equal [blue], bob.misc_post_first_blue_tags_2 + + # Pointless condition to force single-query loading + assert_includes_and_joins_equal( + Author.where('tags.id = tags.id'), + [bob], :misc_post_first_blue_tags_2 + ) + + authors = assert_queries(4) { Author.includes(:misc_post_first_blue_tags_2).to_a } + assert_no_queries do + assert_equal [blue], authors[2].misc_post_first_blue_tags_2 + end + end + private def assert_includes_and_joins_equal(query, expected, association) diff --git a/activerecord/test/cases/batches_test.rb b/activerecord/test/cases/batches_test.rb index 70883ad30f..9e72ac4250 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(5) 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 0476fc94df..4c9475f1cd 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -123,11 +123,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],[2,9],[3,8]], 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/relations_test.rb b/activerecord/test/cases/relations_test.rb index df4e84ca29..0d88c8eded 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -501,22 +501,22 @@ class RelationTest < ActiveRecord::TestCase def test_count posts = Post.scoped - assert_equal 9, posts.count - assert_equal 9, posts.count(:all) - assert_equal 9, 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 7, 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 9, 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 9, posts.select(:comments_count).count(:distinct => false) + assert_equal 11, 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 9, 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') @@ -541,12 +541,12 @@ class RelationTest < ActiveRecord::TestCase def test_size posts = Post.scoped - assert_queries(1) { assert_equal 9, 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 7, 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/posts.yml b/activerecord/test/fixtures/posts.yml index ca6d4c2fe1..264ca164f0 100644 --- a/activerecord/test/fixtures/posts.yml +++ b/activerecord/test/fixtures/posts.yml @@ -64,3 +64,17 @@ misc_by_mary: 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/taggings.yml b/activerecord/test/fixtures/taggings.yml index 7cc7198ded..a337cce019 100644 --- a/activerecord/test/fixtures/taggings.yml +++ b/activerecord/test/fixtures/taggings.yml @@ -38,3 +38,31 @@ misc_post_by_mary: 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 6cb886dc46..d4b7c9a4d5 100644 --- a/activerecord/test/fixtures/tags.yml +++ b/activerecord/test/fixtures/tags.yml @@ -5,3 +5,7 @@ general: misc: id: 2 name: Misc + +blue: + id: 3 + name: Blue diff --git a/activerecord/test/models/author.rb b/activerecord/test/models/author.rb index b5f702018a..c0e082836d 100644 --- a/activerecord/test/models/author.rb +++ b/activerecord/test/models/author.rb @@ -86,7 +86,7 @@ class Author < ActiveRecord::Base 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 :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 @@ -103,6 +103,11 @@ class Author < ActiveRecord::Base 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/post.rb b/activerecord/test/models/post.rb index f3b78c3647..281586b438 100644 --- a/activerecord/test/models/post.rb +++ b/activerecord/test/models/post.rb @@ -64,6 +64,11 @@ class Post < ActiveRecord::Base has_many :funky_tags, :through => :taggings, :source => :tag has_many :super_tags, :through => :taggings 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/tagging.rb b/activerecord/test/models/tagging.rb index a1fa1a9750..c92df88e71 100644 --- a/activerecord/test/models/tagging.rb +++ b/activerecord/test/models/tagging.rb @@ -6,5 +6,6 @@ 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 :taggable, :polymorphic => true, :counter_cache => true -end \ No newline at end of file +end diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 8b9c56b895..ee129162a6 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -537,6 +537,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 :tags, :force => true do |t| From 596cc3b2329a9cc4a30c95c157ce36b2d08975df Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Tue, 19 Oct 2010 12:47:19 +0100 Subject: [PATCH 036/100] Respect the :primary_key option on the through_reflection of (non-nested) through associations --- .../associations/has_many_association.rb | 6 +++--- .../associations/has_one_association.rb | 6 +++--- .../associations/through_association_scope.rb | 4 ++-- .../has_many_through_associations_test.rb | 19 +++++++++++++++++- .../has_one_through_associations_test.rb | 20 ++++++++++++++++++- activerecord/test/fixtures/essays.yml | 6 ++++++ activerecord/test/models/author.rb | 12 ++++++++++- activerecord/test/models/essay.rb | 1 + activerecord/test/schema/schema.rb | 2 ++ 9 files changed, 65 insertions(+), 11 deletions(-) create mode 100644 activerecord/test/fixtures/essays.yml diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index 830a82980d..7eaa05ee36 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -7,9 +7,9 @@ module ActiveRecord # is provided by its child HasManyThroughAssociation. class HasManyAssociation < AssociationCollection #:nodoc: protected - def owner_quoted_id - if @reflection.options[:primary_key] - @owner.class.quote_value(@owner.send(@reflection.options[:primary_key])) + def owner_quoted_id(reflection = @reflection) + if reflection.options[:primary_key] + @owner.class.quote_value(@owner.send(reflection.options[:primary_key])) else @owner.quoted_id end diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index 17901387e9..c6bcfec275 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -64,9 +64,9 @@ module ActiveRecord end protected - def owner_quoted_id - if @reflection.options[:primary_key] - @owner.class.quote_value(@owner.send(@reflection.options[:primary_key])) + def owner_quoted_id(reflection = @reflection) + if reflection.options[:primary_key] + @owner.class.quote_value(@owner.send(reflection.options[:primary_key])) else @owner.quoted_id end diff --git a/activerecord/lib/active_record/associations/through_association_scope.rb b/activerecord/lib/active_record/associations/through_association_scope.rb index feb0a93360..86ceb1a204 100644 --- a/activerecord/lib/active_record/associations/through_association_scope.rb +++ b/activerecord/lib/active_record/associations/through_association_scope.rb @@ -44,14 +44,14 @@ module ActiveRecord # Associate attributes pointing to owner, quoted. def construct_quoted_owner_attributes(reflection) if as = reflection.options[:as] - { "#{as}_id" => owner_quoted_id, + { "#{as}_id" => owner_quoted_id(reflection), "#{as}_type" => reflection.klass.quote_value( @owner.class.base_class.name.to_s, reflection.klass.columns_hash["#{as}_type"]) } elsif reflection.macro == :belongs_to { reflection.klass.primary_key => @owner.class.quote_value(@owner[reflection.primary_key_name]) } else - { reflection.primary_key_name => owner_quoted_id } + { reflection.primary_key_name => owner_quoted_id(reflection) } end 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 4b9f49f1ec..5a2e6b26aa 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -17,11 +17,14 @@ require 'models/developer' require 'models/subscriber' require 'models/book' require 'models/subscription' +require 'models/essay' +require 'models/category' class HasManyThroughAssociationsTest < ActiveRecord::TestCase fixtures :posts, :readers, :people, :comments, :authors, :owners, :pets, :toys, :jobs, :references, :companies, - :subscribers, :books, :subscriptions, :developers + :subscribers, :books, :subscriptions, :developers, + :essays, :categories # Dummies to force column loads so query counts are clean. def setup @@ -449,4 +452,18 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase comment = post.comments.build assert author.comments.include?(comment) end + + def test_has_many_through_polymorphic_with_primary_key_option_on_through_reflection + 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 + end + + def test_has_many_through_with_primary_key_option_on_through_reflection + 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 end 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 5d153147f5..8805968869 100644 --- a/activerecord/test/cases/associations/has_one_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_through_associations_test.rb @@ -9,9 +9,13 @@ require 'models/member_detail' require 'models/minivan' require 'models/dashboard' require 'models/speedometer' +require 'models/category' +require 'models/author' +require 'models/essay' class HasOneThroughAssociationsTest < ActiveRecord::TestCase - fixtures :member_types, :members, :clubs, :memberships, :sponsors, :organizations, :minivans, :dashboards, :speedometers + fixtures :member_types, :members, :clubs, :memberships, :sponsors, :organizations, :minivans, + :dashboards, :speedometers, :categories, :authors, :essays def setup @member = members(:groucho) @@ -212,4 +216,18 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase minivan.dashboard end end + + def test_has_one_through_polymorphic_with_primary_key_option_on_through_reflection + 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 + end + + def test_has_one_through_with_primary_key_option_on_through_reflection + 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 end diff --git a/activerecord/test/fixtures/essays.yml b/activerecord/test/fixtures/essays.yml new file mode 100644 index 0000000000..8c96a469e6 --- /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: 1 + author_id: David diff --git a/activerecord/test/models/author.rb b/activerecord/test/models/author.rb index c0e082836d..1ba01d6b6b 100644 --- a/activerecord/test/models/author.rb +++ b/activerecord/test/models/author.rb @@ -95,8 +95,18 @@ class Author < ActiveRecord::Base 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_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 :essays_2, :primary_key => :name, :class_name => 'Essay', :foreign_key => :author_id + has_many :essay_categories_2, :through => :essays_2, :source => :category belongs_to :author_address, :dependent => :destroy belongs_to :author_address_extra, :dependent => :delete, :class_name => "AuthorAddress" diff --git a/activerecord/test/models/essay.rb b/activerecord/test/models/essay.rb index 6c28f5e49b..6a62042863 100644 --- a/activerecord/test/models/essay.rb +++ b/activerecord/test/models/essay.rb @@ -1,3 +1,4 @@ class Essay < ActiveRecord::Base belongs_to :writer, :primary_key => :name, :polymorphic => true + belongs_to :category end diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index ee129162a6..b5bf9a7349 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -214,6 +214,8 @@ ActiveRecord::Schema.define do t.string :name t.string :writer_id t.string :writer_type + t.integer :category_id + t.integer :author_id end create_table :events, :force => true do |t| From 01838636c6136d9a649ace71db61bb7990f9bd82 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Tue, 19 Oct 2010 14:14:06 +0100 Subject: [PATCH 037/100] Support for :primary_key option on the source reflection of a through association, where the source is a has_one or has_many --- activerecord/lib/active_record/associations.rb | 10 ++++------ .../associations/through_association_scope.rb | 4 ++-- activerecord/lib/active_record/reflection.rb | 8 ++++++++ .../has_many_through_associations_test.rb | 12 +++++++++--- .../has_one_through_associations_test.rb | 12 +++++++++--- activerecord/test/fixtures/essays.yml | 2 +- activerecord/test/fixtures/owners.yml | 1 + activerecord/test/models/author.rb | 2 ++ activerecord/test/models/essay.rb | 3 ++- activerecord/test/schema/schema.rb | 5 +++-- 10 files changed, 41 insertions(+), 18 deletions(-) diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 75e5eb8ee4..29f1c7b81d 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -2173,13 +2173,11 @@ module ActiveRecord if reflection.source_reflection.nil? case reflection.macro when :belongs_to - key = reflection.options[:primary_key] || - reflection.klass.primary_key + key = reflection.association_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 + foreign_key = reflection.active_record_primary_key conditions << polymorphic_conditions(reflection, table) when :has_and_belongs_to_many @@ -2209,13 +2207,13 @@ module ActiveRecord else case reflection.source_reflection.macro when :belongs_to - key = reflection.klass.primary_key + key = reflection.source_reflection.association_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 + foreign_key = reflection.source_reflection.active_record_primary_key when :has_and_belongs_to_many table, join_table = table diff --git a/activerecord/lib/active_record/associations/through_association_scope.rb b/activerecord/lib/active_record/associations/through_association_scope.rb index 86ceb1a204..2b2229f01f 100644 --- a/activerecord/lib/active_record/associations/through_association_scope.rb +++ b/activerecord/lib/active_record/associations/through_association_scope.rb @@ -108,7 +108,7 @@ module ActiveRecord when :belongs_to joins << inner_join_sql( right_table_and_alias, - table_aliases[left], left.klass.primary_key, + table_aliases[left], left.source_reflection.association_primary_key, table_aliases[right], left.source_reflection.primary_key_name, source_type_conditions(left), reflection_conditions(right_index) @@ -124,7 +124,7 @@ module ActiveRecord joins << inner_join_sql( right_table_and_alias, table_aliases[left], left.source_reflection.primary_key_name, - right_table, right.klass.primary_key, + right_table, left.source_reflection.active_record_primary_key, polymorphic_conditions(left, left.source_reflection), reflection_conditions(right_index) ) diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index d8bd6c9873..a46597e497 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -217,6 +217,14 @@ module ActiveRecord def association_foreign_key @association_foreign_key ||= @options[:association_foreign_key] || class_name.foreign_key end + + def association_primary_key + @association_primary_key ||= @options[:primary_key] || klass.primary_key + end + + def active_record_primary_key + @active_record_primary_key ||= @options[:primary_key] || active_record.primary_key + end def counter_cache_column if options[:counter_cache] == true 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 5a2e6b26aa..713c492f5e 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -19,12 +19,13 @@ require 'models/book' require 'models/subscription' require 'models/essay' require 'models/category' +require 'models/owner' class HasManyThroughAssociationsTest < ActiveRecord::TestCase fixtures :posts, :readers, :people, :comments, :authors, :owners, :pets, :toys, :jobs, :references, :companies, :subscribers, :books, :subscriptions, :developers, - :essays, :categories + :essays, :categories, :owners # Dummies to force column loads so query counts are clean. def setup @@ -453,14 +454,19 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert author.comments.include?(comment) end - def test_has_many_through_polymorphic_with_primary_key_option_on_through_reflection + 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_on_through_reflection + 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) 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 8805968869..39e14b4bfd 100644 --- a/activerecord/test/cases/associations/has_one_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_through_associations_test.rb @@ -12,10 +12,11 @@ require 'models/speedometer' require 'models/category' require 'models/author' require 'models/essay' +require 'models/owner' class HasOneThroughAssociationsTest < ActiveRecord::TestCase fixtures :member_types, :members, :clubs, :memberships, :sponsors, :organizations, :minivans, - :dashboards, :speedometers, :categories, :authors, :essays + :dashboards, :speedometers, :categories, :authors, :essays, :owners def setup @member = members(:groucho) @@ -217,14 +218,19 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase end end - def test_has_one_through_polymorphic_with_primary_key_option_on_through_reflection + 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_on_through_reflection + 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) diff --git a/activerecord/test/fixtures/essays.yml b/activerecord/test/fixtures/essays.yml index 8c96a469e6..9d15d82359 100644 --- a/activerecord/test/fixtures/essays.yml +++ b/activerecord/test/fixtures/essays.yml @@ -2,5 +2,5 @@ david_modest_proposal: name: A Modest Proposal writer_type: Author writer_id: David - category_id: 1 + category_id: General author_id: David 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/models/author.rb b/activerecord/test/models/author.rb index 1ba01d6b6b..53b3b80950 100644 --- a/activerecord/test/models/author.rb +++ b/activerecord/test/models/author.rb @@ -98,12 +98,14 @@ class Author < ActiveRecord::Base 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 diff --git a/activerecord/test/models/essay.rb b/activerecord/test/models/essay.rb index 6a62042863..ec4b982b5b 100644 --- a/activerecord/test/models/essay.rb +++ b/activerecord/test/models/essay.rb @@ -1,4 +1,5 @@ class Essay < ActiveRecord::Base belongs_to :writer, :primary_key => :name, :polymorphic => true - belongs_to :category + belongs_to :category, :primary_key => :name + has_one :owner, :primary_key => :name end diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index b5bf9a7349..de3baaf4ab 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -214,8 +214,8 @@ ActiveRecord::Schema.define do t.string :name t.string :writer_id t.string :writer_type - t.integer :category_id - t.integer :author_id + t.string :category_id + t.string :author_id end create_table :events, :force => true do |t| @@ -369,6 +369,7 @@ ActiveRecord::Schema.define do t.string :name t.column :updated_at, :datetime t.column :happy_at, :datetime + t.string :essay_id end From 9ff5fdeda99b3d8c5148d4c40956842518d1c788 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Tue, 19 Oct 2010 14:56:59 +0100 Subject: [PATCH 038/100] Remove unused methods --- activerecord/lib/active_record/reflection.rb | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index a46597e497..6078191773 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -266,9 +266,6 @@ module ActiveRecord [Array.wrap(options[:conditions])] end - def through_reflection_primary_key_name - end - def source_reflection nil end @@ -489,14 +486,6 @@ module ActiveRecord check_validity_of_inverse! end - def through_reflection_primary_key - through_reflection.belongs_to? ? through_reflection.klass.primary_key : through_reflection.primary_key_name - end - - def through_reflection_primary_key_name - through_reflection.primary_key_name if through_reflection.belongs_to? - end - private def derive_class_name # get the class_name of the belongs_to association of the through reflection From 0ceb34295501a797c9e549c581ecee17f837f01c Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Tue, 19 Oct 2010 15:24:30 +0100 Subject: [PATCH 039/100] Bugfix/refactoring --- activerecord/lib/active_record/associations.rb | 12 ++++++------ .../associations/through_association_scope.rb | 10 +++++----- activerecord/lib/active_record/reflection.rb | 9 +++++++++ 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 29f1c7b81d..9e000f2aae 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -2207,18 +2207,18 @@ module ActiveRecord else case reflection.source_reflection.macro when :belongs_to - key = reflection.source_reflection.association_primary_key - foreign_key = reflection.source_reflection.primary_key_name + key = reflection.association_primary_key + foreign_key = reflection.primary_key_name conditions << source_type_conditions(reflection, foreign_table) when :has_many, :has_one - key = reflection.source_reflection.primary_key_name + key = reflection.primary_key_name foreign_key = reflection.source_reflection.active_record_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 + join_key = reflection.primary_key_name + join_foreign_key = reflection.klass.primary_key relation = relation.join(join_table, join_type).on( join_table[join_key]. @@ -2228,7 +2228,7 @@ module ActiveRecord foreign_table = join_table key = reflection.klass.primary_key - foreign_key = reflection.source_reflection.association_foreign_key + foreign_key = reflection.association_foreign_key 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 2b2229f01f..1365851337 100644 --- a/activerecord/lib/active_record/associations/through_association_scope.rb +++ b/activerecord/lib/active_record/associations/through_association_scope.rb @@ -108,8 +108,8 @@ module ActiveRecord when :belongs_to joins << inner_join_sql( right_table_and_alias, - table_aliases[left], left.source_reflection.association_primary_key, - table_aliases[right], left.source_reflection.primary_key_name, + table_aliases[left], left.association_primary_key, + table_aliases[right], left.primary_key_name, source_type_conditions(left), reflection_conditions(right_index) ) @@ -123,7 +123,7 @@ module ActiveRecord joins << inner_join_sql( right_table_and_alias, - table_aliases[left], left.source_reflection.primary_key_name, + table_aliases[left], left.primary_key_name, right_table, left.source_reflection.active_record_primary_key, polymorphic_conditions(left, left.source_reflection), reflection_conditions(right_index) @@ -148,12 +148,12 @@ module ActiveRecord join_table ), left_table, left.klass.primary_key, - join_table, left.source_reflection.association_foreign_key + join_table, left.association_foreign_key ) joins << inner_join_sql( right_table_and_alias, - join_table, left.source_reflection.primary_key_name, + join_table, left.primary_key_name, table_aliases[right], right.klass.primary_key, reflection_conditions(right_index) ) diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 6078191773..7ce2bbb8ae 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -357,6 +357,8 @@ 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 :primary_key_name, :association_foreign_key, :to => :source_reflection + # Gets the source of the through reflection. It checks both a singularized # and pluralized form for :belongs_to or :has_many. # @@ -451,6 +453,13 @@ module ActiveRecord def nested? through_reflection_chain.length > 2 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 ||= source_reflection.options[:primary_key] || klass.primary_key + end # Gets an array of possible :through source reflection names: # From 915ea5ea826d48107e4c1953c7a32cf26727d10e Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Tue, 19 Oct 2010 16:13:06 +0100 Subject: [PATCH 040/100] Support the :primary_key option on a through reflection in a nested through association --- .../associations/through_association_scope.rb | 7 ++----- ...nested_has_many_through_associations_test.rb | 17 ++++++++++++++++- activerecord/test/fixtures/authors.yml | 2 ++ activerecord/test/models/author.rb | 3 +++ activerecord/test/models/organization.rb | 8 +++++++- activerecord/test/schema/schema.rb | 2 ++ 6 files changed, 32 insertions(+), 7 deletions(-) diff --git a/activerecord/lib/active_record/associations/through_association_scope.rb b/activerecord/lib/active_record/associations/through_association_scope.rb index 1365851337..649bbd206a 100644 --- a/activerecord/lib/active_record/associations/through_association_scope.rb +++ b/activerecord/lib/active_record/associations/through_association_scope.rb @@ -76,14 +76,11 @@ module ActiveRecord 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[left], left.association_primary_key, table_aliases[right], left.primary_key_name, reflection_conditions(right_index) ) @@ -91,7 +88,7 @@ module ActiveRecord joins << inner_join_sql( right_table_and_alias, table_aliases[left], left.primary_key_name, - table_aliases[right], right.klass.primary_key, + table_aliases[right], right.association_primary_key, polymorphic_conditions(left, left), reflection_conditions(right_index) ) 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 index c39ec5d139..3a4601b032 100644 --- a/activerecord/test/cases/associations/nested_has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/nested_has_many_through_associations_test.rb @@ -21,6 +21,7 @@ require 'models/organization' require 'models/category' require 'models/categorization' require 'models/membership' +require 'models/essay' # 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 @@ -31,7 +32,7 @@ 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, - :categorizations, :memberships + :categorizations, :memberships, :essays # Through associations can either use the has_many or has_one macros. # @@ -440,6 +441,20 @@ class NestedHasManyThroughAssociationsTest < ActiveRecord::TestCase end 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) diff --git a/activerecord/test/fixtures/authors.yml b/activerecord/test/fixtures/authors.yml index 6f13ec4dac..832236a486 100644 --- a/activerecord/test/fixtures/authors.yml +++ b/activerecord/test/fixtures/authors.yml @@ -3,6 +3,8 @@ 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 diff --git a/activerecord/test/models/author.rb b/activerecord/test/models/author.rb index 53b3b80950..dd8a20ce9b 100644 --- a/activerecord/test/models/author.rb +++ b/activerecord/test/models/author.rb @@ -110,6 +110,9 @@ class Author < ActiveRecord::Base 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_extra, :dependent => :delete, :class_name => "AuthorAddress" diff --git a/activerecord/test/models/organization.rb b/activerecord/test/models/organization.rb index 1da342a0bd..c18c28c696 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/schema/schema.rb b/activerecord/test/schema/schema.rb index de3baaf4ab..c77651a782 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -44,6 +44,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| From b00db54e746ab0f1664d7f160b46beb49587b370 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Tue, 19 Oct 2010 16:23:53 +0100 Subject: [PATCH 041/100] Small refactoring --- activerecord/lib/active_record/associations.rb | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 9e000f2aae..028157d7e9 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -2186,9 +2186,7 @@ module ActiveRecord # 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_key = reflection.primary_key_name join_foreign_key = reflection.active_record.primary_key relation = relation.join(join_table, join_type).on( @@ -2199,10 +2197,8 @@ module ActiveRecord # 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 + foreign_key = reflection.association_foreign_key end else case reflection.source_reflection.macro From 1f7415ab3a5b433ecfb0c10d66343a894d73914a Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Tue, 19 Oct 2010 16:26:35 +0100 Subject: [PATCH 042/100] Fix broken test --- activerecord/test/cases/json_serialization_test.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/activerecord/test/cases/json_serialization_test.rb b/activerecord/test/cases/json_serialization_test.rb index 430be003ac..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 From 82b889f7d37249adaa606558d4c05356b3e84d9a Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Tue, 19 Oct 2010 17:22:42 +0100 Subject: [PATCH 043/100] Add explicit tests for the nested through association changes in reflection.rb --- activerecord/lib/active_record/reflection.rb | 15 ++++- activerecord/test/cases/reflection_test.rb | 64 ++++++++++++++++++++ activerecord/test/models/author.rb | 1 + activerecord/test/models/post.rb | 1 + activerecord/test/models/tagging.rb | 1 + 5 files changed, 80 insertions(+), 2 deletions(-) diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 7ce2bbb8ae..3448cc506c 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -450,15 +450,26 @@ module ActiveRecord 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_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 ||= source_reflection.options[:primary_key] || klass.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 :through source reflection names: diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb index eeb619ac2f..a85ba623e1 100644 --- a/activerecord/test/cases/reflection_test.rb +++ b/activerecord/test/cases/reflection_test.rb @@ -7,6 +7,16 @@ require 'models/subscriber' require 'models/ship' require 'models/pirate' require 'models/price_estimate' +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' class ReflectionTest < ActiveRecord::TestCase include ActiveRecord::Reflection @@ -190,6 +200,60 @@ class ReflectionTest < ActiveRecord::TestCase def test_has_many_through_reflection 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 + # 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 + + # 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 + assert_equal "nick", Subscriber.reflect_on_association(:subscriptions).active_record_primary_key.to_s + assert_equal "name", Author.reflect_on_association(:essay).active_record_primary_key.to_s + end def test_collection_association assert Pirate.reflect_on_association(:birds).collection? diff --git a/activerecord/test/models/author.rb b/activerecord/test/models/author.rb index dd8a20ce9b..7dcfbd268b 100644 --- a/activerecord/test/models/author.rb +++ b/activerecord/test/models/author.rb @@ -90,6 +90,7 @@ class Author < ActiveRecord::Base 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 diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb index 281586b438..7c919a55eb 100644 --- a/activerecord/test/models/post.rb +++ b/activerecord/test/models/post.rb @@ -63,6 +63,7 @@ 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'" diff --git a/activerecord/test/models/tagging.rb b/activerecord/test/models/tagging.rb index c92df88e71..c6ff8d390b 100644 --- a/activerecord/test/models/tagging.rb +++ b/activerecord/test/models/tagging.rb @@ -7,5 +7,6 @@ class Tagging < ActiveRecord::Base 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 :taggable, :polymorphic => true, :counter_cache => true end From 7ee33b80a2048ec3801f02018b0ea81d2abe0011 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Tue, 19 Oct 2010 17:29:19 +0100 Subject: [PATCH 044/100] Remove various comments and code which were just being used during the development of nested through association support (OMFGZ, I might just have nearly finished this\! --- .../lib/active_record/associations.rb | 8 - .../associations/nested_has_many_through.rb | 158 ------------------ .../associations/through_association_scope.rb | 4 - activerecord/lib/active_record/reflection.rb | 24 --- ...sted_has_many_through_associations_test.rb | 5 - 5 files changed, 199 deletions(-) delete mode 100644 activerecord/lib/active_record/associations/nested_has_many_through.rb diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 028157d7e9..44d3258c40 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -39,14 +39,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}.") diff --git a/activerecord/lib/active_record/associations/nested_has_many_through.rb b/activerecord/lib/active_record/associations/nested_has_many_through.rb deleted file mode 100644 index d699a60edb..0000000000 --- a/activerecord/lib/active_record/associations/nested_has_many_through.rb +++ /dev/null @@ -1,158 +0,0 @@ -# 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: - # - # * :joins: 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 - # * :remote_key: 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 - # * :local_key: the name of the key in the local table (not qualified by table name) which will - # take part in the join - # * :conditions: 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 649bbd206a..abe7af418d 100644 --- a/activerecord/lib/active_record/associations/through_association_scope.rb +++ b/activerecord/lib/active_record/associations/through_association_scope.rb @@ -61,10 +61,6 @@ module ActiveRecord end def construct_joins(custom_joins = nil) - # TODO: Remove this at the end - #p @reflection.through_reflection_chain - #p @reflection.through_conditions - "#{construct_through_joins} #{@reflection.options[:joins]} #{custom_joins}" end diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 3448cc506c..1ea892895f 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -131,14 +131,6 @@ 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 @@ -325,16 +317,6 @@ 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 @@ -497,12 +479,6 @@ module ActiveRecord raise HasManyThroughAssociationPolymorphicError.new(active_record.name, self, source_reflection) 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/nested_has_many_through_associations_test.rb b/activerecord/test/cases/associations/nested_has_many_through_associations_test.rb index 3a4601b032..23fa1709ce 100644 --- a/activerecord/test/cases/associations/nested_has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/nested_has_many_through_associations_test.rb @@ -23,11 +23,6 @@ require 'models/categorization' require 'models/membership' require 'models/essay' -# 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, From 2aa9388746412bc88be6a1728ecfbcc8ceacbb30 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Tue, 19 Oct 2010 17:40:14 +0100 Subject: [PATCH 045/100] Add some comments for ThroughReflection#through_reflection_chain --- activerecord/lib/active_record/reflection.rb | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 1ea892895f..824674ee1d 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -368,7 +368,16 @@ module ActiveRecord @through_reflection ||= active_record.reflect_on_association(options[:through]) end - # TODO: Documentation + # 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 From 2c7183c0260ca105c6440b31f60ac010891b69a9 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Tue, 19 Oct 2010 18:16:19 +0100 Subject: [PATCH 046/100] Add some API documentation about nested through associations --- .../lib/active_record/associations.rb | 60 ++++++++++++++++--- 1 file changed, 52 insertions(+), 8 deletions(-) diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 44d3258c40..379a4eb1ef 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -487,6 +487,49 @@ module ActiveRecord # @group.avatars << Avatar.new # this would work if User belonged_to Avatar rather than the other way around # @group.avatars.delete(@group.avatars.last) # so would this # + # === Nested Associations + # + # You can actually specify *any* association with the :through option, including an + # association which has a :through 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 tries to + # add a Commenter in the example above, there would be no way to tell how to set up the + # intermediate Post and Comment objects. + # # === Polymorphic Associations # # Polymorphic associations on models are not restricted on what types of models they @@ -934,10 +977,11 @@ module ActiveRecord # [:as] # Specifies a polymorphic interface (See belongs_to). # [:through] - # Specifies a join model through which to perform the query. Options for :class_name - # and :foreign_key are ignored, as the association uses the source reflection. You - # can only use a :through query through a belongs_to, has_one - # or has_many association on the join model. The collection of join models + # Specifies a join model through which to perform the query. Options for :class_name, + # :primary_key and :foreign_key are ignored, as the association uses the + # source reflection. You can use a :through association through any other, + # association, but if other :through associations are involved then the resulting + # association will be read-only. Otherwise, the collection of join models # can be managed via the collection API. For example, new join models are created for # newly associated objects, and if some are gone their rows are deleted (directly, # no destroy callbacks are triggered). @@ -1061,10 +1105,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 :class_name - # and :foreign_key are ignored, as the association uses the source reflection. You - # can only use a :through query through a has_one or belongs_to - # association on the join model. + # Specifies a Join Model through which to perform the query. Options for :class_name, + # :primary_key, and :foreign_key are ignored, as the association uses the + # source reflection. You can only use a :through query through a has_one + # or belongs_to association on the join model. # [:source] # Specifies the source association name used by has_one :through queries. # Only use it if the name cannot be inferred from the association. From 8aa69490833796ac6f373de746d6fc79dfc5482a Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Tue, 19 Oct 2010 18:21:32 +0100 Subject: [PATCH 047/100] Add a CHANGELOG entry about nested through associations --- activerecord/CHANGELOG | 2 ++ 1 file changed, 2 insertions(+) diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG index 75657cb6ee..191890aefc 100644 --- a/activerecord/CHANGELOG +++ b/activerecord/CHANGELOG @@ -1,5 +1,7 @@ *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 #1812 [Jon Leighton] + * The following code: Model.limit(10).scoping { Model.count } From cf7c475ef187e88044cba139cc2e1dbf5f180b15 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Tue, 19 Oct 2010 18:29:10 +0100 Subject: [PATCH 048/100] Remove obsolete autoload --- activerecord/lib/active_record/associations.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 379a4eb1ef..98a101fac4 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -109,7 +109,6 @@ 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' From 7b84477598137c6261bf2aeb5ce0d1b17e4b2b3c Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Tue, 19 Oct 2010 18:32:02 +0100 Subject: [PATCH 049/100] Fix typo --- activerecord/lib/active_record/associations.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 98a101fac4..25be9a52ff 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -525,7 +525,7 @@ module ActiveRecord # 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 tries to + # is not enough information to know what modification to make. For example, if you tried to # add a Commenter in the example above, there would be no way to tell how to set up the # intermediate Post and Comment objects. # From fcabfa428e57af115aca56f5c9aba99afae2cf7c Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Tue, 19 Oct 2010 18:43:09 +0100 Subject: [PATCH 050/100] Remove obsolete require to active_record/associations/nested_has_many_through --- .../active_record/associations/has_many_through_association.rb | 1 - 1 file changed, 1 deletion(-) 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 419a3d385e..2c9fa3b447 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -1,5 +1,4 @@ require "active_record/associations/through_association_scope" -require "active_record/associations/nested_has_many_through" require 'active_support/core_ext/object/blank' module ActiveRecord From d15de7d97f7080d8d3bc47bef89aa8a922f04c67 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Tue, 19 Oct 2010 19:25:44 +0100 Subject: [PATCH 051/100] Add explicit ordering to nested_has_many_through_associations_test.rb as this was causing failures under postgres --- ...sted_has_many_through_associations_test.rb | 69 ++++++++++--------- 1 file changed, 37 insertions(+), 32 deletions(-) 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 index 23fa1709ce..274ecdaba7 100644 --- a/activerecord/test/cases/associations/nested_has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/nested_has_many_through_associations_test.rb @@ -76,7 +76,7 @@ class NestedHasManyThroughAssociationsTest < ActiveRecord::TestCase luke, david = subscribers(:first), subscribers(:second) author = authors(:david) - assert_equal [luke, david, david], author.subscribers + assert_equal [luke, david, david], author.subscribers.order('subscribers.nick') # All authors with subscribers where one of the subscribers' nick is 'alterself' assert_includes_and_joins_equal( @@ -134,10 +134,11 @@ class NestedHasManyThroughAssociationsTest < ActiveRecord::TestCase 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 + assert_equal [groucho_details, other_details], + members(:groucho).organization_member_details.order('member_details.id') assert_includes_and_joins_equal( - Member.where('member_details.id' => member_details(:groucho).id), + Member.where('member_details.id' => member_details(:groucho).id).order('member_details.id'), [members(:groucho), members(:some_other_guy)], :organization_member_details ) @@ -145,9 +146,9 @@ class NestedHasManyThroughAssociationsTest < ActiveRecord::TestCase where('member_details.id' => 9) assert members.empty? - members = assert_queries(4) { Member.includes(:organization_member_details).to_a } + members = assert_queries(4) { Member.includes(:organization_member_details).to_a.sort_by(&:id) } assert_no_queries do - assert_equal [groucho_details, other_details], members.first.organization_member_details + assert_equal [groucho_details, other_details], members.first.organization_member_details.sort_by(&:id) end end @@ -157,10 +158,11 @@ class NestedHasManyThroughAssociationsTest < ActiveRecord::TestCase 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 + assert_equal [groucho_details, other_details], + members(:groucho).organization_member_details_2.order('member_details.id') assert_includes_and_joins_equal( - Member.where('member_details.id' => groucho_details.id), + Member.where('member_details.id' => groucho_details.id).order('member_details.id'), [members(:groucho), members(:some_other_guy)], :organization_member_details_2 ) @@ -168,9 +170,9 @@ class NestedHasManyThroughAssociationsTest < ActiveRecord::TestCase where('member_details.id' => 9) assert members.empty? - members = assert_queries(4) { Member.includes(:organization_member_details_2).to_a } + members = assert_queries(4) { Member.includes(:organization_member_details_2).to_a.sort_by(&:id) } assert_no_queries do - assert_equal [groucho_details, other_details], members.first.organization_member_details_2 + assert_equal [groucho_details, other_details], members.first.organization_member_details_2.sort_by(&:id) end end @@ -180,16 +182,16 @@ class NestedHasManyThroughAssociationsTest < ActiveRecord::TestCase 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 + assert_equal [general, cooking], authors(:bob).post_categories.order('categories.id') assert_includes_and_joins_equal( Author.where('categories.id' => cooking.id), [authors(:bob)], :post_categories ) - authors = assert_queries(3) { Author.includes(:post_categories).to_a } + authors = assert_queries(3) { Author.includes(:post_categories).to_a.sort_by(&:id) } assert_no_queries do - assert_equal [general, cooking], authors[2].post_categories + assert_equal [general, cooking], authors[2].post_categories.sort_by(&:id) end end @@ -199,16 +201,16 @@ class NestedHasManyThroughAssociationsTest < ActiveRecord::TestCase 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 + assert_equal [greetings, more], categories(:technology).post_comments.order('comments.id') assert_includes_and_joins_equal( - Category.where('comments.id' => more.id), + Category.where('comments.id' => more.id).order('comments.id'), [categories(:general), categories(:technology)], :post_comments ) - categories = assert_queries(3) { Category.includes(:post_comments).to_a } + categories = assert_queries(3) { Category.includes(:post_comments).to_a.sort_by(&:id) } assert_no_queries do - assert_equal [greetings, more], categories[1].post_comments + assert_equal [greetings, more], categories[1].post_comments.sort_by(&:id) end end @@ -218,16 +220,16 @@ class NestedHasManyThroughAssociationsTest < ActiveRecord::TestCase 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 + assert_equal [greetings, more], authors(:bob).category_post_comments.order('comments.id') assert_includes_and_joins_equal( - Author.where('comments.id' => comments(:does_it_hurt).id), + Author.where('comments.id' => comments(:does_it_hurt).id).order('comments.id'), [authors(:david), authors(:mary)], :category_post_comments ) - authors = assert_queries(5) { Author.includes(:category_post_comments).to_a } + authors = assert_queries(5) { Author.includes(:category_post_comments).to_a.sort_by(&:id) } assert_no_queries do - assert_equal [greetings, more], authors[2].category_post_comments + assert_equal [greetings, more], authors[2].category_post_comments.sort_by(&:id) end end @@ -256,16 +258,17 @@ class NestedHasManyThroughAssociationsTest < ActiveRecord::TestCase 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 + assert_equal [welcome_general, thinking_general], + categorizations(:david_welcome_general).post_taggings.order('taggings.id') assert_includes_and_joins_equal( - Categorization.where('taggings.id' => welcome_general.id), + Categorization.where('taggings.id' => welcome_general.id).order('taggings.id'), [categorizations(:david_welcome_general)], :post_taggings ) - categorizations = assert_queries(4) { Categorization.includes(:post_taggings).to_a } + categorizations = assert_queries(4) { Categorization.includes(:post_taggings).to_a.sort_by(&:id) } assert_no_queries do - assert_equal [welcome_general, thinking_general], categorizations.first.post_taggings + assert_equal [welcome_general, thinking_general], categorizations.first.post_taggings.sort_by(&:id) end end @@ -282,7 +285,7 @@ class NestedHasManyThroughAssociationsTest < ActiveRecord::TestCase [members(:groucho)], :nested_member_type ) - members = assert_queries(4) { Member.includes(:nested_member_type).to_a } + members = assert_queries(4) { Member.includes(:nested_member_type).to_a.sort_by(&:id) } assert_no_queries do assert_equal founding, members.first.nested_member_type end @@ -301,7 +304,7 @@ class NestedHasManyThroughAssociationsTest < ActiveRecord::TestCase [members(:blarpy_winkup)], :club_category ) - members = assert_queries(4) { Member.includes(:club_category).to_a } + members = assert_queries(4) { Member.includes(:club_category).to_a.sort_by(&:id) } assert_no_queries do assert_equal general, members.first.club_category end @@ -314,12 +317,14 @@ class NestedHasManyThroughAssociationsTest < ActiveRecord::TestCase 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 + 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) + 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) @@ -333,7 +338,7 @@ class NestedHasManyThroughAssociationsTest < ActiveRecord::TestCase 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 [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) @@ -341,7 +346,7 @@ class NestedHasManyThroughAssociationsTest < ActiveRecord::TestCase end def test_has_many_through_with_foreign_key_option_on_source_reflection - assert_equal [people(:michael), people(:susan)], jobs(:unicyclist).agents + assert_equal [people(:michael), people(:susan)], jobs(:unicyclist).agents.order('people.id') jobs = Job.joins(:agents) assert_equal [jobs(:unicyclist), jobs(:unicyclist)], jobs @@ -413,7 +418,7 @@ class NestedHasManyThroughAssociationsTest < ActiveRecord::TestCase 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 } + authors = assert_queries(3) { Author.includes(:misc_post_first_blue_tags).to_a.sort_by(&:id) } assert_no_queries do assert_equal [blue], authors[2].misc_post_first_blue_tags end @@ -430,7 +435,7 @@ class NestedHasManyThroughAssociationsTest < ActiveRecord::TestCase [bob], :misc_post_first_blue_tags_2 ) - authors = assert_queries(4) { Author.includes(:misc_post_first_blue_tags_2).to_a } + authors = assert_queries(4) { Author.includes(:misc_post_first_blue_tags_2).to_a.sort_by(&:id) } assert_no_queries do assert_equal [blue], authors[2].misc_post_first_blue_tags_2 end From 383d545c88266ef579f225bc8bfb3d3b807ca5bc Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Tue, 19 Oct 2010 19:33:25 +0100 Subject: [PATCH 052/100] Add explicit ordering in relations_test.rb, as the lack of this was causing failures against postgres --- activerecord/test/cases/relations_test.rb | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index 0d88c8eded..0b143224be 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -251,27 +251,27 @@ 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 assert_queries(2) do - posts = Post.preload(:comments).to_a + posts = Post.preload(:comments).order('posts.id').to_a assert posts.first.comments.first end assert_queries(2) do - posts = Post.preload(:author) + posts = Post.preload(:author).order('posts.id') assert posts.first.author end assert_queries(2) do - posts = Post.preload(:author).to_a + posts = Post.preload(:author).order('posts.id').to_a assert posts.first.author end assert_queries(3) do - posts = Post.preload(:author, :comments).to_a + posts = Post.preload(:author, :comments).order('posts.id').to_a assert posts.first.author assert posts.first.comments.first end @@ -279,22 +279,22 @@ 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 assert_queries(2) do - posts = Post.scoped.includes(:comments) + posts = Post.scoped.includes(:comments).order('posts.id') assert posts.first.comments.first end assert_queries(2) do - posts = Post.includes(:author) + posts = Post.includes(:author).order('posts.id') assert posts.first.author end assert_queries(3) do - posts = Post.includes(:author, :comments).to_a + posts = Post.includes(:author, :comments).order('posts.id').to_a assert posts.first.author assert posts.first.comments.first end From 8e53e058acea471eab7a1609dc150aa9fdbfa833 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Tue, 19 Oct 2010 21:41:00 +0100 Subject: [PATCH 053/100] Rename nested_has_many_through_associations_test.rb because it does not only concern has_many associations --- ...associations_test.rb => nested_through_associations_test.rb} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename activerecord/test/cases/associations/{nested_has_many_through_associations_test.rb => nested_through_associations_test.rb} (99%) diff --git a/activerecord/test/cases/associations/nested_has_many_through_associations_test.rb b/activerecord/test/cases/associations/nested_through_associations_test.rb similarity index 99% rename from activerecord/test/cases/associations/nested_has_many_through_associations_test.rb rename to activerecord/test/cases/associations/nested_through_associations_test.rb index 274ecdaba7..bfc290e877 100644 --- a/activerecord/test/cases/associations/nested_has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/nested_through_associations_test.rb @@ -23,7 +23,7 @@ require 'models/categorization' require 'models/membership' require 'models/essay' -class NestedHasManyThroughAssociationsTest < ActiveRecord::TestCase +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, From 9a1a32ac2b8a526f543367bc7e8258bbd7e6a164 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Sun, 31 Oct 2010 11:21:28 +0000 Subject: [PATCH 054/100] Fix naughty trailing whitespace --- .../lib/active_record/association_preload.rb | 30 +-- .../lib/active_record/associations.rb | 98 +++++----- .../associations/alias_tracker.rb | 26 +-- .../has_many_through_association.rb | 4 +- .../has_one_through_association.rb | 2 +- .../associations/through_association_scope.rb | 64 +++---- activerecord/lib/active_record/reflection.rb | 42 ++-- .../has_many_through_associations_test.rb | 10 +- .../has_one_through_associations_test.rb | 12 +- .../nested_through_associations_test.rb | 180 +++++++++--------- activerecord/test/cases/reflection_test.rb | 18 +- activerecord/test/models/author.rb | 6 +- activerecord/test/models/categorization.rb | 2 +- activerecord/test/models/category.rb | 2 +- activerecord/test/models/job.rb | 2 +- activerecord/test/models/member.rb | 8 +- activerecord/test/models/member_detail.rb | 2 +- activerecord/test/models/organization.rb | 2 +- activerecord/test/models/person.rb | 2 +- activerecord/test/models/post.rb | 4 +- activerecord/test/models/reference.rb | 2 +- 21 files changed, 259 insertions(+), 259 deletions(-) diff --git a/activerecord/lib/active_record/association_preload.rb b/activerecord/lib/active_record/association_preload.rb index c3ccb93ffd..8e7416472f 100644 --- a/activerecord/lib/active_record/association_preload.rb +++ b/activerecord/lib/active_record/association_preload.rb @@ -210,9 +210,9 @@ module ActiveRecord return if records.first.send("loaded_#{reflection.name}?") records.each {|record| record.send("set_#{reflection.name}_target", nil)} end - + options = reflection.options - + if options[:through] records_with_through_records = preload_through_records(records, reflection, options[:through]) all_through_records = records_with_through_records.map(&:last).flatten @@ -220,10 +220,10 @@ module ActiveRecord unless all_through_records.empty? source = reflection.source_reflection.name all_through_records.first.class.preload_associations(all_through_records, source, options) - + records_with_through_records.each do |record, through_records| source_records = through_records.map(&source).flatten.compact - + case reflection.macro when :has_many, :has_and_belongs_to_many add_preloaded_records_to_collection([record], reflection.name, source_records) @@ -235,7 +235,7 @@ module ActiveRecord else id_to_record_map, ids = construct_id_map(records, reflection.options[:primary_key]) associated_records = find_associated_records(ids, reflection, preload_options) - + if reflection.macro == :has_many set_association_collection_records( id_to_record_map, reflection.name, @@ -249,7 +249,7 @@ module ActiveRecord end end end - + alias_method :preload_has_one_association, :preload_has_one_or_has_many_association alias_method :preload_has_many_association, :preload_has_one_or_has_many_association @@ -259,12 +259,12 @@ module ActiveRecord # record. This is so that we can preload the source association for each record, # and always be able to access the preloaded association regardless of where we # refer to the record. - # + # # Suffices to say, if AR had an identity map built in then this would be unnecessary. identity_map = {} - + options = {} - + if reflection.options[:source_type] interface = reflection.source_reflection.options[:foreign_type] options[:conditions] = ["#{connection.quote_column_name interface} = ?", reflection.options[:source_type]] @@ -272,20 +272,20 @@ module ActiveRecord else if reflection.options[:conditions] options[:include] = reflection.options[:include] || - reflection.options[:source] + reflection.options[:source] options[:conditions] = reflection.options[:conditions] end - + options[:order] = reflection.options[:order] end - + records.first.class.preload_associations(records, through_association, options) records.map do |record| if reflection.options[:source_type] # Dont cache the association - we would only be caching a subset proxy = record.send(through_association) - + if proxy.respond_to?(:target) through_records = proxy.target proxy.reset @@ -295,11 +295,11 @@ module ActiveRecord else through_records = record.send(through_association) end - + through_records = Array.wrap(through_records).map do |through_record| identity_map[through_record] ||= through_record end - + [record, through_records] end end diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 37dbff4061..f061c8da0f 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -56,7 +56,7 @@ module ActiveRecord super("Cannot dissociate new records through '#{owner.class.name}##{reflection.name}' on '#{reflection.source_reflection.class_name rescue nil}##{reflection.source_reflection.name rescue nil}'. Both records must have an id in order to delete the has_many :through record associating them.") 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.") @@ -487,7 +487,7 @@ module ActiveRecord # @group.avatars.delete(@group.avatars.last) # so would this # # === Nested Associations - # + # # You can actually specify *any* association with the :through option, including an # association which has a :through option itself. For example: # @@ -496,15 +496,15 @@ module ActiveRecord # 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 # @@ -514,19 +514,19 @@ module ActiveRecord # 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 Commenter in the example above, there would be no way to tell how to set up the + # add a Commenter in the example above, there would be no way to tell how to set up the # intermediate Post and Comment objects. # # === Polymorphic Associations @@ -2183,9 +2183,9 @@ module ActiveRecord # What type of join will be generated, either Arel::InnerJoin (default) or Arel::OuterJoin attr_accessor :join_type - + attr_reader :aliased_prefix - + 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 @@ -2198,13 +2198,13 @@ module ActiveRecord end super(reflection.klass) - + @reflection = reflection @join_dependency = join_dependency @parent = parent @join_type = Arel::InnerJoin @aliased_prefix = "t#{ join_dependency.join_parts.size }" - + setup_tables end @@ -2221,17 +2221,17 @@ module ActiveRecord end def join_to(relation) - # The chain starts with the target table, but we want to end with it here (makes + # 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 @@ -2240,25 +2240,25 @@ module ActiveRecord when :has_many, :has_one key = reflection.primary_key_name 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.primary_key_name 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 @@ -2267,41 +2267,41 @@ module ActiveRecord when :belongs_to key = reflection.association_primary_key foreign_key = reflection.primary_key_name - + conditions << source_type_conditions(reflection, foreign_table) when :has_many, :has_one key = reflection.primary_key_name foreign_key = reflection.source_reflection.active_record_primary_key when :has_and_belongs_to_many table, join_table = table - + join_key = reflection.primary_key_name 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) - + relation = relation.join(table, join_type).on(*conditions.flatten.compact) - + # The current table in this iteration becomes the foreign table in the next foreign_table = table index += 1 end - + relation end @@ -2317,11 +2317,11 @@ module ActiveRecord @tables.last end end - + def aliased_table_name table.table_alias || table.name end - + protected def table_alias_for(reflection, join = false) @@ -2336,7 +2336,7 @@ module ActiveRecord end private - + # 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. @@ -2346,44 +2346,44 @@ module ActiveRecord reflection.table_name, table_alias_for(reflection, reflection != self.reflection) ) - + table = Arel::Table.new( reflection.table_name, :engine => arel_engine, :as => aliased_table_name, :columns => reflection.klass.columns ) - + # 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 - + # 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! - + @tables end - + def reflection_conditions(index, table) @reflection.through_conditions.reverse[index].map do |condition| Arel.sql(interpolate_sql(sanitize_sql( @@ -2392,28 +2392,28 @@ module ActiveRecord ))) end end - + def sti_conditions(reflection, table) unless reflection.klass.descends_from_active_record? sti_column = table[reflection.klass.inheritance_column] - + condition = sti_column.eq(reflection.klass.sti_name) - + reflection.klass.descendants.each do |subclass| condition = condition.or(sti_column.eq(subclass.sti_name)) end - + condition 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"]. diff --git a/activerecord/lib/active_record/associations/alias_tracker.rb b/activerecord/lib/active_record/associations/alias_tracker.rb index 10e90ec117..64582188b6 100644 --- a/activerecord/lib/active_record/associations/alias_tracker.rb +++ b/activerecord/lib/active_record/associations/alias_tracker.rb @@ -11,10 +11,10 @@ module ActiveRecord @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? @@ -24,12 +24,12 @@ module ActiveRecord 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 @@ -41,30 +41,30 @@ module ActiveRecord 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 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 2c9fa3b447..c45f2ee224 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -66,7 +66,7 @@ module ActiveRecord def insert_record(record, force = true, validate = true) ensure_not_nested - + if record.new_record? if force record.save! @@ -83,7 +83,7 @@ module ActiveRecord # TODO - add dependent option support def delete_records(records) ensure_not_nested - + klass = @reflection.through_reflection.klass records.each do |associate| klass.delete_all(construct_join_attributes(associate)) 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 de962e01b6..e9dc32efd3 100644 --- a/activerecord/lib/active_record/associations/has_one_through_association.rb +++ b/activerecord/lib/active_record/associations/has_one_through_association.rb @@ -15,7 +15,7 @@ module ActiveRecord def create_through_record(new_value) #nodoc: ensure_not_nested - + klass = @reflection.through_reflection.klass current_object = @owner.send(@reflection.through_reflection.name) diff --git a/activerecord/lib/active_record/associations/through_association_scope.rb b/activerecord/lib/active_record/associations/through_association_scope.rb index abe7af418d..07ce6f1597 100644 --- a/activerecord/lib/active_record/associations/through_association_scope.rb +++ b/activerecord/lib/active_record/associations/through_association_scope.rb @@ -18,7 +18,7 @@ module ActiveRecord :readonly => @reflection.options[:readonly] } end - + def construct_create_scope @reflection.nested? ? {} : construct_owner_attributes(@reflection) end @@ -26,18 +26,18 @@ module ActiveRecord # Build SQL conditions from attributes, qualified by table name. def construct_conditions 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 - + parts = construct_quoted_owner_attributes(reflection).map do |attr, value| "#{table_alias}.#{attr} = #{value}" end parts += reflection_conditions(0) - + "(" + parts.join(') AND (') + ")" end @@ -59,18 +59,18 @@ 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) "#{construct_through_joins} #{@reflection.options[:joins]} #{custom_joins}" end def construct_through_joins joins, right_index = [], 1 - + # 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? case left.macro when :belongs_to @@ -113,7 +113,7 @@ module ActiveRecord else right_table = table_aliases[right] end - + joins << inner_join_sql( right_table_and_alias, table_aliases[left], left.primary_key_name, @@ -121,7 +121,7 @@ module ActiveRecord polymorphic_conditions(left, left.source_reflection), reflection_conditions(right_index) ) - + if right.macro == :has_and_belongs_to_many joins << inner_join_sql( table_name_and_alias( @@ -134,7 +134,7 @@ module ActiveRecord 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]), @@ -143,7 +143,7 @@ module ActiveRecord left_table, left.klass.primary_key, join_table, left.association_foreign_key ) - + joins << inner_join_sql( right_table_and_alias, join_table, left.primary_key_name, @@ -152,10 +152,10 @@ module ActiveRecord ) end end - + right_index += 1 end - + joins.join(" ") end @@ -170,77 +170,77 @@ module ActiveRecord 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, *conditions) conditions << "#{on_left_table}.#{on_left_key} = #{on_right_table}.#{on_right_key}" conditions = conditions.flatten.compact conditions = conditions.map { |sql| "(#{sql})" } * ' AND ' - + "INNER JOIN #{table} ON #{conditions}" end - + def reflection_conditions(index) reflection = @reflection.through_reflection_chain[index] reflection_conditions = @reflection.through_conditions[index] - + conditions = [] - + if reflection.options[:as].nil? && # reflection.klass is a Module if :as is used reflection.klass.finder_needs_type_condition? conditions << reflection.klass.send(:type_condition).to_sql end - + reflection_conditions.each do |condition| sanitized_condition = reflection.klass.send(:sanitize_sql, condition) interpolated_condition = interpolate_sql(sanitized_condition) - + if condition.is_a?(Hash) interpolated_condition.gsub!( @reflection.quoted_table_name, reflection.quoted_table_name ) end - + conditions << interpolated_condition end - + conditions end - + def polymorphic_conditions(reflection, polymorphic_reflection) if polymorphic_reflection.options[:as] "%s.%s = %s" % [ @@ -249,7 +249,7 @@ module ActiveRecord ] end end - + def source_type_conditions(reflection) if reflection.options[:source_type] "%s.%s = %s" % [ @@ -289,7 +289,7 @@ module ActiveRecord join_attributes end - + def ensure_not_nested if @reflection.nested? raise HasManyThroughNestedAssociationsAreReadonly.new(@owner, @reflection) diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 6eb2057f66..ba37fed3c7 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -209,11 +209,11 @@ module ActiveRecord def association_foreign_key @association_foreign_key ||= @options[:association_foreign_key] || class_name.foreign_key end - + def association_primary_key @association_primary_key ||= @options[:primary_key] || klass.primary_key end - + def active_record_primary_key @active_record_primary_key ||= @options[:primary_key] || active_record.primary_key end @@ -249,11 +249,11 @@ module ActiveRecord def through_reflection false end - + def through_reflection_chain [self] end - + def through_conditions [Array.wrap(options[:conditions])] end @@ -340,7 +340,7 @@ module ActiveRecord # in the Active Record class. class ThroughReflection < AssociationReflection #:nodoc: delegate :primary_key_name, :association_foreign_key, :to => :source_reflection - + # Gets the source of the through reflection. It checks both a singularized # and pluralized form for :belongs_to or :has_many. # @@ -367,14 +367,14 @@ module ActiveRecord def through_reflection @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. @@ -389,31 +389,31 @@ module ActiveRecord # 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 @@ -429,24 +429,24 @@ module ActiveRecord 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. @@ -458,7 +458,7 @@ module ActiveRecord while source_reflection.source_reflection source_reflection = source_reflection.source_reflection end - + source_reflection.options[:primary_key] || klass.primary_key end 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 713c492f5e..4e398751d2 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -456,19 +456,19 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase 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 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 39e14b4bfd..1cf8c0539d 100644 --- a/activerecord/test/cases/associations/has_one_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_through_associations_test.rb @@ -217,22 +217,22 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase minivan.dashboard 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 diff --git a/activerecord/test/cases/associations/nested_through_associations_test.rb b/activerecord/test/cases/associations/nested_through_associations_test.rb index bfc290e877..db7c8b6c45 100644 --- a/activerecord/test/cases/associations/nested_through_associations_test.rb +++ b/activerecord/test/cases/associations/nested_through_associations_test.rb @@ -30,18 +30,18 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase :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. @@ -51,18 +51,18 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase # 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 - + 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? - + authors = assert_queries(5) { Author.includes(:tags).to_a } assert_no_queries do assert_equal [general, general], authors.first.tags @@ -74,236 +74,236 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase # Through: has_many through def test_has_many_through_has_many_through_with_has_many_source_reflection luke, david = subscribers(:first), subscribers(:second) - + author = authors(:david) assert_equal [luke, david, david], author.subscribers.order('subscribers.nick') - + # 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 ) - + 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 - + # has_many through # Source: has_one through # Through: has_one def test_has_many_through_has_one_with_has_one_through_source_reflection founding = member_types(:founding) - + assert_equal [founding], members(:groucho).nested_member_types - + assert_includes_and_joins_equal( Member.where('member_types.id' => founding.id), [members(:groucho)], :nested_member_types ) - + members = assert_queries(4) { Member.includes(:nested_member_types).to_a } assert_no_queries do assert_equal [founding], members.first.nested_member_types end end - + # has_many through # Source: has_one # Through: has_one through def test_has_many_through_has_one_through_with_has_one_source_reflection mustache = sponsors(:moustache_club_sponsor_for_groucho) - + assert_equal [mustache], members(:groucho).nested_sponsors - + assert_includes_and_joins_equal( Member.where('sponsors.id' => mustache.id), [members(:groucho)], :nested_sponsors ) - + members = assert_queries(4) { Member.includes(:nested_sponsors).to_a } assert_no_queries do assert_equal [mustache], members.first.nested_sponsors end 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') - + 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? - + members = assert_queries(4) { Member.includes(:organization_member_details).to_a.sort_by(&:id) } assert_no_queries do assert_equal [groucho_details, other_details], members.first.organization_member_details.sort_by(&:id) end 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') - + assert_includes_and_joins_equal( Member.where('member_details.id' => groucho_details.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? - + members = assert_queries(4) { Member.includes(:organization_member_details_2).to_a.sort_by(&:id) } assert_no_queries do assert_equal [groucho_details, other_details], members.first.organization_member_details_2.sort_by(&:id) end 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') - + assert_includes_and_joins_equal( Author.where('categories.id' => cooking.id), [authors(:bob)], :post_categories ) - + authors = assert_queries(3) { Author.includes(:post_categories).to_a.sort_by(&:id) } assert_no_queries do assert_equal [general, cooking], authors[2].post_categories.sort_by(&:id) end 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') - + assert_includes_and_joins_equal( Category.where('comments.id' => more.id).order('comments.id'), [categories(:general), categories(:technology)], :post_comments ) - + categories = assert_queries(3) { Category.includes(:post_comments).to_a.sort_by(&:id) } assert_no_queries do assert_equal [greetings, more], categories[1].post_comments.sort_by(&:id) end 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') - + assert_includes_and_joins_equal( Author.where('comments.id' => comments(:does_it_hurt).id).order('comments.id'), [authors(:david), authors(:mary)], :category_post_comments ) - + authors = assert_queries(5) { Author.includes(:category_post_comments).to_a.sort_by(&:id) } assert_no_queries do assert_equal [greetings, more], authors[2].category_post_comments.sort_by(&:id) end end - + # has_many through # Source: belongs_to # Through: has_many through def test_has_many_through_has_many_through_with_belongs_to_source_reflection general = tags(:general) - + assert_equal [general, general], authors(:david).tagging_tags - + assert_includes_and_joins_equal( Author.where('tags.id' => tags(:general).id), [authors(:david)], :tagging_tags ) - + authors = assert_queries(5) { Author.includes(:tagging_tags).to_a } assert_no_queries do assert_equal [general, general], authors.first.tagging_tags end 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') - + assert_includes_and_joins_equal( Categorization.where('taggings.id' => welcome_general.id).order('taggings.id'), [categorizations(:david_welcome_general)], :post_taggings ) - + categorizations = assert_queries(4) { Categorization.includes(:post_taggings).to_a.sort_by(&:id) } assert_no_queries do assert_equal [welcome_general, thinking_general], categorizations.first.post_taggings.sort_by(&:id) end end - + # has_one through # Source: has_one through # Through: has_one def test_has_one_through_has_one_with_has_one_through_source_reflection founding = member_types(:founding) - + assert_equal founding, members(:groucho).nested_member_type - + assert_includes_and_joins_equal( Member.where('member_types.id' => founding.id), [members(:groucho)], :nested_member_type ) - + members = assert_queries(4) { Member.includes(:nested_member_type).to_a.sort_by(&:id) } assert_no_queries do assert_equal founding, members.first.nested_member_type end end - + # has_one through # Source: belongs_to # Through: has_one through def test_has_one_through_has_one_through_with_belongs_to_source_reflection general = categories(:general) - + assert_equal general, members(:groucho).club_category - + assert_includes_and_joins_equal( Member.where('categories.id' => categories(:technology).id), [members(:blarpy_winkup)], :club_category ) - + members = assert_queries(4) { Member.includes(:club_category).to_a.sort_by(&:id) } assert_no_queries do assert_equal general, members.first.club_category @@ -320,34 +320,34 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase 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 @@ -355,7 +355,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase 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? @@ -366,101 +366,101 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase 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 blue, bob = tags(:blue), authors(:bob) - + assert_equal [blue], bob.misc_post_first_blue_tags - + # Pointless condition to force single-query loading assert_includes_and_joins_equal( Author.where('tags.id = tags.id'), [bob], :misc_post_first_blue_tags ) - + 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) } 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_source_associations blue, bob = tags(:blue), authors(:bob) - + assert_equal [blue], bob.misc_post_first_blue_tags_2 - + # Pointless condition to force single-query loading assert_includes_and_joins_equal( Author.where('tags.id = tags.id'), [bob], :misc_post_first_blue_tags_2 ) - + authors = assert_queries(4) { Author.includes(:misc_post_first_blue_tags_2).to_a.sort_by(&:id) } assert_no_queries do assert_equal [blue], authors[2].misc_post_first_blue_tags_2 end 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 diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb index a85ba623e1..66fe754046 100644 --- a/activerecord/test/cases/reflection_test.rb +++ b/activerecord/test/cases/reflection_test.rb @@ -200,7 +200,7 @@ class ReflectionTest < ActiveRecord::TestCase def test_has_many_through_reflection assert_kind_of ThroughReflection, Subscriber.reflect_on_association(:books) end - + def test_through_reflection_chain expected = [ Author.reflect_on_association(:essay_categories), @@ -208,10 +208,10 @@ class ReflectionTest < ActiveRecord::TestCase 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'"], @@ -220,7 +220,7 @@ class ReflectionTest < ActiveRecord::TestCase ] 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%'"], [], @@ -229,27 +229,27 @@ class ReflectionTest < ActiveRecord::TestCase 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 # 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 - + # 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 assert_equal "nick", Subscriber.reflect_on_association(:subscriptions).active_record_primary_key.to_s assert_equal "name", Author.reflect_on_association(:essay).active_record_primary_key.to_s diff --git a/activerecord/test/models/author.rb b/activerecord/test/models/author.rb index 7dcfbd268b..43bfd93e60 100644 --- a/activerecord/test/models/author.rb +++ b/activerecord/test/models/author.rb @@ -96,7 +96,7 @@ class Author < ActiveRecord::Base 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 @@ -107,7 +107,7 @@ class Author < ActiveRecord::Base 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 @@ -119,7 +119,7 @@ class Author < ActiveRecord::Base 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 diff --git a/activerecord/test/models/categorization.rb b/activerecord/test/models/categorization.rb index bddc1e5f0c..8e2fa96498 100644 --- a/activerecord/test/models/categorization.rb +++ b/activerecord/test/models/categorization.rb @@ -2,6 +2,6 @@ class Categorization < ActiveRecord::Base belongs_to :post belongs_to :category belongs_to :author - + has_many :post_taggings, :through => :author, :source => :taggings end diff --git a/activerecord/test/models/category.rb b/activerecord/test/models/category.rb index c933943813..95825c72ef 100644 --- a/activerecord/test/models/category.rb +++ b/activerecord/test/models/category.rb @@ -23,7 +23,7 @@ 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 diff --git a/activerecord/test/models/job.rb b/activerecord/test/models/job.rb index 46b1d87aa1..f7b0e787b1 100644 --- a/activerecord/test/models/job.rb +++ b/activerecord/test/models/job.rb @@ -2,6 +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 bed62f8b7f..fbf0b80164 100644 --- a/activerecord/test/models/member.rb +++ b/activerecord/test/models/member.rb @@ -9,15 +9,15 @@ class Member < ActiveRecord::Base has_one :member_detail 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 end diff --git a/activerecord/test/models/member_detail.rb b/activerecord/test/models/member_detail.rb index 0f53b69ced..fe619f8732 100644 --- a/activerecord/test/models/member_detail.rb +++ b/activerecord/test/models/member_detail.rb @@ -2,6 +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 c18c28c696..4a4111833f 100644 --- a/activerecord/test/models/organization.rb +++ b/activerecord/test/models/organization.rb @@ -4,7 +4,7 @@ class Organization < ActiveRecord::Base 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 diff --git a/activerecord/test/models/person.rb b/activerecord/test/models/person.rb index d35c51b660..5a5b6f9626 100644 --- a/activerecord/test/models/person.rb +++ b/activerecord/test/models/person.rb @@ -13,7 +13,7 @@ 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 diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb index 68d2b79a3b..e9c8c02e45 100644 --- a/activerecord/test/models/post.rb +++ b/activerecord/test/models/post.rb @@ -46,7 +46,7 @@ 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 @@ -65,7 +65,7 @@ class Post < ActiveRecord::Base 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'" diff --git a/activerecord/test/models/reference.rb b/activerecord/test/models/reference.rb index 2feb15d706..87d4a71963 100644 --- a/activerecord/test/models/reference.rb +++ b/activerecord/test/models/reference.rb @@ -1,7 +1,7 @@ class Reference < ActiveRecord::Base belongs_to :person belongs_to :job - + has_many :agents_posts_authors, :through => :person end From 026dbd28d9dbaddb7cdd7d6fdc1349cc2f43242f Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Sun, 31 Oct 2010 11:16:16 +0000 Subject: [PATCH 055/100] Fix bug with 0bb85ed9ffa9808926b46e8f7e59cab5b85ac19f which missed out a fixtures declaration in cascaded_eager_loading_test.rb --- .../test/cases/associations/cascaded_eager_loading_test.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb index 81e99036af..d997385266 100644 --- a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb +++ b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb @@ -10,7 +10,8 @@ require 'models/reply' require 'models/person' class CascadedEagerLoadingTest < ActiveRecord::TestCase - fixtures :authors, :mixins, :companies, :posts, :topics, :accounts, :comments, :categorizations, :people + fixtures :authors, :mixins, :companies, :posts, :topics, :accounts, :comments, + :categorizations, :people, :categories def test_eager_association_loading_with_cascaded_two_levels authors = Author.find(:all, :include=>{:posts=>:comments}, :order=>"authors.id") From 083d6f267611472b8acfb9801e64971ee6d19994 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Sun, 31 Oct 2010 11:27:58 +0000 Subject: [PATCH 056/100] Update new tests in cascaded_eager_loading_test.rb to work with the modified fixtures in this branch --- .../associations/cascaded_eager_loading_test.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb index d997385266..dbc3bcf758 100644 --- a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb +++ b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb @@ -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 From 73c0b390b3a1ea9487c3f667352463a90af6dd71 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Fri, 4 Mar 2011 22:29:40 +0000 Subject: [PATCH 057/100] When preloading has_and_belongs_to_many associations, we should only instantiate one AR object per actual record in the database. (Even when IM is off.) --- .../preloader/has_and_belongs_to_many.rb | 6 ++++-- .../test/cases/associations/eager_test.rb | 16 ++++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/activerecord/lib/active_record/associations/preloader/has_and_belongs_to_many.rb b/activerecord/lib/active_record/associations/preloader/has_and_belongs_to_many.rb index e794f05340..24be279449 100644 --- a/activerecord/lib/active_record/associations/preloader/has_and_belongs_to_many.rb +++ b/activerecord/lib/active_record/associations/preloader/has_and_belongs_to_many.rb @@ -31,10 +31,12 @@ module ActiveRecord private # Once we have used the join table column (in super), we manually instantiate the - # actual records + # actual records, ensuring that we don't create more than one instances of the same + # record def associated_records_by_owner + records = {} super.each do |owner_key, rows| - rows.map! { |row| klass.instantiate(row) } + rows.map! { |row| records[row[klass.primary_key]] ||= klass.instantiate(row) } end end diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index ed6337b596..40c82f2fb8 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -525,6 +525,22 @@ class EagerAssociationTest < ActiveRecord::TestCase assert posts[1].categories.include?(categories(:general)) end + # This is only really relevant when the identity map is off. Since the preloader for habtm + # gets raw row hashes from the database and then instantiates them, this test ensures that + # it only instantiates one actual object per record from the database. + def test_has_and_belongs_to_many_should_not_instantiate_same_records_multiple_times + welcome = posts(:welcome) + categories = Category.includes(:posts) + + general = categories.find { |c| c == categories(:general) } + technology = categories.find { |c| c == categories(:technology) } + + post1 = general.posts.to_a.find { |p| p == posts(:welcome) } + post2 = technology.posts.to_a.find { |p| p == posts(:welcome) } + + assert_equal post1.object_id, post2.object_id + end + def test_eager_with_has_many_and_limit_and_conditions_on_the_eagers posts = authors(:david).posts.find(:all, :include => :comments, From b5b5558d2f5347707862b8eeb1816da7c02a1d90 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Fri, 4 Mar 2011 21:21:34 +0000 Subject: [PATCH 058/100] Fix a couple of tests in join_model_test.rb which were failing when the identity map is turned off --- activerecord/lib/active_record/associations/association.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index 86904ea2bc..67752da2a5 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -191,8 +191,8 @@ module ActiveRecord else attributes[reflection.foreign_key] = owner[reflection.active_record_primary_key] - if options[:as] - attributes["#{options[:as]}_type"] = owner.class.base_class.name + if reflection.options[:as] + attributes[reflection.type] = owner.class.base_class.name end end attributes From 4206eff1895dccadcbec471798bfbd129404cc94 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Fri, 4 Mar 2011 22:36:44 +0000 Subject: [PATCH 059/100] Stop identity-mapping the through records in the preloader since I fixed the underlying problem in the habtm preloader. --- .../associations/preloader/through_association.rb | 5 ----- 1 file changed, 5 deletions(-) diff --git a/activerecord/lib/active_record/associations/preloader/through_association.rb b/activerecord/lib/active_record/associations/preloader/through_association.rb index 30558ae29c..ad6374d09a 100644 --- a/activerecord/lib/active_record/associations/preloader/through_association.rb +++ b/activerecord/lib/active_record/associations/preloader/through_association.rb @@ -33,13 +33,8 @@ 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? From ddf83d14f1c7ddae07a285a8ad7c45f652edc843 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Sat, 5 Mar 2011 20:10:24 +0000 Subject: [PATCH 060/100] Add a test for STI on the through where the through is nested, and change the code which support this --- .../associations/through_association.rb | 35 ++++++++++++------- activerecord/lib/active_record/reflection.rb | 3 ++ .../nested_through_associations_test.rb | 9 +++++ activerecord/test/fixtures/taggings.yml | 10 ++++++ activerecord/test/models/post.rb | 1 + activerecord/test/models/rating.rb | 1 + 6 files changed, 46 insertions(+), 13 deletions(-) diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb index ed24373cba..8e9259a28d 100644 --- a/activerecord/lib/active_record/associations/through_association.rb +++ b/activerecord/lib/active_record/associations/through_association.rb @@ -10,8 +10,20 @@ module ActiveRecord protected + # We merge in these scopes for two reasons: + # + # 1. To get the scope_for_create on through reflection when building associated objects + # 2. To get the type conditions for any STI classes in the chain + # + # TODO: Don't actually do this. Getting the creation attributes for a non-nested through + # is a special case. The rest (STI conditions) should be handled by the reflection + # itself. def target_scope - super.merge(through_reflection.klass.scoped) + scope = super + through_reflection_chain[1..-1].each do |reflection| + scope = scope.merge(reflection.klass.scoped) + end + scope end def association_scope @@ -227,21 +239,18 @@ module ActiveRecord 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 + conditions = through_conditions[index] 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(process_conditions(conditions, reflection)) + end + end - Arel::Nodes::And.new(conditions) + def process_conditions(conditions, reflection) + 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 end diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index e3e2cac042..7ae9bfc928 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -416,6 +416,9 @@ module ActiveRecord 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 + # + # It is important to use self, rather than the source_reflection, because self + # may has a :source_type option which needs to be used. chain = [self] end diff --git a/activerecord/test/cases/associations/nested_through_associations_test.rb b/activerecord/test/cases/associations/nested_through_associations_test.rb index a4ac69782a..0dd407f342 100644 --- a/activerecord/test/cases/associations/nested_through_associations_test.rb +++ b/activerecord/test/cases/associations/nested_through_associations_test.rb @@ -425,6 +425,15 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase assert !scope.where("comments.type" => "SubSpecialComment").empty? end + def test_has_many_through_with_sti_on_nested_through_reflection + taggings = posts(:sti_comments).special_comments_ratings_taggings + assert_equal [taggings(:special_comment_rating)], taggings + + scope = Post.joins(:special_comments_ratings_taggings).where(:id => posts(:sti_comments).id) + assert scope.where("comments.type" => "Comment").empty? + assert !scope.where("comments.type" => "SpecialComment").empty? + end + def test_nested_has_many_through_writers_should_raise_error david = authors(:david) subscriber = subscribers(:first) diff --git a/activerecord/test/fixtures/taggings.yml b/activerecord/test/fixtures/taggings.yml index a337cce019..d339c12b25 100644 --- a/activerecord/test/fixtures/taggings.yml +++ b/activerecord/test/fixtures/taggings.yml @@ -66,3 +66,13 @@ other_by_mary_blue: taggable_id: 11 taggable_type: Post comment: first + +special_comment_rating: + id: 12 + taggable_id: 2 + taggable_type: Rating + +normal_comment_rating: + id: 13 + taggable_id: 1 + taggable_type: Rating diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb index b39325f949..c3843fd264 100644 --- a/activerecord/test/models/post.rb +++ b/activerecord/test/models/post.rb @@ -50,6 +50,7 @@ class Post < ActiveRecord::Base has_many :nonexistant_comments, :class_name => 'Comment', :conditions => 'comments.id < 0' has_many :special_comments_ratings, :through => :special_comments, :source => :ratings + has_many :special_comments_ratings_taggings, :through => :special_comments_ratings, :source => :taggings 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 index 12c4b5affa..25a52c4ad7 100644 --- a/activerecord/test/models/rating.rb +++ b/activerecord/test/models/rating.rb @@ -1,3 +1,4 @@ class Rating < ActiveRecord::Base belongs_to :comment + has_many :taggings, :as => :taggable end From 7fddb942624478a23173dfa379f7ade6a0fc9218 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Sat, 5 Mar 2011 22:07:30 +0000 Subject: [PATCH 061/100] Push source_type and polymorphic conditions out of ThroughAssociation and JoinDependency::JoinAssociation and into the reflection instead. --- .../join_dependency/join_association.rb | 18 ---------------- .../associations/through_association.rb | 17 --------------- activerecord/lib/active_record/reflection.rb | 21 ++++++++++--------- activerecord/test/cases/reflection_test.rb | 4 ++-- 4 files changed, 13 insertions(+), 47 deletions(-) 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 890e77fca9..5da3416023 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb @@ -76,8 +76,6 @@ module ActiveRecord 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 @@ -103,8 +101,6 @@ module ActiveRecord 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 @@ -239,20 +235,6 @@ module ActiveRecord end end - 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 - - def polymorphic_conditions(reflection, table) - if reflection.options[:as] - table[reflection.type]. - eq(reflection.active_record.base_class.name) - end - end - end end end diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb index 8e9259a28d..11263d5def 100644 --- a/activerecord/lib/active_record/associations/through_association.rb +++ b/activerecord/lib/active_record/associations/through_association.rb @@ -89,7 +89,6 @@ module ActiveRecord 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 @@ -107,7 +106,6 @@ module ActiveRecord 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 @@ -119,7 +117,6 @@ module ActiveRecord 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) ) @@ -254,20 +251,6 @@ module ActiveRecord 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 diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 7ae9bfc928..82f648b873 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -270,7 +270,9 @@ module ActiveRecord end def through_conditions - [Array.wrap(options[:conditions])] + through_conditions = [Array.wrap(options[:conditions])] + through_conditions.first << { type => active_record.base_class.name } if options[:as] + through_conditions end def source_reflection @@ -453,20 +455,19 @@ module ActiveRecord # 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 + conditions = source_reflection.through_conditions # Add to it the conditions from this reflection if necessary. conditions.first << options[:conditions] if options[:conditions] + through_conditions = through_reflection.through_conditions + + if options[:source_type] + through_conditions.first << { foreign_type => options[:source_type] } + end + # Recursively fill out the rest of the array from the through reflection - conditions += through_reflection.through_conditions + conditions += through_conditions # And return conditions diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb index 9529ae56b8..baaa08a359 100644 --- a/activerecord/test/cases/reflection_test.rb +++ b/activerecord/test/cases/reflection_test.rb @@ -216,7 +216,7 @@ class ReflectionTest < ActiveRecord::TestCase def test_through_conditions expected = [ ["tags.name = 'Blue'"], - ["taggings.comment = 'first'"], + ["taggings.comment = 'first'", {"taggable_type"=>"Post"}], ["posts.title LIKE 'misc post%'"] ] actual = Author.reflect_on_association(:misc_post_first_blue_tags).through_conditions @@ -224,7 +224,7 @@ class ReflectionTest < ActiveRecord::TestCase expected = [ ["tags.name = 'Blue'", "taggings.comment = 'first'", "posts.title LIKE 'misc post%'"], - [], + [{"taggable_type"=>"Post"}], [] ] actual = Author.reflect_on_association(:misc_post_first_blue_tags_2).through_conditions From b7f1b3641afe0ff4f3cd344815c6f7bb58821e9e Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Sat, 5 Mar 2011 22:32:49 +0000 Subject: [PATCH 062/100] Use Base#type_condition in JoinAssociation --- .../join_dependency/join_association.rb | 19 ++++--------------- activerecord/lib/active_record/base.rb | 4 ++-- 2 files changed, 6 insertions(+), 17 deletions(-) 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 5da3416023..fc8e75b10d 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb @@ -123,9 +123,11 @@ module ActiveRecord end conditions << table[key].eq(foreign_table[foreign_key]) - conditions << reflection_conditions(index, table) - conditions << sti_conditions(reflection, table) + + if reflection.klass.finder_needs_type_condition? + conditions << reflection.klass.send(:type_condition, table) + end ands = relation.create_and(conditions.flatten.compact) @@ -222,19 +224,6 @@ module ActiveRecord end end - 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 - - # TODO: use IN (...), or possibly AR::Base#type_condition - subclasses.inject(sti_condition) { |attr,subclass| - attr.or(sti_column.eq(subclass.sti_name)) - } - end - end - end end end diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index b3204b2bda..baf82bedd3 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -973,8 +973,8 @@ module ActiveRecord #:nodoc: relation end - def type_condition - sti_column = arel_table[inheritance_column.to_sym] + def type_condition(table = arel_table) + sti_column = table[inheritance_column.to_sym] sti_names = ([self] + descendants).map { |model| model.sti_name } sti_column.in(sti_names) From d02c326a8b5fdcfdb28ba91ee4dbc9c9edf6bf18 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Sun, 6 Mar 2011 18:14:39 +0000 Subject: [PATCH 063/100] Refactor ThroughAssociation#tables to just be a flat array of tables in the order that they should be joined together. --- .../associations/through_association.rb | 145 +++++++++--------- 1 file changed, 70 insertions(+), 75 deletions(-) diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb index 11263d5def..0857d8e253 100644 --- a/activerecord/lib/active_record/associations/through_association.rb +++ b/activerecord/lib/active_record/associations/through_association.rb @@ -27,7 +27,7 @@ module ActiveRecord end def association_scope - scope = super.joins(construct_joins) + scope = join_to(super) scope = scope.where(reflection_conditions(0)) unless options[:include] @@ -57,98 +57,98 @@ module ActiveRecord end def construct_owner_conditions - 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) + super(tables.last, through_reflection_chain.last) end - def construct_joins - joins, right_index = [], 1 + def join_to(scope) + joins = [] + tables = tables().dup # FIXME: Ugly - # 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] + foreign_reflection = through_reflection_chain.first + foreign_table = tables.shift - if left.source_reflection.nil? - case left.macro + through_reflection_chain[1..-1].each_with_index do |reflection, i| + i += 1 + table = tables.shift + + if foreign_reflection.source_reflection.nil? + case foreign_reflection.macro when :belongs_to joins << inner_join( - right_table, - left_table[left.association_primary_key], - right_table[left.foreign_key], - reflection_conditions(right_index) + table, + foreign_table[foreign_reflection.association_primary_key], + table[foreign_reflection.foreign_key], + reflection_conditions(i) ) when :has_many, :has_one joins << inner_join( - right_table, - left_table[left.foreign_key], - right_table[right.association_primary_key], - reflection_conditions(right_index) + table, + foreign_table[foreign_reflection.foreign_key], + table[reflection.association_primary_key], + reflection_conditions(i) ) when :has_and_belongs_to_many + join_table = foreign_table + joins << inner_join( - right_table, - left_table.first[left.foreign_key], - right_table[right.klass.primary_key], - reflection_conditions(right_index) + table, + join_table[foreign_reflection.foreign_key], + table[reflection.klass.primary_key], + reflection_conditions(i) ) end else - case left.source_reflection.macro + case foreign_reflection.source_reflection.macro when :belongs_to joins << inner_join( - right_table, - left_table[left.association_primary_key], - right_table[left.foreign_key], - reflection_conditions(right_index) + table, + foreign_table[foreign_reflection.association_primary_key], + table[foreign_reflection.foreign_key], + reflection_conditions(i) ) when :has_many, :has_one - if right.macro == :has_and_belongs_to_many - join_table, right_table = tables[right] - end - joins << inner_join( - right_table, - left_table[left.foreign_key], - right_table[left.source_reflection.active_record_primary_key], - reflection_conditions(right_index) + table, + foreign_table[foreign_reflection.foreign_key], + table[foreign_reflection.source_reflection.active_record_primary_key], + reflection_conditions(i) ) - if right.macro == :has_and_belongs_to_many + if reflection.macro == :has_and_belongs_to_many + join_table = tables.shift + joins << inner_join( join_table, - right_table[right.klass.primary_key], - join_table[right.association_foreign_key] + table[reflection.klass.primary_key], + join_table[reflection.association_foreign_key] ) + + # hack to make it become the foreign_table + table = join_table end when :has_and_belongs_to_many - join_table, left_table = tables[left] + join_table, table = table, tables.shift joins << inner_join( join_table, - left_table[left.klass.primary_key], - join_table[left.association_foreign_key] + foreign_table[foreign_reflection.klass.primary_key], + join_table[foreign_reflection.association_foreign_key] ) joins << inner_join( - right_table, - join_table[left.foreign_key], - right_table[right.klass.primary_key], - reflection_conditions(right_index) + table, + join_table[foreign_reflection.foreign_key], + table[reflection.klass.primary_key], + reflection_conditions(i) ) end end - right_index += 1 + foreign_reflection = reflection + foreign_table = table end - joins + scope.joins(joins) end # Construct attributes for :through pointing to owner and associate. This is used by the @@ -191,31 +191,26 @@ module ActiveRecord @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) + tables = [] + through_reflection_chain.each do |reflection| + tables << 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) + + tables << 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 || - (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 + tables end end From 5dc1fb39dde569c2633e067cdc895ddc56dd3482 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Sun, 6 Mar 2011 23:38:10 +0000 Subject: [PATCH 064/100] Refactor ThroughAssociation#join_to to be much smaller, and independent of construct_owner_conditions. --- .../associations/through_association.rb | 108 +++++------------- activerecord/lib/active_record/reflection.rb | 6 + 2 files changed, 32 insertions(+), 82 deletions(-) diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb index 0857d8e253..5768915eaf 100644 --- a/activerecord/lib/active_record/associations/through_association.rb +++ b/activerecord/lib/active_record/associations/through_association.rb @@ -28,7 +28,6 @@ module ActiveRecord def association_scope scope = join_to(super) - scope = scope.where(reflection_conditions(0)) unless options[:include] scope = scope.includes(source_options[:include]) @@ -57,95 +56,42 @@ module ActiveRecord end def construct_owner_conditions - super(tables.last, through_reflection_chain.last) end def join_to(scope) joins = [] tables = tables().dup # FIXME: Ugly - foreign_reflection = through_reflection_chain.first - foreign_table = tables.shift + through_reflection_chain.each_with_index do |reflection, i| + table, foreign_table = tables.shift, tables.first - through_reflection_chain[1..-1].each_with_index do |reflection, i| - i += 1 - table = tables.shift + if reflection.source_macro == :has_and_belongs_to_many + join_table = tables.shift - if foreign_reflection.source_reflection.nil? - case foreign_reflection.macro - when :belongs_to - joins << inner_join( - table, - foreign_table[foreign_reflection.association_primary_key], - table[foreign_reflection.foreign_key], - reflection_conditions(i) - ) - when :has_many, :has_one - joins << inner_join( - table, - foreign_table[foreign_reflection.foreign_key], - table[reflection.association_primary_key], - reflection_conditions(i) - ) - when :has_and_belongs_to_many - join_table = foreign_table + joins << inner_join( + join_table, + table[reflection.active_record_primary_key]. + eq(join_table[reflection.association_foreign_key]) + ) - joins << inner_join( - table, - join_table[foreign_reflection.foreign_key], - table[reflection.klass.primary_key], - reflection_conditions(i) - ) - end - else - case foreign_reflection.source_reflection.macro - when :belongs_to - joins << inner_join( - table, - foreign_table[foreign_reflection.association_primary_key], - table[foreign_reflection.foreign_key], - reflection_conditions(i) - ) - when :has_many, :has_one - joins << inner_join( - table, - foreign_table[foreign_reflection.foreign_key], - table[foreign_reflection.source_reflection.active_record_primary_key], - reflection_conditions(i) - ) - - if reflection.macro == :has_and_belongs_to_many - join_table = tables.shift - - joins << inner_join( - join_table, - table[reflection.klass.primary_key], - join_table[reflection.association_foreign_key] - ) - - # hack to make it become the foreign_table - table = join_table - end - when :has_and_belongs_to_many - join_table, table = table, tables.shift - - joins << inner_join( - join_table, - foreign_table[foreign_reflection.klass.primary_key], - join_table[foreign_reflection.association_foreign_key] - ) - - joins << inner_join( - table, - join_table[foreign_reflection.foreign_key], - table[reflection.klass.primary_key], - reflection_conditions(i) - ) - end + table, foreign_table = join_table, tables.first end - foreign_reflection = reflection - foreign_table = table + if reflection.source_macro == :belongs_to + key = reflection.association_primary_key + foreign_key = reflection.foreign_key + else + key = reflection.foreign_key + foreign_key = reflection.association_primary_key + end + + if reflection == through_reflection_chain.last + constraint = table[key].eq owner[foreign_key] + scope = scope.where(constraint).where(reflection_conditions(i)) + else + constraint = table[key].eq foreign_table[foreign_key] + joins << inner_join(foreign_table, constraint, reflection_conditions(i)) + end end scope.joins(joins) @@ -221,9 +167,7 @@ module ActiveRecord name end - def inner_join(table, left_column, right_column, *conditions) - conditions << left_column.eq(right_column) - + def inner_join(table, *conditions) table.create_join( table, table.create_on(table.create_and(conditions.flatten.compact))) diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 82f648b873..5199886f79 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -279,6 +279,8 @@ module ActiveRecord nil end + alias :source_macro :macro + def has_inverse? @options[:inverse_of] end @@ -474,6 +476,10 @@ module ActiveRecord end end + def source_macro + source_reflection.source_macro + end + # A through association is nested iff there would be more than one join table def nested? through_reflection_chain.length > 2 || From cee3f9b36d01e6d54e0bd4c2fd06bee369bfff12 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Mon, 7 Mar 2011 00:04:06 +0000 Subject: [PATCH 065/100] Referencing a table via the ON condition in a join should force that table to be eager-loaded via a JOIN rather than via subsequent queries. --- activerecord/lib/active_record/relation.rb | 13 ++++++++++++- activerecord/test/cases/relations_test.rb | 15 +++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index f939bedc81..5af20bf38b 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -407,8 +407,19 @@ module ActiveRecord private def references_eager_loaded_tables? + joined_tables = arel.join_sources.map do |join| + if join.is_a?(Arel::Nodes::StringJoin) + tables_in_string(join.left) + else + [join.left.table_name, join.left.table_alias] + end + end + + joined_tables += [table.name, table.table_alias] + # always convert table names to downcase as in Oracle quoted table names are in uppercase - joined_tables = (tables_in_string(arel.join_sql) + [table.name, table.table_alias]).compact.map{ |t| t.downcase }.uniq + joined_tables = joined_tables.flatten.compact.map { |t| t.downcase }.uniq + (tables_in_string(to_sql) - joined_tables).any? end diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index 54537f11a8..ac586e2a63 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -850,4 +850,19 @@ class RelationTest < ActiveRecord::TestCase def test_primary_key assert_equal "id", Post.scoped.primary_key end + + def test_eager_loading_with_conditions_on_joins + scope = Post.includes(:comments) + + # This references the comments table, and so it should cause the comments to be eager + # loaded via a JOIN, rather than by subsequent queries. + scope = scope.joins( + Post.arel_table.create_join( + Post.arel_table, + Post.arel_table.create_on(Comment.arel_table[:id].eq(3)) + ) + ) + + assert scope.eager_loading? + end end From bb063b2f1b3e5b6fb2a4732cb696929f1652c555 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Mon, 7 Mar 2011 20:58:32 +0000 Subject: [PATCH 066/100] Fix test_has_many_association_through_a_has_many_association_with_nonstandard_primary_keys --- .../lib/active_record/associations/through_association.rb | 2 +- activerecord/lib/active_record/reflection.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb index 5768915eaf..ae2c8b65ed 100644 --- a/activerecord/lib/active_record/associations/through_association.rb +++ b/activerecord/lib/active_record/associations/through_association.rb @@ -82,7 +82,7 @@ module ActiveRecord foreign_key = reflection.foreign_key else key = reflection.foreign_key - foreign_key = reflection.association_primary_key + foreign_key = reflection.active_record_primary_key end if reflection == through_reflection_chain.last diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 5199886f79..0a9855ec25 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -372,7 +372,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 :foreign_key, :foreign_type, :association_foreign_key, :to => :source_reflection + delegate :foreign_key, :foreign_type, :association_foreign_key, :active_record_primary_key, :to => :source_reflection # Gets the source of the through reflection. It checks both a singularized # and pluralized form for :belongs_to or :has_many. From 6490d65234b89d4d28308b72b13d4834fd44bbb3 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Thu, 10 Mar 2011 19:04:00 +0000 Subject: [PATCH 067/100] Move the code which builds a scope for through associations into a generic AssociationScope class which is capable of building a scope for any association. --- .../lib/active_record/associations.rb | 7 +- .../active_record/associations/association.rb | 44 +----- .../associations/association_scope.rb | 149 ++++++++++++++++++ .../associations/collection_association.rb | 13 -- .../has_and_belongs_to_many_association.rb | 22 --- .../associations/has_many_association.rb | 2 - .../associations/has_one_association.rb | 6 - .../associations/through_association.rb | 128 --------------- activerecord/lib/active_record/reflection.rb | 6 +- 9 files changed, 162 insertions(+), 215 deletions(-) create mode 100644 activerecord/lib/active_record/associations/association_scope.rb diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index ec5b41a3e7..90745112b1 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -140,9 +140,10 @@ module ActiveRecord autoload :HasAndBelongsToMany, 'active_record/associations/builder/has_and_belongs_to_many' end - autoload :Preloader, 'active_record/associations/preloader' - autoload :JoinDependency, 'active_record/associations/join_dependency' - autoload :AliasTracker, 'active_record/associations/alias_tracker' + autoload :Preloader, 'active_record/associations/preloader' + autoload :JoinDependency, 'active_record/associations/join_dependency' + autoload :AssociationScope, 'active_record/associations/association_scope' + autoload :AliasTracker, 'active_record/associations/alias_tracker' # Clears out the association cache. def clear_association_cache #:nodoc: diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index 25b4b9d90d..27c446b12c 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -93,23 +93,9 @@ module ActiveRecord # by scope.scoping { ... } or with_scope { ... } etc, which affects the scope which # actually gets built. def construct_scope - @association_scope = association_scope if klass - end - - def association_scope - scope = klass.unscoped - scope = scope.create_with(creation_attributes) - scope = scope.apply_finder_options(options.slice(:readonly, :include)) - scope = scope.where(interpolate(options[:conditions])) - if select = select_value - scope = scope.select(select) + if klass + @association_scope = AssociationScope.new(self).scope end - scope = scope.extending(*Array.wrap(options[:extend])) - scope.where(construct_owner_conditions) - end - - def aliased_table - klass.arel_table end # Set the inverse association, if possible @@ -174,42 +160,24 @@ module ActiveRecord end end - def select_value - options[:select] - end - - # Implemented by (some) subclasses def creation_attributes - { } - end - - # Returns a hash linking the owner to the association represented by the reflection - def construct_owner_attributes(reflection = reflection) attributes = {} - if reflection.macro == :belongs_to - attributes[reflection.association_primary_key] = owner[reflection.foreign_key] - else + + if [:has_one, :has_many].include?(reflection.macro) && !options[:through] attributes[reflection.foreign_key] = owner[reflection.active_record_primary_key] if reflection.options[:as] attributes[reflection.type] = owner.class.base_class.name end end - attributes - end - # Builds an array of arel nodes from the owner attributes hash - def construct_owner_conditions(table = aliased_table, reflection = reflection) - conditions = construct_owner_attributes(reflection).map do |attr, value| - table[attr].eq(value) - end - table.create_and(conditions) + attributes end # Sets the owner attributes on the given record def set_owner_attributes(record) if owner.persisted? - construct_owner_attributes.each { |key, value| record[key] = value } + creation_attributes.each { |key, value| record[key] = value } end end diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb new file mode 100644 index 0000000000..5df2e964be --- /dev/null +++ b/activerecord/lib/active_record/associations/association_scope.rb @@ -0,0 +1,149 @@ +module ActiveRecord + module Associations + class AssociationScope #:nodoc: + attr_reader :association, :alias_tracker + + delegate :klass, :owner, :reflection, :interpolate, :to => :association + delegate :through_reflection_chain, :through_conditions, :options, :source_options, :to => :reflection + + def initialize(association) + @association = association + @alias_tracker = AliasTracker.new + end + + def scope + scope = klass.unscoped + scope = scope.extending(*Array.wrap(options[:extend])) + + # It's okay to just apply all these like this. The options will only be present if the + # association supports that option; this is enforced by the association builder. + scope = scope.apply_finder_options(options.slice( + :readonly, :include, :order, :limit, :joins, :group, :having, :offset)) + + if options[:through] && !options[:include] + scope = scope.includes(source_options[:include]) + end + + if select = select_value + scope = scope.select(select) + end + + add_constraints(scope) + end + + private + + def select_value + select_value = options[:select] + + if reflection.collection? + select_value ||= options[:uniq] && "DISTINCT #{reflection.quoted_table_name}.*" + end + + if reflection.macro == :has_and_belongs_to_many + select_value ||= reflection.klass.arel_table[Arel.star] + end + + select_value + end + + def add_constraints(scope) + tables = construct_tables + + through_reflection_chain.each_with_index do |reflection, i| + table, foreign_table = tables.shift, tables.first + + if reflection.source_macro == :has_and_belongs_to_many + join_table = tables.shift + + scope = scope.joins(inner_join( + join_table, reflection, + table[reflection.active_record_primary_key]. + eq(join_table[reflection.association_foreign_key]) + )) + + table, foreign_table = join_table, tables.first + end + + if reflection.source_macro == :belongs_to + key = reflection.association_primary_key + foreign_key = reflection.foreign_key + else + key = reflection.foreign_key + foreign_key = reflection.active_record_primary_key + end + + if reflection == through_reflection_chain.last + scope = scope.where(table[key].eq(owner[foreign_key])) + + through_conditions[i].each do |condition| + if options[:through] && condition.is_a?(Hash) + condition = { table.name => condition } + end + + scope = scope.where(interpolate(condition)) + end + else + constraint = table[key].eq foreign_table[foreign_key] + + join = inner_join(foreign_table, reflection, constraint, *through_conditions[i]) + scope = scope.joins(join) + end + end + + scope + end + + def construct_tables + tables = [] + through_reflection_chain.each do |reflection| + tables << alias_tracker.aliased_table_for( + table_name_for(reflection), + table_alias_for(reflection, reflection != self.reflection) + ) + + if reflection.source_macro == :has_and_belongs_to_many + tables << alias_tracker.aliased_table_for( + (reflection.source_reflection || reflection).options[:join_table], + table_alias_for(reflection, true) + ) + end + end + tables + end + + def table_name_for(reflection) + if reflection == self.reflection + # If this is a polymorphic belongs_to, we want to get the klass from the + # association because it depends on the polymorphic_type attribute of + # the owner + klass.table_name + else + reflection.table_name + end + end + + def table_alias_for(reflection, join = false) + name = alias_tracker.pluralize(reflection.name) + name << "_#{self.reflection.name}" + name << "_join" if join + name + end + + def inner_join(table, reflection, *conditions) + conditions = sanitize_conditions(reflection, conditions) + table.create_join(table, table.create_on(conditions)) + end + + def sanitize_conditions(reflection, conditions) + conditions = 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 + + conditions.length == 1 ? conditions.first : Arel::Nodes::And.new(conditions) + end + end + end +end diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index f3761bd2c7..9f4fc44cc6 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -331,11 +331,6 @@ module ActiveRecord @scopes_cache[method][args] ||= scoped.readonly(nil).send(method, *args) end - def association_scope - options = reflection.options.slice(:order, :limit, :joins, :group, :having, :offset) - super.apply_finder_options(options) - end - def load_target if find_target? targets = [] @@ -373,14 +368,6 @@ module ActiveRecord private - def select_value - super || uniq_select_value - end - - def uniq_select_value - options[:uniq] && "DISTINCT #{reflection.quoted_table_name}.*" - end - def custom_counter_sql if options[:counter_sql] interpolate(options[:counter_sql]) diff --git a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb index 028630977d..217213808b 100644 --- a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb +++ b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb @@ -26,10 +26,6 @@ module ActiveRecord record end - def association_scope - super.joins(construct_joins) - end - private def count_records @@ -48,24 +44,6 @@ module ActiveRecord end end - def construct_joins - right = join_table - left = reflection.klass.arel_table - - condition = left[reflection.klass.primary_key].eq( - right[reflection.association_foreign_key]) - - right.create_join(right, right.create_on(condition)) - end - - def construct_owner_conditions - super(join_table) - end - - def select_value - super || reflection.klass.arel_table[Arel.star] - end - def invertible_for?(record) false end diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index cebf3e477a..78c5c4b870 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -94,8 +94,6 @@ module ActiveRecord end end end - - alias creation_attributes construct_owner_attributes end end end diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index e13f97125f..1d2e8667e4 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -39,14 +39,8 @@ module ActiveRecord end end - def association_scope - super.order(options[:order]) - end - private - alias creation_attributes construct_owner_attributes - # The reason that the save param for replace is false, if for create (not just build), # is because the setting of the foreign keys is actually handled by the scoping when # the record is instantiated, and so they are set straight away and do not need to be diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb index ae2c8b65ed..408237c689 100644 --- a/activerecord/lib/active_record/associations/through_association.rb +++ b/activerecord/lib/active_record/associations/through_association.rb @@ -1,5 +1,3 @@ -require 'enumerator' - module ActiveRecord # = Active Record Through Association module Associations @@ -26,77 +24,8 @@ module ActiveRecord scope end - def association_scope - scope = join_to(super) - - unless options[:include] - scope = scope.includes(source_options[:include]) - end - - scope - end - private - # This scope affects the creation of the associated records (not the join records). At the - # moment we only support creating on a :through association when the source reflection is a - # belongs_to. Thus it's not necessary to set a foreign key on the associated record(s), so - # this scope has can legitimately be empty. - def creation_attributes - { } - end - - # TODO: Needed? - def aliased_through_table - name = through_reflection.table_name - - reflection.table_name == name ? - through_reflection.klass.arel_table.alias(name + "_join") : - through_reflection.klass.arel_table - end - - def construct_owner_conditions - end - - def join_to(scope) - joins = [] - tables = tables().dup # FIXME: Ugly - - through_reflection_chain.each_with_index do |reflection, i| - table, foreign_table = tables.shift, tables.first - - if reflection.source_macro == :has_and_belongs_to_many - join_table = tables.shift - - joins << inner_join( - join_table, - table[reflection.active_record_primary_key]. - eq(join_table[reflection.association_foreign_key]) - ) - - table, foreign_table = join_table, tables.first - end - - if reflection.source_macro == :belongs_to - key = reflection.association_primary_key - foreign_key = reflection.foreign_key - else - key = reflection.foreign_key - foreign_key = reflection.active_record_primary_key - end - - if reflection == through_reflection_chain.last - constraint = table[key].eq owner[foreign_key] - scope = scope.where(constraint).where(reflection_conditions(i)) - else - constraint = table[key].eq foreign_table[foreign_key] - joins << inner_join(foreign_table, constraint, reflection_conditions(i)) - end - end - - scope.joins(joins) - end - # Construct attributes for :through pointing to owner and associate. This is used by the # methods which create and delete records on the association. # @@ -133,63 +62,6 @@ module ActiveRecord end end - def alias_tracker - @alias_tracker ||= AliasTracker.new - end - - def tables - @tables ||= begin - tables = [] - through_reflection_chain.each do |reflection| - tables << 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) - - tables << alias_tracker.aliased_table_for( - (reflection.source_reflection || reflection).options[:join_table], - table_alias_for(reflection, true) - ) - end - end - tables - end - end - - def table_alias_for(reflection, join = false) - name = alias_tracker.pluralize(reflection.name) - name << "_#{self.reflection.name}" - name << "_join" if join - name - end - - def inner_join(table, *conditions) - table.create_join( - table, - table.create_on(table.create_and(conditions.flatten.compact))) - end - - def reflection_conditions(index) - reflection = through_reflection_chain[index] - conditions = through_conditions[index] - - unless conditions.empty? - Arel::Nodes::And.new(process_conditions(conditions, reflection)) - end - end - - def process_conditions(conditions, reflection) - 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 - end - # TODO: Think about this in the context of nested associations def stale_state if through_reflection.macro == :belongs_to diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 0a9855ec25..8c73e49e74 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -270,9 +270,9 @@ module ActiveRecord end def through_conditions - through_conditions = [Array.wrap(options[:conditions])] - through_conditions.first << { type => active_record.base_class.name } if options[:as] - through_conditions + conditions = [options[:conditions]].compact + conditions << { type => active_record.base_class.name } if options[:as] + [conditions] end def source_reflection From 2d3d9e3531d0d49a94ded10b993640053bd76c03 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Thu, 10 Mar 2011 19:28:26 +0000 Subject: [PATCH 068/100] Rename Reflection#through_reflection_chain and #through_options to Reflection#chain and Reflection#options as they now no longer relate solely to through associations. --- .../associations/association_scope.rb | 12 ++-- .../join_dependency/join_association.rb | 16 ++--- .../associations/through_association.rb | 5 +- activerecord/lib/active_record/reflection.rb | 68 ++++++++++--------- activerecord/test/cases/reflection_test.rb | 10 +-- 5 files changed, 57 insertions(+), 54 deletions(-) diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb index 5df2e964be..cf859bbdee 100644 --- a/activerecord/lib/active_record/associations/association_scope.rb +++ b/activerecord/lib/active_record/associations/association_scope.rb @@ -4,7 +4,7 @@ module ActiveRecord attr_reader :association, :alias_tracker delegate :klass, :owner, :reflection, :interpolate, :to => :association - delegate :through_reflection_chain, :through_conditions, :options, :source_options, :to => :reflection + delegate :chain, :conditions, :options, :source_options, :to => :reflection def initialize(association) @association = association @@ -50,7 +50,7 @@ module ActiveRecord def add_constraints(scope) tables = construct_tables - through_reflection_chain.each_with_index do |reflection, i| + chain.each_with_index do |reflection, i| table, foreign_table = tables.shift, tables.first if reflection.source_macro == :has_and_belongs_to_many @@ -73,10 +73,10 @@ module ActiveRecord foreign_key = reflection.active_record_primary_key end - if reflection == through_reflection_chain.last + if reflection == chain.last scope = scope.where(table[key].eq(owner[foreign_key])) - through_conditions[i].each do |condition| + conditions[i].each do |condition| if options[:through] && condition.is_a?(Hash) condition = { table.name => condition } end @@ -86,7 +86,7 @@ module ActiveRecord else constraint = table[key].eq foreign_table[foreign_key] - join = inner_join(foreign_table, reflection, constraint, *through_conditions[i]) + join = inner_join(foreign_table, reflection, constraint, *conditions[i]) scope = scope.joins(join) end end @@ -96,7 +96,7 @@ module ActiveRecord def construct_tables tables = [] - through_reflection_chain.each do |reflection| + chain.each do |reflection| tables << alias_tracker.aliased_table_for( table_name_for(reflection), table_alias_for(reflection, reflection != self.reflection) 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 fc8e75b10d..f26881a6c6 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb @@ -22,7 +22,7 @@ module ActiveRecord attr_reader :tables - delegate :options, :through_reflection, :source_reflection, :through_reflection_chain, :to => :reflection + delegate :options, :through_reflection, :source_reflection, :chain, :to => :reflection delegate :table, :table_name, :to => :parent, :prefix => :parent delegate :alias_tracker, :to => :join_dependency @@ -57,14 +57,12 @@ module ActiveRecord end def join_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| + # The chain starts with the target table, but we want to end with it here (makes + # more sense in this context), so we reverse + chain.reverse.each do |reflection| table = tables[index] conditions = [] @@ -178,7 +176,7 @@ module ActiveRecord # 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| + @tables = chain.map do |reflection| table = alias_tracker.aliased_table_for( reflection.table_name, table_alias_for(reflection, reflection != self.reflection) @@ -200,7 +198,7 @@ module ActiveRecord end end - # The joins are generated from the through_reflection_chain in reverse order, so + # The joins are generated from the 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! @@ -219,7 +217,7 @@ module ActiveRecord end def reflection_conditions(index, table) - reflection.through_conditions.reverse[index].map do |condition| + reflection.conditions.reverse[index].map do |condition| process_conditions(condition, table.table_alias || table.name) end end diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb index 408237c689..c0c92e8d72 100644 --- a/activerecord/lib/active_record/associations/through_association.rb +++ b/activerecord/lib/active_record/associations/through_association.rb @@ -3,8 +3,7 @@ module ActiveRecord module Associations module ThroughAssociation #:nodoc: - delegate :source_options, :through_options, :source_reflection, :through_reflection, - :through_reflection_chain, :through_conditions, :to => :reflection + delegate :source_reflection, :through_reflection, :chain, :to => :reflection protected @@ -18,7 +17,7 @@ module ActiveRecord # itself. def target_scope scope = super - through_reflection_chain[1..-1].each do |reflection| + chain[1..-1].each do |reflection| scope = scope.merge(reflection.klass.scoped) end scope diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 8c73e49e74..1f3ace93a9 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -262,23 +262,28 @@ module ActiveRecord end def through_reflection - false - end - - def through_reflection_chain - [self] - end - - def through_conditions - conditions = [options[:conditions]].compact - conditions << { type => active_record.base_class.name } if options[:as] - [conditions] + nil end def source_reflection nil end + # A chain of reflections from this one back to the owner. For more see the explanation in + # ThroughReflection. + def chain + [self] + end + + # An array of arrays of conditions. Each item in the outside array corresponds to a reflection + # in the #chain. The inside arrays are simply conditions (and each condition may itself be + # a hash, array, arel predicate, etc...) + def conditions + conditions = [options[:conditions]].compact + conditions << { type => active_record.base_class.name } if options[:as] + [conditions] + end + alias :source_macro :macro def has_inverse? @@ -401,33 +406,34 @@ 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. + # Returns an array of reflections which are involved in this 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 + # The chain is built by recursively calling #chain on the source reflection and the through + # reflection. The base case for the recursion is a normal association, which just returns + # [self] as its #chain. + def chain + @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 + chain = source_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 # # It is important to use self, rather than the source_reflection, because self # may has a :source_type option which needs to be used. + # + # FIXME: Not sure this is correct justification now that we have #conditions chain = [self] end # Recursively build the rest of the chain - chain += through_reflection.through_reflection_chain + chain += through_reflection.chain # Finally return the completed chain chain @@ -451,18 +457,18 @@ module ActiveRecord # 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 - conditions = source_reflection.through_conditions + # but only Comment.tags will be represented in the #chain. So this method creates an array + # of conditions corresponding to the chain. Each item in the #conditions array corresponds + # to an item in the #chain, and is itself an array of conditions from an arbitrary number + # of relevant reflections, plus any :source_type or polymorphic :as constraints. + def conditions + @conditions ||= begin + conditions = source_reflection.conditions # Add to it the conditions from this reflection if necessary. conditions.first << options[:conditions] if options[:conditions] - through_conditions = through_reflection.through_conditions + through_conditions = through_reflection.conditions if options[:source_type] through_conditions.first << { foreign_type => options[:source_type] } @@ -476,14 +482,14 @@ module ActiveRecord end end + # The macro used by the source association def source_macro source_reflection.source_macro 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 + 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 diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb index baaa08a359..4b881969cc 100644 --- a/activerecord/test/cases/reflection_test.rb +++ b/activerecord/test/cases/reflection_test.rb @@ -202,24 +202,24 @@ class ReflectionTest < ActiveRecord::TestCase assert_kind_of ThroughReflection, Subscriber.reflect_on_association(:books) end - def test_through_reflection_chain + def test_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 + actual = Organization.reflect_on_association(:author_essay_categories).chain assert_equal expected, actual end - def test_through_conditions + def test_conditions expected = [ ["tags.name = 'Blue'"], ["taggings.comment = 'first'", {"taggable_type"=>"Post"}], ["posts.title LIKE 'misc post%'"] ] - actual = Author.reflect_on_association(:misc_post_first_blue_tags).through_conditions + actual = Author.reflect_on_association(:misc_post_first_blue_tags).conditions assert_equal expected, actual expected = [ @@ -227,7 +227,7 @@ class ReflectionTest < ActiveRecord::TestCase [{"taggable_type"=>"Post"}], [] ] - actual = Author.reflect_on_association(:misc_post_first_blue_tags_2).through_conditions + actual = Author.reflect_on_association(:misc_post_first_blue_tags_2).conditions assert_equal expected, actual end From aef3629c6ebae013333e11911934dfff28de875a Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Thu, 10 Mar 2011 23:55:29 +0000 Subject: [PATCH 069/100] Refactor JoinAssociation --- .../join_dependency/join_association.rb | 148 ++++++------------ 1 file changed, 51 insertions(+), 97 deletions(-) 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 f26881a6c6..750f03a120 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb @@ -57,88 +57,46 @@ module ActiveRecord end def join_to(relation) + tables = @tables.dup foreign_table = parent_table - index = 0 # The chain starts with the target table, but we want to end with it here (makes # more sense in this context), so we reverse - chain.reverse.each do |reflection| - table = tables[index] - conditions = [] + chain.reverse.each_with_index do |reflection, i| + table = tables.shift - 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 - 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 + case reflection.source_macro + when :belongs_to + key = reflection.association_primary_key + foreign_key = reflection.foreign_key + when :has_and_belongs_to_many + # Join the join table first... + relation = relation.from(join( + table, + table[reflection.foreign_key]. + eq(foreign_table[reflection.active_record_primary_key]) + )) - join_key = reflection.foreign_key - join_foreign_key = reflection.active_record.primary_key + foreign_table, table = table, tables.shift - 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 + key = reflection.association_primary_key + foreign_key = reflection.association_foreign_key else - case reflection.source_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.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 + key = reflection.foreign_key + foreign_key = reflection.active_record_primary_key end + conditions = self.conditions[i].dup conditions << table[key].eq(foreign_table[foreign_key]) - conditions << reflection_conditions(index, table) if reflection.klass.finder_needs_type_condition? conditions << reflection.klass.send(:type_condition, table) end - ands = relation.create_and(conditions.flatten.compact) - - join = relation.create_join( - table, - relation.create_on(ands), - join_type) - - relation = relation.from(join) + relation = relation.from(join(table, *conditions)) # The current table in this iteration becomes the foreign table in the next foreign_table = table - index += 1 end relation @@ -150,18 +108,18 @@ module ActiveRecord end def table - if tables.last.is_a?(Array) - tables.last.first - else - tables.last - end + tables.last end def aliased_table_name table.table_alias || table.name end - protected + def conditions + @conditions ||= reflection.conditions.reverse + end + + private def table_alias_for(reflection, join = false) name = alias_tracker.pluralize(reflection.name) @@ -170,55 +128,51 @@ module ActiveRecord name end - private - # 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 = chain.map do |reflection| - table = alias_tracker.aliased_table_for( + @tables = [] + chain.each do |reflection| + @tables << 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( + if reflection.source_macro == :has_and_belongs_to_many + @tables << alias_tracker.aliased_table_for( (reflection.source_reflection || reflection).options[:join_table], table_alias_for(reflection, true) ) - - [table, join_table] - else - table end end - # The joins are generated from the 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. + # We construct the tables in the forward order so that the aliases are generated + # correctly, but then reverse the array because that is the order in which we will + # iterate the chain. @tables.reverse! end - def process_conditions(conditions, table_name) - if conditions.respond_to?(:to_proc) - conditions = instance_eval(&conditions) + def join(table, *conditions) + conditions = sanitize_conditions(table, conditions) + table.create_join(table, table.create_on(conditions), join_type) + end + + def sanitize_conditions(table, conditions) + conditions = conditions.map do |condition| + condition = active_record.send(:sanitize_sql, interpolate(condition), table.table_alias || table.name) + condition = Arel.sql(condition) unless condition.is_a?(Arel::Node) + condition end - Arel.sql(sanitize_sql(conditions, table_name)) + conditions.length == 1 ? conditions.first : Arel::Nodes::And.new(conditions) end - def sanitize_sql(condition, table_name) - active_record.send(:sanitize_sql, condition, table_name) - end - - def reflection_conditions(index, table) - reflection.conditions.reverse[index].map do |condition| - process_conditions(condition, table.table_alias || table.name) + def interpolate(conditions) + if conditions.respond_to?(:to_proc) + instance_eval(&conditions) + else + conditions end end From e18679ab0428797369027fc549ef964c8c2038ba Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Fri, 11 Mar 2011 00:47:18 +0000 Subject: [PATCH 070/100] Abstract some common code from AssociationScope and JoinDependency::JoinAssociation into a JoinHelper module --- .../lib/active_record/associations.rb | 1 + .../associations/association_scope.rb | 56 ++++------------ .../join_dependency/join_association.rb | 65 ++++--------------- .../active_record/associations/join_helper.rb | 58 +++++++++++++++++ 4 files changed, 83 insertions(+), 97 deletions(-) create mode 100644 activerecord/lib/active_record/associations/join_helper.rb diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 90745112b1..08fb6bf3c4 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -144,6 +144,7 @@ module ActiveRecord autoload :JoinDependency, 'active_record/associations/join_dependency' autoload :AssociationScope, 'active_record/associations/association_scope' autoload :AliasTracker, 'active_record/associations/alias_tracker' + autoload :JoinHelper, 'active_record/associations/join_helper' # Clears out the association cache. def clear_association_cache #:nodoc: diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb index cf859bbdee..b9bbeed4d5 100644 --- a/activerecord/lib/active_record/associations/association_scope.rb +++ b/activerecord/lib/active_record/associations/association_scope.rb @@ -1,10 +1,12 @@ module ActiveRecord module Associations class AssociationScope #:nodoc: + include JoinHelper + attr_reader :association, :alias_tracker delegate :klass, :owner, :reflection, :interpolate, :to => :association - delegate :chain, :conditions, :options, :source_options, :to => :reflection + delegate :chain, :conditions, :options, :source_options, :active_record, :to => :reflection def initialize(association) @association = association @@ -56,8 +58,8 @@ module ActiveRecord if reflection.source_macro == :has_and_belongs_to_many join_table = tables.shift - scope = scope.joins(inner_join( - join_table, reflection, + scope = scope.joins(join( + join_table, table[reflection.active_record_primary_key]. eq(join_table[reflection.association_foreign_key]) )) @@ -84,32 +86,19 @@ module ActiveRecord scope = scope.where(interpolate(condition)) end else - constraint = table[key].eq foreign_table[foreign_key] - - join = inner_join(foreign_table, reflection, constraint, *conditions[i]) - scope = scope.joins(join) + scope = scope.joins(join( + foreign_table, + table[key].eq(foreign_table[foreign_key]), + *conditions[i] + )) end end scope end - def construct_tables - tables = [] - chain.each do |reflection| - tables << alias_tracker.aliased_table_for( - table_name_for(reflection), - table_alias_for(reflection, reflection != self.reflection) - ) - - if reflection.source_macro == :has_and_belongs_to_many - tables << alias_tracker.aliased_table_for( - (reflection.source_reflection || reflection).options[:join_table], - table_alias_for(reflection, true) - ) - end - end - tables + def alias_suffix + reflection.name end def table_name_for(reflection) @@ -123,27 +112,6 @@ module ActiveRecord end end - def table_alias_for(reflection, join = false) - name = alias_tracker.pluralize(reflection.name) - name << "_#{self.reflection.name}" - name << "_join" if join - name - end - - def inner_join(table, reflection, *conditions) - conditions = sanitize_conditions(reflection, conditions) - table.create_join(table, table.create_on(conditions)) - end - - def sanitize_conditions(reflection, conditions) - conditions = 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 - - conditions.length == 1 ? conditions.first : Arel::Nodes::And.new(conditions) - end end end end 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 750f03a120..7dc6beeede 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb @@ -2,6 +2,8 @@ module ActiveRecord module Associations class JoinDependency # :nodoc: class JoinAssociation < JoinPart # :nodoc: + include JoinHelper + # The reflection of the association represented attr_reader :reflection @@ -26,6 +28,8 @@ module ActiveRecord delegate :table, :table_name, :to => :parent, :prefix => :parent delegate :alias_tracker, :to => :join_dependency + alias :alias_suffix :parent_table_name + def initialize(reflection, join_dependency, parent = nil) reflection.check_validity! @@ -40,8 +44,7 @@ module ActiveRecord @parent = parent @join_type = Arel::InnerJoin @aliased_prefix = "t#{ join_dependency.join_parts.size }" - - setup_tables + @tables = construct_tables.reverse end def ==(other) @@ -86,14 +89,17 @@ module ActiveRecord foreign_key = reflection.active_record_primary_key end - conditions = self.conditions[i].dup - conditions << table[key].eq(foreign_table[foreign_key]) + conditions = self.conditions[i] if reflection.klass.finder_needs_type_condition? - conditions << reflection.klass.send(:type_condition, table) + conditions += [reflection.klass.send(:type_condition, table)] end - relation = relation.from(join(table, *conditions)) + relation = relation.from(join( + table, + table[key].eq(foreign_table[foreign_key]), + *conditions + )) # The current table in this iteration becomes the foreign table in the next foreign_table = table @@ -121,53 +127,6 @@ module ActiveRecord private - def table_alias_for(reflection, join = false) - name = alias_tracker.pluralize(reflection.name) - name << "_#{parent_table_name}" - name << "_join" if join - name - end - - # 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 = [] - chain.each do |reflection| - @tables << alias_tracker.aliased_table_for( - reflection.table_name, - table_alias_for(reflection, reflection != self.reflection) - ) - - if reflection.source_macro == :has_and_belongs_to_many - @tables << alias_tracker.aliased_table_for( - (reflection.source_reflection || reflection).options[:join_table], - table_alias_for(reflection, true) - ) - end - end - - # We construct the tables in the forward order so that the aliases are generated - # correctly, but then reverse the array because that is the order in which we will - # iterate the chain. - @tables.reverse! - end - - def join(table, *conditions) - conditions = sanitize_conditions(table, conditions) - table.create_join(table, table.create_on(conditions), join_type) - end - - def sanitize_conditions(table, conditions) - conditions = conditions.map do |condition| - condition = active_record.send(:sanitize_sql, interpolate(condition), table.table_alias || table.name) - condition = Arel.sql(condition) unless condition.is_a?(Arel::Node) - condition - end - - conditions.length == 1 ? conditions.first : Arel::Nodes::And.new(conditions) - end - def interpolate(conditions) if conditions.respond_to?(:to_proc) instance_eval(&conditions) diff --git a/activerecord/lib/active_record/associations/join_helper.rb b/activerecord/lib/active_record/associations/join_helper.rb new file mode 100644 index 0000000000..6474f39503 --- /dev/null +++ b/activerecord/lib/active_record/associations/join_helper.rb @@ -0,0 +1,58 @@ +module ActiveRecord + module Associations + # Helper class module which gets mixed into JoinDependency::JoinAssociation and AssociationScope + module JoinHelper #:nodoc: + + def join_type + Arel::InnerJoin + end + + private + + def construct_tables + tables = [] + chain.each do |reflection| + tables << alias_tracker.aliased_table_for( + table_name_for(reflection), + table_alias_for(reflection, reflection != self.reflection) + ) + + if reflection.source_macro == :has_and_belongs_to_many + tables << alias_tracker.aliased_table_for( + (reflection.source_reflection || reflection).options[:join_table], + table_alias_for(reflection, true) + ) + end + end + tables + end + + def table_name_for(reflection) + reflection.table_name + end + + def table_alias_for(reflection, join = false) + name = alias_tracker.pluralize(reflection.name) + name << "_#{alias_suffix}" + name << "_join" if join + name + end + + def join(table, *conditions) + table.create_join(table, table.create_on(sanitize(conditions)), join_type) + end + + def sanitize(conditions) + table = conditions.first.left.relation + + conditions = conditions.map do |condition| + condition = active_record.send(:sanitize_sql, interpolate(condition), table.table_alias || table.name) + condition = Arel.sql(condition) unless condition.is_a?(Arel::Node) + condition + end + + conditions.length == 1 ? conditions.first : Arel::Nodes::And.new(conditions) + end + end + end +end From 39a6f4f25d958783c73377ac52886c9edc19632e Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Fri, 11 Mar 2011 00:51:57 +0000 Subject: [PATCH 071/100] Simplify implementation of ThroughReflection#chain --- activerecord/lib/active_record/reflection.rb | 24 ++------------------ activerecord/test/cases/reflection_test.rb | 2 +- 2 files changed, 3 insertions(+), 23 deletions(-) diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 1f3ace93a9..e801bc4afa 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -409,33 +409,13 @@ module ActiveRecord # Returns an array of reflections which are involved in this 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 #chain on the source reflection and the through # reflection. The base case for the recursion is a normal association, which just returns # [self] as its #chain. def chain @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.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 - # - # It is important to use self, rather than the source_reflection, because self - # may has a :source_type option which needs to be used. - # - # FIXME: Not sure this is correct justification now that we have #conditions - chain = [self] - end - - # Recursively build the rest of the chain - chain += through_reflection.chain - - # Finally return the completed chain + chain = source_reflection.chain + through_reflection.chain + chain[0] = self # Use self so we don't lose the information from :source_type chain end end diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb index 4b881969cc..918bc0a842 100644 --- a/activerecord/test/cases/reflection_test.rb +++ b/activerecord/test/cases/reflection_test.rb @@ -204,7 +204,7 @@ class ReflectionTest < ActiveRecord::TestCase def test_chain expected = [ - Author.reflect_on_association(:essay_categories), + Organization.reflect_on_association(:author_essay_categories), Author.reflect_on_association(:essays), Organization.reflect_on_association(:authors) ] From 02a43f9f4585d3c932e12b60ef23543f9c534a2e Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Sat, 12 Mar 2011 08:42:57 +0000 Subject: [PATCH 072/100] Resolve some TODO comments which I decided did not need anything done --- .../lib/active_record/associations/alias_tracker.rb | 2 +- .../active_record/associations/through_association.rb | 11 ++++------- .../lib/active_record/relation/query_methods.rb | 1 - 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/activerecord/lib/active_record/associations/alias_tracker.rb b/activerecord/lib/active_record/associations/alias_tracker.rb index 6fc2bfdb31..634dee2289 100644 --- a/activerecord/lib/active_record/associations/alias_tracker.rb +++ b/activerecord/lib/active_record/associations/alias_tracker.rb @@ -16,7 +16,7 @@ module ActiveRecord 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? + if table_alias == table_name Arel::Table.new(table_name) else Arel::Table.new(table_name).alias(table_alias) diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb index c0c92e8d72..e6ab628719 100644 --- a/activerecord/lib/active_record/associations/through_association.rb +++ b/activerecord/lib/active_record/associations/through_association.rb @@ -9,12 +9,8 @@ module ActiveRecord # We merge in these scopes for two reasons: # - # 1. To get the scope_for_create on through reflection when building associated objects - # 2. To get the type conditions for any STI classes in the chain - # - # TODO: Don't actually do this. Getting the creation attributes for a non-nested through - # is a special case. The rest (STI conditions) should be handled by the reflection - # itself. + # 1. To get the default_scope conditions for any of the other reflections in the chain + # 2. To get the type conditions for any STI models in the chain def target_scope scope = super chain[1..-1].each do |reflection| @@ -61,7 +57,8 @@ module ActiveRecord end end - # TODO: Think about this in the context of nested associations + # Note: this does not capture all cases, for example it would be crazy to try to + # properly support stale-checking for nested associations. def stale_state if through_reflection.macro == :belongs_to owner[through_reflection.foreign_key].to_s diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 0c7a9ec56d..9470e7c6c5 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -260,7 +260,6 @@ module ActiveRecord join_list ) - # TODO: Necessary? join_nodes.each do |join| join_dependency.alias_tracker.aliased_name_for(join.left.name.downcase) end From 37d93ea16046add35fecd8c279e868869ee744a5 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Sat, 12 Mar 2011 09:32:20 +0000 Subject: [PATCH 073/100] Fix tests under postgres - we should always put conditions in the WHERE part not in ON constraints because postgres requires that the table has been joined before the condition references it. --- .../associations/association_scope.rb | 13 ++++++++----- .../join_dependency/join_association.rb | 19 +++++++++++-------- .../active_record/associations/join_helper.rb | 8 +++----- .../nested_through_associations_test.rb | 2 +- 4 files changed, 23 insertions(+), 19 deletions(-) diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb index b9bbeed4d5..ab102b2b8f 100644 --- a/activerecord/lib/active_record/associations/association_scope.rb +++ b/activerecord/lib/active_record/associations/association_scope.rb @@ -86,11 +86,14 @@ module ActiveRecord scope = scope.where(interpolate(condition)) end else - scope = scope.joins(join( - foreign_table, - table[key].eq(foreign_table[foreign_key]), - *conditions[i] - )) + constraint = table[key].eq(foreign_table[foreign_key]) + join = join(foreign_table, constraint) + + scope = scope.joins(join) + + unless conditions[i].empty? + scope = scope.where(sanitize(conditions[i], table)) + end end end 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 7dc6beeede..4121a5b378 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb @@ -74,7 +74,7 @@ module ActiveRecord foreign_key = reflection.foreign_key when :has_and_belongs_to_many # Join the join table first... - relation = relation.from(join( + relation.from(join( table, table[reflection.foreign_key]. eq(foreign_table[reflection.active_record_primary_key]) @@ -89,17 +89,20 @@ module ActiveRecord foreign_key = reflection.active_record_primary_key end - conditions = self.conditions[i] + constraint = table[key].eq(foreign_table[foreign_key]) if reflection.klass.finder_needs_type_condition? - conditions += [reflection.klass.send(:type_condition, table)] + constraint = table.create_and([ + constraint, + reflection.klass.send(:type_condition, table) + ]) end - relation = relation.from(join( - table, - table[key].eq(foreign_table[foreign_key]), - *conditions - )) + relation.from(join(table, constraint)) + + unless conditions[i].empty? + relation.where(sanitize(conditions[i], table)) + end # The current table in this iteration becomes the foreign table in the next foreign_table = table diff --git a/activerecord/lib/active_record/associations/join_helper.rb b/activerecord/lib/active_record/associations/join_helper.rb index 6474f39503..eae546e76e 100644 --- a/activerecord/lib/active_record/associations/join_helper.rb +++ b/activerecord/lib/active_record/associations/join_helper.rb @@ -38,13 +38,11 @@ module ActiveRecord name end - def join(table, *conditions) - table.create_join(table, table.create_on(sanitize(conditions)), join_type) + def join(table, constraint) + table.create_join(table, table.create_on(constraint), join_type) end - def sanitize(conditions) - table = conditions.first.left.relation - + def sanitize(conditions, table) conditions = conditions.map do |condition| condition = active_record.send(:sanitize_sql, interpolate(condition), table.table_alias || table.name) condition = Arel.sql(condition) unless condition.is_a?(Arel::Node) diff --git a/activerecord/test/cases/associations/nested_through_associations_test.rb b/activerecord/test/cases/associations/nested_through_associations_test.rb index 0dd407f342..dd450a2a8e 100644 --- a/activerecord/test/cases/associations/nested_through_associations_test.rb +++ b/activerecord/test/cases/associations/nested_through_associations_test.rb @@ -272,7 +272,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase 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'), + Author.where('comments.id' => comments(:does_it_hurt).id).order('authors.id'), [authors(:david), authors(:mary)], :category_post_comments ) end From 61781d84c11c8b5e37ad7cca5c40864759150a33 Mon Sep 17 00:00:00 2001 From: Gabriel Horner Date: Tue, 15 Mar 2011 09:23:17 -0400 Subject: [PATCH 074/100] doc :anchor option for #match in routes --- actionpack/lib/action_dispatch/routing/mapper.rb | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index f67708722b..64368ca28f 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -364,6 +364,13 @@ module ActionDispatch # match 'path' => 'c#a', :defaults => { :format => 'jpg' } # # See Scoping#defaults for its scope equivalent. + # + # [:anchor] + # Boolean to anchor a #match pattern. Default is true. When set to + # false, the pattern matches any request prefixed with the given path. + # + # # Matches any request starting with 'path' + # match 'path' => 'c#a', :anchor => false def match(path, options=nil) mapping = Mapping.new(@set, @scope, path, options || {}).to_route @set.add_route(*mapping) From 64fe0d4cba1cbe3bb6c78cad129aa9be43fdcc29 Mon Sep 17 00:00:00 2001 From: Manuel Meurer Date: Wed, 16 Mar 2011 11:01:43 +0700 Subject: [PATCH 075/100] Remove incorrect comment that a default value of NULL cannot be set with change_column_default. --- .../connection_adapters/abstract/schema_statements.rb | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb index 3ec7dd02a4..8bae50885f 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -279,12 +279,11 @@ module ActiveRecord raise NotImplementedError, "change_column is not implemented" end - # Sets a new default value for a column. If you want to set the default - # value to +NULL+, you are out of luck. You need to - # DatabaseStatements#execute the appropriate SQL statement yourself. + # Sets a new default value for a column. # ===== Examples # change_column_default(:suppliers, :qualification, 'new') # change_column_default(:accounts, :authorized, 1) + # change_column_default(:users, :email, nil) def change_column_default(table_name, column_name, default) raise NotImplementedError, "change_column_default is not implemented" end From 3edba7189509915c0159e84a7c9be980fae0732f Mon Sep 17 00:00:00 2001 From: Nicolas Cavigneaux Date: Wed, 16 Mar 2011 13:04:39 +0100 Subject: [PATCH 076/100] Add link to mailing-list --- railties/guides/source/contributing_to_ruby_on_rails.textile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/railties/guides/source/contributing_to_ruby_on_rails.textile b/railties/guides/source/contributing_to_ruby_on_rails.textile index 846a25d6d2..1fcc4fd7e3 100644 --- a/railties/guides/source/contributing_to_ruby_on_rails.textile +++ b/railties/guides/source/contributing_to_ruby_on_rails.textile @@ -371,7 +371,7 @@ Now create a ticket with your patch. Go to the "new ticket":http://rails.lightho h4. Get Some Feedback -Now you need to get other people to look at your patch, just as you've looked at other people's patches. You can use the rubyonrails-core mailing list or the #rails-contrib channel on IRC freenode for this. You might also try just talking to Rails developers that you know. +Now you need to get other people to look at your patch, just as you've looked at other people's patches. You can use the "rubyonrails-core mailing list":http://groups.google.com/group/rubyonrails-core/ or the #rails-contrib channel on IRC freenode for this. You might also try just talking to Rails developers that you know. h4. Iterate as Necessary From 9abc94c44516afdcfe4a3b202c336c9578fd6d0d Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Wed, 16 Mar 2011 18:54:34 +0000 Subject: [PATCH 077/100] oracle, y u defy me --- .../test/cases/associations/join_model_test.rb | 2 +- activerecord/test/cases/reflection_test.rb | 10 +++++----- activerecord/test/models/author.rb | 6 ++++-- activerecord/test/models/post.rb | 8 ++++---- activerecord/test/models/tagging.rb | 2 +- 5 files changed, 15 insertions(+), 13 deletions(-) diff --git a/activerecord/test/cases/associations/join_model_test.rb b/activerecord/test/cases/associations/join_model_test.rb index 543eff7d8b..f54d69e7fa 100644 --- a/activerecord/test/cases/associations/join_model_test.rb +++ b/activerecord/test/cases/associations/join_model_test.rb @@ -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), authors(:bob)], posts(:thinking).authors_using_custom_pk.order(:id) + assert_equal [authors(:david), authors(:bob)], posts(:thinking).authors_using_custom_pk.order('authors.id') end def test_both_scoped_and_explicit_joins_should_be_respected diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb index 918bc0a842..97d9669483 100644 --- a/activerecord/test/cases/reflection_test.rb +++ b/activerecord/test/cases/reflection_test.rb @@ -215,16 +215,16 @@ class ReflectionTest < ActiveRecord::TestCase def test_conditions expected = [ - ["tags.name = 'Blue'"], - ["taggings.comment = 'first'", {"taggable_type"=>"Post"}], - ["posts.title LIKE 'misc post%'"] + [{ :tags => { :name => 'Blue' } }], + [{ :taggings => { :comment => 'first' } }, { "taggable_type" => "Post" }], + [{ :posts => { :title => ['misc post by bob', 'misc post by mary'] } }] ] actual = Author.reflect_on_association(:misc_post_first_blue_tags).conditions assert_equal expected, actual expected = [ - ["tags.name = 'Blue'", "taggings.comment = 'first'", "posts.title LIKE 'misc post%'"], - [{"taggable_type"=>"Post"}], + [{ :tags => { :name => 'Blue' } }, { :taggings => { :comment => 'first' } }, { :posts => { :title => ['misc post by bob', 'misc post by mary'] } }], + [{ "taggable_type" => "Post" }], [] ] actual = Author.reflect_on_association(:misc_post_first_blue_tags_2).conditions diff --git a/activerecord/test/models/author.rb b/activerecord/test/models/author.rb index f362f70dec..e0cbc44265 100644 --- a/activerecord/test/models/author.rb +++ b/activerecord/test/models/author.rb @@ -131,10 +131,12 @@ class Author < ActiveRecord::Base 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_posts, :class_name => 'Post', + :conditions => { :posts => { :title => ['misc post by bob', 'misc post by mary'] } } 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%'" + has_many :misc_post_first_blue_tags_2, :through => :posts, :source => :first_blue_tags_2, + :conditions => { :posts => { :title => ['misc post by bob', 'misc post by mary'] } } scope :relation_include_posts, includes(:posts) scope :relation_include_tags, includes(:tags) diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb index c3843fd264..82894a3d57 100644 --- a/activerecord/test/models/post.rb +++ b/activerecord/test/models/post.rb @@ -73,16 +73,16 @@ class Post < ActiveRecord::Base has_many :tags_with_destroy, :through => :taggings, :source => :tag, :dependent => :destroy has_many :tags_with_nullify, :through => :taggings, :source => :tag, :dependent => :nullify - has_many :misc_tags, :through => :taggings, :source => :tag, :conditions => "tags.name = 'Misc'" + 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_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 :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/tagging.rb b/activerecord/test/models/tagging.rb index 17e627a60e..ef323df158 100644 --- a/activerecord/test/models/tagging.rb +++ b/activerecord/test/models/tagging.rb @@ -6,7 +6,7 @@ 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 :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 From f3666040a0a883ab75dd53012cc4ace66f90606b Mon Sep 17 00:00:00 2001 From: Paco Guzman Date: Fri, 18 Mar 2011 11:29:52 +0100 Subject: [PATCH 078/100] remove bank line --- .../connection_adapters/abstract/database_statements.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb index 5c1ce173c8..d54a643d7b 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -237,7 +237,6 @@ module ActiveRecord # add_limit_offset!('SELECT * FROM suppliers', {:limit => 10, :offset => 50}) # generates # SELECT * FROM suppliers LIMIT 10 OFFSET 50 - def add_limit_offset!(sql, options) if limit = options[:limit] sql << " LIMIT #{sanitize_limit(limit)}" From 5b84aebd14fdb1aa9746a8404589c9ada4bcbcbb Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Fri, 18 Mar 2011 23:14:45 +0000 Subject: [PATCH 079/100] Add order clauses to fix some tests which were failing under 1.8 on oracle and postgres --- activerecord/test/cases/identity_map_test.rb | 18 +++++++++--------- activerecord/test/cases/relations_test.rb | 10 +++++----- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/activerecord/test/cases/identity_map_test.rb b/activerecord/test/cases/identity_map_test.rb index 399d6c26df..89f7b92d09 100644 --- a/activerecord/test/cases/identity_map_test.rb +++ b/activerecord/test/cases/identity_map_test.rb @@ -207,31 +207,31 @@ class IdentityMapTest < 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 # With IM we'll retrieve post object from previous query, it'll have comments # already preloaded from first call assert_queries(1) do - posts = Post.preload(:comments).to_a + posts = Post.preload(:comments).order('posts.id') assert posts.first.comments.first end assert_queries(2) do - posts = Post.preload(:author) + posts = Post.preload(:author).order('posts.id') assert posts.first.author end # With IM we'll retrieve post object from previous query, it'll have comments # already preloaded from first call assert_queries(1) do - posts = Post.preload(:author).to_a + posts = Post.preload(:author).order('posts.id') assert posts.first.author end assert_queries(1) do - posts = Post.preload(:author, :comments).to_a + posts = Post.preload(:author, :comments).order('posts.id') assert posts.first.author assert posts.first.comments.first end @@ -239,22 +239,22 @@ class IdentityMapTest < 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 assert_queries(1) do - posts = Post.scoped.includes(:comments) + posts = Post.scoped.includes(:comments).order('posts.id') assert posts.first.comments.first end assert_queries(2) do - posts = Post.includes(:author) + posts = Post.includes(:author).order('posts.id') assert posts.first.author end assert_queries(1) do - posts = Post.includes(:author, :comments).to_a + posts = Post.includes(:author, :comments).order('posts.id') assert posts.first.author assert posts.first.comments.first end diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index 680e1d50cf..94916f61c5 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -286,7 +286,7 @@ class RelationTest < ActiveRecord::TestCase end assert_queries(ActiveRecord::IdentityMap.enabled? ? 1 : 2) do - posts = Post.preload(:comments).to_a + posts = Post.preload(:comments).order('posts.id') assert posts.first.comments.first end @@ -296,12 +296,12 @@ class RelationTest < ActiveRecord::TestCase end assert_queries(ActiveRecord::IdentityMap.enabled? ? 1 : 2) do - posts = Post.preload(:author).to_a + posts = Post.preload(:author).order('posts.id') assert posts.first.author end assert_queries(ActiveRecord::IdentityMap.enabled? ? 1 : 3) do - posts = Post.preload(:author, :comments).to_a + posts = Post.preload(:author, :comments).order('posts.id') assert posts.first.author assert posts.first.comments.first end @@ -314,7 +314,7 @@ class RelationTest < ActiveRecord::TestCase end assert_queries(ActiveRecord::IdentityMap.enabled? ? 1 : 2) do - posts = Post.scoped.includes(:comments) + posts = Post.scoped.includes(:comments).order('posts.id') assert posts.first.comments.first end @@ -324,7 +324,7 @@ class RelationTest < ActiveRecord::TestCase end assert_queries(ActiveRecord::IdentityMap.enabled? ? 1 : 3) do - posts = Post.includes(:author, :comments).to_a + posts = Post.includes(:author, :comments).order('posts.id') assert posts.first.author assert posts.first.comments.first end From 6c309f04c9e2538cc61d6a1c753da3c6cc91b51f Mon Sep 17 00:00:00 2001 From: Gabriel Horner Date: Sat, 19 Mar 2011 19:53:36 -0400 Subject: [PATCH 080/100] Beef up Rails::Railtie::Configuration docs --- railties/lib/rails/railtie/configuration.rb | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/railties/lib/rails/railtie/configuration.rb b/railties/lib/rails/railtie/configuration.rb index e4368866a1..2c7b5bc048 100644 --- a/railties/lib/rails/railtie/configuration.rb +++ b/railties/lib/rails/railtie/configuration.rb @@ -31,26 +31,34 @@ module Rails app_generators(&block) end + # First configurable block to run. Called before any initializers are run. def before_configuration(&block) ActiveSupport.on_load(:before_configuration, :yield => true, &block) end + # Third configurable block to run. Does not run if config.cache_classes + # set to false. def before_eager_load(&block) ActiveSupport.on_load(:before_eager_load, :yield => true, &block) end + # Second configurable block to run. Called before frameworks initialize. def before_initialize(&block) ActiveSupport.on_load(:before_initialize, :yield => true, &block) end + # Last configurable block to run. Called after frameworks initialize. def after_initialize(&block) ActiveSupport.on_load(:after_initialize, :yield => true, &block) end + # Array of callbacks defined by #to_prepare. def to_prepare_blocks @@to_prepare_blocks ||= [] end + # Defines generic callbacks to run before #after_initialize. Useful for + # Rails::Railtie subclasses. def to_prepare(&blk) to_prepare_blocks << blk if blk end From 3378d77b044306d6e92f188932d435483af569e3 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Mon, 21 Mar 2011 16:32:13 -0700 Subject: [PATCH 081/100] use prepared statements to fetch the last insert id --- .../connection_adapters/postgresql_adapter.rb | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 576450bc3a..5a830a50fb 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -453,7 +453,7 @@ module ActiveRecord # If a pk is given, fallback to default sequence name. # Don't fetch last insert id for a table without a pk. if pk && sequence_name ||= default_sequence_name(table, pk) - last_insert_id(table, sequence_name) + last_insert_id(sequence_name) end end end @@ -1038,8 +1038,9 @@ module ActiveRecord end # Returns the current ID of a table's sequence. - def last_insert_id(table, sequence_name) #:nodoc: - Integer(select_value("SELECT currval('#{sequence_name}')")) + def last_insert_id(sequence_name) #:nodoc: + r = exec_query("SELECT currval($1)", 'SQL', [[nil, sequence_name]]) + Integer(r.rows.first.first) end # Executes a SELECT query and returns the results, performing any data type From c24e5548fd372e4d0058ab417230ebc2fa3bebde Mon Sep 17 00:00:00 2001 From: Ryan Bigg Date: Tue, 22 Mar 2011 12:16:13 +1100 Subject: [PATCH 082/100] Querying guide: mention that performing a where on an relation that contains an includes statement will generate a LEFT OUTER JOIN rather than an INNER JOIN or another query --- .../guides/source/active_record_querying.textile | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/railties/guides/source/active_record_querying.textile b/railties/guides/source/active_record_querying.textile index ed3968e226..e0fab46db9 100644 --- a/railties/guides/source/active_record_querying.textile +++ b/railties/guides/source/active_record_querying.textile @@ -747,6 +747,20 @@ h4. Specifying Conditions on Eager Loaded Associations Even though Active Record lets you specify conditions on the eager loaded associations just like +joins+, the recommended way is to use "joins":#joining-tables instead. +However if you must do this, you may use +where+ as you would normally. + + +Post.includes(:comments).where("comments.visible", true) + + +This would generate a query which contains a +LEFT OUTER JOIN+ whereas the +joins+ method would generate one using the +INNER JOIN+ function instead. + + + SELECT "posts"."id" AS t0_r0, ... "comments"."updated_at" AS t1_r5 FROM "posts" LEFT OUTER JOIN "comments" ON "comments"."post_id" = "posts"."id" WHERE (comments.visible) + + +If in this case there were no comments for any posts, all the posts would still be loaded. By using +joins+ (an INNER JOIN), the join conditions *must* match, otherwise no records will be returned. + h3. Scopes Scoping allows you to specify commonly-used ARel queries which can be referenced as method calls on the association objects or models. With these scopes, you can use every method previously covered such as +where+, +joins+ and +includes+. All scope methods will return an +ActiveRecord::Relation+ object which will allow for further methods (such as other scopes) to be called on it. From 5a44951186b410c3a79685f8aeaa926d93a6aab0 Mon Sep 17 00:00:00 2001 From: Ryan Bigg Date: Tue, 22 Mar 2011 12:16:15 +1100 Subject: [PATCH 083/100] Querying guide: mention that performing a where on an relation that contains an includes statement will generate a LEFT OUTER JOIN rather than an INNER JOIN or another query --- railties/guides/source/active_record_querying.textile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/railties/guides/source/active_record_querying.textile b/railties/guides/source/active_record_querying.textile index e0fab46db9..009d541106 100644 --- a/railties/guides/source/active_record_querying.textile +++ b/railties/guides/source/active_record_querying.textile @@ -759,7 +759,9 @@ This would generate a query which contains a +LEFT OUTER JOIN+ whereas the +join SELECT "posts"."id" AS t0_r0, ... "comments"."updated_at" AS t1_r5 FROM "posts" LEFT OUTER JOIN "comments" ON "comments"."post_id" = "posts"."id" WHERE (comments.visible) -If in this case there were no comments for any posts, all the posts would still be loaded. By using +joins+ (an INNER JOIN), the join conditions *must* match, otherwise no records will be returned. +If there was no +where+ condition, this would generate the normal set of two queries. + +If, in the case of this +includes+ query, there were no comments for any posts, all the posts would still be loaded. By using +joins+ (an INNER JOIN), the join conditions *must* match, otherwise no records will be returned. h3. Scopes From 15d3cc21f41af4b3caaae4f3586481effa77058f Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Tue, 22 Mar 2011 09:18:01 -0700 Subject: [PATCH 084/100] pushing id insertion and prefetch primary keys down to Relation#insert --- .../abstract/database_statements.rb | 4 ++++ .../connection_adapters/sqlite_adapter.rb | 4 ++++ activerecord/lib/active_record/persistence.rb | 10 +--------- activerecord/lib/active_record/relation.rb | 17 ++++++++++++++--- 4 files changed, 23 insertions(+), 12 deletions(-) diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb index 5c1ce173c8..756c221fad 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -272,6 +272,10 @@ module ActiveRecord execute "INSERT INTO #{quote_table_name(table_name)} (#{key_list.join(', ')}) VALUES (#{value_list.join(', ')})", 'Fixture Insert' end + def null_insert_value + Arel.sql 'DEFAULT' + end + def empty_insert_statement_value "VALUES(DEFAULT)" end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb index 9ee6b88ab6..ae61d6ce94 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb @@ -336,6 +336,10 @@ module ActiveRecord alter_table(table_name, :rename => {column_name.to_s => new_column_name.to_s}) end + def null_insert_value + Arel.sql 'NULL' + end + def empty_insert_statement_value "VALUES(NULL)" end diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index df7b22080c..17a64b6e86 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -270,17 +270,9 @@ module ActiveRecord # Creates a record with values matching those of the instance attributes # and returns its id. def create - if id.nil? && connection.prefetch_primary_key?(self.class.table_name) - self.id = connection.next_sequence_value(self.class.sequence_name) - end - attributes_values = arel_attributes_values(!id.nil?) - new_id = if attributes_values.empty? - self.class.unscoped.insert connection.empty_insert_statement_value - else - self.class.unscoped.insert attributes_values - end + new_id = self.class.unscoped.insert attributes_values self.id ||= new_id diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 371403f510..8e545f9cad 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -30,15 +30,26 @@ module ActiveRecord end def insert(values) - im = arel.compile_insert values - im.into @table - primary_key_value = nil if primary_key && Hash === values primary_key_value = values[values.keys.find { |k| k.name == primary_key }] + + if !primary_key_value && connection.prefetch_primary_key?(klass.table_name) + primary_key_value = connection.next_sequence_value(klass.sequence_name) + values[klass.arel_table[klass.primary_key]] = primary_key_value + end + end + + im = arel.create_insert + im.into @table + + if values.empty? # empty insert + im.values = im.create_values [connection.null_insert_value], [] + else + im.insert values end @klass.connection.insert( From 2ddfdba9a0dab7d8499c3ad0d13583bddbac4f69 Mon Sep 17 00:00:00 2001 From: Prem Sichanugrist Date: Tue, 22 Mar 2011 22:19:31 +0700 Subject: [PATCH 085/100] Do not show optional (.:format) block for wildcard route [#6605 state:resolved] This will make the output of `rake routes` to be correctly match to the behavior of the application, as the regular expression used to match the path is greedy and won't capture the format part by default --- actionpack/lib/action_dispatch/routing/mapper.rb | 2 +- actionpack/test/action_dispatch/routing/mapper_test.rb | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index cc6b8aa82d..1dba1d416c 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -107,7 +107,7 @@ module ActionDispatch if @options[:format] == false @options.delete(:format) path - elsif path.include?(":format") || path.end_with?('/') + elsif path.include?(":format") || path.end_with?('/') || path.match(/^\/?\*/) path else "#{path}(.:format)" diff --git a/actionpack/test/action_dispatch/routing/mapper_test.rb b/actionpack/test/action_dispatch/routing/mapper_test.rb index 9966234f1b..e21b271907 100644 --- a/actionpack/test/action_dispatch/routing/mapper_test.rb +++ b/actionpack/test/action_dispatch/routing/mapper_test.rb @@ -46,6 +46,13 @@ module ActionDispatch mapper.match '/one/two/', :to => 'posts#index', :as => :main assert_equal '/one/two(.:format)', fakeset.conditions.first[:path_info] end + + def test_map_wildcard + fakeset = FakeSet.new + mapper = Mapper.new fakeset + mapper.match '/*path', :to => 'pages#show', :as => :page + assert_equal '/*path', fakeset.conditions.first[:path_info] + end end end end From ed97c395178391c9083b20121cdd6606a9e92d14 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Tue, 22 Mar 2011 10:10:33 -0700 Subject: [PATCH 086/100] adding missing require --- activerecord/test/cases/associations/join_model_test.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/activerecord/test/cases/associations/join_model_test.rb b/activerecord/test/cases/associations/join_model_test.rb index f54d69e7fa..1f95b31497 100644 --- a/activerecord/test/cases/associations/join_model_test.rb +++ b/activerecord/test/cases/associations/join_model_test.rb @@ -2,6 +2,7 @@ require "cases/helper" require 'models/tag' require 'models/tagging' require 'models/post' +require 'models/rating' require 'models/item' require 'models/comment' require 'models/author' From e06c44800284696868a1e6a273abefec3047312f Mon Sep 17 00:00:00 2001 From: Piotr Sarnacki Date: Tue, 22 Mar 2011 19:35:37 +0100 Subject: [PATCH 087/100] Install rb-fsevent gem if ENV[RB_FSEVENT] is set --- Gemfile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Gemfile b/Gemfile index 7d8949409c..879598c6db 100644 --- a/Gemfile +++ b/Gemfile @@ -36,6 +36,9 @@ platforms :mri_19 do end platforms :ruby do + if ENV["RB_FSEVENT"] + gem 'rb-fsevent' + end gem 'json' gem 'yajl-ruby' gem "nokogiri", ">= 1.4.4" From 137ff0402a05e39c85852b148443750d5d222b66 Mon Sep 17 00:00:00 2001 From: Ryan Bigg Date: Wed, 23 Mar 2011 08:33:06 +1100 Subject: [PATCH 088/100] Caching guide: Capitalize 'Note' so that it's rendered as a proper note --- railties/guides/source/caching_with_rails.textile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/railties/guides/source/caching_with_rails.textile b/railties/guides/source/caching_with_rails.textile index 1b5ec40d16..297ba2d661 100644 --- a/railties/guides/source/caching_with_rails.textile +++ b/railties/guides/source/caching_with_rails.textile @@ -65,7 +65,7 @@ end If you want a more complicated expiration scheme, you can use cache sweepers to expire cached objects when things change. This is covered in the section on Sweepers. -Note: Page caching ignores all parameters. For example +/products?page=1+ will be written out to the filesystem as +products.html+ with no reference to the +page+ parameter. Thus, if someone requests +/products?page=2+ later, they will get the cached first page. Be careful when page caching GET parameters in the URL! +NOTE: Page caching ignores all parameters. For example +/products?page=1+ will be written out to the filesystem as +products.html+ with no reference to the +page+ parameter. Thus, if someone requests +/products?page=2+ later, they will get the cached first page. Be careful when page caching GET parameters in the URL! INFO: Page caching runs in an after filter. Thus, invalid requests won't generate spurious cache entries as long as you halt them. Typically, a redirection in some before filter that checks request preconditions does the job. From ee8ca49414271026575dab70947b446ac6e4b51a Mon Sep 17 00:00:00 2001 From: Dr Nic Williams Date: Tue, 22 Mar 2011 15:11:12 -0700 Subject: [PATCH 089/100] Update 'Getting Started on Windows' tip to go to one-click Rails Installer --- railties/guides/source/getting_started.textile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/railties/guides/source/getting_started.textile b/railties/guides/source/getting_started.textile index e94bdc97b0..0661549644 100644 --- a/railties/guides/source/getting_started.textile +++ b/railties/guides/source/getting_started.textile @@ -149,7 +149,7 @@ Usually run this as the root user: # gem install rails -TIP. If you're working on Windows, you should be aware that the vast majority of Rails development is done in Unix environments. While Ruby and Rails themselves install easily using for example "Ruby Installer":http://rubyinstaller.org/, the supporting ecosystem often assumes you are able to build C-based rubygems and work in a command window. If at all possible, we suggest that you install a Linux virtual machine and use that for Rails development, instead of using Windows. +TIP. If you're working on Windows, you can quickly install Ruby and Rails with "Rails Installer":http://railsinstaller.org. h4. Creating the Blog Application From da6c7bd4b411105e7556ff5015e3c9f6ab1d26fe Mon Sep 17 00:00:00 2001 From: Santiago Pastorino Date: Fri, 18 Mar 2011 13:35:57 -0300 Subject: [PATCH 090/100] Do not in place modify what table_name returns --- activemodel/lib/active_model/attribute_methods.rb | 2 +- activemodel/test/cases/attribute_methods_test.rb | 11 ----------- activerecord/lib/active_record/base.rb | 2 +- 3 files changed, 2 insertions(+), 13 deletions(-) diff --git a/activemodel/lib/active_model/attribute_methods.rb b/activemodel/lib/active_model/attribute_methods.rb index 73468afe55..f6648ea43e 100644 --- a/activemodel/lib/active_model/attribute_methods.rb +++ b/activemodel/lib/active_model/attribute_methods.rb @@ -107,7 +107,7 @@ module ActiveModel sing.send :define_method, name, &block else value = value.to_s if value - sing.send(:define_method, name) { value && value.dup } + sing.send(:define_method, name) { value } end end diff --git a/activemodel/test/cases/attribute_methods_test.rb b/activemodel/test/cases/attribute_methods_test.rb index e814666d99..022c6716bd 100644 --- a/activemodel/test/cases/attribute_methods_test.rb +++ b/activemodel/test/cases/attribute_methods_test.rb @@ -9,10 +9,6 @@ class ModelWithAttributes define_method(:bar) do 'original bar' end - - define_method(:zomg) do - 'original zomg' - end end def attributes @@ -102,13 +98,6 @@ class AttributeMethodsTest < ActiveModel::TestCase assert_equal "value of foo bar", ModelWithAttributesWithSpaces.new.send(:'foo bar') end - def test_defined_methods_always_return_duped_string - ModelWithAttributes.define_attr_method(:zomg, 'lol') - assert_equal 'lol', ModelWithAttributes.zomg - ModelWithAttributes.zomg << 'bbq' - assert_equal 'lol', ModelWithAttributes.zomg - end - test '#define_attr_method generates attribute method' do ModelWithAttributes.define_attr_method(:bar, 'bar') diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 7f506faeee..b778b0c0f0 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -995,7 +995,7 @@ module ActiveRecord #:nodoc: if parent < ActiveRecord::Base && !parent.abstract_class? contained = parent.table_name contained = contained.singularize if parent.pluralize_table_names - contained << '_' + contained += '_' end "#{full_table_name_prefix}#{contained}#{undecorated_table_name(name)}#{table_name_suffix}" else From baa237c974fee8023dd704a4efb418ff0e963de0 Mon Sep 17 00:00:00 2001 From: Santiago Pastorino Date: Mon, 21 Mar 2011 21:36:05 -0300 Subject: [PATCH 091/100] Allow to read and write AR attributes with non valid identifiers --- .../lib/active_record/attribute_methods/read.rb | 5 ++++- .../lib/active_record/attribute_methods/write.rb | 4 +++- activerecord/test/cases/base_test.rb | 12 ++++++++++++ activerecord/test/schema/schema.rb | 3 +++ 4 files changed, 22 insertions(+), 2 deletions(-) diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index ab86d8bad1..affe2d7f53 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -70,7 +70,10 @@ module ActiveRecord if cache_attribute?(attr_name) access_code = "@attributes_cache['#{attr_name}'] ||= (#{access_code})" end - generated_attribute_methods.module_eval("def _#{symbol}; #{access_code}; end; alias #{symbol} _#{symbol}", __FILE__, __LINE__) + generated_attribute_methods.module_eval do + define_method("_#{symbol}") { eval(access_code) } + alias_method(symbol, "_#{symbol}") + end end end diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb index 6a593a7e0e..832f2ed408 100644 --- a/activerecord/lib/active_record/attribute_methods/write.rb +++ b/activerecord/lib/active_record/attribute_methods/write.rb @@ -10,7 +10,9 @@ module ActiveRecord module ClassMethods protected def define_method_attribute=(attr_name) - generated_attribute_methods.module_eval("def #{attr_name}=(new_value); write_attribute('#{attr_name}', new_value); end", __FILE__, __LINE__) + generated_attribute_methods.send(:define_method, "#{attr_name}=") do |new_value| + write_attribute(attr_name, new_value) + end end end diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index d03fc68a11..fba7af741d 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -45,6 +45,8 @@ class ReadonlyTitlePost < Post attr_readonly :title end +class Weird < ActiveRecord::Base; end + class Boolean < ActiveRecord::Base; end class BasicsTest < ActiveRecord::TestCase @@ -477,6 +479,16 @@ class BasicsTest < ActiveRecord::TestCase assert_equal "changed", post.body end + def test_non_valid_identifier_column_name + weird = Weird.create('a$b' => 'value') + weird.reload + assert_equal 'value', weird.send('a$b') + + weird.update_attribute('a$b', 'value2') + weird.reload + assert_equal 'value2', weird.send('a$b') + end + def test_multiparameter_attributes_on_date attributes = { "last_read(1i)" => "2004", "last_read(2i)" => "6", "last_read(3i)" => "24" } topic = Topic.find(1) diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index bdadd0698b..362475de36 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -690,6 +690,9 @@ ActiveRecord::Schema.define do t.integer :molecule_id t.string :name end + create_table :weirds, :force => true do |t| + t.string 'a$b' + end except 'SQLite' do # fk_test_has_fk should be before fk_test_has_pk From 450f7cf01b855b536416fc048a92c4309da2492e Mon Sep 17 00:00:00 2001 From: Santiago Pastorino Date: Tue, 22 Mar 2011 20:11:36 -0300 Subject: [PATCH 092/100] use class_eval with a string when it's possible --- activemodel/lib/active_model/attribute_methods.rb | 10 ++++++++-- .../lib/active_record/attribute_methods/read.rb | 10 +++++++--- .../lib/active_record/attribute_methods/write.rb | 8 ++++++-- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/activemodel/lib/active_model/attribute_methods.rb b/activemodel/lib/active_model/attribute_methods.rb index f6648ea43e..be55581c66 100644 --- a/activemodel/lib/active_model/attribute_methods.rb +++ b/activemodel/lib/active_model/attribute_methods.rb @@ -106,8 +106,14 @@ module ActiveModel if block_given? sing.send :define_method, name, &block else - value = value.to_s if value - sing.send(:define_method, name) { value } + if name =~ /^[a-zA-Z_]\w*[!?=]?$/ + sing.class_eval <<-eorb, __FILE__, __LINE__ + 1 + def #{name}; #{value.nil? ? 'nil' : value.to_s.inspect}; end + eorb + else + value = value.to_s if value + sing.send(:define_method, name) { value } + end end end diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index affe2d7f53..69d5cd83f1 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -70,9 +70,13 @@ module ActiveRecord if cache_attribute?(attr_name) access_code = "@attributes_cache['#{attr_name}'] ||= (#{access_code})" end - generated_attribute_methods.module_eval do - define_method("_#{symbol}") { eval(access_code) } - alias_method(symbol, "_#{symbol}") + if symbol =~ /^[a-zA-Z_]\w*[!?=]?$/ + generated_attribute_methods.module_eval("def _#{symbol}; #{access_code}; end; alias #{symbol} _#{symbol}", __FILE__, __LINE__) + else + generated_attribute_methods.module_eval do + define_method("_#{symbol}") { eval(access_code) } + alias_method(symbol, "_#{symbol}") + end end end end diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb index 832f2ed408..3c4dab304e 100644 --- a/activerecord/lib/active_record/attribute_methods/write.rb +++ b/activerecord/lib/active_record/attribute_methods/write.rb @@ -10,8 +10,12 @@ module ActiveRecord module ClassMethods protected def define_method_attribute=(attr_name) - generated_attribute_methods.send(:define_method, "#{attr_name}=") do |new_value| - write_attribute(attr_name, new_value) + if attr_name =~ /^[a-zA-Z_]\w*[!?=]?$/ + generated_attribute_methods.module_eval("def #{attr_name}=(new_value); write_attribute('#{attr_name}', new_value); end", __FILE__, __LINE__) + else + generated_attribute_methods.send(:define_method, "#{attr_name}=") do |new_value| + write_attribute(attr_name, new_value) + end end end end From 7004434ad85901215e1cb4367b2b8b1e7f9063c7 Mon Sep 17 00:00:00 2001 From: Diego Carrion Date: Wed, 9 Mar 2011 20:27:29 -0300 Subject: [PATCH 093/100] test json decoding with time parsing disabled with all backends and respect ActiveSupport.parse_json_times when converting to yaml Signed-off-by: Santiago Pastorino --- activesupport/lib/active_support/json/backends/yaml.rb | 8 +++++--- activesupport/test/json/decoding_test.rb | 6 ++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/activesupport/lib/active_support/json/backends/yaml.rb b/activesupport/lib/active_support/json/backends/yaml.rb index 077eda548a..f9d8fd8e83 100644 --- a/activesupport/lib/active_support/json/backends/yaml.rb +++ b/activesupport/lib/active_support/json/backends/yaml.rb @@ -70,9 +70,11 @@ module ActiveSupport left_pos.each_with_index do |left, i| scanner.pos = left.succ chunk = scanner.peek(right_pos[i] - scanner.pos + 1) - # overwrite the quotes found around the dates with spaces - while times.size > 0 && times[0] <= right_pos[i] - chunk.insert(times.shift - scanner.pos - 1, '! ') + if ActiveSupport.parse_json_times + # overwrite the quotes found around the dates with spaces + while times.size > 0 && times[0] <= right_pos[i] + chunk.insert(times.shift - scanner.pos - 1, '! ') + end end chunk.gsub!(/\\([\\\/]|u[[:xdigit:]]{4})/) do ustr = $1 diff --git a/activesupport/test/json/decoding_test.rb b/activesupport/test/json/decoding_test.rb index 24d9f88c09..6b890c7d46 100644 --- a/activesupport/test/json/decoding_test.rb +++ b/activesupport/test/json/decoding_test.rb @@ -72,13 +72,11 @@ class TestJSONDecoding < ActiveSupport::TestCase end end end - end - if backends.include?("JSONGem") - test "json decodes time json with time parsing disabled" do + test "json decodes time json with time parsing disabled with the #{backend} backend" do ActiveSupport.parse_json_times = false expected = {"a" => "2007-01-01 01:12:34 Z"} - ActiveSupport::JSON.with_backend "JSONGem" do + ActiveSupport::JSON.with_backend backend do assert_equal expected, ActiveSupport::JSON.decode(%({"a": "2007-01-01 01:12:34 Z"})) end end From a1edbf720620c672566a1681572c3845452ca51a Mon Sep 17 00:00:00 2001 From: Diego Carrion Date: Wed, 9 Mar 2011 21:45:45 -0300 Subject: [PATCH 094/100] parse dates to yaml in json arrays Signed-off-by: Santiago Pastorino --- activesupport/lib/active_support/json/backends/yaml.rb | 4 ++-- activesupport/test/json/decoding_test.rb | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/activesupport/lib/active_support/json/backends/yaml.rb b/activesupport/lib/active_support/json/backends/yaml.rb index f9d8fd8e83..e25e29d36b 100644 --- a/activesupport/lib/active_support/json/backends/yaml.rb +++ b/activesupport/lib/active_support/json/backends/yaml.rb @@ -29,7 +29,7 @@ module ActiveSupport def convert_json_to_yaml(json) #:nodoc: require 'strscan' unless defined? ::StringScanner scanner, quoting, marks, pos, times = ::StringScanner.new(json), false, [], nil, [] - while scanner.scan_until(/(\\['"]|['":,\\]|\\.)/) + while scanner.scan_until(/(\\['"]|['":,\\]|\\.|[\]])/) case char = scanner[1] when '"', "'" if !quoting @@ -43,7 +43,7 @@ module ActiveSupport end quoting = false end - when ":","," + when ":",",", "]" marks << scanner.pos - 1 unless quoting when "\\" scanner.skip(/\\/) diff --git a/activesupport/test/json/decoding_test.rb b/activesupport/test/json/decoding_test.rb index 6b890c7d46..88cf97de7e 100644 --- a/activesupport/test/json/decoding_test.rb +++ b/activesupport/test/json/decoding_test.rb @@ -17,6 +17,8 @@ class TestJSONDecoding < ActiveSupport::TestCase %({"matzue": "松江", "asakusa": "浅草"}) => {"matzue" => "松江", "asakusa" => "浅草"}, %({"a": "2007-01-01"}) => {'a' => Date.new(2007, 1, 1)}, %({"a": "2007-01-01 01:12:34 Z"}) => {'a' => Time.utc(2007, 1, 1, 1, 12, 34)}, + %(["2007-01-01 01:12:34 Z"]) => [Time.utc(2007, 1, 1, 1, 12, 34)], + %(["2007-01-01 01:12:34 Z", "2007-01-01 01:12:35 Z"]) => [Time.utc(2007, 1, 1, 1, 12, 34), Time.utc(2007, 1, 1, 1, 12, 35)], # no time zone %({"a": "2007-01-01 01:12:34"}) => {'a' => "2007-01-01 01:12:34"}, # invalid date From 3a7c7dc73d6111e7c821a81d9c56024dd35769a5 Mon Sep 17 00:00:00 2001 From: Aditya Sanghi Date: Wed, 23 Mar 2011 18:06:22 +0100 Subject: [PATCH 095/100] Fix test for prepend giving a false positive. [#5716 state:resolved] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: José Valim --- .../rails/generators/rails/plugin_new/plugin_new_generator.rb | 2 +- railties/test/generators/shared_generator_tests.rb | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/railties/lib/rails/generators/rails/plugin_new/plugin_new_generator.rb b/railties/lib/rails/generators/rails/plugin_new/plugin_new_generator.rb index c373ca5e67..3cf8410d1e 100644 --- a/railties/lib/rails/generators/rails/plugin_new/plugin_new_generator.rb +++ b/railties/lib/rails/generators/rails/plugin_new/plugin_new_generator.rb @@ -62,7 +62,7 @@ task :default => :test end def generate_test_dummy(force = false) - opts = (options || {}).slice(:skip_active_record, :skip_javascript, :database, :javascript) + opts = (options || {}).slice(:skip_active_record, :skip_javascript, :database, :javascript, :quiet, :pretend, :force, :skip) opts[:force] = force invoke Rails::Generators::AppGenerator, diff --git a/railties/test/generators/shared_generator_tests.rb b/railties/test/generators/shared_generator_tests.rb index 592da39ccd..c9c5d2fad2 100644 --- a/railties/test/generators/shared_generator_tests.rb +++ b/railties/test/generators/shared_generator_tests.rb @@ -26,8 +26,7 @@ module SharedGeneratorTests def test_plugin_new_generate_pretend run_generator ["testapp", "--pretend"] - - default_files.each{ |path| assert_no_file path } + default_files.each{ |path| assert_no_file File.join("testapp",path) } end def test_invalid_database_option_raises_an_error From 8ee81d21fb103be31adb8e0dcde8ed8f5e90a798 Mon Sep 17 00:00:00 2001 From: Murray Steele Date: Fri, 11 Mar 2011 11:41:30 +0000 Subject: [PATCH 096/100] Failing test case to show that habtm join table contents are removed when a model is destroyed but the destruction is blocked by a before_destroy. --- .../test/cases/habtm_destroy_order_test.rb | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/activerecord/test/cases/habtm_destroy_order_test.rb b/activerecord/test/cases/habtm_destroy_order_test.rb index 15598392e2..f2b91d977e 100644 --- a/activerecord/test/cases/habtm_destroy_order_test.rb +++ b/activerecord/test/cases/habtm_destroy_order_test.rb @@ -13,5 +13,39 @@ class HabtmDestroyOrderTest < ActiveRecord::TestCase sicp.destroy end end + assert !sicp.destroyed? + end + + test "not destroying a student with lessons leaves student<=>lesson association intact" do + # test a normal before_destroy doesn't destroy the habtm joins + begin + sicp = Lesson.new(:name => "SICP") + ben = Student.new(:name => "Ben Bitdiddle") + # add a before destroy to student + Student.class_eval do + before_destroy do + raise ActiveRecord::Rollback unless lessons.empty? + end + end + ben.lessons << sicp + ben.save! + ben.destroy + assert !ben.reload.lessons.empty? + ensure + # get rid of it so Student is still like it was + Student.reset_callbacks(:destroy) + end + end + + test "not destroying a lesson with students leaves student<=>lesson association intact" do + # test a more aggressive before_destroy doesn't destroy the habtm joins and still throws the exception + sicp = Lesson.new(:name => "SICP") + ben = Student.new(:name => "Ben Bitdiddle") + sicp.students << ben + sicp.save! + assert_raises LessonError do + sicp.destroy + end + assert !sicp.reload.students.empty? end end From 54c963c89b81cfc4fd7dcad6779e41c85d1180ce Mon Sep 17 00:00:00 2001 From: Murray Steele Date: Fri, 11 Mar 2011 12:02:49 +0000 Subject: [PATCH 097/100] Make clearing of HABTM join table contents happen in an after_destory callback. The old method of redefining destroy meant that clearing the HABTM join table would happen as long as the call to destroy succeeded. Which meant if there was a before_destroy that stopped the instance being destroyed using normal means (returning false, raising ActiveRecord::Rollback) rather than exceptional means the join table would be cleared even though the instance wasn't destroyed. Doing it in an after_destroy hook avoids this and has the advantage of happening inside the DB transaction too. --- .../builder/has_and_belongs_to_many.rb | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb index e40b32826a..4b48757da7 100644 --- a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb +++ b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb @@ -7,24 +7,24 @@ module ActiveRecord::Associations::Builder def build reflection = super check_validity(reflection) - redefine_destroy + define_after_destroy_method reflection end private - def redefine_destroy - # Don't use a before_destroy callback since users' before_destroy - # callbacks will be executed after the association is wiped out. + def define_after_destroy_method name = self.name - model.send(:include, Module.new { - class_eval <<-RUBY, __FILE__, __LINE__ + 1 - def destroy # def destroy - super # super - #{name}.clear # posts.clear - end # end - RUBY - }) + model.send(:class_eval, <<-eoruby, __FILE__, __LINE__ + 1) + def #{after_destroy_method_name} + association(#{name.to_sym.inspect}).delete_all + end + eoruby + model.after_destroy after_destroy_method_name + end + + def after_destroy_method_name + "has_and_belongs_to_many_after_destroy_for_#{name}" end # TODO: These checks should probably be moved into the Reflection, and we should not be From c5908a86492271ca55a6f54ccfd62b521cdc47c9 Mon Sep 17 00:00:00 2001 From: Adam Meehan Date: Tue, 1 Mar 2011 22:18:46 +1100 Subject: [PATCH 098/100] Fix before_type_cast for timezone aware attributes by caching converted value on write. Also remove read method reload arg on timezone attributes. --- .../attribute_methods/time_zone_conversion.rb | 9 ++++--- .../test/cases/attribute_methods_test.rb | 26 ++++++++----------- 2 files changed, 16 insertions(+), 19 deletions(-) diff --git a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb index 76218d2a73..6aac96df6f 100644 --- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb +++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb @@ -21,9 +21,9 @@ module ActiveRecord def define_method_attribute(attr_name) if create_time_zone_conversion_attribute?(attr_name, columns_hash[attr_name]) method_body, line = <<-EOV, __LINE__ + 1 - def _#{attr_name}(reload = false) + def _#{attr_name} cached = @attributes_cache['#{attr_name}'] - return cached if cached && !reload + return cached if cached time = _read_attribute('#{attr_name}') @attributes_cache['#{attr_name}'] = time.acts_like?(:time) ? time.in_time_zone : time end @@ -41,12 +41,13 @@ module ActiveRecord if create_time_zone_conversion_attribute?(attr_name, columns_hash[attr_name]) method_body, line = <<-EOV, __LINE__ + 1 def #{attr_name}=(original_time) - time = original_time.dup unless original_time.nil? + time = original_time unless time.acts_like?(:time) time = time.is_a?(String) ? Time.zone.parse(time) : time.to_time rescue time end time = time.in_time_zone rescue nil if time - write_attribute(:#{attr_name}, (time || original_time)) + write_attribute(:#{attr_name}, original_time) + @attributes_cache["#{attr_name}"] = time end EOV generated_attribute_methods.module_eval(method_body, __FILE__, line) diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb index dfacf58da8..d8638ee776 100644 --- a/activerecord/test/cases/attribute_methods_test.rb +++ b/activerecord/test/cases/attribute_methods_test.rb @@ -118,22 +118,18 @@ class AttributeMethodsTest < ActiveRecord::TestCase end def test_read_attributes_before_type_cast_on_datetime - developer = Developer.find(:first) - if current_adapter?(:Mysql2Adapter, :OracleAdapter) - # Mysql2 and Oracle adapters keep the value in Time instance - assert_equal developer.created_at.to_s(:db), developer.attributes_before_type_cast["created_at"].to_s(:db) - else - assert_equal developer.created_at.to_s(:db), developer.attributes_before_type_cast["created_at"].to_s + in_time_zone "Pacific Time (US & Canada)" do + record = @target.new + + record.written_on = "345643456" + assert_equal "345643456", record.written_on_before_type_cast + assert_equal nil, record.written_on + + record.written_on = "2009-10-11 12:13:14" + assert_equal "2009-10-11 12:13:14", record.written_on_before_type_cast + assert_equal Time.zone.parse("2009-10-11 12:13:14"), record.written_on + assert_equal ActiveSupport::TimeZone["Pacific Time (US & Canada)"], record.written_on.time_zone end - - developer.created_at = "345643456" - - assert_equal developer.created_at_before_type_cast, "345643456" - assert_equal developer.created_at, nil - - developer.created_at = "2010-03-21 21:23:32" - assert_equal developer.created_at_before_type_cast, "2010-03-21 21:23:32" - assert_equal developer.created_at, Time.parse("2010-03-21 21:23:32") end def test_hash_content From 5da9a74bd35284cf5124793c1f7558e506b52592 Mon Sep 17 00:00:00 2001 From: Manfred Stienstra Date: Fri, 25 Feb 2011 12:34:46 +0100 Subject: [PATCH 099/100] Add a failing test case for an implicit action with a before filter. Signed-off-by: Andrew White --- actionpack/test/controller/filters_test.rb | 22 +++++++++++++++++++ .../implicit_actions/edit.html.erb | 1 + .../implicit_actions/show.html.erb | 1 + 3 files changed, 24 insertions(+) create mode 100644 actionpack/test/fixtures/filter_test/implicit_actions/edit.html.erb create mode 100644 actionpack/test/fixtures/filter_test/implicit_actions/show.html.erb diff --git a/actionpack/test/controller/filters_test.rb b/actionpack/test/controller/filters_test.rb index 330fa276d0..c95332220b 100644 --- a/actionpack/test/controller/filters_test.rb +++ b/actionpack/test/controller/filters_test.rb @@ -505,6 +505,16 @@ class FilterTest < ActionController::TestCase end end + class ImplicitActionsController < ActionController::Base + before_filter :find_user, :only => :edit + + private + + def find_user + @user = 'Jenny' + end + end + def test_sweeper_should_not_block_rendering response = test_process(SweeperTestController) assert_equal 'hello world', response.body @@ -783,6 +793,18 @@ class FilterTest < ActionController::TestCase assert_equal("I rescued this: #", response.body) end + def test_filter_runs_for_implicitly_defined_action_when_needed + test_process(ImplicitActionsController, 'edit') + assert_equal 'Jenny', assigns(:user) + assert_equal 'edit', response.body + end + + def test_filter_does_not_run_for_implicity_defined_action_when_not_needed + test_process(ImplicitActionsController, 'show') + assert_nil assigns(:user) + assert_equal 'show', response.body + end + private def test_process(controller, action = "show") @controller = controller.is_a?(Class) ? controller.new : controller diff --git a/actionpack/test/fixtures/filter_test/implicit_actions/edit.html.erb b/actionpack/test/fixtures/filter_test/implicit_actions/edit.html.erb new file mode 100644 index 0000000000..8491ab9f80 --- /dev/null +++ b/actionpack/test/fixtures/filter_test/implicit_actions/edit.html.erb @@ -0,0 +1 @@ +edit \ No newline at end of file diff --git a/actionpack/test/fixtures/filter_test/implicit_actions/show.html.erb b/actionpack/test/fixtures/filter_test/implicit_actions/show.html.erb new file mode 100644 index 0000000000..0a89cecf05 --- /dev/null +++ b/actionpack/test/fixtures/filter_test/implicit_actions/show.html.erb @@ -0,0 +1 @@ +show \ No newline at end of file From 9772de8d459960cc114c5b214343b7ce08fea21c Mon Sep 17 00:00:00 2001 From: Andrew White Date: Wed, 23 Mar 2011 23:09:00 +0000 Subject: [PATCH 100/100] Fix filter :only and :except with implicit actions The method_name argument is "default_render" for implicit actions so use the action_name attribute to determine which callbacks to run. [#5673 state:resolved] --- .../lib/abstract_controller/callbacks.rb | 2 +- actionpack/test/controller/filters_test.rb | 27 +++++++++++-------- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/actionpack/lib/abstract_controller/callbacks.rb b/actionpack/lib/abstract_controller/callbacks.rb index 95992c2698..1943ca4436 100644 --- a/actionpack/lib/abstract_controller/callbacks.rb +++ b/actionpack/lib/abstract_controller/callbacks.rb @@ -14,7 +14,7 @@ module AbstractController # Override AbstractController::Base's process_action to run the # process_action callbacks around the normal behavior. def process_action(method_name, *args) - run_callbacks(:process_action, method_name) do + run_callbacks(:process_action, action_name) do super end end diff --git a/actionpack/test/controller/filters_test.rb b/actionpack/test/controller/filters_test.rb index c95332220b..9e44e8e088 100644 --- a/actionpack/test/controller/filters_test.rb +++ b/actionpack/test/controller/filters_test.rb @@ -506,12 +506,17 @@ class FilterTest < ActionController::TestCase end class ImplicitActionsController < ActionController::Base - before_filter :find_user, :only => :edit + before_filter :find_only, :only => :edit + before_filter :find_except, :except => :edit private - def find_user - @user = 'Jenny' + def find_only + @only = 'Only' + end + + def find_except + @except = 'Except' end end @@ -793,16 +798,16 @@ class FilterTest < ActionController::TestCase assert_equal("I rescued this: #", response.body) end - def test_filter_runs_for_implicitly_defined_action_when_needed - test_process(ImplicitActionsController, 'edit') - assert_equal 'Jenny', assigns(:user) - assert_equal 'edit', response.body - end - - def test_filter_does_not_run_for_implicity_defined_action_when_not_needed + def test_filters_obey_only_and_except_for_implicit_actions test_process(ImplicitActionsController, 'show') - assert_nil assigns(:user) + assert_equal 'Except', assigns(:except) + assert_nil assigns(:only) assert_equal 'show', response.body + + test_process(ImplicitActionsController, 'edit') + assert_equal 'Only', assigns(:only) + assert_nil assigns(:except) + assert_equal 'edit', response.body end private