Skip to content

Commit

Permalink
Merge branch '9b4a0b9-with-reverts-and-fixes' into 92b3160-with-rever…
Browse files Browse the repository at this point in the history
…ts-and-fixes
  • Loading branch information
adrianna-chang-shopify committed Jan 20, 2025
2 parents 264d8f4 + 04a950d commit 83311f4
Show file tree
Hide file tree
Showing 17 changed files with 101 additions and 202 deletions.
14 changes: 0 additions & 14 deletions activerecord/lib/active_record/associations/association.rb
Original file line number Diff line number Diff line change
Expand Up @@ -188,16 +188,6 @@ def extensions
# not reraised. The proxy is \reset and +nil+ is the return value.
def load_target
@target = find_target(async: false) if (@stale_state && stale_target?) || find_target?
if !@target && set_through_target_for_new_record?
reflections = reflection.chain
reflections.pop
reflections.reverse!

@target = reflections.reduce(through_association.target) do |middle_target, through_reflection|
break unless middle_target
middle_target.association(through_reflection.source_reflection_name).load_target
end
end

loaded! unless loaded?
target
Expand Down Expand Up @@ -331,10 +321,6 @@ def find_target?
!loaded? && (!owner.new_record? || foreign_key_present?) && klass
end

def set_through_target_for_new_record?
owner.new_record? && reflection.through_reflection? && through_association.target
end

# Returns true if there is a foreign key present on the owner which
# references the target. This is used to determine whether we can load
# the target if the owner is currently a new record (and therefore
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -272,18 +272,6 @@ def include?(record)
def load_target
if find_target?
@target = merge_target_lists(find_target, target)
elsif target.empty? && set_through_target_for_new_record?
reflections = reflection.chain.reverse!

@target = reflections.each_cons(2).reduce(through_association.target) do |middle_target, (middle_reflection, through_reflection)|
if middle_target.nil? || (middle_reflection.collection? && middle_target.empty?)
break []
elsif middle_reflection.collection?
middle_target.flat_map { |record| record.association(through_reflection.source_reflection_name).load_target }
else
middle_target.association(through_reflection.source_reflection_name).load_target
end
end
end

loaded!
Expand Down
36 changes: 19 additions & 17 deletions activerecord/lib/active_record/autosave_association.rb
Original file line number Diff line number Diff line change
Expand Up @@ -370,27 +370,29 @@ def validate_collection_association(reflection)
# enabled records if they're marked_for_destruction? or destroyed.
def association_valid?(association, record)
return true if record.destroyed? || (association.options[:autosave] && record.marked_for_destruction?)
if custom_validation_context?
context = validation_context

context = validation_context if custom_validation_context?
return true if record.valid?(context)

if record.changed? || record.new_record? || context
associated_errors = record.errors.objects
else
# If the associated record is unchanged we shouldn't auto validate it.
# Even if a record is invalid you should still be able to create new references
# to it.
return true if !record.new_record? && !record.changed?
# If there are existing invalid records in the DB, we should still be able to reference them.
# Unless a record (no matter where in the association chain) is invalid and is being changed.
associated_errors = record.errors.objects.select { |error| error.is_a?(Associations::NestedError) }
end

unless valid = record.valid?(context)
if association.options[:autosave]
record.errors.each { |error|
self.errors.objects.append(
Associations::NestedError.new(association, error)
)
}
else
errors.add(association.reflection.name)
end
if association.options[:autosave]
associated_errors.each { |error|
errors.objects.append(
Associations::NestedError.new(association, error)
)
}
elsif associated_errors.any?
errors.add(association.reflection.name)
end
valid

errors.any?
end

# Is used as an around_save callback to check while saving a collection
Expand Down
4 changes: 0 additions & 4 deletions activerecord/lib/active_record/reflection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -655,10 +655,6 @@ def source_reflection
self
end

def source_reflection_name
source_reflection.name
end

# A chain of reflections from this one back to the owner. For more see the explanation in
# ThroughReflection.
def collect_join_chain
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@
require "models/zine"
require "models/interest"
require "models/human"
require "models/account"

class HasManyThroughAssociationsTest < ActiveRecord::TestCase
fixtures :posts, :readers, :people, :comments, :authors, :categories, :taggings, :tags,
Expand All @@ -57,84 +56,6 @@ def setup
Reader.create person_id: 0, post_id: 0
end

def test_setting_has_many_through_one_association_on_new_record_sets_through_records
account_1, account_2 = Account.create!(credit_limit: 100), Account.create!(credit_limit: 100)
firm = Firm.new(accounts: [account_1, account_2])
client = Client.new
client.firm = firm

assert_predicate account_1, :persisted?
assert_predicate account_2, :persisted?
assert_predicate client, :new_record?
assert_predicate client.firm, :new_record?
assert_no_queries { assert_equal [account_1, account_2].sort, client.accounts.sort }
end

