aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/test/cases/associations
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord/test/cases/associations')
-rw-r--r--activerecord/test/cases/associations/belongs_to_associations_test.rb1196
-rw-r--r--activerecord/test/cases/associations/bidirectional_destroy_dependencies_test.rb43
-rw-r--r--activerecord/test/cases/associations/callbacks_test.rb191
-rw-r--r--activerecord/test/cases/associations/cascaded_eager_loading_test.rb184
-rw-r--r--activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb44
-rw-r--r--activerecord/test/cases/associations/eager_load_nested_include_test.rb126
-rw-r--r--activerecord/test/cases/associations/eager_singularization_test.rb148
-rw-r--r--activerecord/test/cases/associations/eager_test.rb1518
-rw-r--r--activerecord/test/cases/associations/extension_test.rb94
-rw-r--r--activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb1020
-rw-r--r--activerecord/test/cases/associations/has_many_associations_test.rb2643
-rw-r--r--activerecord/test/cases/associations/has_many_through_associations_test.rb1408
-rw-r--r--activerecord/test/cases/associations/has_one_associations_test.rb728
-rw-r--r--activerecord/test/cases/associations/has_one_through_associations_test.rb404
-rw-r--r--activerecord/test/cases/associations/inner_join_association_test.rb159
-rw-r--r--activerecord/test/cases/associations/inverse_associations_test.rb740
-rw-r--r--activerecord/test/cases/associations/join_model_test.rb778
-rw-r--r--activerecord/test/cases/associations/left_outer_join_association_test.rb90
-rw-r--r--activerecord/test/cases/associations/nested_through_associations_test.rb622
-rw-r--r--activerecord/test/cases/associations/required_test.rb132
20 files changed, 12268 insertions, 0 deletions
diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb
new file mode 100644
index 0000000000..9e6d94191b
--- /dev/null
+++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb
@@ -0,0 +1,1196 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/developer"
+require "models/project"
+require "models/company"
+require "models/topic"
+require "models/reply"
+require "models/computer"
+require "models/post"
+require "models/author"
+require "models/tag"
+require "models/tagging"
+require "models/comment"
+require "models/sponsor"
+require "models/member"
+require "models/essay"
+require "models/toy"
+require "models/invoice"
+require "models/line_item"
+require "models/column"
+require "models/record"
+require "models/admin"
+require "models/admin/user"
+require "models/ship"
+require "models/treasure"
+require "models/parrot"
+
+class BelongsToAssociationsTest < ActiveRecord::TestCase
+ fixtures :accounts, :companies, :developers, :projects, :topics,
+ :developers_projects, :computers, :authors, :author_addresses,
+ :posts, :tags, :taggings, :comments, :sponsors, :members
+
+ def test_belongs_to
+ firm = Client.find(3).firm
+ assert_not_nil firm
+ assert_equal companies(:first_firm).name, firm.name
+ end
+
+ def test_missing_attribute_error_is_raised_when_no_foreign_key_attribute
+ assert_raises(ActiveModel::MissingAttributeError) { Client.select(:id).first.firm }
+ end
+
+ def test_belongs_to_does_not_use_order_by
+ ActiveRecord::SQLCounter.clear_log
+ Client.find(3).firm
+ ensure
+ assert ActiveRecord::SQLCounter.log_all.all? { |sql| /order by/i !~ sql }, "ORDER BY was used in the query"
+ end
+
+ def test_belongs_to_with_primary_key
+ client = Client.create(name: "Primary key client", firm_name: companies(:first_firm).name)
+ assert_equal companies(:first_firm).name, client.firm_with_primary_key.name
+ end
+
+ def test_belongs_to_with_primary_key_joins_on_correct_column
+ sql = Client.joins(:firm_with_primary_key).to_sql
+ if current_adapter?(:Mysql2Adapter)
+ assert_no_match(/`firm_with_primary_keys_companies`\.`id`/, sql)
+ assert_match(/`firm_with_primary_keys_companies`\.`name`/, sql)
+ elsif current_adapter?(:OracleAdapter)
+ # on Oracle aliases are truncated to 30 characters and are quoted in uppercase
+ assert_no_match(/"firm_with_primary_keys_compani"\."id"/i, sql)
+ assert_match(/"firm_with_primary_keys_compani"\."name"/i, sql)
+ else
+ assert_no_match(/"firm_with_primary_keys_companies"\."id"/, sql)
+ assert_match(/"firm_with_primary_keys_companies"\."name"/, sql)
+ end
+ end
+
+ def test_optional_relation
+ original_value = ActiveRecord::Base.belongs_to_required_by_default
+ ActiveRecord::Base.belongs_to_required_by_default = true
+
+ model = Class.new(ActiveRecord::Base) do
+ self.table_name = "accounts"
+ def self.name; "Temp"; end
+ belongs_to :company, optional: true
+ end
+
+ account = model.new
+ assert_predicate account, :valid?
+ ensure
+ ActiveRecord::Base.belongs_to_required_by_default = original_value
+ end
+
+ def test_not_optional_relation
+ original_value = ActiveRecord::Base.belongs_to_required_by_default
+ ActiveRecord::Base.belongs_to_required_by_default = true
+
+ model = Class.new(ActiveRecord::Base) do
+ self.table_name = "accounts"
+ def self.name; "Temp"; end
+ belongs_to :company, optional: false
+ end
+
+ account = model.new
+ assert_not_predicate account, :valid?
+ assert_equal [{ error: :blank }], account.errors.details[:company]
+ ensure
+ ActiveRecord::Base.belongs_to_required_by_default = original_value
+ end
+
+ def test_required_belongs_to_config
+ original_value = ActiveRecord::Base.belongs_to_required_by_default
+ ActiveRecord::Base.belongs_to_required_by_default = true
+
+ model = Class.new(ActiveRecord::Base) do
+ self.table_name = "accounts"
+ def self.name; "Temp"; end
+ belongs_to :company
+ end
+
+ account = model.new
+ assert_not_predicate account, :valid?
+ assert_equal [{ error: :blank }], account.errors.details[:company]
+ ensure
+ ActiveRecord::Base.belongs_to_required_by_default = original_value
+ end
+
+ def test_default
+ david = developers(:david)
+ jamis = developers(:jamis)
+
+ model = Class.new(ActiveRecord::Base) do
+ self.table_name = "ships"
+ def self.name; "Temp"; end
+ belongs_to :developer, default: -> { david }
+ end
+
+ ship = model.create!
+ assert_equal david, ship.developer
+
+ ship = model.create!(developer: jamis)
+ assert_equal jamis, ship.developer
+
+ ship.update!(developer: nil)
+ assert_equal david, ship.developer
+ end
+
+ def test_default_with_lambda
+ model = Class.new(ActiveRecord::Base) do
+ self.table_name = "ships"
+ def self.name; "Temp"; end
+ belongs_to :developer, default: -> { default_developer }
+
+ def default_developer
+ Developer.first
+ end
+ end
+
+ ship = model.create!
+ assert_equal developers(:david), ship.developer
+
+ ship = model.create!(developer: developers(:jamis))
+ assert_equal developers(:jamis), ship.developer
+ end
+
+ def test_default_scope_on_relations_is_not_cached
+ counter = 0
+
+ comments = Class.new(ActiveRecord::Base) {
+ self.table_name = "comments"
+ self.inheritance_column = "not_there"
+
+ posts = Class.new(ActiveRecord::Base) {
+ self.table_name = "posts"
+ self.inheritance_column = "not_there"
+
+ default_scope -> {
+ counter += 1
+ where("id = :inc", inc: counter)
+ }
+
+ has_many :comments, anonymous_class: comments
+ }
+ belongs_to :post, anonymous_class: posts, inverse_of: false
+ }
+
+ assert_equal 0, counter
+ comment = comments.first
+ assert_equal 0, counter
+ sql = capture_sql { comment.post }
+ comment.reload
+ assert_not_equal sql, capture_sql { comment.post }
+ end
+
+ def test_proxy_assignment
+ account = Account.find(1)
+ assert_nothing_raised { account.firm = account.firm }
+ end
+
+ def test_type_mismatch
+ assert_raise(ActiveRecord::AssociationTypeMismatch) { Account.find(1).firm = 1 }
+ assert_raise(ActiveRecord::AssociationTypeMismatch) { Account.find(1).firm = Project.find(1) }
+ end
+
+ def test_raises_type_mismatch_with_namespaced_class
+ assert_nil defined?(Region), "This test requires that there is no top-level Region class"
+
+ ActiveRecord::Base.connection.instance_eval do
+ create_table(:admin_regions) { |t| t.string :name }
+ add_column :admin_users, :region_id, :integer
+ end
+ Admin.const_set "RegionalUser", Class.new(Admin::User) { belongs_to(:region) }
+ Admin.const_set "Region", Class.new(ActiveRecord::Base)
+
+ e = assert_raise(ActiveRecord::AssociationTypeMismatch) {
+ Admin::RegionalUser.new(region: "wrong value")
+ }
+ assert_match(/^Region\([^)]+\) expected, got "wrong value" which is an instance of String\([^)]+\)$/, e.message)
+ ensure
+ Admin.send :remove_const, "Region" if Admin.const_defined?("Region")
+ Admin.send :remove_const, "RegionalUser" if Admin.const_defined?("RegionalUser")
+
+ ActiveRecord::Base.connection.instance_eval do
+ remove_column :admin_users, :region_id if column_exists?(:admin_users, :region_id)
+ drop_table :admin_regions, if_exists: true
+ end
+
+ Admin::User.reset_column_information
+ end
+
+ def test_natural_assignment
+ apple = Firm.create("name" => "Apple")
+ citibank = Account.create("credit_limit" => 10)
+ citibank.firm = apple
+ assert_equal apple.id, citibank.firm_id
+ end
+
+ def test_id_assignment
+ apple = Firm.create("name" => "Apple")
+ citibank = Account.create("credit_limit" => 10)
+ citibank.firm_id = apple
+ assert_nil citibank.firm_id
+ end
+
+ def test_natural_assignment_with_primary_key
+ apple = Firm.create("name" => "Apple")
+ citibank = Client.create("name" => "Primary key client")
+ citibank.firm_with_primary_key = apple
+ assert_equal apple.name, citibank.firm_name
+ end
+
+ def test_eager_loading_with_primary_key
+ Firm.create("name" => "Apple")
+ Client.create("name" => "Citibank", :firm_name => "Apple")
+ citibank_result = Client.all.merge!(where: { name: "Citibank" }, includes: :firm_with_primary_key).first
+ assert_predicate citibank_result.association(:firm_with_primary_key), :loaded?
+ end
+
+ def test_eager_loading_with_primary_key_as_symbol
+ Firm.create("name" => "Apple")
+ Client.create("name" => "Citibank", :firm_name => "Apple")
+ citibank_result = Client.all.merge!(where: { name: "Citibank" }, includes: :firm_with_primary_key_symbols).first
+ assert_predicate citibank_result.association(:firm_with_primary_key_symbols), :loaded?
+ end
+
+ def test_creating_the_belonging_object
+ citibank = Account.create("credit_limit" => 10)
+ apple = citibank.create_firm("name" => "Apple")
+ assert_equal apple, citibank.firm
+ citibank.save
+ citibank.reload
+ assert_equal apple, citibank.firm
+ end
+
+ def test_creating_the_belonging_object_with_primary_key
+ client = Client.create(name: "Primary key client")
+ apple = client.create_firm_with_primary_key("name" => "Apple")
+ assert_equal apple, client.firm_with_primary_key
+ client.save
+ client.reload
+ assert_equal apple, client.firm_with_primary_key
+ end
+
+ def test_building_the_belonging_object
+ citibank = Account.create("credit_limit" => 10)
+ apple = citibank.build_firm("name" => "Apple")
+ citibank.save
+ assert_equal apple.id, citibank.firm_id
+ end
+
+ def test_building_the_belonging_object_with_implicit_sti_base_class
+ account = Account.new
+ company = account.build_firm
+ assert_kind_of Company, company, "Expected #{company.class} to be a Company"
+ end
+
+ def test_building_the_belonging_object_with_explicit_sti_base_class
+ account = Account.new
+ company = account.build_firm(type: "Company")
+ assert_kind_of Company, company, "Expected #{company.class} to be a Company"
+ end
+
+ def test_building_the_belonging_object_with_sti_subclass
+ account = Account.new
+ company = account.build_firm(type: "Firm")
+ assert_kind_of Firm, company, "Expected #{company.class} to be a Firm"
+ end
+
+ def test_building_the_belonging_object_with_an_invalid_type
+ account = Account.new
+ assert_raise(ActiveRecord::SubclassNotFound) { account.build_firm(type: "InvalidType") }
+ end
+
+ def test_building_the_belonging_object_with_an_unrelated_type
+ account = Account.new
+ assert_raise(ActiveRecord::SubclassNotFound) { account.build_firm(type: "Account") }
+ end
+
+ def test_building_the_belonging_object_with_primary_key
+ client = Client.create(name: "Primary key client")
+ apple = client.build_firm_with_primary_key("name" => "Apple")
+ client.save
+ assert_equal apple.name, client.firm_name
+ end
+
+ def test_create!
+ client = Client.create!(name: "Jimmy")
+ account = client.create_account!(credit_limit: 10)
+ assert_equal account, client.account
+ assert_predicate account, :persisted?
+ client.save
+ client.reload
+ assert_equal account, client.account
+ end
+
+ def test_failing_create!
+ client = Client.create!(name: "Jimmy")
+ assert_raise(ActiveRecord::RecordInvalid) { client.create_account! }
+ assert_not_nil client.account
+ assert_predicate client.account, :new_record?
+ end
+
+ def test_reloading_the_belonging_object
+ odegy_account = accounts(:odegy_account)
+
+ assert_equal "Odegy", odegy_account.firm.name
+ Company.where(id: odegy_account.firm_id).update_all(name: "ODEGY")
+ assert_equal "Odegy", odegy_account.firm.name
+
+ assert_equal "ODEGY", odegy_account.reload_firm.name
+ end
+
+ def test_natural_assignment_to_nil
+ client = Client.find(3)
+ client.firm = nil
+ client.save
+ client.association(:firm).reload
+ assert_nil client.firm
+ assert_nil client.client_of
+ end
+
+ def test_natural_assignment_to_nil_with_primary_key
+ client = Client.create(name: "Primary key client", firm_name: companies(:first_firm).name)
+ client.firm_with_primary_key = nil
+ client.save
+ client.association(:firm_with_primary_key).reload
+ assert_nil client.firm_with_primary_key
+ assert_nil client.client_of
+ end
+
+ def test_with_different_class_name
+ assert_equal Company.find(1).name, Company.find(3).firm_with_other_name.name
+ assert_not_nil Company.find(3).firm_with_other_name, "Microsoft should have a firm"
+ end
+
+ def test_with_condition
+ assert_equal Company.find(1).name, Company.find(3).firm_with_condition.name
+ assert_not_nil Company.find(3).firm_with_condition, "Microsoft should have a firm"
+ end
+
+ def test_polymorphic_association_class
+ sponsor = Sponsor.new
+ assert_nil sponsor.association(:sponsorable).send(:klass)
+ sponsor.association(:sponsorable).reload
+ assert_nil sponsor.sponsorable
+
+ sponsor.sponsorable_type = "" # the column doesn't have to be declared NOT NULL
+ assert_nil sponsor.association(:sponsorable).send(:klass)
+ sponsor.association(:sponsorable).reload
+ assert_nil sponsor.sponsorable
+
+ sponsor.sponsorable = Member.new name: "Bert"
+ assert_equal Member, sponsor.association(:sponsorable).send(:klass)
+ end
+
+ def test_with_polymorphic_and_condition
+ sponsor = Sponsor.create
+ member = Member.create name: "Bert"
+ sponsor.sponsorable = member
+
+ assert_equal member, sponsor.sponsorable
+ assert_nil sponsor.sponsorable_with_conditions
+ end
+
+ def test_with_select
+ assert_equal 1, Company.find(2).firm_with_select.attributes.size
+ assert_equal 1, Company.all.merge!(includes: :firm_with_select).find(2).firm_with_select.attributes.size
+ end
+
+ def test_belongs_to_without_counter_cache_option
+ # Ship has a conventionally named `treasures_count` column, but the counter_cache
+ # option is not given on the association.
+ ship = Ship.create(name: "Countless")
+
+ assert_no_difference lambda { ship.reload.treasures_count }, "treasures_count should not be changed unless counter_cache is given on the relation" do
+ treasure = Treasure.new(name: "Gold", ship: ship)
+ treasure.save
+ end
+
+ assert_no_difference lambda { ship.reload.treasures_count }, "treasures_count should not be changed unless counter_cache is given on the relation" do
+ treasure = ship.treasures.first
+ treasure.destroy
+ end
+ end
+
+ def test_belongs_to_counter
+ debate = Topic.create("title" => "debate")
+ assert_equal 0, debate.read_attribute("replies_count"), "No replies yet"
+
+ trash = debate.replies.create("title" => "blah!", "content" => "world around!")
+ assert_equal 1, Topic.find(debate.id).read_attribute("replies_count"), "First reply created"
+
+ trash.destroy
+ assert_equal 0, Topic.find(debate.id).read_attribute("replies_count"), "First reply deleted"
+ end
+
+ def test_belongs_to_counter_with_assigning_nil
+ post = Post.find(1)
+ comment = Comment.find(1)
+
+ assert_equal post.id, comment.post_id
+ assert_equal 2, Post.find(post.id).comments.size
+
+ comment.post = nil
+
+ assert_equal 1, Post.find(post.id).comments.size
+ end
+
+ def test_belongs_to_with_primary_key_counter
+ debate = Topic.create("title" => "debate")
+ debate2 = Topic.create("title" => "debate2")
+ reply = Reply.create("title" => "blah!", "content" => "world around!", "parent_title" => "debate")
+
+ assert_equal 1, debate.reload.replies_count
+ assert_equal 0, debate2.reload.replies_count
+
+ reply.topic_with_primary_key = debate2
+
+ assert_equal 0, debate.reload.replies_count
+ assert_equal 1, debate2.reload.replies_count
+
+ reply.topic_with_primary_key = nil
+
+ assert_equal 0, debate.reload.replies_count
+ assert_equal 0, debate2.reload.replies_count
+ end
+
+ def test_belongs_to_counter_with_reassigning
+ topic1 = Topic.create("title" => "t1")
+ topic2 = Topic.create("title" => "t2")
+ reply1 = Reply.new("title" => "r1", "content" => "r1")
+ reply1.topic = topic1
+
+ assert reply1.save
+ assert_equal 1, Topic.find(topic1.id).replies.size
+ assert_equal 0, Topic.find(topic2.id).replies.size
+
+ reply1.topic = Topic.find(topic2.id)
+
+ assert_no_queries do
+ reply1.topic = topic2
+ end
+
+ assert reply1.save
+ assert_equal 0, Topic.find(topic1.id).replies.size
+ assert_equal 1, Topic.find(topic2.id).replies.size
+
+ reply1.topic = nil
+
+ assert_equal 0, Topic.find(topic1.id).replies.size
+ assert_equal 0, Topic.find(topic2.id).replies.size
+
+ reply1.topic = topic1
+
+ assert_equal 1, Topic.find(topic1.id).replies.size
+ assert_equal 0, Topic.find(topic2.id).replies.size
+
+ reply1.destroy
+
+ assert_equal 0, Topic.find(topic1.id).replies.size
+ assert_equal 0, Topic.find(topic2.id).replies.size
+ end
+
+ def test_belongs_to_reassign_with_namespaced_models_and_counters
+ topic1 = Web::Topic.create("title" => "t1")
+ topic2 = Web::Topic.create("title" => "t2")
+ reply1 = Web::Reply.new("title" => "r1", "content" => "r1")
+ reply1.topic = topic1
+
+ assert reply1.save
+ assert_equal 1, Web::Topic.find(topic1.id).replies.size
+ assert_equal 0, Web::Topic.find(topic2.id).replies.size
+
+ reply1.topic = Web::Topic.find(topic2.id)
+
+ assert reply1.save
+ assert_equal 0, Web::Topic.find(topic1.id).replies.size
+ assert_equal 1, Web::Topic.find(topic2.id).replies.size
+ end
+
+ def test_belongs_to_counter_after_save
+ topic = Topic.create!(title: "monday night")
+ topic.replies.create!(title: "re: monday night", content: "football")
+ assert_equal 1, Topic.find(topic.id)[:replies_count]
+
+ topic.save!
+ assert_equal 1, Topic.find(topic.id)[:replies_count]
+ end
+
+ def test_belongs_to_with_touch_option_on_touch
+ line_item = LineItem.create!
+ Invoice.create!(line_items: [line_item])
+
+ assert_queries(1) { line_item.touch }
+ end
+
+ def test_belongs_to_with_touch_on_multiple_records
+ line_item = LineItem.create!(amount: 1)
+ line_item2 = LineItem.create!(amount: 2)
+ Invoice.create!(line_items: [line_item, line_item2])
+
+ assert_queries(1) do
+ LineItem.transaction do
+ line_item.touch
+ line_item2.touch
+ end
+ end
+
+ assert_queries(2) do
+ line_item.touch
+ line_item2.touch
+ end
+ end
+
+ def test_belongs_to_with_touch_option_on_touch_without_updated_at_attributes
+ assert_not LineItem.column_names.include?("updated_at")
+
+ line_item = LineItem.create!
+ invoice = Invoice.create!(line_items: [line_item])
+ initial = invoice.updated_at
+ travel(1.second) do
+ line_item.touch
+ end
+
+ assert_not_equal initial, invoice.reload.updated_at
+ end
+
+ def test_belongs_to_with_touch_option_on_touch_and_removed_parent
+ line_item = LineItem.create!
+ Invoice.create!(line_items: [line_item])
+
+ line_item.invoice = nil
+
+ assert_queries(2) { line_item.touch }
+ end
+
+ def test_belongs_to_with_touch_option_on_update
+ line_item = LineItem.create!
+ Invoice.create!(line_items: [line_item])
+
+ assert_queries(2) { line_item.update amount: 10 }
+ end
+
+ def test_belongs_to_with_touch_option_on_empty_update
+ line_item = LineItem.create!
+ Invoice.create!(line_items: [line_item])
+
+ assert_queries(0) { line_item.save }
+ end
+
+ def test_belongs_to_with_touch_option_on_destroy
+ line_item = LineItem.create!
+ Invoice.create!(line_items: [line_item])
+
+ assert_queries(2) { line_item.destroy }
+ end
+
+ def test_belongs_to_with_touch_option_on_destroy_with_destroyed_parent
+ line_item = LineItem.create!
+ invoice = Invoice.create!(line_items: [line_item])
+ invoice.destroy
+
+ assert_queries(1) { line_item.destroy }
+ end
+
+ def test_belongs_to_with_touch_option_on_touch_and_reassigned_parent
+ line_item = LineItem.create!
+ Invoice.create!(line_items: [line_item])
+
+ line_item.invoice = Invoice.create!
+
+ assert_queries(3) { line_item.touch }
+ end
+
+ def test_belongs_to_counter_after_update
+ topic = Topic.create!(title: "37s")
+ topic.replies.create!(title: "re: 37s", content: "rails")
+ assert_equal 1, Topic.find(topic.id)[:replies_count]
+
+ topic.update(title: "37signals")
+ assert_equal 1, Topic.find(topic.id)[:replies_count]
+ end
+
+ def test_belongs_to_counter_when_update_columns
+ topic = Topic.create!(title: "37s")
+ topic.replies.create!(title: "re: 37s", content: "rails")
+ assert_equal 1, Topic.find(topic.id)[:replies_count]
+
+ topic.update_columns(content: "rails is wonderful")
+ assert_equal 1, Topic.find(topic.id)[:replies_count]
+ end
+
+ def test_assignment_before_child_saved
+ final_cut = Client.new("name" => "Final Cut")
+ firm = Firm.find(1)
+ final_cut.firm = firm
+ assert_not_predicate final_cut, :persisted?
+ assert final_cut.save
+ assert_predicate final_cut, :persisted?
+ assert_predicate firm, :persisted?
+ assert_equal firm, final_cut.firm
+ final_cut.association(:firm).reload
+ assert_equal firm, final_cut.firm
+ end
+
+ def test_assignment_before_child_saved_with_primary_key
+ final_cut = Client.new("name" => "Final Cut")
+ firm = Firm.find(1)
+ final_cut.firm_with_primary_key = firm
+ assert_not_predicate final_cut, :persisted?
+ assert final_cut.save
+ assert_predicate final_cut, :persisted?
+ assert_predicate firm, :persisted?
+ assert_equal firm, final_cut.firm_with_primary_key
+ final_cut.association(:firm_with_primary_key).reload
+ assert_equal firm, final_cut.firm_with_primary_key
+ end
+
+ def test_new_record_with_foreign_key_but_no_object
+ client = Client.new("firm_id" => 1)
+ assert_equal Firm.first, client.firm_with_basic_id
+ end
+
+ def test_setting_foreign_key_after_nil_target_loaded
+ client = Client.new
+ client.firm_with_basic_id
+ client.firm_id = 1
+
+ assert_equal companies(:first_firm), client.firm_with_basic_id
+ end
+
+ def test_polymorphic_setting_foreign_key_after_nil_target_loaded
+ sponsor = Sponsor.new
+ sponsor.sponsorable
+ sponsor.sponsorable_id = 1
+ sponsor.sponsorable_type = "Member"
+
+ assert_equal members(:groucho), sponsor.sponsorable
+ end
+
+ def test_dont_find_target_when_foreign_key_is_null
+ tagging = taggings(:thinking_general)
+ assert_queries(0) { tagging.super_tag }
+ end
+
+ def test_dont_find_target_when_saving_foreign_key_after_stale_association_loaded
+ client = Client.create!(name: "Test client", firm_with_basic_id: Firm.find(1))
+ client.firm_id = Firm.create!(name: "Test firm").id
+ assert_queries(1) { client.save! }
+ end
+
+ def test_field_name_same_as_foreign_key
+ computer = Computer.find(1)
+ assert_not_nil computer.developer, ":foreign key == attribute didn't lock up" # '
+ end
+
+ def test_counter_cache
+ topic = Topic.create title: "Zoom-zoom-zoom"
+ assert_equal 0, topic[:replies_count]
+
+ reply = Reply.create(title: "re: zoom", content: "speedy quick!")
+ reply.topic = topic
+
+ assert_equal 1, topic.reload[:replies_count]
+ assert_equal 1, topic.replies.size
+
+ topic[:replies_count] = 15
+ assert_equal 15, topic.replies.size
+ end
+
+ def test_counter_cache_double_destroy
+ topic = Topic.create title: "Zoom-zoom-zoom"
+
+ 5.times do
+ topic.replies.create(title: "re: zoom", content: "speedy quick!")
+ end
+
+ assert_equal 5, topic.reload[:replies_count]
+ assert_equal 5, topic.replies.size
+
+ reply = topic.replies.first
+
+ reply.destroy
+ assert_equal 4, topic.reload[:replies_count]
+
+ reply.destroy
+ assert_equal 4, topic.reload[:replies_count]
+ assert_equal 4, topic.replies.size
+ end
+
+ def test_concurrent_counter_cache_double_destroy
+ topic = Topic.create title: "Zoom-zoom-zoom"
+
+ 5.times do
+ topic.replies.create(title: "re: zoom", content: "speedy quick!")
+ end
+
+ assert_equal 5, topic.reload[:replies_count]
+ assert_equal 5, topic.replies.size
+
+ reply = topic.replies.first
+ reply_clone = Reply.find(reply.id)
+
+ reply.destroy
+ assert_equal 4, topic.reload[:replies_count]
+
+ reply_clone.destroy
+ assert_equal 4, topic.reload[:replies_count]
+ assert_equal 4, topic.replies.size
+ end
+
+ def test_custom_counter_cache
+ reply = Reply.create(title: "re: zoom", content: "speedy quick!")
+ assert_equal 0, reply[:replies_count]
+
+ silly = SillyReply.create(title: "gaga", content: "boo-boo")
+ silly.reply = reply
+
+ assert_equal 1, reply.reload[:replies_count]
+ assert_equal 1, reply.replies.size
+
+ reply[:replies_count] = 17
+ assert_equal 17, reply.replies.size
+ end
+
+ def test_replace_counter_cache
+ topic = Topic.create(title: "Zoom-zoom-zoom")
+ reply = Reply.create(title: "re: zoom", content: "speedy quick!")
+
+ reply.topic = topic
+ reply.save
+ topic.reload
+
+ assert_equal 1, topic.replies_count
+ end
+
+ def test_association_assignment_sticks
+ post = Post.first
+
+ author1, author2 = Author.all.merge!(limit: 2).to_a
+ assert_not_nil author1
+ assert_not_nil author2
+
+ # make sure the association is loaded
+ post.author
+
+ # set the association by id, directly
+ post.author_id = author2.id
+
+ # save and reload
+ post.save!
+ post.reload
+
+ # the author id of the post should be the id we set
+ assert_equal post.author_id, author2.id
+ end
+
+ def test_cant_save_readonly_association
+ assert_raise(ActiveRecord::ReadOnlyRecord) { companies(:first_client).readonly_firm.save! }
+ assert_predicate companies(:first_client).readonly_firm, :readonly?
+ end
+
+ def test_polymorphic_assignment_foreign_key_type_string
+ comment = Comment.first
+ comment.author = Author.first
+ comment.resource = Member.first
+ comment.save
+
+ assert_equal Comment.all.to_a,
+ Comment.includes(:author).to_a
+
+ assert_equal Comment.all.to_a,
+ Comment.includes(:resource).to_a
+ end
+
+ def test_polymorphic_assignment_foreign_type_field_updating
+ # should update when assigning a saved record
+ sponsor = Sponsor.new
+ member = Member.create
+ sponsor.sponsorable = member
+ assert_equal "Member", sponsor.sponsorable_type
+
+ # should update when assigning a new record
+ sponsor = Sponsor.new
+ member = Member.new
+ sponsor.sponsorable = member
+ assert_equal "Member", sponsor.sponsorable_type
+ end
+
+ def test_polymorphic_assignment_with_primary_key_foreign_type_field_updating
+ # should update when assigning a saved record
+ essay = Essay.new
+ writer = Author.create(name: "David")
+ essay.writer = writer
+ assert_equal "Author", essay.writer_type
+
+ # should update when assigning a new record
+ essay = Essay.new
+ writer = Author.new
+ essay.writer = writer
+ assert_equal "Author", essay.writer_type
+ end
+
+ def test_polymorphic_assignment_updates_foreign_id_field_for_new_and_saved_records
+ sponsor = Sponsor.new
+ saved_member = Member.create
+ new_member = Member.new
+
+ sponsor.sponsorable = saved_member
+ assert_equal saved_member.id, sponsor.sponsorable_id
+
+ sponsor.sponsorable = new_member
+ assert_nil sponsor.sponsorable_id
+ end
+
+ def test_assignment_updates_foreign_id_field_for_new_and_saved_records
+ client = Client.new
+ saved_firm = Firm.create name: "Saved"
+ new_firm = Firm.new
+
+ client.firm = saved_firm
+ assert_equal saved_firm.id, client.client_of
+
+ client.firm = new_firm
+ assert_nil client.client_of
+ end
+
+ def test_polymorphic_assignment_with_primary_key_updates_foreign_id_field_for_new_and_saved_records
+ essay = Essay.new
+ saved_writer = Author.create(name: "David")
+ new_writer = Author.new
+
+ essay.writer = saved_writer
+ assert_equal saved_writer.name, essay.writer_id
+
+ essay.writer = new_writer
+ assert_nil essay.writer_id
+ end
+
+ def test_polymorphic_assignment_with_nil
+ essay = Essay.new
+ assert_nil essay.writer_id
+ assert_nil essay.writer_type
+
+ essay.writer_id = 1
+ essay.writer_type = "Author"
+
+ essay.writer = nil
+ assert_nil essay.writer_id
+ assert_nil essay.writer_type
+ end
+
+ def test_belongs_to_proxy_should_not_respond_to_private_methods
+ assert_raise(NoMethodError) { companies(:first_firm).private_method }
+ assert_raise(NoMethodError) { companies(:second_client).firm.private_method }
+ end
+
+ def test_belongs_to_proxy_should_respond_to_private_methods_via_send
+ companies(:first_firm).send(:private_method)
+ companies(:second_client).firm.send(:private_method)
+ end
+
+ def test_save_of_record_with_loaded_belongs_to
+ @account = companies(:first_firm).account
+
+ assert_nothing_raised do
+ Account.find(@account.id).save!
+ Account.all.merge!(includes: :firm).find(@account.id).save!
+ end
+
+ @account.firm.delete
+
+ assert_nothing_raised do
+ Account.find(@account.id).save!
+ Account.all.merge!(includes: :firm).find(@account.id).save!
+ end
+ end
+
+ def test_dependent_delete_and_destroy_with_belongs_to
+ AuthorAddress.destroyed_author_address_ids.clear
+
+ author_address = author_addresses(:david_address)
+ author_address_extra = author_addresses(:david_address_extra)
+ assert_equal [], AuthorAddress.destroyed_author_address_ids
+
+ assert_difference "AuthorAddress.count", -2 do
+ authors(:david).destroy
+ end
+
+ assert_equal [], AuthorAddress.where(id: [author_address.id, author_address_extra.id])
+ assert_equal [author_address.id], AuthorAddress.destroyed_author_address_ids
+ end
+
+ def test_belongs_to_invalid_dependent_option_raises_exception
+ error = assert_raise ArgumentError do
+ Class.new(Author).belongs_to :special_author_address, dependent: :nullify
+ end
+ assert_equal error.message, "The :dependent option must be one of [:destroy, :delete], but is :nullify"
+ end
+
+ def test_attributes_are_being_set_when_initialized_from_belongs_to_association_with_where_clause
+ new_firm = accounts(:signals37).build_firm(name: "Apple")
+ assert_equal new_firm.name, "Apple"
+ end
+
+ def test_attributes_are_set_without_error_when_initialized_from_belongs_to_association_with_array_in_where_clause
+ new_account = Account.where(credit_limit: [ 50, 60 ]).new
+ assert_nil new_account.credit_limit
+ end
+
+ def test_reassigning_the_parent_id_updates_the_object
+ client = companies(:second_client)
+
+ client.firm
+ client.firm_with_condition
+ firm_proxy = client.send(:association_instance_get, :firm)
+ firm_with_condition_proxy = client.send(:association_instance_get, :firm_with_condition)
+
+ assert_not_predicate firm_proxy, :stale_target?
+ assert_not_predicate firm_with_condition_proxy, :stale_target?
+ assert_equal companies(:first_firm), client.firm
+ assert_equal companies(:first_firm), client.firm_with_condition
+
+ client.client_of = companies(:another_firm).id
+
+ assert_predicate firm_proxy, :stale_target?
+ assert_predicate firm_with_condition_proxy, :stale_target?
+ assert_equal companies(:another_firm), client.firm
+ assert_equal companies(:another_firm), client.firm_with_condition
+ end
+
+ def test_polymorphic_reassignment_of_associated_id_updates_the_object
+ sponsor = sponsors(:moustache_club_sponsor_for_groucho)
+
+ sponsor.sponsorable
+ proxy = sponsor.send(:association_instance_get, :sponsorable)
+
+ assert_not_predicate proxy, :stale_target?
+ assert_equal members(:groucho), sponsor.sponsorable
+
+ sponsor.sponsorable_id = members(:some_other_guy).id
+
+ assert_predicate proxy, :stale_target?
+ assert_equal members(:some_other_guy), sponsor.sponsorable
+ end
+
+ def test_polymorphic_reassignment_of_associated_type_updates_the_object
+ sponsor = sponsors(:moustache_club_sponsor_for_groucho)
+
+ sponsor.sponsorable
+ proxy = sponsor.send(:association_instance_get, :sponsorable)
+
+ assert_not_predicate proxy, :stale_target?
+ assert_equal members(:groucho), sponsor.sponsorable
+
+ sponsor.sponsorable_type = "Firm"
+
+ assert_predicate proxy, :stale_target?
+ assert_equal companies(:first_firm), sponsor.sponsorable
+ end
+
+ def test_reloading_association_with_key_change
+ client = companies(:second_client)
+ firm = client.association(:firm)
+
+ client.firm = companies(:another_firm)
+ firm.reload
+ assert_equal companies(:another_firm), firm.target
+
+ client.client_of = companies(:first_firm).id
+ firm.reload
+ assert_equal companies(:first_firm), firm.target
+ end
+
+ def test_polymorphic_counter_cache
+ tagging = taggings(:welcome_general)
+ post = posts(:welcome)
+ comment = comments(:greetings)
+
+ assert_difference lambda { post.reload.tags_count }, -1 do
+ assert_difference "comment.reload.tags_count", +1 do
+ tagging.taggable = comment
+ end
+ end
+ end
+
+ def test_polymorphic_with_custom_foreign_type
+ sponsor = sponsors(:moustache_club_sponsor_for_groucho)
+ groucho = members(:groucho)
+ other = members(:some_other_guy)
+
+ assert_equal groucho, sponsor.sponsorable
+ assert_equal groucho, sponsor.thing
+
+ sponsor.thing = other
+
+ assert_equal other, sponsor.sponsorable
+ assert_equal other, sponsor.thing
+
+ sponsor.sponsorable = groucho
+
+ assert_equal groucho, sponsor.sponsorable
+ assert_equal groucho, sponsor.thing
+ end
+
+ def test_build_with_conditions
+ client = companies(:second_client)
+ firm = client.build_bob_firm
+
+ assert_equal "Bob", firm.name
+ end
+
+ def test_create_with_conditions
+ client = companies(:second_client)
+ firm = client.create_bob_firm
+
+ assert_equal "Bob", firm.name
+ end
+
+ def test_create_bang_with_conditions
+ client = companies(:second_client)
+ firm = client.create_bob_firm!
+
+ assert_equal "Bob", firm.name
+ end
+
+ def test_build_with_block
+ client = Client.create(name: "Client Company")
+
+ firm = client.build_firm { |f| f.name = "Agency Company" }
+ assert_equal "Agency Company", firm.name
+ end
+
+ def test_create_with_block
+ client = Client.create(name: "Client Company")
+
+ firm = client.create_firm { |f| f.name = "Agency Company" }
+ assert_equal "Agency Company", firm.name
+ end
+
+ def test_create_bang_with_block
+ client = Client.create(name: "Client Company")
+
+ firm = client.create_firm! { |f| f.name = "Agency Company" }
+ assert_equal "Agency Company", firm.name
+ end
+
+ def test_should_set_foreign_key_on_create_association
+ client = Client.create! name: "fuu"
+
+ firm = client.create_firm name: "baa"
+ assert_equal firm.id, client.client_of
+ end
+
+ def test_should_set_foreign_key_on_create_association!
+ client = Client.create! name: "fuu"
+
+ firm = client.create_firm! name: "baa"
+ assert_equal firm.id, client.client_of
+ end
+
+ def test_self_referential_belongs_to_with_counter_cache_assigning_nil
+ comment = Comment.create! post: posts(:thinking), body: "fuu"
+ comment.parent = nil
+ comment.save!
+
+ assert_nil comment.reload.parent
+ assert_equal 0, comments(:greetings).reload.children_count
+ end
+
+ def test_belongs_to_with_id_assigning
+ post = posts(:welcome)
+ comment = Comment.create! body: "foo", post: post
+ parent = comments(:greetings)
+ assert_equal 0, parent.reload.children_count
+ comment.parent_id = parent.id
+
+ comment.save!
+ assert_equal 1, parent.reload.children_count
+ end
+
+ def test_belongs_to_with_out_of_range_value_assigning
+ model = Class.new(Comment) do
+ def self.name; "Temp"; end
+ validates :post, presence: true
+ end
+
+ comment = model.new
+ comment.post_id = 9223372036854775808 # out of range in the bigint
+
+ assert_nil comment.post
+ assert_not_predicate comment, :valid?
+ assert_equal [{ error: :blank }], comment.errors.details[:post]
+ end
+
+ def test_polymorphic_with_custom_primary_key
+ toy = Toy.create!
+ sponsor = Sponsor.create!(sponsorable: toy)
+
+ assert_equal toy, sponsor.reload.sponsorable
+ end
+
+ test "stale tracking doesn't care about the type" do
+ apple = Firm.create("name" => "Apple")
+ citibank = Account.create("credit_limit" => 10)
+
+ citibank.firm_id = apple.id
+ citibank.firm # load it
+
+ citibank.firm_id = apple.id.to_s
+
+ assert_not_predicate citibank.association(:firm), :stale_target?
+ end
+
+ def test_reflect_the_most_recent_change
+ author1, author2 = Author.limit(2)
+ post = Post.new(title: "foo", body: "bar")
+
+ post.author = author1
+ post.author_id = author2.id
+
+ assert post.save
+ assert_equal post.author_id, author2.id
+ end
+
+ test "dangerous association name raises ArgumentError" do
+ [:errors, "errors", :save, "save"].each do |name|
+ assert_raises(ArgumentError, "Association #{name} should not be allowed") do
+ Class.new(ActiveRecord::Base) do
+ belongs_to name
+ end
+ end
+ end
+ end
+
+ test "belongs_to works with model called Record" do
+ record = Record.create!
+ Column.create! record: record
+ assert_equal 1, Column.count
+ end
+
+ def test_multiple_counter_cache_with_after_create_update
+ post = posts(:welcome)
+ parent = comments(:greetings)
+
+ assert_difference "parent.reload.children_count", +1 do
+ assert_difference "post.reload.comments_count", +1 do
+ CommentWithAfterCreateUpdate.create(body: "foo", post: post, parent: parent)
+ end
+ end
+ end
+end
+
+class BelongsToWithForeignKeyTest < ActiveRecord::TestCase
+ fixtures :authors, :author_addresses
+
+ def test_destroy_linked_models
+ address = AuthorAddress.create!
+ author = Author.create! name: "Author", author_address_id: address.id
+
+ author.destroy!
+ end
+end
diff --git a/activerecord/test/cases/associations/bidirectional_destroy_dependencies_test.rb b/activerecord/test/cases/associations/bidirectional_destroy_dependencies_test.rb
new file mode 100644
index 0000000000..88221b012e
--- /dev/null
+++ b/activerecord/test/cases/associations/bidirectional_destroy_dependencies_test.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/content"
+
+class BidirectionalDestroyDependenciesTest < ActiveRecord::TestCase
+ fixtures :content, :content_positions
+
+ def setup
+ Content.destroyed_ids.clear
+ ContentPosition.destroyed_ids.clear
+ end
+
+ def test_bidirectional_dependence_when_destroying_item_with_belongs_to_association
+ content_position = ContentPosition.find(1)
+ content = content_position.content
+ assert_not_nil content
+
+ content_position.destroy
+
+ assert_equal [content_position.id], ContentPosition.destroyed_ids
+ assert_equal [content.id], Content.destroyed_ids
+ end
+
+ def test_bidirectional_dependence_when_destroying_item_with_has_one_association
+ content = Content.find(1)
+ content_position = content.content_position
+ assert_not_nil content_position
+
+ content.destroy
+
+ assert_equal [content.id], Content.destroyed_ids
+ assert_equal [content_position.id], ContentPosition.destroyed_ids
+ end
+
+ def test_bidirectional_dependence_when_destroying_item_with_has_one_association_fails_first_time
+ content = ContentWhichRequiresTwoDestroyCalls.find(1)
+
+ 2.times { content.destroy }
+
+ assert_equal content.destroyed?, true
+ end
+end
diff --git a/activerecord/test/cases/associations/callbacks_test.rb b/activerecord/test/cases/associations/callbacks_test.rb
new file mode 100644
index 0000000000..25d55dc4c9
--- /dev/null
+++ b/activerecord/test/cases/associations/callbacks_test.rb
@@ -0,0 +1,191 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/post"
+require "models/author"
+require "models/project"
+require "models/developer"
+require "models/computer"
+require "models/company"
+
+class AssociationCallbacksTest < ActiveRecord::TestCase
+ fixtures :posts, :authors, :author_addresses, :projects, :developers
+
+ def setup
+ @david = authors(:david)
+ @thinking = posts(:thinking)
+ @authorless = posts(:authorless)
+ assert_empty @david.post_log
+ end
+
+ def test_adding_macro_callbacks
+ @david.posts_with_callbacks << @thinking
+ assert_equal ["before_adding#{@thinking.id}", "after_adding#{@thinking.id}"], @david.post_log
+ @david.posts_with_callbacks << @thinking
+ assert_equal ["before_adding#{@thinking.id}", "after_adding#{@thinking.id}", "before_adding#{@thinking.id}",
+ "after_adding#{@thinking.id}"], @david.post_log
+ end
+
+ def test_adding_with_proc_callbacks
+ @david.posts_with_proc_callbacks << @thinking
+ assert_equal ["before_adding#{@thinking.id}", "after_adding#{@thinking.id}"], @david.post_log
+ @david.posts_with_proc_callbacks << @thinking
+ assert_equal ["before_adding#{@thinking.id}", "after_adding#{@thinking.id}", "before_adding#{@thinking.id}",
+ "after_adding#{@thinking.id}"], @david.post_log
+ end
+
+ def test_removing_with_macro_callbacks
+ first_post, second_post = @david.posts_with_callbacks[0, 2]
+ @david.posts_with_callbacks.delete(first_post)
+ assert_equal ["before_removing#{first_post.id}", "after_removing#{first_post.id}"], @david.post_log
+ @david.posts_with_callbacks.delete(second_post)
+ assert_equal ["before_removing#{first_post.id}", "after_removing#{first_post.id}", "before_removing#{second_post.id}",
+ "after_removing#{second_post.id}"], @david.post_log
+ end
+
+ def test_removing_with_proc_callbacks
+ first_post, second_post = @david.posts_with_callbacks[0, 2]
+ @david.posts_with_proc_callbacks.delete(first_post)
+ assert_equal ["before_removing#{first_post.id}", "after_removing#{first_post.id}"], @david.post_log
+ @david.posts_with_proc_callbacks.delete(second_post)
+ assert_equal ["before_removing#{first_post.id}", "after_removing#{first_post.id}", "before_removing#{second_post.id}",
+ "after_removing#{second_post.id}"], @david.post_log
+ end
+
+ def test_multiple_callbacks
+ @david.posts_with_multiple_callbacks << @thinking
+ assert_equal ["before_adding#{@thinking.id}", "before_adding_proc#{@thinking.id}", "after_adding#{@thinking.id}",
+ "after_adding_proc#{@thinking.id}"], @david.post_log
+ @david.posts_with_multiple_callbacks << @thinking
+ assert_equal ["before_adding#{@thinking.id}", "before_adding_proc#{@thinking.id}", "after_adding#{@thinking.id}",
+ "after_adding_proc#{@thinking.id}", "before_adding#{@thinking.id}", "before_adding_proc#{@thinking.id}",
+ "after_adding#{@thinking.id}", "after_adding_proc#{@thinking.id}"], @david.post_log
+ end
+
+ def test_has_many_callbacks_with_create
+ morten = Author.create name: "Morten"
+ post = morten.posts_with_proc_callbacks.create! title: "Hello", body: "How are you doing?"
+ assert_equal ["before_adding<new>", "after_adding#{post.id}"], morten.post_log
+ end
+
+ def test_has_many_callbacks_with_create!
+ morten = Author.create! name: "Morten"
+ post = morten.posts_with_proc_callbacks.create title: "Hello", body: "How are you doing?"
+ assert_equal ["before_adding<new>", "after_adding#{post.id}"], morten.post_log
+ end
+
+ def test_has_many_callbacks_for_save_on_parent
+ jack = Author.new name: "Jack"
+ jack.posts_with_callbacks.build title: "Call me back!", body: "Before you wake up and after you sleep"
+
+ callback_log = ["before_adding<new>", "after_adding#{jack.posts_with_callbacks.first.id}"]
+ assert_equal callback_log, jack.post_log
+ assert jack.save
+ assert_equal 1, jack.posts_with_callbacks.count
+ assert_equal callback_log, jack.post_log
+ end
+
+ def test_has_many_callbacks_for_destroy_on_parent
+ firm = Firm.create! name: "Firm"
+ client = firm.clients.create! name: "Client"
+ firm.destroy
+
+ assert_equal ["before_remove#{client.id}", "after_remove#{client.id}"], firm.log
+ end
+
+ def test_has_and_belongs_to_many_add_callback
+ david = developers(:david)
+ ar = projects(:active_record)
+ assert_empty ar.developers_log
+ ar.developers_with_callbacks << david
+ assert_equal ["before_adding#{david.id}", "after_adding#{david.id}"], ar.developers_log
+ ar.developers_with_callbacks << david
+ assert_equal ["before_adding#{david.id}", "after_adding#{david.id}", "before_adding#{david.id}",
+ "after_adding#{david.id}"], ar.developers_log
+ end
+
+ def test_has_and_belongs_to_many_before_add_called_before_save
+ dev = nil
+ new_dev = nil
+ klass = Class.new(Project) do
+ def self.name; Project.name; end
+ has_and_belongs_to_many :developers_with_callbacks,
+ class_name: "Developer",
+ before_add: lambda { |o, r|
+ dev = r
+ new_dev = r.new_record?
+ }
+ end
+ rec = klass.create!
+ alice = Developer.new(name: "alice")
+ rec.developers_with_callbacks << alice
+ assert_equal alice, dev
+ assert_not_nil new_dev
+ assert new_dev, "record should not have been saved"
+ assert_not_predicate alice, :new_record?
+ end
+
+ def test_has_and_belongs_to_many_after_add_called_after_save
+ ar = projects(:active_record)
+ assert_empty ar.developers_log
+ alice = Developer.new(name: "alice")
+ ar.developers_with_callbacks << alice
+ assert_equal "after_adding#{alice.id}", ar.developers_log.last
+
+ bob = ar.developers_with_callbacks.create(name: "bob")
+ assert_equal "after_adding#{bob.id}", ar.developers_log.last
+
+ ar.developers_with_callbacks.build(name: "charlie")
+ assert_equal "after_adding<new>", ar.developers_log.last
+ end
+
+ def test_has_and_belongs_to_many_remove_callback
+ david = developers(:david)
+ jamis = developers(:jamis)
+ activerecord = projects(:active_record)
+ assert_empty activerecord.developers_log
+ activerecord.developers_with_callbacks.delete(david)
+ assert_equal ["before_removing#{david.id}", "after_removing#{david.id}"], activerecord.developers_log
+
+ activerecord.developers_with_callbacks.delete(jamis)
+ assert_equal ["before_removing#{david.id}", "after_removing#{david.id}", "before_removing#{jamis.id}",
+ "after_removing#{jamis.id}"], activerecord.developers_log
+ end
+
+ def test_has_and_belongs_to_many_does_not_fire_callbacks_on_clear
+ activerecord = projects(:active_record)
+ assert_empty activerecord.developers_log
+ if activerecord.developers_with_callbacks.size == 0
+ activerecord.developers << developers(:david)
+ activerecord.developers << developers(:jamis)
+ activerecord.reload
+ assert activerecord.developers_with_callbacks.size == 2
+ end
+ activerecord.developers_with_callbacks.flat_map { |d| ["before_removing#{d.id}", "after_removing#{d.id}"] }.sort
+ assert activerecord.developers_with_callbacks.clear
+ assert_empty activerecord.developers_log
+ end
+
+ def test_has_many_and_belongs_to_many_callbacks_for_save_on_parent
+ project = Project.new name: "Callbacks"
+ project.developers_with_callbacks.build name: "Jack", salary: 95000
+
+ callback_log = ["before_adding<new>", "after_adding<new>"]
+ assert_equal callback_log, project.developers_log
+ assert project.save
+ assert_equal 1, project.developers_with_callbacks.size
+ assert_equal callback_log, project.developers_log
+ end
+
+ def test_dont_add_if_before_callback_raises_exception
+ assert_not_includes @david.unchangeable_posts, @authorless
+ begin
+ @david.unchangeable_posts << @authorless
+ rescue Exception
+ end
+ assert_empty @david.post_log
+ assert_not_includes @david.unchangeable_posts, @authorless
+ @david.reload
+ assert_not_includes @david.unchangeable_posts, @authorless
+ end
+end
diff --git a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb
new file mode 100644
index 0000000000..e717621928
--- /dev/null
+++ b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb
@@ -0,0 +1,184 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/post"
+require "models/comment"
+require "models/author"
+require "models/categorization"
+require "models/category"
+require "models/company"
+require "models/topic"
+require "models/reply"
+require "models/person"
+require "models/vertex"
+require "models/edge"
+
+class CascadedEagerLoadingTest < ActiveRecord::TestCase
+ fixtures :authors, :author_addresses, :mixins, :companies, :posts, :topics, :accounts, :comments,
+ :categorizations, :people, :categories, :edges, :vertices
+
+ def test_eager_association_loading_with_cascaded_two_levels
+ authors = Author.all.merge!(includes: { posts: :comments }, order: "authors.id").to_a
+ assert_equal 3, authors.size
+ assert_equal 5, authors[0].posts.size
+ assert_equal 3, authors[1].posts.size
+ assert_equal 10, authors[0].posts.collect { |post| post.comments.size }.inject(0) { |sum, i| sum + i }
+ end
+
+ def test_eager_association_loading_with_cascaded_two_levels_and_one_level
+ authors = Author.all.merge!(includes: [{ posts: :comments }, :categorizations], order: "authors.id").to_a
+ assert_equal 3, authors.size
+ assert_equal 5, authors[0].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
+ end
+
+ def test_eager_association_loading_with_hmt_does_not_table_name_collide_when_joining_associations
+ authors = Author.joins(:posts).eager_load(:comments).where(posts: { tags_count: 1 }).to_a
+ assert_equal 3, assert_no_queries { authors.size }
+ assert_equal 10, assert_no_queries { authors[0].comments.size }
+ end
+
+ def test_eager_association_loading_grafts_stashed_associations_to_correct_parent
+ assert_equal people(:michael), Person.eager_load(primary_contact: :primary_contact).where("primary_contacts_people_2.first_name = ?", "Susan").order("people.id").first
+ end
+
+ def test_cascaded_eager_association_loading_with_join_for_count
+ categories = Category.joins(:categorizations).includes([{ posts: :comments }, :authors])
+
+ assert_equal 4, categories.count
+ assert_equal 4, categories.to_a.count
+ assert_equal 3, categories.distinct.count
+ assert_equal 3, categories.to_a.uniq.size # Must uniq since instantiating with inner joins will get dupes
+ end
+
+ def test_cascaded_eager_association_loading_with_duplicated_includes
+ categories = Category.includes(:categorizations).includes(categorizations: :author).where("categorizations.id is not null").references(:categorizations)
+ assert_nothing_raised do
+ assert_equal 3, categories.count
+ assert_equal 3, categories.to_a.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").references(:posts)
+ assert_nothing_raised do
+ assert_equal 3, categories.count
+ assert_equal 3, categories.to_a.size
+ end
+ end
+
+ def test_eager_association_loading_with_join_for_count
+ authors = Author.joins(:special_posts).includes([:posts, :categorizations])
+
+ assert_nothing_raised { authors.count }
+ assert_queries(3) { authors.to_a }
+ end
+
+ def test_eager_association_loading_with_cascaded_two_levels_with_two_has_many_associations
+ authors = Author.all.merge!(includes: { posts: [:comments, :categorizations] }, order: "authors.id").to_a
+ assert_equal 3, authors.size
+ assert_equal 5, authors[0].posts.size
+ assert_equal 3, authors[1].posts.size
+ assert_equal 10, authors[0].posts.collect { |post| post.comments.size }.inject(0) { |sum, i| sum + i }
+ end
+
+ def test_eager_association_loading_with_cascaded_two_levels_and_self_table_reference
+ authors = Author.all.merge!(includes: { posts: [:comments, :author] }, order: "authors.id").to_a
+ 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
+ end
+
+ def test_eager_association_loading_with_cascaded_two_levels_with_condition
+ authors = Author.all.merge!(includes: { posts: :comments }, where: "authors.id=1", order: "authors.id").to_a
+ assert_equal 1, authors.size
+ assert_equal 5, authors[0].posts.size
+ end
+
+ def test_eager_association_loading_with_cascaded_three_levels_by_ping_pong
+ firms = Firm.all.merge!(includes: { account: { firm: :account } }, order: "companies.id").to_a
+ assert_equal 2, firms.size
+ assert_equal firms.first.account, firms.first.account.firm.account
+ assert_equal companies(:first_firm).account, assert_no_queries { firms.first.account.firm.account }
+ assert_equal companies(:first_firm).account.firm.account, assert_no_queries { firms.first.account.firm.account }
+ end
+
+ def test_eager_association_loading_with_has_many_sti
+ topics = Topic.all.merge!(includes: :replies, order: "topics.id").to_a
+ first, second, = topics(:first).replies.size, topics(:second).replies.size
+ assert_no_queries do
+ assert_equal first, topics[0].replies.size
+ assert_equal second, topics[1].replies.size
+ end
+ end
+
+ def test_eager_association_loading_with_has_many_sti_and_subclasses
+ silly = SillyReply.new(title: "gaga", content: "boo-boo", parent_id: 1)
+ silly.parent_id = 1
+ assert silly.save
+
+ topics = Topic.all.merge!(includes: :replies, order: ["topics.id", "replies_topics.id"]).to_a
+ assert_no_queries do
+ assert_equal 2, topics[0].replies.size
+ assert_equal 0, topics[1].replies.size
+ end
+ end
+
+ def test_eager_association_loading_with_belongs_to_sti
+ replies = Reply.all.merge!(includes: :topic, order: "topics.id").to_a
+ assert_includes replies, topics(:second)
+ assert_not_includes replies, topics(:first)
+ assert_equal topics(:first), assert_no_queries { replies.first.topic }
+ end
+
+ def test_eager_association_loading_with_multiple_stis_and_order
+ author = Author.all.merge!(includes: { posts: [ :special_comments, :very_special_comment ] }, order: ["authors.name", "comments.body", "very_special_comments_posts.body"], where: "posts.id = 4").first
+ assert_equal authors(:david), author
+ assert_no_queries do
+ author.posts.first.special_comments
+ author.posts.first.very_special_comment
+ end
+ end
+
+ def test_eager_association_loading_of_stis_with_multiple_references
+ authors = Author.all.merge!(includes: { posts: { special_comments: { post: [ :special_comments, :very_special_comment ] } } }, order: "comments.body, very_special_comments_posts.body", where: "posts.id = 4").to_a
+ assert_equal [authors(:david)], authors
+ assert_no_queries do
+ authors.first.posts.first.special_comments.first.post.special_comments
+ authors.first.posts.first.special_comments.first.post.very_special_comment
+ end
+ end
+
+ def test_eager_association_loading_where_first_level_returns_nil
+ authors = Author.all.merge!(includes: { post_about_thinking: :comments }, order: "authors.id DESC").to_a
+ assert_equal [authors(:bob), authors(:mary), authors(:david)], authors
+ assert_no_queries do
+ authors[2].post_about_thinking.comments.first
+ end
+ end
+
+ def test_eager_association_loading_with_recursive_cascading_four_levels_has_many_through
+ source = Vertex.all.merge!(includes: { sinks: { sinks: { sinks: :sinks } } }, order: "vertices.id").first
+ assert_equal vertices(:vertex_4), assert_no_queries { source.sinks.first.sinks.first.sinks.first }
+ end
+
+ def test_eager_association_loading_with_recursive_cascading_four_levels_has_and_belongs_to_many
+ sink = Vertex.all.merge!(includes: { sources: { sources: { sources: :sources } } }, order: "vertices.id DESC").first
+ assert_equal vertices(:vertex_1), assert_no_queries { sink.sources.first.sources.first.sources.first.sources.first }
+ end
+
+ def test_eager_association_loading_with_cascaded_interdependent_one_level_and_two_levels
+ authors_relation = Author.all.merge!(includes: [:comments, { posts: :categorizations }], order: "authors.id")
+ authors = authors_relation.to_a
+ assert_equal 3, authors.size
+ assert_equal 10, authors[0].comments.size
+ assert_equal 1, authors[1].comments.size
+ assert_equal 5, authors[0].posts.size
+ assert_equal 3, authors[1].posts.size
+ assert_equal 3, authors[0].posts.collect { |post| post.categorizations.size }.inject(0) { |sum, i| sum + i }
+ end
+end
diff --git a/activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb b/activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb
new file mode 100644
index 0000000000..8754889143
--- /dev/null
+++ b/activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/post"
+require "models/tagging"
+
+module Namespaced
+ class Post < ActiveRecord::Base
+ self.table_name = "posts"
+ has_one :tagging, as: :taggable, class_name: "Tagging"
+ end
+end
+
+class EagerLoadIncludeFullStiClassNamesTest < ActiveRecord::TestCase
+ def setup
+ post = Namespaced::Post.create(title: "Great stuff", body: "This is not", author_id: 1)
+ @tagging = Tagging.create(taggable: post)
+ @old = ActiveRecord::Base.store_full_sti_class
+ end
+
+ def teardown
+ ActiveRecord::Base.store_full_sti_class = @old
+ end
+
+ def test_class_names_with_includes
+ ActiveRecord::Base.store_full_sti_class = false
+ post = Namespaced::Post.includes(:tagging).find_by_title("Great stuff")
+ assert_nil post.tagging
+
+ ActiveRecord::Base.store_full_sti_class = true
+ post = Namespaced::Post.includes(:tagging).find_by_title("Great stuff")
+ assert_equal @tagging, post.tagging
+ end
+
+ def test_class_names_with_eager_load
+ ActiveRecord::Base.store_full_sti_class = false
+ post = Namespaced::Post.eager_load(:tagging).find_by_title("Great stuff")
+ assert_nil post.tagging
+
+ ActiveRecord::Base.store_full_sti_class = true
+ post = Namespaced::Post.eager_load(:tagging).find_by_title("Great stuff")
+ assert_equal @tagging, post.tagging
+ end
+end
diff --git a/activerecord/test/cases/associations/eager_load_nested_include_test.rb b/activerecord/test/cases/associations/eager_load_nested_include_test.rb
new file mode 100644
index 0000000000..c5b2b77bd4
--- /dev/null
+++ b/activerecord/test/cases/associations/eager_load_nested_include_test.rb
@@ -0,0 +1,126 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/post"
+require "models/tag"
+require "models/author"
+require "models/comment"
+require "models/category"
+require "models/categorization"
+require "models/tagging"
+
+module Remembered
+ extend ActiveSupport::Concern
+
+ included do
+ after_create :remember
+ private
+ def remember; self.class.remembered << self; end
+ end
+
+ module ClassMethods
+ def remembered; @@remembered ||= []; end
+ def sample; @@remembered.sample; end
+ end
+end
+
+class ShapeExpression < ActiveRecord::Base
+ belongs_to :shape, polymorphic: true
+ belongs_to :paint, polymorphic: true
+end
+
+class Circle < ActiveRecord::Base
+ has_many :shape_expressions, as: :shape
+ include Remembered
+end
+class Square < ActiveRecord::Base
+ has_many :shape_expressions, as: :shape
+ include Remembered
+end
+class Triangle < ActiveRecord::Base
+ has_many :shape_expressions, as: :shape
+ include Remembered
+end
+class PaintColor < ActiveRecord::Base
+ has_many :shape_expressions, as: :paint
+ belongs_to :non_poly, foreign_key: "non_poly_one_id", class_name: "NonPolyOne"
+ include Remembered
+end
+class PaintTexture < ActiveRecord::Base
+ has_many :shape_expressions, as: :paint
+ belongs_to :non_poly, foreign_key: "non_poly_two_id", class_name: "NonPolyTwo"
+ include Remembered
+end
+class NonPolyOne < ActiveRecord::Base
+ has_many :paint_colors
+ include Remembered
+end
+class NonPolyTwo < ActiveRecord::Base
+ has_many :paint_textures
+ include Remembered
+end
+
+class EagerLoadPolyAssocsTest < ActiveRecord::TestCase
+ NUM_SIMPLE_OBJS = 50
+ NUM_SHAPE_EXPRESSIONS = 100
+
+ def setup
+ generate_test_object_graphs
+ end
+
+ teardown do
+ [Circle, Square, Triangle, PaintColor, PaintTexture,
+ ShapeExpression, NonPolyOne, NonPolyTwo].each(&:delete_all)
+ end
+
+ def generate_test_object_graphs
+ 1.upto(NUM_SIMPLE_OBJS) do
+ [Circle, Square, Triangle, NonPolyOne, NonPolyTwo].map(&:create!)
+ end
+ 1.upto(NUM_SIMPLE_OBJS) do
+ PaintColor.create!(non_poly_one_id: NonPolyOne.sample.id)
+ PaintTexture.create!(non_poly_two_id: NonPolyTwo.sample.id)
+ end
+ 1.upto(NUM_SHAPE_EXPRESSIONS) do
+ shape_type = [Circle, Square, Triangle].sample
+ paint_type = [PaintColor, PaintTexture].sample
+ ShapeExpression.create!(shape_type: shape_type.to_s, shape_id: shape_type.sample.id,
+ paint_type: paint_type.to_s, paint_id: paint_type.sample.id)
+ end
+ end
+
+ def test_include_query
+ res = ShapeExpression.all.merge!(includes: [ :shape, { paint: :non_poly } ]).to_a
+ assert_equal NUM_SHAPE_EXPRESSIONS, res.size
+ assert_queries(0) do
+ res.each do |se|
+ assert_not_nil se.paint.non_poly, "this is the association that was loading incorrectly before the change"
+ assert_not_nil se.shape, "just making sure other associations still work"
+ end
+ end
+ end
+end
+
+class EagerLoadNestedIncludeWithMissingDataTest < ActiveRecord::TestCase
+ def setup
+ @davey_mcdave = Author.create(name: "Davey McDave")
+ @first_post = @davey_mcdave.posts.create(title: "Davey Speaks", body: "Expressive wordage")
+ @first_comment = @first_post.comments.create(body: "Inflamatory doublespeak")
+ @first_categorization = @davey_mcdave.categorizations.create(category: Category.first, post: @first_post)
+ end
+
+ teardown do
+ @davey_mcdave.destroy
+ @first_post.destroy
+ @first_comment.destroy
+ @first_categorization.destroy
+ end
+
+ def test_missing_data_in_a_nested_include_should_not_cause_errors_when_constructing_objects
+ assert_nothing_raised do
+ # @davey_mcdave doesn't have any author_favorites
+ includes = { posts: :comments, categorizations: :category, author_favorites: :favorite_author }
+ Author.all.merge!(includes: includes, where: { authors: { name: @davey_mcdave.name } }, order: "categories.name").to_a
+ end
+ end
+end
diff --git a/activerecord/test/cases/associations/eager_singularization_test.rb b/activerecord/test/cases/associations/eager_singularization_test.rb
new file mode 100644
index 0000000000..420a5a805b
--- /dev/null
+++ b/activerecord/test/cases/associations/eager_singularization_test.rb
@@ -0,0 +1,148 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+
+class EagerSingularizationTest < ActiveRecord::TestCase
+ class Virus < ActiveRecord::Base
+ belongs_to :octopus
+ end
+
+ class Octopus < ActiveRecord::Base
+ has_one :virus
+ end
+
+ class Pass < ActiveRecord::Base
+ belongs_to :bus
+ end
+
+ class Bus < ActiveRecord::Base
+ has_many :passes
+ end
+
+ class Mess < ActiveRecord::Base
+ has_and_belongs_to_many :crises
+ end
+
+ class Crisis < ActiveRecord::Base
+ has_and_belongs_to_many :messes
+ has_many :analyses, dependent: :destroy
+ has_many :successes, through: :analyses
+ has_many :dresses, dependent: :destroy
+ has_many :compresses, through: :dresses
+ end
+
+ class Analysis < ActiveRecord::Base
+ belongs_to :crisis
+ belongs_to :success
+ end
+
+ class Success < ActiveRecord::Base
+ has_many :analyses, dependent: :destroy
+ has_many :crises, through: :analyses
+ end
+
+ class Dress < ActiveRecord::Base
+ belongs_to :crisis
+ has_many :compresses
+ end
+
+ class Compress < ActiveRecord::Base
+ belongs_to :dress
+ end
+
+ def setup
+ connection.create_table :viri do |t|
+ t.column :octopus_id, :integer
+ t.column :species, :string
+ end
+ connection.create_table :octopi do |t|
+ t.column :species, :string
+ end
+ connection.create_table :passes do |t|
+ t.column :bus_id, :integer
+ t.column :rides, :integer
+ end
+ connection.create_table :buses do |t|
+ t.column :name, :string
+ end
+ connection.create_table :crises_messes, id: false do |t|
+ t.column :crisis_id, :integer
+ t.column :mess_id, :integer
+ end
+ connection.create_table :messes do |t|
+ t.column :name, :string
+ end
+ connection.create_table :crises do |t|
+ t.column :name, :string
+ end
+ connection.create_table :successes do |t|
+ t.column :name, :string
+ end
+ connection.create_table :analyses do |t|
+ t.column :crisis_id, :integer
+ t.column :success_id, :integer
+ end
+ connection.create_table :dresses do |t|
+ t.column :crisis_id, :integer
+ end
+ connection.create_table :compresses do |t|
+ t.column :dress_id, :integer
+ end
+ end
+
+ teardown do
+ connection.drop_table :viri
+ connection.drop_table :octopi
+ connection.drop_table :passes
+ connection.drop_table :buses
+ connection.drop_table :crises_messes
+ connection.drop_table :messes
+ connection.drop_table :crises
+ connection.drop_table :successes
+ connection.drop_table :analyses
+ connection.drop_table :dresses
+ connection.drop_table :compresses
+ end
+
+ def test_eager_no_extra_singularization_belongs_to
+ assert_nothing_raised do
+ Virus.all.merge!(includes: :octopus).to_a
+ end
+ end
+
+ def test_eager_no_extra_singularization_has_one
+ assert_nothing_raised do
+ Octopus.all.merge!(includes: :virus).to_a
+ end
+ end
+
+ def test_eager_no_extra_singularization_has_many
+ assert_nothing_raised do
+ Bus.all.merge!(includes: :passes).to_a
+ end
+ end
+
+ def test_eager_no_extra_singularization_has_and_belongs_to_many
+ assert_nothing_raised do
+ Crisis.all.merge!(includes: :messes).to_a
+ Mess.all.merge!(includes: :crises).to_a
+ end
+ end
+
+ def test_eager_no_extra_singularization_has_many_through_belongs_to
+ assert_nothing_raised do
+ Crisis.all.merge!(includes: :successes).to_a
+ end
+ end
+
+ def test_eager_no_extra_singularization_has_many_through_has_many
+ assert_nothing_raised do
+ Crisis.all.merge!(includes: :compresses).to_a
+ end
+ end
+
+ private
+ def connection
+ ActiveRecord::Base.connection
+ end
+end
diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb
new file mode 100644
index 0000000000..cb07ca5cd3
--- /dev/null
+++ b/activerecord/test/cases/associations/eager_test.rb
@@ -0,0 +1,1518 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/post"
+require "models/tagging"
+require "models/tag"
+require "models/comment"
+require "models/author"
+require "models/essay"
+require "models/category"
+require "models/company"
+require "models/person"
+require "models/reader"
+require "models/owner"
+require "models/pet"
+require "models/reference"
+require "models/job"
+require "models/subscriber"
+require "models/subscription"
+require "models/book"
+require "models/developer"
+require "models/computer"
+require "models/project"
+require "models/member"
+require "models/membership"
+require "models/club"
+require "models/categorization"
+require "models/sponsor"
+require "models/mentor"
+require "models/contract"
+
+class EagerAssociationTest < ActiveRecord::TestCase
+ fixtures :posts, :comments, :authors, :essays, :author_addresses, :categories, :categories_posts,
+ :companies, :accounts, :tags, :taggings, :people, :readers, :categorizations,
+ :owners, :pets, :author_favorites, :jobs, :references, :subscribers, :subscriptions, :books,
+ :developers, :projects, :developers_projects, :members, :memberships, :clubs, :sponsors
+
+ def test_eager_with_has_one_through_join_model_with_conditions_on_the_through
+ member = Member.all.merge!(includes: :favourite_club).find(members(:some_other_guy).id)
+ assert_nil member.favourite_club
+ end
+
+ def test_should_work_inverse_of_with_eager_load
+ author = authors(:david)
+ assert_same author, author.posts.first.author
+ assert_same author, author.posts.eager_load(:comments).first.author
+ end
+
+ def test_loading_with_one_association
+ posts = Post.all.merge!(includes: :comments).to_a
+ post = posts.find { |p| p.id == 1 }
+ assert_equal 2, post.comments.size
+ assert_includes post.comments, comments(:greetings)
+
+ post = Post.all.merge!(includes: :comments, where: "posts.title = 'Welcome to the weblog'").first
+ assert_equal 2, post.comments.size
+ assert_includes post.comments, comments(:greetings)
+
+ posts = Post.all.merge!(includes: :last_comment).to_a
+ post = posts.find { |p| p.id == 1 }
+ assert_equal Post.find(1).last_comment, post.last_comment
+ end
+
+ def test_loading_with_one_association_with_non_preload
+ posts = Post.all.merge!(includes: :last_comment, order: "comments.id DESC").to_a
+ post = posts.find { |p| p.id == 1 }
+ assert_equal Post.find(1).last_comment, post.last_comment
+ end
+
+ def test_loading_conditions_with_or
+ posts = authors(:david).posts.references(:comments).merge(
+ includes: :comments,
+ where: "comments.body like 'Normal%' OR comments.#{QUOTED_TYPE} = 'SpecialComment'"
+ ).to_a
+ assert_nil posts.detect { |p| p.author_id != authors(:david).id },
+ "expected to find only david's posts"
+ end
+
+ def test_loading_with_scope_including_joins
+ assert_equal clubs(:boring_club), Member.preload(:general_club).find(1).general_club
+ assert_equal clubs(:boring_club), Member.eager_load(:general_club).find(1).general_club
+ end
+
+ def test_with_ordering
+ list = Post.all.merge!(includes: :comments, order: "posts.id DESC").to_a
+ [: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
+ end
+
+ def test_has_many_through_with_order
+ authors = Author.includes(:favorite_authors).to_a
+ assert authors.count > 0
+ assert_no_queries { authors.map(&:favorite_authors) }
+ end
+
+ def test_eager_loaded_has_one_association_with_references_does_not_run_additional_queries
+ Post.update_all(author_id: nil)
+ authors = Author.includes(:post).references(:post).to_a
+ assert authors.count > 0
+ assert_no_queries { authors.map(&:post) }
+ end
+
+ def test_calculate_with_string_in_from_and_eager_loading
+ assert_equal 10, Post.from("authors, posts").eager_load(:comments).where("posts.author_id = authors.id").count
+ end
+
+ def test_with_two_tables_in_from_without_getting_double_quoted
+ posts = Post.select("posts.*").from("authors, posts").eager_load(:comments).where("posts.author_id = authors.id").order("posts.id").to_a
+ assert_equal 2, posts.first.comments.size
+ end
+
+ def test_loading_with_multiple_associations
+ posts = Post.all.merge!(includes: [ :comments, :author, :categories ], order: "posts.id").to_a
+ assert_equal 2, posts.first.comments.size
+ assert_equal 2, posts.first.categories.size
+ assert_includes posts.first.comments, comments(:greetings)
+ end
+
+ def test_duplicate_middle_objects
+ comments = Comment.all.merge!(where: "post_id = 1", includes: [post: :author]).to_a
+ assert_no_queries do
+ comments.each { |comment| comment.post.author.name }
+ end
+ end
+
+ def test_preloading_has_many_in_multiple_queries_with_more_ids_than_database_can_handle
+ assert_called(Comment.connection, :in_clause_length, returns: 5) do
+ posts = Post.all.merge!(includes: :comments).to_a
+ assert_equal 11, posts.size
+ end
+ end
+
+ def test_preloading_has_many_in_one_queries_when_database_has_no_limit_on_ids_it_can_handle
+ assert_called(Comment.connection, :in_clause_length, returns: nil) do
+ posts = Post.all.merge!(includes: :comments).to_a
+ assert_equal 11, posts.size
+ end
+ end
+
+ def test_preloading_habtm_in_multiple_queries_with_more_ids_than_database_can_handle
+ assert_called(Comment.connection, :in_clause_length, times: 2, returns: 5) do
+ posts = Post.all.merge!(includes: :categories).to_a
+ assert_equal 11, posts.size
+ end
+ end
+
+ def test_preloading_habtm_in_one_queries_when_database_has_no_limit_on_ids_it_can_handle
+ assert_called(Comment.connection, :in_clause_length, times: 2, returns: nil) do
+ posts = Post.all.merge!(includes: :categories).to_a
+ assert_equal 11, posts.size
+ end
+ end
+
+ def test_load_associated_records_in_one_query_when_adapter_has_no_limit
+ assert_called(Comment.connection, :in_clause_length, returns: nil) do
+ post = posts(:welcome)
+ assert_queries(2) do
+ Post.includes(:comments).where(id: post.id).to_a
+ end
+ end
+ end
+
+ def test_load_associated_records_in_several_queries_when_many_ids_passed
+ assert_called(Comment.connection, :in_clause_length, returns: 1) do
+ post1, post2 = posts(:welcome), posts(:thinking)
+ assert_queries(3) do
+ Post.includes(:comments).where(id: [post1.id, post2.id]).to_a
+ end
+ end
+ end
+
+ def test_load_associated_records_in_one_query_when_a_few_ids_passed
+ assert_called(Comment.connection, :in_clause_length, returns: 3) do
+ post = posts(:welcome)
+ assert_queries(2) do
+ Post.includes(:comments).where(id: post.id).to_a
+ end
+ end
+ end
+
+ def test_including_duplicate_objects_from_belongs_to
+ popular_post = Post.create!(title: "foo", body: "I like cars!")
+ comment = popular_post.comments.create!(body: "lol")
+ popular_post.readers.create!(person: people(:michael))
+ popular_post.readers.create!(person: people(:david))
+
+ readers = Reader.all.merge!(where: ["post_id = ?", popular_post.id],
+ includes: { post: :comments }).to_a
+ readers.each do |reader|
+ assert_equal [comment], reader.post.comments
+ end
+ end
+
+ def test_including_duplicate_objects_from_has_many
+ car_post = Post.create!(title: "foo", body: "I like cars!")
+ car_post.categories << categories(:general)
+ car_post.categories << categories(:technology)
+
+ comment = car_post.comments.create!(body: "hmm")
+ categories = Category.all.merge!(where: { "posts.id" => car_post.id },
+ includes: { posts: :comments }).to_a
+ categories.each do |category|
+ assert_equal [comment], category.posts[0].comments
+ end
+ end
+
+ def test_associations_loaded_for_all_records
+ post = Post.create!(title: "foo", body: "I like cars!")
+ SpecialComment.create!(body: "Come on!", post: post)
+ first_category = Category.create! name: "First!", posts: [post]
+ second_category = Category.create! name: "Second!", posts: [post]
+
+ categories = Category.where(id: [first_category.id, second_category.id]).includes(posts: :special_comments)
+ assert_equal categories.map { |category| category.posts.first.special_comments.loaded? }, [true, true]
+ end
+
+ def test_finding_with_includes_on_has_many_association_with_same_include_includes_only_once
+ author_id = authors(:david).id
+ author = assert_queries(3) { Author.all.merge!(includes: { posts_with_comments: :comments }).find(author_id) } # find the author, then find the posts, then find the comments
+ author.posts_with_comments.each do |post_with_comments|
+ assert_equal post_with_comments.comments.length, post_with_comments.comments.count
+ assert_nil post_with_comments.comments.to_a.uniq!
+ end
+ end
+
+ def test_finding_with_includes_on_has_one_association_with_same_include_includes_only_once
+ author = authors(:david)
+ post = author.post_about_thinking_with_last_comment
+ last_comment = post.last_comment
+ author = assert_queries(3) { Author.all.merge!(includes: { post_about_thinking_with_last_comment: :last_comment }).find(author.id) } # find the author, then find the posts, then find the comments
+ assert_no_queries do
+ assert_equal post, author.post_about_thinking_with_last_comment
+ assert_equal last_comment, author.post_about_thinking_with_last_comment.last_comment
+ end
+ end
+
+ def test_finding_with_includes_on_belongs_to_association_with_same_include_includes_only_once
+ post = posts(:welcome)
+ author = post.author
+ author_address = author.author_address
+ post = assert_queries(3) { Post.all.merge!(includes: { author_with_address: :author_address }).find(post.id) } # find the post, then find the author, then find the address
+ assert_no_queries do
+ assert_equal author, post.author_with_address
+ assert_equal author_address, post.author_with_address.author_address
+ end
+ end
+
+ def test_finding_with_includes_on_null_belongs_to_association_with_same_include_includes_only_once
+ post = posts(:welcome)
+ post.update!(author: nil)
+ post = assert_queries(1) { Post.all.merge!(includes: { author_with_address: :author_address }).find(post.id) }
+ # find the post, then find the author which is null so no query for the author or address
+ assert_no_queries do
+ assert_nil post.author_with_address
+ end
+ end
+
+ def test_finding_with_includes_on_null_belongs_to_polymorphic_association
+ sponsor = sponsors(:moustache_club_sponsor_for_groucho)
+ sponsor.update!(sponsorable: nil)
+ sponsor = assert_queries(1) { Sponsor.all.merge!(includes: :sponsorable).find(sponsor.id) }
+ assert_no_queries do
+ assert_nil sponsor.sponsorable
+ end
+ end
+
+ def test_finding_with_includes_on_empty_polymorphic_type_column
+ sponsor = sponsors(:moustache_club_sponsor_for_groucho)
+ sponsor.update!(sponsorable_type: "", sponsorable_id: nil) # sponsorable_type column might be declared NOT NULL
+ sponsor = assert_queries(1) do
+ assert_nothing_raised { Sponsor.all.merge!(includes: :sponsorable).find(sponsor.id) }
+ end
+ assert_no_queries do
+ assert_nil sponsor.sponsorable
+ end
+ end
+
+ def test_loading_from_an_association
+ posts = authors(:david).posts.merge(includes: :comments, order: "posts.id").to_a
+ assert_equal 2, posts.first.comments.size
+ end
+
+ def test_loading_from_an_association_that_has_a_hash_of_conditions
+ assert_not_empty Author.all.merge!(includes: :hello_posts_with_hash_conditions).find(authors(:david).id).hello_posts
+ end
+
+ def test_loading_with_no_associations
+ assert_nil Post.all.merge!(includes: :author).find(posts(:authorless).id).author
+ end
+
+ # Regression test for 21c75e5
+ def test_nested_loading_does_not_raise_exception_when_association_does_not_exist
+ assert_nothing_raised do
+ Post.all.merge!(includes: { author: :author_addresss }).find(posts(:authorless).id)
+ end
+ end
+
+ def test_three_level_nested_preloading_does_not_raise_exception_when_association_does_not_exist
+ post_id = Comment.where(author_id: nil).where.not(post_id: nil).first.post_id
+
+ assert_nothing_raised do
+ Post.preload(comments: [{ author: :essays }]).find(post_id)
+ end
+ end
+
+ def test_nested_loading_through_has_one_association
+ aa = AuthorAddress.all.merge!(includes: { author: :posts }).find(author_addresses(:david_address).id)
+ assert_equal aa.author.posts.count, aa.author.posts.length
+ end
+
+ def test_nested_loading_through_has_one_association_with_order
+ aa = AuthorAddress.all.merge!(includes: { author: :posts }, order: "author_addresses.id").find(author_addresses(:david_address).id)
+ assert_equal aa.author.posts.count, aa.author.posts.length
+ end
+
+ def test_nested_loading_through_has_one_association_with_order_on_association
+ aa = AuthorAddress.all.merge!(includes: { author: :posts }, order: "authors.id").find(author_addresses(:david_address).id)
+ assert_equal aa.author.posts.count, aa.author.posts.length
+ end
+
+ def test_nested_loading_through_has_one_association_with_order_on_nested_association
+ aa = AuthorAddress.all.merge!(includes: { author: :posts }, order: "posts.id").find(author_addresses(:david_address).id)
+ assert_equal aa.author.posts.count, aa.author.posts.length
+ end
+
+ def test_nested_loading_through_has_one_association_with_conditions
+ aa = AuthorAddress.references(:author_addresses).merge(
+ includes: { author: :posts },
+ where: "author_addresses.id > 0"
+ ).find author_addresses(:david_address).id
+ assert_equal aa.author.posts.count, aa.author.posts.length
+ end
+
+ def test_nested_loading_through_has_one_association_with_conditions_on_association
+ aa = AuthorAddress.references(:authors).merge(
+ includes: { author: :posts },
+ where: "authors.id > 0"
+ ).find author_addresses(:david_address).id
+ assert_equal aa.author.posts.count, aa.author.posts.length
+ end
+
+ def test_nested_loading_through_has_one_association_with_conditions_on_nested_association
+ aa = AuthorAddress.references(:posts).merge(
+ includes: { author: :posts },
+ where: "posts.id > 0"
+ ).find author_addresses(:david_address).id
+ assert_equal aa.author.posts.count, aa.author.posts.length
+ end
+
+ def test_eager_association_loading_with_belongs_to_and_foreign_keys
+ pets = Pet.all.merge!(includes: :owner).to_a
+ assert_equal 4, pets.length
+ end
+
+ def test_eager_association_loading_with_belongs_to
+ comments = Comment.all.merge!(includes: :post).to_a
+ assert_equal 11, comments.length
+ titles = comments.map { |c| c.post.title }
+ assert_includes titles, posts(:welcome).title
+ assert_includes titles, posts(:sti_post_and_comments).title
+ end
+
+ def test_eager_association_loading_with_belongs_to_and_limit
+ comments = Comment.all.merge!(includes: :post, limit: 5, order: "comments.id").to_a
+ assert_equal 5, comments.length
+ assert_equal [1, 2, 3, 5, 6], comments.collect(&:id)
+ end
+
+ def test_eager_association_loading_with_belongs_to_and_limit_and_conditions
+ comments = Comment.all.merge!(includes: :post, where: "post_id = 4", limit: 3, order: "comments.id").to_a
+ assert_equal 3, comments.length
+ assert_equal [5, 6, 7], comments.collect(&:id)
+ end
+
+ def test_eager_association_loading_with_belongs_to_and_limit_and_offset
+ comments = Comment.all.merge!(includes: :post, limit: 3, offset: 2, order: "comments.id").to_a
+ assert_equal 3, comments.length
+ assert_equal [3, 5, 6], comments.collect(&:id)
+ end
+
+ def test_eager_association_loading_with_belongs_to_and_limit_and_offset_and_conditions
+ comments = Comment.all.merge!(includes: :post, where: "post_id = 4", limit: 3, offset: 1, order: "comments.id").to_a
+ assert_equal 3, comments.length
+ assert_equal [6, 7, 8], comments.collect(&:id)
+ end
+
+ def test_eager_association_loading_with_belongs_to_and_limit_and_offset_and_conditions_array
+ comments = Comment.all.merge!(includes: :post, where: ["post_id = ?", 4], limit: 3, offset: 1, order: "comments.id").to_a
+ assert_equal 3, comments.length
+ assert_equal [6, 7, 8], comments.collect(&:id)
+ end
+
+ def test_eager_association_loading_with_belongs_to_and_conditions_string_with_unquoted_table_name
+ assert_nothing_raised do
+ Comment.includes(:post).references(:posts).where("posts.id = ?", 4)
+ end
+ end
+
+ def test_eager_association_loading_with_belongs_to_and_conditions_hash
+ comments = []
+ assert_nothing_raised do
+ comments = Comment.all.merge!(includes: :post, where: { posts: { id: 4 } }, limit: 3, order: "comments.id").to_a
+ end
+ assert_equal 3, comments.length
+ assert_equal [5, 6, 7], comments.collect(&:id)
+ assert_no_queries do
+ comments.first.post
+ end
+ end
+
+ def test_eager_association_loading_with_belongs_to_and_conditions_string_with_quoted_table_name
+ quoted_posts_id = Comment.connection.quote_table_name("posts") + "." + Comment.connection.quote_column_name("id")
+ assert_nothing_raised do
+ Comment.includes(:post).references(:posts).where("#{quoted_posts_id} = ?", 4)
+ end
+ end
+
+ def test_eager_association_loading_with_belongs_to_and_order_string_with_unquoted_table_name
+ assert_nothing_raised do
+ Comment.all.merge!(includes: :post, order: "posts.id").to_a
+ end
+ end
+
+ def test_eager_association_loading_with_belongs_to_and_order_string_with_quoted_table_name
+ quoted_posts_id = Comment.connection.quote_table_name("posts") + "." + Comment.connection.quote_column_name("id")
+ assert_nothing_raised do
+ Comment.includes(:post).references(:posts).order(Arel.sql(quoted_posts_id))
+ end
+ end
+
+ def test_eager_association_loading_with_belongs_to_and_limit_and_multiple_associations
+ posts = Post.all.merge!(includes: [:author, :very_special_comment], limit: 1, order: "posts.id").to_a
+ assert_equal 1, posts.length
+ assert_equal [1], posts.collect(&:id)
+ end
+
+ def test_eager_association_loading_with_belongs_to_and_limit_and_offset_and_multiple_associations
+ posts = Post.all.merge!(includes: [:author, :very_special_comment], limit: 1, offset: 1, order: "posts.id").to_a
+ assert_equal 1, posts.length
+ assert_equal [2], posts.collect(&:id)
+ end
+
+ def test_eager_association_loading_with_belongs_to_inferred_foreign_key_from_association_name
+ author_favorite = AuthorFavorite.all.merge!(includes: :favorite_author).first
+ assert_equal authors(:mary), assert_no_queries { author_favorite.favorite_author }
+ end
+
+ def test_eager_load_belongs_to_quotes_table_and_column_names
+ job = Job.includes(:ideal_reference).find jobs(:unicyclist).id
+ references(:michael_unicyclist)
+ assert_no_queries { assert_equal references(:michael_unicyclist), job.ideal_reference }
+ end
+
+ def test_eager_load_has_one_quotes_table_and_column_names
+ michael = Person.all.merge!(includes: :favourite_reference).find(people(:michael).id)
+ references(:michael_unicyclist)
+ assert_no_queries { assert_equal references(:michael_unicyclist), michael.favourite_reference }
+ end
+
+ def test_eager_load_has_many_quotes_table_and_column_names
+ michael = Person.all.merge!(includes: :references).find(people(:michael).id)
+ references(:michael_magician, :michael_unicyclist)
+ assert_no_queries { assert_equal references(:michael_magician, :michael_unicyclist), michael.references.sort_by(&:id) }
+ end
+
+ def test_eager_load_has_many_through_quotes_table_and_column_names
+ michael = Person.all.merge!(includes: :jobs).find(people(:michael).id)
+ jobs(:magician, :unicyclist)
+ assert_no_queries { assert_equal jobs(:unicyclist, :magician), michael.jobs.sort_by(&:id) }
+ end
+
+ def test_eager_load_has_many_with_string_keys
+ subscriptions = subscriptions(:webster_awdr, :webster_rfr)
+ subscriber = Subscriber.all.merge!(includes: :subscriptions).find(subscribers(:second).id)
+ assert_equal subscriptions, subscriber.subscriptions.sort_by(&:id)
+ end
+
+ def test_string_id_column_joins
+ s = Subscriber.create! do |c|
+ c.id = "PL"
+ end
+
+ b = Book.create!
+
+ Subscription.create!(subscriber_id: "PL", book_id: b.id)
+ s.reload
+ s.book_ids = s.book_ids
+ end
+
+ def test_eager_load_has_many_through_with_string_keys
+ books = books(:awdr, :rfr)
+ subscriber = Subscriber.all.merge!(includes: :books).find(subscribers(:second).id)
+ assert_equal books, subscriber.books.sort_by(&:id)
+ end
+
+ def test_eager_load_belongs_to_with_string_keys
+ subscriber = subscribers(:second)
+ subscription = Subscription.all.merge!(includes: :subscriber).find(subscriptions(:webster_awdr).id)
+ assert_equal subscriber, subscription.subscriber
+ end
+
+ def test_eager_association_loading_with_explicit_join
+ posts = Post.all.merge!(includes: :comments, joins: "INNER JOIN authors ON posts.author_id = authors.id AND authors.name = 'Mary'", limit: 1, order: "author_id").to_a
+ assert_equal 1, posts.length
+ end
+
+ def test_eager_with_has_many_through
+ posts_with_comments = people(:michael).posts.merge(includes: :comments, order: "posts.id").to_a
+ posts_with_author = people(:michael).posts.merge(includes: :author, order: "posts.id").to_a
+ posts_with_comments_and_author = people(:michael).posts.merge(includes: [ :comments, :author ], order: "posts.id").to_a
+ assert_equal 2, posts_with_comments.inject(0) { |sum, post| sum + post.comments.size }
+ assert_equal authors(:david), assert_no_queries { posts_with_author.first.author }
+ assert_equal authors(:david), assert_no_queries { posts_with_comments_and_author.first.author }
+ end
+
+ def test_eager_with_has_many_through_a_belongs_to_association
+ author = authors(:mary)
+ Post.create!(author: author, title: "TITLE", body: "BODY")
+ author.author_favorites.create(favorite_author_id: 1)
+ author.author_favorites.create(favorite_author_id: 2)
+ posts_with_author_favorites = author.posts.merge(includes: :author_favorites).to_a
+ assert_no_queries { posts_with_author_favorites.first.author_favorites.first.author_id }
+ end
+
+ def test_eager_with_has_many_through_an_sti_join_model
+ author = Author.all.merge!(includes: :special_post_comments, order: "authors.id").first
+ assert_equal [comments(:does_it_hurt)], assert_no_queries { author.special_post_comments }
+ end
+
+ def test_preloading_has_many_through_with_implicit_source
+ authors = Author.includes(:very_special_comments).to_a
+ assert_no_queries do
+ special_comment_authors = authors.map { |author| [author.name, author.very_special_comments.size] }
+ assert_equal [["David", 1], ["Mary", 0], ["Bob", 0]], special_comment_authors
+ end
+ end
+
+ def test_eager_with_has_many_through_an_sti_join_model_with_conditions_on_both
+ author = Author.all.merge!(includes: :special_nonexistent_post_comments, order: "authors.id").first
+ assert_equal [], author.special_nonexistent_post_comments
+ end
+
+ def test_eager_with_has_many_through_join_model_with_conditions
+ assert_equal Author.all.merge!(includes: :hello_post_comments,
+ order: "authors.id").first.hello_post_comments.sort_by(&:id),
+ Author.all.merge!(order: "authors.id").first.hello_post_comments.sort_by(&:id)
+ end
+
+ def test_eager_with_has_many_through_join_model_with_conditions_on_top_level
+ assert_equal comments(:more_greetings), Author.all.merge!(includes: :comments_with_order_and_conditions).find(authors(:david).id).comments_with_order_and_conditions.first
+ end
+
+ def test_eager_with_has_many_through_join_model_with_include
+ author_comments = Author.all.merge!(includes: :comments_with_include).find(authors(:david).id).comments_with_include.to_a
+ assert_no_queries do
+ author_comments.first.post.title
+ end
+ end
+
+ def test_eager_with_has_many_through_with_conditions_join_model_with_include
+ post_tags = Post.find(posts(:welcome).id).misc_tags
+ eager_post_tags = Post.all.merge!(includes: :misc_tags).find(1).misc_tags
+ assert_equal post_tags, eager_post_tags
+ end
+
+ def test_eager_with_has_many_through_join_model_ignores_default_includes
+ assert_nothing_raised do
+ authors(:david).comments_on_posts_with_default_include.to_a
+ end
+ end
+
+ def test_eager_with_has_many_and_limit
+ posts = Post.all.merge!(order: "posts.id asc", includes: [ :author, :comments ], limit: 2).to_a
+ assert_equal 2, posts.size
+ assert_equal 3, posts.inject(0) { |sum, post| sum + post.comments.size }
+ end
+
+ def test_eager_with_has_many_and_limit_and_conditions
+ posts = Post.all.merge!(includes: [ :author, :comments ], limit: 2, where: "posts.body = 'hello'", order: "posts.id").to_a
+ assert_equal 2, posts.size
+ assert_equal [4, 5], posts.collect(&:id)
+ end
+
+ def test_eager_with_has_many_and_limit_and_conditions_array
+ posts = Post.all.merge!(includes: [ :author, :comments ], limit: 2, where: [ "posts.body = ?", "hello" ], order: "posts.id").to_a
+ assert_equal 2, posts.size
+ assert_equal [4, 5], posts.collect(&:id)
+ end
+
+ def test_eager_with_has_many_and_limit_and_conditions_array_on_the_eagers
+ posts = Post.includes(:author, :comments).limit(2).references(:author).where("authors.name = ?", "David")
+ assert_equal 2, posts.size
+
+ count = Post.includes(:author, :comments).limit(2).references(:author).where("authors.name = ?", "David").count
+ assert_equal posts.size, count
+ end
+
+ def test_eager_with_has_many_and_limit_and_high_offset
+ posts = Post.all.merge!(includes: [ :author, :comments ], limit: 2, offset: 10, where: { "authors.name" => "David" }).to_a
+ assert_equal 0, posts.size
+ end
+
+ def test_eager_with_has_many_and_limit_and_high_offset_and_multiple_array_conditions
+ assert_queries(1) do
+ posts = Post.references(:authors, :comments).
+ merge(includes: [ :author, :comments ], limit: 2, offset: 10,
+ where: [ "authors.name = ? and comments.body = ?", "David", "go crazy" ]).to_a
+ assert_equal 0, posts.size
+ end
+ end
+
+ def test_eager_with_has_many_and_limit_and_high_offset_and_multiple_hash_conditions
+ assert_queries(1) do
+ posts = Post.all.merge!(includes: [ :author, :comments ], limit: 2, offset: 10,
+ where: { "authors.name" => "David", "comments.body" => "go crazy" }).to_a
+ assert_equal 0, posts.size
+ end
+ end
+
+ def test_count_eager_with_has_many_and_limit_and_high_offset
+ posts = Post.all.merge!(includes: [ :author, :comments ], limit: 2, offset: 10, where: { "authors.name" => "David" }).count(:all)
+ assert_equal 0, posts
+ end
+
+ def test_eager_with_has_many_and_limit_with_no_results
+ posts = Post.all.merge!(includes: [ :author, :comments ], limit: 2, where: "posts.title = 'magic forest'").to_a
+ assert_equal 0, posts.size
+ end
+
+ def test_eager_count_performed_on_a_has_many_association_with_multi_table_conditional
+ author = authors(:david)
+ author_posts_without_comments = author.posts.select { |post| post.comments.blank? }
+ assert_equal author_posts_without_comments.size, author.posts.includes(:comments).where("comments.id is null").references(:comments).count
+ end
+
+ def test_eager_count_performed_on_a_has_many_through_association_with_multi_table_conditional
+ person = people(:michael)
+ person_posts_without_comments = person.posts.select { |post| post.comments.blank? }
+ assert_equal person_posts_without_comments.size, person.posts_with_no_comments.count
+ end
+
+ def test_eager_with_has_and_belongs_to_many_and_limit
+ posts = Post.all.merge!(includes: :categories, order: "posts.id", limit: 3).to_a
+ assert_equal 3, posts.size
+ assert_equal 2, posts[0].categories.size
+ assert_equal 1, posts[1].categories.size
+ assert_equal 0, posts[2].categories.size
+ assert_includes posts[0].categories, categories(:technology)
+ assert_includes posts[1].categories, categories(:general)
+ end
+
+ # 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 == welcome }
+ post2 = technology.posts.to_a.find { |p| p == 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
+ .includes(:comments)
+ .where("comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment'")
+ .references(:comments)
+ .limit(2)
+ .to_a
+ assert_equal 2, posts.size
+
+ count =
+ Post.includes(:comments, :author)
+ .where("authors.name = 'David' AND (comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment')")
+ .references(:authors, :comments)
+ .limit(2)
+ .count
+ assert_equal count, posts.size
+ end
+
+ def test_eager_with_has_many_and_limit_and_scoped_conditions_on_the_eagers
+ posts = nil
+ Post.includes(:comments)
+ .where("comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment'")
+ .references(:comments)
+ .scoping do
+
+ posts = authors(:david).posts.limit(2).to_a
+ assert_equal 2, posts.size
+ end
+
+ Post.includes(:comments, :author)
+ .where("authors.name = 'David' AND (comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment')")
+ .references(:authors, :comments)
+ .scoping do
+
+ count = Post.limit(2).count
+ assert_equal count, posts.size
+ end
+ end
+
+ def test_eager_association_loading_with_habtm
+ posts = Post.all.merge!(includes: :categories, order: "posts.id").to_a
+ assert_equal 2, posts[0].categories.size
+ assert_equal 1, posts[1].categories.size
+ assert_equal 0, posts[2].categories.size
+ assert_includes posts[0].categories, categories(:technology)
+ assert_includes posts[1].categories, categories(:general)
+ end
+
+ def test_eager_with_inheritance
+ SpecialPost.all.merge!(includes: [ :comments ]).to_a
+ end
+
+ def test_eager_has_one_with_association_inheritance
+ post = Post.all.merge!(includes: [ :very_special_comment ]).find(4)
+ assert_equal "VerySpecialComment", post.very_special_comment.class.to_s
+ end
+
+ def test_eager_has_many_with_association_inheritance
+ post = Post.all.merge!(includes: [ :special_comments ]).find(4)
+ post.special_comments.each do |special_comment|
+ assert special_comment.is_a?(SpecialComment)
+ end
+ end
+
+ def test_eager_habtm_with_association_inheritance
+ post = Post.all.merge!(includes: [ :special_categories ]).find(6)
+ assert_equal 1, post.special_categories.size
+ post.special_categories.each do |special_category|
+ assert_equal "SpecialCategory", special_category.class.to_s
+ end
+ end
+
+ def test_eager_with_has_one_dependent_does_not_destroy_dependent
+ assert_not_nil companies(:first_firm).account
+ f = Firm.all.merge!(includes: :account,
+ where: ["companies.name = ?", "37signals"]).first
+ assert_not_nil f.account
+ assert_equal companies(:first_firm, :reload).account, f.account
+ end
+
+ def test_eager_with_multi_table_conditional_properly_counts_the_records_when_using_size
+ author = authors(:david)
+ posts_with_no_comments = author.posts.select { |post| post.comments.blank? }
+ assert_equal posts_with_no_comments.size, author.posts_with_no_comments.size
+ assert_equal posts_with_no_comments, author.posts_with_no_comments
+ end
+
+ def test_eager_with_invalid_association_reference
+ e = assert_raise(ActiveRecord::AssociationNotFoundError) {
+ Post.all.merge!(includes: :monkeys).find(6)
+ }
+ assert_equal("Association named 'monkeys' was not found on Post; perhaps you misspelled it?", e.message)
+
+ e = assert_raise(ActiveRecord::AssociationNotFoundError) {
+ Post.all.merge!(includes: [ :monkeys ]).find(6)
+ }
+ assert_equal("Association named 'monkeys' was not found on Post; perhaps you misspelled it?", e.message)
+
+ e = assert_raise(ActiveRecord::AssociationNotFoundError) {
+ Post.all.merge!(includes: [ "monkeys" ]).find(6)
+ }
+ assert_equal("Association named 'monkeys' was not found on Post; perhaps you misspelled it?", e.message)
+
+ e = assert_raise(ActiveRecord::AssociationNotFoundError) {
+ Post.all.merge!(includes: [ :monkeys, :elephants ]).find(6)
+ }
+ assert_equal("Association named 'monkeys' was not found on Post; perhaps you misspelled it?", e.message)
+ end
+
+ def test_eager_has_many_through_with_order
+ tag = OrderedTag.create(name: "Foo")
+ post1 = Post.create!(title: "Beaches", body: "I like beaches!")
+ post2 = Post.create!(title: "Pools", body: "I like pools!")
+
+ Tagging.create!(taggable_type: "Post", taggable_id: post1.id, tag: tag)
+ Tagging.create!(taggable_type: "Post", taggable_id: post2.id, tag: tag)
+
+ tag_with_includes = OrderedTag.includes(:tagged_posts).find(tag.id)
+ assert_equal tag_with_includes.ordered_taggings.map(&:taggable).map(&:title), tag_with_includes.tagged_posts.map(&:title)
+ end
+
+ def test_eager_has_many_through_multiple_with_order
+ tag1 = OrderedTag.create!(name: "Bar")
+ tag2 = OrderedTag.create!(name: "Foo")
+
+ post1 = Post.create!(title: "Beaches", body: "I like beaches!")
+ post2 = Post.create!(title: "Pools", body: "I like pools!")
+
+ Tagging.create!(taggable: post1, tag: tag1)
+ Tagging.create!(taggable: post2, tag: tag1)
+ Tagging.create!(taggable: post2, tag: tag2)
+ Tagging.create!(taggable: post1, tag: tag2)
+
+ tags_with_includes = OrderedTag.where(id: [tag1, tag2].map(&:id)).includes(:tagged_posts).order(:id).to_a
+ tag1_with_includes = tags_with_includes.first
+ tag2_with_includes = tags_with_includes.last
+
+ assert_equal([post2, post1].map(&:title), tag1_with_includes.tagged_posts.map(&:title))
+ assert_equal([post1, post2].map(&:title), tag2_with_includes.tagged_posts.map(&:title))
+ end
+
+ def test_eager_with_default_scope
+ developer = EagerDeveloperWithDefaultScope.where(name: "David").first
+ projects = Project.order(:id).to_a
+ assert_no_queries do
+ assert_equal(projects, developer.projects)
+ end
+ end
+
+ def test_eager_with_default_scope_as_class_method
+ developer = EagerDeveloperWithClassMethodDefaultScope.where(name: "David").first
+ projects = Project.order(:id).to_a
+ assert_no_queries do
+ assert_equal(projects, developer.projects)
+ end
+ end
+
+ def test_eager_with_default_scope_as_class_method_using_find_method
+ david = developers(:david)
+ developer = EagerDeveloperWithClassMethodDefaultScope.find(david.id)
+ projects = Project.order(:id).to_a
+ assert_no_queries do
+ assert_equal(projects, developer.projects)
+ end
+ end
+
+ def test_eager_with_default_scope_as_class_method_using_find_by_method
+ developer = EagerDeveloperWithClassMethodDefaultScope.find_by(name: "David")
+ projects = Project.order(:id).to_a
+ assert_no_queries do
+ assert_equal(projects, developer.projects)
+ end
+ end
+
+ def test_eager_with_default_scope_as_lambda
+ developer = EagerDeveloperWithLambdaDefaultScope.where(name: "David").first
+ projects = Project.order(:id).to_a
+ assert_no_queries do
+ assert_equal(projects, developer.projects)
+ end
+ end
+
+ def test_eager_with_default_scope_as_block
+ # warm up the habtm cache
+ EagerDeveloperWithBlockDefaultScope.where(name: "David").first.projects
+ developer = EagerDeveloperWithBlockDefaultScope.where(name: "David").first
+ projects = Project.order(:id).to_a
+ assert_no_queries do
+ assert_equal(projects, developer.projects)
+ end
+ end
+
+ def test_eager_with_default_scope_as_callable
+ developer = EagerDeveloperWithCallableDefaultScope.where(name: "David").first
+ projects = Project.order(:id).to_a
+ assert_no_queries do
+ assert_equal(projects, developer.projects)
+ end
+ end
+
+ def test_limited_eager_with_order
+ assert_equal(
+ posts(:thinking, :sti_comments),
+ Post.all.merge!(
+ includes: [:author, :comments], where: { "authors.name" => "David" },
+ order: Arel.sql("UPPER(posts.title)"), limit: 2, offset: 1
+ ).to_a
+ )
+ assert_equal(
+ posts(:sti_post_and_comments, :sti_comments),
+ Post.all.merge!(
+ includes: [:author, :comments], where: { "authors.name" => "David" },
+ order: Arel.sql("UPPER(posts.title) DESC"), limit: 2, offset: 1
+ ).to_a
+ )
+ end
+
+ def test_limited_eager_with_multiple_order_columns
+ assert_equal(
+ posts(:thinking, :sti_comments),
+ Post.all.merge!(
+ includes: [:author, :comments], where: { "authors.name" => "David" },
+ order: [Arel.sql("UPPER(posts.title)"), "posts.id"], limit: 2, offset: 1
+ ).to_a
+ )
+ assert_equal(
+ posts(:sti_post_and_comments, :sti_comments),
+ Post.all.merge!(
+ includes: [:author, :comments], where: { "authors.name" => "David" },
+ order: [Arel.sql("UPPER(posts.title) DESC"), "posts.id"], limit: 2, offset: 1
+ ).to_a
+ )
+ end
+
+ def test_limited_eager_with_numeric_in_association
+ assert_equal(
+ people(:david, :susan),
+ Person.references(:number1_fans_people).merge(
+ includes: [:readers, :primary_contact, :number1_fan],
+ where: "number1_fans_people.first_name like 'M%'",
+ order: "people.id", limit: 2, offset: 0
+ ).to_a
+ )
+ end
+
+ def test_polymorphic_type_condition
+ post = Post.all.merge!(includes: :taggings).find(posts(:thinking).id)
+ assert_includes post.taggings, taggings(:thinking_general)
+ post = SpecialPost.all.merge!(includes: :taggings).find(posts(:thinking).id)
+ assert_includes post.taggings, taggings(:thinking_general)
+ end
+
+ def test_eager_with_multiple_associations_with_same_table_has_many_and_habtm
+ # Eager includes of has many and habtm associations aren't necessarily sorted in the same way
+ def assert_equal_after_sort(item1, item2, item3 = nil)
+ assert_equal(item1.sort { |a, b| a.id <=> b.id }, item2.sort { |a, b| a.id <=> b.id })
+ assert_equal(item3.sort { |a, b| a.id <=> b.id }, item2.sort { |a, b| a.id <=> b.id }) if item3
+ end
+ # Test regular association, association with conditions, association with
+ # STI, and association with conditions assured not to be true
+ post_types = [:posts, :other_posts, :special_posts]
+ # test both has_many and has_and_belongs_to_many
+ [Author, Category].each do |className|
+ d1 = find_all_ordered(className)
+ # test including all post types at once
+ d2 = find_all_ordered(className, post_types)
+ d1.each_index do |i|
+ assert_equal(d1[i], d2[i])
+ assert_equal_after_sort(d1[i].posts, d2[i].posts)
+ post_types[1..-1].each do |post_type|
+ # test including post_types together
+ d3 = find_all_ordered(className, [:posts, post_type])
+ assert_equal(d1[i], d3[i])
+ assert_equal_after_sort(d1[i].posts, d3[i].posts)
+ assert_equal_after_sort(d1[i].send(post_type), d2[i].send(post_type), d3[i].send(post_type))
+ end
+ end
+ end
+ end
+
+ def test_eager_with_multiple_associations_with_same_table_has_one
+ d1 = find_all_ordered(Firm)
+ d2 = find_all_ordered(Firm, :account)
+ d1.each_index do |i|
+ assert_equal(d1[i], d2[i])
+ if d1[i].account.nil?
+ assert_nil(d2[i].account)
+ else
+ assert_equal(d1[i].account, d2[i].account)
+ end
+ end
+ end
+
+ def test_eager_with_multiple_associations_with_same_table_belongs_to
+ firm_types = [:firm, :firm_with_basic_id, :firm_with_other_name, :firm_with_condition]
+ d1 = find_all_ordered(Client)
+ d2 = find_all_ordered(Client, firm_types)
+ d1.each_index do |i|
+ assert_equal(d1[i], d2[i])
+ firm_types.each do |type|
+ if (expected = d1[i].send(type)).nil?
+ assert_nil(d2[i].send(type))
+ else
+ assert_equal(expected, d2[i].send(type))
+ end
+ end
+ end
+ end
+ def test_eager_with_valid_association_as_string_not_symbol
+ assert_nothing_raised { Post.all.merge!(includes: "comments").to_a }
+ end
+
+ def test_eager_with_floating_point_numbers
+ assert_queries(2) do
+ # Before changes, the floating point numbers will be interpreted as table names and will cause this to run in one query
+ Comment.all.merge!(where: "123.456 = 123.456", includes: :post).to_a
+ end
+ end
+
+ def test_preconfigured_includes_with_belongs_to
+ author = posts(:welcome).author_with_posts
+ assert_no_queries { assert_equal 5, author.posts.size }
+ end
+
+ def test_preconfigured_includes_with_has_one
+ comment = posts(:sti_comments).very_special_comment_with_post
+ assert_no_queries { assert_equal posts(:sti_comments), comment.post }
+ end
+
+ def test_eager_association_with_scope_with_joins
+ assert_nothing_raised do
+ Post.includes(:very_special_comment_with_post_with_joins).to_a
+ end
+ end
+
+ def test_preconfigured_includes_with_has_many
+ posts = authors(:david).posts_with_comments
+ one = posts.detect { |p| p.id == 1 }
+ assert_no_queries do
+ assert_equal 5, posts.size
+ assert_equal 2, one.comments.size
+ end
+ end
+
+ def test_preconfigured_includes_with_habtm
+ posts = authors(:david).posts_with_categories
+ one = posts.detect { |p| p.id == 1 }
+ assert_no_queries do
+ assert_equal 5, posts.size
+ assert_equal 2, one.categories.size
+ end
+ end
+
+ def test_preconfigured_includes_with_has_many_and_habtm
+ posts = authors(:david).posts_with_comments_and_categories
+ one = posts.detect { |p| p.id == 1 }
+ assert_no_queries do
+ assert_equal 5, posts.size
+ assert_equal 2, one.comments.size
+ assert_equal 2, one.categories.size
+ end
+ end
+
+ def test_count_with_include
+ assert_equal 3, authors(:david).posts_with_comments.where("length(comments.body) > 15").references(:comments).count
+ end
+
+ def test_association_loading_notification
+ notifications = messages_for("instantiation.active_record") do
+ Developer.all.merge!(includes: "projects", where: { "developers_projects.access_level" => 1 }, limit: 5).to_a.size
+ end
+
+ message = notifications.first
+ payload = message.last
+ count = Developer.all.merge!(includes: "projects", where: { "developers_projects.access_level" => 1 }, limit: 5).to_a.size
+
+ # eagerloaded row count should be greater than just developer count
+ assert_operator payload[:record_count], :>, count
+ assert_equal Developer.name, payload[:class_name]
+ end
+
+ def test_base_messages
+ notifications = messages_for("instantiation.active_record") do
+ Developer.all.to_a
+ end
+ message = notifications.first
+ payload = message.last
+
+ assert_equal Developer.all.to_a.count, payload[:record_count]
+ assert_equal Developer.name, payload[:class_name]
+ end
+
+ def messages_for(name)
+ notifications = []
+ ActiveSupport::Notifications.subscribe(name) do |*args|
+ notifications << args
+ end
+ yield
+ notifications
+ ensure
+ ActiveSupport::Notifications.unsubscribe(name)
+ end
+
+ def test_load_with_sti_sharing_association
+ assert_queries(2) do # should not do 1 query per subclass
+ Comment.includes(:post).to_a
+ end
+ end
+
+ def test_conditions_on_join_table_with_include_and_limit
+ assert_equal 3, Developer.all.merge!(includes: "projects", where: { "developers_projects.access_level" => 1 }, limit: 5).to_a.size
+ end
+
+ def test_dont_create_temporary_active_record_instances
+ Developer.instance_count = 0
+ developers = Developer.all.merge!(includes: "projects", where: { "developers_projects.access_level" => 1 }, limit: 5).to_a
+ assert_equal developers.count, Developer.instance_count
+ end
+
+ def test_order_on_join_table_with_include_and_limit
+ assert_equal 5, Developer.all.merge!(includes: "projects", order: "developers_projects.joined_on DESC", limit: 5).to_a.size
+ end
+
+ def test_eager_loading_with_order_on_joined_table_preloads
+ posts = assert_queries(2) do
+ Post.all.merge!(joins: :comments, includes: :author, order: "comments.id DESC").to_a
+ end
+ 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
+ posts = assert_queries(2) do
+ Post.all.merge!(select: "distinct posts.*", includes: :author, joins: [:comments], where: "comments.body like 'Thank you%'", order: "posts.id").to_a
+ end
+ assert_equal [posts(:welcome)], posts
+ assert_equal authors(:david), assert_no_queries { posts[0].author }
+
+ posts = assert_queries(2) do
+ Post.all.merge!(includes: :author, joins: { taggings: :tag }, where: "tags.name = 'General'", order: "posts.id").to_a
+ end
+ assert_equal posts(:welcome, :thinking), posts
+
+ posts = assert_queries(2) do
+ Post.all.merge!(includes: :author, joins: { taggings: { tag: :taggings } }, where: "taggings_tags.super_tag_id=2", order: "posts.id").to_a
+ end
+ assert_equal posts(:welcome, :thinking), posts
+ end
+
+ def test_preload_has_many_with_association_condition_and_default_scope
+ post = Post.create!(title: "Beaches", body: "I like beaches!")
+ Reader.create! person: people(:david), post: post
+ LazyReader.create! person: people(:susan), post: post
+
+ assert_equal 1, post.lazy_readers.to_a.size
+ assert_equal 2, post.lazy_readers_skimmers_or_not.to_a.size
+
+ post_with_readers = Post.includes(:lazy_readers_skimmers_or_not).find(post.id)
+ assert_equal 2, post_with_readers.lazy_readers_skimmers_or_not.to_a.size
+ end
+
+ def test_eager_loading_with_conditions_on_string_joined_table_preloads
+ posts = assert_queries(2) do
+ Post.all.merge!(select: "distinct posts.*", includes: :author, joins: "INNER JOIN comments on comments.post_id = posts.id", where: "comments.body like 'Thank you%'", order: "posts.id").to_a
+ end
+ assert_equal [posts(:welcome)], posts
+ assert_equal authors(:david), assert_no_queries { posts[0].author }
+
+ posts = assert_queries(2) do
+ Post.all.merge!(select: "distinct posts.*", includes: :author, joins: ["INNER JOIN comments on comments.post_id = posts.id"], where: "comments.body like 'Thank you%'", order: "posts.id").to_a
+ end
+ assert_equal [posts(:welcome)], posts
+ assert_equal authors(:david), assert_no_queries { posts[0].author }
+ end
+
+ def test_eager_loading_with_select_on_joined_table_preloads
+ posts = assert_queries(2) do
+ Post.all.merge!(select: "posts.*, authors.name as author_name", includes: :comments, joins: :author, order: "posts.id").to_a
+ end
+ assert_equal "David", posts[0].author_name
+ assert_equal posts(:welcome).comments, assert_no_queries { posts[0].comments }
+ end
+
+ def test_eager_loading_with_conditions_on_join_model_preloads
+ authors = assert_queries(2) do
+ Author.all.merge!(includes: :author_address, joins: :comments, where: "posts.title like 'Welcome%'").to_a
+ end
+ assert_equal authors(:david), authors[0]
+ assert_equal author_addresses(:david_address), authors[0].author_address
+ end
+
+ def test_preload_belongs_to_uses_exclusive_scope
+ people = Person.males.merge(includes: :primary_contact).to_a
+ assert_not_equal people.length, 0
+ people.each do |person|
+ assert_no_queries { assert_not_nil person.primary_contact }
+ assert_equal Person.find(person.id).primary_contact, person.primary_contact
+ end
+ end
+
+ def test_preload_has_many_uses_exclusive_scope
+ people = Person.males.includes(:agents).to_a
+ people.each do |person|
+ assert_equal Person.find(person.id).agents, person.agents
+ end
+ end
+
+ def test_preload_has_many_using_primary_key
+ expected = Firm.first.clients_using_primary_key.to_a
+ firm = Firm.includes(:clients_using_primary_key).first
+ assert_no_queries do
+ assert_equal expected, firm.clients_using_primary_key
+ end
+ end
+
+ def test_include_has_many_using_primary_key
+ expected = Firm.find(1).clients_using_primary_key.sort_by(&:name)
+ # Oracle adapter truncates alias to 30 characters
+ if current_adapter?(:OracleAdapter)
+ firm = Firm.all.merge!(includes: :clients_using_primary_key, order: "clients_using_primary_keys_companies"[0, 30] + ".name").find(1)
+ else
+ firm = Firm.all.merge!(includes: :clients_using_primary_key, order: "clients_using_primary_keys_companies.name").find(1)
+ end
+ assert_no_queries do
+ assert_equal expected, firm.clients_using_primary_key
+ end
+ end
+
+ def test_preload_has_one_using_primary_key
+ expected = accounts(:signals37)
+ firm = Firm.all.merge!(includes: :account_using_primary_key, order: "companies.id").first
+ assert_no_queries do
+ assert_equal expected, firm.account_using_primary_key
+ end
+ end
+
+ def test_include_has_one_using_primary_key
+ expected = accounts(:signals37)
+ firm = Firm.all.merge!(includes: :account_using_primary_key, order: "accounts.id").to_a.detect { |f| f.id == 1 }
+ assert_no_queries do
+ assert_equal expected, firm.account_using_primary_key
+ end
+ end
+
+ def test_preloading_empty_belongs_to
+ c = Client.create!(name: "Foo", client_of: Company.maximum(:id) + 1)
+
+ client = assert_queries(2) { Client.preload(:firm).find(c.id) }
+ assert_no_queries { assert_nil client.firm }
+ end
+
+ def test_preloading_empty_belongs_to_polymorphic
+ t = Tagging.create!(taggable_type: "Post", taggable_id: Post.maximum(:id) + 1, tag: tags(:general))
+
+ tagging = assert_queries(2) { Tagging.preload(:taggable).find(t.id) }
+ assert_no_queries { assert_nil tagging.taggable }
+ end
+
+ def test_preloading_through_empty_belongs_to
+ c = Client.create!(name: "Foo", client_of: Company.maximum(:id) + 1)
+
+ client = assert_queries(2) { Client.preload(:accounts).find(c.id) }
+ assert_no_queries { assert client.accounts.empty? }
+ end
+
+ def test_preloading_has_many_through_with_distinct
+ mary = Author.includes(:unique_categorized_posts).where(id: authors(:mary).id).first
+ assert_equal 1, mary.unique_categorized_posts.length
+ assert_equal 1, mary.unique_categorized_post_ids.length
+ end
+
+ def test_preloading_has_one_using_reorder
+ klass = Class.new(ActiveRecord::Base) do
+ def self.name; "TempAuthor"; end
+ self.table_name = "authors"
+ has_one :post, class_name: "PostWithDefaultScope", foreign_key: :author_id
+ has_one :reorderd_post, -> { reorder(title: :desc) }, class_name: "PostWithDefaultScope", foreign_key: :author_id
+ end
+
+ author = klass.first
+ # PRECONDITION: make sure ordering results in different results
+ assert_not_equal author.post, author.reorderd_post
+
+ preloaded_reorderd_post = klass.preload(:reorderd_post).first.reorderd_post
+
+ assert_equal author.reorderd_post, preloaded_reorderd_post
+ assert_equal Post.order(title: :desc).first.title, preloaded_reorderd_post.title
+ end
+
+ def test_preloading_polymorphic_with_custom_foreign_type
+ sponsor = sponsors(:moustache_club_sponsor_for_groucho)
+ groucho = members(:groucho)
+
+ sponsor = assert_queries(2) {
+ Sponsor.includes(:thing).where(id: sponsor.id).first
+ }
+ assert_no_queries { assert_equal groucho, sponsor.thing }
+ end
+
+ def test_joins_with_includes_should_preload_via_joins
+ post = assert_queries(1) { Post.includes(:comments).joins(:comments).order("posts.id desc").to_a.first }
+
+ assert_queries(0) do
+ assert_not_equal 0, post.comments.to_a.count
+ end
+ end
+
+ def test_join_eager_with_empty_order_should_generate_valid_sql
+ assert_nothing_raised do
+ Post.includes(:comments).order("").where(comments: { body: "Thank you for the welcome" }).first
+ end
+ end
+
+ def test_deep_including_through_habtm
+ # warm up habtm cache
+ posts = Post.all.merge!(includes: { categories: :categorizations }, order: "posts.id").to_a
+ posts[0].categories[0].categorizations.length
+
+ posts = Post.all.merge!(includes: { categories: :categorizations }, order: "posts.id").to_a
+ assert_no_queries { assert_equal 2, posts[0].categories[0].categorizations.length }
+ assert_no_queries { assert_equal 1, posts[0].categories[1].categorizations.length }
+ assert_no_queries { assert_equal 2, posts[1].categories[0].categorizations.length }
+ end
+
+ def test_eager_load_multiple_associations_with_references
+ mentor = Mentor.create!(name: "Barış Can DAYLIK")
+ developer = Developer.create!(name: "Mehmet Emin İNAÇ", mentor: mentor)
+ Contract.create!(developer: developer)
+ project = Project.create!(name: "VNGRS", mentor: mentor)
+ project.developers << developer
+ projects = Project.references(:mentors).includes(mentor: { developers: :contracts }, developers: :contracts)
+ assert_equal projects.last.mentor.developers.first.contracts, projects.last.developers.last.contracts
+ end
+
+ def test_preloading_has_many_through_with_custom_scope
+ project = Project.includes(:developers_named_david_with_hash_conditions).find(projects(:active_record).id)
+ assert_equal [developers(:david)], project.developers_named_david_with_hash_conditions
+ end
+
+ test "scoping with a circular preload" do
+ assert_equal Comment.find(1), Comment.preload(post: :comments).scoping { Comment.find(1) }
+ end
+
+ test "circular preload does not modify unscoped" do
+ expected = FirstPost.unscoped.find(2)
+ FirstPost.preload(comments: :first_post).find(1)
+ assert_equal expected, FirstPost.unscoped.find(2)
+ end
+
+ test "preload ignores the scoping" do
+ assert_equal(
+ Comment.find(1).post,
+ Post.where("1 = 0").scoping { Comment.preload(:post).find(1).post }
+ )
+ end
+
+ test "deep preload" do
+ post = Post.preload(author: :posts, comments: :post).first
+
+ assert_predicate post.author.association(:posts), :loaded?
+ assert_predicate post.comments.first.association(:post), :loaded?
+ end
+
+ test "preloading does not cache has many association subset when preloaded with a through association" do
+ author = Author.includes(:comments_with_order_and_conditions, :posts).first
+ assert_no_queries { assert_equal 2, author.comments_with_order_and_conditions.size }
+ assert_no_queries { assert_equal 5, author.posts.size, "should not cache a subset of the association" }
+ end
+
+ test "preloading a through association twice does not reset it" do
+ members = Member.includes(current_membership: :club).includes(:club).to_a
+ assert_no_queries {
+ assert_equal 3, members.map(&:current_membership).map(&:club).size
+ }
+ end
+
+ test "works in combination with order(:symbol) and reorder(:symbol)" do
+ author = Author.includes(:posts).references(:posts).order(:name).find_by("posts.title IS NOT NULL")
+ assert_equal authors(:bob), author
+
+ author = Author.includes(:posts).references(:posts).reorder(:name).find_by("posts.title IS NOT NULL")
+ assert_equal authors(:bob), author
+ end
+
+ test "preloading with a polymorphic association and using the existential predicate but also using a select" do
+ assert_equal authors(:david), authors(:david).essays.includes(:writer).first.writer
+
+ assert_nothing_raised do
+ authors(:david).essays.includes(:writer).select(:name).any?
+ end
+ end
+
+ test "preloading the same association twice works" do
+ Member.create!
+ members = Member.preload(:current_membership).includes(current_membership: :club).all.to_a
+ assert_no_queries {
+ members_with_membership = members.select(&:current_membership)
+ assert_equal 3, members_with_membership.map(&:current_membership).map(&:club).size
+ }
+ end
+
+ test "preloading with a polymorphic association and using the existential predicate" do
+ assert_equal authors(:david), authors(:david).essays.includes(:writer).first.writer
+
+ assert_nothing_raised do
+ authors(:david).essays.includes(:writer).any?
+ authors(:david).essays.includes(:writer).exists?
+ authors(:david).essays.includes(:owner).where("name IS NOT NULL").exists?
+ end
+ end
+
+ test "preloading associations with string joins and order references" do
+ author = assert_queries(2) {
+ Author.includes(:posts).joins("LEFT JOIN posts ON posts.author_id = authors.id").order("posts.title DESC").first
+ }
+ assert_no_queries {
+ assert_equal 5, author.posts.size
+ }
+ end
+
+ test "including associations with where.not adds implicit references" do
+ author = assert_queries(2) {
+ Author.includes(:posts).where.not(posts: { title: "Welcome to the weblog" }).last
+ }
+
+ assert_no_queries {
+ assert_equal 2, author.posts.size
+ }
+ end
+
+ test "including association based on sql condition and no database column" do
+ assert_equal pets(:parrot), Owner.including_last_pet.first.last_pet
+ end
+
+ test "preloading and eager loading of instance dependent associations is not supported" do
+ message = "association scope 'posts_with_signature' is"
+ error = assert_raises(ArgumentError) do
+ Author.includes(:posts_with_signature).to_a
+ end
+ assert_match message, error.message
+
+ error = assert_raises(ArgumentError) do
+ Author.preload(:posts_with_signature).to_a
+ end
+ assert_match message, error.message
+
+ error = assert_raises(ArgumentError) do
+ Author.eager_load(:posts_with_signature).to_a
+ end
+ assert_match message, error.message
+ end
+
+ test "preload with invalid argument" do
+ exception = assert_raises(ArgumentError) do
+ Author.preload(10).to_a
+ end
+ assert_equal("10 was not recognized for preload", exception.message)
+ end
+
+ test "associations with extensions are not instance dependent" do
+ assert_nothing_raised do
+ Author.includes(:posts_with_extension).to_a
+ end
+ end
+
+ test "including associations with extensions and an instance dependent scope is not supported" do
+ e = assert_raises(ArgumentError) do
+ Author.includes(:posts_with_extension_and_instance).to_a
+ end
+ assert_match(/Preloading instance dependent scopes is not supported/, e.message)
+ end
+
+ test "preloading readonly association" do
+ # has-one
+ firm = Firm.where(id: "1").preload(:readonly_account).first!
+ assert_predicate firm.readonly_account, :readonly?
+
+ # has_and_belongs_to_many
+ project = Project.where(id: "2").preload(:readonly_developers).first!
+ assert_predicate project.readonly_developers.first, :readonly?
+
+ # has-many :through
+ david = Author.where(id: "1").preload(:readonly_comments).first!
+ assert_predicate david.readonly_comments.first, :readonly?
+ end
+
+ test "eager-loading non-readonly association" do
+ # has_one
+ firm = Firm.where(id: "1").eager_load(:account).first!
+ assert_not_predicate firm.account, :readonly?
+
+ # has_and_belongs_to_many
+ project = Project.where(id: "2").eager_load(:developers).first!
+ assert_not_predicate project.developers.first, :readonly?
+
+ # has_many :through
+ david = Author.where(id: "1").eager_load(:comments).first!
+ assert_not_predicate david.comments.first, :readonly?
+
+ # belongs_to
+ post = Post.where(id: "1").eager_load(:author).first!
+ assert_not_predicate post.author, :readonly?
+ end
+
+ test "eager-loading readonly association" do
+ # has-one
+ firm = Firm.where(id: "1").eager_load(:readonly_account).first!
+ assert_predicate firm.readonly_account, :readonly?
+
+ # has_and_belongs_to_many
+ project = Project.where(id: "2").eager_load(:readonly_developers).first!
+ assert_predicate project.readonly_developers.first, :readonly?
+
+ # has-many :through
+ david = Author.where(id: "1").eager_load(:readonly_comments).first!
+ assert_predicate david.readonly_comments.first, :readonly?
+
+ # belongs_to
+ post = Post.where(id: "1").eager_load(:readonly_author).first!
+ assert_predicate post.readonly_author, :readonly?
+ end
+
+ test "preloading a polymorphic association with references to the associated table" do
+ post = Post.includes(:tags).references(:tags).where("tags.name = ?", "General").first
+ assert_equal posts(:welcome), post
+ end
+
+ test "eager-loading a polymorphic association with references to the associated table" do
+ post = Post.eager_load(:tags).where("tags.name = ?", "General").first
+ assert_equal posts(:welcome), post
+ end
+
+ test "eager-loading with a polymorphic association and using the existential predicate" do
+ assert_equal true, authors(:david).essays.eager_load(:writer).exists?
+ end
+
+ # CollectionProxy#reader is expensive, so the preloader avoids calling it.
+ test "preloading has_many_through association avoids calling association.reader" do
+ ActiveRecord::Associations::HasManyAssociation.any_instance.expects(:reader).never
+ Author.preload(:readonly_comments).first!
+ end
+
+ private
+ def find_all_ordered(klass, include = nil)
+ klass.order("#{klass.table_name}.#{klass.primary_key}").includes(include).to_a
+ end
+end
diff --git a/activerecord/test/cases/associations/extension_test.rb b/activerecord/test/cases/associations/extension_test.rb
new file mode 100644
index 0000000000..5eacb5a3d8
--- /dev/null
+++ b/activerecord/test/cases/associations/extension_test.rb
@@ -0,0 +1,94 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/post"
+require "models/comment"
+require "models/project"
+require "models/developer"
+require "models/computer"
+require "models/company_in_module"
+
+class AssociationsExtensionsTest < ActiveRecord::TestCase
+ fixtures :projects, :developers, :developers_projects, :comments, :posts
+
+ def test_extension_on_has_many
+ assert_equal comments(:more_greetings), posts(:welcome).comments.find_most_recent
+ end
+
+ def test_extension_on_habtm
+ assert_equal projects(:action_controller), developers(:david).projects.find_most_recent
+ end
+
+ def test_named_extension_on_habtm
+ assert_equal projects(:action_controller), developers(:david).projects_extended_by_name.find_most_recent
+ end
+
+ def test_named_two_extensions_on_habtm
+ assert_equal projects(:action_controller), developers(:david).projects_extended_by_name_twice.find_most_recent
+ assert_equal projects(:active_record), developers(:david).projects_extended_by_name_twice.find_least_recent
+ end
+
+ def test_named_extension_and_block_on_habtm
+ assert_equal projects(:action_controller), developers(:david).projects_extended_by_name_and_block.find_most_recent
+ assert_equal projects(:active_record), developers(:david).projects_extended_by_name_and_block.find_least_recent
+ end
+
+ def test_extension_with_scopes
+ assert_equal comments(:greetings), posts(:welcome).comments.offset(1).find_most_recent
+ assert_equal comments(:greetings), posts(:welcome).comments.not_again.find_most_recent
+ end
+
+ def test_extension_with_dirty_target
+ comment = posts(:welcome).comments.build(body: "New comment")
+ assert_equal comment, posts(:welcome).comments.with_content("New comment")
+ end
+
+ def test_marshalling_extensions
+ david = developers(:david)
+ assert_equal projects(:action_controller), david.projects.find_most_recent
+
+ marshalled = Marshal.dump(david)
+
+ # Marshaling an association shouldn't make it unusable by wiping its reflection.
+ assert_not_nil david.association(:projects).reflection
+
+ david_too = Marshal.load(marshalled)
+ assert_equal projects(:action_controller), david_too.projects.find_most_recent
+ end
+
+ def test_marshalling_named_extensions
+ david = developers(:david)
+ assert_equal projects(:action_controller), david.projects_extended_by_name.find_most_recent
+
+ marshalled = Marshal.dump(david)
+ david = Marshal.load(marshalled)
+
+ assert_equal projects(:action_controller), david.projects_extended_by_name.find_most_recent
+ end
+
+ def test_extension_name
+ extend!(Developer)
+ extend!(MyApplication::Business::Developer)
+
+ assert Object.const_get "DeveloperAssociationNameAssociationExtension"
+ assert MyApplication::Business.const_get "DeveloperAssociationNameAssociationExtension"
+ end
+
+ def test_proxy_association_after_scoped
+ post = posts(:welcome)
+ assert_equal post.association(:comments), post.comments.the_association
+ assert_equal post.association(:comments), post.comments.where("1=1").the_association
+ end
+
+ def test_association_with_default_scope
+ assert_raises OopsError do
+ posts(:welcome).comments.destroy_all
+ end
+ end
+
+ private
+
+ def extend!(model)
+ ActiveRecord::Associations::Builder::HasMany.define_extensions(model, :association_name) {}
+ end
+end
diff --git a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb
new file mode 100644
index 0000000000..5f771fe85f
--- /dev/null
+++ b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb
@@ -0,0 +1,1020 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/developer"
+require "models/computer"
+require "models/project"
+require "models/company"
+require "models/course"
+require "models/customer"
+require "models/order"
+require "models/categorization"
+require "models/category"
+require "models/post"
+require "models/author"
+require "models/tag"
+require "models/tagging"
+require "models/parrot"
+require "models/person"
+require "models/pirate"
+require "models/professor"
+require "models/treasure"
+require "models/price_estimate"
+require "models/club"
+require "models/user"
+require "models/member"
+require "models/membership"
+require "models/sponsor"
+require "models/country"
+require "models/treaty"
+require "models/vertex"
+require "models/publisher"
+require "models/publisher/article"
+require "models/publisher/magazine"
+require "active_support/core_ext/string/conversions"
+
+class ProjectWithAfterCreateHook < ActiveRecord::Base
+ self.table_name = "projects"
+ has_and_belongs_to_many :developers,
+ class_name: "DeveloperForProjectWithAfterCreateHook",
+ join_table: "developers_projects",
+ foreign_key: "project_id",
+ association_foreign_key: "developer_id"
+
+ after_create :add_david
+
+ def add_david
+ david = DeveloperForProjectWithAfterCreateHook.find_by_name("David")
+ david.projects << self
+ end
+end
+
+class DeveloperForProjectWithAfterCreateHook < ActiveRecord::Base
+ self.table_name = "developers"
+ has_and_belongs_to_many :projects,
+ class_name: "ProjectWithAfterCreateHook",
+ join_table: "developers_projects",
+ association_foreign_key: "project_id",
+ foreign_key: "developer_id"
+end
+
+class ProjectWithSymbolsForKeys < ActiveRecord::Base
+ self.table_name = "projects"
+ has_and_belongs_to_many :developers,
+ class_name: "DeveloperWithSymbolsForKeys",
+ join_table: :developers_projects,
+ foreign_key: :project_id,
+ association_foreign_key: "developer_id"
+end
+
+class DeveloperWithSymbolsForKeys < ActiveRecord::Base
+ self.table_name = "developers"
+ has_and_belongs_to_many :projects,
+ class_name: "ProjectWithSymbolsForKeys",
+ join_table: :developers_projects,
+ association_foreign_key: :project_id,
+ foreign_key: "developer_id"
+end
+
+class SubDeveloper < Developer
+ self.table_name = "developers"
+ has_and_belongs_to_many :special_projects,
+ join_table: "developers_projects",
+ foreign_key: "project_id",
+ association_foreign_key: "developer_id"
+end
+
+class DeveloperWithSymbolClassName < Developer
+ has_and_belongs_to_many :projects, class_name: :ProjectWithSymbolsForKeys
+end
+
+class DeveloperWithExtendOption < Developer
+ module NamedExtension
+ def category
+ "sns"
+ end
+ end
+
+ has_and_belongs_to_many :projects, extend: NamedExtension
+end
+
+class ProjectUnscopingDavidDefaultScope < ActiveRecord::Base
+ self.table_name = "projects"
+ has_and_belongs_to_many :developers, -> { unscope(where: "name") },
+ class_name: "LazyBlockDeveloperCalledDavid",
+ join_table: "developers_projects",
+ foreign_key: "project_id",
+ association_foreign_key: "developer_id"
+end
+
+class Kitchen < ActiveRecord::Base
+ has_one :sink
+end
+
+class Sink < ActiveRecord::Base
+ has_and_belongs_to_many :sources, join_table: :edges
+ belongs_to :kitchen
+ accepts_nested_attributes_for :kitchen
+end
+
+class Source < ActiveRecord::Base
+ self.table_name = "men"
+ has_and_belongs_to_many :sinks, join_table: :edges
+end
+
+class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
+ fixtures :accounts, :companies, :categories, :posts, :categories_posts, :developers, :projects, :developers_projects,
+ :parrots, :pirates, :parrots_pirates, :treasures, :price_estimates, :tags, :taggings, :computers
+
+ def setup_data_for_habtm_case
+ ActiveRecord::Base.connection.execute("delete from countries_treaties")
+
+ country = Country.new(name: "India")
+ country.country_id = "c1"
+ country.save!
+
+ treaty = Treaty.new(name: "peace")
+ treaty.treaty_id = "t1"
+ country.treaties << treaty
+ end
+
+ def test_marshal_dump
+ post = posts :welcome
+ preloaded = Post.includes(:categories).find post.id
+ assert_equal preloaded, Marshal.load(Marshal.dump(preloaded))
+ end
+
+ def test_should_property_quote_string_primary_keys
+ setup_data_for_habtm_case
+
+ con = ActiveRecord::Base.connection
+ sql = "select * from countries_treaties"
+ record = con.select_rows(sql).last
+ assert_equal "c1", record[0]
+ assert_equal "t1", record[1]
+ end
+
+ def test_proper_usage_of_primary_keys_and_join_table
+ setup_data_for_habtm_case
+
+ assert_equal "country_id", Country.primary_key
+ assert_equal "treaty_id", Treaty.primary_key
+
+ country = Country.first
+ assert_equal 1, country.treaties.count
+ end
+
+ def test_join_table_composite_primary_key_should_not_warn
+ country = Country.new(name: "India")
+ country.country_id = "c1"
+ country.save!
+
+ treaty = Treaty.new(name: "peace")
+ treaty.treaty_id = "t1"
+ warning = capture(:stderr) do
+ country.treaties << treaty
+ end
+ assert_no_match(/WARNING: Active Record does not support composite primary key\./, warning)
+ end
+
+ def test_has_and_belongs_to_many
+ david = Developer.find(1)
+
+ assert_not_empty david.projects
+ assert_equal 2, david.projects.size
+
+ active_record = Project.find(1)
+ assert_not_empty active_record.developers
+ assert_equal 3, active_record.developers.size
+ assert_includes active_record.developers, david
+ end
+
+ def test_adding_single
+ jamis = Developer.find(2)
+ jamis.projects.reload # causing the collection to load
+ action_controller = Project.find(2)
+ assert_equal 1, jamis.projects.size
+ assert_equal 1, action_controller.developers.size
+
+ jamis.projects << action_controller
+
+ assert_equal 2, jamis.projects.size
+ assert_equal 2, jamis.projects.reload.size
+ assert_equal 2, action_controller.developers.reload.size
+ end
+
+ def test_adding_type_mismatch
+ jamis = Developer.find(2)
+ assert_raise(ActiveRecord::AssociationTypeMismatch) { jamis.projects << nil }
+ assert_raise(ActiveRecord::AssociationTypeMismatch) { jamis.projects << 1 }
+ end
+
+ def test_adding_from_the_project
+ jamis = Developer.find(2)
+ action_controller = Project.find(2)
+ action_controller.developers.reload
+ assert_equal 1, jamis.projects.size
+ assert_equal 1, action_controller.developers.size
+
+ action_controller.developers << jamis
+
+ assert_equal 2, jamis.projects.reload.size
+ assert_equal 2, action_controller.developers.size
+ assert_equal 2, action_controller.developers.reload.size
+ end
+
+ def test_adding_from_the_project_fixed_timestamp
+ jamis = Developer.find(2)
+ action_controller = Project.find(2)
+ action_controller.developers.reload
+ assert_equal 1, jamis.projects.size
+ assert_equal 1, action_controller.developers.size
+ updated_at = jamis.updated_at
+
+ action_controller.developers << jamis
+
+ assert_equal updated_at, jamis.updated_at
+ assert_equal 2, jamis.projects.reload.size
+ assert_equal 2, action_controller.developers.size
+ assert_equal 2, action_controller.developers.reload.size
+ end
+
+ def test_adding_multiple
+ aredridel = Developer.new("name" => "Aredridel")
+ aredridel.save
+ aredridel.projects.reload
+ aredridel.projects.push(Project.find(1), Project.find(2))
+ assert_equal 2, aredridel.projects.size
+ assert_equal 2, aredridel.projects.reload.size
+ end
+
+ def test_adding_a_collection
+ aredridel = Developer.new("name" => "Aredridel")
+ aredridel.save
+ aredridel.projects.reload
+ aredridel.projects.concat([Project.find(1), Project.find(2)])
+ assert_equal 2, aredridel.projects.size
+ assert_equal 2, aredridel.projects.reload.size
+ end
+
+ def test_habtm_adding_before_save
+ no_of_devels = Developer.count
+ no_of_projects = Project.count
+ aredridel = Developer.new("name" => "Aredridel")
+ aredridel.projects.concat([Project.find(1), p = Project.new("name" => "Projekt")])
+ assert_not_predicate aredridel, :persisted?
+ assert_not_predicate p, :persisted?
+ assert aredridel.save
+ assert_predicate aredridel, :persisted?
+ assert_equal no_of_devels + 1, Developer.count
+ assert_equal no_of_projects + 1, Project.count
+ assert_equal 2, aredridel.projects.size
+ assert_equal 2, aredridel.projects.reload.size
+ end
+
+ def test_habtm_saving_multiple_relationships
+ new_project = Project.new("name" => "Grimetime")
+ amount_of_developers = 4
+ developers = (0...amount_of_developers).collect { |i| Developer.create(name: "JME #{i}") }.reverse
+
+ new_project.developer_ids = [developers[0].id, developers[1].id]
+ new_project.developers_with_callback_ids = [developers[2].id, developers[3].id]
+ assert new_project.save
+
+ new_project.reload
+ assert_equal amount_of_developers, new_project.developers.size
+ assert_equal developers, new_project.developers
+ end
+
+ def test_habtm_distinct_order_preserved
+ assert_equal developers(:poor_jamis, :jamis, :david), projects(:active_record).non_unique_developers
+ assert_equal developers(:poor_jamis, :jamis, :david), projects(:active_record).developers
+ end
+
+ def test_habtm_collection_size_from_build
+ devel = Developer.create("name" => "Fred Wu")
+ devel.projects << Project.create("name" => "Grimetime")
+ devel.projects.build
+
+ assert_equal 2, devel.projects.size
+ end
+
+ def test_habtm_collection_size_from_params
+ devel = Developer.new(
+ projects_attributes: {
+ "0" => {}
+ })
+
+ assert_equal 1, devel.projects.size
+ end
+
+ def test_build
+ devel = Developer.find(1)
+ proj = assert_no_queries(ignore_none: false) { devel.projects.build("name" => "Projekt") }
+ assert_not_predicate devel.projects, :loaded?
+
+ assert_equal devel.projects.last, proj
+ assert_predicate devel.projects, :loaded?
+
+ assert_not_predicate proj, :persisted?
+ devel.save
+ assert_predicate proj, :persisted?
+ assert_equal devel.projects.last, proj
+ assert_equal Developer.find(1).projects.sort_by(&:id).last, proj # prove join table is updated
+ end
+
+ def test_new_aliased_to_build
+ devel = Developer.find(1)
+ proj = assert_no_queries(ignore_none: false) { devel.projects.new("name" => "Projekt") }
+ assert_not_predicate devel.projects, :loaded?
+
+ assert_equal devel.projects.last, proj
+ assert_predicate devel.projects, :loaded?
+
+ assert_not_predicate proj, :persisted?
+ devel.save
+ assert_predicate proj, :persisted?
+ assert_equal devel.projects.last, proj
+ assert_equal Developer.find(1).projects.sort_by(&:id).last, proj # prove join table is updated
+ end
+
+ def test_build_by_new_record
+ devel = Developer.new(name: "Marcel", salary: 75000)
+ devel.projects.build(name: "Make bed")
+ proj2 = devel.projects.build(name: "Lie in it")
+ assert_equal devel.projects.last, proj2
+ assert_not_predicate proj2, :persisted?
+ devel.save
+ assert_predicate devel, :persisted?
+ assert_predicate proj2, :persisted?
+ assert_equal devel.projects.last, proj2
+ assert_equal Developer.find_by_name("Marcel").projects.last, proj2 # prove join table is updated
+ end
+
+ def test_create
+ devel = Developer.find(1)
+ proj = devel.projects.create("name" => "Projekt")
+ assert_not_predicate devel.projects, :loaded?
+
+ assert_equal devel.projects.last, proj
+ assert_not_predicate devel.projects, :loaded?
+
+ assert_predicate proj, :persisted?
+ assert_equal Developer.find(1).projects.sort_by(&:id).last, proj # prove join table is updated
+ end
+
+ def test_creation_respects_hash_condition
+ # in Oracle '' is saved as null therefore need to save ' ' in not null column
+ post = categories(:general).post_with_conditions.build(body: " ")
+
+ assert post.save
+ assert_equal "Yet Another Testing Title", post.title
+
+ # in Oracle '' is saved as null therefore need to save ' ' in not null column
+ another_post = categories(:general).post_with_conditions.create(body: " ")
+
+ assert_predicate another_post, :persisted?
+ assert_equal "Yet Another Testing Title", another_post.title
+ end
+
+ def test_distinct_after_the_fact
+ dev = developers(:jamis)
+ dev.projects << projects(:active_record)
+ dev.projects << projects(:active_record)
+
+ assert_equal 3, dev.projects.size
+ assert_equal 1, dev.projects.uniq.size
+ end
+
+ def test_distinct_before_the_fact
+ projects(:active_record).developers << developers(:jamis)
+ projects(:active_record).developers << developers(:david)
+ assert_equal 3, projects(:active_record, :reload).developers.size
+ end
+
+ def test_distinct_option_prevents_duplicate_push
+ project = projects(:active_record)
+ project.developers << developers(:jamis)
+ project.developers << developers(:david)
+ assert_equal 3, project.developers.size
+
+ project.developers << developers(:david)
+ project.developers << developers(:jamis)
+ assert_equal 3, project.developers.size
+ end
+
+ def test_distinct_when_association_already_loaded
+ project = projects(:active_record)
+ project.developers << [ developers(:jamis), developers(:david), developers(:jamis), developers(:david) ]
+ assert_equal 3, Project.includes(:developers).find(project.id).developers.size
+ end
+
+ def test_deleting
+ david = Developer.find(1)
+ active_record = Project.find(1)
+ david.projects.reload
+ assert_equal 2, david.projects.size
+ assert_equal 3, active_record.developers.size
+
+ david.projects.delete(active_record)
+
+ assert_equal 1, david.projects.size
+ assert_equal 1, david.projects.reload.size
+ assert_equal 2, active_record.developers.reload.size
+ end
+
+ def test_deleting_array
+ david = Developer.find(1)
+ david.projects.reload
+ david.projects.delete(Project.all.to_a)
+ assert_equal 0, david.projects.size
+ assert_equal 0, david.projects.reload.size
+ end
+
+ def test_deleting_all
+ david = Developer.find(1)
+ david.projects.reload
+ david.projects.clear
+ assert_equal 0, david.projects.size
+ assert_equal 0, david.projects.reload.size
+ end
+
+ def test_removing_associations_on_destroy
+ david = DeveloperWithBeforeDestroyRaise.find(1)
+ assert_not_empty david.projects
+ david.destroy
+ assert_empty david.projects
+ assert_empty DeveloperWithBeforeDestroyRaise.connection.select_all("SELECT * FROM developers_projects WHERE developer_id = 1")
+ end
+
+ def test_destroying
+ david = Developer.find(1)
+ project = Project.find(1)
+ david.projects.reload
+ assert_equal 2, david.projects.size
+ assert_equal 3, project.developers.size
+
+ assert_no_difference "Project.count" do
+ david.projects.destroy(project)
+ end
+
+ join_records = Developer.connection.select_all("SELECT * FROM developers_projects WHERE developer_id = #{david.id} AND project_id = #{project.id}")
+ assert_empty join_records
+
+ assert_equal 1, david.reload.projects.size
+ assert_equal 1, david.projects.reload.size
+ end
+
+ def test_destroying_many
+ david = Developer.find(1)
+ david.projects.reload
+ projects = Project.all.to_a
+
+ assert_no_difference "Project.count" do
+ david.projects.destroy(*projects)
+ end
+
+ join_records = Developer.connection.select_all("SELECT * FROM developers_projects WHERE developer_id = #{david.id}")
+ assert_empty join_records
+
+ assert_equal 0, david.reload.projects.size
+ assert_equal 0, david.projects.reload.size
+ end
+
+ def test_destroy_all
+ david = Developer.find(1)
+ david.projects.reload
+ assert_not_empty david.projects
+
+ assert_no_difference "Project.count" do
+ david.projects.destroy_all
+ end
+
+ join_records = Developer.connection.select_all("SELECT * FROM developers_projects WHERE developer_id = #{david.id}")
+ assert_empty join_records
+
+ assert_empty david.projects
+ assert_empty david.projects.reload
+ end
+
+ def test_destroy_associations_destroys_multiple_associations
+ george = parrots(:george)
+ assert_not_empty george.pirates
+ assert_not_empty george.treasures
+
+ assert_no_difference "Pirate.count" do
+ assert_no_difference "Treasure.count" do
+ george.destroy_associations
+ end
+ end
+
+ join_records = Parrot.connection.select_all("SELECT * FROM parrots_pirates WHERE parrot_id = #{george.id}")
+ assert_empty join_records
+ assert_empty george.pirates.reload
+
+ join_records = Parrot.connection.select_all("SELECT * FROM parrots_treasures WHERE parrot_id = #{george.id}")
+ assert_empty join_records
+ assert_empty george.treasures.reload
+ end
+
+ def test_associations_with_conditions
+ assert_equal 3, projects(:active_record).developers.size
+ assert_equal 1, projects(:active_record).developers_named_david.size
+ assert_equal 1, projects(:active_record).developers_named_david_with_hash_conditions.size
+
+ assert_equal developers(:david), projects(:active_record).developers_named_david.find(developers(:david).id)
+ assert_equal developers(:david), projects(:active_record).developers_named_david_with_hash_conditions.find(developers(:david).id)
+ assert_equal developers(:david), projects(:active_record).salaried_developers.find(developers(:david).id)
+
+ projects(:active_record).developers_named_david.clear
+ assert_equal 2, projects(:active_record, :reload).developers.size
+ end
+
+ def test_find_in_association
+ # Using sql
+ assert_equal developers(:david), projects(:active_record).developers.find(developers(:david).id), "SQL find"
+
+ # Using ruby
+ active_record = projects(:active_record)
+ active_record.developers.reload
+ assert_equal developers(:david), active_record.developers.find(developers(:david).id), "Ruby find"
+ end
+
+ def test_include_uses_array_include_after_loaded
+ project = projects(:active_record)
+ project.developers.load_target
+
+ developer = project.developers.first
+
+ assert_no_queries(ignore_none: false) do
+ assert_predicate project.developers, :loaded?
+ assert_includes project.developers, developer
+ end
+ end
+
+ def test_include_checks_if_record_exists_if_target_not_loaded
+ project = projects(:active_record)
+ developer = project.developers.first
+
+ project.reload
+ assert_not_predicate project.developers, :loaded?
+ assert_queries(1) do
+ assert_includes project.developers, developer
+ end
+ assert_not_predicate project.developers, :loaded?
+ end
+
+ def test_include_returns_false_for_non_matching_record_to_verify_scoping
+ project = projects(:active_record)
+ developer = Developer.create name: "Bryan", salary: 50_000
+
+ assert_not_predicate project.developers, :loaded?
+ assert ! project.developers.include?(developer)
+ end
+
+ def test_find_with_merged_options
+ assert_equal 1, projects(:active_record).limited_developers.size
+ assert_equal 1, projects(:active_record).limited_developers.to_a.size
+ assert_equal 3, projects(:active_record).limited_developers.limit(nil).to_a.size
+ end
+
+ def test_dynamic_find_should_respect_association_order
+ # Developers are ordered 'name DESC, id DESC'
+ high_id_jamis = projects(:active_record).developers.create(name: "Jamis")
+
+ assert_equal high_id_jamis, projects(:active_record).developers.merge(where: "name = 'Jamis'").first
+ assert_equal high_id_jamis, projects(:active_record).developers.find_by_name("Jamis")
+ end
+
+ def test_find_should_append_to_association_order
+ ordered_developers = projects(:active_record).developers.order("projects.id")
+ assert_equal ["developers.name desc, developers.id desc", "projects.id"], ordered_developers.order_values
+ end
+
+ def test_dynamic_find_all_should_respect_readonly_access
+ projects(:active_record).readonly_developers.each { |d| assert_raise(ActiveRecord::ReadOnlyRecord) { d.save! } if d.valid? }
+ projects(:active_record).readonly_developers.each(&:readonly?)
+ end
+
+ def test_new_with_values_in_collection
+ jamis = DeveloperForProjectWithAfterCreateHook.find_by_name("Jamis")
+ david = DeveloperForProjectWithAfterCreateHook.find_by_name("David")
+ project = ProjectWithAfterCreateHook.new(name: "Cooking with Bertie")
+ project.developers << jamis
+ project.save!
+ project.reload
+
+ assert_includes project.developers, jamis
+ assert_includes project.developers, david
+ end
+
+ def test_find_in_association_with_options
+ developers = projects(:active_record).developers.to_a
+ assert_equal 3, developers.size
+
+ assert_equal developers(:poor_jamis), projects(:active_record).developers.where("salary < 10000").first
+ end
+
+ def test_association_with_extend_option
+ eponine = DeveloperWithExtendOption.create(name: "Eponine")
+ assert_equal "sns", eponine.projects.category
+ end
+
+ def test_replace_with_less
+ david = developers(:david)
+ david.projects = [projects(:action_controller)]
+ assert david.save
+ assert_equal 1, david.projects.length
+ end
+
+ def test_replace_with_new
+ david = developers(:david)
+ david.projects = [projects(:action_controller), Project.new("name" => "ActionWebSearch")]
+ david.save
+ assert_equal 2, david.projects.length
+ assert_not_includes david.projects, projects(:active_record)
+ end
+
+ def test_replace_on_new_object
+ new_developer = Developer.new("name" => "Matz")
+ new_developer.projects = [projects(:action_controller), Project.new("name" => "ActionWebSearch")]
+ new_developer.save
+ assert_equal 2, new_developer.projects.length
+ end
+
+ def test_consider_type
+ developer = Developer.first
+ special_project = SpecialProject.create("name" => "Special Project")
+
+ other_project = developer.projects.first
+ developer.special_projects << special_project
+ developer.reload
+
+ assert_includes developer.projects, special_project
+ assert_includes developer.special_projects, special_project
+ assert_not_includes developer.special_projects, other_project
+ end
+
+ def test_symbol_join_table
+ developer = Developer.first
+ sp = developer.sym_special_projects.create("name" => "omg")
+ developer.reload
+ assert_includes developer.sym_special_projects, sp
+ end
+
+ def test_update_columns_after_push_without_duplicate_join_table_rows
+ developer = Developer.new("name" => "Kano")
+ project = SpecialProject.create("name" => "Special Project")
+ assert developer.save
+ developer.projects << project
+ developer.update_columns("name" => "Bruza")
+ assert_equal 1, Developer.connection.select_value(<<-end_sql).to_i
+ SELECT count(*) FROM developers_projects
+ WHERE project_id = #{project.id}
+ AND developer_id = #{developer.id}
+ end_sql
+ end
+
+ def test_updating_attributes_on_non_rich_associations
+ welcome = categories(:technology).posts.first
+ welcome.title = "Something else"
+ assert welcome.save!
+ end
+
+ def test_habtm_respects_select
+ categories(:technology).select_testing_posts.reload.each do |o|
+ assert_respond_to o, :correctness_marker
+ end
+ assert_respond_to categories(:technology).select_testing_posts.first, :correctness_marker
+ end
+
+ def test_habtm_selects_all_columns_by_default
+ assert_equal Project.column_names.sort, developers(:david).projects.first.attributes.keys.sort
+ end
+
+ def test_habtm_respects_select_query_method
+ assert_equal ["id"], developers(:david).projects.select(:id).first.attributes.keys
+ end
+
+ def test_join_table_alias
+ # FIXME: `references` has no impact on the aliases generated for the join
+ # query. The fact that we pass `:developers_projects_join` to `references`
+ # and that the SQL string contains `developers_projects_join` is merely a
+ # coincidence.
+ assert_equal(
+ 3,
+ Developer.references(:developers_projects_join).merge(
+ includes: { projects: :developers },
+ where: "projects_developers_projects_join.joined_on IS NOT NULL"
+ ).to_a.size
+ )
+ end
+
+ def test_join_with_group
+ # FIXME: `references` has no impact on the aliases generated for the join
+ # query. The fact that we pass `:developers_projects_join` to `references`
+ # and that the SQL string contains `developers_projects_join` is merely a
+ # coincidence.
+ group = Developer.columns.inject([]) do |g, c|
+ g << "developers.#{c.name}"
+ g << "developers_projects_2.#{c.name}"
+ end
+ Project.columns.each { |c| group << "projects.#{c.name}" }
+
+ assert_equal(
+ 3,
+ Developer.references(:developers_projects_join).merge(
+ includes: { projects: :developers }, where: "projects_developers_projects_join.joined_on IS NOT NULL",
+ group: group.join(",")
+ ).to_a.size
+ )
+ end
+
+ def test_find_grouped
+ all_posts_from_category1 = Post.all.merge!(where: "category_id = 1", joins: :categories).to_a
+ grouped_posts_of_category1 = Post.all.merge!(where: "category_id = 1", group: "author_id", select: "count(posts.id) as posts_count", joins: :categories).to_a
+ assert_equal 5, all_posts_from_category1.size
+ assert_equal 2, grouped_posts_of_category1.size
+ end
+
+ def test_find_scoped_grouped
+ assert_equal 5, categories(:general).posts_grouped_by_title.to_a.size
+ assert_equal 1, categories(:technology).posts_grouped_by_title.to_a.size
+ end
+
+ def test_find_scoped_grouped_having
+ assert_equal 2, projects(:active_record).well_paid_salary_groups.to_a.size
+ assert projects(:active_record).well_paid_salary_groups.all? { |g| g.salary > 10000 }
+ end
+
+ def test_get_ids
+ assert_equal projects(:active_record, :action_controller).map(&:id).sort, developers(:david).project_ids.sort
+ assert_equal [projects(:active_record).id], developers(:jamis).project_ids
+ end
+
+ def test_get_ids_for_loaded_associations
+ developer = developers(:david)
+ developer.projects.reload
+ assert_queries(0) do
+ developer.project_ids
+ developer.project_ids
+ end
+ end
+
+ def test_get_ids_for_unloaded_associations_does_not_load_them
+ developer = developers(:david)
+ assert_not_predicate developer.projects, :loaded?
+ assert_equal projects(:active_record, :action_controller).map(&:id).sort, developer.project_ids.sort
+ assert_not_predicate developer.projects, :loaded?
+ end
+
+ def test_assign_ids
+ developer = Developer.new("name" => "Joe")
+ developer.project_ids = projects(:active_record, :action_controller).map(&:id)
+ developer.save
+ developer.reload
+ assert_equal 2, developer.projects.length
+ assert_equal [projects(:active_record), projects(:action_controller)].map(&:id).sort, developer.project_ids.sort
+ end
+
+ def test_assign_ids_ignoring_blanks
+ developer = Developer.new("name" => "Joe")
+ developer.project_ids = [projects(:active_record).id, nil, projects(:action_controller).id, ""]
+ developer.save
+ developer.reload
+ assert_equal 2, developer.projects.length
+ assert_equal [projects(:active_record), projects(:action_controller)].map(&:id).sort, developer.project_ids.sort
+ end
+
+ def test_scoped_find_on_through_association_doesnt_return_read_only_records
+ tag = Post.find(1).tags.find_by_name("General")
+
+ assert_nothing_raised do
+ tag.save!
+ end
+ end
+
+ def test_has_many_through_polymorphic_has_manys_works
+ assert_equal [10, 20].to_set, pirates(:redbeard).treasure_estimates.map(&:price).to_set
+ end
+
+ def test_symbols_as_keys
+ developer = DeveloperWithSymbolsForKeys.new(name: "David")
+ project = ProjectWithSymbolsForKeys.new(name: "Rails Testing")
+ project.developers << developer
+ project.save!
+
+ assert_equal 1, project.developers.size
+ assert_equal 1, developer.projects.size
+ assert_equal developer, project.developers.first
+ assert_equal project, developer.projects.first
+ end
+
+ def test_dynamic_find_should_respect_association_include
+ # SQL error in sort clause if :include is not included
+ # due to Unknown column 'authors.id'
+ assert Category.find(1).posts_with_authors_sorted_by_author_id.find_by_title("Welcome to the weblog")
+ end
+
+ def test_count
+ david = Developer.find(1)
+ assert_equal 2, david.projects.count
+ end
+
+ def test_association_proxy_transaction_method_starts_transaction_in_association_class
+ assert_called(Post, :transaction) do
+ Category.first.posts.transaction do
+ # nothing
+ end
+ end
+ end
+
+ def test_caching_of_columns
+ david = Developer.find(1)
+ # clear cache possibly created by other tests
+ david.projects.reset_column_information
+
+ assert_queries(:any) { david.projects.columns }
+ assert_no_queries { david.projects.columns }
+
+ ## and again to verify that reset_column_information clears the cache correctly
+ david.projects.reset_column_information
+
+ assert_queries(:any) { david.projects.columns }
+ assert_no_queries { david.projects.columns }
+ end
+
+ def test_attributes_are_being_set_when_initialized_from_habtm_association_with_where_clause
+ new_developer = projects(:action_controller).developers.where(name: "Marcelo").build
+ assert_equal new_developer.name, "Marcelo"
+ end
+
+ def test_attributes_are_being_set_when_initialized_from_habtm_association_with_multiple_where_clauses
+ new_developer = projects(:action_controller).developers.where(name: "Marcelo").where(salary: 90_000).build
+ assert_equal new_developer.name, "Marcelo"
+ assert_equal new_developer.salary, 90_000
+ end
+
+ def test_include_method_in_has_and_belongs_to_many_association_should_return_true_for_instance_added_with_build
+ project = Project.new
+ developer = project.developers.build
+ assert_includes project.developers, developer
+ end
+
+ def test_destruction_does_not_error_without_primary_key
+ redbeard = pirates(:redbeard)
+ george = parrots(:george)
+ redbeard.parrots << george
+ assert_equal 2, george.pirates.count
+ Pirate.includes(:parrots).where(parrot: redbeard.parrot).find(redbeard.id).destroy
+ assert_equal 1, george.pirates.count
+ assert_equal [], Pirate.where(id: redbeard.id)
+ end
+
+ def test_has_and_belongs_to_many_associations_on_new_records_use_null_relations
+ projects = Developer.new.projects
+ assert_no_queries(ignore_none: false) do
+ assert_equal [], projects
+ assert_equal [], projects.where(title: "omg")
+ assert_equal [], projects.pluck(:title)
+ assert_equal 0, projects.count
+ end
+ end
+
+ def test_association_with_validate_false_does_not_run_associated_validation_callbacks_on_create
+ rich_person = RichPerson.new
+
+ treasure = Treasure.new
+ treasure.rich_people << rich_person
+ treasure.valid?
+
+ assert_equal 1, treasure.rich_people.size
+ assert_nil rich_person.first_name, "should not run associated person validation on create when validate: false"
+ end
+
+ def test_association_with_validate_false_does_not_run_associated_validation_callbacks_on_update
+ rich_person = RichPerson.create!
+ person_first_name = rich_person.first_name
+ assert_not_nil person_first_name
+
+ treasure = Treasure.new
+ treasure.rich_people << rich_person
+ treasure.valid?
+
+ assert_equal 1, treasure.rich_people.size
+ assert_equal person_first_name, rich_person.first_name, "should not run associated person validation on update when validate: false"
+ end
+
+ def test_custom_join_table
+ assert_equal "edges", Vertex.reflect_on_association(:sources).join_table
+ end
+
+ def test_has_and_belongs_to_many_in_a_namespaced_model_pointing_to_a_namespaced_model
+ magazine = Publisher::Magazine.create
+ article = Publisher::Article.create
+ magazine.articles << article
+ magazine.save
+
+ assert_includes magazine.articles, article
+ end
+
+ def test_has_and_belongs_to_many_in_a_namespaced_model_pointing_to_a_non_namespaced_model
+ article = Publisher::Article.create
+ tag = Tag.create
+ article.tags << tag
+ article.save
+
+ assert_includes article.tags, tag
+ end
+
+ def test_redefine_habtm
+ child = SubDeveloper.new("name" => "Aredridel")
+ child.special_projects << SpecialProject.new("name" => "Special Project")
+ assert child.save, "child object should be saved"
+ end
+
+ def test_habtm_with_reflection_using_class_name_and_fixtures
+ assert_not_nil Developer._reflections["shared_computers"]
+ # Checking the fixture for named association is important here, because it's the only way
+ # we've been able to reproduce this bug
+ assert_not_nil File.read(File.expand_path("../../fixtures/developers.yml", __dir__)).index("shared_computers")
+ assert_equal developers(:david).shared_computers.first, computers(:laptop)
+ end
+
+ def test_with_symbol_class_name
+ assert_nothing_raised do
+ developer = DeveloperWithSymbolClassName.new
+ developer.projects
+ end
+ end
+
+ def test_alternate_database
+ professor = Professor.create(name: "Plum")
+ course = Course.create(name: "Forensics")
+ assert_equal 0, professor.courses.count
+ assert_nothing_raised do
+ professor.courses << course
+ end
+ assert_equal 1, professor.courses.count
+ end
+
+ def test_habtm_scope_can_unscope
+ project = ProjectUnscopingDavidDefaultScope.new
+ project.save!
+
+ developer = LazyBlockDeveloperCalledDavid.new(name: "Not David")
+ developer.save!
+ project.developers << developer
+
+ projects = ProjectUnscopingDavidDefaultScope.includes(:developers).where(id: project.id)
+ assert_equal 1, projects.first.developers.size
+ end
+
+ def test_preloaded_associations_size
+ assert_equal Project.first.salaried_developers.size,
+ Project.preload(:salaried_developers).first.salaried_developers.size
+
+ assert_equal Project.includes(:salaried_developers).references(:salaried_developers).first.salaried_developers.size,
+ Project.preload(:salaried_developers).first.salaried_developers.size
+
+ # Nested HATBM
+ first_project = Developer.first.projects.first
+ preloaded_first_project =
+ Developer.preload(projects: :salaried_developers).
+ first.
+ projects.
+ detect { |p| p.id == first_project.id }
+
+ assert preloaded_first_project.salaried_developers.loaded?, true
+ assert_equal first_project.salaried_developers.size, preloaded_first_project.salaried_developers.size
+ end
+
+ def test_has_and_belongs_to_many_is_useable_with_belongs_to_required_by_default
+ assert_difference "Project.first.developers_required_by_default.size", 1 do
+ Project.first.developers_required_by_default.create!(name: "Sean", salary: 50000)
+ end
+ end
+
+ def test_association_name_is_the_same_as_join_table_name
+ user = User.create!
+ assert_nothing_raised { user.jobs_pool.clear }
+ end
+
+ def test_has_and_belongs_to_many_while_partial_writes_false
+ begin
+ original_partial_writes = ActiveRecord::Base.partial_writes
+ ActiveRecord::Base.partial_writes = false
+ developer = Developer.new(name: "Mehmet Emin İNAÇ")
+ developer.projects << Project.new(name: "Bounty")
+
+ assert developer.save
+ ensure
+ ActiveRecord::Base.partial_writes = original_partial_writes
+ end
+ end
+
+ def test_has_and_belongs_to_many_with_belongs_to
+ sink = Sink.create! kitchen: Kitchen.new, sources: [Source.new]
+ assert_equal 1, sink.sources.count
+ end
+end
diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb
new file mode 100644
index 0000000000..00821f2319
--- /dev/null
+++ b/activerecord/test/cases/associations/has_many_associations_test.rb
@@ -0,0 +1,2643 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/developer"
+require "models/computer"
+require "models/project"
+require "models/company"
+require "models/contract"
+require "models/topic"
+require "models/reply"
+require "models/category"
+require "models/image"
+require "models/post"
+require "models/author"
+require "models/essay"
+require "models/comment"
+require "models/person"
+require "models/reader"
+require "models/tagging"
+require "models/tag"
+require "models/invoice"
+require "models/line_item"
+require "models/car"
+require "models/bulb"
+require "models/engine"
+require "models/categorization"
+require "models/minivan"
+require "models/speedometer"
+require "models/reference"
+require "models/job"
+require "models/college"
+require "models/student"
+require "models/pirate"
+require "models/ship"
+require "models/ship_part"
+require "models/treasure"
+require "models/parrot"
+require "models/tyre"
+require "models/subscriber"
+require "models/subscription"
+require "models/zine"
+require "models/interest"
+
+class HasManyAssociationsTestForReorderWithJoinDependency < ActiveRecord::TestCase
+ fixtures :authors, :author_addresses, :posts, :comments
+
+ def test_should_generate_valid_sql
+ author = authors(:david)
+ # this can fail on adapters which require ORDER BY expressions to be included in the SELECT expression
+ # if the reorder clauses are not correctly handled
+ assert author.posts_with_comments_sorted_by_comment_id.where("comments.id > 0").reorder("posts.comments_count DESC", "posts.tags_count DESC").last
+ end
+end
+
+class HasManyAssociationsTestPrimaryKeys < ActiveRecord::TestCase
+ fixtures :authors, :author_addresses, :essays, :subscribers, :subscriptions, :people
+
+ def test_custom_primary_key_on_new_record_should_fetch_with_query
+ subscriber = Subscriber.new(nick: "webster132")
+ assert_not_predicate subscriber.subscriptions, :loaded?
+
+ assert_queries 1 do
+ assert_equal 2, subscriber.subscriptions.size
+ end
+
+ assert_equal Subscription.where(subscriber_id: "webster132"), subscriber.subscriptions
+ end
+
+ def test_association_primary_key_on_new_record_should_fetch_with_query
+ author = Author.new(name: "David")
+ assert_not_predicate author.essays, :loaded?
+
+ assert_queries 1 do
+ assert_equal 1, author.essays.size
+ end
+
+ assert_equal Essay.where(writer_id: "David"), author.essays
+ end
+
+ def test_has_many_custom_primary_key
+ david = authors(:david)
+ assert_equal Essay.where(writer_id: "David"), david.essays
+ end
+
+ def test_ids_on_unloaded_association_with_custom_primary_key
+ david = people(:david)
+ assert_equal Essay.where(writer_id: "David").pluck(:id), david.essay_ids
+ end
+
+ def test_ids_on_loaded_association_with_custom_primary_key
+ david = people(:david)
+ david.essays.load
+ assert_equal Essay.where(writer_id: "David").pluck(:id), david.essay_ids
+ end
+
+ def test_has_many_assignment_with_custom_primary_key
+ david = people(:david)
+
+ assert_equal ["A Modest Proposal"], david.essays.map(&:name)
+ david.essays = [Essay.create!(name: "Remote Work")]
+ assert_equal ["Remote Work"], david.essays.map(&:name)
+ end
+
+ def test_blank_custom_primary_key_on_new_record_should_not_run_queries
+ author = Author.new
+ assert_not_predicate author.essays, :loaded?
+
+ assert_queries 0 do
+ assert_equal 0, author.essays.size
+ end
+ end
+end
+
+class HasManyAssociationsTest < ActiveRecord::TestCase
+ fixtures :accounts, :categories, :companies, :developers, :projects,
+ :developers_projects, :topics, :authors, :author_addresses, :comments,
+ :posts, :readers, :taggings, :cars, :jobs, :tags,
+ :categorizations, :zines, :interests
+
+ def setup
+ Client.destroyed_client_ids.clear
+ end
+
+ def test_sti_subselect_count
+ tag = Tag.first
+ len = Post.tagged_with(tag.id).limit(10).size
+ assert_operator len, :>, 0
+ end
+
+ def test_anonymous_has_many
+ developer = Class.new(ActiveRecord::Base) {
+ self.table_name = "developers"
+ dev = self
+
+ developer_project = Class.new(ActiveRecord::Base) {
+ self.table_name = "developers_projects"
+ belongs_to :developer, anonymous_class: dev
+ }
+ has_many :developer_projects, anonymous_class: developer_project, foreign_key: "developer_id"
+ }
+ dev = developer.first
+ named = Developer.find(dev.id)
+ assert_operator dev.developer_projects.count, :>, 0
+ assert_equal named.projects.map(&:id).sort,
+ dev.developer_projects.map(&:project_id).sort
+ end
+
+ def test_default_scope_on_relations_is_not_cached
+ counter = 0
+ posts = Class.new(ActiveRecord::Base) {
+ self.table_name = "posts"
+ self.inheritance_column = "not_there"
+ post = self
+
+ comments = Class.new(ActiveRecord::Base) {
+ self.table_name = "comments"
+ self.inheritance_column = "not_there"
+ belongs_to :post, anonymous_class: post
+ default_scope -> {
+ counter += 1
+ where("id = :inc", inc: counter)
+ }
+ }
+ has_many :comments, anonymous_class: comments, foreign_key: "post_id"
+ }
+ assert_equal 0, counter
+ post = posts.first
+ assert_equal 0, counter
+ sql = capture_sql { post.comments.to_a }
+ post.comments.reset
+ assert_not_equal sql, capture_sql { post.comments.to_a }
+ end
+
+ def test_has_many_build_with_options
+ college = College.create(name: "UFMT")
+ Student.create(active: true, college_id: college.id, name: "Sarah")
+
+ assert_equal college.students, Student.where(active: true, college_id: college.id)
+ end
+
+ def test_add_record_to_collection_should_change_its_updated_at
+ ship = Ship.create(name: "dauntless")
+ part = ShipPart.create(name: "cockpit")
+ updated_at = part.updated_at
+
+ travel(1.second) do
+ ship.parts << part
+ end
+
+ assert_equal part.ship, ship
+ assert_not_equal part.updated_at, updated_at
+ end
+
+ def test_clear_collection_should_not_change_updated_at
+ # GH#17161: .clear calls delete_all (and returns the association),
+ # which is intended to not touch associated objects's updated_at field
+ ship = Ship.create(name: "dauntless")
+ part = ShipPart.create(name: "cockpit", ship_id: ship.id)
+
+ ship.parts.clear
+ part.reload
+
+ assert_nil part.ship
+ assert_not_predicate part, :updated_at_changed?
+ end
+
+ def test_create_from_association_should_respect_default_scope
+ car = Car.create(name: "honda")
+ assert_equal "honda", car.name
+
+ bulb = Bulb.create
+ assert_equal "defaulty", bulb.name
+
+ bulb = car.bulbs.build
+ assert_equal "defaulty", bulb.name
+
+ bulb = car.bulbs.create
+ assert_equal "defaulty", bulb.name
+ end
+
+ def test_build_and_create_from_association_should_respect_passed_attributes_over_default_scope
+ car = Car.create(name: "honda")
+
+ bulb = car.bulbs.build(name: "exotic")
+ assert_equal "exotic", bulb.name
+
+ bulb = car.bulbs.create(name: "exotic")
+ assert_equal "exotic", bulb.name
+
+ bulb = car.awesome_bulbs.build(frickinawesome: false)
+ assert_equal false, bulb.frickinawesome
+
+ bulb = car.awesome_bulbs.create(frickinawesome: false)
+ assert_equal false, bulb.frickinawesome
+ end
+
+ def test_build_from_association_should_respect_scope
+ author = Author.new
+
+ post = author.thinking_posts.build
+ assert_equal "So I was thinking", post.title
+ end
+
+ def test_create_from_association_with_nil_values_should_work
+ car = Car.create(name: "honda")
+
+ bulb = car.bulbs.new(nil)
+ assert_equal "defaulty", bulb.name
+
+ bulb = car.bulbs.build(nil)
+ assert_equal "defaulty", bulb.name
+
+ bulb = car.bulbs.create(nil)
+ assert_equal "defaulty", bulb.name
+ end
+
+ def test_build_from_association_sets_inverse_instance
+ car = Car.new(name: "honda")
+
+ bulb = car.bulbs.build
+ assert_equal car, bulb.car
+ end
+
+ def test_do_not_call_callbacks_for_delete_all
+ car = Car.create(name: "honda")
+ car.funky_bulbs.create!
+ assert_equal 1, car.funky_bulbs.count
+ assert_nothing_raised { car.reload.funky_bulbs.delete_all }
+ assert_equal 0, car.funky_bulbs.count, "bulbs should have been deleted using :delete_all strategy"
+ end
+
+ def test_delete_all_on_association_is_the_same_as_not_loaded
+ author = authors :david
+ author.thinking_posts.create!(body: "test")
+ author.reload
+ expected_sql = capture_sql { author.thinking_posts.delete_all }
+
+ author.thinking_posts.create!(body: "test")
+ author.reload
+ author.thinking_posts.inspect
+ loaded_sql = capture_sql { author.thinking_posts.delete_all }
+ assert_equal(expected_sql, loaded_sql)
+ end
+
+ def test_delete_all_on_association_with_nil_dependency_is_the_same_as_not_loaded
+ author = authors :david
+ author.posts.create!(title: "test", body: "body")
+ author.reload
+ expected_sql = capture_sql { author.posts.delete_all }
+
+ author.posts.create!(title: "test", body: "body")
+ author.reload
+ author.posts.to_a
+ loaded_sql = capture_sql { author.posts.delete_all }
+ assert_equal(expected_sql, loaded_sql)
+ end
+
+ def test_building_the_associated_object_with_implicit_sti_base_class
+ firm = DependentFirm.new
+ company = firm.companies.build
+ assert_kind_of Company, company, "Expected #{company.class} to be a Company"
+ end
+
+ def test_building_the_associated_object_with_explicit_sti_base_class
+ firm = DependentFirm.new
+ company = firm.companies.build(type: "Company")
+ assert_kind_of Company, company, "Expected #{company.class} to be a Company"
+ end
+
+ def test_building_the_associated_object_with_sti_subclass
+ firm = DependentFirm.new
+ company = firm.companies.build(type: "Client")
+ assert_kind_of Client, company, "Expected #{company.class} to be a Client"
+ end
+
+ def test_building_the_associated_object_with_an_invalid_type
+ firm = DependentFirm.new
+ assert_raise(ActiveRecord::SubclassNotFound) { firm.companies.build(type: "Invalid") }
+ end
+
+ def test_building_the_associated_object_with_an_unrelated_type
+ firm = DependentFirm.new
+ assert_raise(ActiveRecord::SubclassNotFound) { firm.companies.build(type: "Account") }
+ end
+
+ test "building the association with an array" do
+ speedometer = Speedometer.new(speedometer_id: "a")
+ data = [{ name: "first" }, { name: "second" }]
+ speedometer.minivans.build(data)
+
+ assert_equal 2, speedometer.minivans.size
+ assert speedometer.save
+ assert_equal ["first", "second"], speedometer.reload.minivans.map(&:name)
+ end
+
+ def test_association_keys_bypass_attribute_protection
+ car = Car.create(name: "honda")
+
+ bulb = car.bulbs.new
+ assert_equal car.id, bulb.car_id
+
+ bulb = car.bulbs.new car_id: car.id + 1
+ assert_equal car.id, bulb.car_id
+
+ bulb = car.bulbs.build
+ assert_equal car.id, bulb.car_id
+
+ bulb = car.bulbs.build car_id: car.id + 1
+ assert_equal car.id, bulb.car_id
+
+ bulb = car.bulbs.create
+ assert_equal car.id, bulb.car_id
+
+ bulb = car.bulbs.create car_id: car.id + 1
+ assert_equal car.id, bulb.car_id
+ end
+
+ def test_association_protect_foreign_key
+ invoice = Invoice.create
+
+ line_item = invoice.line_items.new
+ assert_equal invoice.id, line_item.invoice_id
+
+ line_item = invoice.line_items.new invoice_id: invoice.id + 1
+ assert_equal invoice.id, line_item.invoice_id
+
+ line_item = invoice.line_items.build
+ assert_equal invoice.id, line_item.invoice_id
+
+ line_item = invoice.line_items.build invoice_id: invoice.id + 1
+ assert_equal invoice.id, line_item.invoice_id
+
+ line_item = invoice.line_items.create
+ assert_equal invoice.id, line_item.invoice_id
+
+ line_item = invoice.line_items.create invoice_id: invoice.id + 1
+ assert_equal invoice.id, line_item.invoice_id
+ end
+
+ # When creating objects on the association, we must not do it within a scope (even though it
+ # would be convenient), because this would cause that scope to be applied to any callbacks etc.
+ def test_build_and_create_should_not_happen_within_scope
+ car = cars(:honda)
+ scope = car.foo_bulbs.where_values_hash
+
+ bulb = car.foo_bulbs.build
+ assert_not_equal scope, bulb.scope_after_initialize.where_values_hash
+
+ bulb = car.foo_bulbs.create
+ assert_not_equal scope, bulb.scope_after_initialize.where_values_hash
+
+ bulb = car.foo_bulbs.create!
+ assert_not_equal scope, bulb.scope_after_initialize.where_values_hash
+ end
+
+ def test_no_sql_should_be_fired_if_association_already_loaded
+ Car.create(name: "honda")
+ bulbs = Car.first.bulbs
+ bulbs.to_a # to load all instances of bulbs
+
+ assert_no_queries do
+ bulbs.first()
+ end
+
+ assert_no_queries do
+ bulbs.second()
+ end
+
+ assert_no_queries do
+ bulbs.third()
+ end
+
+ assert_no_queries do
+ bulbs.fourth()
+ end
+
+ assert_no_queries do
+ bulbs.fifth()
+ end
+
+ assert_no_queries do
+ bulbs.forty_two()
+ end
+
+ assert_no_queries do
+ bulbs.third_to_last()
+ end
+
+ assert_no_queries do
+ bulbs.second_to_last()
+ end
+
+ assert_no_queries do
+ bulbs.last()
+ end
+ end
+
+ def test_finder_method_with_dirty_target
+ company = companies(:first_firm)
+ new_clients = []
+ assert_no_queries(ignore_none: false) do
+ new_clients << company.clients_of_firm.build(name: "Another Client")
+ new_clients << company.clients_of_firm.build(name: "Another Client II")
+ new_clients << company.clients_of_firm.build(name: "Another Client III")
+ end
+
+ assert_not_predicate company.clients_of_firm, :loaded?
+ assert_queries(1) do
+ assert_same new_clients[0], company.clients_of_firm.third
+ assert_same new_clients[1], company.clients_of_firm.fourth
+ assert_same new_clients[2], company.clients_of_firm.fifth
+ assert_same new_clients[0], company.clients_of_firm.third_to_last
+ assert_same new_clients[1], company.clients_of_firm.second_to_last
+ assert_same new_clients[2], company.clients_of_firm.last
+ end
+ end
+
+ def test_finder_bang_method_with_dirty_target
+ company = companies(:first_firm)
+ new_clients = []
+ assert_no_queries(ignore_none: false) do
+ new_clients << company.clients_of_firm.build(name: "Another Client")
+ new_clients << company.clients_of_firm.build(name: "Another Client II")
+ new_clients << company.clients_of_firm.build(name: "Another Client III")
+ end
+
+ assert_not_predicate company.clients_of_firm, :loaded?
+ assert_queries(1) do
+ assert_same new_clients[0], company.clients_of_firm.third!
+ assert_same new_clients[1], company.clients_of_firm.fourth!
+ assert_same new_clients[2], company.clients_of_firm.fifth!
+ assert_same new_clients[0], company.clients_of_firm.third_to_last!
+ assert_same new_clients[1], company.clients_of_firm.second_to_last!
+ assert_same new_clients[2], company.clients_of_firm.last!
+ end
+ end
+
+ def test_create_resets_cached_counters
+ Reader.delete_all
+
+ person = Person.create!(first_name: "tenderlove")
+
+ post = Post.first
+
+ assert_equal [], person.readers
+ assert_nil person.readers.find_by_post_id(post.id)
+
+ person.readers.create(post_id: post.id)
+
+ assert_equal 1, person.readers.count
+ assert_equal 1, person.readers.length
+ assert_equal post, person.readers.first.post
+ assert_equal person, person.readers.first.person
+ end
+
+ def test_update_all_respects_association_scope
+ person = Person.new
+ person.first_name = "Naruto"
+ person.references << Reference.new
+ person.id = 10
+ person.references
+ person.save!
+ assert_equal 1, person.references.update_all(favourite: true)
+ end
+
+ def test_exists_respects_association_scope
+ person = Person.new
+ person.first_name = "Sasuke"
+ person.references << Reference.new
+ person.id = 10
+ person.references
+ person.save!
+ assert_predicate person.references, :exists?
+ end
+
+ def test_counting_with_counter_sql
+ assert_equal 3, Firm.first.clients.count
+ end
+
+ def test_counting
+ assert_equal 3, Firm.first.plain_clients.count
+ end
+
+ def test_counting_with_single_hash
+ assert_equal 1, Firm.first.plain_clients.where(name: "Microsoft").count
+ end
+
+ def test_counting_with_column_name_and_hash
+ assert_equal 3, Firm.first.plain_clients.count(:name)
+ end
+
+ def test_counting_with_association_limit
+ firm = companies(:first_firm)
+ assert_equal firm.limited_clients.length, firm.limited_clients.size
+ assert_equal firm.limited_clients.length, firm.limited_clients.count
+ end
+
+ def test_finding
+ assert_equal 3, Firm.first.clients.length
+ end
+
+ def test_finding_array_compatibility
+ assert_equal 3, Firm.order(:id).find { |f| f.id > 0 }.clients.length
+ end
+
+ def test_find_many_with_merged_options
+ assert_equal 1, companies(:first_firm).limited_clients.size
+ assert_equal 1, companies(:first_firm).limited_clients.to_a.size
+ assert_equal 3, companies(:first_firm).limited_clients.limit(nil).to_a.size
+ end
+
+ def test_find_should_append_to_association_order
+ ordered_clients = companies(:first_firm).clients_sorted_desc.order("companies.id")
+ assert_equal ["id DESC", "companies.id"], ordered_clients.order_values
+ end
+
+ def test_dynamic_find_should_respect_association_order
+ assert_equal companies(:another_first_firm_client), companies(:first_firm).clients_sorted_desc.where("type = 'Client'").first
+ assert_equal companies(:another_first_firm_client), companies(:first_firm).clients_sorted_desc.find_by_type("Client")
+ end
+
+ def test_taking
+ posts(:other_by_bob).destroy
+ assert_equal posts(:misc_by_bob), authors(:bob).posts.take
+ assert_equal posts(:misc_by_bob), authors(:bob).posts.take!
+ authors(:bob).posts.to_a
+ assert_equal posts(:misc_by_bob), authors(:bob).posts.take
+ assert_equal posts(:misc_by_bob), authors(:bob).posts.take!
+ end
+
+ def test_taking_not_found
+ authors(:bob).posts.delete_all
+ assert_raise(ActiveRecord::RecordNotFound) { authors(:bob).posts.take! }
+ authors(:bob).posts.to_a
+ assert_raise(ActiveRecord::RecordNotFound) { authors(:bob).posts.take! }
+ end
+
+ def test_taking_with_a_number
+ klass = Class.new(Author) do
+ has_many :posts, -> { order(:id) }
+
+ def self.name
+ "Author"
+ end
+ end
+
+ # taking from unloaded Relation
+ bob = klass.find(authors(:bob).id)
+ new_post = bob.posts.build
+ assert_not_predicate bob.posts, :loaded?
+ assert_equal [posts(:misc_by_bob)], bob.posts.take(1)
+ assert_equal [posts(:misc_by_bob), posts(:other_by_bob)], bob.posts.take(2)
+ assert_equal [posts(:misc_by_bob), posts(:other_by_bob), new_post], bob.posts.take(3)
+
+ # taking from loaded Relation
+ bob.posts.load
+ assert_predicate bob.posts, :loaded?
+ assert_equal [posts(:misc_by_bob)], bob.posts.take(1)
+ assert_equal [posts(:misc_by_bob), posts(:other_by_bob)], bob.posts.take(2)
+ assert_equal [posts(:misc_by_bob), posts(:other_by_bob), new_post], bob.posts.take(3)
+ end
+
+ def test_taking_with_inverse_of
+ interests(:woodsmanship).destroy
+ interests(:survival).destroy
+
+ zine = zines(:going_out)
+ interest = zine.interests.take
+ assert_equal interests(:hunting), interest
+ assert_same zine, interest.zine
+ end
+
+ def test_cant_save_has_many_readonly_association
+ authors(:david).readonly_comments.each { |c| assert_raise(ActiveRecord::ReadOnlyRecord) { c.save! } }
+ authors(:david).readonly_comments.each { |c| assert c.readonly? }
+ end
+
+ def test_finding_default_orders
+ assert_equal "Summit", Firm.first.clients.first.name
+ end
+
+ def test_finding_with_different_class_name_and_order
+ assert_equal "Apex", Firm.first.clients_sorted_desc.first.name
+ end
+
+ def test_finding_with_foreign_key
+ assert_equal "Microsoft", Firm.first.clients_of_firm.first.name
+ end
+
+ def test_finding_with_condition
+ assert_equal "Microsoft", Firm.first.clients_like_ms.first.name
+ end
+
+ def test_finding_with_condition_hash
+ assert_equal "Microsoft", Firm.first.clients_like_ms_with_hash_conditions.first.name
+ end
+
+ def test_finding_using_primary_key
+ assert_equal "Summit", Firm.first.clients_using_primary_key.first.name
+ end
+
+ def test_update_all_on_association_accessed_before_save
+ firm = Firm.new(name: "Firm")
+ firm.clients << Client.first
+ firm.save!
+ assert_equal firm.clients.count, firm.clients.update_all(description: "Great!")
+ end
+
+ def test_update_all_on_association_accessed_before_save_with_explicit_foreign_key
+ firm = Firm.new(name: "Firm", id: 100)
+ firm.clients << Client.first
+ firm.save!
+ assert_equal firm.clients.count, firm.clients.update_all(description: "Great!")
+ end
+
+ def test_belongs_to_sanity
+ c = Client.new
+ assert_nil c.firm, "belongs_to failed sanity check on new object"
+ end
+
+ def test_find_ids
+ firm = Firm.first
+
+ assert_raise(ActiveRecord::RecordNotFound) { firm.clients.find }
+
+ client = firm.clients.find(2)
+ assert_kind_of Client, client
+
+ client_ary = firm.clients.find([2])
+ assert_kind_of Array, client_ary
+ assert_equal client, client_ary.first
+
+ client_ary = firm.clients.find(2, 3)
+ assert_kind_of Array, client_ary
+ assert_equal 2, client_ary.size
+ assert_equal client, client_ary.first
+
+ assert_raise(ActiveRecord::RecordNotFound) { firm.clients.find(2, 99) }
+ end
+
+ def test_find_one_message_on_primary_key
+ firm = Firm.first
+
+ e = assert_raises(ActiveRecord::RecordNotFound) do
+ firm.clients.find(0)
+ end
+ assert_equal 0, e.id
+ assert_equal "id", e.primary_key
+ assert_equal "Client", e.model
+ assert_match (/\ACouldn't find Client with 'id'=0/), e.message
+ end
+
+ def test_find_ids_and_inverse_of
+ force_signal37_to_load_all_clients_of_firm
+
+ assert_predicate companies(:first_firm).clients_of_firm, :loaded?
+
+ firm = companies(:first_firm)
+ client = firm.clients_of_firm.find(3)
+ assert_kind_of Client, client
+
+ client_ary = firm.clients_of_firm.find([3])
+ assert_kind_of Array, client_ary
+ assert_equal client, client_ary.first
+ end
+
+ def test_find_all
+ firm = Firm.first
+ assert_equal 3, firm.clients.where("#{QUOTED_TYPE} = 'Client'").to_a.length
+ assert_equal 1, firm.clients.where("name = 'Summit'").to_a.length
+ end
+
+ def test_find_each
+ firm = companies(:first_firm)
+
+ assert_not_predicate firm.clients, :loaded?
+
+ assert_queries(4) do
+ firm.clients.find_each(batch_size: 1) { |c| assert_equal firm.id, c.firm_id }
+ end
+
+ assert_not_predicate firm.clients, :loaded?
+ end
+
+ def test_find_each_with_conditions
+ firm = companies(:first_firm)
+
+ assert_queries(2) do
+ firm.clients.where(name: "Microsoft").find_each(batch_size: 1) do |c|
+ assert_equal firm.id, c.firm_id
+ assert_equal "Microsoft", c.name
+ end
+ end
+
+ assert_not_predicate firm.clients, :loaded?
+ end
+
+ def test_find_in_batches
+ firm = companies(:first_firm)
+
+ assert_not_predicate firm.clients, :loaded?
+
+ assert_queries(2) do
+ firm.clients.find_in_batches(batch_size: 2) do |clients|
+ clients.each { |c| assert_equal firm.id, c.firm_id }
+ end
+ end
+
+ assert_not_predicate firm.clients, :loaded?
+ end
+
+ def test_find_all_sanitized
+ firm = Firm.first
+ summit = firm.clients.where("name = 'Summit'").to_a
+ assert_equal summit, firm.clients.where("name = ?", "Summit").to_a
+ assert_equal summit, firm.clients.where("name = :name", name: "Summit").to_a
+ end
+
+ def test_find_first
+ firm = Firm.first
+ client2 = Client.find(2)
+ assert_equal firm.clients.first, firm.clients.order("id").first
+ assert_equal client2, firm.clients.where("#{QUOTED_TYPE} = 'Client'").order("id").first
+ end
+
+ def test_find_first_sanitized
+ firm = Firm.first
+ client2 = Client.find(2)
+ assert_equal client2, firm.clients.where("#{QUOTED_TYPE} = ?", "Client").first
+ assert_equal client2, firm.clients.where("#{QUOTED_TYPE} = :type", type: "Client").first
+ end
+
+ def test_find_first_after_reset_scope
+ firm = Firm.first
+ collection = firm.clients
+
+ original_object = collection.first
+ assert_same original_object, collection.first, "Expected second call to #first to cache the same object"
+
+ # It should return a different object, since the association has been reloaded
+ assert_not_same original_object, firm.clients.first, "Expected #first to return a new object"
+ end
+
+ def test_find_first_after_reset
+ firm = Firm.first
+ collection = firm.clients
+
+ original_object = collection.first
+ assert_same original_object, collection.first, "Expected second call to #first to cache the same object"
+ collection.reset
+
+ # It should return a different object, since the association has been reloaded
+ assert_not_same original_object, collection.first, "Expected #first after #reset to return a new object"
+ end
+
+ def test_find_first_after_reload
+ firm = Firm.first
+ collection = firm.clients
+
+ original_object = collection.first
+ assert_same original_object, collection.first, "Expected second call to #first to cache the same object"
+ collection.reload
+
+ # It should return a different object, since the association has been reloaded
+ assert_not_same original_object, collection.first, "Expected #first after #reload to return a new object"
+ end
+
+ def test_find_all_with_include_and_conditions
+ assert_nothing_raised do
+ Developer.all.merge!(joins: :audit_logs, where: { "audit_logs.message" => nil, :name => "Smith" }).to_a
+ end
+ end
+
+ def test_find_in_collection
+ assert_equal Client.find(2).name, companies(:first_firm).clients.find(2).name
+ assert_raise(ActiveRecord::RecordNotFound) { companies(:first_firm).clients.find(6) }
+ end
+
+ def test_find_grouped
+ all_clients_of_firm1 = Client.all.merge!(where: "firm_id = 1").to_a
+ grouped_clients_of_firm1 = Client.all.merge!(where: "firm_id = 1", group: "firm_id", select: "firm_id, count(id) as clients_count").to_a
+ assert_equal 3, all_clients_of_firm1.size
+ assert_equal 1, grouped_clients_of_firm1.size
+ end
+
+ def test_find_scoped_grouped
+ assert_equal 1, companies(:first_firm).clients_grouped_by_firm_id.size
+ assert_equal 1, companies(:first_firm).clients_grouped_by_firm_id.length
+ assert_equal 3, companies(:first_firm).clients_grouped_by_name.size
+ assert_equal 3, companies(:first_firm).clients_grouped_by_name.length
+ end
+
+ def test_find_scoped_grouped_having
+ assert_equal 2, authors(:david).popular_grouped_posts.length
+ assert_equal 0, authors(:mary).popular_grouped_posts.length
+ end
+
+ def test_default_select
+ assert_equal Comment.column_names.sort, posts(:welcome).comments.first.attributes.keys.sort
+ end
+
+ def test_select_query_method
+ assert_equal ["id", "body"], posts(:welcome).comments.select(:id, :body).first.attributes.keys
+ end
+
+ def test_select_with_block
+ assert_equal [1], posts(:welcome).comments.select { |c| c.id == 1 }.map(&:id)
+ end
+
+ def test_select_with_block_and_dirty_target
+ assert_equal 2, posts(:welcome).comments.select { true }.size
+ posts(:welcome).comments.build
+ assert_equal 3, posts(:welcome).comments.select { true }.size
+ end
+
+ def test_select_without_foreign_key
+ assert_equal companies(:first_firm).accounts.first.credit_limit, companies(:first_firm).accounts.select(:credit_limit).first.credit_limit
+ end
+
+ def test_adding
+ force_signal37_to_load_all_clients_of_firm
+
+ assert_predicate companies(:first_firm).clients_of_firm, :loaded?
+
+ natural = Client.new("name" => "Natural Company")
+ companies(:first_firm).clients_of_firm << natural
+ assert_equal 3, companies(:first_firm).clients_of_firm.size # checking via the collection
+ assert_equal 3, companies(:first_firm).clients_of_firm.reload.size # checking using the db
+ assert_equal natural, companies(:first_firm).clients_of_firm.last
+ end
+
+ def test_adding_using_create
+ first_firm = companies(:first_firm)
+ assert_equal 3, first_firm.plain_clients.size
+ first_firm.plain_clients.create(name: "Natural Company")
+ assert_equal 4, first_firm.plain_clients.length
+ assert_equal 4, first_firm.plain_clients.size
+ end
+
+ def test_create_with_bang_on_has_many_when_parent_is_new_raises
+ error = assert_raise(ActiveRecord::RecordNotSaved) do
+ firm = Firm.new
+ firm.plain_clients.create! name: "Whoever"
+ end
+
+ assert_equal "You cannot call create unless the parent is saved", error.message
+ end
+
+ def test_regular_create_on_has_many_when_parent_is_new_raises
+ error = assert_raise(ActiveRecord::RecordNotSaved) do
+ firm = Firm.new
+ firm.plain_clients.create name: "Whoever"
+ end
+
+ assert_equal "You cannot call create unless the parent is saved", error.message
+ end
+
+ def test_create_with_bang_on_has_many_raises_when_record_not_saved
+ assert_raise(ActiveRecord::RecordInvalid) do
+ firm = Firm.first
+ firm.plain_clients.create!
+ end
+ end
+
+ def test_create_with_bang_on_habtm_when_parent_is_new_raises
+ error = assert_raise(ActiveRecord::RecordNotSaved) do
+ Developer.new("name" => "Aredridel").projects.create!
+ end
+
+ assert_equal "You cannot call create unless the parent is saved", error.message
+ end
+
+ def test_adding_a_mismatch_class
+ assert_raise(ActiveRecord::AssociationTypeMismatch) { companies(:first_firm).clients_of_firm << nil }
+ assert_raise(ActiveRecord::AssociationTypeMismatch) { companies(:first_firm).clients_of_firm << 1 }
+ assert_raise(ActiveRecord::AssociationTypeMismatch) { companies(:first_firm).clients_of_firm << Topic.find(1) }
+ end
+
+ def test_adding_a_collection
+ force_signal37_to_load_all_clients_of_firm
+
+ assert_predicate companies(:first_firm).clients_of_firm, :loaded?
+
+ companies(:first_firm).clients_of_firm.concat([Client.new("name" => "Natural Company"), Client.new("name" => "Apple")])
+ assert_equal 4, companies(:first_firm).clients_of_firm.size
+ assert_equal 4, companies(:first_firm).clients_of_firm.reload.size
+ end
+
+ def test_transactions_when_adding_to_persisted
+ good = Client.new(name: "Good")
+ bad = Client.new(name: "Bad", raise_on_save: true)
+
+ begin
+ companies(:first_firm).clients_of_firm.concat(good, bad)
+ rescue Client::RaisedOnSave
+ end
+
+ assert_not_includes companies(:first_firm).clients_of_firm.reload, good
+ end
+
+ def test_transactions_when_adding_to_new_record
+ assert_no_queries(ignore_none: false) do
+ firm = Firm.new
+ firm.clients_of_firm.concat(Client.new("name" => "Natural Company"))
+ end
+ end
+
+ def test_inverse_on_before_validate
+ firm = companies(:first_firm)
+ assert_queries(1) do
+ firm.clients_of_firm << Client.new("name" => "Natural Company")
+ end
+ end
+
+ def test_new_aliased_to_build
+ company = companies(:first_firm)
+ new_client = assert_no_queries(ignore_none: false) { company.clients_of_firm.new("name" => "Another Client") }
+ assert_not_predicate company.clients_of_firm, :loaded?
+
+ assert_equal "Another Client", new_client.name
+ assert_not_predicate new_client, :persisted?
+ assert_equal new_client, company.clients_of_firm.last
+ end
+
+ def test_build
+ company = companies(:first_firm)
+ new_client = assert_no_queries(ignore_none: false) { company.clients_of_firm.build("name" => "Another Client") }
+ assert_not_predicate company.clients_of_firm, :loaded?
+
+ assert_equal "Another Client", new_client.name
+ assert_not_predicate new_client, :persisted?
+ assert_equal new_client, company.clients_of_firm.last
+ end
+
+ def test_collection_size_after_building
+ company = companies(:first_firm) # company already has one client
+ company.clients_of_firm.build("name" => "Another Client")
+ company.clients_of_firm.build("name" => "Yet Another Client")
+ assert_equal 4, company.clients_of_firm.size
+ assert_equal 4, company.clients_of_firm.uniq.size
+ end
+
+ def test_collection_not_empty_after_building
+ company = companies(:first_firm)
+ assert_empty company.contracts
+ company.contracts.build
+ assert_not_empty company.contracts
+ end
+
+ def test_collection_size_twice_for_regressions
+ post = posts(:thinking)
+ assert_equal 0, post.readers.size
+ # This test needs a post that has no readers, we assert it to ensure it holds,
+ # but need to reload the post because the very call to #size hides the bug.
+ post.reload
+ post.readers.build
+ size1 = post.readers.size
+ size2 = post.readers.size
+ assert_equal size1, size2
+ end
+
+ def test_build_many
+ company = companies(:first_firm)
+ new_clients = assert_no_queries(ignore_none: false) { company.clients_of_firm.build([{ "name" => "Another Client" }, { "name" => "Another Client II" }]) }
+ assert_equal 2, new_clients.size
+ end
+
+ def test_build_followed_by_save_does_not_load_target
+ companies(:first_firm).clients_of_firm.build("name" => "Another Client")
+ assert companies(:first_firm).save
+ assert_not_predicate companies(:first_firm).clients_of_firm, :loaded?
+ end
+
+ def test_build_without_loading_association
+ first_topic = topics(:first)
+ Reply.column_names
+
+ assert_equal 1, first_topic.replies.length
+
+ assert_no_queries do
+ first_topic.replies.build(title: "Not saved", content: "Superstars")
+ assert_equal 2, first_topic.replies.size
+ end
+
+ assert_equal 2, first_topic.replies.to_ary.size
+ end
+
+ def test_build_via_block
+ company = companies(:first_firm)
+ new_client = assert_no_queries(ignore_none: false) { company.clients_of_firm.build { |client| client.name = "Another Client" } }
+ assert_not_predicate company.clients_of_firm, :loaded?
+
+ assert_equal "Another Client", new_client.name
+ assert_not_predicate new_client, :persisted?
+ assert_equal new_client, company.clients_of_firm.last
+ end
+
+ def test_build_many_via_block
+ company = companies(:first_firm)
+ new_clients = assert_no_queries(ignore_none: false) do
+ company.clients_of_firm.build([{ "name" => "Another Client" }, { "name" => "Another Client II" }]) do |client|
+ client.name = "changed"
+ end
+ end
+
+ assert_equal 2, new_clients.size
+ assert_equal "changed", new_clients.first.name
+ assert_equal "changed", new_clients.last.name
+ end
+
+ def test_create_without_loading_association
+ first_firm = companies(:first_firm)
+ Firm.column_names
+ Client.column_names
+
+ assert_equal 2, first_firm.clients_of_firm.size
+ first_firm.clients_of_firm.reset
+
+ assert_queries(1) do
+ first_firm.clients_of_firm.create(name: "Superstars")
+ end
+
+ assert_equal 3, first_firm.clients_of_firm.size
+ end
+
+ def test_create
+ force_signal37_to_load_all_clients_of_firm
+
+ assert_predicate companies(:first_firm).clients_of_firm, :loaded?
+
+ new_client = companies(:first_firm).clients_of_firm.create("name" => "Another Client")
+ assert_predicate new_client, :persisted?
+ assert_equal new_client, companies(:first_firm).clients_of_firm.last
+ assert_equal new_client, companies(:first_firm).clients_of_firm.reload.last
+ end
+
+ def test_create_many
+ companies(:first_firm).clients_of_firm.create([{ "name" => "Another Client" }, { "name" => "Another Client II" }])
+ assert_equal 4, companies(:first_firm).clients_of_firm.reload.size
+ end
+
+ def test_create_followed_by_save_does_not_load_target
+ companies(:first_firm).clients_of_firm.create("name" => "Another Client")
+ assert companies(:first_firm).save
+ assert_not_predicate companies(:first_firm).clients_of_firm, :loaded?
+ end
+
+ def test_deleting
+ force_signal37_to_load_all_clients_of_firm
+
+ assert_predicate companies(:first_firm).clients_of_firm, :loaded?
+
+ companies(:first_firm).clients_of_firm.delete(companies(:first_firm).clients_of_firm.first)
+ assert_equal 1, companies(:first_firm).clients_of_firm.size
+ assert_equal 1, companies(:first_firm).clients_of_firm.reload.size
+ end
+
+ def test_deleting_before_save
+ new_firm = Firm.new("name" => "A New Firm, Inc.")
+ new_client = new_firm.clients_of_firm.build("name" => "Another Client")
+ assert_equal 1, new_firm.clients_of_firm.size
+ new_firm.clients_of_firm.delete(new_client)
+ assert_equal 0, new_firm.clients_of_firm.size
+ end
+
+ def test_has_many_without_counter_cache_option
+ # Ship has a conventionally named `treasures_count` column, but the counter_cache
+ # option is not given on the association.
+ ship = Ship.create(name: "Countless", treasures_count: 10)
+
+ assert_not_predicate Ship.reflect_on_association(:treasures), :has_cached_counter?
+
+ # Count should come from sql count() of treasures rather than treasures_count attribute
+ assert_equal ship.treasures.size, 0
+
+ assert_no_difference lambda { ship.reload.treasures_count }, "treasures_count should not be changed" do
+ ship.treasures.create(name: "Gold")
+ end
+
+ assert_no_difference lambda { ship.reload.treasures_count }, "treasures_count should not be changed" do
+ ship.treasures.destroy_all
+ end
+ end
+
+ def test_deleting_updates_counter_cache
+ topic = Topic.order("id ASC").first
+ assert_equal topic.replies.to_a.size, topic.replies_count
+
+ topic.replies.delete(topic.replies.first)
+ topic.reload
+ assert_equal topic.replies.to_a.size, topic.replies_count
+ end
+
+ def test_counter_cache_updates_in_memory_after_concat
+ topic = Topic.create title: "Zoom-zoom-zoom"
+
+ topic.replies << Reply.create(title: "re: zoom", content: "speedy quick!")
+ assert_equal 1, topic.replies_count
+ assert_equal 1, topic.replies.size
+ assert_equal 1, topic.reload.replies.size
+ end
+
+ def test_counter_cache_updates_in_memory_after_create
+ topic = Topic.create title: "Zoom-zoom-zoom"
+
+ topic.replies.create!(title: "re: zoom", content: "speedy quick!")
+ assert_equal 1, topic.replies_count
+ assert_equal 1, topic.replies.size
+ assert_equal 1, topic.reload.replies.size
+ end
+
+ def test_counter_cache_updates_in_memory_after_create_with_array
+ topic = Topic.create title: "Zoom-zoom-zoom"
+
+ topic.replies.create!([
+ { title: "re: zoom", content: "speedy quick!" },
+ { title: "re: zoom 2", content: "OMG lol!" },
+ ])
+ assert_equal 2, topic.replies_count
+ assert_equal 2, topic.replies.size
+ assert_equal 2, topic.reload.replies.size
+ end
+
+ def test_pushing_association_updates_counter_cache
+ topic = Topic.order("id ASC").first
+ reply = Reply.create!
+
+ assert_difference "topic.reload.replies_count", 1 do
+ topic.replies << reply
+ end
+ end
+
+ def test_deleting_updates_counter_cache_without_dependent_option
+ post = posts(:welcome)
+
+ assert_difference "post.reload.tags_count", -1 do
+ post.taggings.delete(post.taggings.first)
+ end
+ end
+
+ def test_deleting_updates_counter_cache_with_dependent_delete_all
+ post = posts(:welcome)
+ post.update_columns(taggings_with_delete_all_count: post.tags_count)
+
+ assert_difference "post.reload.taggings_with_delete_all_count", -1 do
+ post.taggings_with_delete_all.delete(post.taggings_with_delete_all.first)
+ end
+ end
+
+ def test_deleting_updates_counter_cache_with_dependent_destroy
+ post = posts(:welcome)
+ post.update_columns(taggings_with_destroy_count: post.tags_count)
+
+ assert_difference "post.reload.taggings_with_destroy_count", -1 do
+ post.taggings_with_destroy.delete(post.taggings_with_destroy.first)
+ end
+ end
+
+ def test_calling_empty_with_counter_cache
+ post = posts(:welcome)
+ assert_queries(0) do
+ assert_not_empty post.comments
+ end
+ end
+
+ def test_custom_named_counter_cache
+ topic = topics(:first)
+
+ assert_difference "topic.reload.replies_count", -1 do
+ topic.approved_replies.clear
+ end
+ end
+
+ def test_calling_update_on_id_changes_the_counter_cache
+ topic = Topic.order("id ASC").first
+ original_count = topic.replies.to_a.size
+ assert_equal original_count, topic.replies_count
+
+ first_reply = topic.replies.first
+ first_reply.update(parent_id: nil)
+ assert_equal original_count - 1, topic.reload.replies_count
+
+ first_reply.update(parent_id: topic.id)
+ assert_equal original_count, topic.reload.replies_count
+ end
+
+ def test_calling_update_changing_ids_doesnt_change_counter_cache
+ topic1 = Topic.find(1)
+ topic2 = Topic.find(3)
+ original_count1 = topic1.replies.to_a.size
+ original_count2 = topic2.replies.to_a.size
+
+ reply1 = topic1.replies.first
+ reply2 = topic2.replies.first
+
+ reply1.update(parent_id: topic2.id)
+ assert_equal original_count1 - 1, topic1.reload.replies_count
+ assert_equal original_count2 + 1, topic2.reload.replies_count
+
+ reply2.update(parent_id: topic1.id)
+ assert_equal original_count1, topic1.reload.replies_count
+ assert_equal original_count2, topic2.reload.replies_count
+ end
+
+ def test_deleting_a_collection
+ force_signal37_to_load_all_clients_of_firm
+
+ assert_predicate companies(:first_firm).clients_of_firm, :loaded?
+
+ companies(:first_firm).clients_of_firm.create("name" => "Another Client")
+ assert_equal 3, companies(:first_firm).clients_of_firm.size
+ companies(:first_firm).clients_of_firm.delete([companies(:first_firm).clients_of_firm[0], companies(:first_firm).clients_of_firm[1], companies(:first_firm).clients_of_firm[2]])
+ assert_equal 0, companies(:first_firm).clients_of_firm.size
+ assert_equal 0, companies(:first_firm).clients_of_firm.reload.size
+ end
+
+ def test_delete_all
+ force_signal37_to_load_all_clients_of_firm
+
+ assert_predicate companies(:first_firm).clients_of_firm, :loaded?
+
+ companies(:first_firm).dependent_clients_of_firm.create("name" => "Another Client")
+ clients = companies(:first_firm).dependent_clients_of_firm.to_a
+ assert_equal 3, clients.count
+
+ assert_difference "Client.count", -(clients.count) do
+ companies(:first_firm).dependent_clients_of_firm.delete_all
+ end
+ end
+
+ def test_delete_all_with_not_yet_loaded_association_collection
+ force_signal37_to_load_all_clients_of_firm
+
+ assert_predicate companies(:first_firm).clients_of_firm, :loaded?
+
+ companies(:first_firm).clients_of_firm.create("name" => "Another Client")
+ assert_equal 3, companies(:first_firm).clients_of_firm.size
+ companies(:first_firm).clients_of_firm.reset
+ companies(:first_firm).clients_of_firm.delete_all
+ assert_equal 0, companies(:first_firm).clients_of_firm.size
+ assert_equal 0, companies(:first_firm).clients_of_firm.reload.size
+ end
+
+ def test_transaction_when_deleting_persisted
+ good = Client.new(name: "Good")
+ bad = Client.new(name: "Bad", raise_on_destroy: true)
+
+ companies(:first_firm).clients_of_firm = [good, bad]
+
+ begin
+ companies(:first_firm).clients_of_firm.destroy(good, bad)
+ rescue Client::RaisedOnDestroy
+ end
+
+ assert_equal [good, bad], companies(:first_firm).clients_of_firm.reload
+ end
+
+ def test_transaction_when_deleting_new_record
+ assert_no_queries(ignore_none: false) do
+ firm = Firm.new
+ client = Client.new("name" => "New Client")
+ firm.clients_of_firm << client
+ firm.clients_of_firm.destroy(client)
+ end
+ end
+
+ def test_clearing_an_association_collection
+ firm = companies(:first_firm)
+ client_id = firm.clients_of_firm.first.id
+ assert_equal 2, firm.clients_of_firm.size
+
+ firm.clients_of_firm.clear
+
+ assert_equal 0, firm.clients_of_firm.size
+ assert_equal 0, firm.clients_of_firm.reload.size
+ assert_equal [], Client.destroyed_client_ids[firm.id]
+
+ # Should not be destroyed since the association is not dependent.
+ assert_nothing_raised do
+ assert_nil Client.find(client_id).firm
+ end
+ end
+
+ def test_clearing_updates_counter_cache
+ topic = Topic.first
+
+ assert_difference "topic.reload.replies_count", -1 do
+ topic.replies.clear
+ end
+ end
+
+ def test_clearing_updates_counter_cache_when_inverse_counter_cache_is_a_symbol_with_dependent_destroy
+ car = Car.first
+ car.engines.create!
+
+ assert_difference "car.reload.engines_count", -1 do
+ car.engines.clear
+ end
+ end
+
+ def test_clearing_a_dependent_association_collection
+ firm = companies(:first_firm)
+ client_id = firm.dependent_clients_of_firm.first.id
+ assert_equal 2, firm.dependent_clients_of_firm.size
+ assert_equal 1, Client.find_by_id(client_id).client_of
+
+ # :delete_all is called on each client since the dependent options is :destroy
+ firm.dependent_clients_of_firm.clear
+
+ assert_equal 0, firm.dependent_clients_of_firm.size
+ assert_equal 0, firm.dependent_clients_of_firm.reload.size
+ assert_equal [], Client.destroyed_client_ids[firm.id]
+
+ # Should be destroyed since the association is dependent.
+ assert_nil Client.find_by_id(client_id)
+ end
+
+ def test_delete_all_with_option_delete_all
+ firm = companies(:first_firm)
+ client_id = firm.dependent_clients_of_firm.first.id
+ firm.dependent_clients_of_firm.delete_all(:delete_all)
+ assert_nil Client.find_by_id(client_id)
+ end
+
+ def test_delete_all_accepts_limited_parameters
+ firm = companies(:first_firm)
+ assert_raise(ArgumentError) do
+ firm.dependent_clients_of_firm.delete_all(:destroy)
+ end
+ end
+
+ def test_clearing_an_exclusively_dependent_association_collection
+ firm = companies(:first_firm)
+ client_id = firm.exclusively_dependent_clients_of_firm.first.id
+ assert_equal 2, firm.exclusively_dependent_clients_of_firm.size
+
+ assert_equal [], Client.destroyed_client_ids[firm.id]
+
+ # :exclusively_dependent means each client is deleted directly from
+ # the database without looping through them calling destroy.
+ firm.exclusively_dependent_clients_of_firm.clear
+
+ assert_equal 0, firm.exclusively_dependent_clients_of_firm.size
+ assert_equal 0, firm.exclusively_dependent_clients_of_firm.reload.size
+ # no destroy-filters should have been called
+ assert_equal [], Client.destroyed_client_ids[firm.id]
+
+ # Should be destroyed since the association is exclusively dependent.
+ assert_nil Client.find_by_id(client_id)
+ end
+
+ def test_dependent_association_respects_optional_conditions_on_delete
+ firm = companies(:odegy)
+ Client.create(client_of: firm.id, name: "BigShot Inc.")
+ Client.create(client_of: firm.id, name: "SmallTime Inc.")
+ # only one of two clients is included in the association due to the :conditions key
+ assert_equal 2, Client.where(client_of: firm.id).size
+ assert_equal 1, firm.dependent_conditional_clients_of_firm.size
+ firm.destroy
+ # only the correctly associated client should have been deleted
+ assert_equal 1, Client.where(client_of: firm.id).size
+ end
+
+ def test_dependent_association_respects_optional_sanitized_conditions_on_delete
+ firm = companies(:odegy)
+ Client.create(client_of: firm.id, name: "BigShot Inc.")
+ Client.create(client_of: firm.id, name: "SmallTime Inc.")
+ # only one of two clients is included in the association due to the :conditions key
+ assert_equal 2, Client.where(client_of: firm.id).size
+ assert_equal 1, firm.dependent_sanitized_conditional_clients_of_firm.size
+ firm.destroy
+ # only the correctly associated client should have been deleted
+ assert_equal 1, Client.where(client_of: firm.id).size
+ end
+
+ def test_dependent_association_respects_optional_hash_conditions_on_delete
+ firm = companies(:odegy)
+ Client.create(client_of: firm.id, name: "BigShot Inc.")
+ Client.create(client_of: firm.id, name: "SmallTime Inc.")
+ # only one of two clients is included in the association due to the :conditions key
+ assert_equal 2, Client.where(client_of: firm.id).size
+ assert_equal 1, firm.dependent_hash_conditional_clients_of_firm.size
+ firm.destroy
+ # only the correctly associated client should have been deleted
+ assert_equal 1, Client.where(client_of: firm.id).size
+ end
+
+ def test_delete_all_association_with_primary_key_deletes_correct_records
+ firm = Firm.first
+ # break the vanilla firm_id foreign key
+ assert_equal 3, firm.clients.count
+ firm.clients.first.update_columns(firm_id: nil)
+ assert_equal 2, firm.clients.reload.count
+ assert_equal 2, firm.clients_using_primary_key_with_delete_all.count
+ old_record = firm.clients_using_primary_key_with_delete_all.first
+ firm = Firm.first
+ firm.destroy
+ assert_nil Client.find_by_id(old_record.id)
+ end
+
+ def test_creation_respects_hash_condition
+ ms_client = companies(:first_firm).clients_like_ms_with_hash_conditions.build
+
+ assert ms_client.save
+ assert_equal "Microsoft", ms_client.name
+
+ another_ms_client = companies(:first_firm).clients_like_ms_with_hash_conditions.create
+
+ assert_predicate another_ms_client, :persisted?
+ assert_equal "Microsoft", another_ms_client.name
+ end
+
+ def test_clearing_without_initial_access
+ firm = companies(:first_firm)
+
+ firm.clients_of_firm.clear
+
+ assert_equal 0, firm.clients_of_firm.size
+ assert_equal 0, firm.clients_of_firm.reload.size
+ end
+
+ def test_deleting_a_item_which_is_not_in_the_collection
+ force_signal37_to_load_all_clients_of_firm
+
+ assert_predicate companies(:first_firm).clients_of_firm, :loaded?
+
+ summit = Client.find_by_name("Summit")
+ companies(:first_firm).clients_of_firm.delete(summit)
+ assert_equal 2, companies(:first_firm).clients_of_firm.size
+ assert_equal 2, companies(:first_firm).clients_of_firm.reload.size
+ assert_equal 2, summit.client_of
+ end
+
+ def test_deleting_by_integer_id
+ david = Developer.find(1)
+
+ assert_difference "david.projects.count", -1 do
+ assert_equal 1, david.projects.delete(1).size
+ end
+
+ assert_equal 1, david.projects.size
+ end
+
+ def test_deleting_by_string_id
+ david = Developer.find(1)
+
+ assert_difference "david.projects.count", -1 do
+ assert_equal 1, david.projects.delete("1").size
+ end
+
+ assert_equal 1, david.projects.size
+ end
+
+ def test_deleting_self_type_mismatch
+ david = Developer.find(1)
+ david.projects.reload
+ assert_raise(ActiveRecord::AssociationTypeMismatch) { david.projects.delete(Project.find(1).developers) }
+ end
+
+ def test_destroying
+ force_signal37_to_load_all_clients_of_firm
+
+ assert_predicate companies(:first_firm).clients_of_firm, :loaded?
+
+ assert_difference "Client.count", -1 do
+ companies(:first_firm).clients_of_firm.destroy(companies(:first_firm).clients_of_firm.first)
+ end
+
+ assert_equal 1, companies(:first_firm).reload.clients_of_firm.size
+ assert_equal 1, companies(:first_firm).clients_of_firm.reload.size
+ end
+
+ def test_destroying_by_integer_id
+ force_signal37_to_load_all_clients_of_firm
+
+ assert_predicate companies(:first_firm).clients_of_firm, :loaded?
+
+ assert_difference "Client.count", -1 do
+ companies(:first_firm).clients_of_firm.destroy(companies(:first_firm).clients_of_firm.first.id)
+ end
+
+ assert_equal 1, companies(:first_firm).reload.clients_of_firm.size
+ assert_equal 1, companies(:first_firm).clients_of_firm.reload.size
+ end
+
+ def test_destroying_by_string_id
+ force_signal37_to_load_all_clients_of_firm
+
+ assert_predicate companies(:first_firm).clients_of_firm, :loaded?
+
+ assert_difference "Client.count", -1 do
+ companies(:first_firm).clients_of_firm.destroy(companies(:first_firm).clients_of_firm.first.id.to_s)
+ end
+
+ assert_equal 1, companies(:first_firm).reload.clients_of_firm.size
+ assert_equal 1, companies(:first_firm).clients_of_firm.reload.size
+ end
+
+ def test_destroying_a_collection
+ force_signal37_to_load_all_clients_of_firm
+
+ assert_predicate companies(:first_firm).clients_of_firm, :loaded?
+
+ companies(:first_firm).clients_of_firm.create("name" => "Another Client")
+ assert_equal 3, companies(:first_firm).clients_of_firm.size
+
+ assert_difference "Client.count", -2 do
+ companies(:first_firm).clients_of_firm.destroy([companies(:first_firm).clients_of_firm[0], companies(:first_firm).clients_of_firm[1]])
+ end
+
+ assert_equal 1, companies(:first_firm).reload.clients_of_firm.size
+ assert_equal 1, companies(:first_firm).clients_of_firm.reload.size
+ end
+
+ def test_destroy_all
+ force_signal37_to_load_all_clients_of_firm
+
+ assert_predicate companies(:first_firm).clients_of_firm, :loaded?
+
+ clients = companies(:first_firm).clients_of_firm.to_a
+ assert !clients.empty?, "37signals has clients after load"
+ destroyed = companies(:first_firm).clients_of_firm.destroy_all
+ assert_equal clients.sort_by(&:id), destroyed.sort_by(&:id)
+ assert destroyed.all?(&:frozen?), "destroyed clients should be frozen"
+ assert companies(:first_firm).clients_of_firm.empty?, "37signals has no clients after destroy all"
+ assert companies(:first_firm).clients_of_firm.reload.empty?, "37signals has no clients after destroy all and refresh"
+ end
+
+ def test_dependence
+ firm = companies(:first_firm)
+ assert_equal 3, firm.clients.size
+ firm.destroy
+ assert_empty Client.all.merge!(where: "firm_id=#{firm.id}").to_a
+ end
+
+ def test_dependence_for_associations_with_hash_condition
+ david = authors(:david)
+ assert_difference("Post.count", -1) { assert david.destroy }
+ end
+
+ def test_destroy_dependent_when_deleted_from_association
+ firm = Firm.first
+ assert_equal 3, firm.clients.size
+
+ client = firm.clients.first
+ firm.clients.delete(client)
+
+ assert_raise(ActiveRecord::RecordNotFound) { Client.find(client.id) }
+ assert_raise(ActiveRecord::RecordNotFound) { firm.clients.find(client.id) }
+ assert_equal 2, firm.clients.size
+ end
+
+ def test_three_levels_of_dependence
+ topic = Topic.create "title" => "neat and simple"
+ reply = topic.replies.create "title" => "neat and simple", "content" => "still digging it"
+ reply.replies.create "title" => "neat and simple", "content" => "ain't complaining"
+
+ assert_nothing_raised { topic.destroy }
+ end
+
+ def test_dependence_with_transaction_support_on_failure
+ firm = companies(:first_firm)
+ clients = firm.clients
+ assert_equal 3, clients.length
+ clients.last.instance_eval { def overwrite_to_raise() raise "Trigger rollback" end }
+
+ firm.destroy rescue "do nothing"
+
+ assert_equal 3, Client.all.merge!(where: "firm_id=#{firm.id}").to_a.size
+ end
+
+ def test_dependence_on_account
+ num_accounts = Account.count
+ companies(:first_firm).destroy
+ assert_equal num_accounts - 1, Account.count
+ end
+
+ def test_depends_and_nullify
+ num_accounts = Account.count
+
+ core = companies(:rails_core)
+ assert_equal accounts(:rails_core_account), core.account
+ assert_equal companies(:leetsoft, :jadedpixel), core.companies
+ core.destroy
+ assert_nil accounts(:rails_core_account).reload.firm_id
+ assert_nil companies(:leetsoft).reload.client_of
+ assert_nil companies(:jadedpixel).reload.client_of
+
+ assert_equal num_accounts, Account.count
+ end
+
+ def test_restrict_with_exception
+ firm = RestrictedWithExceptionFirm.create!(name: "restrict")
+ firm.companies.create(name: "child")
+
+ assert_not_empty firm.companies
+ assert_raise(ActiveRecord::DeleteRestrictionError) { firm.destroy }
+ assert RestrictedWithExceptionFirm.exists?(name: "restrict")
+ assert firm.companies.exists?(name: "child")
+ end
+
+ def test_restrict_with_error
+ firm = RestrictedWithErrorFirm.create!(name: "restrict")
+ firm.companies.create(name: "child")
+
+ assert_not_empty firm.companies
+
+ firm.destroy
+
+ assert_not_empty firm.errors
+
+ assert_equal "Cannot delete record because dependent companies exist", firm.errors[:base].first
+ assert RestrictedWithErrorFirm.exists?(name: "restrict")
+ assert firm.companies.exists?(name: "child")
+ end
+
+ def test_restrict_with_error_with_locale
+ I18n.backend = I18n::Backend::Simple.new
+ I18n.backend.store_translations "en", activerecord: { attributes: { restricted_with_error_firm: { companies: "client companies" } } }
+ firm = RestrictedWithErrorFirm.create!(name: "restrict")
+ firm.companies.create(name: "child")
+
+ assert_not_empty firm.companies
+
+ firm.destroy
+
+ assert_not_empty firm.errors
+
+ assert_equal "Cannot delete record because dependent client companies exist", firm.errors[:base].first
+ assert RestrictedWithErrorFirm.exists?(name: "restrict")
+ assert firm.companies.exists?(name: "child")
+ ensure
+ I18n.backend.reload!
+ end
+
+ def test_included_in_collection
+ assert_equal true, companies(:first_firm).clients.include?(Client.find(2))
+ end
+
+ def test_included_in_collection_for_new_records
+ client = Client.create(name: "Persisted")
+ assert_nil client.client_of
+ assert_equal false, Firm.new.clients_of_firm.include?(client),
+ "includes a client that does not belong to any firm"
+ end
+
+ def test_adding_array_and_collection
+ assert_nothing_raised { Firm.first.clients + Firm.all.last.clients }
+ end
+
+ def test_replace_with_less
+ firm = Firm.first
+ firm.clients = [companies(:first_client)]
+ assert firm.save, "Could not save firm"
+ firm.reload
+ assert_equal 1, firm.clients.length
+ end
+
+ def test_replace_with_less_and_dependent_nullify
+ num_companies = Company.count
+ companies(:rails_core).companies = []
+ assert_equal num_companies, Company.count
+ end
+
+ def test_replace_with_new
+ firm = Firm.first
+ firm.clients = [companies(:second_client), Client.new("name" => "New Client")]
+ firm.save
+ firm.reload
+ assert_equal 2, firm.clients.length
+ assert_equal false, firm.clients.include?(:first_client)
+ end
+
+ def test_replace_failure
+ firm = companies(:first_firm)
+ account = Account.new
+ orig_accounts = firm.accounts.to_a
+
+ assert_not_predicate account, :valid?
+ assert_not_empty orig_accounts
+ error = assert_raise ActiveRecord::RecordNotSaved do
+ firm.accounts = [account]
+ end
+
+ assert_equal orig_accounts, firm.accounts
+ assert_equal "Failed to replace accounts because one or more of the " \
+ "new records could not be saved.", error.message
+ end
+
+ def test_replace_with_same_content
+ firm = Firm.first
+ firm.clients = []
+ firm.save
+
+ assert_queries(0, ignore_none: true) do
+ firm.clients = []
+ end
+
+ assert_equal [], firm.send("clients=", [])
+ end
+
+ def test_transactions_when_replacing_on_persisted
+ good = Client.new(name: "Good")
+ bad = Client.new(name: "Bad", raise_on_save: true)
+
+ companies(:first_firm).clients_of_firm = [good]
+
+ begin
+ companies(:first_firm).clients_of_firm = [bad]
+ rescue Client::RaisedOnSave
+ end
+
+ assert_equal [good], companies(:first_firm).clients_of_firm.reload
+ end
+
+ def test_transactions_when_replacing_on_new_record
+ assert_no_queries(ignore_none: false) do
+ firm = Firm.new
+ firm.clients_of_firm = [Client.new("name" => "New Client")]
+ end
+ end
+
+ def test_get_ids
+ assert_equal [companies(:first_client).id, companies(:second_client).id, companies(:another_first_firm_client).id], companies(:first_firm).client_ids
+ end
+
+ def test_get_ids_for_loaded_associations
+ company = companies(:first_firm)
+ company.clients.reload
+ assert_queries(0) do
+ company.client_ids
+ company.client_ids
+ end
+ end
+
+ def test_get_ids_for_unloaded_associations_does_not_load_them
+ company = companies(:first_firm)
+ assert_not_predicate company.clients, :loaded?
+ assert_equal [companies(:first_client).id, companies(:second_client).id, companies(:another_first_firm_client).id], company.client_ids
+ assert_not_predicate company.clients, :loaded?
+ end
+
+ def test_counter_cache_on_unloaded_association
+ car = Car.create(name: "My AppliCar")
+ assert_equal car.engines.size, 0
+ end
+
+ def test_get_ids_ignores_include_option
+ assert_equal [readers(:michael_welcome).id], posts(:welcome).readers_with_person_ids
+ end
+
+ def test_get_ids_for_ordered_association
+ assert_equal [companies(:another_first_firm_client).id, companies(:second_client).id, companies(:first_client).id], companies(:first_firm).clients_ordered_by_name_ids
+ end
+
+ def test_get_ids_for_association_on_new_record_does_not_try_to_find_records
+ Company.columns # Load schema information so we don't query below
+ Contract.columns # if running just this test.
+
+ company = Company.new
+ assert_queries(0) do
+ company.contract_ids
+ end
+
+ assert_equal [], company.contract_ids
+ end
+
+ def test_set_ids_for_association_on_new_record_applies_association_correctly
+ contract_a = Contract.create!
+ contract_b = Contract.create!
+ Contract.create! # another contract
+ company = Company.new(name: "Some Company")
+
+ company.contract_ids = [contract_a.id, contract_b.id]
+ assert_equal [contract_a.id, contract_b.id], company.contract_ids
+ assert_equal [contract_a, contract_b], company.contracts
+
+ company.save!
+ assert_equal company, contract_a.reload.company
+ assert_equal company, contract_b.reload.company
+ end
+
+ def test_assign_ids_ignoring_blanks
+ firm = Firm.create!(name: "Apple")
+ firm.client_ids = [companies(:first_client).id, nil, companies(:second_client).id, ""]
+ firm.save!
+
+ assert_equal 2, firm.clients.reload.size
+ assert_equal true, firm.clients.include?(companies(:second_client))
+ end
+
+ def test_get_ids_for_through
+ assert_equal [comments(:eager_other_comment1).id], authors(:mary).comment_ids
+ end
+
+ def test_modifying_a_through_a_has_many_should_raise
+ [
+ lambda { authors(:mary).comment_ids = [comments(:greetings).id, comments(:more_greetings).id] },
+ lambda { authors(:mary).comments = [comments(:greetings), comments(:more_greetings)] },
+ lambda { authors(:mary).comments << Comment.create!(body: "Yay", post_id: 424242) },
+ lambda { authors(:mary).comments.delete(authors(:mary).comments.first) },
+ ].each { |block| assert_raise(ActiveRecord::HasManyThroughCantAssociateThroughHasOneOrManyReflection, &block) }
+ end
+
+ def test_associations_order_should_be_priority_over_throughs_order
+ david = authors(:david)
+ expected = [12, 10, 9, 8, 7, 6, 5, 3, 2, 1]
+ assert_equal expected, david.comments_desc.map(&:id)
+ assert_equal expected, Author.includes(:comments_desc).find(david.id).comments_desc.map(&:id)
+ end
+
+ def test_dynamic_find_should_respect_association_order_for_through
+ assert_equal Comment.find(10), authors(:david).comments_desc.where("comments.type = 'SpecialComment'").first
+ assert_equal Comment.find(10), authors(:david).comments_desc.find_by_type("SpecialComment")
+ end
+
+ def test_has_many_through_respects_hash_conditions
+ assert_equal authors(:david).hello_posts, authors(:david).hello_posts_with_hash_conditions
+ assert_equal authors(:david).hello_post_comments, authors(:david).hello_post_comments_with_hash_conditions
+ end
+
+ def test_include_uses_array_include_after_loaded
+ firm = companies(:first_firm)
+ firm.clients.load_target
+
+ client = firm.clients.first
+
+ assert_no_queries do
+ assert_predicate firm.clients, :loaded?
+ assert_equal true, firm.clients.include?(client)
+ end
+ end
+
+ def test_include_checks_if_record_exists_if_target_not_loaded
+ firm = companies(:first_firm)
+ client = firm.clients.first
+
+ firm.reload
+ assert_not_predicate firm.clients, :loaded?
+ assert_queries(1) do
+ assert_equal true, firm.clients.include?(client)
+ end
+ assert_not_predicate firm.clients, :loaded?
+ end
+
+ def test_include_returns_false_for_non_matching_record_to_verify_scoping
+ firm = companies(:first_firm)
+ client = Client.create!(name: "Not Associated")
+
+ assert_not_predicate firm.clients, :loaded?
+ assert_equal false, firm.clients.include?(client)
+ end
+
+ def test_calling_first_nth_or_last_on_association_should_not_load_association
+ firm = companies(:first_firm)
+ firm.clients.first
+ firm.clients.second
+ firm.clients.last
+ assert_not_predicate firm.clients, :loaded?
+ end
+
+ def test_calling_first_or_last_on_loaded_association_should_not_fetch_with_query
+ firm = companies(:first_firm)
+ firm.clients.load_target
+ assert_predicate firm.clients, :loaded?
+
+ assert_no_queries(ignore_none: false) do
+ firm.clients.first
+ assert_equal 2, firm.clients.first(2).size
+ firm.clients.last
+ assert_equal 2, firm.clients.last(2).size
+ end
+ end
+
+ def test_calling_first_or_last_on_existing_record_with_build_should_load_association
+ firm = companies(:first_firm)
+ firm.clients.build(name: "Foo")
+ assert_not_predicate firm.clients, :loaded?
+
+ assert_queries 1 do
+ firm.clients.first
+ firm.clients.second
+ firm.clients.last
+ end
+
+ assert_predicate firm.clients, :loaded?
+ end
+
+ def test_calling_first_nth_or_last_on_existing_record_with_create_should_not_load_association
+ firm = companies(:first_firm)
+ firm.clients.create(name: "Foo")
+ assert_not_predicate firm.clients, :loaded?
+
+ assert_queries 3 do
+ firm.clients.first
+ firm.clients.second
+ firm.clients.last
+ end
+
+ assert_not_predicate firm.clients, :loaded?
+ end
+
+ def test_calling_first_nth_or_last_on_new_record_should_not_run_queries
+ firm = Firm.new
+
+ assert_no_queries do
+ firm.clients.first
+ firm.clients.second
+ firm.clients.last
+ end
+ end
+
+ def test_calling_first_or_last_with_integer_on_association_should_not_load_association
+ firm = companies(:first_firm)
+ firm.clients.create(name: "Foo")
+ assert_not_predicate firm.clients, :loaded?
+
+ assert_queries 2 do
+ firm.clients.first(2)
+ firm.clients.last(2)
+ end
+
+ assert_not_predicate firm.clients, :loaded?
+ end
+
+ def test_calling_many_should_count_instead_of_loading_association
+ firm = companies(:first_firm)
+ assert_queries(1) do
+ firm.clients.many? # use count query
+ end
+ assert_not_predicate firm.clients, :loaded?
+ end
+
+ def test_calling_many_on_loaded_association_should_not_use_query
+ firm = companies(:first_firm)
+ firm.clients.load # force load
+ assert_no_queries { assert firm.clients.many? }
+ end
+
+ def test_calling_many_should_defer_to_collection_if_using_a_block
+ firm = companies(:first_firm)
+ assert_queries(1) do
+ firm.clients.expects(:size).never
+ firm.clients.many? { true }
+ end
+ assert_predicate firm.clients, :loaded?
+ end
+
+ def test_calling_many_should_return_false_if_none_or_one
+ firm = companies(:another_firm)
+ assert_not_predicate firm.clients_like_ms, :many?
+ assert_equal 0, firm.clients_like_ms.size
+
+ firm = companies(:first_firm)
+ assert_not_predicate firm.limited_clients, :many?
+ assert_equal 1, firm.limited_clients.size
+ end
+
+ def test_calling_many_should_return_true_if_more_than_one
+ firm = companies(:first_firm)
+ assert_predicate firm.clients, :many?
+ assert_equal 3, firm.clients.size
+ end
+
+ def test_calling_none_should_count_instead_of_loading_association
+ firm = companies(:first_firm)
+ assert_queries(1) do
+ firm.clients.none? # use count query
+ end
+ assert_not_predicate firm.clients, :loaded?
+ end
+
+ def test_calling_none_on_loaded_association_should_not_use_query
+ firm = companies(:first_firm)
+ firm.clients.load # force load
+ assert_no_queries { assert ! firm.clients.none? }
+ end
+
+ def test_calling_none_should_defer_to_collection_if_using_a_block
+ firm = companies(:first_firm)
+ assert_queries(1) do
+ firm.clients.expects(:size).never
+ firm.clients.none? { true }
+ end
+ assert_predicate firm.clients, :loaded?
+ end
+
+ def test_calling_none_should_return_true_if_none
+ firm = companies(:another_firm)
+ assert_predicate firm.clients_like_ms, :none?
+ assert_equal 0, firm.clients_like_ms.size
+ end
+
+ def test_calling_none_should_return_false_if_any
+ firm = companies(:first_firm)
+ assert_not_predicate firm.limited_clients, :none?
+ assert_equal 1, firm.limited_clients.size
+ end
+
+ def test_calling_one_should_count_instead_of_loading_association
+ firm = companies(:first_firm)
+ assert_queries(1) do
+ firm.clients.one? # use count query
+ end
+ assert_not_predicate firm.clients, :loaded?
+ end
+
+ def test_calling_one_on_loaded_association_should_not_use_query
+ firm = companies(:first_firm)
+ firm.clients.load # force load
+ assert_no_queries { assert ! firm.clients.one? }
+ end
+
+ def test_calling_one_should_defer_to_collection_if_using_a_block
+ firm = companies(:first_firm)
+ assert_queries(1) do
+ firm.clients.expects(:size).never
+ firm.clients.one? { true }
+ end
+ assert_predicate firm.clients, :loaded?
+ end
+
+ def test_calling_one_should_return_false_if_zero
+ firm = companies(:another_firm)
+ assert_not_predicate firm.clients_like_ms, :one?
+ assert_equal 0, firm.clients_like_ms.size
+ end
+
+ def test_calling_one_should_return_true_if_one
+ firm = companies(:first_firm)
+ assert_predicate firm.limited_clients, :one?
+ assert_equal 1, firm.limited_clients.size
+ end
+
+ def test_calling_one_should_return_false_if_more_than_one
+ firm = companies(:first_firm)
+ assert_not_predicate firm.clients, :one?
+ assert_equal 3, firm.clients.size
+ end
+
+ def test_joins_with_namespaced_model_should_use_correct_type
+ old = ActiveRecord::Base.store_full_sti_class
+ ActiveRecord::Base.store_full_sti_class = true
+
+ firm = Namespaced::Firm.create(name: "Some Company")
+ firm.clients.create(name: "Some Client")
+
+ stats = Namespaced::Firm.all.merge!(
+ select: "#{Namespaced::Firm.table_name}.id, COUNT(#{Namespaced::Client.table_name}.id) AS num_clients",
+ joins: :clients,
+ group: "#{Namespaced::Firm.table_name}.id"
+ ).find firm.id
+ assert_equal 1, stats.num_clients.to_i
+ ensure
+ ActiveRecord::Base.store_full_sti_class = old
+ end
+
+ def test_association_proxy_transaction_method_starts_transaction_in_association_class
+ Comment.expects(:transaction)
+ Post.first.comments.transaction do
+ # nothing
+ end
+ end
+
+ def test_sending_new_to_association_proxy_should_have_same_effect_as_calling_new
+ client_association = companies(:first_firm).clients
+ assert_equal client_association.new.attributes, client_association.send(:new).attributes
+ end
+
+ def test_creating_using_primary_key
+ firm = Firm.first
+ client = firm.clients_using_primary_key.create!(name: "test")
+ assert_equal firm.name, client.firm_name
+ end
+
+ def test_defining_has_many_association_with_delete_all_dependency_lazily_evaluates_target_class
+ ActiveRecord::Reflection::AssociationReflection.any_instance.expects(:class_name).never
+ class_eval(<<-EOF, __FILE__, __LINE__ + 1)
+ class DeleteAllModel < ActiveRecord::Base
+ has_many :nonentities, :dependent => :delete_all
+ end
+ EOF
+ end
+
+ def test_defining_has_many_association_with_nullify_dependency_lazily_evaluates_target_class
+ ActiveRecord::Reflection::AssociationReflection.any_instance.expects(:class_name).never
+ class_eval(<<-EOF, __FILE__, __LINE__ + 1)
+ class NullifyModel < ActiveRecord::Base
+ has_many :nonentities, :dependent => :nullify
+ end
+ EOF
+ end
+
+ def test_attributes_are_being_set_when_initialized_from_has_many_association_with_where_clause
+ new_comment = posts(:welcome).comments.where(body: "Some content").build
+ assert_equal new_comment.body, "Some content"
+ end
+
+ def test_attributes_are_being_set_when_initialized_from_has_many_association_with_multiple_where_clauses
+ new_comment = posts(:welcome).comments.where(body: "Some content").where(type: "SpecialComment").build
+ assert_equal new_comment.body, "Some content"
+ assert_equal new_comment.type, "SpecialComment"
+ assert_equal new_comment.post_id, posts(:welcome).id
+ end
+
+ def test_include_method_in_has_many_association_should_return_true_for_instance_added_with_build
+ post = Post.new
+ comment = post.comments.build
+ assert_equal true, post.comments.include?(comment)
+ end
+
+ def test_load_target_respects_protected_attributes
+ topic = Topic.create!
+ reply = topic.replies.create(title: "reply 1")
+ reply.approved = false
+ reply.save!
+
+ # Save with a different object instance, so the instance that's still held
+ # in topic.relies doesn't know about the changed attribute.
+ reply2 = Reply.find(reply.id)
+ reply2.approved = true
+ reply2.save!
+
+ # Force loading the collection from the db. This will merge the existing
+ # object (reply) with what gets loaded from the db (which includes the
+ # changed approved attribute). approved is a protected attribute, so if mass
+ # assignment is used, it won't get updated and will still be false.
+ first = topic.replies.to_a.first
+ assert_equal reply.id, first.id
+ assert_equal true, first.approved?
+ end
+
+ def test_to_a_should_dup_target
+ ary = topics(:first).replies.to_a
+ target = topics(:first).replies.target
+
+ assert_not_equal target.object_id, ary.object_id
+ end
+
+ def test_merging_with_custom_attribute_writer
+ bulb = Bulb.new(color: "red")
+ assert_equal "RED!", bulb.color
+
+ car = Car.create!
+ car.bulbs << bulb
+
+ assert_equal "RED!", car.bulbs.to_a.first.color
+ end
+
+ def test_abstract_class_with_polymorphic_has_many
+ post = SubStiPost.create! title: "fooo", body: "baa"
+ tagging = Tagging.create! taggable: post
+ assert_equal [tagging], post.taggings
+ end
+
+ def test_with_polymorphic_has_many_with_custom_columns_name
+ post = Post.create! title: "foo", body: "bar"
+ image = Image.create!
+
+ post.images << image
+
+ assert_equal [image], post.images
+ end
+
+ def test_build_with_polymorphic_has_many_does_not_allow_to_override_type_and_id
+ welcome = posts(:welcome)
+ tagging = welcome.taggings.build(taggable_id: 99, taggable_type: "ShouldNotChange")
+
+ assert_equal welcome.id, tagging.taggable_id
+ assert_equal "Post", tagging.taggable_type
+ end
+
+ def test_build_from_polymorphic_association_sets_inverse_instance
+ post = Post.new
+ tagging = post.taggings.build
+
+ assert_equal post, tagging.taggable
+ end
+
+ def test_dont_call_save_callbacks_twice_on_has_many
+ firm = companies(:first_firm)
+ contract = firm.contracts.create!
+
+ assert_equal 1, contract.hi_count
+ assert_equal 1, contract.bye_count
+ end
+
+ def test_association_attributes_are_available_to_after_initialize
+ car = Car.create(name: "honda")
+ bulb = car.bulbs.build
+
+ assert_equal car.id, bulb.attributes_after_initialize["car_id"]
+ end
+
+ def test_attributes_are_set_when_initialized_from_has_many_null_relationship
+ car = Car.new name: "honda"
+ bulb = car.bulbs.where(name: "headlight").first_or_initialize
+ assert_equal "headlight", bulb.name
+ end
+
+ def test_attributes_are_set_when_initialized_from_polymorphic_has_many_null_relationship
+ post = Post.new title: "title", body: "bar"
+ tag = Tag.create!(name: "foo")
+
+ tagging = post.taggings.where(tag: tag).first_or_initialize
+
+ assert_equal tag.id, tagging.tag_id
+ assert_equal "Post", tagging.taggable_type
+ end
+
+ def test_replace
+ car = Car.create(name: "honda")
+ bulb1 = car.bulbs.create
+ bulb2 = Bulb.create
+
+ assert_equal [bulb1], car.bulbs
+ car.bulbs.replace([bulb2])
+ assert_equal [bulb2], car.bulbs
+ assert_equal [bulb2], car.reload.bulbs
+ end
+
+ def test_replace_returns_target
+ car = Car.create(name: "honda")
+ bulb1 = car.bulbs.create
+ bulb2 = car.bulbs.create
+ bulb3 = Bulb.create
+
+ assert_equal [bulb1, bulb2], car.bulbs
+ result = car.bulbs.replace([bulb3, bulb1])
+ assert_equal [bulb1, bulb3], car.bulbs
+ assert_equal [bulb1, bulb3], result
+ end
+
+ def test_collection_association_with_private_kernel_method
+ firm = companies(:first_firm)
+ assert_equal [accounts(:signals37)], firm.accounts.open
+ end
+
+ test "first_or_initialize adds the record to the association" do
+ firm = Firm.create! name: "omg"
+ client = firm.clients_of_firm.first_or_initialize
+ assert_equal [client], firm.clients_of_firm
+ end
+
+ test "first_or_create adds the record to the association" do
+ firm = Firm.create! name: "omg"
+ firm.clients_of_firm.load_target
+ client = firm.clients_of_firm.first_or_create name: "lol"
+ assert_equal [client], firm.clients_of_firm
+ assert_equal [client], firm.reload.clients_of_firm
+ end
+
+ test "delete_all, when not loaded, doesn't load the records" do
+ post = posts(:welcome)
+
+ assert post.taggings_with_delete_all.count > 0
+ assert_not_predicate post.taggings_with_delete_all, :loaded?
+
+ # 2 queries: one DELETE and another to update the counter cache
+ assert_queries(2) do
+ post.taggings_with_delete_all.delete_all
+ end
+ end
+
+ test "has many associations on new records use null relations" do
+ post = Post.new
+
+ assert_no_queries(ignore_none: false) do
+ assert_equal [], post.comments
+ assert_equal [], post.comments.where(body: "omg")
+ assert_equal [], post.comments.pluck(:body)
+ assert_equal 0, post.comments.sum(:id)
+ assert_equal 0, post.comments.count
+ end
+ end
+
+ test "collection proxy respects default scope" do
+ author = authors(:mary)
+ assert_not_predicate author.first_posts, :exists?
+ end
+
+ test "association with extend option" do
+ post = posts(:welcome)
+ assert_equal "lifo", post.comments_with_extend.author
+ assert_equal "hello", post.comments_with_extend.greeting
+ end
+
+ test "association with extend option with multiple extensions" do
+ post = posts(:welcome)
+ assert_equal "lifo", post.comments_with_extend_2.author
+ assert_equal "hullo", post.comments_with_extend_2.greeting
+ end
+
+ test "extend option affects per association" do
+ post = posts(:welcome)
+ assert_equal "lifo", post.comments_with_extend.author
+ assert_equal "lifo", post.comments_with_extend_2.author
+ assert_equal "hello", post.comments_with_extend.greeting
+ assert_equal "hullo", post.comments_with_extend_2.greeting
+ end
+
+ test "delete record with complex joins" do
+ david = authors(:david)
+
+ post = david.posts.first
+ post.type = "PostWithSpecialCategorization"
+ post.save
+
+ categorization = post.categorizations.first
+ categorization.special = true
+ categorization.save
+
+ assert_not_equal [], david.posts_with_special_categorizations
+ david.posts_with_special_categorizations = []
+ assert_equal [], david.posts_with_special_categorizations
+ end
+
+ test "does not duplicate associations when used with natural primary keys" do
+ speedometer = Speedometer.create!(id: "4")
+ speedometer.minivans.create!(minivan_id: "a-van-red", name: "a van", color: "red")
+
+ assert_equal 1, speedometer.minivans.to_a.size, "Only one association should be present:\n#{speedometer.minivans.to_a}"
+ assert_equal 1, speedometer.reload.minivans.to_a.size
+ end
+
+ test "can unscope the default scope of the associated model" do
+ car = Car.create!
+ bulb1 = Bulb.create! name: "defaulty", car: car
+ bulb2 = Bulb.create! name: "other", car: car
+
+ assert_equal [bulb1], car.bulbs
+ assert_equal [bulb1, bulb2], car.all_bulbs.sort_by(&:id)
+ end
+
+ test "can unscope and where the default scope of the associated model" do
+ Car.has_many :other_bulbs, -> { unscope(where: [:name]).where(name: "other") }, class_name: "Bulb"
+ car = Car.create!
+ bulb1 = Bulb.create! name: "defaulty", car: car
+ bulb2 = Bulb.create! name: "other", car: car
+
+ assert_equal [bulb1], car.bulbs
+ assert_equal [bulb2], car.other_bulbs
+ end
+
+ test "can rewhere the default scope of the associated model" do
+ Car.has_many :old_bulbs, -> { rewhere(name: "old") }, class_name: "Bulb"
+ car = Car.create!
+ bulb1 = Bulb.create! name: "defaulty", car: car
+ bulb2 = Bulb.create! name: "old", car: car
+
+ assert_equal [bulb1], car.bulbs
+ assert_equal [bulb2], car.old_bulbs
+ end
+
+ test "unscopes the default scope of associated model when used with include" do
+ car = Car.create!
+ bulb = Bulb.create! name: "other", car: car
+
+ assert_equal [bulb], Car.find(car.id).all_bulbs
+ assert_equal [bulb], Car.includes(:all_bulbs).find(car.id).all_bulbs
+ assert_equal [bulb], Car.eager_load(:all_bulbs).find(car.id).all_bulbs
+ end
+
+ test "raises RecordNotDestroyed when replaced child can't be destroyed" do
+ car = Car.create!
+ original_child = FailedBulb.create!(car: car)
+
+ error = assert_raise(ActiveRecord::RecordNotDestroyed) do
+ car.failed_bulbs = [FailedBulb.create!]
+ end
+
+ assert_equal [original_child], car.reload.failed_bulbs
+ assert_equal "Failed to destroy the record", error.message
+ end
+
+ test "updates counter cache when default scope is given" do
+ topic = DefaultRejectedTopic.create approved: true
+
+ assert_difference "topic.reload.replies_count", 1 do
+ topic.approved_replies.create!
+ end
+ end
+
+ test "dangerous association name raises ArgumentError" do
+ [:errors, "errors", :save, "save"].each do |name|
+ assert_raises(ArgumentError, "Association #{name} should not be allowed") do
+ Class.new(ActiveRecord::Base) do
+ has_many name
+ end
+ end
+ end
+ end
+
+ test "passes custom context validation to validate children" do
+ pirate = FamousPirate.new
+ pirate.famous_ships << ship = FamousShip.new
+
+ assert_predicate pirate, :valid?
+ assert_not pirate.valid?(:conference)
+ assert_equal "can't be blank", ship.errors[:name].first
+ end
+
+ test "association with instance dependent scope" do
+ bob = authors(:bob)
+ Post.create!(title: "signed post by bob", body: "stuff", author: authors(:bob))
+ Post.create!(title: "anonymous post", body: "more stuff", author: authors(:bob))
+ assert_equal ["misc post by bob", "other post by bob",
+ "signed post by bob"], bob.posts_with_signature.map(&:title).sort
+
+ assert_equal [], authors(:david).posts_with_signature.map(&:title)
+ end
+
+ test "associations autosaves when object is already persisted" do
+ bulb = Bulb.create!
+ tyre = Tyre.create!
+
+ car = Car.create! do |c|
+ c.bulbs << bulb
+ c.tyres << tyre
+ end
+
+ assert_equal 1, car.bulbs.count
+ assert_equal 1, car.tyres.count
+ end
+
+ test "associations replace in memory when records have the same id" do
+ bulb = Bulb.create!
+ car = Car.create!(bulbs: [bulb])
+
+ new_bulb = Bulb.find(bulb.id)
+ new_bulb.name = "foo"
+ car.bulbs = [new_bulb]
+
+ assert_equal "foo", car.bulbs.first.name
+ end
+
+ test "in memory replacement executes no queries" do
+ bulb = Bulb.create!
+ car = Car.create!(bulbs: [bulb])
+
+ new_bulb = Bulb.find(bulb.id)
+
+ assert_no_queries do
+ car.bulbs = [new_bulb]
+ end
+ end
+
+ test "in memory replacements do not execute callbacks" do
+ raise_after_add = false
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = :cars
+ has_many :bulbs, after_add: proc { raise if raise_after_add }
+
+ def self.name
+ "Car"
+ end
+ end
+ bulb = Bulb.create!
+ car = klass.create!(bulbs: [bulb])
+
+ new_bulb = Bulb.find(bulb.id)
+ raise_after_add = true
+
+ assert_nothing_raised do
+ car.bulbs = [new_bulb]
+ end
+ end
+
+ test "in memory replacements sets inverse instance" do
+ bulb = Bulb.create!
+ car = Car.create!(bulbs: [bulb])
+
+ new_bulb = Bulb.find(bulb.id)
+ car.bulbs = [new_bulb]
+
+ assert_same car, new_bulb.car
+ end
+
+ test "reattach to new objects replaces inverse association and foreign key" do
+ bulb = Bulb.create!(car: Car.create!)
+ assert bulb.car_id
+ car = Car.new
+ car.bulbs << bulb
+ assert_equal car, bulb.car
+ assert_nil bulb.car_id
+ end
+
+ test "in memory replacement maintains order" do
+ first_bulb = Bulb.create!
+ second_bulb = Bulb.create!
+ car = Car.create!(bulbs: [first_bulb, second_bulb])
+
+ same_bulb = Bulb.find(first_bulb.id)
+ car.bulbs = [second_bulb, same_bulb]
+
+ assert_equal [first_bulb, second_bulb], car.bulbs
+ end
+
+ test "association size calculation works with default scoped selects when not previously fetched" do
+ firm = Firm.create!(name: "Firm")
+ 5.times { firm.developers_with_select << Developer.create!(name: "Developer") }
+
+ same_firm = Firm.find(firm.id)
+ assert_equal 5, same_firm.developers_with_select.size
+ end
+
+ test "prevent double insertion of new object when the parent association loaded in the after save callback" do
+ reset_callbacks(:save, Bulb) do
+ Bulb.after_save { |record| record.car.bulbs.load }
+
+ car = Car.create!
+ car.bulbs << Bulb.new
+
+ assert_equal 1, car.bulbs.size
+ end
+ end
+
+ test "prevent double firing the before save callback of new object when the parent association saved in the callback" do
+ reset_callbacks(:save, Bulb) do
+ count = 0
+ Bulb.before_save { |record| record.car.save && count += 1 }
+
+ car = Car.create!
+ car.bulbs.create!
+
+ assert_equal 1, count
+ end
+ end
+
+ class AuthorWithErrorDestroyingAssociation < ActiveRecord::Base
+ self.table_name = "authors"
+ has_many :posts_with_error_destroying,
+ class_name: "PostWithErrorDestroying",
+ foreign_key: :author_id,
+ dependent: :destroy
+ end
+
+ class PostWithErrorDestroying < ActiveRecord::Base
+ self.table_name = "posts"
+ self.inheritance_column = nil
+ before_destroy -> { throw :abort }
+ end
+
+ def test_destroy_does_not_raise_when_association_errors_on_destroy
+ assert_no_difference "AuthorWithErrorDestroyingAssociation.count" do
+ author = AuthorWithErrorDestroyingAssociation.first
+
+ assert_not author.destroy
+ end
+ end
+
+ def test_destroy_with_bang_bubbles_errors_from_associations
+ error = assert_raises ActiveRecord::RecordNotDestroyed do
+ AuthorWithErrorDestroyingAssociation.first.destroy!
+ end
+
+ assert_instance_of PostWithErrorDestroying, error.record
+ end
+
+ def test_ids_reader_memoization
+ car = Car.create!(name: "TofaÅŸ")
+ bulb = Bulb.create!(car: car)
+
+ assert_equal [bulb.id], car.bulb_ids
+ assert_no_queries { car.bulb_ids }
+
+ bulb2 = car.bulbs.create!
+
+ assert_equal [bulb.id, bulb2.id], car.bulb_ids
+ assert_no_queries { car.bulb_ids }
+ end
+
+ def test_loading_association_in_validate_callback_doesnt_affect_persistence
+ reset_callbacks(:validation, Bulb) do
+ Bulb.after_validation { |record| record.car.bulbs.load }
+
+ car = Car.create!(name: "Car")
+ bulb = car.bulbs.create!
+
+ assert_equal [bulb], car.bulbs
+ end
+ end
+
+ private
+
+ def force_signal37_to_load_all_clients_of_firm
+ companies(:first_firm).clients_of_firm.load_target
+ end
+
+ def reset_callbacks(kind, klass)
+ old_callbacks = {}
+ old_callbacks[klass] = klass.send("_#{kind}_callbacks").dup
+ klass.subclasses.each do |subclass|
+ old_callbacks[subclass] = subclass.send("_#{kind}_callbacks").dup
+ end
+ yield
+ ensure
+ klass.send("_#{kind}_callbacks=", old_callbacks[klass])
+ klass.subclasses.each do |subclass|
+ subclass.send("_#{kind}_callbacks=", old_callbacks[subclass])
+ end
+ 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
new file mode 100644
index 0000000000..3b3d4037b9
--- /dev/null
+++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb
@@ -0,0 +1,1408 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/post"
+require "models/person"
+require "models/reference"
+require "models/job"
+require "models/reader"
+require "models/comment"
+require "models/rating"
+require "models/tag"
+require "models/tagging"
+require "models/author"
+require "models/owner"
+require "models/pet"
+require "models/pet_treasure"
+require "models/toy"
+require "models/treasure"
+require "models/contract"
+require "models/company"
+require "models/developer"
+require "models/computer"
+require "models/subscriber"
+require "models/book"
+require "models/subscription"
+require "models/essay"
+require "models/category"
+require "models/categorization"
+require "models/member"
+require "models/membership"
+require "models/club"
+require "models/organization"
+require "models/user"
+require "models/family"
+require "models/family_tree"
+
+class HasManyThroughAssociationsTest < ActiveRecord::TestCase
+ fixtures :posts, :readers, :people, :comments, :authors, :categories, :taggings, :tags,
+ :owners, :pets, :toys, :jobs, :references, :companies, :members, :author_addresses,
+ :subscribers, :books, :subscriptions, :developers, :categorizations, :essays,
+ :categories_posts, :clubs, :memberships, :organizations
+
+ # Dummies to force column loads so query counts are clean.
+ def setup
+ Person.create first_name: "gummy"
+ Reader.create person_id: 0, post_id: 0
+ end
+
+ def test_marshal_dump
+ preloaded = Post.includes(:first_blue_tags).first
+ assert_equal preloaded, Marshal.load(Marshal.dump(preloaded))
+ end
+
+ def test_preload_sti_rhs_class
+ developers = Developer.includes(:firms).all.to_a
+ assert_no_queries do
+ developers.each(&:firms)
+ end
+ end
+
+ def test_preload_sti_middle_relation
+ club = Club.create!(name: "Aaron cool banana club")
+ member1 = Member.create!(name: "Aaron")
+ member2 = Member.create!(name: "Cat")
+
+ SuperMembership.create! club: club, member: member1
+ CurrentMembership.create! club: club, member: member2
+
+ club1 = Club.includes(:members).find_by_id club.id
+ assert_equal [member1, member2].sort_by(&:id),
+ club1.members.sort_by(&:id)
+ end
+
+ def test_ordered_has_many_through
+ person_prime = Class.new(ActiveRecord::Base) do
+ def self.name; "Person"; end
+
+ has_many :readers
+ has_many :posts, -> { order("posts.id DESC") }, through: :readers
+ end
+ posts = person_prime.includes(:posts).first.posts
+
+ assert_operator posts.length, :>, 1
+ posts.each_cons(2) do |left, right|
+ assert_operator left.id, :>, right.id
+ end
+ end
+
+ def test_singleton_has_many_through
+ book = make_model "Book"
+ subscription = make_model "Subscription"
+ subscriber = make_model "Subscriber"
+
+ subscriber.primary_key = "nick"
+ subscription.belongs_to :book, anonymous_class: book
+ subscription.belongs_to :subscriber, anonymous_class: subscriber
+
+ book.has_many :subscriptions, anonymous_class: subscription
+ book.has_many :subscribers, through: :subscriptions, anonymous_class: subscriber
+
+ anonbook = book.first
+ namebook = Book.find anonbook.id
+
+ assert_operator anonbook.subscribers.count, :>, 0
+ anonbook.subscribers.each do |s|
+ assert_instance_of subscriber, s
+ end
+ assert_equal namebook.subscribers.map(&:id).sort,
+ anonbook.subscribers.map(&:id).sort
+ end
+
+ def test_no_pk_join_table_append
+ lesson, _, student = make_no_pk_hm_t
+
+ sicp = lesson.new(name: "SICP")
+ ben = student.new(name: "Ben Bitdiddle")
+ sicp.students << ben
+ assert sicp.save!
+ end
+
+ def test_no_pk_join_table_delete
+ lesson, lesson_student, student = make_no_pk_hm_t
+
+ sicp = lesson.new(name: "SICP")
+ ben = student.new(name: "Ben Bitdiddle")
+ louis = student.new(name: "Louis Reasoner")
+ sicp.students << ben
+ sicp.students << louis
+ assert sicp.save!
+
+ sicp.students.reload
+ assert_operator lesson_student.count, :>=, 2
+ assert_no_difference("student.count") do
+ assert_difference("lesson_student.count", -2) do
+ sicp.students.destroy(*student.all.to_a)
+ end
+ end
+ end
+
+ def test_no_pk_join_model_callbacks
+ lesson, lesson_student, student = make_no_pk_hm_t
+
+ after_destroy_called = false
+ lesson_student.after_destroy do
+ after_destroy_called = true
+ end
+
+ sicp = lesson.new(name: "SICP")
+ ben = student.new(name: "Ben Bitdiddle")
+ sicp.students << ben
+ assert sicp.save!
+
+ sicp.students.reload
+ sicp.students.destroy(*student.all.to_a)
+ assert after_destroy_called, "after destroy should be called"
+ end
+
+ def test_pk_is_not_required_for_join
+ post = Post.includes(:scategories).first
+ post2 = Post.includes(:categories).first
+
+ assert_operator post.categories.length, :>, 0
+ assert_equal post2.categories, post.categories
+ end
+
+ def test_include?
+ person = Person.new
+ post = Post.new
+ person.posts << post
+ assert_includes person.posts, post
+ end
+
+ def test_associate_existing
+ post = posts(:thinking)
+ person = people(:david)
+
+ assert_queries(1) do
+ post.people << person
+ end
+
+ assert_queries(1) do
+ assert_includes post.people, person
+ end
+
+ assert_includes post.reload.people.reload, person
+ end
+
+ def test_delete_all_for_with_dependent_option_destroy
+ person = people(:david)
+ assert_equal 1, person.jobs_with_dependent_destroy.count
+
+ assert_no_difference "Job.count" do
+ assert_difference "Reference.count", -1 do
+ person.reload.jobs_with_dependent_destroy.delete_all
+ end
+ end
+ end
+
+ def test_delete_all_for_with_dependent_option_nullify
+ person = people(:david)
+ assert_equal 1, person.jobs_with_dependent_nullify.count
+
+ assert_no_difference "Job.count" do
+ assert_no_difference "Reference.count" do
+ person.reload.jobs_with_dependent_nullify.delete_all
+ end
+ end
+ end
+
+ def test_delete_all_for_with_dependent_option_delete_all
+ person = people(:david)
+ assert_equal 1, person.jobs_with_dependent_delete_all.count
+
+ assert_no_difference "Job.count" do
+ assert_difference "Reference.count", -1 do
+ person.reload.jobs_with_dependent_delete_all.delete_all
+ end
+ end
+ end
+
+ def test_concat
+ person = people(:david)
+ post = posts(:thinking)
+ post.people.concat [person]
+ assert_equal 1, post.people.size
+ assert_equal 1, post.people.reload.size
+ end
+
+ def test_associate_existing_record_twice_should_add_to_target_twice
+ post = posts(:thinking)
+ person = people(:david)
+
+ assert_difference "post.people.to_a.count", 2 do
+ post.people << person
+ post.people << person
+ end
+ end
+
+ def test_associate_existing_record_twice_should_add_records_twice
+ post = posts(:thinking)
+ person = people(:david)
+
+ assert_difference "post.people.count", 2 do
+ post.people << person
+ post.people << person
+ end
+ end
+
+ def test_add_two_instance_and_then_deleting
+ post = posts(:thinking)
+ person = people(:david)
+
+ post.people << person
+ post.people << person
+
+ counts = ["post.people.count", "post.people.to_a.count", "post.readers.count", "post.readers.to_a.count"]
+ assert_difference counts, -2 do
+ post.people.delete(person)
+ end
+
+ assert_not_includes post.people.reload, person
+ end
+
+ def test_associating_new
+ assert_queries(1) { posts(:thinking) }
+ new_person = nil # so block binding catches it
+
+ assert_queries(0) do
+ new_person = Person.new first_name: "bob"
+ end
+
+ # Associating new records always saves them
+ # Thus, 1 query for the new person record, 1 query for the new join table record
+ assert_queries(2) do
+ posts(:thinking).people << new_person
+ end
+
+ assert_queries(1) do
+ assert_includes posts(:thinking).people, new_person
+ end
+
+ assert_includes posts(:thinking).reload.people.reload, new_person
+ end
+
+ def test_associate_new_by_building
+ assert_queries(1) { posts(:thinking) }
+
+ assert_queries(0) do
+ posts(:thinking).people.build(first_name: "Bob")
+ posts(:thinking).people.new(first_name: "Ted")
+ end
+
+ # Should only need to load the association once
+ assert_queries(1) do
+ assert_includes posts(:thinking).people.collect(&:first_name), "Bob"
+ assert_includes posts(:thinking).people.collect(&:first_name), "Ted"
+ end
+
+ # 2 queries for each new record (1 to save the record itself, 1 for the join model)
+ # * 2 new records = 4
+ # + 1 query to save the actual post = 5
+ assert_queries(5) do
+ posts(:thinking).body += "-changed"
+ posts(:thinking).save
+ end
+
+ assert_includes posts(:thinking).reload.people.reload.collect(&:first_name), "Bob"
+ assert_includes posts(:thinking).reload.people.reload.collect(&:first_name), "Ted"
+ end
+
+ def test_build_then_save_with_has_many_inverse
+ post = posts(:thinking)
+ person = post.people.build(first_name: "Bob")
+ person.save
+ post.reload
+
+ assert_includes post.people, person
+ end
+
+ def test_build_then_save_with_has_one_inverse
+ post = posts(:thinking)
+ person = post.single_people.build(first_name: "Bob")
+ person.save
+ post.reload
+
+ assert_includes post.single_people, person
+ end
+
+ def test_build_then_remove_then_save
+ post = posts(:thinking)
+ post.people.build(first_name: "Bob")
+ ted = post.people.build(first_name: "Ted")
+ post.people.delete(ted)
+ post.save!
+ post.reload
+
+ assert_equal ["Bob"], post.people.collect(&:first_name)
+ end
+
+ def test_both_parent_ids_set_when_saving_new
+ post = Post.new(title: "Hello", body: "world")
+ person = Person.new(first_name: "Sean")
+
+ post.people = [person]
+ post.save
+
+ assert post.id
+ assert person.id
+ assert_equal post.id, post.readers.first.post_id
+ assert_equal person.id, post.readers.first.person_id
+ end
+
+ def test_delete_association
+ assert_queries(2) { posts(:welcome);people(:michael); }
+
+ assert_queries(1) do
+ posts(:welcome).people.delete(people(:michael))
+ end
+
+ assert_queries(1) do
+ assert_empty posts(:welcome).people
+ end
+
+ assert_empty posts(:welcome).reload.people.reload
+ end
+
+ def test_destroy_association
+ assert_no_difference "Person.count" do
+ assert_difference "Reader.count", -1 do
+ posts(:welcome).people.destroy(people(:michael))
+ end
+ end
+
+ assert_empty posts(:welcome).reload.people
+ assert_empty posts(:welcome).people.reload
+ end
+
+ def test_destroy_all
+ assert_no_difference "Person.count" do
+ assert_difference "Reader.count", -1 do
+ posts(:welcome).people.destroy_all
+ end
+ end
+
+ assert_empty posts(:welcome).reload.people
+ assert_empty posts(:welcome).people.reload
+ end
+
+ def test_should_raise_exception_for_destroying_mismatching_records
+ assert_no_difference ["Person.count", "Reader.count"] do
+ assert_raise(ActiveRecord::AssociationTypeMismatch) { posts(:welcome).people.destroy(posts(:thinking)) }
+ end
+ end
+
+ def test_delete_through_belongs_to_with_dependent_nullify
+ Reference.make_comments = true
+
+ person = people(:michael)
+ job = jobs(:magician)
+ reference = Reference.where(job_id: job.id, person_id: person.id).first
+
+ assert_no_difference ["Job.count", "Reference.count"] do
+ assert_difference "person.jobs.count", -1 do
+ person.jobs_with_dependent_nullify.delete(job)
+ end
+ end
+
+ assert_nil reference.reload.job_id
+ ensure
+ Reference.make_comments = false
+ end
+
+ def test_delete_through_belongs_to_with_dependent_delete_all
+ Reference.make_comments = true
+
+ person = people(:michael)
+ job = jobs(:magician)
+
+ # Make sure we're not deleting everything
+ assert person.jobs.count >= 2
+
+ assert_no_difference "Job.count" do
+ assert_difference ["person.jobs.count", "Reference.count"], -1 do
+ person.jobs_with_dependent_delete_all.delete(job)
+ end
+ end
+
+ # Check that the destroy callback on Reference did not run
+ assert_nil person.reload.comments
+ ensure
+ Reference.make_comments = false
+ end
+
+ def test_delete_through_belongs_to_with_dependent_destroy
+ Reference.make_comments = true
+
+ person = people(:michael)
+ job = jobs(:magician)
+
+ # Make sure we're not deleting everything
+ assert person.jobs.count >= 2
+
+ assert_no_difference "Job.count" do
+ assert_difference ["person.jobs.count", "Reference.count"], -1 do
+ person.jobs_with_dependent_destroy.delete(job)
+ end
+ end
+
+ # Check that the destroy callback on Reference ran
+ assert_equal "Reference destroyed", person.reload.comments
+ ensure
+ Reference.make_comments = false
+ end
+
+ def test_belongs_to_with_dependent_destroy
+ person = PersonWithDependentDestroyJobs.find(1)
+
+ # Create a reference which is not linked to a job. This should not be destroyed.
+ person.references.create!
+
+ assert_no_difference "Job.count" do
+ assert_difference "Reference.count", -person.jobs.count do
+ person.destroy
+ end
+ end
+ end
+
+ def test_belongs_to_with_dependent_delete_all
+ person = PersonWithDependentDeleteAllJobs.find(1)
+
+ # Create a reference which is not linked to a job. This should not be destroyed.
+ person.references.create!
+
+ assert_no_difference "Job.count" do
+ assert_difference "Reference.count", -person.jobs.count do
+ person.destroy
+ end
+ end
+ end
+
+ def test_belongs_to_with_dependent_nullify
+ person = PersonWithDependentNullifyJobs.find(1)
+
+ references = person.references.to_a
+
+ assert_no_difference ["Reference.count", "Job.count"] do
+ person.destroy
+ end
+
+ references.each do |reference|
+ assert_nil reference.reload.job_id
+ end
+ end
+
+ def test_update_counter_caches_on_delete
+ post = posts(:welcome)
+ tag = post.tags.create!(name: "doomed")
+
+ assert_difference ["post.reload.tags_count"], -1 do
+ posts(:welcome).tags.delete(tag)
+ end
+ end
+
+ def test_update_counter_caches_on_delete_with_dependent_destroy
+ post = posts(:welcome)
+ tag = post.tags.create!(name: "doomed")
+ post.update_columns(tags_with_destroy_count: post.tags.count)
+
+ assert_difference ["post.reload.tags_with_destroy_count"], -1 do
+ posts(:welcome).tags_with_destroy.delete(tag)
+ end
+ end
+
+ def test_update_counter_caches_on_delete_with_dependent_nullify
+ post = posts(:welcome)
+ tag = post.tags.create!(name: "doomed")
+ post.update_columns(tags_with_nullify_count: post.tags.count)
+
+ assert_no_difference "post.reload.tags_count" do
+ assert_difference "post.reload.tags_with_nullify_count", -1 do
+ posts(:welcome).tags_with_nullify.delete(tag)
+ end
+ end
+ end
+
+ def test_update_counter_caches_on_replace_association
+ post = posts(:welcome)
+ tag = post.tags.create!(name: "doomed")
+ tag.tagged_posts << posts(:thinking)
+
+ tag.tagged_posts = []
+ post.reload
+
+ assert_equal(post.taggings.count, post.tags_count)
+ end
+
+ def test_update_counter_caches_on_destroy
+ post = posts(:welcome)
+ tag = post.tags.create!(name: "doomed")
+
+ assert_difference "post.reload.tags_count", -1 do
+ tag.tagged_posts.destroy(post)
+ end
+ end
+
+ def test_update_counter_caches_on_destroy_with_indestructible_through_record
+ post = posts(:welcome)
+ tag = post.indestructible_tags.create!(name: "doomed")
+ post.update_columns(indestructible_tags_count: post.indestructible_tags.count)
+
+ assert_no_difference "post.reload.indestructible_tags_count" do
+ posts(:welcome).indestructible_tags.destroy(tag)
+ end
+ end
+
+ def test_replace_association
+ assert_queries(4) { posts(:welcome);people(:david);people(:michael); posts(:welcome).people.reload }
+
+ # 1 query to delete the existing reader (michael)
+ # 1 query to associate the new reader (david)
+ assert_queries(2) do
+ posts(:welcome).people = [people(:david)]
+ end
+
+ assert_queries(0) {
+ assert_includes posts(:welcome).people, people(:david)
+ assert_not_includes posts(:welcome).people, people(:michael)
+ }
+
+ assert_includes posts(:welcome).reload.people.reload, people(:david)
+ assert_not_includes posts(:welcome).reload.people.reload, people(:michael)
+ end
+
+ def test_replace_order_is_preserved
+ posts(:welcome).people.clear
+ posts(:welcome).people = [people(:david), people(:michael)]
+ assert_equal [people(:david).id, people(:michael).id], posts(:welcome).readers.order("id").map(&:person_id)
+
+ # Test the inverse order in case the first success was a coincidence
+ posts(:welcome).people.clear
+ posts(:welcome).people = [people(:michael), people(:david)]
+ assert_equal [people(:michael).id, people(:david).id], posts(:welcome).readers.order("id").map(&:person_id)
+ end
+
+ def test_replace_by_id_order_is_preserved
+ posts(:welcome).people.clear
+ posts(:welcome).person_ids = [people(:david).id, people(:michael).id]
+ assert_equal [people(:david).id, people(:michael).id], posts(:welcome).readers.order("id").map(&:person_id)
+
+ # Test the inverse order in case the first success was a coincidence
+ posts(:welcome).people.clear
+ posts(:welcome).person_ids = [people(:michael).id, people(:david).id]
+ assert_equal [people(:michael).id, people(:david).id], posts(:welcome).readers.order("id").map(&:person_id)
+ end
+
+ def test_associate_with_create
+ assert_queries(1) { posts(:thinking) }
+
+ # 1 query for the new record, 1 for the join table record
+ # No need to update the actual collection yet!
+ assert_queries(2) do
+ posts(:thinking).people.create(first_name: "Jeb")
+ end
+
+ # *Now* we actually need the collection so it's loaded
+ assert_queries(1) do
+ assert_includes posts(:thinking).people.collect(&:first_name), "Jeb"
+ end
+
+ assert_includes posts(:thinking).reload.people.reload.collect(&:first_name), "Jeb"
+ end
+
+ def test_through_record_is_built_when_created_with_where
+ assert_difference("posts(:thinking).readers.count", 1) do
+ posts(:thinking).people.where(first_name: "Jeb").create
+ end
+ end
+
+ def test_associate_with_create_and_no_options
+ peeps = posts(:thinking).people.count
+ posts(:thinking).people.create(first_name: "foo")
+ assert_equal peeps + 1, posts(:thinking).people.count
+ end
+
+ def test_associate_with_create_with_through_having_conditions
+ impatient_people = posts(:thinking).impatient_people.count
+ posts(:thinking).impatient_people.create!(first_name: "foo")
+ assert_equal impatient_people + 1, posts(:thinking).impatient_people.count
+ end
+
+ def test_associate_with_create_exclamation_and_no_options
+ peeps = posts(:thinking).people.count
+ posts(:thinking).people.create!(first_name: "foo")
+ assert_equal peeps + 1, posts(:thinking).people.count
+ end
+
+ def test_create_on_new_record
+ p = Post.new
+
+ error = assert_raises(ActiveRecord::RecordNotSaved) { p.people.create(first_name: "mew") }
+ assert_equal "You cannot call create unless the parent is saved", error.message
+
+ error = assert_raises(ActiveRecord::RecordNotSaved) { p.people.create!(first_name: "snow") }
+ assert_equal "You cannot call create unless the parent is saved", error.message
+ end
+
+ def test_associate_with_create_and_invalid_options
+ firm = companies(:first_firm)
+ assert_no_difference("firm.developers.count") { assert_nothing_raised { firm.developers.create(name: "0") } }
+ end
+
+ def test_associate_with_create_and_valid_options
+ firm = companies(:first_firm)
+ assert_difference("firm.developers.count", 1) { firm.developers.create(name: "developer") }
+ end
+
+ def test_associate_with_create_bang_and_invalid_options
+ firm = companies(:first_firm)
+ assert_no_difference("firm.developers.count") { assert_raises(ActiveRecord::RecordInvalid) { firm.developers.create!(name: "0") } }
+ end
+
+ def test_associate_with_create_bang_and_valid_options
+ firm = companies(:first_firm)
+ assert_difference("firm.developers.count", 1) { firm.developers.create!(name: "developer") }
+ end
+
+ def test_push_with_invalid_record
+ firm = companies(:first_firm)
+ assert_raises(ActiveRecord::RecordInvalid) { firm.developers << Developer.new(name: "0") }
+ end
+
+ def test_push_with_invalid_join_record
+ repair_validations(Contract) do
+ Contract.validate { |r| r.errors[:base] << "Invalid Contract" }
+
+ firm = companies(:first_firm)
+ lifo = Developer.new(name: "lifo")
+ assert_raises(ActiveRecord::RecordInvalid) { firm.developers << lifo }
+
+ lifo = Developer.create!(name: "lifo")
+ assert_raises(ActiveRecord::RecordInvalid) { firm.developers << lifo }
+ end
+ end
+
+ def test_clear_associations
+ assert_queries(2) { posts(:welcome);posts(:welcome).people.reload }
+
+ assert_queries(1) do
+ posts(:welcome).people.clear
+ end
+
+ assert_queries(0) do
+ assert_empty posts(:welcome).people
+ end
+
+ assert_empty posts(:welcome).reload.people.reload
+ end
+
+ def test_association_callback_ordering
+ Post.reset_log
+ log = Post.log
+ post = posts(:thinking)
+
+ post.people_with_callbacks << people(:michael)
+ assert_equal [
+ [:added, :before, "Michael"],
+ [:added, :after, "Michael"]
+ ], log.last(2)
+
+ post.people_with_callbacks.push(people(:david), Person.create!(first_name: "Bob"), Person.new(first_name: "Lary"))
+ assert_equal [
+ [:added, :before, "David"],
+ [:added, :after, "David"],
+ [:added, :before, "Bob"],
+ [:added, :after, "Bob"],
+ [:added, :before, "Lary"],
+ [:added, :after, "Lary"]
+ ], log.last(6)
+
+ post.people_with_callbacks.build(first_name: "Ted")
+ assert_equal [
+ [:added, :before, "Ted"],
+ [:added, :after, "Ted"]
+ ], log.last(2)
+
+ post.people_with_callbacks.create(first_name: "Sam")
+ assert_equal [
+ [:added, :before, "Sam"],
+ [:added, :after, "Sam"]
+ ], log.last(2)
+
+ post.people_with_callbacks = [people(:michael), people(:david), Person.new(first_name: "Julian"), Person.create!(first_name: "Roger")]
+ assert_equal((%w(Ted Bob Sam Lary) * 2).sort, log[-12..-5].collect(&:last).sort)
+ assert_equal [
+ [:added, :before, "Julian"],
+ [:added, :after, "Julian"],
+ [:added, :before, "Roger"],
+ [:added, :after, "Roger"]
+ ], log.last(4)
+ end
+
+ def test_dynamic_find_should_respect_association_include
+ # SQL error in sort clause if :include is not included
+ # due to Unknown column 'comments.id'
+ assert Person.find(1).posts_with_comments_sorted_by_comment_id.find_by_title("Welcome to the weblog")
+ end
+
+ def test_count_with_include_should_alias_join_table
+ assert_equal 2, people(:michael).posts.includes(:readers).count
+ end
+
+ def test_inner_join_with_quoted_table_name
+ assert_equal 2, people(:michael).jobs.size
+ end
+
+ def test_get_ids
+ assert_equal [posts(:welcome).id, posts(:authorless).id].sort, people(:michael).post_ids.sort
+ end
+
+ def test_get_ids_for_has_many_through_with_conditions_should_not_preload
+ Tagging.create!(taggable_type: "Post", taggable_id: posts(:welcome).id, tag: tags(:misc))
+ assert_not_called(ActiveRecord::Associations::Preloader, :new) do
+ posts(:welcome).misc_tag_ids
+ end
+ end
+
+ def test_get_ids_for_loaded_associations
+ person = people(:michael)
+ person.posts.reload
+ assert_queries(0) do
+ person.post_ids
+ person.post_ids
+ end
+ end
+
+ def test_get_ids_for_unloaded_associations_does_not_load_them
+ person = people(:michael)
+ assert_not_predicate person.posts, :loaded?
+ assert_equal [posts(:welcome).id, posts(:authorless).id].sort, person.post_ids.sort
+ assert_not_predicate person.posts, :loaded?
+ end
+
+ def test_association_proxy_transaction_method_starts_transaction_in_association_class
+ assert_called(Tag, :transaction) do
+ Post.first.tags.transaction do
+ # nothing
+ end
+ end
+ end
+
+ def test_has_many_association_through_a_belongs_to_association_where_the_association_doesnt_exist
+ post = Post.create!(title: "TITLE", body: "BODY")
+ assert_equal [], post.author_favorites
+ end
+
+ def test_has_many_association_through_a_belongs_to_association
+ author = authors(:mary)
+ post = Post.create!(author: author, title: "TITLE", body: "BODY")
+ author.author_favorites.create(favorite_author_id: 1)
+ author.author_favorites.create(favorite_author_id: 2)
+ author.author_favorites.create(favorite_author_id: 3)
+ assert_equal post.author.author_favorites, post.author_favorites
+ end
+
+ def test_merge_join_association_with_has_many_through_association_proxy
+ author = authors(:mary)
+ assert_nothing_raised { author.comments.ratings.to_sql }
+ end
+
+ def test_has_many_association_through_a_has_many_association_with_nonstandard_primary_keys
+ assert_equal 2, owners(:blackbeard).toys.count
+ end
+
+ def test_find_on_has_many_association_collection_with_include_and_conditions
+ post_with_no_comments = people(:michael).posts_with_no_comments.first
+ assert_equal post_with_no_comments, posts(:authorless)
+ end
+
+ def test_has_many_through_has_one_reflection
+ assert_equal [comments(:eager_sti_on_associations_vs_comment)], authors(:david).very_special_comments
+ end
+
+ def test_modifying_has_many_through_has_one_reflection_should_raise
+ [
+ lambda { authors(:david).very_special_comments = [VerySpecialComment.create!(body: "Gorp!", post_id: 1011), VerySpecialComment.create!(body: "Eep!", post_id: 1012)] },
+ lambda { authors(:david).very_special_comments << VerySpecialComment.create!(body: "Hoohah!", post_id: 1013) },
+ lambda { authors(:david).very_special_comments.delete(authors(:david).very_special_comments.first) },
+ ].each { |block| assert_raise(ActiveRecord::HasManyThroughCantAssociateThroughHasOneOrManyReflection, &block) }
+ end
+
+ def test_has_many_association_through_a_has_many_association_to_self
+ sarah = Person.create!(first_name: "Sarah", primary_contact_id: people(:susan).id, gender: "F", number1_fan_id: 1)
+ john = Person.create!(first_name: "John", primary_contact_id: sarah.id, gender: "M", number1_fan_id: 1)
+ assert_equal sarah.agents, [john]
+ assert_equal people(:susan).agents.flat_map(&:agents).sort, people(:susan).agents_of_agents.sort
+ end
+
+ def test_associate_existing_with_nonstandard_primary_key_on_belongs_to
+ Categorization.create(author: authors(:mary), named_category_name: categories(:general).name)
+ assert_equal categories(:general), authors(:mary).named_categories.first
+ end
+
+ def test_collection_build_with_nonstandard_primary_key_on_belongs_to
+ author = authors(:mary)
+ category = author.named_categories.build(name: "Primary")
+ author.save
+ assert Categorization.exists?(author_id: author.id, named_category_name: category.name)
+ assert_includes author.named_categories.reload, category
+ end
+
+ def test_collection_create_with_nonstandard_primary_key_on_belongs_to
+ author = authors(:mary)
+ category = author.named_categories.create(name: "Primary")
+ assert Categorization.exists?(author_id: author.id, named_category_name: category.name)
+ assert_includes author.named_categories.reload, category
+ end
+
+ def test_collection_exists
+ author = authors(:mary)
+ category = Category.create!(author_ids: [author.id], name: "Primary")
+ assert category.authors.exists?(id: author.id)
+ assert category.reload.authors.exists?(id: author.id)
+ end
+
+ def test_collection_delete_with_nonstandard_primary_key_on_belongs_to
+ author = authors(:mary)
+ category = author.named_categories.create(name: "Primary")
+ author.named_categories.delete(category)
+ assert !Categorization.exists?(author_id: author.id, named_category_name: category.name)
+ assert_empty author.named_categories.reload
+ end
+
+ def test_collection_singular_ids_getter_with_string_primary_keys
+ book = books(:awdr)
+ assert_equal 2, book.subscriber_ids.size
+ assert_equal [subscribers(:first).nick, subscribers(:second).nick].sort, book.subscriber_ids.sort
+ end
+
+ def test_collection_singular_ids_setter
+ company = companies(:rails_core)
+ dev = Developer.first
+
+ company.developer_ids = [dev.id]
+ assert_equal [dev], company.developers
+ end
+
+ def test_collection_singular_ids_setter_with_required_type_cast
+ company = companies(:rails_core)
+ dev = Developer.first
+
+ company.developer_ids = [dev.id.to_s]
+ assert_equal [dev], company.developers
+ end
+
+ def test_collection_singular_ids_setter_with_string_primary_keys
+ assert_nothing_raised do
+ book = books(:awdr)
+ book.subscriber_ids = [subscribers(:second).nick]
+ assert_equal [subscribers(:second)], book.subscribers.reload
+
+ book.subscriber_ids = []
+ assert_equal [], book.subscribers.reload
+ end
+ end
+
+ def test_collection_singular_ids_setter_raises_exception_when_invalid_ids_set
+ company = companies(:rails_core)
+ ids = [Developer.first.id, -9999]
+ e = assert_raises(ActiveRecord::RecordNotFound) { company.developer_ids = ids }
+ msg = "Couldn't find all Developers with 'id': (1, -9999) (found 1 results, but was looking for 2). Couldn't find Developer with id -9999."
+ assert_equal(msg, e.message)
+ end
+
+ def test_collection_singular_ids_through_setter_raises_exception_when_invalid_ids_set
+ author = authors(:david)
+ ids = [categories(:general).name, "Unknown"]
+ e = assert_raises(ActiveRecord::RecordNotFound) { author.essay_category_ids = ids }
+ msg = "Couldn't find all Categories with 'name': (General, Unknown) (found 1 results, but was looking for 2). Couldn't find Category with name Unknown."
+ assert_equal msg, e.message
+ end
+
+ def test_build_a_model_from_hm_through_association_with_where_clause
+ assert_nothing_raised { books(:awdr).subscribers.where(nick: "marklazz").build }
+ end
+
+ def test_attributes_are_being_set_when_initialized_from_hm_through_association_with_where_clause
+ new_subscriber = books(:awdr).subscribers.where(nick: "marklazz").build
+ assert_equal new_subscriber.nick, "marklazz"
+ end
+
+ def test_attributes_are_being_set_when_initialized_from_hm_through_association_with_multiple_where_clauses
+ new_subscriber = books(:awdr).subscribers.where(nick: "marklazz").where(name: "Marcelo Giorgi").build
+ assert_equal new_subscriber.nick, "marklazz"
+ assert_equal new_subscriber.name, "Marcelo Giorgi"
+ end
+
+ def test_include_method_in_association_through_should_return_true_for_instance_added_with_build
+ person = Person.new
+ reference = person.references.build
+ job = reference.build_job
+ assert_includes person.jobs, job
+ end
+
+ def test_include_method_in_association_through_should_return_true_for_instance_added_with_nested_builds
+ author = Author.new
+ post = author.posts.build
+ comment = post.comments.build
+ assert_includes author.comments, comment
+ end
+
+ def test_through_association_readonly_should_be_false
+ assert_not_predicate people(:michael).posts.first, :readonly?
+ assert_not_predicate people(:michael).posts.to_a.first, :readonly?
+ end
+
+ def test_can_update_through_association
+ assert_nothing_raised do
+ people(:michael).posts.first.update!(title: "Can write")
+ end
+ end
+
+ def test_has_many_through_polymorphic_with_rewhere
+ post = TaggedPost.create!(title: "Tagged", body: "Post")
+ tag = post.tags.create!(name: "Tag")
+ assert_equal [tag], TaggedPost.preload(:tags).last.tags
+ assert_equal [tag], TaggedPost.eager_load(:tags).last.tags
+ end
+
+ def test_has_many_through_polymorphic_with_primary_key_option
+ assert_equal [categories(:general)], authors(:david).essay_categories
+
+ authors = Author.joins(:essay_categories).where("categories.id" => categories(:general).id)
+ assert_equal authors(:david), authors.first
+
+ assert_equal [owners(:blackbeard)], authors(:david).essay_owners
+
+ authors = Author.joins(:essay_owners).where("owners.name = 'blackbeard'")
+ assert_equal authors(:david), authors.first
+ end
+
+ def test_has_many_through_with_primary_key_option
+ assert_equal [categories(:general)], authors(:david).essay_categories_2
+
+ authors = Author.joins(:essay_categories_2).where("categories.id" => categories(:general).id)
+ assert_equal authors(:david), authors.first
+ end
+
+ def test_size_of_through_association_should_increase_correctly_when_has_many_association_is_added
+ post = posts(:thinking)
+ readers = post.readers.size
+ post.people << people(:michael)
+ assert_equal readers + 1, post.readers.size
+ end
+
+ def test_has_many_through_with_default_scope_on_join_model
+ assert_equal posts(:welcome).comments.order("id").to_a, authors(:david).comments_on_first_posts
+ end
+
+ def test_create_has_many_through_with_default_scope_on_join_model
+ category = authors(:david).special_categories.create(name: "Foo")
+ assert_equal 1, category.categorizations.where(special: true).count
+ end
+
+ def test_joining_has_many_through_with_distinct
+ mary = Author.joins(:unique_categorized_posts).where(id: authors(:mary).id).first
+ assert_equal 1, mary.unique_categorized_posts.length
+ assert_equal 1, mary.unique_categorized_post_ids.length
+ end
+
+ def test_joining_has_many_through_belongs_to
+ posts = Post.joins(:author_categorizations).order("posts.id").
+ where("categorizations.id" => categorizations(:mary_thinking_sti).id)
+
+ assert_equal [posts(:eager_other), posts(:misc_by_mary), posts(:other_by_mary)], posts
+ end
+
+ def test_select_chosen_fields_only
+ author = authors(:david)
+ assert_equal ["body", "id"].sort, author.comments.select("comments.body").first.attributes.keys.sort
+ end
+
+ def test_get_has_many_through_belongs_to_ids_with_conditions
+ assert_equal [categories(:general).id], authors(:mary).categories_like_general_ids
+ end
+
+ def test_get_collection_singular_ids_on_has_many_through_with_conditions_and_include
+ person = Person.first
+ assert_equal person.posts_with_no_comment_ids, person.posts_with_no_comments.map(&:id)
+ end
+
+ def test_count_has_many_through_with_named_scope
+ assert_equal 2, authors(:mary).categories.count
+ assert_equal 1, authors(:mary).categories.general.count
+ end
+
+ def test_has_many_through_belongs_to_should_update_when_the_through_foreign_key_changes
+ post = posts(:eager_other)
+
+ post.author_categorizations
+ proxy = post.send(:association_instance_get, :author_categorizations)
+
+ assert_not_predicate proxy, :stale_target?
+ assert_equal authors(:mary).categorizations.sort_by(&:id), post.author_categorizations.sort_by(&:id)
+
+ post.author_id = authors(:david).id
+
+ assert_predicate proxy, :stale_target?
+ assert_equal authors(:david).categorizations.sort_by(&:id), post.author_categorizations.sort_by(&:id)
+ end
+
+ def test_create_with_conditions_hash_on_through_association
+ member = members(:groucho)
+ club = member.clubs.create!
+
+ assert_equal true, club.reload.membership.favourite
+ end
+
+ def test_deleting_from_has_many_through_a_belongs_to_should_not_try_to_update_counter
+ post = posts(:welcome)
+ address = author_addresses(:david_address)
+
+ assert_includes post.author_addresses, address
+ post.author_addresses.delete(address)
+ assert_predicate post[:author_count], :nil?
+ end
+
+ def test_primary_key_option_on_source
+ post = posts(:welcome)
+ category = categories(:general)
+ Categorization.create!(post_id: post.id, named_category_name: category.name)
+
+ assert_equal [category], post.named_categories
+ assert_equal [category.name], post.named_category_ids # checks when target loaded
+ assert_equal [category.name], post.reload.named_category_ids # checks when target no loaded
+ end
+
+ def test_create_should_not_raise_exception_when_join_record_has_errors
+ repair_validations(Categorization) do
+ Categorization.validate { |r| r.errors[:base] << "Invalid Categorization" }
+ Category.create(name: "Fishing", authors: [Author.first])
+ end
+ end
+
+ def test_assign_array_to_new_record_builds_join_records
+ c = Category.new(name: "Fishing", authors: [Author.first])
+ assert_equal 1, c.categorizations.size
+ end
+
+ def test_create_bang_should_raise_exception_when_join_record_has_errors
+ repair_validations(Categorization) do
+ Categorization.validate { |r| r.errors[:base] << "Invalid Categorization" }
+ assert_raises(ActiveRecord::RecordInvalid) do
+ Category.create!(name: "Fishing", authors: [Author.first])
+ end
+ end
+ end
+
+ def test_save_bang_should_raise_exception_when_join_record_has_errors
+ repair_validations(Categorization) do
+ Categorization.validate { |r| r.errors[:base] << "Invalid Categorization" }
+ c = Category.new(name: "Fishing", authors: [Author.first])
+ assert_raises(ActiveRecord::RecordInvalid) do
+ c.save!
+ end
+ end
+ end
+
+ def test_save_returns_falsy_when_join_record_has_errors
+ repair_validations(Categorization) do
+ Categorization.validate { |r| r.errors[:base] << "Invalid Categorization" }
+ c = Category.new(name: "Fishing", authors: [Author.first])
+ assert_not c.save
+ end
+ end
+
+ def test_preloading_empty_through_association_via_joins
+ person = Person.create!(first_name: "Gaga")
+ person = Person.where(id: person.id).where("readers.id = 1 or 1=1").references(:readers).includes(:posts).to_a.first
+
+ assert person.posts.loaded?, "person.posts should be loaded"
+ assert_equal [], person.posts
+ end
+
+ def test_preloading_empty_through_with_polymorphic_source_association
+ owner = Owner.create!(name: "Rainbow Unicat")
+ pet = Pet.create!(owner: owner)
+ person = Person.create!(first_name: "Gaga")
+ treasure = Treasure.create!(looter: person)
+ non_looted_treasure = Treasure.create!()
+ PetTreasure.create!(pet: pet, treasure: treasure, rainbow_color: "Ultra violet indigo")
+ PetTreasure.create!(pet: pet, treasure: non_looted_treasure, rainbow_color: "Ultra violet indigo")
+
+ assert_equal [person], Owner.where(name: "Rainbow Unicat").includes(pets: :persons).first.persons.to_a
+ end
+
+ def test_explicitly_joining_join_table
+ assert_equal owners(:blackbeard).toys, owners(:blackbeard).toys.with_pet
+ end
+
+ def test_has_many_through_with_polymorphic_source
+ post = tags(:general).tagged_posts.create! title: "foo", body: "bar"
+ assert_equal [tags(:general)], post.reload.tags
+ end
+
+ def test_has_many_through_obeys_order_on_through_association
+ owner = owners(:blackbeard)
+ assert_includes owner.toys.to_sql, "pets.name desc"
+ assert_equal ["parrot", "bulbul"], owner.toys.map { |r| r.pet.name }
+ end
+
+ def test_has_many_through_associations_sum_on_columns
+ post1 = Post.create(title: "active", body: "sample")
+ post2 = Post.create(title: "inactive", body: "sample")
+
+ person1 = Person.create(first_name: "aaron", followers_count: 1)
+ person2 = Person.create(first_name: "schmit", followers_count: 2)
+ person3 = Person.create(first_name: "bill", followers_count: 3)
+ person4 = Person.create(first_name: "cal", followers_count: 4)
+
+ Reader.create(post_id: post1.id, person_id: person1.id)
+ Reader.create(post_id: post1.id, person_id: person2.id)
+ Reader.create(post_id: post1.id, person_id: person3.id)
+ Reader.create(post_id: post1.id, person_id: person4.id)
+
+ Reader.create(post_id: post2.id, person_id: person1.id)
+ Reader.create(post_id: post2.id, person_id: person2.id)
+ Reader.create(post_id: post2.id, person_id: person3.id)
+ Reader.create(post_id: post2.id, person_id: person4.id)
+
+ active_persons = Person.joins(:readers).joins(:posts).distinct(true).where("posts.title" => "active")
+
+ assert_equal active_persons.map(&:followers_count).reduce(:+), 10
+ assert_equal active_persons.sum(:followers_count), 10
+ assert_equal active_persons.sum(:followers_count), active_persons.map(&:followers_count).reduce(:+)
+ end
+
+ def test_has_many_through_associations_on_new_records_use_null_relations
+ person = Person.new
+
+ assert_no_queries(ignore_none: false) do
+ assert_equal [], person.posts
+ assert_equal [], person.posts.where(body: "omg")
+ assert_equal [], person.posts.pluck(:body)
+ assert_equal 0, person.posts.sum(:tags_count)
+ assert_equal 0, person.posts.count
+ end
+ end
+
+ def test_has_many_through_with_default_scope_on_the_target
+ person = people(:michael)
+ assert_equal [posts(:thinking).id], person.first_posts.map(&:id)
+
+ readers(:michael_authorless).update(first_post_id: 1)
+ assert_equal [posts(:thinking).id], person.reload.first_posts.map(&:id)
+ end
+
+ def test_has_many_through_with_includes_in_through_association_scope
+ assert_not_empty posts(:welcome).author_address_extra_with_address
+ end
+
+ def test_insert_records_via_has_many_through_association_with_scope
+ club = Club.create!
+ member = Member.create!
+ Membership.create!(club: club, member: member)
+
+ club.favourites << member
+ assert_equal [member], club.favourites
+
+ club.reload
+ assert_equal [member], club.favourites
+ end
+
+ def test_has_many_through_unscope_default_scope
+ post = Post.create!(title: "Beaches", body: "I like beaches!")
+ Reader.create! person: people(:david), post: post
+ LazyReader.create! person: people(:susan), post: post
+
+ assert_equal 2, post.people.to_a.size
+ assert_equal 1, post.lazy_people.to_a.size
+
+ assert_equal 2, post.lazy_readers_unscope_skimmers.to_a.size
+ assert_equal 2, post.lazy_people_unscope_skimmers.to_a.size
+ end
+
+ def test_has_many_through_add_with_sti_middle_relation
+ club = SuperClub.create!(name: "Fight Club")
+ member = Member.create!(name: "Tyler Durden")
+
+ club.members << member
+ assert_equal 1, SuperMembership.where(member_id: member.id, club_id: club.id).count
+ end
+
+ def test_build_for_has_many_through_association
+ organization = organizations(:nsa)
+ author = organization.author
+ post_direct = author.posts.build
+ post_through = organization.posts.build
+ assert_equal post_direct.author_id, post_through.author_id
+ end
+
+ def test_has_many_through_with_scope_that_should_not_be_fully_merged
+ Club.has_many :distinct_memberships, -> { distinct }, class_name: "Membership"
+ Club.has_many :special_favourites, through: :distinct_memberships, source: :member
+
+ assert_nil Club.new.special_favourites.distinct_value
+ end
+
+ def test_has_many_through_do_not_cache_association_reader_if_the_though_method_has_default_scopes
+ member = Member.create!
+ club = Club.create!
+ TenantMembership.create!(
+ member: member,
+ club: club
+ )
+
+ TenantMembership.current_member = member
+
+ tenant_clubs = member.tenant_clubs
+ assert_equal [club], tenant_clubs
+
+ TenantMembership.current_member = nil
+
+ other_member = Member.create!
+ other_club = Club.create!
+ TenantMembership.create!(
+ member: other_member,
+ club: other_club
+ )
+
+ tenant_clubs = other_member.tenant_clubs
+ assert_equal [other_club], tenant_clubs
+ ensure
+ TenantMembership.current_member = nil
+ end
+
+ def test_has_many_through_with_scope_that_has_joined_same_table_with_parent_relation
+ assert_equal authors(:david), Author.joins(:comments_for_first_author).take
+ end
+
+ def test_has_many_through_with_unscope_should_affect_to_through_scope
+ assert_equal [comments(:eager_other_comment1)], authors(:mary).unordered_comments
+ end
+
+ def test_has_many_through_with_scope_should_accept_string_and_hash_join
+ assert_equal authors(:david), Author.joins({ comments_for_first_author: :post }, "inner join posts posts_alias on authors.id = posts_alias.author_id").eager_load(:categories).take
+ end
+
+ def test_has_many_through_with_scope_should_respect_table_alias
+ family = Family.create!
+ users = 3.times.map { User.create! }
+ FamilyTree.create!(member: users[0], family: family)
+ FamilyTree.create!(member: users[1], family: family)
+ FamilyTree.create!(member: users[2], family: family, token: "wat")
+
+ assert_equal 2, users[0].family_members.to_a.size
+ assert_equal 0, users[2].family_members.to_a.size
+ end
+
+ def test_through_scope_is_affected_by_unscoping
+ author = authors(:david)
+
+ expected = author.comments.to_a
+ FirstPost.unscoped do
+ assert_equal expected.sort_by(&:id), author.comments_on_first_posts.sort_by(&:id)
+ end
+ end
+
+ def test_through_scope_isnt_affected_by_scoping
+ author = authors(:david)
+
+ expected = author.comments_on_first_posts.to_a
+ FirstPost.where(id: 2).scoping do
+ author.comments_on_first_posts.reset
+ assert_equal expected.sort_by(&:id), author.comments_on_first_posts.sort_by(&:id)
+ end
+ end
+
+ def test_incorrectly_ordered_through_associations
+ assert_raises(ActiveRecord::HasManyThroughOrderError) do
+ DeveloperWithIncorrectlyOrderedHasManyThrough.create(
+ companies: [Company.create]
+ )
+ end
+ end
+
+ def test_has_many_through_update_ids_with_conditions
+ author = Author.create!(name: "Bill")
+ category = categories(:general)
+
+ author.update(
+ special_categories_with_condition_ids: [category.id],
+ nonspecial_categories_with_condition_ids: [category.id]
+ )
+
+ assert_equal [category.id], author.special_categories_with_condition_ids
+ assert_equal [category.id], author.nonspecial_categories_with_condition_ids
+
+ author.update(nonspecial_categories_with_condition_ids: [])
+ author.reload
+
+ assert_equal [category.id], author.special_categories_with_condition_ids
+ assert_equal [], author.nonspecial_categories_with_condition_ids
+ end
+
+ def test_single_has_many_through_association_with_unpersisted_parent_instance
+ post_with_single_has_many_through = Class.new(Post) do
+ def self.name; "PostWithSingleHasManyThrough"; end
+ has_many :subscriptions, through: :author
+ end
+ post = post_with_single_has_many_through.new
+
+ post.author = authors(:mary)
+ book1 = Book.create!(name: "essays on single has many through associations 1")
+ post.author.books << book1
+ subscription1 = Subscription.first
+ book1.subscriptions << subscription1
+ assert_equal [subscription1], post.subscriptions.to_a
+
+ post.author = authors(:bob)
+ book2 = Book.create!(name: "essays on single has many through associations 2")
+ post.author.books << book2
+ subscription2 = Subscription.second
+ book2.subscriptions << subscription2
+ assert_equal [subscription2], post.subscriptions.to_a
+ end
+
+ def test_nested_has_many_through_association_with_unpersisted_parent_instance
+ post_with_nested_has_many_through = Class.new(Post) do
+ def self.name; "PostWithNestedHasManyThrough"; end
+ has_many :books, through: :author
+ has_many :subscriptions, through: :books
+ end
+ post = post_with_nested_has_many_through.new
+
+ post.author = authors(:mary)
+ book1 = Book.create!(name: "essays on nested has many through associations 1")
+ post.author.books << book1
+ subscription1 = Subscription.first
+ book1.subscriptions << subscription1
+ assert_equal [subscription1], post.subscriptions.to_a
+
+ post.author = authors(:bob)
+ book2 = Book.create!(name: "essays on nested has many through associations 2")
+ post.author.books << book2
+ subscription2 = Subscription.second
+ book2.subscriptions << subscription2
+ assert_equal [subscription2], post.subscriptions.to_a
+ end
+
+ private
+ def make_model(name)
+ Class.new(ActiveRecord::Base) { define_singleton_method(:name) { name } }
+ end
+
+ def make_no_pk_hm_t
+ lesson = make_model "Lesson"
+ student = make_model "Student"
+
+ lesson_student = make_model "LessonStudent"
+ lesson_student.table_name = "lessons_students"
+
+ lesson_student.belongs_to :lesson, anonymous_class: lesson
+ lesson_student.belongs_to :student, anonymous_class: student
+ lesson.has_many :lesson_students, anonymous_class: lesson_student
+ lesson.has_many :students, through: :lesson_students, anonymous_class: student
+ [lesson, lesson_student, student]
+ end
+end
diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb
new file mode 100644
index 0000000000..1a213ef7e4
--- /dev/null
+++ b/activerecord/test/cases/associations/has_one_associations_test.rb
@@ -0,0 +1,728 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/developer"
+require "models/computer"
+require "models/project"
+require "models/company"
+require "models/ship"
+require "models/pirate"
+require "models/car"
+require "models/bulb"
+require "models/author"
+require "models/image"
+require "models/post"
+
+class HasOneAssociationsTest < ActiveRecord::TestCase
+ self.use_transactional_tests = false unless supports_savepoints?
+ fixtures :accounts, :companies, :developers, :projects, :developers_projects, :ships, :pirates, :authors, :author_addresses
+
+ def setup
+ Account.destroyed_account_ids.clear
+ end
+
+ def test_has_one
+ assert_equal companies(:first_firm).account, Account.find(1)
+ assert_equal Account.find(1).credit_limit, companies(:first_firm).account.credit_limit
+ end
+
+ def test_has_one_does_not_use_order_by
+ ActiveRecord::SQLCounter.clear_log
+ companies(:first_firm).account
+ ensure
+ log_all = ActiveRecord::SQLCounter.log_all
+ assert log_all.all? { |sql| /order by/i !~ sql }, "ORDER BY was used in the query: #{log_all}"
+ end
+
+ def test_has_one_cache_nils
+ firm = companies(:another_firm)
+ assert_queries(1) { assert_nil firm.account }
+ assert_queries(0) { assert_nil firm.account }
+
+ firms = Firm.all.merge!(includes: :account).to_a
+ assert_queries(0) { firms.each(&:account) }
+ end
+
+ def test_with_select
+ assert_equal Firm.find(1).account_with_select.attributes.size, 2
+ assert_equal Firm.all.merge!(includes: :account_with_select).find(1).account_with_select.attributes.size, 2
+ end
+
+ def test_finding_using_primary_key
+ firm = companies(:first_firm)
+ assert_equal Account.find_by_firm_id(firm.id), firm.account
+ firm.firm_id = companies(:rails_core).id
+ assert_equal accounts(:rails_core_account), firm.account_using_primary_key
+ end
+
+ def test_update_with_foreign_and_primary_keys
+ firm = companies(:first_firm)
+ account = firm.account_using_foreign_and_primary_keys
+ assert_equal Account.find_by_firm_name(firm.name), account
+ firm.save
+ firm.reload
+ assert_equal account, firm.account_using_foreign_and_primary_keys
+ end
+
+ def test_can_marshal_has_one_association_with_nil_target
+ firm = Firm.new
+ assert_nothing_raised do
+ assert_equal firm.attributes, Marshal.load(Marshal.dump(firm)).attributes
+ end
+
+ firm.account
+ assert_nothing_raised do
+ assert_equal firm.attributes, Marshal.load(Marshal.dump(firm)).attributes
+ end
+ end
+
+ def test_proxy_assignment
+ company = companies(:first_firm)
+ assert_nothing_raised { company.account = company.account }
+ end
+
+ def test_type_mismatch
+ assert_raise(ActiveRecord::AssociationTypeMismatch) { companies(:first_firm).account = 1 }
+ assert_raise(ActiveRecord::AssociationTypeMismatch) { companies(:first_firm).account = Project.find(1) }
+ end
+
+ def test_natural_assignment
+ apple = Firm.create("name" => "Apple")
+ citibank = Account.create("credit_limit" => 10)
+ apple.account = citibank
+ assert_equal apple.id, citibank.firm_id
+ end
+
+ def test_natural_assignment_to_nil
+ old_account_id = companies(:first_firm).account.id
+ companies(:first_firm).account = nil
+ companies(:first_firm).save
+ assert_nil companies(:first_firm).account
+ # account is dependent, therefore is destroyed when reference to owner is lost
+ assert_raise(ActiveRecord::RecordNotFound) { Account.find(old_account_id) }
+ end
+
+ def test_nullification_on_association_change
+ firm = companies(:rails_core)
+ old_account_id = firm.account.id
+ firm.account = Account.new(credit_limit: 5)
+ # account is dependent with nullify, therefore its firm_id should be nil
+ assert_nil Account.find(old_account_id).firm_id
+ end
+
+ def test_nullification_on_destroyed_association
+ developer = Developer.create!(name: "Someone")
+ ship = Ship.create!(name: "Planet Caravan", developer: developer)
+ ship.destroy
+ assert_not_predicate ship, :persisted?
+ assert_not_predicate developer, :persisted?
+ end
+
+ def test_natural_assignment_to_nil_after_destroy
+ firm = companies(:rails_core)
+ old_account_id = firm.account.id
+ firm.account.destroy
+ firm.account = nil
+ assert_nil companies(:rails_core).account
+ assert_raise(ActiveRecord::RecordNotFound) { Account.find(old_account_id) }
+ end
+
+ def test_association_change_calls_delete
+ companies(:first_firm).deletable_account = Account.new(credit_limit: 5)
+ assert_equal [], Account.destroyed_account_ids[companies(:first_firm).id]
+ end
+
+ def test_association_change_calls_destroy
+ companies(:first_firm).account = Account.new(credit_limit: 5)
+ assert_equal [companies(:first_firm).id], Account.destroyed_account_ids[companies(:first_firm).id]
+ end
+
+ def test_natural_assignment_to_already_associated_record
+ company = companies(:first_firm)
+ account = accounts(:signals37)
+ assert_equal company.account, account
+ company.account = account
+ company.reload
+ account.reload
+ assert_equal company.account, account
+ end
+
+ def test_dependence
+ num_accounts = Account.count
+
+ firm = Firm.find(1)
+ assert_not_nil firm.account
+ account_id = firm.account.id
+ assert_equal [], Account.destroyed_account_ids[firm.id]
+
+ firm.destroy
+ assert_equal num_accounts - 1, Account.count
+ assert_equal [account_id], Account.destroyed_account_ids[firm.id]
+ end
+
+ def test_exclusive_dependence
+ num_accounts = Account.count
+
+ firm = ExclusivelyDependentFirm.find(9)
+ assert_not_nil firm.account
+ assert_equal [], Account.destroyed_account_ids[firm.id]
+
+ firm.destroy
+ assert_equal num_accounts - 1, Account.count
+ assert_equal [], Account.destroyed_account_ids[firm.id]
+ end
+
+ def test_dependence_with_nil_associate
+ firm = DependentFirm.new(name: "nullify")
+ firm.save!
+ assert_nothing_raised { firm.destroy }
+ end
+
+ def test_restrict_with_exception
+ firm = RestrictedWithExceptionFirm.create!(name: "restrict")
+ firm.create_account(credit_limit: 10)
+
+ assert_not_nil firm.account
+
+ assert_raise(ActiveRecord::DeleteRestrictionError) { firm.destroy }
+ assert RestrictedWithExceptionFirm.exists?(name: "restrict")
+ assert_predicate firm.account, :present?
+ end
+
+ def test_restrict_with_error
+ firm = RestrictedWithErrorFirm.create!(name: "restrict")
+ firm.create_account(credit_limit: 10)
+
+ assert_not_nil firm.account
+
+ firm.destroy
+
+ assert_not_empty firm.errors
+ assert_equal "Cannot delete record because a dependent account exists", firm.errors[:base].first
+ assert RestrictedWithErrorFirm.exists?(name: "restrict")
+ assert_predicate firm.account, :present?
+ end
+
+ def test_restrict_with_error_with_locale
+ I18n.backend = I18n::Backend::Simple.new
+ I18n.backend.store_translations "en", activerecord: { attributes: { restricted_with_error_firm: { account: "firm account" } } }
+ firm = RestrictedWithErrorFirm.create!(name: "restrict")
+ firm.create_account(credit_limit: 10)
+
+ assert_not_nil firm.account
+
+ firm.destroy
+
+ assert_not_empty firm.errors
+ assert_equal "Cannot delete record because a dependent firm account exists", firm.errors[:base].first
+ assert RestrictedWithErrorFirm.exists?(name: "restrict")
+ assert_predicate firm.account, :present?
+ ensure
+ I18n.backend.reload!
+ end
+
+ def test_successful_build_association
+ firm = Firm.new("name" => "GlobalMegaCorp")
+ firm.save
+
+ account = firm.build_account("credit_limit" => 1000)
+ assert account.save
+ assert_equal account, firm.account
+ end
+
+ def test_build_association_dont_create_transaction
+ assert_no_queries(ignore_none: false) {
+ Firm.new.build_account
+ }
+ end
+
+ def test_building_the_associated_object_with_implicit_sti_base_class
+ firm = DependentFirm.new
+ company = firm.build_company
+ assert_kind_of Company, company, "Expected #{company.class} to be a Company"
+ end
+
+ def test_building_the_associated_object_with_explicit_sti_base_class
+ firm = DependentFirm.new
+ company = firm.build_company(type: "Company")
+ assert_kind_of Company, company, "Expected #{company.class} to be a Company"
+ end
+
+ def test_building_the_associated_object_with_sti_subclass
+ firm = DependentFirm.new
+ company = firm.build_company(type: "Client")
+ assert_kind_of Client, company, "Expected #{company.class} to be a Client"
+ end
+
+ def test_building_the_associated_object_with_an_invalid_type
+ firm = DependentFirm.new
+ assert_raise(ActiveRecord::SubclassNotFound) { firm.build_company(type: "Invalid") }
+ end
+
+ def test_building_the_associated_object_with_an_unrelated_type
+ firm = DependentFirm.new
+ assert_raise(ActiveRecord::SubclassNotFound) { firm.build_company(type: "Account") }
+ end
+
+ def test_build_and_create_should_not_happen_within_scope
+ pirate = pirates(:blackbeard)
+ scope = pirate.association(:foo_bulb).scope.where_values_hash
+
+ bulb = pirate.build_foo_bulb
+ assert_not_equal scope, bulb.scope_after_initialize.where_values_hash
+
+ bulb = pirate.create_foo_bulb
+ assert_not_equal scope, bulb.scope_after_initialize.where_values_hash
+
+ bulb = pirate.create_foo_bulb!
+ assert_not_equal scope, bulb.scope_after_initialize.where_values_hash
+ end
+
+ def test_create_association
+ firm = Firm.create(name: "GlobalMegaCorp")
+ account = firm.create_account(credit_limit: 1000)
+ assert_equal account, firm.reload.account
+ end
+
+ def test_create_association_with_bang
+ firm = Firm.create(name: "GlobalMegaCorp")
+ account = firm.create_account!(credit_limit: 1000)
+ assert_equal account, firm.reload.account
+ end
+
+ def test_create_association_with_bang_failing
+ firm = Firm.create(name: "GlobalMegaCorp")
+ assert_raise ActiveRecord::RecordInvalid do
+ firm.create_account!
+ end
+ account = firm.account
+ assert_not_nil account
+ account.credit_limit = 5
+ account.save
+ assert_equal account, firm.reload.account
+ end
+
+ def test_create_with_inexistent_foreign_key_failing
+ firm = Firm.create(name: "GlobalMegaCorp")
+
+ assert_raises(ActiveRecord::UnknownAttributeError) do
+ firm.create_account_with_inexistent_foreign_key
+ end
+ end
+
+ def test_create_when_parent_is_new_raises
+ firm = Firm.new
+ error = assert_raise(ActiveRecord::RecordNotSaved) do
+ firm.create_account
+ end
+
+ assert_equal "You cannot call create unless the parent is saved", error.message
+ end
+
+ def test_reload_association
+ odegy = companies(:odegy)
+
+ assert_equal 53, odegy.account.credit_limit
+ Account.where(id: odegy.account.id).update_all(credit_limit: 80)
+ assert_equal 53, odegy.account.credit_limit
+
+ assert_equal 80, odegy.reload_account.credit_limit
+ end
+
+ def test_build
+ firm = Firm.new("name" => "GlobalMegaCorp")
+ firm.save
+
+ firm.account = account = Account.new("credit_limit" => 1000)
+ assert_equal account, firm.account
+ assert account.save
+ assert_equal account, firm.account
+ end
+
+ def test_create
+ firm = Firm.new("name" => "GlobalMegaCorp")
+ firm.save
+ firm.account = account = Account.create("credit_limit" => 1000)
+ assert_equal account, firm.account
+ end
+
+ def test_create_before_save
+ firm = Firm.new("name" => "GlobalMegaCorp")
+ firm.account = account = Account.create("credit_limit" => 1000)
+ assert_equal account, firm.account
+ end
+
+ def test_dependence_with_missing_association
+ Account.destroy_all
+ firm = Firm.find(1)
+ assert_nil firm.account
+ firm.destroy
+ end
+
+ def test_dependence_with_missing_association_and_nullify
+ Account.destroy_all
+ firm = DependentFirm.first
+ assert_nil firm.account
+ firm.destroy
+ end
+
+ def test_finding_with_interpolated_condition
+ firm = Firm.first
+ superior = firm.clients.create(name: "SuperiorCo")
+ superior.rating = 10
+ superior.save
+ assert_equal 10, firm.clients_with_interpolated_conditions.first.rating
+ end
+
+ def test_assignment_before_child_saved
+ firm = Firm.find(1)
+ firm.account = a = Account.new("credit_limit" => 1000)
+ assert_predicate a, :persisted?
+ assert_equal a, firm.account
+ assert_equal a, firm.account
+ firm.association(:account).reload
+ assert_equal a, firm.account
+ end
+
+ def test_save_still_works_after_accessing_nil_has_one
+ jp = Company.new name: "Jaded Pixel"
+ jp.dummy_account.nil?
+
+ assert_nothing_raised do
+ jp.save!
+ end
+ end
+
+ def test_cant_save_readonly_association
+ assert_raise(ActiveRecord::ReadOnlyRecord) { companies(:first_firm).readonly_account.save! }
+ assert_predicate companies(:first_firm).readonly_account, :readonly?
+ end
+
+ def test_has_one_proxy_should_not_respond_to_private_methods
+ assert_raise(NoMethodError) { accounts(:signals37).private_method }
+ assert_raise(NoMethodError) { companies(:first_firm).account.private_method }
+ end
+
+ def test_has_one_proxy_should_respond_to_private_methods_via_send
+ accounts(:signals37).send(:private_method)
+ companies(:first_firm).account.send(:private_method)
+ end
+
+ def test_save_of_record_with_loaded_has_one
+ @firm = companies(:first_firm)
+ assert_not_nil @firm.account
+
+ assert_nothing_raised do
+ Firm.find(@firm.id).save!
+ Firm.all.merge!(includes: :account).find(@firm.id).save!
+ end
+
+ @firm.account.destroy
+
+ assert_nothing_raised do
+ Firm.find(@firm.id).save!
+ Firm.all.merge!(includes: :account).find(@firm.id).save!
+ end
+ end
+
+ def test_build_respects_hash_condition
+ account = companies(:first_firm).build_account_limit_500_with_hash_conditions
+ assert account.save
+ assert_equal 500, account.credit_limit
+ end
+
+ def test_create_respects_hash_condition
+ account = companies(:first_firm).create_account_limit_500_with_hash_conditions
+ assert_predicate account, :persisted?
+ assert_equal 500, account.credit_limit
+ end
+
+ def test_attributes_are_being_set_when_initialized_from_has_one_association_with_where_clause
+ new_account = companies(:first_firm).build_account(firm_name: "Account")
+ assert_equal new_account.firm_name, "Account"
+ end
+
+ def test_creation_failure_without_dependent_option
+ pirate = pirates(:blackbeard)
+ orig_ship = pirate.ship
+
+ assert_equal ships(:black_pearl), orig_ship
+ new_ship = pirate.create_ship
+ assert_not_equal ships(:black_pearl), new_ship
+ assert_equal new_ship, pirate.ship
+ assert_predicate new_ship, :new_record?
+ assert_nil orig_ship.pirate_id
+ assert !orig_ship.changed? # check it was saved
+ end
+
+ def test_creation_failure_with_dependent_option
+ pirate = pirates(:blackbeard).becomes(DestructivePirate)
+ orig_ship = pirate.dependent_ship
+
+ new_ship = pirate.create_dependent_ship
+ assert_predicate new_ship, :new_record?
+ assert_predicate orig_ship, :destroyed?
+ end
+
+ def test_creation_failure_due_to_new_record_should_raise_error
+ pirate = pirates(:redbeard)
+ new_ship = Ship.new
+
+ error = assert_raise(ActiveRecord::RecordNotSaved) do
+ pirate.ship = new_ship
+ end
+
+ assert_equal "Failed to save the new associated ship.", error.message
+ assert_nil pirate.ship
+ assert_nil new_ship.pirate_id
+ end
+
+ def test_replacement_failure_due_to_existing_record_should_raise_error
+ pirate = pirates(:blackbeard)
+ pirate.ship.name = nil
+
+ assert_not_predicate pirate.ship, :valid?
+ error = assert_raise(ActiveRecord::RecordNotSaved) do
+ pirate.ship = ships(:interceptor)
+ end
+
+ assert_equal ships(:black_pearl), pirate.ship
+ assert_equal pirate.id, pirate.ship.pirate_id
+ assert_equal "Failed to remove the existing associated ship. " \
+ "The record failed to save after its foreign key was set to nil.", error.message
+ end
+
+ def test_replacement_failure_due_to_new_record_should_raise_error
+ pirate = pirates(:blackbeard)
+ new_ship = Ship.new
+
+ error = assert_raise(ActiveRecord::RecordNotSaved) do
+ pirate.ship = new_ship
+ end
+
+ assert_equal "Failed to save the new associated ship.", error.message
+ assert_equal ships(:black_pearl), pirate.ship
+ assert_equal pirate.id, pirate.ship.pirate_id
+ assert_equal pirate.id, ships(:black_pearl).reload.pirate_id
+ assert_nil new_ship.pirate_id
+ end
+
+ def test_association_keys_bypass_attribute_protection
+ car = Car.create(name: "honda")
+
+ bulb = car.build_bulb
+ assert_equal car.id, bulb.car_id
+
+ bulb = car.build_bulb car_id: car.id + 1
+ assert_equal car.id, bulb.car_id
+
+ bulb = car.create_bulb
+ assert_equal car.id, bulb.car_id
+
+ bulb = car.create_bulb car_id: car.id + 1
+ assert_equal car.id, bulb.car_id
+ end
+
+ def test_association_protect_foreign_key
+ pirate = Pirate.create!(catchphrase: "Don' botharrr talkin' like one, savvy?")
+
+ ship = pirate.build_ship
+ assert_equal pirate.id, ship.pirate_id
+
+ ship = pirate.build_ship pirate_id: pirate.id + 1
+ assert_equal pirate.id, ship.pirate_id
+
+ ship = pirate.create_ship
+ assert_equal pirate.id, ship.pirate_id
+
+ ship = pirate.create_ship pirate_id: pirate.id + 1
+ assert_equal pirate.id, ship.pirate_id
+ end
+
+ def test_build_with_block
+ car = Car.create(name: "honda")
+
+ bulb = car.build_bulb { |b| b.color = "Red" }
+ assert_equal "RED!", bulb.color
+ end
+
+ def test_create_with_block
+ car = Car.create(name: "honda")
+
+ bulb = car.create_bulb { |b| b.color = "Red" }
+ assert_equal "RED!", bulb.color
+ end
+
+ def test_create_bang_with_block
+ car = Car.create(name: "honda")
+
+ bulb = car.create_bulb! { |b| b.color = "Red" }
+ assert_equal "RED!", bulb.color
+ end
+
+ def test_association_attributes_are_available_to_after_initialize
+ car = Car.create(name: "honda")
+ bulb = car.create_bulb
+
+ assert_equal car.id, bulb.attributes_after_initialize["car_id"]
+ end
+
+ def test_has_one_transaction
+ company = companies(:first_firm)
+ account = Account.find(1)
+
+ company.account # force loading
+ assert_no_queries { company.account = account }
+
+ company.account = nil
+ assert_no_queries { company.account = nil }
+ account = Account.find(2)
+ assert_queries { company.account = account }
+
+ assert_no_queries { Firm.new.account = account }
+ end
+
+ def test_has_one_assignment_dont_trigger_save_on_change_of_same_object
+ pirate = Pirate.create!(catchphrase: "Don' botharrr talkin' like one, savvy?")
+ ship = pirate.build_ship(name: "old name")
+ ship.save!
+
+ ship.name = "new name"
+ assert_predicate ship, :changed?
+ assert_queries(1) do
+ # One query for updating name, not triggering query for updating pirate_id
+ pirate.ship = ship
+ end
+
+ assert_equal "new name", pirate.ship.reload.name
+ end
+
+ def test_has_one_assignment_triggers_save_on_change_on_replacing_object
+ pirate = Pirate.create!(catchphrase: "Don' botharrr talkin' like one, savvy?")
+ ship = pirate.build_ship(name: "old name")
+ ship.save!
+
+ new_ship = Ship.create(name: "new name")
+ assert_queries(2) do
+ # One query to nullify the old ship, one query to update the new ship
+ pirate.ship = new_ship
+ end
+
+ assert_equal "new name", pirate.ship.reload.name
+ end
+
+ def test_has_one_autosave_with_primary_key_manually_set
+ post = Post.create(id: 1234, title: "Some title", body: "Some content")
+ author = Author.new(id: 33, name: "Hank Moody")
+
+ author.post = post
+ author.save
+ author.reload
+
+ assert_not_nil author.post
+ assert_equal author.post, post
+ end
+
+ def test_has_one_loading_for_new_record
+ post = Post.create!(author_id: 42, title: "foo", body: "bar")
+ author = Author.new(id: 42)
+ assert_equal post, author.post
+ end
+
+ def test_has_one_relationship_cannot_have_a_counter_cache
+ assert_raise(ArgumentError) do
+ Class.new(ActiveRecord::Base) do
+ has_one :thing, counter_cache: true
+ end
+ end
+ end
+
+ def test_with_polymorphic_has_one_with_custom_columns_name
+ post = Post.create! title: "foo", body: "bar"
+ image = Image.create!
+
+ post.main_image = image
+ post.reload
+
+ assert_equal image, post.main_image
+ end
+
+ test "dangerous association name raises ArgumentError" do
+ [:errors, "errors", :save, "save"].each do |name|
+ assert_raises(ArgumentError, "Association #{name} should not be allowed") do
+ Class.new(ActiveRecord::Base) do
+ has_one name
+ end
+ end
+ end
+ end
+
+ class SpecialBook < ActiveRecord::Base
+ self.table_name = "books"
+ belongs_to :author, class_name: "SpecialAuthor"
+ has_one :subscription, class_name: "SpecialSupscription", foreign_key: "subscriber_id"
+ end
+
+ class SpecialAuthor < ActiveRecord::Base
+ self.table_name = "authors"
+ has_one :book, class_name: "SpecialBook", foreign_key: "author_id"
+ end
+
+ class SpecialSupscription < ActiveRecord::Base
+ self.table_name = "subscriptions"
+ belongs_to :book, class_name: "SpecialBook"
+ end
+
+ def test_association_enum_works_properly
+ author = SpecialAuthor.create!(name: "Test")
+ book = SpecialBook.create!(status: "published")
+ author.book = book
+
+ refute_equal 0, SpecialAuthor.joins(:book).where(books: { status: "published" }).count
+ end
+
+ def test_association_enum_works_properly_with_nested_join
+ author = SpecialAuthor.create!(name: "Test")
+ book = SpecialBook.create!(status: "published")
+ author.book = book
+
+ where_clause = { books: { subscriptions: { subscriber_id: nil } } }
+ assert_nothing_raised do
+ SpecialAuthor.joins(book: :subscription).where.not(where_clause)
+ end
+ end
+
+ class DestroyByParentBook < ActiveRecord::Base
+ self.table_name = "books"
+ belongs_to :author, class_name: "DestroyByParentAuthor"
+ before_destroy :dont, unless: :destroyed_by_association
+
+ def dont
+ throw(:abort)
+ end
+ end
+
+ class DestroyByParentAuthor < ActiveRecord::Base
+ self.table_name = "authors"
+ has_one :book, class_name: "DestroyByParentBook", foreign_key: "author_id", dependent: :destroy
+ end
+
+ test "destroyed_by_association set in child destroy callback on parent destroy" do
+ author = DestroyByParentAuthor.create!(name: "Test")
+ book = DestroyByParentBook.create!(author: author)
+
+ author.destroy
+
+ assert_not DestroyByParentBook.exists?(book.id)
+ end
+
+ test "destroyed_by_association set in child destroy callback on replace" do
+ author = DestroyByParentAuthor.create!(name: "Test")
+ book = DestroyByParentBook.create!(author: author)
+
+ author.book = DestroyByParentBook.create!
+ author.save!
+
+ assert_not DestroyByParentBook.exists?(book.id)
+ 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
new file mode 100644
index 0000000000..9964f084ac
--- /dev/null
+++ b/activerecord/test/cases/associations/has_one_through_associations_test.rb
@@ -0,0 +1,404 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/club"
+require "models/member_type"
+require "models/member"
+require "models/membership"
+require "models/sponsor"
+require "models/organization"
+require "models/member_detail"
+require "models/minivan"
+require "models/dashboard"
+require "models/speedometer"
+require "models/category"
+require "models/author"
+require "models/essay"
+require "models/owner"
+require "models/post"
+require "models/comment"
+require "models/categorization"
+require "models/customer"
+require "models/carrier"
+require "models/shop_account"
+require "models/customer_carrier"
+
+class HasOneThroughAssociationsTest < ActiveRecord::TestCase
+ fixtures :member_types, :members, :clubs, :memberships, :sponsors, :organizations, :minivans,
+ :dashboards, :speedometers, :authors, :author_addresses, :posts, :comments, :categories, :essays, :owners
+
+ def setup
+ @member = members(:groucho)
+ end
+
+ def test_has_one_through_with_has_one
+ assert_equal clubs(:boring_club), @member.club
+ end
+
+ def test_creating_association_creates_through_record
+ new_member = Member.create(name: "Chris")
+ new_member.club = Club.create(name: "LRUG")
+ assert_not_nil new_member.current_membership
+ assert_not_nil new_member.club
+ end
+
+ def test_creating_association_builds_through_record
+ new_member = Member.create(name: "Chris")
+ new_club = new_member.association(:club).build
+ assert new_member.current_membership
+ assert_equal new_club, new_member.club
+ assert_predicate new_club, :new_record?
+ assert_predicate new_member.current_membership, :new_record?
+ assert new_member.save
+ assert_predicate new_club, :persisted?
+ assert_predicate new_member.current_membership, :persisted?
+ end
+
+ def test_creating_association_builds_through_record_for_new
+ new_member = Member.new(name: "Jane")
+ new_member.club = clubs(:moustache_club)
+ assert new_member.current_membership
+ assert_equal clubs(:moustache_club), new_member.current_membership.club
+ assert_equal clubs(:moustache_club), new_member.club
+ assert new_member.save
+ assert_equal clubs(:moustache_club), new_member.club
+ end
+
+ def test_creating_association_sets_both_parent_ids_for_new
+ member = Member.new(name: "Sean Griffin")
+ club = Club.new(name: "Da Club")
+
+ member.club = club
+
+ member.save!
+
+ assert member.id
+ assert club.id
+ assert_equal member.id, member.current_membership.member_id
+ assert_equal club.id, member.current_membership.club_id
+ end
+
+ def test_replace_target_record
+ new_club = Club.create(name: "Marx Bros")
+ @member.club = new_club
+ @member.reload
+ assert_equal new_club, @member.club
+ end
+
+ def test_replacing_target_record_deletes_old_association
+ assert_no_difference "Membership.count" do
+ new_club = Club.create(name: "Bananarama")
+ @member.club = new_club
+ @member.reload
+ end
+ end
+
+ def test_set_record_to_nil_should_delete_association
+ @member.club = nil
+ @member.reload
+ assert_nil @member.current_membership
+ assert_nil @member.club
+ end
+
+ def test_set_record_after_delete_association
+ @member.club = nil
+ @member.club = clubs(:moustache_club)
+ @member.reload
+ assert_equal clubs(:moustache_club), @member.club
+ end
+
+ def test_has_one_through_polymorphic
+ assert_equal clubs(:moustache_club), @member.sponsor_club
+ end
+
+ def test_has_one_through_eager_loading
+ members = assert_queries(3) do # base table, through table, clubs table
+ Member.all.merge!(includes: :club, where: ["name = ?", "Groucho Marx"]).to_a
+ end
+ assert_equal 1, members.size
+ assert_not_nil assert_no_queries { members[0].club }
+ end
+
+ def test_has_one_through_eager_loading_through_polymorphic
+ members = assert_queries(3) do # base table, through table, clubs table
+ Member.all.merge!(includes: :sponsor_club, where: ["name = ?", "Groucho Marx"]).to_a
+ end
+ assert_equal 1, members.size
+ assert_not_nil assert_no_queries { members[0].sponsor_club }
+ end
+
+ def test_has_one_through_with_conditions_eager_loading
+ # conditions on the through table
+ assert_equal clubs(:moustache_club), Member.all.merge!(includes: :favourite_club).find(@member.id).favourite_club
+ memberships(:membership_of_favourite_club).update_columns(favourite: false)
+ assert_nil Member.all.merge!(includes: :favourite_club).find(@member.id).reload.favourite_club
+
+ # conditions on the source table
+ assert_equal clubs(:moustache_club), Member.all.merge!(includes: :hairy_club).find(@member.id).hairy_club
+ clubs(:moustache_club).update_columns(name: "Association of Clean-Shaven Persons")
+ assert_nil Member.all.merge!(includes: :hairy_club).find(@member.id).reload.hairy_club
+ end
+
+ def test_has_one_through_polymorphic_with_source_type
+ assert_equal members(:groucho), clubs(:moustache_club).sponsored_member
+ end
+
+ def test_eager_has_one_through_polymorphic_with_source_type
+ clubs = Club.all.merge!(includes: :sponsored_member, where: ["name = ?", "Moustache and Eyebrow Fancier Club"]).to_a
+ # Only the eyebrow fanciers club has a sponsored_member
+ assert_not_nil assert_no_queries { clubs[0].sponsored_member }
+ end
+
+ def test_has_one_through_nonpreload_eagerloading
+ members = assert_queries(1) do
+ Member.all.merge!(includes: :club, where: ["members.name = ?", "Groucho Marx"], order: "clubs.name").to_a # force fallback
+ end
+ assert_equal 1, members.size
+ assert_not_nil assert_no_queries { members[0].club }
+ end
+
+ def test_has_one_through_nonpreload_eager_loading_through_polymorphic
+ members = assert_queries(1) do
+ Member.all.merge!(includes: :sponsor_club, where: ["members.name = ?", "Groucho Marx"], order: "clubs.name").to_a # force fallback
+ end
+ assert_equal 1, members.size
+ assert_not_nil assert_no_queries { members[0].sponsor_club }
+ end
+
+ def test_has_one_through_nonpreload_eager_loading_through_polymorphic_with_more_than_one_through_record
+ Sponsor.new(sponsor_club: clubs(:crazy_club), sponsorable: members(:groucho)).save!
+ members = assert_queries(1) do
+ Member.all.merge!(includes: :sponsor_club, where: ["members.name = ?", "Groucho Marx"], order: "clubs.name DESC").to_a # force fallback
+ end
+ assert_equal 1, members.size
+ assert_not_nil assert_no_queries { members[0].sponsor_club }
+ assert_equal clubs(:crazy_club), members[0].sponsor_club
+ end
+
+ def test_uninitialized_has_one_through_should_return_nil_for_unsaved_record
+ assert_nil Member.new.club
+ end
+
+ def test_assigning_association_correctly_assigns_target
+ new_member = Member.create(name: "Chris")
+ new_member.club = new_club = Club.create(name: "LRUG")
+ assert_equal new_club, new_member.association(:club).target
+ end
+
+ def test_has_one_through_proxy_should_not_respond_to_private_methods
+ assert_raise(NoMethodError) { clubs(:moustache_club).private_method }
+ assert_raise(NoMethodError) { @member.club.private_method }
+ end
+
+ def test_has_one_through_proxy_should_respond_to_private_methods_via_send
+ clubs(:moustache_club).send(:private_method)
+ @member.club.send(:private_method)
+ end
+
+ def test_assigning_to_has_one_through_preserves_decorated_join_record
+ @organization = organizations(:nsa)
+ assert_difference "MemberDetail.count", 1 do
+ @member_detail = MemberDetail.new(extra_data: "Extra")
+ @member.member_detail = @member_detail
+ @member.organization = @organization
+ end
+ assert_equal @organization, @member.organization
+ assert_includes @organization.members, @member
+ assert_equal "Extra", @member.member_detail.extra_data
+ end
+
+ def test_reassigning_has_one_through
+ @organization = organizations(:nsa)
+ @new_organization = organizations(:discordians)
+
+ assert_difference "MemberDetail.count", 1 do
+ @member_detail = MemberDetail.new(extra_data: "Extra")
+ @member.member_detail = @member_detail
+ @member.organization = @organization
+ end
+ assert_equal @organization, @member.organization
+ assert_equal "Extra", @member.member_detail.extra_data
+ assert_includes @organization.members, @member
+ assert_not_includes @new_organization.members, @member
+
+ assert_no_difference "MemberDetail.count" do
+ @member.organization = @new_organization
+ end
+ assert_equal @new_organization, @member.organization
+ assert_equal "Extra", @member.member_detail.extra_data
+ assert_not_includes @organization.members, @member
+ assert_includes @new_organization.members, @member
+ end
+
+ def test_preloading_has_one_through_on_belongs_to
+ MemberDetail.delete_all
+ assert_not_nil @member.member_type
+ @organization = organizations(:nsa)
+ @member_detail = MemberDetail.new
+ @member.member_detail = @member_detail
+ @member.organization = @organization
+ @member_details = assert_queries(3) do
+ MemberDetail.all.merge!(includes: :member_type).to_a
+ end
+ @new_detail = @member_details[0]
+ assert_predicate @new_detail.send(:association, :member_type), :loaded?
+ assert_no_queries { @new_detail.member_type }
+ end
+
+ def test_save_of_record_with_loaded_has_one_through
+ @club = @member.club
+ assert_not_nil @club.sponsored_member
+
+ assert_nothing_raised do
+ Club.find(@club.id).save!
+ Club.all.merge!(includes: :sponsored_member).find(@club.id).save!
+ end
+
+ @club.sponsor.destroy
+
+ assert_nothing_raised do
+ Club.find(@club.id).save!
+ Club.all.merge!(includes: :sponsored_member).find(@club.id).save!
+ end
+ end
+
+ def test_through_belongs_to_after_destroy
+ @member_detail = MemberDetail.new(extra_data: "Extra")
+ @member.member_detail = @member_detail
+ @member.save!
+
+ assert_not_nil @member_detail.member_type
+ @member_detail.destroy
+ assert_queries(1) do
+ @member_detail.association(:member_type).reload
+ assert_not_nil @member_detail.member_type
+ end
+
+ @member_detail.member.destroy
+ assert_queries(1) do
+ @member_detail.association(:member_type).reload
+ assert_nil @member_detail.member_type
+ end
+ end
+
+ def test_value_is_properly_quoted
+ minivan = Minivan.find("m1")
+ assert_nothing_raised do
+ 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
+
+ def test_has_one_through_with_default_scope_on_join_model
+ assert_equal posts(:welcome).comments.order("id").first, authors(:david).comment_on_first_post
+ end
+
+ def test_has_one_through_many_raises_exception
+ assert_raise(ActiveRecord::HasOneThroughCantAssociateThroughCollection) do
+ members(:groucho).club_through_many
+ end
+ end
+
+ def test_has_one_through_polymorphic_association
+ assert_raise(ActiveRecord::HasOneAssociationPolymorphicThroughError) do
+ @member.premium_club
+ end
+ end
+
+ def test_has_one_through_belongs_to_should_update_when_the_through_foreign_key_changes
+ minivan = minivans(:cool_first)
+
+ minivan.dashboard
+ proxy = minivan.send(:association_instance_get, :dashboard)
+
+ assert_not_predicate proxy, :stale_target?
+ assert_equal dashboards(:cool_first), minivan.dashboard
+
+ minivan.speedometer_id = speedometers(:second).id
+
+ assert_predicate proxy, :stale_target?
+ assert_equal dashboards(:second), minivan.dashboard
+ end
+
+ def test_has_one_through_belongs_to_setting_belongs_to_foreign_key_after_nil_target_loaded
+ minivan = Minivan.new
+
+ minivan.dashboard
+ proxy = minivan.send(:association_instance_get, :dashboard)
+
+ minivan.speedometer_id = speedometers(:second).id
+
+ assert_predicate proxy, :stale_target?
+ assert_equal dashboards(:second), minivan.dashboard
+ end
+
+ def test_assigning_has_one_through_belongs_to_with_new_record_owner
+ minivan = Minivan.new
+ dashboard = dashboards(:cool_first)
+
+ minivan.dashboard = dashboard
+
+ assert_equal dashboard, minivan.dashboard
+ assert_equal dashboard, minivan.speedometer.dashboard
+ end
+
+ def test_has_one_through_with_custom_select_on_join_model_default_scope
+ assert_equal clubs(:boring_club), members(:groucho).selected_club
+ end
+
+ def test_has_one_through_relationship_cannot_have_a_counter_cache
+ assert_raise(ArgumentError) do
+ Class.new(ActiveRecord::Base) do
+ has_one :thing, through: :other_thing, counter_cache: true
+ end
+ end
+ end
+
+ def test_has_one_through_do_not_cache_association_reader_if_the_though_method_has_default_scopes
+ customer = Customer.create!
+ carrier = Carrier.create!
+ customer_carrier = CustomerCarrier.create!(
+ customer: customer,
+ carrier: carrier,
+ )
+ account = ShopAccount.create!(customer_carrier: customer_carrier)
+
+ CustomerCarrier.current_customer = customer
+
+ account_carrier = account.carrier
+ assert_equal carrier, account_carrier
+
+ CustomerCarrier.current_customer = nil
+
+ other_carrier = Carrier.create!
+ other_customer = Customer.create!
+ other_customer_carrier = CustomerCarrier.create!(
+ customer: other_customer,
+ carrier: other_carrier,
+ )
+ other_account = ShopAccount.create!(customer_carrier: other_customer_carrier)
+
+ account_carrier = other_account.carrier
+ assert_equal other_carrier, account_carrier
+ ensure
+ CustomerCarrier.current_customer = nil
+ end
+end
diff --git a/activerecord/test/cases/associations/inner_join_association_test.rb b/activerecord/test/cases/associations/inner_join_association_test.rb
new file mode 100644
index 0000000000..ca0620dc3b
--- /dev/null
+++ b/activerecord/test/cases/associations/inner_join_association_test.rb
@@ -0,0 +1,159 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/post"
+require "models/comment"
+require "models/author"
+require "models/essay"
+require "models/category"
+require "models/categorization"
+require "models/person"
+require "models/tagging"
+require "models/tag"
+
+class InnerJoinAssociationTest < ActiveRecord::TestCase
+ fixtures :authors, :author_addresses, :essays, :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
+ assert_equal authors(:david), result.first
+ end
+
+ def test_construct_finder_sql_does_not_table_name_collide_on_duplicate_associations
+ assert_nothing_raised do
+ sql = Person.joins(agents: { agents: :agents }).joins(agents: { agents: { primary_contact: :agents } }).to_sql
+ assert_match(/agents_people_4/i, sql)
+ end
+ end
+
+ def test_construct_finder_sql_does_not_table_name_collide_on_duplicate_associations_with_left_outer_joins
+ sql = Person.joins(agents: :agents).left_outer_joins(agents: :agents).to_sql
+ assert_match(/agents_people_4/i, sql)
+ end
+
+ def test_construct_finder_sql_does_not_table_name_collide_with_string_joins
+ sql = Person.joins(:agents).joins("JOIN people agents_people ON agents_people.primary_contact_id = people.id").to_sql
+ assert_match(/agents_people_2/i, sql)
+ end
+
+ def test_construct_finder_sql_does_not_table_name_collide_with_aliased_joins
+ people = Person.arel_table
+ agents = people.alias("agents_people")
+ constraint = agents[:primary_contact_id].eq(people[:id])
+ sql = Person.joins(:agents).joins(agents.create_join(agents, agents.create_on(constraint))).to_sql
+ assert_match(/agents_people_2/i, sql)
+ end
+
+ def test_construct_finder_sql_ignores_empty_joins_hash
+ sql = Author.joins({}).to_sql
+ assert_no_match(/JOIN/i, sql)
+ end
+
+ def test_construct_finder_sql_ignores_empty_joins_array
+ sql = Author.joins([]).to_sql
+ assert_no_match(/JOIN/i, sql)
+ end
+
+ def test_join_conditions_added_to_join_clause
+ sql = Author.joins(:essays).to_sql
+ assert_match(/writer_type.*?=.*?Author/i, sql)
+ assert_no_match(/WHERE/i, sql)
+ end
+
+ def test_join_association_conditions_support_string_and_arel_expressions
+ assert_equal 0, Author.joins(:welcome_posts_with_one_comment).count
+ assert_equal 1, Author.joins(:welcome_posts_with_comments).count
+ end
+
+ def test_join_conditions_allow_nil_associations
+ authors = Author.includes(:essays).where(essays: { id: nil })
+ assert_equal 2, authors.count
+ end
+
+ def test_find_with_implicit_inner_joins_without_select_does_not_imply_readonly
+ authors = Author.joins(:posts)
+ assert_not authors.empty?, "expected authors to be non-empty"
+ assert authors.none?(&:readonly?), "expected no authors to be readonly"
+ end
+
+ def test_find_with_implicit_inner_joins_honors_readonly_with_select
+ authors = Author.joins(:posts).select("authors.*").to_a
+ assert !authors.empty?, "expected authors to be non-empty"
+ assert authors.all? { |a| !a.readonly? }, "expected no authors to be readonly"
+ end
+
+ def test_find_with_implicit_inner_joins_honors_readonly_false
+ authors = Author.joins(:posts).readonly(false).to_a
+ assert !authors.empty?, "expected authors to be non-empty"
+ assert authors.all? { |a| !a.readonly? }, "expected no authors to be readonly"
+ end
+
+ def test_find_with_implicit_inner_joins_does_not_set_associations
+ authors = Author.joins(:posts).select("authors.*").to_a
+ assert !authors.empty?, "expected authors to be non-empty"
+ assert authors.all? { |a| !a.instance_variable_defined?(:@posts) }, "expected no authors to have the @posts association loaded"
+ end
+
+ def test_count_honors_implicit_inner_joins
+ real_count = Author.all.to_a.sum { |a| a.posts.count }
+ assert_equal real_count, Author.joins(:posts).count, "plain inner join count should match the number of referenced posts records"
+ end
+
+ def test_calculate_honors_implicit_inner_joins
+ real_count = Author.all.to_a.sum { |a| a.posts.count }
+ assert_equal real_count, Author.joins(:posts).calculate(:count, "authors.id"), "plain inner join count should match the number of referenced posts records"
+ end
+
+ def test_calculate_honors_implicit_inner_joins_and_distinct_and_conditions
+ real_count = Author.all.to_a.select { |a| a.posts.any? { |p| p.title.start_with?("Welcome") } }.length
+ authors_with_welcoming_post_titles = Author.all.merge!(joins: :posts, where: "posts.title like 'Welcome%'").distinct.calculate(:count, "authors.id")
+ 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_empty scope.where("comments.type" => "Comment")
+ assert_not_empty scope.where("comments.type" => "SpecialComment")
+ assert_not_empty scope.where("comments.type" => "SubSpecialComment")
+ end
+
+ def test_find_with_conditions_on_reflection
+ assert_not_empty posts(:welcome).comments
+ assert Post.joins(:nonexistent_comments).where(id: posts(:welcome).id).empty? # [sic!]
+ end
+
+ def test_find_with_conditions_on_through_reflection
+ assert_not_empty posts(:welcome).tags
+ assert_empty Post.joins(:misc_tags).where(id: posts(:welcome).id)
+ end
+
+ test "the default scope of the target is applied when joining associations" do
+ author = Author.create! name: "Jon"
+ author.categorizations.create!
+ author.categorizations.create! special: true
+
+ assert_equal [author], Author.where(id: author).joins(:special_categorizations)
+ end
+
+ test "the default scope of the target is correctly aliased when joining associations" do
+ author = Author.create! name: "Jon"
+ author.categories.create! name: "Not Special"
+ author.special_categories.create! name: "Special"
+
+ categories = author.categories.includes(:special_categorizations).references(:special_categorizations).to_a
+ assert_equal 2, categories.size
+ end
+
+ test "the correct records are loaded when including an aliased association" do
+ author = Author.create! name: "Jon"
+ author.categories.create! name: "Not Special"
+ author.special_categories.create! name: "Special"
+
+ categories = author.categories.eager_load(:special_categorizations).order(:name).to_a
+ assert_equal 0, categories.first.special_categorizations.size
+ assert_equal 1, categories.second.special_categorizations.size
+ end
+end
diff --git a/activerecord/test/cases/associations/inverse_associations_test.rb b/activerecord/test/cases/associations/inverse_associations_test.rb
new file mode 100644
index 0000000000..bad1bcdb67
--- /dev/null
+++ b/activerecord/test/cases/associations/inverse_associations_test.rb
@@ -0,0 +1,740 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/man"
+require "models/face"
+require "models/interest"
+require "models/zine"
+require "models/club"
+require "models/sponsor"
+require "models/rating"
+require "models/comment"
+require "models/car"
+require "models/bulb"
+require "models/mixed_case_monkey"
+require "models/admin"
+require "models/admin/account"
+require "models/admin/user"
+require "models/developer"
+require "models/company"
+require "models/project"
+require "models/author"
+require "models/post"
+
+class AutomaticInverseFindingTests < ActiveRecord::TestCase
+ fixtures :ratings, :comments, :cars
+
+ def test_has_one_and_belongs_to_should_find_inverse_automatically_on_multiple_word_name
+ monkey_reflection = MixedCaseMonkey.reflect_on_association(:man)
+ man_reflection = Man.reflect_on_association(:mixed_case_monkey)
+
+ assert monkey_reflection.has_inverse?, "The monkey reflection should have an inverse"
+ assert_equal man_reflection, monkey_reflection.inverse_of, "The monkey reflection's inverse should be the man reflection"
+
+ assert man_reflection.has_inverse?, "The man reflection should have an inverse"
+ assert_equal monkey_reflection, man_reflection.inverse_of, "The man reflection's inverse should be the monkey reflection"
+ end
+
+ def test_has_many_and_belongs_to_should_find_inverse_automatically_for_model_in_module
+ account_reflection = Admin::Account.reflect_on_association(:users)
+ user_reflection = Admin::User.reflect_on_association(:account)
+
+ assert account_reflection.has_inverse?, "The Admin::Account reflection should have an inverse"
+ assert_equal user_reflection, account_reflection.inverse_of, "The Admin::Account reflection's inverse should be the Admin::User reflection"
+ end
+
+ def test_has_one_and_belongs_to_should_find_inverse_automatically
+ car_reflection = Car.reflect_on_association(:bulb)
+ bulb_reflection = Bulb.reflect_on_association(:car)
+
+ assert car_reflection.has_inverse?, "The Car reflection should have an inverse"
+ assert_equal bulb_reflection, car_reflection.inverse_of, "The Car reflection's inverse should be the Bulb reflection"
+
+ assert bulb_reflection.has_inverse?, "The Bulb reflection should have an inverse"
+ assert_equal car_reflection, bulb_reflection.inverse_of, "The Bulb reflection's inverse should be the Car reflection"
+ end
+
+ def test_has_many_and_belongs_to_should_find_inverse_automatically
+ comment_reflection = Comment.reflect_on_association(:ratings)
+ rating_reflection = Rating.reflect_on_association(:comment)
+
+ assert comment_reflection.has_inverse?, "The Comment reflection should have an inverse"
+ assert_equal rating_reflection, comment_reflection.inverse_of, "The Comment reflection's inverse should be the Rating reflection"
+ end
+
+ def test_has_many_and_belongs_to_should_find_inverse_automatically_for_sti
+ author_reflection = Author.reflect_on_association(:posts)
+ author_child_reflection = Author.reflect_on_association(:special_posts)
+ post_reflection = Post.reflect_on_association(:author)
+
+ assert_respond_to author_reflection, :has_inverse?
+ assert author_reflection.has_inverse?, "The Author reflection should have an inverse"
+ assert_equal post_reflection, author_reflection.inverse_of, "The Author reflection's inverse should be the Post reflection"
+
+ assert_respond_to author_child_reflection, :has_inverse?
+ assert author_child_reflection.has_inverse?, "The Author reflection should have an inverse"
+ assert_equal post_reflection, author_child_reflection.inverse_of, "The Author reflection's inverse should be the Post reflection"
+ end
+
+ def test_has_one_and_belongs_to_automatic_inverse_shares_objects
+ car = Car.first
+ bulb = Bulb.create!(car: car)
+
+ assert_equal car.bulb, bulb, "The Car's bulb should be the original bulb"
+
+ car.bulb.color = "Blue"
+ assert_equal car.bulb.color, bulb.color, "Changing the bulb's color on the car association should change the bulb's color"
+
+ bulb.color = "Red"
+ assert_equal bulb.color, car.bulb.color, "Changing the bulb's color should change the bulb's color on the car association"
+ end
+
+ def test_has_many_and_belongs_to_automatic_inverse_shares_objects_on_rating
+ comment = Comment.first
+ rating = Rating.create!(comment: comment)
+
+ assert_equal rating.comment, comment, "The Rating's comment should be the original Comment"
+
+ rating.comment.body = "Fennec foxes are the smallest of the foxes."
+ assert_equal rating.comment.body, comment.body, "Changing the Comment's body on the association should change the original Comment's body"
+
+ comment.body = "Kittens are adorable."
+ assert_equal comment.body, rating.comment.body, "Changing the original Comment's body should change the Comment's body on the association"
+ end
+
+ def test_has_many_and_belongs_to_automatic_inverse_shares_objects_on_comment
+ rating = Rating.create!
+ comment = Comment.first
+ rating.comment = comment
+
+ assert_equal rating.comment, comment, "The Rating's comment should be the original Comment"
+
+ rating.comment.body = "Fennec foxes are the smallest of the foxes."
+ assert_equal rating.comment.body, comment.body, "Changing the Comment's body on the association should change the original Comment's body"
+
+ comment.body = "Kittens are adorable."
+ assert_equal comment.body, rating.comment.body, "Changing the original Comment's body should change the Comment's body on the association"
+ end
+
+ def test_polymorphic_and_has_many_through_relationships_should_not_have_inverses
+ sponsor_reflection = Sponsor.reflect_on_association(:sponsorable)
+
+ assert !sponsor_reflection.has_inverse?, "A polymorphic association should not find an inverse automatically"
+
+ club_reflection = Club.reflect_on_association(:members)
+
+ assert !club_reflection.has_inverse?, "A has_many_through association should not find an inverse automatically"
+ end
+
+ def test_polymorphic_has_one_should_find_inverse_automatically
+ man_reflection = Man.reflect_on_association(:polymorphic_face_without_inverse)
+
+ assert_predicate man_reflection, :has_inverse?
+ end
+end
+
+class InverseAssociationTests < ActiveRecord::TestCase
+ def test_should_allow_for_inverse_of_options_in_associations
+ assert_nothing_raised do
+ Class.new(ActiveRecord::Base).has_many(:wheels, inverse_of: :car)
+ end
+
+ assert_nothing_raised do
+ Class.new(ActiveRecord::Base).has_one(:engine, inverse_of: :car)
+ end
+
+ assert_nothing_raised do
+ Class.new(ActiveRecord::Base).belongs_to(:car, inverse_of: :driver)
+ end
+ end
+
+ def test_should_be_able_to_ask_a_reflection_if_it_has_an_inverse
+ has_one_with_inverse_ref = Man.reflect_on_association(:face)
+ assert_predicate has_one_with_inverse_ref, :has_inverse?
+
+ has_many_with_inverse_ref = Man.reflect_on_association(:interests)
+ assert_predicate has_many_with_inverse_ref, :has_inverse?
+
+ belongs_to_with_inverse_ref = Face.reflect_on_association(:man)
+ assert_predicate belongs_to_with_inverse_ref, :has_inverse?
+
+ has_one_without_inverse_ref = Club.reflect_on_association(:sponsor)
+ assert_not_predicate has_one_without_inverse_ref, :has_inverse?
+
+ has_many_without_inverse_ref = Club.reflect_on_association(:memberships)
+ assert_not_predicate has_many_without_inverse_ref, :has_inverse?
+
+ belongs_to_without_inverse_ref = Sponsor.reflect_on_association(:sponsor_club)
+ assert_not_predicate belongs_to_without_inverse_ref, :has_inverse?
+ end
+
+ def test_inverse_of_method_should_supply_the_actual_reflection_instance_it_is_the_inverse_of
+ has_one_ref = Man.reflect_on_association(:face)
+ assert_equal Face.reflect_on_association(:man), has_one_ref.inverse_of
+
+ has_many_ref = Man.reflect_on_association(:interests)
+ assert_equal Interest.reflect_on_association(:man), has_many_ref.inverse_of
+
+ belongs_to_ref = Face.reflect_on_association(:man)
+ assert_equal Man.reflect_on_association(:face), belongs_to_ref.inverse_of
+ end
+
+ def test_associations_with_no_inverse_of_should_return_nil
+ has_one_ref = Club.reflect_on_association(:sponsor)
+ assert_nil has_one_ref.inverse_of
+
+ has_many_ref = Club.reflect_on_association(:memberships)
+ assert_nil has_many_ref.inverse_of
+
+ belongs_to_ref = Sponsor.reflect_on_association(:sponsor_club)
+ assert_nil belongs_to_ref.inverse_of
+ end
+
+ def test_this_inverse_stuff
+ firm = Firm.create!(name: "Adequate Holdings")
+ Project.create!(name: "Project 1", firm: firm)
+ Developer.create!(name: "Gorbypuff", firm: firm)
+
+ new_project = Project.last
+ assert Project.reflect_on_association(:lead_developer).inverse_of.present?, "Expected inverse of to be present"
+ assert new_project.lead_developer.present?, "Expected lead developer to be present on the project"
+ end
+end
+
+class InverseHasOneTests < ActiveRecord::TestCase
+ fixtures :men, :faces
+
+ def test_parent_instance_should_be_shared_with_child_on_find
+ m = men(:gordon)
+ f = m.face
+ assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance"
+ m.name = "Bongo"
+ assert_equal m.name, f.man.name, "Name of man should be the same after changes to parent instance"
+ f.man.name = "Mungo"
+ assert_equal m.name, f.man.name, "Name of man should be the same after changes to child-owned instance"
+ end
+
+ def test_parent_instance_should_be_shared_with_eager_loaded_child_on_find
+ m = Man.all.merge!(where: { name: "Gordon" }, includes: :face).first
+ f = m.face
+ assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance"
+ m.name = "Bongo"
+ assert_equal m.name, f.man.name, "Name of man should be the same after changes to parent instance"
+ f.man.name = "Mungo"
+ assert_equal m.name, f.man.name, "Name of man should be the same after changes to child-owned instance"
+
+ m = Man.all.merge!(where: { name: "Gordon" }, includes: :face, order: "faces.id").first
+ f = m.face
+ assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance"
+ m.name = "Bongo"
+ assert_equal m.name, f.man.name, "Name of man should be the same after changes to parent instance"
+ f.man.name = "Mungo"
+ assert_equal m.name, f.man.name, "Name of man should be the same after changes to child-owned instance"
+ end
+
+ def test_parent_instance_should_be_shared_with_newly_built_child
+ m = Man.first
+ f = m.build_face(description: "haunted")
+ assert_not_nil f.man
+ assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance"
+ m.name = "Bongo"
+ assert_equal m.name, f.man.name, "Name of man should be the same after changes to parent instance"
+ f.man.name = "Mungo"
+ assert_equal m.name, f.man.name, "Name of man should be the same after changes to just-built-child-owned instance"
+ end
+
+ def test_parent_instance_should_be_shared_with_newly_created_child
+ m = Man.first
+ f = m.create_face(description: "haunted")
+ assert_not_nil f.man
+ assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance"
+ m.name = "Bongo"
+ assert_equal m.name, f.man.name, "Name of man should be the same after changes to parent instance"
+ f.man.name = "Mungo"
+ assert_equal m.name, f.man.name, "Name of man should be the same after changes to newly-created-child-owned instance"
+ end
+
+ def test_parent_instance_should_be_shared_with_newly_created_child_via_bang_method
+ m = Man.first
+ f = m.create_face!(description: "haunted")
+ assert_not_nil f.man
+ assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance"
+ m.name = "Bongo"
+ assert_equal m.name, f.man.name, "Name of man should be the same after changes to parent instance"
+ f.man.name = "Mungo"
+ assert_equal m.name, f.man.name, "Name of man should be the same after changes to newly-created-child-owned instance"
+ end
+
+ def test_parent_instance_should_be_shared_with_replaced_via_accessor_child
+ m = Man.first
+ f = Face.new(description: "haunted")
+ m.face = f
+ assert_not_nil f.man
+ assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance"
+ m.name = "Bongo"
+ assert_equal m.name, f.man.name, "Name of man should be the same after changes to parent instance"
+ f.man.name = "Mungo"
+ assert_equal m.name, f.man.name, "Name of man should be the same after changes to replaced-child-owned instance"
+ end
+
+ def test_trying_to_use_inverses_that_dont_exist_should_raise_an_error
+ assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Man.first.dirty_face }
+ end
+end
+
+class InverseHasManyTests < ActiveRecord::TestCase
+ fixtures :men, :interests, :posts, :authors, :author_addresses
+
+ def test_parent_instance_should_be_shared_with_every_child_on_find
+ m = men(:gordon)
+ is = m.interests
+ is.each do |i|
+ assert_equal m.name, i.man.name, "Name of man should be the same before changes to parent instance"
+ m.name = "Bongo"
+ assert_equal m.name, i.man.name, "Name of man should be the same after changes to parent instance"
+ i.man.name = "Mungo"
+ assert_equal m.name, i.man.name, "Name of man should be the same after changes to child-owned instance"
+ end
+ end
+
+ def test_parent_instance_should_be_shared_with_every_child_on_find_for_sti
+ a = authors(:david)
+ ps = a.posts
+ ps.each do |p|
+ assert_equal a.name, p.author.name, "Name of man should be the same before changes to parent instance"
+ a.name = "Bongo"
+ assert_equal a.name, p.author.name, "Name of man should be the same after changes to parent instance"
+ p.author.name = "Mungo"
+ assert_equal a.name, p.author.name, "Name of man should be the same after changes to child-owned instance"
+ end
+
+ sps = a.special_posts
+ sps.each do |sp|
+ assert_equal a.name, sp.author.name, "Name of man should be the same before changes to parent instance"
+ a.name = "Bongo"
+ assert_equal a.name, sp.author.name, "Name of man should be the same after changes to parent instance"
+ sp.author.name = "Mungo"
+ assert_equal a.name, sp.author.name, "Name of man should be the same after changes to child-owned instance"
+ end
+ end
+
+ def test_parent_instance_should_be_shared_with_eager_loaded_children
+ m = Man.all.merge!(where: { name: "Gordon" }, includes: :interests).first
+ is = m.interests
+ is.each do |i|
+ assert_equal m.name, i.man.name, "Name of man should be the same before changes to parent instance"
+ m.name = "Bongo"
+ assert_equal m.name, i.man.name, "Name of man should be the same after changes to parent instance"
+ i.man.name = "Mungo"
+ assert_equal m.name, i.man.name, "Name of man should be the same after changes to child-owned instance"
+ end
+
+ m = Man.all.merge!(where: { name: "Gordon" }, includes: :interests, order: "interests.id").first
+ is = m.interests
+ is.each do |i|
+ assert_equal m.name, i.man.name, "Name of man should be the same before changes to parent instance"
+ m.name = "Bongo"
+ assert_equal m.name, i.man.name, "Name of man should be the same after changes to parent instance"
+ i.man.name = "Mungo"
+ assert_equal m.name, i.man.name, "Name of man should be the same after changes to child-owned instance"
+ end
+ end
+
+ def test_parent_instance_should_be_shared_with_newly_block_style_built_child
+ m = Man.first
+ i = m.interests.build { |ii| ii.topic = "Industrial Revolution Re-enactment" }
+ assert_not_nil i.topic, "Child attributes supplied to build via blocks should be populated"
+ assert_not_nil i.man
+ assert_equal m.name, i.man.name, "Name of man should be the same before changes to parent instance"
+ m.name = "Bongo"
+ assert_equal m.name, i.man.name, "Name of man should be the same after changes to parent instance"
+ i.man.name = "Mungo"
+ assert_equal m.name, i.man.name, "Name of man should be the same after changes to just-built-child-owned instance"
+ end
+
+ def test_parent_instance_should_be_shared_with_newly_created_via_bang_method_child
+ m = Man.first
+ i = m.interests.create!(topic: "Industrial Revolution Re-enactment")
+ assert_not_nil i.man
+ assert_equal m.name, i.man.name, "Name of man should be the same before changes to parent instance"
+ m.name = "Bongo"
+ assert_equal m.name, i.man.name, "Name of man should be the same after changes to parent instance"
+ i.man.name = "Mungo"
+ assert_equal m.name, i.man.name, "Name of man should be the same after changes to newly-created-child-owned instance"
+ end
+
+ def test_parent_instance_should_be_shared_with_newly_block_style_created_child
+ m = Man.first
+ i = m.interests.create { |ii| ii.topic = "Industrial Revolution Re-enactment" }
+ assert_not_nil i.topic, "Child attributes supplied to create via blocks should be populated"
+ assert_not_nil i.man
+ assert_equal m.name, i.man.name, "Name of man should be the same before changes to parent instance"
+ m.name = "Bongo"
+ assert_equal m.name, i.man.name, "Name of man should be the same after changes to parent instance"
+ i.man.name = "Mungo"
+ assert_equal m.name, i.man.name, "Name of man should be the same after changes to newly-created-child-owned instance"
+ end
+
+ def test_parent_instance_should_be_shared_within_create_block_of_new_child
+ man = Man.first
+ interest = man.interests.create do |i|
+ assert i.man.equal?(man), "Man of child should be the same instance as a parent"
+ end
+ assert interest.man.equal?(man), "Man of the child should still be the same instance as a parent"
+ end
+
+ def test_parent_instance_should_be_shared_within_build_block_of_new_child
+ man = Man.first
+ interest = man.interests.build do |i|
+ assert i.man.equal?(man), "Man of child should be the same instance as a parent"
+ end
+ assert interest.man.equal?(man), "Man of the child should still be the same instance as a parent"
+ end
+
+ def test_parent_instance_should_be_shared_with_poked_in_child
+ m = men(:gordon)
+ i = Interest.create(topic: "Industrial Revolution Re-enactment")
+ m.interests << i
+ assert_not_nil i.man
+ assert_equal m.name, i.man.name, "Name of man should be the same before changes to parent instance"
+ m.name = "Bongo"
+ assert_equal m.name, i.man.name, "Name of man should be the same after changes to parent instance"
+ i.man.name = "Mungo"
+ assert_equal m.name, i.man.name, "Name of man should be the same after changes to newly-created-child-owned instance"
+ end
+
+ def test_parent_instance_should_be_shared_with_replaced_via_accessor_children
+ m = Man.first
+ i = Interest.new(topic: "Industrial Revolution Re-enactment")
+ m.interests = [i]
+ assert_not_nil i.man
+ assert_equal m.name, i.man.name, "Name of man should be the same before changes to parent instance"
+ m.name = "Bongo"
+ assert_equal m.name, i.man.name, "Name of man should be the same after changes to parent instance"
+ i.man.name = "Mungo"
+ assert_equal m.name, i.man.name, "Name of man should be the same after changes to replaced-child-owned instance"
+ end
+
+ def test_parent_instance_should_be_shared_with_first_and_last_child
+ man = Man.first
+
+ assert man.interests.first.man.equal? man
+ assert man.interests.last.man.equal? man
+ end
+
+ def test_parent_instance_should_be_shared_with_first_n_and_last_n_children
+ man = Man.first
+
+ interests = man.interests.first(2)
+ assert interests[0].man.equal? man
+ assert interests[1].man.equal? man
+
+ interests = man.interests.last(2)
+ assert interests[0].man.equal? man
+ assert interests[1].man.equal? man
+ end
+
+ def test_parent_instance_should_find_child_instance_using_child_instance_id
+ man = Man.create!
+ interest = Interest.create!
+ man.interests = [interest]
+
+ assert interest.equal?(man.interests.first), "The inverse association should use the interest already created and held in memory"
+ assert interest.equal?(man.interests.find(interest.id)), "The inverse association should use the interest already created and held in memory"
+ assert man.equal?(man.interests.first.man), "Two inversion should lead back to the same object that was originally held"
+ assert man.equal?(man.interests.find(interest.id).man), "Two inversions should lead back to the same object that was originally held"
+ end
+
+ def test_parent_instance_should_find_child_instance_using_child_instance_id_when_created
+ man = Man.create!
+ interest = Interest.create!(man: man)
+
+ assert man.equal?(man.interests.first.man), "Two inverses should lead back to the same object that was originally held"
+ assert man.equal?(man.interests.find(interest.id).man), "Two inversions should lead back to the same object that was originally held"
+
+ assert_nil man.interests.find(interest.id).man.name, "The name of the man should match before the name is changed"
+ man.name = "Ben Bitdiddle"
+ assert_equal man.name, man.interests.find(interest.id).man.name, "The name of the man should match after the parent name is changed"
+ man.interests.find(interest.id).man.name = "Alyssa P. Hacker"
+ assert_equal man.name, man.interests.find(interest.id).man.name, "The name of the man should match after the child name is changed"
+ end
+
+ def test_find_on_child_instance_with_id_should_not_load_all_child_records
+ man = Man.create!
+ interest = Interest.create!(man: man)
+
+ man.interests.find(interest.id)
+ assert_not_predicate man.interests, :loaded?
+ end
+
+ def test_raise_record_not_found_error_when_invalid_ids_are_passed
+ # delete all interest records to ensure that hard coded invalid_id(s)
+ # are indeed invalid.
+ Interest.delete_all
+
+ man = Man.create!
+
+ invalid_id = 245324523
+ assert_raise(ActiveRecord::RecordNotFound) { man.interests.find(invalid_id) }
+
+ invalid_ids = [8432342, 2390102913, 2453245234523452]
+ assert_raise(ActiveRecord::RecordNotFound) { man.interests.find(invalid_ids) }
+ end
+
+ def test_raise_record_not_found_error_when_no_ids_are_passed
+ man = Man.create!
+
+ exception = assert_raise(ActiveRecord::RecordNotFound) { man.interests.load.find() }
+
+ assert_equal exception.model, "Interest"
+ assert_equal exception.primary_key, "id"
+ end
+
+ def test_trying_to_use_inverses_that_dont_exist_should_raise_an_error
+ assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Man.first.secret_interests }
+ end
+
+ def test_child_instance_should_point_to_parent_without_saving
+ man = Man.new
+ i = Interest.create(topic: "Industrial Revolution Re-enactment")
+
+ man.interests << i
+ assert_not_nil i.man
+
+ i.man.name = "Charles"
+ assert_equal i.man.name, man.name
+
+ assert_not_predicate man, :persisted?
+ end
+
+ def test_inverse_instance_should_be_set_before_find_callbacks_are_run
+ reset_callbacks(Interest, :find) do
+ Interest.after_find { raise unless association(:man).loaded? && man.present? }
+
+ assert_predicate Man.first.interests.reload, :any?
+ assert_predicate Man.includes(:interests).first.interests, :any?
+ assert_predicate Man.joins(:interests).includes(:interests).first.interests, :any?
+ end
+ end
+
+ def test_inverse_instance_should_be_set_before_initialize_callbacks_are_run
+ reset_callbacks(Interest, :initialize) do
+ Interest.after_initialize { raise unless association(:man).loaded? && man.present? }
+
+ assert_predicate Man.first.interests.reload, :any?
+ assert_predicate Man.includes(:interests).first.interests, :any?
+ assert_predicate Man.joins(:interests).includes(:interests).first.interests, :any?
+ end
+ end
+
+ def reset_callbacks(target, type)
+ old_callbacks = target.send(:get_callbacks, type).deep_dup
+ yield
+ ensure
+ target.send(:set_callbacks, type, old_callbacks) if old_callbacks
+ end
+end
+
+class InverseBelongsToTests < ActiveRecord::TestCase
+ fixtures :men, :faces, :interests
+
+ def test_child_instance_should_be_shared_with_parent_on_find
+ f = faces(:trusting)
+ m = f.man
+ assert_equal f.description, m.face.description, "Description of face should be the same before changes to child instance"
+ f.description = "gormless"
+ assert_equal f.description, m.face.description, "Description of face should be the same after changes to child instance"
+ m.face.description = "pleasing"
+ assert_equal f.description, m.face.description, "Description of face should be the same after changes to parent-owned instance"
+ end
+
+ def test_eager_loaded_child_instance_should_be_shared_with_parent_on_find
+ f = Face.all.merge!(includes: :man, where: { description: "trusting" }).first
+ m = f.man
+ assert_equal f.description, m.face.description, "Description of face should be the same before changes to child instance"
+ f.description = "gormless"
+ assert_equal f.description, m.face.description, "Description of face should be the same after changes to child instance"
+ m.face.description = "pleasing"
+ assert_equal f.description, m.face.description, "Description of face should be the same after changes to parent-owned instance"
+
+ f = Face.all.merge!(includes: :man, order: "men.id", where: { description: "trusting" }).first
+ m = f.man
+ assert_equal f.description, m.face.description, "Description of face should be the same before changes to child instance"
+ f.description = "gormless"
+ assert_equal f.description, m.face.description, "Description of face should be the same after changes to child instance"
+ m.face.description = "pleasing"
+ assert_equal f.description, m.face.description, "Description of face should be the same after changes to parent-owned instance"
+ end
+
+ def test_child_instance_should_be_shared_with_newly_built_parent
+ f = faces(:trusting)
+ m = f.build_man(name: "Charles")
+ assert_not_nil m.face
+ assert_equal f.description, m.face.description, "Description of face should be the same before changes to child instance"
+ f.description = "gormless"
+ assert_equal f.description, m.face.description, "Description of face should be the same after changes to child instance"
+ m.face.description = "pleasing"
+ assert_equal f.description, m.face.description, "Description of face should be the same after changes to just-built-parent-owned instance"
+ end
+
+ def test_child_instance_should_be_shared_with_newly_created_parent
+ f = faces(:trusting)
+ m = f.create_man(name: "Charles")
+ assert_not_nil m.face
+ assert_equal f.description, m.face.description, "Description of face should be the same before changes to child instance"
+ f.description = "gormless"
+ assert_equal f.description, m.face.description, "Description of face should be the same after changes to child instance"
+ m.face.description = "pleasing"
+ assert_equal f.description, m.face.description, "Description of face should be the same after changes to newly-created-parent-owned instance"
+ end
+
+ def test_should_not_try_to_set_inverse_instances_when_the_inverse_is_a_has_many
+ i = interests(:trainspotting)
+ m = i.man
+ assert_not_nil m.interests
+ iz = m.interests.detect { |_iz| _iz.id == i.id }
+ assert_not_nil iz
+ assert_equal i.topic, iz.topic, "Interest topics should be the same before changes to child"
+ i.topic = "Eating cheese with a spoon"
+ assert_not_equal i.topic, iz.topic, "Interest topics should not be the same after changes to child"
+ iz.topic = "Cow tipping"
+ assert_not_equal i.topic, iz.topic, "Interest topics should not be the same after changes to parent-owned instance"
+ end
+
+ def test_child_instance_should_be_shared_with_replaced_via_accessor_parent
+ f = Face.first
+ m = Man.new(name: "Charles")
+ f.man = m
+ assert_not_nil m.face
+ assert_equal f.description, m.face.description, "Description of face should be the same before changes to child instance"
+ f.description = "gormless"
+ assert_equal f.description, m.face.description, "Description of face should be the same after changes to child instance"
+ m.face.description = "pleasing"
+ assert_equal f.description, m.face.description, "Description of face should be the same after changes to replaced-parent-owned instance"
+ end
+
+ def test_trying_to_use_inverses_that_dont_exist_should_raise_an_error
+ assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Face.first.horrible_man }
+ end
+end
+
+class InversePolymorphicBelongsToTests < ActiveRecord::TestCase
+ fixtures :men, :faces, :interests
+
+ def test_child_instance_should_be_shared_with_parent_on_find
+ f = Face.all.merge!(where: { description: "confused" }).first
+ m = f.polymorphic_man
+ assert_equal f.description, m.polymorphic_face.description, "Description of face should be the same before changes to child instance"
+ f.description = "gormless"
+ assert_equal f.description, m.polymorphic_face.description, "Description of face should be the same after changes to child instance"
+ m.polymorphic_face.description = "pleasing"
+ assert_equal f.description, m.polymorphic_face.description, "Description of face should be the same after changes to parent-owned instance"
+ end
+
+ def test_eager_loaded_child_instance_should_be_shared_with_parent_on_find
+ f = Face.all.merge!(where: { description: "confused" }, includes: :man).first
+ m = f.polymorphic_man
+ assert_equal f.description, m.polymorphic_face.description, "Description of face should be the same before changes to child instance"
+ f.description = "gormless"
+ assert_equal f.description, m.polymorphic_face.description, "Description of face should be the same after changes to child instance"
+ m.polymorphic_face.description = "pleasing"
+ assert_equal f.description, m.polymorphic_face.description, "Description of face should be the same after changes to parent-owned instance"
+
+ f = Face.all.merge!(where: { description: "confused" }, includes: :man, order: "men.id").first
+ m = f.polymorphic_man
+ assert_equal f.description, m.polymorphic_face.description, "Description of face should be the same before changes to child instance"
+ f.description = "gormless"
+ assert_equal f.description, m.polymorphic_face.description, "Description of face should be the same after changes to child instance"
+ m.polymorphic_face.description = "pleasing"
+ assert_equal f.description, m.polymorphic_face.description, "Description of face should be the same after changes to parent-owned instance"
+ end
+
+ def test_child_instance_should_be_shared_with_replaced_via_accessor_parent
+ face = faces(:confused)
+ new_man = Man.new
+
+ assert_not_nil face.polymorphic_man
+ face.polymorphic_man = new_man
+
+ assert_equal face.description, new_man.polymorphic_face.description, "Description of face should be the same before changes to parent instance"
+ face.description = "Bongo"
+ assert_equal face.description, new_man.polymorphic_face.description, "Description of face should be the same after changes to parent instance"
+ new_man.polymorphic_face.description = "Mungo"
+ assert_equal face.description, new_man.polymorphic_face.description, "Description of face should be the same after changes to replaced-parent-owned instance"
+ end
+
+ def test_inversed_instance_should_not_be_reloaded_after_stale_state_changed
+ new_man = Man.new
+ face = Face.new
+ new_man.face = face
+
+ old_inversed_man = face.man
+ new_man.save!
+ new_inversed_man = face.man
+
+ assert_equal old_inversed_man.object_id, new_inversed_man.object_id
+ end
+
+ def test_inversed_instance_should_not_be_reloaded_after_stale_state_changed_with_validation
+ face = Face.new man: Man.new
+
+ old_inversed_man = face.man
+ face.save!
+ new_inversed_man = face.man
+
+ assert_equal old_inversed_man.object_id, new_inversed_man.object_id
+ end
+
+ def test_should_not_try_to_set_inverse_instances_when_the_inverse_is_a_has_many
+ i = interests(:llama_wrangling)
+ m = i.polymorphic_man
+ assert_not_nil m.polymorphic_interests
+ iz = m.polymorphic_interests.detect { |_iz| _iz.id == i.id }
+ assert_not_nil iz
+ assert_equal i.topic, iz.topic, "Interest topics should be the same before changes to child"
+ i.topic = "Eating cheese with a spoon"
+ assert_not_equal i.topic, iz.topic, "Interest topics should not be the same after changes to child"
+ iz.topic = "Cow tipping"
+ assert_not_equal i.topic, iz.topic, "Interest topics should not be the same after changes to parent-owned instance"
+ end
+
+ def test_trying_to_access_inverses_that_dont_exist_shouldnt_raise_an_error
+ # Ideally this would, if only for symmetry's sake with other association types
+ assert_nothing_raised { Face.first.horrible_polymorphic_man }
+ end
+
+ def test_trying_to_set_polymorphic_inverses_that_dont_exist_at_all_should_raise_an_error
+ # fails because no class has the correct inverse_of for horrible_polymorphic_man
+ assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Face.first.horrible_polymorphic_man = Man.first }
+ end
+
+ def test_trying_to_set_polymorphic_inverses_that_dont_exist_on_the_instance_being_set_should_raise_an_error
+ # passes because Man does have the correct inverse_of
+ assert_nothing_raised { Face.first.polymorphic_man = Man.first }
+ # fails because Interest does have the correct inverse_of
+ assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Face.first.polymorphic_man = Interest.first }
+ end
+end
+
+# NOTE - these tests might not be meaningful, ripped as they were from the parental_control plugin
+# which would guess the inverse rather than look for an explicit configuration option.
+class InverseMultipleHasManyInversesForSameModel < ActiveRecord::TestCase
+ fixtures :men, :interests, :zines
+
+ def test_that_we_can_load_associations_that_have_the_same_reciprocal_name_from_different_models
+ assert_nothing_raised do
+ i = Interest.first
+ i.zine
+ i.man
+ end
+ end
+
+ def test_that_we_can_create_associations_that_have_the_same_reciprocal_name_from_different_models
+ assert_nothing_raised do
+ i = Interest.first
+ i.build_zine(title: "Get Some in Winter! 2008")
+ i.build_man(name: "Gordon")
+ i.save!
+ end
+ end
+end
diff --git a/activerecord/test/cases/associations/join_model_test.rb b/activerecord/test/cases/associations/join_model_test.rb
new file mode 100644
index 0000000000..f19a9f5f7a
--- /dev/null
+++ b/activerecord/test/cases/associations/join_model_test.rb
@@ -0,0 +1,778 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/tag"
+require "models/tagging"
+require "models/post"
+require "models/rating"
+require "models/item"
+require "models/comment"
+require "models/author"
+require "models/category"
+require "models/categorization"
+require "models/vertex"
+require "models/edge"
+require "models/book"
+require "models/citation"
+require "models/aircraft"
+require "models/engine"
+require "models/car"
+
+class AssociationsJoinModelTest < ActiveRecord::TestCase
+ self.use_transactional_tests = false unless supports_savepoints?
+
+ fixtures :posts, :authors, :author_addresses, :categories, :categorizations, :comments, :tags, :taggings, :author_favorites, :vertices, :items, :books,
+ # Reload edges table from fixtures as otherwise repeated test was failing
+ :edges
+
+ def test_has_many
+ assert_includes authors(:david).categories, categories(:general)
+ end
+
+ def test_has_many_inherited
+ assert_includes authors(:mary).categories, categories(:sti_test)
+ end
+
+ def test_inherited_has_many
+ assert_includes categories(:sti_test).authors, authors(:mary)
+ end
+
+ def test_has_many_distinct_through_join_model
+ assert_equal 2, authors(:mary).categorized_posts.size
+ assert_equal 1, authors(:mary).unique_categorized_posts.size
+ end
+
+ def test_has_many_distinct_through_count
+ author = authors(:mary)
+ assert_not_predicate authors(:mary).unique_categorized_posts, :loaded?
+ assert_queries(1) { assert_equal 1, author.unique_categorized_posts.count }
+ assert_queries(1) { assert_equal 1, author.unique_categorized_posts.count(:title) }
+ assert_queries(1) { assert_equal 0, author.unique_categorized_posts.where(title: nil).count(:title) }
+ assert_not_predicate authors(:mary).unique_categorized_posts, :loaded?
+ end
+
+ def test_has_many_distinct_through_find
+ assert_equal 1, authors(:mary).unique_categorized_posts.to_a.size
+ end
+
+ def test_polymorphic_has_many_going_through_join_model
+ assert_equal tags(:general), tag = posts(:welcome).tags.first
+ assert_no_queries do
+ tag.tagging
+ end
+ end
+
+ def test_count_polymorphic_has_many
+ assert_equal 1, posts(:welcome).taggings.count
+ assert_equal 1, posts(:welcome).tags.count
+ end
+
+ def test_polymorphic_has_many_going_through_join_model_with_find
+ assert_equal tags(:general), tag = posts(:welcome).tags.first
+ assert_no_queries do
+ tag.tagging
+ end
+ end
+
+ def test_polymorphic_has_many_going_through_join_model_with_include_on_source_reflection
+ assert_equal tags(:general), tag = posts(:welcome).funky_tags.first
+ assert_no_queries do
+ tag.tagging
+ end
+ end
+
+ def test_polymorphic_has_many_going_through_join_model_with_include_on_source_reflection_with_find
+ assert_equal tags(:general), tag = posts(:welcome).funky_tags.first
+ assert_no_queries do
+ tag.tagging
+ end
+ end
+
+ def test_polymorphic_has_many_going_through_join_model_with_custom_select_and_joins
+ assert_equal tags(:general), tag = posts(:welcome).tags.add_joins_and_select.first
+ assert_nothing_raised { tag.author_id }
+ end
+
+ def test_polymorphic_has_many_going_through_join_model_with_custom_foreign_key
+ assert_equal tags(:misc), taggings(:welcome_general).super_tag
+ assert_equal tags(:misc), posts(:welcome).super_tags.first
+ end
+
+ def test_polymorphic_has_many_create_model_with_inheritance_and_custom_base_class
+ post = SubAbstractStiPost.create title: "SubAbstractStiPost", body: "SubAbstractStiPost body"
+ assert_instance_of SubAbstractStiPost, post
+
+ tagging = tags(:misc).taggings.create(taggable: post)
+ assert_equal "SubAbstractStiPost", tagging.taggable_type
+ end
+
+ def test_polymorphic_has_many_going_through_join_model_with_inheritance
+ assert_equal tags(:general), posts(:thinking).tags.first
+ end
+
+ def test_polymorphic_has_many_going_through_join_model_with_inheritance_with_custom_class_name
+ assert_equal tags(:general), posts(:thinking).funky_tags.first
+ end
+
+ def test_polymorphic_has_many_create_model_with_inheritance
+ post = posts(:thinking)
+ assert_instance_of SpecialPost, post
+
+ tagging = tags(:misc).taggings.create(taggable: post)
+ assert_equal "Post", tagging.taggable_type
+ end
+
+ def test_polymorphic_has_one_create_model_with_inheritance
+ tagging = tags(:misc).create_tagging(taggable: posts(:thinking))
+ assert_equal "Post", tagging.taggable_type
+ end
+
+ def test_set_polymorphic_has_many
+ tagging = tags(:misc).taggings.create
+ posts(:thinking).taggings << tagging
+ assert_equal "Post", tagging.taggable_type
+ end
+
+ def test_set_polymorphic_has_one
+ tagging = tags(:misc).taggings.create
+ posts(:thinking).tagging = tagging
+
+ assert_equal "Post", tagging.taggable_type
+ assert_equal posts(:thinking).id, tagging.taggable_id
+ assert_equal posts(:thinking), tagging.taggable
+ end
+
+ def test_set_polymorphic_has_one_on_new_record
+ tagging = tags(:misc).taggings.create
+ post = Post.new title: "foo", body: "bar"
+ post.tagging = tagging
+ post.save!
+
+ assert_equal "Post", tagging.taggable_type
+ assert_equal post.id, tagging.taggable_id
+ assert_equal post, tagging.taggable
+ end
+
+ def test_create_polymorphic_has_many_with_scope
+ old_count = posts(:welcome).taggings.count
+ tagging = posts(:welcome).taggings.create(tag: tags(:misc))
+ assert_equal "Post", tagging.taggable_type
+ assert_equal old_count + 1, posts(:welcome).taggings.count
+ end
+
+ def test_create_bang_polymorphic_with_has_many_scope
+ old_count = posts(:welcome).taggings.count
+ tagging = posts(:welcome).taggings.create!(tag: tags(:misc))
+ assert_equal "Post", tagging.taggable_type
+ assert_equal old_count + 1, posts(:welcome).taggings.count
+ end
+
+ def test_create_polymorphic_has_one_with_scope
+ old_count = Tagging.count
+ tagging = posts(:welcome).create_tagging(tag: tags(:misc))
+ assert_equal "Post", tagging.taggable_type
+ assert_equal old_count + 1, Tagging.count
+ end
+
+ def test_delete_polymorphic_has_many_with_delete_all
+ assert_equal 1, posts(:welcome).taggings.count
+ posts(:welcome).taggings.first.update_columns taggable_type: "PostWithHasManyDeleteAll"
+ post = find_post_with_dependency(1, :has_many, :taggings, :delete_all)
+
+ old_count = Tagging.count
+ post.destroy
+ assert_equal old_count - 1, Tagging.count
+ assert_equal 0, posts(:welcome).taggings.count
+ end
+
+ def test_delete_polymorphic_has_many_with_destroy
+ assert_equal 1, posts(:welcome).taggings.count
+ posts(:welcome).taggings.first.update_columns taggable_type: "PostWithHasManyDestroy"
+ post = find_post_with_dependency(1, :has_many, :taggings, :destroy)
+
+ old_count = Tagging.count
+ post.destroy
+ assert_equal old_count - 1, Tagging.count
+ assert_equal 0, posts(:welcome).taggings.count
+ end
+
+ def test_delete_polymorphic_has_many_with_nullify
+ assert_equal 1, posts(:welcome).taggings.count
+ posts(:welcome).taggings.first.update_columns taggable_type: "PostWithHasManyNullify"
+ post = find_post_with_dependency(1, :has_many, :taggings, :nullify)
+
+ old_count = Tagging.count
+ post.destroy
+ assert_equal old_count, Tagging.count
+ assert_equal 0, posts(:welcome).taggings.count
+ end
+
+ def test_delete_polymorphic_has_one_with_destroy
+ assert posts(:welcome).tagging
+ posts(:welcome).tagging.update_columns taggable_type: "PostWithHasOneDestroy"
+ post = find_post_with_dependency(1, :has_one, :tagging, :destroy)
+
+ old_count = Tagging.count
+ post.destroy
+ assert_equal old_count - 1, Tagging.count
+ posts(:welcome).association(:tagging).reload
+ assert_nil posts(:welcome).tagging
+ end
+
+ def test_delete_polymorphic_has_one_with_nullify
+ assert posts(:welcome).tagging
+ posts(:welcome).tagging.update_columns taggable_type: "PostWithHasOneNullify"
+ post = find_post_with_dependency(1, :has_one, :tagging, :nullify)
+
+ old_count = Tagging.count
+ post.destroy
+ assert_equal old_count, Tagging.count
+ posts(:welcome).association(:tagging).reload
+ assert_nil posts(:welcome).tagging
+ end
+
+ def test_has_many_with_piggyback
+ assert_equal "2", categories(:sti_test).authors_with_select.first.post_id.to_s
+ end
+
+ def test_create_through_has_many_with_piggyback
+ category = categories(:sti_test)
+ ernie = category.authors_with_select.create(name: "Ernie")
+ assert_nothing_raised do
+ assert_equal ernie, category.authors_with_select.detect { |a| a.name == "Ernie" }
+ end
+ end
+
+ def test_include_has_many_through
+ posts = Post.all.merge!(order: "posts.id").to_a
+ posts_with_authors = Post.all.merge!(includes: :authors, order: "posts.id").to_a
+ assert_equal posts.length, posts_with_authors.length
+ posts.length.times do |i|
+ assert_equal posts[i].authors.length, assert_no_queries { posts_with_authors[i].authors.length }
+ end
+ end
+
+ def test_include_polymorphic_has_one
+ post = Post.includes(:tagging).find posts(:welcome).id
+ tagging = taggings(:welcome_general)
+ assert_no_queries do
+ assert_equal tagging, post.tagging
+ end
+ end
+
+ def test_include_polymorphic_has_one_defined_in_abstract_parent
+ item = Item.includes(:tagging).find items(:dvd).id
+ tagging = taggings(:godfather)
+ assert_no_queries do
+ assert_equal tagging, item.tagging
+ end
+ end
+
+ def test_include_polymorphic_has_many_through
+ posts = Post.all.merge!(order: "posts.id").to_a
+ posts_with_tags = Post.all.merge!(includes: :tags, order: "posts.id").to_a
+ assert_equal posts.length, posts_with_tags.length
+ posts.length.times do |i|
+ assert_equal posts[i].tags.length, assert_no_queries { posts_with_tags[i].tags.length }
+ end
+ end
+
+ def test_include_polymorphic_has_many
+ posts = Post.all.merge!(order: "posts.id").to_a
+ posts_with_taggings = Post.all.merge!(includes: :taggings, order: "posts.id").to_a
+ assert_equal posts.length, posts_with_taggings.length
+ posts.length.times do |i|
+ assert_equal posts[i].taggings.length, assert_no_queries { posts_with_taggings[i].taggings.length }
+ end
+ end
+
+ def test_has_many_find_all
+ assert_equal [categories(:general)], authors(:david).categories.to_a
+ end
+
+ def test_has_many_find_first
+ assert_equal categories(:general), authors(:david).categories.first
+ end
+
+ def test_has_many_with_hash_conditions
+ assert_equal categories(:general), authors(:david).categories_like_general.first
+ end
+
+ def test_has_many_find_conditions
+ assert_equal categories(:general), authors(:david).categories.where("categories.name = 'General'").first
+ assert_nil authors(:david).categories.where("categories.name = 'Technology'").first
+ end
+
+ def test_has_many_array_methods_called_by_method_missing
+ assert authors(:david).categories.any? { |category| category.name == "General" }
+ assert_nothing_raised { authors(:david).categories.sort }
+ end
+
+ def test_has_many_going_through_join_model_with_custom_foreign_key
+ assert_equal [authors(:bob)], posts(:thinking).authors
+ assert_equal [authors(:mary)], posts(:authorless).authors
+ end
+
+ def test_has_many_going_through_join_model_with_custom_primary_key
+ assert_equal [authors(:david)], posts(:thinking).authors_using_author_id
+ end
+
+ def test_has_many_going_through_polymorphic_join_model_with_custom_primary_key
+ assert_equal [tags(:general)], posts(:eager_other).tags_using_author_id
+ end
+
+ def test_has_many_through_with_custom_primary_key_on_belongs_to_source
+ assert_equal [authors(:david), authors(:david)], posts(:thinking).author_using_custom_pk
+ 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("authors.id")
+ end
+
+ def test_belongs_to_polymorphic_with_counter_cache
+ assert_equal 1, posts(:welcome)[:tags_count]
+ tagging = posts(:welcome).taggings.create(tag: tags(:general))
+ assert_equal 2, posts(:welcome, :reload)[:tags_count]
+ tagging.destroy
+ assert_equal 1, posts(:welcome, :reload)[:tags_count]
+ end
+
+ def test_unavailable_through_reflection
+ assert_raise(ActiveRecord::HasManyThroughAssociationNotFoundError) { authors(:david).nothings }
+ end
+
+ def test_has_many_through_join_model_with_conditions
+ assert_equal [], posts(:welcome).invalid_taggings
+ assert_equal [], posts(:welcome).invalid_tags
+ end
+
+ def test_has_many_polymorphic
+ assert_raise ActiveRecord::HasManyThroughAssociationPolymorphicSourceError do
+ tags(:general).taggables
+ end
+
+ assert_raise ActiveRecord::HasManyThroughAssociationPolymorphicThroughError do
+ taggings(:welcome_general).things
+ end
+
+ assert_raise ActiveRecord::EagerLoadPolymorphicError do
+ tags(:general).taggings.includes(:taggable).where("bogus_table.column = 1").references(:bogus_table).to_a
+ end
+ end
+
+ def test_has_many_polymorphic_with_source_type
+ # added sort by ID as otherwise Oracle select sometimes returned rows in different order
+ assert_equal posts(:welcome, :thinking).sort_by(&:id), tags(:general).tagged_posts.sort_by(&:id)
+ end
+
+ def test_has_many_polymorphic_associations_merges_through_scope
+ Tag.has_many :null_taggings, -> { none }, class_name: :Tagging
+ Tag.has_many :null_tagged_posts, through: :null_taggings, source: "taggable", source_type: "Post"
+ assert_equal [], tags(:general).null_tagged_posts
+ refute_equal [], tags(:general).tagged_posts
+ end
+
+ def test_eager_has_many_polymorphic_with_source_type
+ tag_with_include = Tag.all.merge!(includes: :tagged_posts).find(tags(:general).id)
+ desired = posts(:welcome, :thinking)
+ assert_no_queries do
+ # added sort by ID as otherwise test using JRuby was failing as array elements were in different order
+ assert_equal desired.sort_by(&:id), tag_with_include.tagged_posts.sort_by(&:id)
+ end
+ assert_equal 5, tag_with_include.taggings.length
+ end
+
+ def test_has_many_through_has_many_find_all
+ assert_equal comments(:greetings), authors(:david).comments.order("comments.id").to_a.first
+ end
+
+ def test_has_many_through_has_many_find_all_with_custom_class
+ assert_equal comments(:greetings), authors(:david).funky_comments.order("comments.id").to_a.first
+ end
+
+ def test_has_many_through_has_many_find_first
+ assert_equal comments(:greetings), authors(:david).comments.order("comments.id").first
+ end
+
+ def test_has_many_through_has_many_find_conditions
+ options = { where: "comments.#{QUOTED_TYPE}='SpecialComment'", order: "comments.id" }
+ assert_equal comments(:does_it_hurt), authors(:david).comments.merge(options).first
+ end
+
+ def test_has_many_through_has_many_find_by_id
+ assert_equal comments(:more_greetings), authors(:david).comments.find(2)
+ end
+
+ def test_has_many_through_polymorphic_has_one
+ assert_equal Tagging.find(1, 2).sort_by(&:id), authors(:david).taggings_2.sort_by(&:id)
+ end
+
+ def test_has_many_through_polymorphic_has_many
+ assert_equal taggings(:welcome_general, :thinking_general), authors(:david).taggings.distinct.sort_by(&:id)
+ end
+
+ def test_include_has_many_through_polymorphic_has_many
+ author = Author.includes(:taggings).find authors(:david).id
+ expected_taggings = taggings(:welcome_general, :thinking_general)
+ assert_no_queries do
+ assert_equal expected_taggings, author.taggings.uniq.sort_by(&:id)
+ end
+ end
+
+ def test_eager_load_has_many_through_has_many
+ author = Author.all.merge!(where: ["name = ?", "David"], includes: :comments, order: "comments.id").first
+ SpecialComment.new; VerySpecialComment.new
+ assert_no_queries do
+ assert_equal [1, 2, 3, 5, 6, 7, 8, 9, 10, 12], author.comments.collect(&:id)
+ end
+ end
+
+ def test_eager_load_has_many_through_has_many_with_conditions
+ post = Post.all.merge!(includes: :invalid_tags).first
+ assert_no_queries do
+ post.invalid_tags
+ end
+ end
+
+ def test_eager_belongs_to_and_has_one_not_singularized
+ assert_nothing_raised do
+ Author.all.merge!(includes: :author_address).first
+ AuthorAddress.all.merge!(includes: :author).first
+ end
+ end
+
+ def test_self_referential_has_many_through
+ assert_equal [authors(:mary)], authors(:david).favorite_authors
+ assert_equal [], authors(:mary).favorite_authors
+ end
+
+ def test_add_to_self_referential_has_many_through
+ new_author = Author.create(name: "Bob")
+ authors(:david).author_favorites.create favorite_author: new_author
+ assert_equal new_author, authors(:david).reload.favorite_authors.first
+ end
+
+ def test_has_many_through_uses_conditions_specified_on_the_has_many_association
+ author = Author.first
+ assert_predicate author.comments, :present?
+ assert_predicate author.nonexistent_comments, :blank?
+ end
+
+ def test_has_many_through_uses_correct_attributes
+ assert_nil posts(:thinking).tags.find_by_name("General").attributes["tag_id"]
+ end
+
+ def test_associating_unsaved_records_with_has_many_through
+ saved_post = posts(:thinking)
+ new_tag = Tag.new(name: "new")
+
+ saved_post.tags << new_tag
+ assert new_tag.persisted? # consistent with habtm!
+ assert_predicate saved_post, :persisted?
+ assert_includes saved_post.tags, new_tag
+
+ assert_predicate new_tag, :persisted?
+ assert_includes saved_post.reload.tags.reload, new_tag
+
+ new_post = Post.new(title: "Association replacement works!", body: "You best believe it.")
+ saved_tag = tags(:general)
+
+ new_post.tags << saved_tag
+ assert_not_predicate new_post, :persisted?
+ assert_predicate saved_tag, :persisted?
+ assert_includes new_post.tags, saved_tag
+
+ new_post.save!
+ assert_predicate new_post, :persisted?
+ assert_includes new_post.reload.tags.reload, saved_tag
+
+ assert_not_predicate posts(:thinking).tags.build, :persisted?
+ assert_not_predicate posts(:thinking).tags.new, :persisted?
+ end
+
+ def test_create_associate_when_adding_to_has_many_through
+ count = posts(:thinking).tags.count
+ push = Tag.create!(name: "pushme")
+ post_thinking = posts(:thinking)
+ assert_nothing_raised { post_thinking.tags << push }
+ assert_nil(wrong = post_thinking.tags.detect { |t| t.class != Tag },
+ "Expected a Tag in tags collection, got #{wrong.class}.")
+ assert_nil(wrong = post_thinking.taggings.detect { |t| t.class != Tagging },
+ "Expected a Tagging in taggings collection, got #{wrong.class}.")
+ assert_equal(count + 1, post_thinking.reload.tags.size)
+ assert_equal(count + 1, post_thinking.tags.reload.size)
+
+ assert_kind_of Tag, post_thinking.tags.create!(name: "foo")
+ assert_nil(wrong = post_thinking.tags.detect { |t| t.class != Tag },
+ "Expected a Tag in tags collection, got #{wrong.class}.")
+ assert_nil(wrong = post_thinking.taggings.detect { |t| t.class != Tagging },
+ "Expected a Tagging in taggings collection, got #{wrong.class}.")
+ assert_equal(count + 2, post_thinking.reload.tags.size)
+ assert_equal(count + 2, post_thinking.tags.reload.size)
+
+ assert_nothing_raised { post_thinking.tags.concat(Tag.create!(name: "abc"), Tag.create!(name: "def")) }
+ assert_nil(wrong = post_thinking.tags.detect { |t| t.class != Tag },
+ "Expected a Tag in tags collection, got #{wrong.class}.")
+ assert_nil(wrong = post_thinking.taggings.detect { |t| t.class != Tagging },
+ "Expected a Tagging in taggings collection, got #{wrong.class}.")
+ assert_equal(count + 4, post_thinking.reload.tags.size)
+ assert_equal(count + 4, post_thinking.tags.reload.size)
+
+ # Raises if the wrong reflection name is used to set the Edge belongs_to
+ assert_nothing_raised { vertices(:vertex_1).sinks << vertices(:vertex_5) }
+ end
+
+ def test_add_to_join_table_with_no_id
+ assert_nothing_raised { vertices(:vertex_1).sinks << vertices(:vertex_5) }
+ end
+
+ def test_has_many_through_collection_size_doesnt_load_target_if_not_loaded
+ author = authors(:david)
+ assert_equal 10, author.comments.size
+ assert_not_predicate author.comments, :loaded?
+ end
+
+ def test_has_many_through_collection_size_uses_counter_cache_if_it_exists
+ c = categories(:general)
+ c.categorizations_count = 100
+ assert_equal 100, c.categorizations.size
+ assert_not_predicate c.categorizations, :loaded?
+ end
+
+ def test_adding_junk_to_has_many_through_should_raise_type_mismatch
+ assert_raise(ActiveRecord::AssociationTypeMismatch) { posts(:thinking).tags << "Uhh what now?" }
+ end
+
+ def test_adding_to_has_many_through_should_return_self
+ tags = posts(:thinking).tags
+ assert_equal tags, posts(:thinking).tags.push(tags(:general))
+ end
+
+ def test_delete_associate_when_deleting_from_has_many_through_with_nonstandard_id
+ count = books(:awdr).references.count
+ references_before = books(:awdr).references
+ book = Book.create!(name: "Getting Real")
+ book_awdr = books(:awdr)
+ book_awdr.references << book
+ assert_equal(count + 1, book_awdr.references.reload.size)
+
+ assert_nothing_raised { book_awdr.references.delete(book) }
+ assert_equal(count, book_awdr.references.size)
+ assert_equal(count, book_awdr.references.reload.size)
+ assert_equal(references_before.sort, book_awdr.references.sort)
+ end
+
+ def test_delete_associate_when_deleting_from_has_many_through
+ count = posts(:thinking).tags.count
+ tags_before = posts(:thinking).tags.sort
+ tag = Tag.create!(name: "doomed")
+ post_thinking = posts(:thinking)
+ post_thinking.tags << tag
+ assert_equal(count + 1, post_thinking.taggings.reload.size)
+ assert_equal(count + 1, post_thinking.reload.tags.reload.size)
+ assert_not_equal(tags_before, post_thinking.tags.sort)
+
+ assert_nothing_raised { post_thinking.tags.delete(tag) }
+ assert_equal(count, post_thinking.tags.size)
+ assert_equal(count, post_thinking.tags.reload.size)
+ assert_equal(count, post_thinking.taggings.reload.size)
+ assert_equal(tags_before, post_thinking.tags.sort)
+ end
+
+ def test_delete_associate_when_deleting_from_has_many_through_with_multiple_tags
+ count = posts(:thinking).tags.count
+ tags_before = posts(:thinking).tags.sort
+ doomed = Tag.create!(name: "doomed")
+ doomed2 = Tag.create!(name: "doomed2")
+ quaked = Tag.create!(name: "quaked")
+ post_thinking = posts(:thinking)
+ post_thinking.tags << doomed << doomed2
+ assert_equal(count + 2, post_thinking.reload.tags.reload.size)
+
+ assert_nothing_raised { post_thinking.tags.delete(doomed, doomed2, quaked) }
+ assert_equal(count, post_thinking.tags.size)
+ assert_equal(count, post_thinking.tags.reload.size)
+ assert_equal(tags_before, post_thinking.tags.sort)
+ end
+
+ def test_deleting_junk_from_has_many_through_should_raise_type_mismatch
+ assert_raise(ActiveRecord::AssociationTypeMismatch) { posts(:thinking).tags.delete(Object.new) }
+ end
+
+ def test_deleting_by_integer_id_from_has_many_through
+ post = posts(:thinking)
+
+ assert_difference "post.tags.count", -1 do
+ assert_equal 1, post.tags.delete(1).size
+ end
+
+ assert_equal 0, post.tags.size
+ end
+
+ def test_deleting_by_string_id_from_has_many_through
+ post = posts(:thinking)
+
+ assert_difference "post.tags.count", -1 do
+ assert_equal 1, post.tags.delete("1").size
+ end
+
+ assert_equal 0, post.tags.size
+ end
+
+ def test_has_many_through_sum_uses_calculations
+ assert_nothing_raised { authors(:david).comments.sum(:post_id) }
+ end
+
+ def test_calculations_on_has_many_through_should_disambiguate_fields
+ assert_nothing_raised { authors(:david).categories.maximum(:id) }
+ end
+
+ def test_calculations_on_has_many_through_should_not_disambiguate_fields_unless_necessary
+ assert_nothing_raised { authors(:david).categories.maximum("categories.id") }
+ end
+
+ def test_has_many_through_has_many_with_sti
+ assert_equal [comments(:does_it_hurt)], authors(:david).special_post_comments
+ end
+
+ def test_distinct_has_many_through_should_retain_order
+ comment_ids = authors(:david).comments.map(&:id)
+ assert_equal comment_ids.sort, authors(:david).ordered_uniq_comments.map(&:id)
+ assert_equal comment_ids.sort.reverse, authors(:david).ordered_uniq_comments_desc.map(&:id)
+ end
+
+ def test_polymorphic_has_many
+ expected = taggings(:welcome_general)
+ p = Post.all.merge!(includes: :taggings).find(posts(:welcome).id)
+ assert_no_queries { assert_includes p.taggings, expected }
+ assert_includes posts(:welcome).taggings, taggings(:welcome_general)
+ end
+
+ def test_polymorphic_has_one
+ expected = posts(:welcome)
+
+ tagging = Tagging.all.merge!(includes: :taggable).find(taggings(:welcome_general).id)
+ assert_no_queries { assert_equal expected, tagging.taggable }
+ end
+
+ def test_polymorphic_belongs_to
+ p = Post.all.merge!(includes: { taggings: :taggable }).find(posts(:welcome).id)
+ assert_no_queries { assert_equal posts(:welcome), p.taggings.first.taggable }
+ end
+
+ def test_preload_polymorphic_has_many_through
+ posts = Post.all.merge!(order: "posts.id").to_a
+ posts_with_tags = Post.all.merge!(includes: :tags, order: "posts.id").to_a
+ assert_equal posts.length, posts_with_tags.length
+ posts.length.times do |i|
+ assert_equal posts[i].tags.length, assert_no_queries { posts_with_tags[i].tags.length }
+ end
+ end
+
+ def test_preload_polymorph_many_types
+ taggings = Tagging.all.merge!(includes: :taggable, where: ["taggable_type != ?", "FakeModel"]).to_a
+ assert_no_queries do
+ taggings.first.taggable.id
+ taggings[1].taggable.id
+ end
+
+ taggables = taggings.map(&:taggable)
+ assert_includes taggables, items(:dvd)
+ assert_includes taggables, posts(:welcome)
+ end
+
+ def test_preload_nil_polymorphic_belongs_to
+ assert_nothing_raised do
+ Tagging.all.merge!(includes: :taggable, where: ["taggable_type IS NULL"]).to_a
+ end
+ end
+
+ def test_preload_polymorphic_has_many
+ posts = Post.all.merge!(order: "posts.id").to_a
+ posts_with_taggings = Post.all.merge!(includes: :taggings, order: "posts.id").to_a
+ assert_equal posts.length, posts_with_taggings.length
+ posts.length.times do |i|
+ assert_equal posts[i].taggings.length, assert_no_queries { posts_with_taggings[i].taggings.length }
+ end
+ end
+
+ def test_belongs_to_shared_parent
+ comments = Comment.all.merge!(includes: :post, where: "post_id = 1").to_a
+ assert_no_queries do
+ assert_equal comments.first.post, comments[1].post
+ end
+ end
+
+ def test_has_many_through_include_uses_array_include_after_loaded
+ david = authors(:david)
+ david.categories.load_target
+
+ category = david.categories.first
+
+ assert_no_queries do
+ assert_predicate david.categories, :loaded?
+ assert_includes david.categories, category
+ end
+ end
+
+ def test_has_many_through_include_checks_if_record_exists_if_target_not_loaded
+ david = authors(:david)
+ category = david.categories.first
+
+ david.reload
+ assert_not_predicate david.categories, :loaded?
+ assert_queries(1) do
+ assert_includes david.categories, category
+ end
+ assert_not_predicate david.categories, :loaded?
+ end
+
+ def test_has_many_through_include_returns_false_for_non_matching_record_to_verify_scoping
+ david = authors(:david)
+ category = Category.create!(name: "Not Associated")
+
+ assert_not_predicate david.categories, :loaded?
+ assert ! david.categories.include?(category)
+ end
+
+ def test_has_many_through_goes_through_all_sti_classes
+ sub_sti_post = SubStiPost.create!(title: "test", body: "test", author_id: 1)
+ new_comment = sub_sti_post.comments.create(body: "test")
+
+ assert_equal [9, 10, new_comment.id], authors(:david).sti_post_comments.map(&:id).sort
+ end
+
+ def test_has_many_with_pluralize_table_names_false
+ aircraft = Aircraft.create!(name: "Airbus 380")
+ engine = Engine.create!(car_id: aircraft.id)
+ assert_equal aircraft.engines, [engine]
+ end
+
+ def test_proper_error_message_for_eager_load_and_includes_association_errors
+ includes_error = assert_raises(ActiveRecord::ConfigurationError) {
+ Post.includes(:nonexistent_relation).where(nonexistent_relation: { name: "Rochester" }).find(1)
+ }
+ assert_equal("Can't join 'Post' to association named 'nonexistent_relation'; perhaps you misspelled it?", includes_error.message)
+
+ eager_load_error = assert_raises(ActiveRecord::ConfigurationError) {
+ Post.eager_load(:nonexistent_relation).where(nonexistent_relation: { name: "Rochester" }).find(1)
+ }
+ assert_equal("Can't join 'Post' to association named 'nonexistent_relation'; perhaps you misspelled it?", eager_load_error.message)
+
+ includes_and_eager_load_error = assert_raises(ActiveRecord::ConfigurationError) {
+ Post.eager_load(:nonexistent_relation).includes(:nonexistent_relation).where(nonexistent_relation: { name: "Rochester" }).find(1)
+ }
+ assert_equal("Can't join 'Post' to association named 'nonexistent_relation'; perhaps you misspelled it?", includes_and_eager_load_error.message)
+ end
+
+ private
+ # create dynamic Post models to allow different dependency options
+ def find_post_with_dependency(post_id, association, association_name, dependency)
+ class_name = "PostWith#{association.to_s.classify}#{dependency.to_s.classify}"
+ Post.find(post_id).update_columns type: class_name
+ klass = Object.const_set(class_name, Class.new(ActiveRecord::Base))
+ klass.table_name = "posts"
+ klass.send(association, association_name, as: :taggable, dependent: dependency)
+ klass.find(post_id)
+ end
+end
diff --git a/activerecord/test/cases/associations/left_outer_join_association_test.rb b/activerecord/test/cases/associations/left_outer_join_association_test.rb
new file mode 100644
index 0000000000..7b5c394177
--- /dev/null
+++ b/activerecord/test/cases/associations/left_outer_join_association_test.rb
@@ -0,0 +1,90 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/post"
+require "models/comment"
+require "models/author"
+require "models/essay"
+require "models/categorization"
+require "models/person"
+
+class LeftOuterJoinAssociationTest < ActiveRecord::TestCase
+ fixtures :authors, :author_addresses, :essays, :posts, :comments, :categorizations, :people
+
+ def test_construct_finder_sql_applies_aliases_tables_on_association_conditions
+ result = Author.left_outer_joins(:thinking_posts, :welcome_posts).to_a
+ assert_equal authors(:david), result.first
+ end
+
+ def test_construct_finder_sql_does_not_table_name_collide_on_duplicate_associations
+ assert_nothing_raised do
+ queries = capture_sql do
+ Person.left_outer_joins(agents: { agents: :agents })
+ .left_outer_joins(agents: { agents: { primary_contact: :agents } }).to_a
+ end
+ assert queries.any? { |sql| /agents_people_4/i.match?(sql) }
+ end
+ end
+
+ def test_left_outer_joins_count_is_same_as_size_of_loaded_results
+ assert_equal 17, Post.left_outer_joins(:comments).to_a.size
+ assert_equal 17, Post.left_outer_joins(:comments).count
+ end
+
+ def test_left_joins_aliases_left_outer_joins
+ assert_equal Post.left_outer_joins(:comments).to_sql, Post.left_joins(:comments).to_sql
+ end
+
+ def test_left_outer_joins_return_has_value_for_every_comment
+ all_post_ids = Post.pluck(:id)
+ assert_equal all_post_ids, all_post_ids & Post.left_outer_joins(:comments).pluck(:id)
+ end
+
+ def test_left_outer_joins_actually_does_a_left_outer_join
+ queries = capture_sql { Author.left_outer_joins(:posts).to_a }
+ assert queries.any? { |sql| /LEFT OUTER JOIN/i.match?(sql) }
+ end
+
+ def test_construct_finder_sql_ignores_empty_left_outer_joins_hash
+ queries = capture_sql { Author.left_outer_joins({}).to_a }
+ assert queries.none? { |sql| /LEFT OUTER JOIN/i.match?(sql) }
+ end
+
+ def test_construct_finder_sql_ignores_empty_left_outer_joins_array
+ queries = capture_sql { Author.left_outer_joins([]).to_a }
+ assert queries.none? { |sql| /LEFT OUTER JOIN/i.match?(sql) }
+ end
+
+ def test_left_outer_joins_forbids_to_use_string_as_argument
+ assert_raise(ArgumentError) { Author.left_outer_joins('LEFT OUTER JOIN "posts" ON "posts"."user_id" = "users"."id"').to_a }
+ end
+
+ def test_join_conditions_added_to_join_clause
+ queries = capture_sql { Author.left_outer_joins(:essays).to_a }
+ assert queries.any? { |sql| /writer_type.*?=.*?(Author|\?|\$1|\:a1)/i.match?(sql) }
+ assert queries.none? { |sql| /WHERE/i.match?(sql) }
+ end
+
+ def test_find_with_sti_join
+ scope = Post.left_outer_joins(:special_comments).where(id: posts(:sti_comments).id)
+
+ # The join should match SpecialComment and its subclasses only
+ assert_empty scope.where("comments.type" => "Comment")
+ assert_not_empty scope.where("comments.type" => "SpecialComment")
+ assert_not_empty scope.where("comments.type" => "SubSpecialComment")
+ end
+
+ def test_does_not_override_select
+ authors = Author.select("authors.name, #{%{(authors.author_address_id || ' ' || authors.author_address_extra_id) as addr_id}}").left_outer_joins(:posts)
+ assert_predicate authors, :any?
+ assert_respond_to authors.first, :addr_id
+ end
+
+ test "the default scope of the target is applied when joining associations" do
+ author = Author.create! name: "Jon"
+ author.categorizations.create!
+ author.categorizations.create! special: true
+
+ assert_equal [author], Author.where(id: author).left_outer_joins(:special_categorizations)
+ end
+end
diff --git a/activerecord/test/cases/associations/nested_through_associations_test.rb b/activerecord/test/cases/associations/nested_through_associations_test.rb
new file mode 100644
index 0000000000..03ed1c1d47
--- /dev/null
+++ b/activerecord/test/cases/associations/nested_through_associations_test.rb
@@ -0,0 +1,622 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/author"
+require "models/post"
+require "models/person"
+require "models/reference"
+require "models/job"
+require "models/reader"
+require "models/comment"
+require "models/tag"
+require "models/tagging"
+require "models/subscriber"
+require "models/book"
+require "models/subscription"
+require "models/rating"
+require "models/member"
+require "models/member_detail"
+require "models/member_type"
+require "models/sponsor"
+require "models/club"
+require "models/organization"
+require "models/category"
+require "models/categorization"
+require "models/membership"
+require "models/essay"
+require "models/hotel"
+require "models/department"
+require "models/chef"
+require "models/cake_designer"
+require "models/drink_designer"
+
+class NestedThroughAssociationsTest < ActiveRecord::TestCase
+ fixtures :authors, :author_addresses, :books, :posts, :subscriptions, :subscribers, :tags, :taggings,
+ :people, :readers, :references, :jobs, :ratings, :comments, :members, :member_details,
+ :member_types, :sponsors, :clubs, :organizations, :categories, :categories_posts,
+ :categorizations, :memberships, :essays
+
+ # Through associations can either use the has_many or has_one macros.
+ #
+ # has_many
+ # - Source reflection can be has_many, has_one, belongs_to or has_and_belongs_to_many
+ # - Through reflection can be has_many, has_one, belongs_to or has_and_belongs_to_many
+ #
+ # has_one
+ # - Source reflection can be has_one or belongs_to
+ # - Through reflection can be has_one or belongs_to
+ #
+ # Additionally, the source reflection and/or through reflection may be subject to
+ # polymorphism and/or STI.
+ #
+ # When testing these, we need to make sure it works via loading the association directly, or
+ # joining the association, or including the association. We also need to ensure that associations
+ # are readonly where relevant.
+
+ # has_many through
+ # Source: has_many through
+ # Through: has_many
+ def test_has_many_through_has_many_with_has_many_through_source_reflection
+ general = tags(:general)
+ assert_equal [general, general], authors(:david).tags
+ end
+
+ def test_has_many_through_has_many_with_has_many_through_source_reflection_preload
+ authors = assert_queries(5) { Author.includes(:tags).to_a }
+ general = tags(:general)
+
+ assert_no_queries do
+ assert_equal [general, general], authors.first.tags
+ end
+ end
+
+ def test_has_many_through_has_many_with_has_many_through_source_reflection_preload_via_joins
+ assert_includes_and_joins_equal(
+ Author.where("tags.id" => tags(:general).id),
+ [authors(:david)], :tags
+ )
+
+ # This ensures that the polymorphism of taggings is being observed correctly
+ authors = Author.joins(:tags).where("taggings.taggable_type" => "FakeModel")
+ assert_empty authors
+ end
+
+ # has_many through
+ # Source: has_many
+ # Through: has_many through
+ def test_has_many_through_has_many_through_with_has_many_source_reflection
+ luke, david = subscribers(:first), subscribers(:second)
+ assert_equal [luke, david, david], authors(:david).subscribers.order("subscribers.nick")
+ end
+
+ def test_has_many_through_has_many_through_with_has_many_source_reflection_preload
+ luke, david = subscribers(:first), subscribers(:second)
+ authors = assert_queries(4) { Author.includes(:subscribers).to_a }
+ assert_no_queries do
+ assert_equal [luke, david, david], authors.first.subscribers.sort_by(&:nick)
+ end
+ end
+
+ def test_has_many_through_has_many_through_with_has_many_source_reflection_preload_via_joins
+ # All authors with subscribers where one of the subscribers' nick is 'alterself'
+ assert_includes_and_joins_equal(
+ Author.where("subscribers.nick" => "alterself"),
+ [authors(:david)], :subscribers
+ )
+ end
+
+ # has_many through
+ # Source: has_one through
+ # Through: has_one
+ def test_has_many_through_has_one_with_has_one_through_source_reflection
+ assert_equal [member_types(:founding)], members(:groucho).nested_member_types
+ end
+
+ def test_has_many_through_has_one_with_has_one_through_source_reflection_preload
+ members = assert_queries(4) { Member.includes(:nested_member_types).to_a }
+ founding = member_types(:founding)
+ assert_no_queries do
+ assert_equal [founding], members.first.nested_member_types
+ end
+ end
+
+ def test_has_many_through_has_one_with_has_one_through_source_reflection_preload_via_joins
+ assert_includes_and_joins_equal(
+ Member.where("member_types.id" => member_types(:founding).id),
+ [members(:groucho)], :nested_member_types
+ )
+ end
+
+ # has_many through
+ # Source: has_one
+ # Through: has_one through
+ def test_has_many_through_has_one_through_with_has_one_source_reflection
+ assert_equal [sponsors(:moustache_club_sponsor_for_groucho)], members(:groucho).nested_sponsors
+ end
+
+ def test_has_many_through_has_one_through_with_has_one_source_reflection_preload
+ members = assert_queries(4) { Member.includes(:nested_sponsors).to_a }
+ mustache = sponsors(:moustache_club_sponsor_for_groucho)
+ assert_no_queries(ignore_none: false) do
+ assert_equal [mustache], members.first.nested_sponsors
+ end
+ end
+
+ def test_has_many_through_has_one_through_with_has_one_source_reflection_preload_via_joins
+ assert_includes_and_joins_equal(
+ Member.where("sponsors.id" => sponsors(:moustache_club_sponsor_for_groucho).id),
+ [members(:groucho)], :nested_sponsors
+ )
+ end
+
+ # has_many through
+ # Source: has_many through
+ # Through: has_one
+ def test_has_many_through_has_one_with_has_many_through_source_reflection
+ groucho_details, other_details = member_details(:groucho), member_details(:some_other_guy)
+
+ assert_equal [groucho_details, other_details],
+ members(:groucho).organization_member_details.order("member_details.id")
+ end
+
+ def test_has_many_through_has_one_with_has_many_through_source_reflection_preload
+ ActiveRecord::Base.connection.table_alias_length # preheat cache
+ members = assert_queries(4) { Member.includes(:organization_member_details).to_a.sort_by(&:id) }
+ groucho_details, other_details = member_details(:groucho), member_details(:some_other_guy)
+
+ assert_no_queries do
+ assert_equal [groucho_details, other_details], members.first.organization_member_details.sort_by(&:id)
+ end
+ end
+
+ def test_has_many_through_has_one_with_has_many_through_source_reflection_preload_via_joins
+ assert_includes_and_joins_equal(
+ Member.where("member_details.id" => member_details(:groucho).id).order("member_details.id"),
+ [members(:groucho), members(:some_other_guy)], :organization_member_details
+ )
+
+ members = Member.joins(:organization_member_details).
+ where("member_details.id" => 9)
+ assert_empty members
+ end
+
+ # has_many through
+ # Source: has_many
+ # Through: has_one through
+ def test_has_many_through_has_one_through_with_has_many_source_reflection
+ groucho_details, other_details = member_details(:groucho), member_details(:some_other_guy)
+
+ assert_equal [groucho_details, other_details],
+ members(:groucho).organization_member_details_2.order("member_details.id")
+ end
+
+ def test_has_many_through_has_one_through_with_has_many_source_reflection_preload
+ members = assert_queries(4) { Member.includes(:organization_member_details_2).to_a.sort_by(&:id) }
+ groucho_details, other_details = member_details(:groucho), member_details(:some_other_guy)
+
+ # postgresql test if randomly executed then executes "SHOW max_identifier_length". Hence
+ # the need to ignore certain predefined sqls that deal with system calls.
+ assert_no_queries(ignore_none: false) do
+ assert_equal [groucho_details, other_details], members.first.organization_member_details_2.sort_by(&:id)
+ end
+ end
+
+ def test_has_many_through_has_one_through_with_has_many_source_reflection_preload_via_joins
+ assert_includes_and_joins_equal(
+ Member.where("member_details.id" => member_details(:groucho).id).order("member_details.id"),
+ [members(:groucho), members(:some_other_guy)], :organization_member_details_2
+ )
+
+ members = Member.joins(:organization_member_details_2).
+ where("member_details.id" => 9)
+ assert_empty members
+ end
+
+ # has_many through
+ # Source: has_and_belongs_to_many
+ # Through: has_many
+ def test_has_many_through_has_many_with_has_and_belongs_to_many_source_reflection
+ general, cooking = categories(:general), categories(:cooking)
+
+ assert_equal [general, cooking], authors(:bob).post_categories.order("categories.id")
+ end
+
+ def test_has_many_through_has_many_with_has_and_belongs_to_many_source_reflection_preload
+ authors = assert_queries(4) { Author.includes(:post_categories).to_a.sort_by(&:id) }
+ general, cooking = categories(:general), categories(:cooking)
+
+ assert_no_queries do
+ assert_equal [general, cooking], authors[2].post_categories.sort_by(&:id)
+ end
+ end
+
+ def test_has_many_through_has_many_with_has_and_belongs_to_many_source_reflection_preload_via_joins
+ # preload table schemas
+ Author.joins(:post_categories).first
+
+ assert_includes_and_joins_equal(
+ Author.where("categories.id" => categories(:cooking).id),
+ [authors(:bob)], :post_categories
+ )
+ end
+
+ # has_many through
+ # Source: has_many
+ # Through: has_and_belongs_to_many
+ def test_has_many_through_has_and_belongs_to_many_with_has_many_source_reflection
+ greetings, more = comments(:greetings), comments(:more_greetings)
+
+ assert_equal [greetings, more], categories(:technology).post_comments.order("comments.id")
+ end
+
+ def test_has_many_through_has_and_belongs_to_many_with_has_many_source_reflection_preload
+ Category.includes(:post_comments).to_a # preheat cache
+ categories = assert_queries(4) { Category.includes(:post_comments).to_a.sort_by(&:id) }
+ greetings, more = comments(:greetings), comments(:more_greetings)
+
+ assert_no_queries do
+ assert_equal [greetings, more], categories[1].post_comments.sort_by(&:id)
+ end
+ end
+
+ def test_has_many_through_has_and_belongs_to_many_with_has_many_source_reflection_preload_via_joins
+ # preload table schemas
+ Category.joins(:post_comments).first
+
+ assert_includes_and_joins_equal(
+ Category.where("comments.id" => comments(:more_greetings).id).order("categories.id"),
+ [categories(:general), categories(:technology)], :post_comments
+ )
+ end
+
+ # has_many through
+ # Source: has_many through a habtm
+ # Through: has_many through
+ def test_has_many_through_has_many_with_has_many_through_habtm_source_reflection
+ greetings, more = comments(:greetings), comments(:more_greetings)
+
+ assert_equal [greetings, more], authors(:bob).category_post_comments.order("comments.id")
+ end
+
+ def test_has_many_through_has_many_with_has_many_through_habtm_source_reflection_preload
+ authors = assert_queries(6) { Author.includes(:category_post_comments).to_a.sort_by(&:id) }
+ greetings, more = comments(:greetings), comments(:more_greetings)
+
+ assert_no_queries do
+ assert_equal [greetings, more], authors[2].category_post_comments.sort_by(&:id)
+ end
+ end
+
+ def test_has_many_through_has_many_with_has_many_through_habtm_source_reflection_preload_via_joins
+ # preload table schemas
+ Author.joins(:category_post_comments).first
+
+ assert_includes_and_joins_equal(
+ Author.where("comments.id" => comments(:does_it_hurt).id).order("authors.id"),
+ [authors(:david), authors(:mary)], :category_post_comments
+ )
+ end
+
+ # has_many through
+ # Source: belongs_to
+ # Through: has_many through
+ def test_has_many_through_has_many_through_with_belongs_to_source_reflection
+ assert_equal [tags(:general), tags(:general)], authors(:david).tagging_tags
+ end
+
+ def test_has_many_through_has_many_through_with_belongs_to_source_reflection_preload
+ authors = assert_queries(5) { Author.includes(:tagging_tags).to_a }
+ general = tags(:general)
+
+ assert_no_queries do
+ assert_equal [general, general], authors.first.tagging_tags
+ end
+ end
+
+ def test_has_many_through_has_many_through_with_belongs_to_source_reflection_preload_via_joins
+ assert_includes_and_joins_equal(
+ Author.where("tags.id" => tags(:general).id),
+ [authors(:david)], :tagging_tags
+ )
+ end
+
+ # has_many through
+ # Source: has_many through
+ # Through: belongs_to
+ def test_has_many_through_belongs_to_with_has_many_through_source_reflection
+ welcome_general, thinking_general = taggings(:welcome_general), taggings(:thinking_general)
+
+ assert_equal [welcome_general, thinking_general],
+ categorizations(:david_welcome_general).post_taggings.order("taggings.id")
+ end
+
+ def test_has_many_through_belongs_to_with_has_many_through_source_reflection_preload
+ categorizations = assert_queries(4) { Categorization.includes(:post_taggings).to_a.sort_by(&:id) }
+ welcome_general, thinking_general = taggings(:welcome_general), taggings(:thinking_general)
+
+ assert_no_queries do
+ assert_equal [welcome_general, thinking_general], categorizations.first.post_taggings.sort_by(&:id)
+ end
+ end
+
+ def test_has_many_through_belongs_to_with_has_many_through_source_reflection_preload_via_joins
+ assert_includes_and_joins_equal(
+ Categorization.where("taggings.id" => taggings(:welcome_general).id).order("taggings.id"),
+ [categorizations(:david_welcome_general)], :post_taggings
+ )
+ end
+
+ # has_one through
+ # Source: has_one through
+ # Through: has_one
+ def test_has_one_through_has_one_with_has_one_through_source_reflection
+ assert_equal member_types(:founding), members(:groucho).nested_member_type
+ end
+
+ def test_has_one_through_has_one_with_has_one_through_source_reflection_preload
+ members = assert_queries(4) { Member.includes(:nested_member_type).to_a.sort_by(&:id) }
+ founding = member_types(:founding)
+
+ assert_no_queries do
+ assert_equal founding, members.first.nested_member_type
+ end
+ end
+
+ def test_has_one_through_has_one_with_has_one_through_source_reflection_preload_via_joins
+ assert_includes_and_joins_equal(
+ Member.where("member_types.id" => member_types(:founding).id),
+ [members(:groucho)], :nested_member_type
+ )
+ end
+
+ # has_one through
+ # Source: belongs_to
+ # Through: has_one through
+ def test_has_one_through_has_one_through_with_belongs_to_source_reflection
+ assert_equal categories(:general), members(:groucho).club_category
+ end
+
+ def test_joins_and_includes_from_through_models_not_included_in_association
+ prev_default_scope = Club.default_scopes
+
+ [:includes, :preload, :joins, :eager_load].each do |q|
+ Club.default_scopes = [proc { Club.send(q, :category) }]
+ assert_equal categories(:general), members(:groucho).reload.club_category
+ end
+ ensure
+ Club.default_scopes = prev_default_scope
+ end
+
+ def test_has_one_through_has_one_through_with_belongs_to_source_reflection_preload
+ members = assert_queries(4) { Member.includes(:club_category).to_a.sort_by(&:id) }
+ general = categories(:general)
+
+ assert_no_queries do
+ assert_equal general, members.first.club_category
+ end
+ end
+
+ def test_has_one_through_has_one_through_with_belongs_to_source_reflection_preload_via_joins
+ assert_includes_and_joins_equal(
+ Member.where("categories.id" => categories(:technology).id),
+ [members(:blarpy_winkup)], :club_category
+ )
+ end
+
+ def test_distinct_has_many_through_a_has_many_through_association_on_source_reflection
+ author = authors(:david)
+ assert_equal [tags(:general)], author.distinct_tags
+ end
+
+ def test_distinct_has_many_through_a_has_many_through_association_on_through_reflection
+ author = authors(:david)
+ assert_equal [subscribers(:first), subscribers(:second)],
+ author.distinct_subscribers.order("subscribers.nick")
+ end
+
+ def test_nested_has_many_through_with_a_table_referenced_multiple_times
+ author = authors(:bob)
+ assert_equal [posts(:misc_by_bob), posts(:misc_by_mary), posts(:other_by_bob), posts(:other_by_mary)],
+ author.similar_posts.sort_by(&:id)
+
+ # Mary and Bob both have posts in misc, but they are the only ones.
+ authors = Author.joins(:similar_posts).where("posts.id" => posts(:misc_by_bob).id)
+ assert_equal [authors(:mary), authors(:bob)], authors.distinct.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_empty authors
+ authors = Author.joins(:similar_posts).where("taggings_authors_join.taggable_type" => "FakeModel")
+ assert_empty authors
+ end
+
+ def test_nested_has_many_through_with_scope_on_polymorphic_reflection
+ authors = Author.joins(:ordered_posts).where("posts.id" => posts(:misc_by_bob).id)
+ assert_equal [authors(:mary), authors(:bob)], authors.distinct.sort_by(&:id)
+ end
+
+ def test_has_many_through_with_foreign_key_option_on_through_reflection
+ assert_equal [posts(:welcome), posts(:authorless)], people(:david).agents_posts.order("posts.id")
+ assert_equal [authors(:david)], references(:david_unicyclist).agents_posts_authors
+
+ references = Reference.joins(:agents_posts_authors).where("authors.id" => authors(:david).id)
+ assert_equal [references(:david_unicyclist)], references
+ end
+
+ def test_has_many_through_with_foreign_key_option_on_source_reflection
+ assert_equal [people(:michael), people(:susan)], jobs(:unicyclist).agents.order("people.id")
+
+ jobs = Job.joins(:agents)
+ assert_equal [jobs(:unicyclist), jobs(:unicyclist)], jobs
+ end
+
+ def test_has_many_through_with_sti_on_through_reflection
+ ratings = posts(:sti_comments).special_comments_ratings.sort_by(&:id)
+ assert_equal [ratings(:special_comment_rating), ratings(:sub_special_comment_rating)], ratings
+
+ # Ensure STI is respected in the join
+ scope = Post.joins(:special_comments_ratings).where(id: posts(:sti_comments).id)
+ assert_empty scope.where("comments.type" => "Comment")
+ assert_not_empty scope.where("comments.type" => "SpecialComment")
+ assert_not_empty scope.where("comments.type" => "SubSpecialComment")
+ 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_empty scope.where("comments.type" => "Comment")
+ assert_not_empty scope.where("comments.type" => "SpecialComment")
+ 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::HasOneThroughNestedAssociationsAreReadonly) do
+ groucho.nested_member_type = founding
+ end
+ end
+
+ def test_nested_has_many_through_with_conditions_on_through_associations
+ assert_equal [tags(:blue)], authors(:bob).misc_post_first_blue_tags
+ end
+
+ def test_nested_has_many_through_with_conditions_on_through_associations_preload
+ assert_empty Author.where("tags.id" => 100).joins(:misc_post_first_blue_tags)
+
+ authors = assert_queries(3) { Author.includes(:misc_post_first_blue_tags).to_a.sort_by(&:id) }
+ blue = tags(:blue)
+
+ assert_no_queries do
+ assert_equal [blue], authors[2].misc_post_first_blue_tags
+ end
+ end
+
+ def test_nested_has_many_through_with_conditions_on_through_associations_preload_via_joins
+ # Pointless condition to force single-query loading
+ assert_includes_and_joins_equal(
+ Author.where("tags.id = tags.id").references(:tags),
+ [authors(:bob)], :misc_post_first_blue_tags
+ )
+ end
+
+ def test_nested_has_many_through_with_conditions_on_source_associations
+ assert_equal [tags(:blue)], authors(:bob).misc_post_first_blue_tags_2
+ end
+
+ def test_nested_has_many_through_with_conditions_on_source_associations_preload
+ authors = assert_queries(4) { Author.includes(:misc_post_first_blue_tags_2).to_a.sort_by(&:id) }
+ blue = tags(:blue)
+
+ assert_no_queries do
+ assert_equal [blue], authors[2].misc_post_first_blue_tags_2
+ end
+ end
+
+ def test_nested_has_many_through_with_conditions_on_source_associations_preload_via_joins
+ # Pointless condition to force single-query loading
+ assert_includes_and_joins_equal(
+ Author.where("tags.id = tags.id").references(:tags),
+ [authors(:bob)], :misc_post_first_blue_tags_2
+ )
+ end
+
+ def test_nested_has_many_through_with_foreign_key_option_on_the_source_reflection_through_reflection
+ assert_equal [categories(:general)], organizations(:nsa).author_essay_categories
+
+ organizations = Organization.joins(:author_essay_categories).
+ where("categories.id" => categories(:general).id)
+ assert_equal [organizations(:nsa)], organizations
+
+ assert_equal categories(:general), organizations(:nsa).author_owned_essay_category
+
+ organizations = Organization.joins(:author_owned_essay_category).
+ where("categories.id" => categories(:general).id)
+ assert_equal [organizations(:nsa)], organizations
+ end
+
+ def test_nested_has_many_through_should_not_be_autosaved
+ c = Categorization.new
+ c.author = authors(:david)
+ c.post_taggings.to_a
+ assert_not_empty c.post_taggings
+ c.save
+ assert_not_empty c.post_taggings
+ end
+
+ def test_polymorphic_has_many_through_when_through_association_has_not_loaded
+ cake_designer = CakeDesigner.create!(chef: Chef.new)
+ drink_designer = DrinkDesigner.create!(chef: Chef.new)
+ department = Department.create!(chefs: [cake_designer.chef, drink_designer.chef])
+ Hotel.create!(departments: [department])
+ hotel = Hotel.includes(:cake_designers, :drink_designers).take
+
+ assert_equal [cake_designer], hotel.cake_designers
+ assert_equal [drink_designer], hotel.drink_designers
+ end
+
+ def test_polymorphic_has_many_through_when_through_association_has_already_loaded
+ cake_designer = CakeDesigner.create!(chef: Chef.new)
+ drink_designer = DrinkDesigner.create!(chef: Chef.new)
+ department = Department.create!(chefs: [cake_designer.chef, drink_designer.chef])
+ Hotel.create!(departments: [department])
+ hotel = Hotel.includes(:chefs, :cake_designers, :drink_designers).take
+
+ assert_equal [cake_designer], hotel.cake_designers
+ assert_equal [drink_designer], hotel.drink_designers
+ end
+
+ def test_polymorphic_has_many_through_joined_different_table_twice
+ cake_designer = CakeDesigner.create!(chef: Chef.new)
+ drink_designer = DrinkDesigner.create!(chef: Chef.new)
+ department = Department.create!(chefs: [cake_designer.chef, drink_designer.chef])
+ hotel = Hotel.create!(departments: [department])
+
+ assert_equal hotel, Hotel.joins(:cake_designers, :drink_designers).take
+ end
+
+ private
+
+ def assert_includes_and_joins_equal(query, expected, association)
+ actual = assert_queries(1) { query.joins(association).to_a.uniq }
+ assert_equal expected, actual
+
+ actual = assert_queries(1) { query.includes(association).to_a.uniq }
+ assert_equal expected, actual
+ end
+end
diff --git a/activerecord/test/cases/associations/required_test.rb b/activerecord/test/cases/associations/required_test.rb
new file mode 100644
index 0000000000..65a3bb5efe
--- /dev/null
+++ b/activerecord/test/cases/associations/required_test.rb
@@ -0,0 +1,132 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+
+class RequiredAssociationsTest < ActiveRecord::TestCase
+ self.use_transactional_tests = false
+
+ class Parent < ActiveRecord::Base
+ end
+
+ class Child < ActiveRecord::Base
+ end
+
+ setup do
+ @connection = ActiveRecord::Base.connection
+ @connection.create_table :parents, force: true
+ @connection.create_table :children, force: true do |t|
+ t.belongs_to :parent
+ end
+ end
+
+ teardown do
+ @connection.drop_table "parents", if_exists: true
+ @connection.drop_table "children", if_exists: true
+ end
+
+ test "belongs_to associations can be optional by default" do
+ begin
+ original_value = ActiveRecord::Base.belongs_to_required_by_default
+ ActiveRecord::Base.belongs_to_required_by_default = false
+
+ model = subclass_of(Child) do
+ belongs_to :parent, inverse_of: false,
+ class_name: "RequiredAssociationsTest::Parent"
+ end
+
+ assert model.new.save
+ assert model.new(parent: Parent.new).save
+ ensure
+ ActiveRecord::Base.belongs_to_required_by_default = original_value
+ end
+ end
+
+ test "required belongs_to associations have presence validated" do
+ model = subclass_of(Child) do
+ belongs_to :parent, required: true, inverse_of: false,
+ class_name: "RequiredAssociationsTest::Parent"
+ end
+
+ record = model.new
+ assert_not record.save
+ assert_equal ["Parent must exist"], record.errors.full_messages
+
+ record.parent = Parent.new
+ assert record.save
+ end
+
+ test "belongs_to associations can be required by default" do
+ begin
+ original_value = ActiveRecord::Base.belongs_to_required_by_default
+ ActiveRecord::Base.belongs_to_required_by_default = true
+
+ model = subclass_of(Child) do
+ belongs_to :parent, inverse_of: false,
+ class_name: "RequiredAssociationsTest::Parent"
+ end
+
+ record = model.new
+ assert_not record.save
+ assert_equal ["Parent must exist"], record.errors.full_messages
+
+ record.parent = Parent.new
+ assert record.save
+ ensure
+ ActiveRecord::Base.belongs_to_required_by_default = original_value
+ end
+ end
+
+ test "has_one associations are not required by default" do
+ model = subclass_of(Parent) do
+ has_one :child, inverse_of: false,
+ class_name: "RequiredAssociationsTest::Child"
+ end
+
+ assert model.new.save
+ assert model.new(child: Child.new).save
+ end
+
+ test "required has_one associations have presence validated" do
+ model = subclass_of(Parent) do
+ has_one :child, required: true, inverse_of: false,
+ class_name: "RequiredAssociationsTest::Child"
+ end
+
+ record = model.new
+ assert_not record.save
+ assert_equal ["Child must exist"], record.errors.full_messages
+
+ record.child = Child.new
+ assert record.save
+ end
+
+ test "required has_one associations have a correct error message" do
+ model = subclass_of(Parent) do
+ has_one :child, required: true, inverse_of: false,
+ class_name: "RequiredAssociationsTest::Child"
+ end
+
+ record = model.create
+ assert_equal ["Child must exist"], record.errors.full_messages
+ end
+
+ test "required belongs_to associations have a correct error message" do
+ model = subclass_of(Child) do
+ belongs_to :parent, required: true, inverse_of: false,
+ class_name: "RequiredAssociationsTest::Parent"
+ end
+
+ record = model.create
+ assert_equal ["Parent must exist"], record.errors.full_messages
+ end
+
+ private
+
+ def subclass_of(klass, &block)
+ subclass = Class.new(klass, &block)
+ def subclass.name
+ superclass.name
+ end
+ subclass
+ end
+end