diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb index 67591dd0dc..7803845612 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -109,7 +109,8 @@ module ActiveRecord end def invertible_for?(record) - inverse_reflection_for(record) + inverse = inverse_reflection_for(record) + inverse && (inverse.has_one? || ActiveRecord::Base.has_many_inversing) end def stale_state diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index 0cf1704bdf..3ad44d6571 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -286,6 +286,8 @@ module ActiveRecord end def target=(record) + return super unless ActiveRecord::Base.has_many_inversing + case record when Array super diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index f40c912cc1..435416055f 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -128,6 +128,8 @@ module ActiveRecord mattr_accessor :reading_role, instance_accessor: false, default: :reading + mattr_accessor :has_many_inversing, instance_accessor: false, default: false + class_attribute :default_connection_handler, instance_writer: false self.filter_attributes = [] diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index c0998e8bc6..df5b34480f 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -28,6 +28,7 @@ module ActiveRecord config.active_record.use_schema_cache_dump = true config.active_record.maintain_test_schema = true + config.active_record.has_many_inversing = false config.active_record.sqlite3 = ActiveSupport::OrderedOptions.new config.active_record.sqlite3.represent_boolean_as_integer = nil diff --git a/activerecord/test/cases/associations/inverse_associations_test.rb b/activerecord/test/cases/associations/inverse_associations_test.rb index cf3e7c164c..9f5270e580 100644 --- a/activerecord/test/cases/associations/inverse_associations_test.rb +++ b/activerecord/test/cases/associations/inverse_associations_test.rb @@ -607,7 +607,7 @@ class InverseBelongsToTests < ActiveRecord::TestCase 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_try_to_set_inverse_instances_when_the_inverse_is_a_has_many + 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 @@ -615,14 +615,31 @@ class InverseBelongsToTests < ActiveRecord::TestCase 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_equal i.topic, iz.topic, "Interest topics should be the same after changes to child" + assert_not_equal i.topic, iz.topic, "Interest topics should not be the same after changes to child" iz.topic = "Cow tipping" - assert_equal i.topic, iz.topic, "Interest topics should be the same after changes to parent-owned instance" + assert_not_equal i.topic, iz.topic, "Interest topics should not be the same after changes to parent-owned instance" end - def test_does_not_trigger_association_callbacks_on_set_when_the_inverse_is_a_has_many - man = interests(:trainspotting).man_with_callbacks - assert_not_predicate man, :add_callback_called? + def test_with_has_many_inversing_should_try_to_set_inverse_instances_when_the_inverse_is_a_has_many + with_has_many_inversing do + 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_equal i.topic, iz.topic, "Interest topics should be the same after changes to child" + iz.topic = "Cow tipping" + assert_equal i.topic, iz.topic, "Interest topics should be the same after changes to parent-owned instance" + end + end + + def test_with_has_many_inversing_does_not_trigger_association_callbacks_on_set_when_the_inverse_is_a_has_many + with_has_many_inversing do + man = interests(:trainspotting).man_with_callbacks + assert_not_predicate man, :add_callback_called? + end end def test_child_instance_should_be_shared_with_replaced_via_accessor_parent @@ -709,7 +726,7 @@ class InversePolymorphicBelongsToTests < ActiveRecord::TestCase assert_equal old_inversed_man.object_id, new_inversed_man.object_id end - def test_should_try_to_set_inverse_instances_when_the_inverse_is_a_has_many + 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 @@ -717,14 +734,31 @@ class InversePolymorphicBelongsToTests < ActiveRecord::TestCase 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_equal i.topic, iz.topic, "Interest topics should be the same after changes to child" + assert_not_equal i.topic, iz.topic, "Interest topics should not be the same after changes to child" iz.topic = "Cow tipping" - assert_equal i.topic, iz.topic, "Interest topics should be the same after changes to parent-owned instance" + assert_not_equal i.topic, iz.topic, "Interest topics should not be the same after changes to parent-owned instance" end - def test_does_not_trigger_association_callbacks_on_set_when_the_inverse_is_a_has_many - man = interests(:llama_wrangling).polymorphic_man_with_callbacks - assert_not_predicate man, :add_callback_called? + def test_with_has_many_inversing_should_try_to_set_inverse_instances_when_the_inverse_is_a_has_many + with_has_many_inversing do + 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_equal i.topic, iz.topic, "Interest topics should be the same after changes to child" + iz.topic = "Cow tipping" + assert_equal i.topic, iz.topic, "Interest topics should be the same after changes to parent-owned instance" + end + end + + def test_with_has_many_inversing_does_not_trigger_association_callbacks_on_set_when_the_inverse_is_a_has_many + with_has_many_inversing do + man = interests(:llama_wrangling).polymorphic_man_with_callbacks + assert_not_predicate man, :add_callback_called? + end end def test_trying_to_access_inverses_that_dont_exist_shouldnt_raise_an_error diff --git a/activerecord/test/cases/test_case.rb b/activerecord/test/cases/test_case.rb index 1b8bad32a4..9696d407d8 100644 --- a/activerecord/test/cases/test_case.rb +++ b/activerecord/test/cases/test_case.rb @@ -79,6 +79,14 @@ module ActiveRecord model.reset_column_information model.column_names.include?(column_name.to_s) end + + def with_has_many_inversing + old = ActiveRecord::Base.has_many_inversing + ActiveRecord::Base.has_many_inversing = true + yield + ensure + ActiveRecord::Base.has_many_inversing = old + end end class PostgreSQLTestCase < TestCase diff --git a/railties/lib/rails/application/configuration.rb b/railties/lib/rails/application/configuration.rb index 934578e9f1..93d3c430b5 100644 --- a/railties/lib/rails/application/configuration.rb +++ b/railties/lib/rails/application/configuration.rb @@ -154,6 +154,10 @@ module Rails end when "6.1" load_defaults "6.0" + + if respond_to?(:active_record) + active_record.has_many_inversing = true + end else raise "Unknown version #{target_version.to_s.inspect}" end diff --git a/railties/lib/rails/generators/rails/app/app_generator.rb b/railties/lib/rails/generators/rails/app/app_generator.rb index 27749ce3a1..1dfffca3ba 100644 --- a/railties/lib/rails/generators/rails/app/app_generator.rb +++ b/railties/lib/rails/generators/rails/app/app_generator.rb @@ -477,7 +477,7 @@ module Rails def delete_new_framework_defaults unless options[:update] - remove_file "config/initializers/new_framework_defaults_6_0.rb" + remove_file "config/initializers/new_framework_defaults_6_1.rb" end end diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_6_0.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_6_0.rb.tt deleted file mode 100644 index 92240ef5f5..0000000000 --- a/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_6_0.rb.tt +++ /dev/null @@ -1,45 +0,0 @@ -# Be sure to restart your server when you modify this file. -# -# This file contains migration options to ease your Rails 6.0 upgrade. -# -# Once upgraded flip defaults one by one to migrate to the new default. -# -# Read the Guide for Upgrading Ruby on Rails for more info on each option. - -# Don't force requests from old versions of IE to be UTF-8 encoded. -# Rails.application.config.action_view.default_enforce_utf8 = false - -# Embed purpose and expiry metadata inside signed and encrypted -# cookies for increased security. -# -# This option is not backwards compatible with earlier Rails versions. -# It's best enabled when your entire app is migrated and stable on 6.0. -# Rails.application.config.action_dispatch.use_cookies_with_metadata = true - -# Change the return value of `ActionDispatch::Response#content_type` to Content-Type header without modification. -# Rails.application.config.action_dispatch.return_only_media_type_on_content_type = false - -# Return false instead of self when enqueuing is aborted from a callback. -# Rails.application.config.active_job.return_false_on_aborted_enqueue = true - -# Send Active Storage analysis and purge jobs to dedicated queues. -# Rails.application.config.active_storage.queues.analysis = :active_storage_analysis -# Rails.application.config.active_storage.queues.purge = :active_storage_purge - -# When assigning to a collection of attachments declared via `has_many_attached`, replace existing -# attachments instead of appending. Use #attach to add new attachments without replacing existing ones. -# Rails.application.config.active_storage.replace_on_assign_to_many = true - -# Use ActionMailer::MailDeliveryJob for sending parameterized and normal mail. -# -# The default delivery jobs (ActionMailer::Parameterized::DeliveryJob, ActionMailer::DeliveryJob), -# will be removed in Rails 6.1. This setting is not backwards compatible with earlier Rails versions. -# If you send mail in the background, job workers need to have a copy of -# MailDeliveryJob to ensure all delivery jobs are processed properly. -# Make sure your entire app is migrated and stable on 6.0 before using this setting. -# Rails.application.config.action_mailer.delivery_job = "ActionMailer::MailDeliveryJob" - -# Enable the same cache key to be reused when the object being cached of type -# `ActiveRecord::Relation` changes by moving the volatile information (max updated at and count) -# of the relation's cache key into the cache version to support recycling cache key. -# Rails.application.config.active_record.collection_cache_versioning = true diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_6_1.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_6_1.rb.tt new file mode 100644 index 0000000000..c3bf0aa145 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_6_1.rb.tt @@ -0,0 +1,10 @@ +# Be sure to restart your server when you modify this file. +# +# This file contains migration options to ease your Rails 6.1 upgrade. +# +# Once upgraded flip defaults one by one to migrate to the new default. +# +# Read the Guide for Upgrading Ruby on Rails for more info on each option. + +# Support for inversing belongs_to -> has_many Active Record associations. +# Rails.application.config.active_record.has_many_inversing = true diff --git a/railties/test/application/configuration_test.rb b/railties/test/application/configuration_test.rb index 90da870233..bacafbc810 100644 --- a/railties/test/application/configuration_test.rb +++ b/railties/test/application/configuration_test.rb @@ -2144,42 +2144,6 @@ module ApplicationTests assert_equal({}, Rails.application.config.my_custom_config) end - test "represent_boolean_as_integer is deprecated" do - remove_from_config '.*config\.load_defaults.*\n' - - app_file "config/initializers/new_framework_defaults_6_0.rb", <<-RUBY - Rails.application.config.active_record.sqlite3.represent_boolean_as_integer = true - RUBY - - app_file "app/models/post.rb", <<-RUBY - class Post < ActiveRecord::Base - end - RUBY - - app "development" - assert_deprecated do - force_lazy_load_hooks { Post } - end - end - - test "represent_boolean_as_integer raises when the value is false" do - remove_from_config '.*config\.load_defaults.*\n' - - app_file "config/initializers/new_framework_defaults_6_0.rb", <<-RUBY - Rails.application.config.active_record.sqlite3.represent_boolean_as_integer = false - RUBY - - app_file "app/models/post.rb", <<-RUBY - class Post < ActiveRecord::Base - end - RUBY - - app "development" - assert_raises(RuntimeError) do - force_lazy_load_hooks { Post } - end - end - test "config_for containing ERB tags should evaluate" do app_file "config/custom.yml", <<-RUBY development: @@ -2287,6 +2251,18 @@ module ApplicationTests assert_equal "https://example.org/", last_response.location end + test "ActiveSupport::MessageEncryptor.use_authenticated_message_encryption can be configured via config.active_support.use_authenticated_message_encryption" do + remove_from_config '.*config\.load_defaults.*\n' + + app_file "config/initializers/new_framework_defaults_6_1.rb", <<-RUBY + Rails.application.config.active_record.has_many_inversing = true + RUBY + + app "development" + + assert_equal true, ActiveRecord::Base.has_many_inversing + end + test "ActiveSupport::MessageEncryptor.use_authenticated_message_encryption is true by default for new apps" do app "development" @@ -2301,18 +2277,6 @@ module ApplicationTests assert_equal false, ActiveSupport::MessageEncryptor.use_authenticated_message_encryption end - test "ActiveSupport::MessageEncryptor.use_authenticated_message_encryption can be configured via config.active_support.use_authenticated_message_encryption" do - remove_from_config '.*config\.load_defaults.*\n' - - app_file "config/initializers/new_framework_defaults_6_0.rb", <<-RUBY - Rails.application.config.active_support.use_authenticated_message_encryption = true - RUBY - - app "development" - - assert_equal true, ActiveSupport::MessageEncryptor.use_authenticated_message_encryption - end - test "ActiveSupport::Digest.hash_digest_class is Digest::SHA1 by default for new apps" do app "development" @@ -2327,18 +2291,6 @@ module ApplicationTests assert_equal Digest::MD5, ActiveSupport::Digest.hash_digest_class end - test "ActiveSupport::Digest.hash_digest_class can be configured via config.active_support.use_sha1_digests" do - remove_from_config '.*config\.load_defaults.*\n' - - app_file "config/initializers/new_framework_defaults_6_0.rb", <<-RUBY - Rails.application.config.active_support.use_sha1_digests = true - RUBY - - app "development" - - assert_equal Digest::SHA1, ActiveSupport::Digest.hash_digest_class - end - test "custom serializers should be able to set via config.active_job.custom_serializers in an initializer" do class ::DummySerializer < ActiveJob::Serializers::ObjectSerializer; end @@ -2365,18 +2317,6 @@ module ApplicationTests assert_equal true, ActionView::Helpers::FormTagHelper.default_enforce_utf8 end - test "ActionView::Helpers::FormTagHelper.default_enforce_utf8 can be configured via config.action_view.default_enforce_utf8" do - remove_from_config '.*config\.load_defaults.*\n' - - app_file "config/initializers/new_framework_defaults_6_0.rb", <<-RUBY - Rails.application.config.action_view.default_enforce_utf8 = true - RUBY - - app "development" - - assert_equal true, ActionView::Helpers::FormTagHelper.default_enforce_utf8 - end - test "ActionView::Template.finalize_compiled_template_methods is true by default" do app "test" assert_deprecated do @@ -2413,18 +2353,6 @@ module ApplicationTests assert_equal false, ActiveJob::Base.return_false_on_aborted_enqueue end - test "ActiveJob::Base.return_false_on_aborted_enqueue can be configured in the new framework defaults" do - remove_from_config '.*config\.load_defaults.*\n' - - app_file "config/initializers/new_framework_defaults_6_0.rb", <<-RUBY - Rails.application.config.active_job.return_false_on_aborted_enqueue = true - RUBY - - app "development" - - assert_equal true, ActiveJob::Base.return_false_on_aborted_enqueue - end - test "ActiveStorage.queues[:analysis] is :active_storage_analysis by default" do app "development" @@ -2468,18 +2396,6 @@ module ApplicationTests assert_equal true, ActionDispatch::Response.return_only_media_type_on_content_type end - test "ActionDispatch::Response.return_only_media_type_on_content_type can be configured in the new framework defaults" do - remove_from_config '.*config\.load_defaults.*\n' - - app_file "config/initializers/new_framework_defaults_6_0.rb", <<-RUBY - Rails.application.config.action_dispatch.return_only_media_type_on_content_type = false - RUBY - - app "development" - - assert_equal false, ActionDispatch::Response.return_only_media_type_on_content_type - end - test "ActionMailbox.logger is Rails.logger by default" do app "development" @@ -2566,18 +2482,6 @@ module ApplicationTests assert_equal ActionMailer::DeliveryJob, ActionMailer::Base.delivery_job end - test "ActionMailer::Base.delivery_job can be configured in the new framework defaults" do - remove_from_config '.*config\.load_defaults.*\n' - - app_file "config/initializers/new_framework_defaults_6_0.rb", <<-RUBY - Rails.application.config.action_mailer.delivery_job = "ActionMailer::MailDeliveryJob" - RUBY - - app "development" - - assert_equal ActionMailer::MailDeliveryJob, ActionMailer::Base.delivery_job - end - test "ActiveRecord::Base.filter_attributes should equal to filter_parameters" do app_file "config/initializers/filter_parameters_logging.rb", <<-RUBY Rails.application.config.filter_parameters += [ :password, :credit_card_number ] diff --git a/railties/test/generators/app_generator_test.rb b/railties/test/generators/app_generator_test.rb index 690b5cb2ab..afb61089de 100644 --- a/railties/test/generators/app_generator_test.rb +++ b/railties/test/generators/app_generator_test.rb @@ -212,7 +212,7 @@ class AppGeneratorTest < Rails::Generators::TestCase def test_new_application_doesnt_need_defaults run_generator - assert_no_file "config/initializers/new_framework_defaults_6_0.rb" + assert_no_file "config/initializers/new_framework_defaults_6_1.rb" end def test_new_application_load_defaults @@ -269,14 +269,14 @@ class AppGeneratorTest < Rails::Generators::TestCase app_root = File.join(destination_root, "myapp") run_generator [app_root] - assert_no_file "#{app_root}/config/initializers/new_framework_defaults_6_0.rb" + assert_no_file "#{app_root}/config/initializers/new_framework_defaults_6_1.rb" stub_rails_application(app_root) do generator = Rails::Generators::AppGenerator.new ["rails"], { update: true }, { destination_root: app_root, shell: @shell } generator.send(:app_const) quietly { generator.send(:update_config_files) } - assert_file "#{app_root}/config/initializers/new_framework_defaults_6_0.rb" + assert_file "#{app_root}/config/initializers/new_framework_defaults_6_1.rb" end end