def test_setting_has_many_through_many_association_on_new_record_sets_through_records
subscriber_1, subscriber_2 = Subscriber.create!(nick: "nick 1"), Subscriber.create!(nick: "nick 2")
subscription_1 = Subscription.new(subscriber: subscriber_1)
subscription_2 = Subscription.new(subscriber: subscriber_2)
book = Book.new
book.subscriptions = [subscription_1, subscription_2]

assert_predicate subscriber_1, :persisted?
assert_predicate subscriber_2, :persisted?
assert_predicate book, :new_record?
book.subscriptions.each { |subscription| assert_predicate subscription, :new_record? }
assert_no_queries { assert_equal [subscriber_1, subscriber_2].sort, book.subscribers.sort }
end

def test_setting_nested_has_many_through_one_association_on_new_record_sets_nested_through_records
post_tagging_1, post_tagging_2 = Tagging.create!, Tagging.create!
post = Post.create!(title: "Tagged", body: "Post", taggings: [post_tagging_1, post_tagging_2])
author = Author.new(name: "Josh")
author.posts = [post]
categorization = Categorization.new
categorization.author = author

assert_predicate post_tagging_1, :persisted?
assert_predicate post_tagging_2, :persisted?
assert_predicate post, :persisted?
assert_predicate categorization, :new_record?
assert_predicate categorization.author, :new_record?
assert_no_queries { assert_equal [post_tagging_1, post_tagging_2].sort, categorization.post_taggings.sort }
end

def test_setting_nested_has_many_through_one_association_on_new_record_sets_targetless_nested_through_records
post = Post.create!(title: "Tagged", body: "Post")
post_tagging_1, post_tagging_2 = Tagging.create!(taggable: post), Tagging.create!(taggable: post)
author = Author.new(name: "Josh")
author.posts = [post]
categorization = Categorization.new
categorization.author = author

assert_predicate post_tagging_1, :persisted?
assert_predicate post_tagging_2, :persisted?
assert_predicate post, :persisted?
assert_predicate categorization, :new_record?
assert_predicate categorization.author, :new_record?
assert_queries_count(1) { assert_equal [post_tagging_1, post_tagging_2].sort, categorization.post_taggings.sort }
end

def test_setting_nested_has_many_through_many_association_on_new_record_sets_nested_through_records
account_1 = Account.create!(firm_name: "account 1", credit_limit: 100)
subscriber_1 = Subscriber.create!(nick: "nick 1", account: account_1)
account_2 = Account.create!(firm_name: "account 2", credit_limit: 100)
subscriber_2 = Subscriber.create!(nick: "nick 2", account: account_2)
subscription_1 = Subscription.new(subscriber: subscriber_1)
subscription_2 = Subscription.new(subscriber: subscriber_2)
book = Book.new
book.subscriptions = [subscription_1, subscription_2]

assert_predicate subscriber_1, :persisted?
assert_predicate subscriber_2, :persisted?
assert_predicate account_1, :persisted?
assert_predicate account_2, :persisted?
assert_predicate book, :new_record?
book.subscriptions.each { |subscription| assert_predicate subscription, :new_record? }
assert_no_queries { assert_equal [account_1, account_2].sort, book.subscriber_accounts.sort }
end

def test_has_many_through_create_record
assert books(:awdr).subscribers.create!(nick: "bob")
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
require "models/shop_account"
require "models/customer_carrier"
require "models/cpk"
require "models/rating"

class HasOneThroughAssociationsTest < ActiveRecord::TestCase
fixtures :member_types, :members, :clubs, :memberships, :sponsors, :organizations, :minivans,
Expand All @@ -44,63 +43,6 @@ def test_has_one_through_executes_limited_query
end
end

def test_setting_association_on_new_record_sets_through_record
club = Club.create!
membership = CurrentMembership.new(club: club)
member = Member.new
member.current_membership = membership

assert_predicate club, :persisted?
assert_predicate member, :new_record?
assert_predicate member.current_membership, :new_record?
assert_no_queries { assert_equal club, member.club }
end

def test_setting_association_on_new_record_sets_nested_through_record
category = Category.create!(name: "General")
club = Club.create!(category: category)
membership = CurrentMembership.new(club: club)
member = Member.new
member.current_membership = membership

assert_predicate category, :persisted?
assert_predicate club, :persisted?
assert_predicate member, :new_record?
assert_predicate member.current_membership, :new_record?
assert_no_queries { assert_equal club.category, member.club_category }
end

def test_setting_association_on_new_record_sets_not_loaded_nested_through_record
category = Category.create!(name: "General")
club = Club.create!(category: category)
club.reload
membership = CurrentMembership.new(club: club)
member = Member.new
member.current_membership = membership

assert_predicate category, :persisted?
assert_predicate club, :persisted?
assert_predicate member, :new_record?
assert_predicate member.current_membership, :new_record?
assert_queries_count(1) { assert_equal club.category, member.club_category }
end

def test_setting_association_on_new_record_sets_nested_through_record_with_belongs_to
essay = Essay.create!
author = Author.create!(name: "Josh", owned_essay: essay)
post = Post.create!(title: "Foo", body: "Bar", author: author)
comment = Comment.new(post: post)
rating = Rating.new
rating.comment = comment

