9cadf61835
This commit adds a deprecation warning for the `query_constraints:` association option. This option will change behavior in the future versions of Rails and applications are encouraged to switch to `foreign_key:` to preserve the current behavior.
1708 lines
59 KiB
Ruby
1708 lines
59 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require "pp"
|
|
require "cases/helper"
|
|
require "models/computer"
|
|
require "models/developer"
|
|
require "models/project"
|
|
require "models/company"
|
|
require "models/categorization"
|
|
require "models/category"
|
|
require "models/post"
|
|
require "models/author"
|
|
require "models/book"
|
|
require "models/comment"
|
|
require "models/tag"
|
|
require "models/tagging"
|
|
require "models/person"
|
|
require "models/reader"
|
|
require "models/ship_part"
|
|
require "models/ship"
|
|
require "models/liquid"
|
|
require "models/molecule"
|
|
require "models/electron"
|
|
require "models/human"
|
|
require "models/interest"
|
|
require "models/pirate"
|
|
require "models/parrot"
|
|
require "models/bird"
|
|
require "models/treasure"
|
|
require "models/price_estimate"
|
|
require "models/invoice"
|
|
require "models/discount"
|
|
require "models/line_item"
|
|
require "models/shipping_line"
|
|
require "models/essay"
|
|
require "models/member"
|
|
require "models/membership"
|
|
require "models/sharded"
|
|
require "models/cpk"
|
|
require "models/member_detail"
|
|
require "models/organization"
|
|
require "models/dog"
|
|
require "models/other_dog"
|
|
|
|
|
|
class AssociationsTest < ActiveRecord::TestCase
|
|
fixtures :accounts, :companies, :developers, :projects, :developers_projects,
|
|
:computers, :people, :readers, :authors, :author_addresses, :author_favorites,
|
|
:comments, :posts, :sharded_blogs, :sharded_blog_posts, :sharded_comments, :sharded_tags, :sharded_blog_posts_tags,
|
|
:cpk_orders, :cpk_books, :cpk_reviews
|
|
|
|
def test_eager_loading_should_not_change_count_of_children
|
|
liquid = Liquid.create(name: "salty")
|
|
molecule = liquid.molecules.create(name: "molecule_1")
|
|
molecule.electrons.create(name: "electron_1")
|
|
molecule.electrons.create(name: "electron_2")
|
|
|
|
liquids = Liquid.includes(molecules: :electrons).references(:molecules).where("molecules.id is not null")
|
|
assert_equal 1, liquids[0].molecules.length
|
|
end
|
|
|
|
def test_subselect
|
|
author = authors :david
|
|
favs = author.author_favorites
|
|
fav2 = author.author_favorites.where(author: Author.where(id: author.id)).to_a
|
|
assert_equal favs, fav2
|
|
end
|
|
|
|
def test_loading_the_association_target_should_keep_child_records_marked_for_destruction
|
|
ship = Ship.create!(name: "The good ship Dollypop")
|
|
part = ship.parts.create!(name: "Mast")
|
|
part.mark_for_destruction
|
|
assert_predicate ship.parts[0], :marked_for_destruction?
|
|
end
|
|
|
|
def test_loading_the_association_target_should_load_most_recent_attributes_for_child_records_marked_for_destruction
|
|
ship = Ship.create!(name: "The good ship Dollypop")
|
|
part = ship.parts.create!(name: "Mast")
|
|
part.mark_for_destruction
|
|
ShipPart.find(part.id).update_columns(name: "Deck")
|
|
assert_equal "Deck", ship.parts[0].name
|
|
end
|
|
|
|
def test_loading_cpk_association_when_persisted_and_in_memory_differ
|
|
order = Cpk::Order.create!(id: [1, 2], status: "paid")
|
|
book = order.books.create!(id: [3, 4], title: "Book")
|
|
|
|
Cpk::Book.find(book.id).update_columns(title: "A different title")
|
|
order.books.load
|
|
|
|
assert_equal [3, 4], book.id
|
|
end
|
|
|
|
def test_include_with_order_works
|
|
assert_nothing_raised { Account.all.merge!(order: "id", includes: :firm).first }
|
|
assert_nothing_raised { Account.all.merge!(order: :id, includes: :firm).first }
|
|
end
|
|
|
|
def test_bad_collection_keys
|
|
assert_raise(ArgumentError, "ActiveRecord should have barked on bad collection keys") do
|
|
Class.new(ActiveRecord::Base).has_many(:wheels, name: "wheels")
|
|
end
|
|
end
|
|
|
|
def test_should_construct_new_finder_sql_after_create
|
|
person = Person.new first_name: "clark"
|
|
assert_equal [], person.readers.to_a
|
|
person.save!
|
|
reader = Reader.create! person: person, post: Post.new(title: "foo", body: "bar")
|
|
assert person.readers.find(reader.id)
|
|
end
|
|
|
|
def test_force_reload
|
|
firm = Firm.new("name" => "A New Firm, Inc")
|
|
firm.save
|
|
firm.clients.each { } # forcing to load all clients
|
|
assert_predicate firm.clients, :empty?, "New firm shouldn't have client objects"
|
|
assert_equal 0, firm.clients.size, "New firm should have 0 clients"
|
|
|
|
client = Client.new("name" => "TheClient.com", "firm_id" => firm.id)
|
|
client.save
|
|
|
|
assert_predicate firm.clients, :empty?, "New firm should have cached no client objects"
|
|
assert_equal 0, firm.clients.size, "New firm should have cached 0 clients count"
|
|
|
|
firm.clients.reload
|
|
|
|
assert_not firm.clients.empty?, "New firm should have reloaded client objects"
|
|
assert_equal 1, firm.clients.size, "New firm should have reloaded clients count"
|
|
end
|
|
|
|
def test_using_limitable_reflections_helper
|
|
using_limitable_reflections = lambda { |reflections| Tagging.all.send :using_limitable_reflections?, reflections }
|
|
belongs_to_reflections = [Tagging.reflect_on_association(:tag), Tagging.reflect_on_association(:super_tag)]
|
|
has_many_reflections = [Tag.reflect_on_association(:taggings), Developer.reflect_on_association(:projects)]
|
|
mixed_reflections = (belongs_to_reflections + has_many_reflections).uniq
|
|
assert using_limitable_reflections.call(belongs_to_reflections), "Belong to associations are limitable"
|
|
assert_not using_limitable_reflections.call(has_many_reflections), "All has many style associations are not limitable"
|
|
assert_not using_limitable_reflections.call(mixed_reflections), "No collection associations (has many style) should pass"
|
|
end
|
|
|
|
def test_association_with_references
|
|
firm = companies(:first_firm)
|
|
assert_equal [:foo], firm.association_with_references.references_values
|
|
end
|
|
|
|
def test_belongs_to_a_model_with_composite_foreign_key_finds_associated_record
|
|
comment = sharded_comments(:great_comment_blog_post_one)
|
|
blog_post = sharded_blog_posts(:great_post_blog_one)
|
|
|
|
assert_equal(blog_post, comment.blog_post)
|
|
end
|
|
|
|
def test_belongs_to_a_cpk_model_by_id_attribute
|
|
order = cpk_orders(:cpk_groceries_order_1)
|
|
_order_shop_id, order_id = order.id
|
|
agreement = Cpk::OrderAgreement.create(order_id: order_id, signature: "signed")
|
|
|
|
assert_equal(order, agreement.order)
|
|
end
|
|
|
|
def test_belongs_to_a_model_with_composite_primary_key_uses_composite_pk_in_sql
|
|
comment = sharded_comments(:great_comment_blog_post_one)
|
|
|
|
sql = capture_sql do
|
|
comment.blog_post
|
|
end.first
|
|
|
|
assert_match(/#{Regexp.escape(Sharded::BlogPost.lease_connection.quote_table_name("sharded_blog_posts.blog_id"))} =/, sql)
|
|
assert_match(/#{Regexp.escape(Sharded::BlogPost.lease_connection.quote_table_name("sharded_blog_posts.id"))} =/, sql)
|
|
end
|
|
|
|
def test_querying_by_whole_associated_records_using_query_constraints
|
|
comments = [sharded_comments(:great_comment_blog_post_one), sharded_comments(:great_comment_blog_post_two)]
|
|
|
|
blog_posts = Sharded::BlogPost.where(comments: comments).to_a
|
|
|
|
expected_posts = [sharded_blog_posts(:great_post_blog_one), sharded_blog_posts(:great_post_blog_two)]
|
|
assert_equal(expected_posts.map(&:id).sort, blog_posts.map(&:id).sort)
|
|
end
|
|
|
|
def test_querying_by_single_associated_record_works_using_query_constraints
|
|
comments = [sharded_comments(:great_comment_blog_post_one), sharded_comments(:great_comment_blog_post_two)]
|
|
|
|
blog_posts = Sharded::BlogPost.where(comments: comments.last).to_a
|
|
|
|
expected_posts = [sharded_blog_posts(:great_post_blog_two)]
|
|
assert_equal(expected_posts.map(&:id).sort, blog_posts.map(&:id).sort)
|
|
end
|
|
|
|
def test_querying_by_relation_with_composite_key
|
|
expected_posts = [sharded_blog_posts(:great_post_blog_one), sharded_blog_posts(:great_post_blog_two)]
|
|
|
|
blog_posts = Sharded::BlogPost.where(comments: Sharded::Comment.where(body: "I really enjoyed the post!")).to_a
|
|
|
|
assert_equal(expected_posts.map(&:id).sort, blog_posts.map(&:id).sort)
|
|
end
|
|
|
|
def test_has_many_association_with_composite_foreign_key_loads_records
|
|
blog_post = sharded_blog_posts(:great_post_blog_one)
|
|
|
|
comments = blog_post.comments.to_a
|
|
assert_includes(comments, sharded_comments(:wow_comment_blog_post_one))
|
|
assert_includes(comments, sharded_comments(:great_comment_blog_post_one))
|
|
end
|
|
|
|
def test_belongs_to_with_explicit_composite_foreign_key
|
|
car = Cpk::Car.create(make: "Tesla", model: "Model S")
|
|
review = Cpk::CarReview.create(car: car, comment: "Great car!", rating: 5)
|
|
|
|
review.reload
|
|
|
|
sql = capture_sql do
|
|
assert_equal(car, review.car)
|
|
end
|
|
|
|
assert_match(/#{Regexp.escape(Cpk::Car.lease_connection.quote_table_name("cpk_cars.make"))} =/, sql.first)
|
|
assert_match(/#{Regexp.escape(Cpk::Car.lease_connection.quote_table_name("cpk_cars.model"))} =/, sql.first)
|
|
end
|
|
|
|
def test_cpk_model_has_many_records_by_id_attribute
|
|
order = cpk_orders(:cpk_groceries_order_1)
|
|
_order_shop_id, order_id = order.id
|
|
agreements = 2.times.map { Cpk::OrderAgreement.create(order_id: order_id, signature: "signed") }
|
|
|
|
assert_equal(agreements.sort, order.order_agreements.to_a.sort)
|
|
end
|
|
|
|
def test_has_many_association_from_a_model_with_query_constraints_different_from_the_association
|
|
blog_post = sharded_blog_posts(:great_post_blog_one)
|
|
blog_post = Sharded::BlogPostWithRevision.find(blog_post.id)
|
|
comments = []
|
|
expected_comments = Sharded::Comment.where(blog_id: blog_post.blog_id, blog_post_id: blog_post.id).to_a
|
|
|
|
sql = capture_sql do
|
|
comments = blog_post.comments.to_a
|
|
end.first
|
|
|
|
assert_match(/WHERE .*#{Regexp.escape(Sharded::Comment.lease_connection.quote_table_name("sharded_comments.blog_id"))} =/, sql)
|
|
assert_not_empty(comments)
|
|
assert_equal(expected_comments.sort, comments.sort)
|
|
end
|
|
|
|
def test_query_constraints_over_three_without_defining_explicit_foreign_key_query_constraints_raises
|
|
Sharded::BlogPostWithRevision.has_many :comments_without_query_constraints, primary_key: [:blog_id, :id], class_name: "Comment"
|
|
blog_post = sharded_blog_posts(:great_post_blog_one)
|
|
blog_post = Sharded::BlogPostWithRevision.find(blog_post.id)
|
|
|
|
error = assert_raises ArgumentError do
|
|
blog_post.comments_without_query_constraints.to_a
|
|
end
|
|
|
|
assert_equal "The query constraints list on the `Sharded::BlogPostWithRevision` model has more than 2 attributes. Active Record is unable to derive the query constraints for the association. You need to explicitly define the query constraints for this association.", error.message
|
|
end
|
|
|
|
def test_model_with_composite_query_constraints_has_many_association_sql
|
|
blog_post = sharded_blog_posts(:great_post_blog_one)
|
|
|
|
sql = capture_sql do
|
|
blog_post.comments.to_a
|
|
end.first
|
|
|
|
assert_match(/#{Regexp.escape(Sharded::Comment.lease_connection.quote_table_name("sharded_comments.blog_post_id"))} =/, sql)
|
|
assert_match(/#{Regexp.escape(Sharded::Comment.lease_connection.quote_table_name("sharded_comments.blog_id"))} =/, sql)
|
|
end
|
|
|
|
def test_belongs_to_association_does_not_use_parent_query_constraints_if_not_configured_to
|
|
comment = sharded_comments(:great_comment_blog_post_one)
|
|
blog_post = Sharded::BlogPost.new(blog_id: comment.blog_id, title: "Following best practices")
|
|
|
|
comment.blog_post_by_id = blog_post
|
|
|
|
comment.save
|
|
|
|
assert_predicate blog_post, :persisted?
|
|
assert_equal(blog_post, comment.blog_post_by_id)
|
|
end
|
|
|
|
def test_polymorphic_belongs_to_uses_parent_query_constraints
|
|
parent_post = sharded_blog_posts(:great_post_blog_one)
|
|
child_post = Sharded::BlogPost.create!(title: "Child post", blog_id: parent_post.blog_id, parent: parent_post)
|
|
child_post.reload # reload to forget the parent association
|
|
|
|
assert_equal parent_post, child_post.parent
|
|
end
|
|
|
|
def test_preloads_model_with_query_constraints_by_explicitly_configured_fk_and_pk
|
|
comment = sharded_comments(:great_comment_blog_post_one)
|
|
comments = Sharded::Comment.where(id: comment.id).preload(:blog_post_by_id).to_a
|
|
comment = comments.first
|
|
assert_equal(comment.blog_post_by_id, comment.blog_post)
|
|
end
|
|
|
|
def test_append_composite_foreign_key_has_many_association
|
|
blog_post = sharded_blog_posts(:great_post_blog_one)
|
|
comment = Sharded::Comment.new(body: "Great post! :clap:")
|
|
comment.save
|
|
blog_post.comments << comment
|
|
|
|
assert_includes(blog_post.comments, comment)
|
|
assert_equal(blog_post.id, comment.blog_post_id)
|
|
assert_equal(blog_post.blog_id, comment.blog_id)
|
|
end
|
|
|
|
def test_nullify_composite_foreign_key_has_many_association
|
|
blog_post = sharded_blog_posts(:great_post_blog_one)
|
|
comment = sharded_comments(:great_comment_blog_post_one)
|
|
|
|
assert_not_empty(blog_post.comments)
|
|
blog_post.comments = []
|
|
|
|
comment = Sharded::Comment.find(comment.id)
|
|
assert_nil(comment.blog_post_id)
|
|
assert_nil(comment.blog_id)
|
|
|
|
assert_empty(blog_post.comments)
|
|
assert_empty(blog_post.reload.comments)
|
|
end
|
|
|
|
def test_assign_persisted_composite_foreign_key_belongs_to_association
|
|
comment = sharded_comments(:great_comment_blog_post_one)
|
|
another_blog = sharded_blogs(:sharded_blog_two)
|
|
assert_not_equal(comment.blog_id, another_blog.id)
|
|
|
|
blog_post = Sharded::BlogPost.new(title: "New post", blog_id: another_blog.id)
|
|
blog_post.save
|
|
comment.blog_post = blog_post
|
|
|
|
assert_equal(blog_post, comment.blog_post)
|
|
assert_equal(comment.blog_id, blog_post.blog_id)
|
|
assert_equal(another_blog.id, comment.blog_id)
|
|
assert_equal(comment.blog_post_id, blog_post.id)
|
|
end
|
|
|
|
def test_nullify_composite_foreign_key_belongs_to_association
|
|
comment = sharded_comments(:great_comment_blog_post_one)
|
|
assert_not_nil(comment.blog_post)
|
|
|
|
comment.blog_post = nil
|
|
assert_nil(comment.blog_id)
|
|
assert_nil(comment.blog_post_id)
|
|
|
|
comment.save
|
|
assert_nil(comment.blog_post)
|
|
assert_nil(comment.reload.blog_post)
|
|
end
|
|
|
|
def test_assign_composite_foreign_key_belongs_to_association
|
|
comment = sharded_comments(:great_comment_blog_post_one)
|
|
another_blog = sharded_blogs(:sharded_blog_two)
|
|
assert_not_equal(comment.blog_id, another_blog.id)
|
|
|
|
blog_post = Sharded::BlogPost.new(title: "New post", blog_id: another_blog.id)
|
|
comment.blog_post = blog_post
|
|
|
|
assert_equal(blog_post, comment.blog_post)
|
|
assert_equal(comment.blog_id, blog_post.blog_id)
|
|
assert_equal(another_blog.id, comment.blog_id)
|
|
end
|
|
|
|
def test_query_constraints_that_dont_include_the_primary_key_raise_with_a_single_column
|
|
original = Sharded::BlogPost.instance_variable_get(:@query_constraints_list)
|
|
Sharded::BlogPost.query_constraints :title
|
|
Sharded::BlogPost.has_many :comments_without_single_column_query_constraints, primary_key: [:blog_id, :id], class_name: "Comment"
|
|
blog_post = sharded_blog_posts(:great_post_blog_one)
|
|
|
|
error = assert_raises ArgumentError do
|
|
blog_post.comments_without_single_column_query_constraints.to_a
|
|
end
|
|
|
|
assert_equal "The query constraints on the `Sharded::BlogPost` model does not include the primary key so Active Record is unable to derive the foreign key constraints for the association. You need to explicitly define the query constraints for this association.", error.message
|
|
ensure
|
|
Sharded::BlogPost.instance_variable_set(:@query_constraints_list, original)
|
|
end
|
|
|
|
def test_query_constraints_that_dont_include_the_primary_key_raise_with_multiple_columns
|
|
original = Sharded::BlogPost.instance_variable_get(:@query_constraints_list)
|
|
Sharded::BlogPost.query_constraints :title, :revision
|
|
Sharded::BlogPost.has_many :comments_without_multiple_column_query_constraints, primary_key: [:blog_id, :id], class_name: "Comment"
|
|
blog_post = sharded_blog_posts(:great_post_blog_one)
|
|
|
|
error = assert_raises ArgumentError do
|
|
blog_post.comments_without_multiple_column_query_constraints.to_a
|
|
end
|
|
|
|
assert_equal "The query constraints on the `Sharded::BlogPost` model does not include the primary key so Active Record is unable to derive the foreign key constraints for the association. You need to explicitly define the query constraints for this association.", error.message
|
|
ensure
|
|
Sharded::BlogPost.instance_variable_set(:@query_constraints_list, original)
|
|
end
|
|
|
|
def test_assign_belongs_to_cpk_model_by_id_attribute
|
|
order = cpk_orders(:cpk_groceries_order_1)
|
|
agreement = Cpk::OrderAgreement.new(signature: "signed")
|
|
|
|
agreement.order = order
|
|
agreement.save
|
|
|
|
assert_not_nil(agreement.reload.order)
|
|
assert_not_nil(agreement.order_id)
|
|
|
|
assert_equal(order, agreement.order)
|
|
_shop_id, order_id = order.id
|
|
assert_equal(order_id, agreement.order_id)
|
|
end
|
|
|
|
def test_append_composite_foreign_key_has_many_association_with_autosave
|
|
blog_post = sharded_blog_posts(:great_post_blog_one)
|
|
comment = Sharded::Comment.new(body: "Great post! :clap:")
|
|
blog_post.comments << comment
|
|
|
|
assert_predicate(comment, :persisted?)
|
|
assert_includes(blog_post.comments, comment)
|
|
assert_equal(blog_post.id, comment.blog_post_id)
|
|
assert_equal(blog_post.blog_id, comment.blog_id)
|
|
end
|
|
|
|
def test_assign_composite_foreign_key_belongs_to_association_with_autosave
|
|
comment = sharded_comments(:great_comment_blog_post_one)
|
|
another_blog = sharded_blogs(:sharded_blog_two)
|
|
assert_not_equal(comment.blog_id, another_blog.id)
|
|
|
|
blog_post = Sharded::BlogPost.new(title: "New post", blog_id: another_blog.id)
|
|
comment.blog_post = blog_post
|
|
comment.save
|
|
|
|
assert_predicate(blog_post, :persisted?)
|
|
assert_equal(blog_post, comment.blog_post)
|
|
assert_equal(comment.blog_id, blog_post.blog_id)
|
|
assert_equal(another_blog.id, comment.blog_id)
|
|
assert_equal(comment.blog_post_id, blog_post.id)
|
|
end
|
|
|
|
def test_append_composite_has_many_through_association
|
|
blog_post = sharded_blog_posts(:great_post_blog_one)
|
|
tag = Sharded::Tag.new(name: "Ruby on Rails", blog_id: blog_post.blog_id)
|
|
tag.save
|
|
|
|
blog_post.tags << tag
|
|
|
|
assert_includes(blog_post.reload.tags, tag)
|
|
assert_predicate Sharded::BlogPostTag.where(blog_post_id: blog_post.id, blog_id: blog_post.blog_id, tag_id: tag.id), :exists?
|
|
end
|
|
|
|
def test_append_composite_has_many_through_association_with_autosave
|
|
blog_post = sharded_blog_posts(:great_post_blog_one)
|
|
tag = Sharded::Tag.new(name: "Ruby on Rails", blog_id: blog_post.blog_id)
|
|
|
|
blog_post.tags << tag
|
|
|
|
assert_includes(blog_post.reload.tags, tag)
|
|
assert_predicate Sharded::BlogPostTag.where(blog_post_id: blog_post.id, blog_id: blog_post.blog_id, tag_id: tag.id), :exists?
|
|
end
|
|
|
|
def test_nullify_composite_has_many_through_association
|
|
blog_post = sharded_blog_posts(:great_post_blog_one)
|
|
assert_not_empty(blog_post.tags)
|
|
|
|
blog_post.tags = []
|
|
|
|
assert_empty(blog_post.tags)
|
|
assert_empty(blog_post.reload.tags)
|
|
assert_not_predicate Sharded::BlogPostTag.where(blog_post_id: blog_post.id, blog_id: blog_post.blog_id), :exists?
|
|
end
|
|
|
|
def test_using_query_constraints_warns_about_changing_behavior
|
|
has_many_expected_message = <<~MSG.squish
|
|
Setting `query_constraints:` option on `Sharded::BlogPost.has_many :qc_deprecated_comments` is deprecated.
|
|
To maintain current behavior, use the `foreign_key` option instead.
|
|
MSG
|
|
|
|
assert_deprecated(has_many_expected_message, ActiveRecord.deprecator) do
|
|
Sharded::BlogPost.has_many :qc_deprecated_comments,
|
|
class_name: "Sharded::Comment", query_constraints: [:blog_id, :blog_post_id]
|
|
end
|
|
|
|
belongs_to_expected_message = <<~MSG.squish
|
|
Setting `query_constraints:` option on `Sharded::Comment.belongs_to :qc_deprecated_blog_post` is deprecated.
|
|
To maintain current behavior, use the `foreign_key` option instead.
|
|
MSG
|
|
|
|
assert_deprecated(belongs_to_expected_message, ActiveRecord.deprecator) do
|
|
Sharded::Comment.belongs_to :qc_deprecated_blog_post,
|
|
class_name: "Sharded::Blog", query_constraints: [:blog_id, :blog_post_id]
|
|
end
|
|
end
|
|
end
|
|
|
|
class AssociationProxyTest < ActiveRecord::TestCase
|
|
fixtures :authors, :author_addresses, :posts, :categorizations, :categories, :developers, :projects, :developers_projects, :members
|
|
|
|
def test_push_does_not_load_target
|
|
david = authors(:david)
|
|
|
|
david.posts << (post = Post.new(title: "New on Edge", body: "More cool stuff!"))
|
|
assert_not_predicate david.posts, :loaded?
|
|
assert_includes david.posts, post
|
|
end
|
|
|
|
def test_push_has_many_through_does_not_load_target
|
|
david = authors(:david)
|
|
|
|
david.categories << categories(:technology)
|
|
assert_not_predicate david.categories, :loaded?
|
|
assert_includes david.categories, categories(:technology)
|
|
end
|
|
|
|
def test_push_followed_by_save_does_not_load_target
|
|
david = authors(:david)
|
|
|
|
david.posts << (post = Post.new(title: "New on Edge", body: "More cool stuff!"))
|
|
assert_not_predicate david.posts, :loaded?
|
|
david.save
|
|
assert_not_predicate david.posts, :loaded?
|
|
assert_includes david.posts, post
|
|
end
|
|
|
|
def test_push_does_not_lose_additions_to_new_record
|
|
josh = Author.new(name: "Josh")
|
|
josh.posts << Post.new(title: "New on Edge", body: "More cool stuff!")
|
|
assert_predicate josh.posts, :loaded?
|
|
assert_equal 1, josh.posts.size
|
|
end
|
|
|
|
def test_append_behaves_like_push
|
|
josh = Author.new(name: "Josh")
|
|
josh.posts.append Post.new(title: "New on Edge", body: "More cool stuff!")
|
|
assert_predicate josh.posts, :loaded?
|
|
assert_equal 1, josh.posts.size
|
|
end
|
|
|
|
def test_prepend_is_not_defined
|
|
josh = Author.new(name: "Josh")
|
|
assert_raises(NoMethodError) { josh.posts.prepend Post.new }
|
|
end
|
|
|
|
def test_save_on_parent_does_not_load_target
|
|
david = developers(:david)
|
|
|
|
assert_not_predicate david.projects, :loaded?
|
|
david.update_columns(created_at: Time.now)
|
|
assert_not_predicate david.projects, :loaded?
|
|
end
|
|
|
|
def test_load_does_load_target
|
|
david = developers(:david)
|
|
|
|
assert_not_predicate david.projects, :loaded?
|
|
david.projects.load
|
|
assert_predicate david.projects, :loaded?
|
|
end
|
|
|
|
def test_inspect_does_not_reload_a_not_yet_loaded_target
|
|
andreas = Developer.new name: "Andreas", log: "new developer added"
|
|
assert_not_predicate andreas.audit_logs, :loaded?
|
|
assert_match(/message: "new developer added"/, andreas.audit_logs.inspect)
|
|
assert_predicate andreas.audit_logs, :loaded?
|
|
end
|
|
|
|
def test_pretty_print_does_not_reload_a_not_yet_loaded_target
|
|
andreas = Developer.new(log: "new developer added")
|
|
assert_not_predicate andreas.audit_logs, :loaded?
|
|
out = StringIO.new
|
|
PP.pp(andreas.audit_logs, out)
|
|
assert_match(/message: "new developer added"/, out.string)
|
|
assert_predicate andreas.audit_logs, :loaded?
|
|
end
|
|
|
|
def test_save_on_parent_saves_children
|
|
developer = Developer.create name: "Bryan", salary: 50_000
|
|
assert_equal 1, developer.reload.audit_logs.size
|
|
end
|
|
|
|
def test_create_via_association_with_block
|
|
post = authors(:david).posts.create(title: "New on Edge") { |p| p.body = "More cool stuff!" }
|
|
assert_equal "New on Edge", post.title
|
|
assert_equal "More cool stuff!", post.body
|
|
end
|
|
|
|
def test_create_with_bang_via_association_with_block
|
|
post = authors(:david).posts.create!(title: "New on Edge") { |p| p.body = "More cool stuff!" }
|
|
assert_equal "New on Edge", post.title
|
|
assert_equal "More cool stuff!", post.body
|
|
end
|
|
|
|
def test_reload_returns_association
|
|
david = developers(:david)
|
|
assert_nothing_raised do
|
|
assert_equal david.projects, david.projects.reload.reload
|
|
end
|
|
end
|
|
|
|
def test_proxy_association_accessor
|
|
david = developers(:david)
|
|
assert_equal david.association(:projects), david.projects.proxy_association
|
|
end
|
|
|
|
def test_scoped_allows_conditions
|
|
assert developers(:david).projects.merge(where: "foo").to_sql.include?("foo")
|
|
end
|
|
|
|
test "getting a scope from an association" do
|
|
david = developers(:david)
|
|
|
|
assert david.projects.scope.is_a?(ActiveRecord::Relation)
|
|
assert_equal david.projects, david.projects.scope
|
|
end
|
|
|
|
test "proxy object is cached" do
|
|
david = developers(:david)
|
|
assert_same david.projects, david.projects
|
|
end
|
|
|
|
test "proxy object can be stubbed" do
|
|
david = developers(:david)
|
|
david.projects.define_singleton_method(:extra_method) { 42 }
|
|
|
|
assert_equal 42, david.projects.extra_method
|
|
end
|
|
|
|
test "inverses get set of subsets of the association" do
|
|
human = Human.create
|
|
human.interests.create
|
|
|
|
human = Human.find(human.id)
|
|
|
|
assert_queries_count(1) do
|
|
assert_equal human, human.interests.where("1=1").first.human
|
|
end
|
|
end
|
|
|
|
test "first! works on loaded associations" do
|
|
david = authors(:david)
|
|
assert_equal david.first_posts.first, david.first_posts.reload.first!
|
|
assert_predicate david.first_posts, :loaded?
|
|
assert_no_queries { david.first_posts.first! }
|
|
end
|
|
|
|
def test_pluck_uses_loaded_target
|
|
david = authors(:david)
|
|
assert_equal david.first_posts.pluck(:title), david.first_posts.load.pluck(:title)
|
|
assert_predicate david.first_posts, :loaded?
|
|
assert_no_queries { david.first_posts.pluck(:title) }
|
|
end
|
|
|
|
def test_pick_uses_loaded_target
|
|
david = authors(:david)
|
|
assert_equal david.first_posts.pick(:title), david.first_posts.load.pick(:title)
|
|
assert_predicate david.first_posts, :loaded?
|
|
assert_no_queries { david.first_posts.pick(:title) }
|
|
end
|
|
|
|
def test_reset_unloads_target
|
|
david = authors(:david)
|
|
david.posts.reload
|
|
|
|
assert_predicate david.posts, :loaded?
|
|
assert_predicate david.posts, :loaded
|
|
david.posts.reset
|
|
assert_not_predicate david.posts, :loaded?
|
|
assert_not_predicate david.posts, :loaded
|
|
end
|
|
|
|
def test_target_merging_ignores_persisted_in_memory_records
|
|
david = authors(:david)
|
|
assert david.thinking_posts.include?(posts(:thinking))
|
|
|
|
david.thinking_posts.create!(title: "Something else entirely", body: "Does not matter.")
|
|
|
|
assert_equal 1, david.thinking_posts.size
|
|
assert_equal 1, david.thinking_posts.to_a.size
|
|
end
|
|
|
|
def test_target_merging_ignores_persisted_in_memory_records_when_loaded_records_are_empty
|
|
member = members(:blarpy_winkup)
|
|
assert_empty member.favorite_memberships
|
|
|
|
membership = member.favorite_memberships.create!
|
|
membership.update!(favorite: false)
|
|
|
|
assert_empty member.favorite_memberships.to_a
|
|
end
|
|
|
|
def test_target_merging_recognizes_updated_in_memory_records
|
|
member = members(:blarpy_winkup)
|
|
membership = member.create_membership!(favorite: false)
|
|
|
|
assert_empty member.favorite_memberships
|
|
|
|
membership.update!(favorite: true)
|
|
|
|
assert_not_empty member.favorite_memberships.to_a
|
|
end
|
|
|
|
def test_size_differentiates_between_new_and_persisted_in_memory_records_when_loaded_records_are_empty
|
|
member = members(:blarpy_winkup)
|
|
assert_empty member.favorite_memberships
|
|
|
|
membership = member.favorite_memberships.create!
|
|
membership.update!(favorite: false)
|
|
|
|
# CollectionAssociation#size has different behavior when loaded vs. non-loaded
|
|
# the first call will mark the association as loaded and the second call will
|
|
# take a different code path, so it's important to keep both assertions
|
|
assert_equal 0, member.favorite_memberships.size
|
|
assert_equal 0, member.favorite_memberships.size
|
|
end
|
|
end
|
|
|
|
class OverridingAssociationsTest < ActiveRecord::TestCase
|
|
class DifferentPerson < ActiveRecord::Base; end
|
|
|
|
class PeopleList < ActiveRecord::Base
|
|
has_and_belongs_to_many :has_and_belongs_to_many, before_add: :enlist
|
|
has_many :has_many, before_add: :enlist
|
|
belongs_to :belongs_to
|
|
has_one :has_one
|
|
end
|
|
|
|
class DifferentPeopleList < PeopleList
|
|
# Different association with the same name, callbacks should be omitted here.
|
|
has_and_belongs_to_many :has_and_belongs_to_many, class_name: "DifferentPerson"
|
|
has_many :has_many, class_name: "DifferentPerson"
|
|
belongs_to :belongs_to, class_name: "DifferentPerson"
|
|
has_one :has_one, class_name: "DifferentPerson"
|
|
end
|
|
|
|
def test_habtm_association_redefinition_callbacks_should_differ_and_not_inherited
|
|
# redeclared association on AR descendant should not inherit callbacks from superclass
|
|
callbacks = PeopleList.before_add_for_has_and_belongs_to_many
|
|
assert_equal(1, callbacks.length)
|
|
callbacks = DifferentPeopleList.before_add_for_has_and_belongs_to_many
|
|
assert_equal([], callbacks)
|
|
end
|
|
|
|
def test_has_many_association_redefinition_callbacks_should_differ_and_not_inherited
|
|
# redeclared association on AR descendant should not inherit callbacks from superclass
|
|
callbacks = PeopleList.before_add_for_has_many
|
|
assert_equal(1, callbacks.length)
|
|
callbacks = DifferentPeopleList.before_add_for_has_many
|
|
assert_equal([], callbacks)
|
|
end
|
|
|
|
def test_habtm_association_redefinition_reflections_should_differ_and_not_inherited
|
|
assert_not_equal(
|
|
PeopleList.reflect_on_association(:has_and_belongs_to_many),
|
|
DifferentPeopleList.reflect_on_association(:has_and_belongs_to_many)
|
|
)
|
|
end
|
|
|
|
def test_has_many_association_redefinition_reflections_should_differ_and_not_inherited
|
|
assert_not_equal(
|
|
PeopleList.reflect_on_association(:has_many),
|
|
DifferentPeopleList.reflect_on_association(:has_many)
|
|
)
|
|
end
|
|
|
|
def test_belongs_to_association_redefinition_reflections_should_differ_and_not_inherited
|
|
assert_not_equal(
|
|
PeopleList.reflect_on_association(:belongs_to),
|
|
DifferentPeopleList.reflect_on_association(:belongs_to)
|
|
)
|
|
end
|
|
|
|
def test_has_one_association_redefinition_reflections_should_differ_and_not_inherited
|
|
assert_not_equal(
|
|
PeopleList.reflect_on_association(:has_one),
|
|
DifferentPeopleList.reflect_on_association(:has_one)
|
|
)
|
|
end
|
|
|
|
def test_requires_symbol_argument
|
|
assert_raises ArgumentError do
|
|
Class.new(Post) do
|
|
belongs_to "author"
|
|
end
|
|
end
|
|
end
|
|
|
|
class ModelAssociatedToClassesThatDoNotExist < ActiveRecord::Base
|
|
self.table_name = "accounts" # this is just to avoid adding a new model just for this test
|
|
|
|
has_one :non_existent_has_one_class
|
|
belongs_to :non_existent_belongs_to_class
|
|
has_many :non_existent_has_many_classes
|
|
end
|
|
|
|
def test_associations_raise_with_name_error_if_associated_to_classes_that_do_not_exist
|
|
assert_raises NameError do
|
|
ModelAssociatedToClassesThatDoNotExist.new.non_existent_has_one_class
|
|
end
|
|
|
|
assert_raises NameError do
|
|
ModelAssociatedToClassesThatDoNotExist.new.non_existent_belongs_to_class
|
|
end
|
|
|
|
assert_raises NameError do
|
|
ModelAssociatedToClassesThatDoNotExist.new.non_existent_has_many_classes
|
|
end
|
|
end
|
|
end
|
|
|
|
class PreloaderTest < ActiveRecord::TestCase
|
|
fixtures :posts, :comments, :books, :authors, :tags, :taggings, :essays, :categories, :author_addresses,
|
|
:sharded_blog_posts, :sharded_comments, :sharded_blog_posts_tags, :sharded_tags,
|
|
:members, :member_details, :organizations, :cpk_orders, :cpk_order_agreements,
|
|
:dogs, :other_dogs
|
|
|
|
def test_preload_with_scope
|
|
post = posts(:welcome)
|
|
|
|
preloader = ActiveRecord::Associations::Preloader.new(records: [post], associations: :comments, scope: Comment.where(body: "Thank you for the welcome"))
|
|
preloader.call
|
|
|
|
assert_predicate post.comments, :loaded?
|
|
assert_equal [comments(:greetings)], post.comments
|
|
end
|
|
|
|
def test_preload_makes_correct_number_of_queries_on_array
|
|
post = posts(:welcome)
|
|
|
|
assert_queries_count(1) do
|
|
preloader = ActiveRecord::Associations::Preloader.new(records: [post], associations: :comments)
|
|
preloader.call
|
|
end
|
|
end
|
|
|
|
def test_preload_makes_correct_number_of_queries_on_relation
|
|
post = posts(:welcome)
|
|
relation = Post.where(id: post.id)
|
|
|
|
assert_queries_count(2) do
|
|
preloader = ActiveRecord::Associations::Preloader.new(records: relation, associations: :comments)
|
|
preloader.call
|
|
end
|
|
end
|
|
|
|
def test_preload_does_not_concatenate_duplicate_records
|
|
post = posts(:welcome)
|
|
post.reload
|
|
post.comments.create!(body: "A new comment")
|
|
|
|
ActiveRecord::Associations::Preloader.new(records: [post], associations: :comments).call
|
|
|
|
assert_equal post.comments.length, post.comments.count
|
|
assert_equal post.comments.all.to_a, post.comments
|
|
end
|
|
|
|
def test_preload_for_hmt_with_conditions
|
|
post = posts(:welcome)
|
|
_normal_category = post.categories.create!(name: "Normal")
|
|
special_category = post.special_categories.create!(name: "Special")
|
|
|
|
preloader = ActiveRecord::Associations::Preloader.new(records: [post], associations: :hmt_special_categories)
|
|
preloader.call
|
|
|
|
assert_equal 1, post.hmt_special_categories.length
|
|
assert_equal [special_category], post.hmt_special_categories
|
|
end
|
|
|
|
def test_preload_groups_queries_with_same_scope
|
|
book = books(:awdr)
|
|
post = posts(:welcome)
|
|
|
|
assert_queries_count(1) do
|
|
preloader = ActiveRecord::Associations::Preloader.new(records: [book, post], associations: :author)
|
|
preloader.call
|
|
end
|
|
|
|
assert_no_queries do
|
|
book.author
|
|
post.author
|
|
end
|
|
end
|
|
|
|
def test_preload_grouped_queries_with_already_loaded_records
|
|
book = books(:awdr)
|
|
post = posts(:welcome)
|
|
book.author
|
|
|
|
assert_no_queries do
|
|
ActiveRecord::Associations::Preloader.new(records: [book, post], associations: :author).call
|
|
book.author
|
|
post.author
|
|
end
|
|
end
|
|
|
|
def test_preload_grouped_queries_of_middle_records
|
|
comments = [
|
|
comments(:eager_sti_on_associations_s_comment1),
|
|
comments(:eager_sti_on_associations_s_comment2),
|
|
]
|
|
|
|
assert_queries_count(2) do
|
|
ActiveRecord::Associations::Preloader.new(records: comments, associations: [:author, :ordinary_post]).call
|
|
end
|
|
end
|
|
|
|
def test_preload_grouped_queries_of_through_records
|
|
author = authors(:david)
|
|
|
|
assert_queries_count(3) do
|
|
ActiveRecord::Associations::Preloader.new(records: [author], associations: [:hello_post_comments, :comments]).call
|
|
end
|
|
end
|
|
|
|
def test_preload_through_records_with_already_loaded_middle_record
|
|
member = members(:groucho)
|
|
expected_member_detail_ids = member.organization_member_details_2.pluck(:id)
|
|
|
|
member.reload.organization # load through record
|
|
|
|
assert_queries_count(1) do
|
|
ActiveRecord::Associations::Preloader.new(records: [member], associations: :organization_member_details_2).call
|
|
end
|
|
|
|
assert_no_queries do
|
|
assert_equal expected_member_detail_ids.sort, member.organization_member_details_2.map(&:id).sort
|
|
end
|
|
end
|
|
|
|
def test_preload_with_instance_dependent_scope
|
|
david = authors(:david)
|
|
david2 = Author.create!(name: "David")
|
|
bob = authors(:bob)
|
|
post = Post.create!(
|
|
author: david,
|
|
title: "test post",
|
|
body: "this post is about David"
|
|
)
|
|
post2 = Post.create!(
|
|
author: david,
|
|
title: "test post 2",
|
|
body: "this post is also about David"
|
|
)
|
|
|
|
assert_queries_count(2) do
|
|
preloader = ActiveRecord::Associations::Preloader.new(records: [david, david2, bob], associations: :posts_mentioning_author)
|
|
preloader.call
|
|
end
|
|
|
|
assert_predicate david.posts_mentioning_author, :loaded?
|
|
assert_predicate david2.posts_mentioning_author, :loaded?
|
|
assert_predicate bob.posts_mentioning_author, :loaded?
|
|
|
|
assert_equal [post, post2].sort, david.posts_mentioning_author.sort
|
|
assert_equal [], david2.posts_mentioning_author
|
|
assert_equal [], bob.posts_mentioning_author
|
|
end
|
|
|
|
def test_preload_with_instance_dependent_through_scope
|
|
david = authors(:david)
|
|
david2 = Author.create!(name: "David")
|
|
bob = authors(:bob)
|
|
comment1 = david.posts.first.comments.create!(body: "Hi David!")
|
|
comment2 = david.posts.first.comments.create!(body: "This comment mentions david")
|
|
|
|
assert_queries_count(2) do
|
|
preloader = ActiveRecord::Associations::Preloader.new(records: [david, david2, bob], associations: :comments_mentioning_author)
|
|
preloader.call
|
|
end
|
|
|
|
assert_predicate david.comments_mentioning_author, :loaded?
|
|
assert_predicate david2.comments_mentioning_author, :loaded?
|
|
assert_predicate bob.comments_mentioning_author, :loaded?
|
|
|
|
assert_equal [comment1, comment2].sort, david.comments_mentioning_author.sort
|
|
assert_equal [], david2.comments_mentioning_author
|
|
assert_equal [], bob.comments_mentioning_author
|
|
end
|
|
|
|
def test_preload_with_through_instance_dependent_scope
|
|
david = authors(:david)
|
|
david2 = Author.create!(name: "David")
|
|
bob = authors(:bob)
|
|
post = Post.create!(
|
|
author: david,
|
|
title: "test post",
|
|
body: "this post is about David"
|
|
)
|
|
Post.create!(
|
|
author: david,
|
|
title: "test post 2",
|
|
body: "this post is also about David"
|
|
)
|
|
post3 = Post.create!(
|
|
author: bob,
|
|
title: "test post 3",
|
|
body: "this post is about Bob"
|
|
)
|
|
comment1 = post.comments.create!(body: "hi!")
|
|
comment2 = post.comments.create!(body: "hello!")
|
|
comment3 = post3.comments.create!(body: "HI BOB!")
|
|
|
|
assert_queries_count(3) do
|
|
preloader = ActiveRecord::Associations::Preloader.new(records: [david, david2, bob], associations: :comments_on_posts_mentioning_author)
|
|
preloader.call
|
|
end
|
|
|
|
assert_predicate david.comments_on_posts_mentioning_author, :loaded?
|
|
assert_predicate david2.comments_on_posts_mentioning_author, :loaded?
|
|
assert_predicate bob.comments_on_posts_mentioning_author, :loaded?
|
|
|
|
assert_equal [comment1, comment2].sort, david.comments_on_posts_mentioning_author.sort
|
|
assert_equal [], david2.comments_on_posts_mentioning_author
|
|
assert_equal [comment3], bob.comments_on_posts_mentioning_author
|
|
end
|
|
|
|
def test_some_already_loaded_associations
|
|
item_discount = Discount.create(amount: 5)
|
|
shipping_discount = Discount.create(amount: 20)
|
|
|
|
invoice = Invoice.new
|
|
line_item = LineItem.new(amount: 20)
|
|
line_item.discount_applications << LineItemDiscountApplication.new(discount: item_discount)
|
|
invoice.line_items << line_item
|
|
|
|
shipping_line = ShippingLine.new(amount: 50)
|
|
shipping_line.discount_applications << ShippingLineDiscountApplication.new(discount: shipping_discount)
|
|
invoice.shipping_lines << shipping_line
|
|
|
|
invoice.save!
|
|
invoice.reload
|
|
|
|
# SELECT "line_items".* FROM "line_items" WHERE "line_items"."invoice_id" = ?
|
|
# SELECT "shipping_lines".* FROM shipping_lines WHERE "shipping_lines"."invoice_id" = ?
|
|
# SELECT "line_item_discount_applications".* FROM "line_item_discount_applications" WHERE "line_item_discount_applications"."line_item_id" = ?
|
|
# SELECT "shipping_line_discount_applications".* FROM "shipping_line_discount_applications" WHERE "shipping_line_discount_applications"."shipping_line_id" = ?
|
|
# SELECT "discounts".* FROM "discounts" WHERE "discounts"."id" IN (?, ?).
|
|
assert_queries_count(5) do
|
|
preloader = ActiveRecord::Associations::Preloader.new(records: [invoice], associations: [
|
|
line_items: { discount_applications: :discount },
|
|
shipping_lines: { discount_applications: :discount },
|
|
])
|
|
preloader.call
|
|
end
|
|
|
|
assert_no_queries do
|
|
assert_not_nil invoice.line_items.first.discount_applications.first.discount
|
|
assert_not_nil invoice.shipping_lines.first.discount_applications.first.discount
|
|
end
|
|
|
|
invoice.reload
|
|
invoice.line_items.map { |i| i.discount_applications.to_a }
|
|
# `line_items` and `line_item_discount_applications` are already preloaded, so we expect:
|
|
# SELECT "shipping_lines".* FROM shipping_lines WHERE "shipping_lines"."invoice_id" = ?
|
|
# SELECT "shipping_line_discount_applications".* FROM "shipping_line_discount_applications" WHERE "shipping_line_discount_applications"."shipping_line_id" = ?
|
|
# SELECT "discounts".* FROM "discounts" WHERE "discounts"."id" = ?.
|
|
assert_queries_count(3) do
|
|
preloader = ActiveRecord::Associations::Preloader.new(records: [invoice], associations: [
|
|
line_items: { discount_applications: :discount },
|
|
shipping_lines: { discount_applications: :discount },
|
|
])
|
|
preloader.call
|
|
end
|
|
|
|
assert_no_queries do
|
|
assert_not_nil invoice.line_items.first.discount_applications.first.discount
|
|
assert_not_nil invoice.shipping_lines.first.discount_applications.first.discount
|
|
end
|
|
end
|
|
|
|
def test_preload_through
|
|
comments = [
|
|
comments(:eager_sti_on_associations_s_comment1),
|
|
comments(:eager_sti_on_associations_s_comment2),
|
|
]
|
|
|
|
assert_queries_count(2) do
|
|
preloader = ActiveRecord::Associations::Preloader.new(records: comments, associations: [:author, :post])
|
|
preloader.call
|
|
end
|
|
|
|
assert_no_queries do
|
|
comments.each(&:author)
|
|
end
|
|
end
|
|
|
|
def test_preload_groups_queries_with_same_scope_at_second_level
|
|
author = nil
|
|
|
|
# Expected
|
|
# SELECT FROM authors ...
|
|
# SELECT FROM posts ... (thinking)
|
|
# SELECT FROM posts ... (welcome)
|
|
# SELECT FROM comments ... (comments for both welcome and thinking)
|
|
assert_queries_count(4) do
|
|
author = Author
|
|
.where(name: "David")
|
|
.includes(thinking_posts: :comments, welcome_posts: :comments)
|
|
.first
|
|
end
|
|
|
|
assert_no_queries do
|
|
author.thinking_posts.map(&:comments)
|
|
author.welcome_posts.map(&:comments)
|
|
end
|
|
end
|
|
|
|
def test_preload_groups_queries_with_same_sql_at_second_level
|
|
author = nil
|
|
|
|
# Expected
|
|
# SELECT FROM authors ...
|
|
# SELECT FROM posts ... (thinking)
|
|
# SELECT FROM posts ... (welcome)
|
|
# SELECT FROM comments ... (comments for both welcome and thinking)
|
|
assert_queries_count(4) do
|
|
author = Author
|
|
.where(name: "David")
|
|
.includes(thinking_posts: :comments, welcome_posts: :comments_with_extending)
|
|
.first
|
|
end
|
|
|
|
assert_no_queries do
|
|
author.thinking_posts.map(&:comments)
|
|
author.welcome_posts.map(&:comments_with_extending)
|
|
end
|
|
end
|
|
|
|
def test_preload_with_grouping_sets_inverse_association
|
|
mary = authors(:mary)
|
|
bob = authors(:bob)
|
|
|
|
AuthorFavorite.create!(author: mary, favorite_author: bob)
|
|
favorites = AuthorFavorite.all.load
|
|
|
|
assert_queries_count(1) do
|
|
preloader = ActiveRecord::Associations::Preloader.new(records: favorites, associations: [:author, :favorite_author])
|
|
preloader.call
|
|
end
|
|
|
|
assert_no_queries do
|
|
favorites.first.author
|
|
favorites.first.favorite_author
|
|
end
|
|
end
|
|
|
|
def test_preload_can_group_separate_levels
|
|
mary = authors(:mary)
|
|
bob = authors(:bob)
|
|
|
|
AuthorFavorite.create!(author: mary, favorite_author: bob)
|
|
|
|
assert_queries_count(3) do
|
|
preloader = ActiveRecord::Associations::Preloader.new(records: [mary], associations: [:posts, favorite_authors: :posts])
|
|
preloader.call
|
|
end
|
|
|
|
assert_no_queries do
|
|
mary.posts
|
|
mary.favorite_authors.map(&:posts)
|
|
end
|
|
end
|
|
|
|
def test_preload_can_group_multi_level_ping_pong_through
|
|
mary = authors(:mary)
|
|
bob = authors(:bob)
|
|
|
|
AuthorFavorite.create!(author: mary, favorite_author: bob)
|
|
|
|
associations = { similar_posts: :comments, favorite_authors: { similar_posts: :comments } }
|
|
|
|
assert_queries_count(9) do
|
|
preloader = ActiveRecord::Associations::Preloader.new(records: [mary], associations: associations)
|
|
preloader.call
|
|
end
|
|
|
|
assert_no_queries do
|
|
mary.similar_posts.map(&:comments).each(&:to_a)
|
|
mary.favorite_authors.flat_map(&:similar_posts).map(&:comments).each(&:to_a)
|
|
end
|
|
|
|
# Preloading with automatic scope inversing reduces the number of queries
|
|
tag_reflection = Tagging.reflect_on_association(:tag)
|
|
taggings_reflection = Tag.reflect_on_association(:taggings)
|
|
|
|
assert tag_reflection.scope
|
|
assert_not taggings_reflection.scope
|
|
|
|
with_automatic_scope_inversing(tag_reflection, taggings_reflection) do
|
|
mary.reload
|
|
|
|
assert_queries_count(8) do
|
|
preloader = ActiveRecord::Associations::Preloader.new(records: [mary], associations: associations)
|
|
preloader.call
|
|
end
|
|
end
|
|
end
|
|
|
|
def test_preload_does_not_group_same_class_different_scope
|
|
post = posts(:welcome)
|
|
postesque = Postesque.create(author: Author.last)
|
|
postesque.reload
|
|
|
|
# When the scopes differ in the generated SQL:
|
|
# SELECT "authors".* FROM "authors" WHERE (name LIKE '%a%') AND "authors"."id" = ?
|
|
# SELECT "authors".* FROM "authors" WHERE "authors"."id" = ?.
|
|
assert_queries_count(2) do
|
|
preloader = ActiveRecord::Associations::Preloader.new(records: [post, postesque], associations: :author_with_the_letter_a)
|
|
preloader.call
|
|
end
|
|
|
|
assert_no_queries do
|
|
post.author_with_the_letter_a
|
|
postesque.author_with_the_letter_a
|
|
end
|
|
|
|
post.reload
|
|
postesque.reload
|
|
|
|
# When the generated SQL is identical, but one scope has preload values.
|
|
assert_queries_count(3) do
|
|
preloader = ActiveRecord::Associations::Preloader.new(records: [post, postesque], associations: :author_with_address)
|
|
preloader.call
|
|
end
|
|
|
|
assert_no_queries do
|
|
post.author_with_address
|
|
postesque.author_with_address
|
|
end
|
|
end
|
|
|
|
def test_preload_does_not_group_same_scope_different_key_name
|
|
post = posts(:welcome)
|
|
postesque = Postesque.create(author: Author.last)
|
|
postesque.reload
|
|
|
|
assert_queries_count(2) do
|
|
preloader = ActiveRecord::Associations::Preloader.new(records: [post, postesque], associations: :author)
|
|
preloader.call
|
|
end
|
|
|
|
assert_no_queries do
|
|
post.author
|
|
postesque.author
|
|
end
|
|
end
|
|
|
|
def test_multi_database_polymorphic_preload_with_same_table_name
|
|
dog = dogs(:sophie)
|
|
dog_comment = comments(:greetings)
|
|
dog_comment.origin_type = dog.class.name
|
|
dog_comment.origin_id = dog.id
|
|
|
|
other_dog = other_dogs(:lassie)
|
|
other_dog_comment = comments(:more_greetings)
|
|
other_dog_comment.origin_type = other_dog.class.name
|
|
other_dog_comment.origin_id = other_dog.id
|
|
|
|
# Both Dog and OtherDog are backed by a table named `dogs`,
|
|
# however they are stored in different databases and should
|
|
# therefore result in two separate queries rather than be batched
|
|
# together.
|
|
#
|
|
# Expected
|
|
# SELECT FROM dogs ... (Dog)
|
|
# SELECT FROM dogs ... (OtherDog)
|
|
assert_queries_count(2) do
|
|
preloader = ActiveRecord::Associations::Preloader.new(records: [dog_comment, other_dog_comment], associations: :origin)
|
|
preloader.call
|
|
end
|
|
end
|
|
|
|
def test_preload_with_available_records
|
|
post = posts(:welcome)
|
|
david = authors(:david)
|
|
|
|
assert_no_queries do
|
|
ActiveRecord::Associations::Preloader.new(records: [post], associations: :author, available_records: [[david]]).call
|
|
|
|
assert_predicate post.association(:author), :loaded?
|
|
assert_same david, post.author
|
|
end
|
|
end
|
|
|
|
def test_preload_with_available_records_sti
|
|
book = Book.create!
|
|
essay_special = EssaySpecial.create!
|
|
book.essay = essay_special
|
|
book.save!
|
|
book.reload
|
|
|
|
assert_not_predicate book.association(:essay), :loaded?
|
|
|
|
assert_no_queries do
|
|
ActiveRecord::Associations::Preloader.new(records: [book], associations: :essay, available_records: [[essay_special]]).call
|
|
end
|
|
|
|
assert_predicate book.association(:essay), :loaded?
|
|
assert_same essay_special, book.essay
|
|
end
|
|
|
|
def test_preload_with_only_some_records_available
|
|
bob_post = posts(:misc_by_bob)
|
|
mary_post = posts(:misc_by_mary)
|
|
bob = authors(:bob)
|
|
mary = authors(:mary)
|
|
|
|
assert_queries_count(1) do
|
|
ActiveRecord::Associations::Preloader.new(records: [bob_post, mary_post], associations: :author, available_records: [bob]).call
|
|
end
|
|
|
|
assert_no_queries do
|
|
assert_same bob, bob_post.author
|
|
assert_equal mary, mary_post.author
|
|
end
|
|
end
|
|
|
|
def test_preload_with_some_records_already_loaded
|
|
bob_post = posts(:misc_by_bob)
|
|
mary_post = posts(:misc_by_mary)
|
|
bob = bob_post.author
|
|
mary = authors(:mary)
|
|
|
|
assert_predicate bob_post.association(:author), :loaded?
|
|
assert_not mary_post.association(:author).loaded?
|
|
|
|
assert_queries_count(1) do
|
|
ActiveRecord::Associations::Preloader.new(records: [bob_post, mary_post], associations: :author).call
|
|
end
|
|
|
|
assert_no_queries do
|
|
assert_same bob, bob_post.author
|
|
assert_equal mary, mary_post.author
|
|
end
|
|
end
|
|
|
|
def test_preload_with_available_records_with_through_association
|
|
author = authors(:david)
|
|
categories = Category.all.to_a
|
|
|
|
assert_queries_count(1) do
|
|
# One query to get the middle records (i.e. essays)
|
|
ActiveRecord::Associations::Preloader.new(records: [author], associations: :essay_category, available_records: categories).call
|
|
end
|
|
|
|
assert_predicate author.association(:essay_category), :loaded?
|
|
assert categories.map(&:__id__).include?(author.essay_category.__id__)
|
|
end
|
|
|
|
def test_preload_with_only_some_records_available_with_through_associations
|
|
mary = authors(:mary)
|
|
mary_essay = essays(:mary_stay_home)
|
|
mary_category = categories(:technology)
|
|
mary_essay.update!(category: mary_category)
|
|
|
|
dave = authors(:david)
|
|
dave_category = categories(:general)
|
|
|
|
assert_queries_count(2) do
|
|
ActiveRecord::Associations::Preloader.new(records: [mary, dave], associations: :essay_category, available_records: [mary_category]).call
|
|
end
|
|
|
|
assert_no_queries do
|
|
assert_same mary_category, mary.essay_category
|
|
assert_equal dave_category, dave.essay_category
|
|
end
|
|
end
|
|
|
|
def test_preload_with_available_records_with_multiple_classes
|
|
essay = essays(:david_modest_proposal)
|
|
general = categories(:general)
|
|
david = authors(:david)
|
|
|
|
assert_no_queries do
|
|
ActiveRecord::Associations::Preloader.new(records: [essay], associations: [:category, :author], available_records: [general, david]).call
|
|
|
|
assert_predicate essay.association(:category), :loaded?
|
|
assert_predicate essay.association(:author), :loaded?
|
|
assert_same general, essay.category
|
|
assert_same david, essay.author
|
|
end
|
|
end
|
|
|
|
def test_preload_with_available_records_queries_when_scoped
|
|
post = posts(:welcome)
|
|
david = authors(:david)
|
|
|
|
assert_queries_count(1) do
|
|
ActiveRecord::Associations::Preloader.new(records: [post], associations: :author, scope: Author.where(name: "David"), available_records: [david]).call
|
|
end
|
|
|
|
assert_predicate post.association(:author), :loaded?
|
|
assert_not_equal david.__id__, post.author.__id__
|
|
end
|
|
|
|
def test_preload_with_available_records_queries_when_collection
|
|
post = posts(:welcome)
|
|
comments = Comment.all.to_a
|
|
|
|
assert_queries_count(1) do
|
|
ActiveRecord::Associations::Preloader.new(records: [post], associations: :comments, available_records: comments).call
|
|
end
|
|
|
|
assert_predicate post.association(:comments), :loaded?
|
|
assert_empty post.comments.map(&:__id__) & comments.map(&:__id__)
|
|
end
|
|
|
|
def test_preload_with_available_records_queries_when_incomplete
|
|
post = posts(:welcome)
|
|
bob = authors(:bob)
|
|
david = authors(:david)
|
|
|
|
assert_queries_count(1) do
|
|
ActiveRecord::Associations::Preloader.new(records: [post], associations: :author, available_records: [bob]).call
|
|
end
|
|
|
|
assert_no_queries do
|
|
assert_predicate post.association(:author), :loaded?
|
|
assert_equal david, post.author
|
|
end
|
|
end
|
|
|
|
def test_preload_with_unpersisted_records_no_ops
|
|
author = Author.new
|
|
new_post_with_author = Post.new(author: author)
|
|
new_post_without_author = Post.new
|
|
posts = [new_post_with_author, new_post_without_author]
|
|
|
|
assert_no_queries do
|
|
ActiveRecord::Associations::Preloader.new(records: posts, associations: :author).call
|
|
|
|
assert_same author, new_post_with_author.author
|
|
assert_nil new_post_without_author.author
|
|
end
|
|
end
|
|
|
|
def test_preload_wont_set_the_wrong_target
|
|
post = posts(:welcome)
|
|
post.update!(author_id: 54321)
|
|
some_other_record = categories(:general)
|
|
some_other_record.update!(id: 54321)
|
|
|
|
assert_raises do
|
|
some_other_record.association(:author)
|
|
end
|
|
|
|
assert_nothing_raised do
|
|
ActiveRecord::Associations::Preloader.new(records: [post], associations: :author, available_records: [[some_other_record]]).call
|
|
assert_predicate post.association(:author), :loaded?
|
|
assert_not_equal some_other_record, post.author
|
|
end
|
|
end
|
|
|
|
def test_preload_has_many_association_with_composite_foreign_key
|
|
blog_post = sharded_blog_posts(:great_post_blog_one)
|
|
blog_posts = [blog_post, sharded_blog_posts(:great_post_blog_two)]
|
|
|
|
::ActiveRecord::Associations::Preloader.new(records: blog_posts, associations: [:comments]).call
|
|
|
|
assert_predicate blog_post.association(:comments), :loaded?
|
|
assert_includes(blog_post.comments.to_a, sharded_comments(:great_comment_blog_post_one))
|
|
end
|
|
|
|
def test_preload_belongs_to_association_with_composite_foreign_key
|
|
comment = sharded_comments(:great_comment_blog_post_one)
|
|
comments = [comment, sharded_comments(:great_comment_blog_post_two)]
|
|
|
|
ActiveRecord::Associations::Preloader.new(records: comments, associations: :blog_post).call
|
|
|
|
assert_predicate comment.association(:blog_post), :loaded?
|
|
assert_equal sharded_blog_posts(:great_post_blog_one), comment.blog_post
|
|
end
|
|
|
|
def test_preload_loaded_belongs_to_association_with_composite_foreign_key
|
|
comment = sharded_comments(:great_comment_blog_post_one)
|
|
comment.blog_post
|
|
|
|
assert_no_queries do
|
|
ActiveRecord::Associations::Preloader.new(records: [comment], associations: :blog_post).call
|
|
end
|
|
end
|
|
|
|
def test_preload_has_many_through_association_with_composite_query_constraints
|
|
tag = sharded_tags(:short_read_blog_one)
|
|
|
|
tags = [tag, sharded_tags(:breaking_news_blog_2)]
|
|
|
|
ActiveRecord::Associations::Preloader.new(records: tags, associations: :blog_posts).call
|
|
|
|
assert tags.all? { |tag| tag.association(:blog_posts).loaded? }
|
|
|
|
expected_blog_post_ids = Sharded::BlogPostTag
|
|
.where(blog_id: tag.blog_id, tag_id: tag.id)
|
|
.pluck(:blog_post_id)
|
|
|
|
assert_not_empty(expected_blog_post_ids)
|
|
|
|
assert_equal(expected_blog_post_ids.sort, tag.blog_posts.map(&:id).sort)
|
|
end
|
|
|
|
def test_preloads_has_many_on_model_with_a_composite_primary_key_through_id_attribute
|
|
order = cpk_orders(:cpk_groceries_order_2)
|
|
_shop_id, order_id = order.id
|
|
order_agreements = Cpk::OrderAgreement.where(order_id: order_id).to_a
|
|
|
|
assert_not_empty order_agreements
|
|
assert_equal order_agreements.sort, order.order_agreements.sort
|
|
|
|
loaded_order = nil
|
|
sql = capture_sql do
|
|
loaded_order = Cpk::Order.where(id: order_id).includes(:order_agreements).to_a.first
|
|
end
|
|
|
|
assert_equal 2, sql.size
|
|
preload_sql = sql.last
|
|
|
|
c = Cpk::OrderAgreement.lease_connection
|
|
order_id_column = Regexp.escape(c.quote_table_name("cpk_order_agreements.order_id"))
|
|
order_id_constraint = /#{order_id_column} = (\?|(\d+)|\$\d)$/
|
|
expectation = /SELECT.*WHERE.* #{order_id_constraint}/
|
|
|
|
assert_match(expectation, preload_sql)
|
|
assert_equal order_agreements.sort, loaded_order.order_agreements.sort
|
|
end
|
|
|
|
def test_preloads_belongs_to_a_composite_primary_key_model_through_id_attribute
|
|
order_agreement = cpk_order_agreements(:order_agreement_three)
|
|
order = cpk_orders(:cpk_groceries_order_2)
|
|
assert_equal order, order_agreement.order
|
|
|
|
loaded_order_agreement = nil
|
|
sql = capture_sql do
|
|
loaded_order_agreement = Cpk::OrderAgreement.where(id: order_agreement.id).includes(:order).to_a.first
|
|
end
|
|
|
|
assert_equal 2, sql.size
|
|
preload_sql = sql.last
|
|
|
|
c = Cpk::Order.lease_connection
|
|
order_id = Regexp.escape(c.quote_table_name("cpk_orders.id"))
|
|
order_constraint = /#{order_id} = (\?|(\d+)|\$\d)$/
|
|
expectation = /SELECT.*WHERE.* #{order_constraint}/
|
|
|
|
assert_match(expectation, preload_sql)
|
|
assert_equal order, loaded_order_agreement.order
|
|
end
|
|
|
|
def test_preload_keeps_built_has_many_records_no_ops
|
|
post = Post.new
|
|
comment = post.comments.build
|
|
|
|
assert_no_queries do
|
|
ActiveRecord::Associations::Preloader.new(records: [post], associations: :comments).call
|
|
|
|
assert_equal [comment], post.comments.to_a
|
|
end
|
|
end
|
|
|
|
def test_preload_keeps_built_has_many_records_after_query
|
|
post = posts(:welcome)
|
|
comment = post.comments.build
|
|
|
|
assert_queries_count(1) do
|
|
ActiveRecord::Associations::Preloader.new(records: [post], associations: :comments).call
|
|
|
|
assert_includes post.comments.to_a, comment
|
|
end
|
|
end
|
|
|
|
|
|
def test_preload_keeps_built_belongs_to_records_no_ops
|
|
post = Post.new
|
|
author = post.build_author
|
|
|
|
assert_no_queries do
|
|
ActiveRecord::Associations::Preloader.new(records: [post], associations: :author).call
|
|
|
|
assert_same author, post.author
|
|
end
|
|
end
|
|
|
|
def test_preload_keeps_built_belongs_to_records_after_query
|
|
post = posts(:welcome)
|
|
author = post.build_author
|
|
|
|
assert_no_queries do
|
|
ActiveRecord::Associations::Preloader.new(records: [post], associations: :author).call
|
|
|
|
assert_same author, post.author
|
|
end
|
|
end
|
|
end
|
|
|
|
class GeneratedMethodsTest < ActiveRecord::TestCase
|
|
fixtures :developers, :computers, :posts, :comments
|
|
|
|
def test_association_methods_override_attribute_methods_of_same_name
|
|
assert_equal(developers(:david), computers(:workstation).developer)
|
|
# this next line will fail if the attribute methods module is generated lazily
|
|
# after the association methods module is generated
|
|
assert_equal(developers(:david), computers(:workstation).developer)
|
|
assert_equal(developers(:david).id, computers(:workstation)[:developer])
|
|
end
|
|
|
|
def test_model_method_overrides_association_method
|
|
assert_equal(comments(:greetings).body, posts(:welcome).first_comment)
|
|
end
|
|
|
|
module MyModule
|
|
def comments; :none end
|
|
end
|
|
|
|
class MyArticle < ActiveRecord::Base
|
|
self.table_name = "articles"
|
|
include MyModule
|
|
has_many :comments, inverse_of: false
|
|
end
|
|
|
|
def test_included_module_overwrites_association_methods
|
|
assert_equal :none, MyArticle.new.comments
|
|
end
|
|
end
|
|
|
|
class WithAnnotationsTest < ActiveRecord::TestCase
|
|
fixtures :pirates, :parrots, :parrots_pirates, :pirates, :treasures
|
|
|
|
def test_belongs_to_with_annotation_includes_a_query_comment
|
|
pirate = SpacePirate.where.not(parrot_id: nil).first
|
|
assert pirate, "should have a Pirate record"
|
|
|
|
log = capture_sql do
|
|
pirate.parrot
|
|
end
|
|
assert_not_predicate log, :empty?
|
|
assert_predicate log.select { |query| query.match?(%r{/\*}) }, :empty?
|
|
|
|
assert_queries_match(%r{/\* that tells jokes \*/}) do
|
|
pirate.parrot_with_annotation
|
|
end
|
|
end
|
|
|
|
def test_has_and_belongs_to_many_with_annotation_includes_a_query_comment
|
|
pirate = SpacePirate.first
|
|
assert pirate, "should have a Pirate record"
|
|
|
|
log = capture_sql do
|
|
pirate.parrots.first
|
|
end
|
|
assert_not_predicate log, :empty?
|
|
assert_predicate log.select { |query| query.match?(%r{/\*}) }, :empty?
|
|
|
|
assert_queries_match(%r{/\* that are very colorful \*/}) do
|
|
pirate.parrots_with_annotation.first
|
|
end
|
|
end
|
|
|
|
def test_has_one_with_annotation_includes_a_query_comment
|
|
pirate = SpacePirate.first
|
|
assert pirate, "should have a Pirate record"
|
|
|
|
log = capture_sql do
|
|
pirate.ship
|
|
end
|
|
assert_not_predicate log, :empty?
|
|
assert_predicate log.select { |query| query.match?(%r{/\*}) }, :empty?
|
|
|
|
assert_queries_match(%r{/\* that is a rocket \*/}) do
|
|
pirate.ship_with_annotation
|
|
end
|
|
end
|
|
|
|
def test_has_many_with_annotation_includes_a_query_comment
|
|
pirate = SpacePirate.first
|
|
assert pirate, "should have a Pirate record"
|
|
|
|
log = capture_sql do
|
|
pirate.birds.first
|
|
end
|
|
assert_not_predicate log, :empty?
|
|
assert_predicate log.select { |query| query.match?(%r{/\*}) }, :empty?
|
|
|
|
assert_queries_match(%r{/\* that are also parrots \*/}) do
|
|
pirate.birds_with_annotation.first
|
|
end
|
|
end
|
|
|
|
def test_has_many_through_with_annotation_includes_a_query_comment
|
|
pirate = SpacePirate.first
|
|
assert pirate, "should have a Pirate record"
|
|
|
|
log = capture_sql do
|
|
pirate.treasure_estimates.first
|
|
end
|
|
assert_not_predicate log, :empty?
|
|
assert_predicate log.select { |query| query.match?(%r{/\*}) }, :empty?
|
|
|
|
assert_queries_match(%r{/\* yarrr \*/}) do
|
|
pirate.treasure_estimates_with_annotation.first
|
|
end
|
|
end
|
|
|
|
def test_has_many_through_with_annotation_includes_a_query_comment_when_eager_loading
|
|
pirate = SpacePirate.first
|
|
assert pirate, "should have a Pirate record"
|
|
|
|
log = capture_sql do
|
|
pirate.treasure_estimates.first
|
|
end
|
|
assert_not_predicate log, :empty?
|
|
assert_predicate log.select { |query| query.match?(%r{/\*}) }, :empty?
|
|
|
|
assert_queries_match(%r{/\* yarrr \*/}) do
|
|
SpacePirate.includes(:treasure_estimates_with_annotation, :treasures).first
|
|
end
|
|
end
|
|
end
|