assert_predicate essay, :persisted?
assert_predicate author, :persisted?
assert_predicate post, :persisted?
assert_predicate rating, :new_record?
assert_predicate rating.comment, :new_record?
assert_no_queries { assert_equal essay, rating.owned_essay }
end

def test_creating_association_creates_through_record
new_member = Member.create(name: "Chris")
new_member.club = Club.create(name: "LRUG")
Expand Down
70 changes: 69 additions & 1 deletion activerecord/test/cases/autosave_association_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
require "models/parrot"
require "models/pirate"
require "models/project"
require "models/price_estimate"
require "models/ship"
require "models/ship_part"
require "models/squeak"
Expand Down Expand Up @@ -1490,7 +1491,7 @@ def test_should_skip_validation_on_habtm_if_destroyed
assert_predicate @pirate, :valid?
end

def test_should_skip_validation_on_habtm_if_persisted_and_unchanged
def test_should_be_valid_on_habtm_if_persisted_and_unchanged
parrot = @pirate.parrots.create!(name: "parrots_1")
parrot.update_column(:name, "")
parrot.reload
Expand All @@ -1501,6 +1502,73 @@ def test_should_skip_validation_on_habtm_if_persisted_and_unchanged
new_pirate.save!
end

def test_should_be_invalid_on_habtm_when_any_record_in_the_association_chain_is_invalid_and_was_changed
treasure = @pirate.treasures.create!(name: "gold")
estimate = treasure.price_estimates.create!(price: 1)
estimate.update_columns(price: "not a number")

assert_not_predicate estimate, :valid?

treasures = @pirate.treasures.eager_load(:price_estimates).to_a
treasures.first.price_estimates.first.price = "not a price"
new_pirate = Pirate.new(
catchphrase: "Arr",
treasures: treasures,
)

assert_raises(ActiveRecord::RecordInvalid) do
new_pirate.save!
end
assert_equal(["Treasures is invalid"], new_pirate.errors.full_messages)
end

def test_should_be_invalid_on_habtm_when_any_record_in_the_association_chain_is_invalid_and_was_changed_with_autosave
super_pirate = Class.new(Pirate) do
self.table_name = "pirates"
has_many :great_treasures, class_name: "Treasure", foreign_key: "looter_id", autosave: true

def self.name
"SuperPirate"
end
end
@pirate = super_pirate.create(catchphrase: "Don' botharrr talkin' like one, savvy?")
treasure = @pirate.great_treasures.create!(name: "gold")
estimate = treasure.price_estimates.create!(price: 1)
estimate.update_columns(price: "not a number")

assert_not_predicate estimate, :valid?

treasures = @pirate.great_treasures.eager_load(:price_estimates).to_a
treasures.first.price_estimates.first.price = "not a price"
new_pirate = super_pirate.new(
catchphrase: "Arr",
great_treasures: treasures,
)

assert_raises(ActiveRecord::RecordInvalid) do
new_pirate.save!
end
assert_equal(["Great treasures price estimates price is not a number"], new_pirate.errors.full_messages)
end

def test_should_be_valid_on_habtm_when_any_record_in_the_association_chain_is_invalid_but_was_not_changed
treasure = @pirate.treasures.create!(name: "gold")
estimate = treasure.price_estimates.create!(price: 1)
estimate.update_columns(price: "not a number")

assert_not_predicate estimate, :valid?

treasures = @pirate.treasures.eager_load(:price_estimates).to_a
new_pirate = Pirate.new(
catchphrase: "Arr",
treasures: treasures,
)

assert_nothing_raised do
new_pirate.save!
end
end

def test_a_child_marked_for_destruction_should_not_be_destroyed_twice_while_saving_habtm
@pirate.parrots.create!(name: "parrots_1")

Expand Down
17 changes: 12 additions & 5 deletions activerecord/test/cases/nested_attributes_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1206,12 +1206,19 @@ def self.name; "Guitar"; end
end

class TestNestedAttributesWithExtend < ActiveRecord::TestCase
setup do
Pirate.accepts_nested_attributes_for :treasures
end

def test_extend_affects_nested_attributes
pirate = Pirate.create!(catchphrase: "Don' botharrr talkin' like one, savvy?")
super_pirate = Class.new(ActiveRecord::Base) do
self.table_name = "pirates"

has_many :treasures, as: :looter, extend: Pirate::PostTreasuresExtension
self.accepts_nested_attributes_for :treasures

def self.name
"SuperPirate"
end
end

pirate = super_pirate.create!(catchphrase: "Don' botharrr talkin' like one, savvy?")
pirate.treasures_attributes = [{ id: nil }]
assert_equal "from extension", pirate.treasures[0].name
end
Expand Down
1 change: 0 additions & 1 deletion activerecord/test/models/book.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ class Book < ActiveRecord::Base

has_many :subscriptions
has_many :subscribers, through: :subscriptions
has_many :subscriber_accounts, through: :subscribers, source: :account

has_one :essay

Expand Down
Loading

0 comments on commit 83311f4

Please sign in to comment.