提交 39a72e85 编写于 作者: D David Heinemeier Hansson

Merge branch 'main' into support-propshaft

* main: (21 commits)
  Don't require role when passing shard to connected_to
  Replace ableist language
  Move AllMessages behaviour into Matcher
  Remove "matches?" from AS::N subscriber classes
  Add ability to lazily load the schema cache on connection
  Remove message from ActiveRecord::Rollback example
  Use queue_classic branch which works on psql 14
  Restore set_autoload_path triggering before bootstrap
  Specify ORDER BY enumsortorder for postgres enums
  Automatically infer inverse_of with scopes
  Remove unnecessary if in ExtendedDeterministicUniquenessValidator
  Replace "ActionText" with "Action Text" [ci skip]
  Fix new method name in DatabaseConfig#config deprecation message
  Remove unused instrumentation hooks from action_controller
  Update the documentation for find_by method
  Change "bash" to "shell" in getting started guide
  Clarify try calls public methods only
  Fix invalid link to List of Free Programming Books
  Update docs for nested_attributes to reflect default belongs_to behavior and validating parent model.
  Fix typo
  ...
......@@ -67,7 +67,7 @@ group :job do
gem "sidekiq", require: false
gem "sucker_punch", require: false
gem "delayed_job", require: false
gem "queue_classic", github: "QueueClassic/queue_classic", require: false, platforms: :ruby
gem "queue_classic", github: "jhawthorn/queue_classic", branch: "fix-connection-pg-14", require: false, platforms: :ruby
gem "sneakers", require: false
gem "que", require: false
gem "backburner", require: false
......
GIT
remote: https://github.com/QueueClassic/queue_classic.git
revision: 1e40ddd810c416619ead88316b2b251936ee2495
specs:
queue_classic (4.0.0.pre.alpha1)
pg (>= 0.17, < 2.0)
GIT
remote: https://github.com/brianmario/mysql2.git
revision: 7f4e844fccf6afa888d0bd108d4707a2a7784484
specs:
mysql2 (0.5.3)
GIT
remote: https://github.com/jhawthorn/queue_classic.git
revision: dbfee9bda71020b8f11ae8d9b85932462b7fbee0
branch: fix-connection-pg-14
specs:
queue_classic (4.0.0.pre.alpha1)
pg (>= 0.17, < 2.0)
GIT
remote: https://github.com/matthewd/websocket-client-simple.git
revision: e161305f1a466b9398d86df3b1731b03362da91b
......
......@@ -43,7 +43,7 @@ def test_deliveries_are_cleared_on_setup_and_teardown
end
end
class CrazyNameMailerTest < ActionMailer::TestCase
class ManuallySetNameMailerTest < ActionMailer::TestCase
tests TestTestMailer
def test_set_mailer_class_manual
......@@ -51,7 +51,7 @@ def test_set_mailer_class_manual
end
end
class CrazySymbolNameMailerTest < ActionMailer::TestCase
class ManuallySetSymbolNameMailerTest < ActionMailer::TestCase
tests :test_test_mailer
def test_set_mailer_class_manual_using_symbol
......@@ -59,7 +59,7 @@ def test_set_mailer_class_manual_using_symbol
end
end
class CrazyStringNameMailerTest < ActionMailer::TestCase
class ManuallySetStringNameMailerTest < ActionMailer::TestCase
tests "test_test_mailer"
def test_set_mailer_class_manual_using_string
......
......@@ -35,6 +35,20 @@ module Callbacks
skip_after_callbacks_if_terminated: true
end
class ActionFilter
def initialize(actions)
@actions = Array(actions).map(&:to_s).to_set
end
def match?(controller)
@actions.include?(controller.action_name)
end
alias after match?
alias before match?
alias around match?
end
module ClassMethods
# If +:only+ or +:except+ are used, convert the options into the
# +:if+ and +:unless+ options of ActiveSupport::Callbacks.
......@@ -62,8 +76,7 @@ def _normalize_callback_options(options)
def _normalize_callback_option(options, from, to) # :nodoc:
if from = options.delete(from)
_from = Array(from).map(&:to_s).to_set
from = proc { |c| _from.include? c.action_name }
from = ActionFilter.new(from)
options[to] = Array(options[to]).unshift(from)
end
end
......
......@@ -62,8 +62,7 @@ def unpermitted_parameters(event)
end
end
%w(write_fragment read_fragment exist_fragment?
expire_fragment expire_page write_page).each do |method|
%w(write_fragment read_fragment exist_fragment? expire_fragment).each do |method|
class_eval <<-METHOD, __FILE__, __LINE__ + 1
def #{method}(event)
return unless logger.info? && ActionController::Base.enable_fragment_cache_logging
......
......@@ -1166,7 +1166,7 @@ def determine_class(name)
end
end
class CrazyNameTest < ActionController::TestCase
class ManuallySetNameTest < ActionController::TestCase
tests ContentController
def test_controller_class_can_be_set_manually_not_just_inferred
......@@ -1174,7 +1174,7 @@ def test_controller_class_can_be_set_manually_not_just_inferred
end
end
class CrazySymbolNameTest < ActionController::TestCase
class ManuallySetSymbolNameTest < ActionController::TestCase
tests :content
def test_set_controller_class_using_symbol
......@@ -1182,7 +1182,7 @@ def test_set_controller_class_using_symbol
end
end
class CrazyStringNameTest < ActionController::TestCase
class ManuallySetStringNameTest < ActionController::TestCase
tests "content"
def test_set_controller_class_using_string
......
......@@ -908,7 +908,7 @@ class RequestFormat < BaseRequestTest
assert_equal [ Mime[:html] ], request.formats
request = stub_request "HTTP_ACCEPT" => "koz-asked/something-crazy",
request = stub_request "HTTP_ACCEPT" => "koz-asked/something-wild",
"QUERY_STRING" => ""
assert_equal [ Mime[:html] ], request.formats
......
......@@ -69,7 +69,7 @@ def with_test_route_set
end
end
class CrazyHelperTest < ActionView::TestCase
class ManuallySetHelperTest < ActionView::TestCase
tests PeopleHelper
def test_helper_class_can_be_set_manually_not_just_inferred
......@@ -77,7 +77,7 @@ def test_helper_class_can_be_set_manually_not_just_inferred
end
end
class CrazySymbolHelperTest < ActionView::TestCase
class ManuallySetSymbolHelperTest < ActionView::TestCase
tests :people
def test_set_helper_class_using_symbol
......@@ -85,7 +85,7 @@ def test_set_helper_class_using_symbol
end
end
class CrazyStringHelperTest < ActionView::TestCase
class ManuallySetStringHelperTest < ActionView::TestCase
tests "people"
def test_set_helper_class_using_string
......
......@@ -30,7 +30,7 @@ def test_simple_format_included_in_isolation
def test_simple_format
assert_equal "<p></p>", simple_format(nil)
assert_equal "<p>crazy\n<br /> cross\n<br /> platform linebreaks</p>", simple_format("crazy\r\n cross\r platform linebreaks")
assert_equal "<p>ridiculous\n<br /> cross\n<br /> platform linebreaks</p>", simple_format("ridiculous\r\n cross\r platform linebreaks")
assert_equal "<p>A paragraph</p>\n\n<p>and another one!</p>", simple_format("A paragraph\n\nand another one!")
assert_equal "<p>A paragraph\n<br /> With a newline</p>", simple_format("A paragraph\n With a newline")
......
* Don't require `role` when passing `shard` to `connected_to`.
`connected_to` can now be called with a `shard` only. Note that `role` is still inherited if `connected_to` calls are nested.
*Eileen M. Uchitelle*
* Add option to lazily load the schema cache on the connection.
Previously, the only way to load the schema cache in Active Record was through the Railtie on boot. This option provides the ability to load the schema cache on the connection after it's been established. Loading the cache lazily on the connection can be beneficial for Rails applications that use multiple databases because it will load the cache at the time the connection is established. Currently Railties doesn't have access to the connections before boot.
To use the cache, set `config.active_record.lazily_load_schema_cache = true` in your application configuration. In addition a `schema_cache_path` should be set in your database configuration if you don't want to use the default "db/schema_cache.yml" path.
*Eileen M. Uchitelle*
* Allow automatic `inverse_of` detection for associations with scopes.
Automatic `inverse_of` detection now works for associations with scopes. For
example, the `comments` association here now automatically detects
`inverse_of: :post`, so we don't need to pass that option:
```ruby
class Post < ActiveRecord::Base
has_many :comments, -> { visible }
end
class Comment < ActiveRecord::Base
belongs_to :post
end
```
Note that the automatic detection still won't work if the inverse
association has a scope. In this example a scope on the `post` association
would still prevent Rails from finding the inverse for the `comments`
association.
This will be the default for new apps in Rails 7. To opt in:
```ruby
config.active_record.automatic_scope_inversing = true
```
*Daniel Colson*, *Chris Bloom*
* Accept optional transaction args to `ActiveRecord::Locking::Pessimistic#with_lock`
`#with_lock` now accepts transaction options like `requires_new:`,
......@@ -388,7 +431,7 @@
config.active_record.partial_inserts = true
```
If a migration remove the default value of a column, this option
If a migration removes the default value of a column, this option
would cause old processes to no longer be able to create new records.
If you need to remove a column, you should first use `ignored_columns`
......
......@@ -170,6 +170,12 @@ module Tasks
autoload :TestDatabases, "active_record/test_databases"
autoload :TestFixtures, "active_record/fixtures"
# Lazily load the schema cache. This option will load the schema cache
# when a connection is established rather than on boot. If set,
# +config.active_record.use_schema_cache_dump+ will be set to false.
singleton_class.attr_accessor :lazily_load_schema_cache
self.lazily_load_schema_cache = false
# A list of tables or regex's to match tables to ignore when
# dumping the schema cache. For example if this is set to +[/^_/]+
# the schema cache will not dump tables named with an underscore.
......
......@@ -748,9 +748,10 @@ def association_instance_set(name, association)
# inverse detection only works on #has_many, #has_one, and
# #belongs_to associations.
#
# <tt>:foreign_key</tt> and <tt>:through</tt> options on the associations,
# or a custom scope, will also prevent the association's inverse
# from being found automatically.
# <tt>:foreign_key</tt> and <tt>:through</tt> options on the associations
# will also prevent the association's inverse from being found automatically,
# as will a custom scopes in some cases. See further details in the
# {Active Record Associations guide}[https://guides.rubyonrails.org/association_basics.html#bi-directional-associations].
#
# The automatic guessing of the inverse association uses a heuristic based
# on the name of the class, so it may not work for all associations,
......
......@@ -74,8 +74,8 @@ def construct_join_attributes(*records)
end
end
# Note: this does not capture all cases, for example it would be crazy to try to
# properly support stale-checking for nested associations.
# Note: this does not capture all cases, for example it would be impractical
# to try to properly support stale-checking for nested associations.
def stale_state
if through_reflection.belongs_to?
owner[through_reflection.foreign_key] && owner[through_reflection.foreign_key].to_s
......
......@@ -19,6 +19,13 @@ def get_schema_cache(connection)
def set_schema_cache(cache)
self.schema_cache = cache
end
def lazily_set_schema_cache
return unless ActiveRecord.lazily_load_schema_cache
cache = SchemaCache.load_from(db_config.lazy_schema_cache_path)
set_schema_cache(cache)
end
end
class NullPool # :nodoc:
......@@ -147,6 +154,8 @@ def initialize(pool_config)
@async_executor = build_async_executor
lazily_set_schema_cache
@reaper = Reaper.new(self, db_config.reaping_frequency)
@reaper.run
end
......
......@@ -459,7 +459,7 @@ def enum_types
query = <<~SQL
SELECT
type.typname AS name,
string_agg(enum.enumlabel, ',') AS value
string_agg(enum.enumlabel, ',' ORDER BY enum.enumsortorder) AS value
FROM pg_enum AS enum
JOIN pg_type AS type
ON (type.oid = enum.enumtypid)
......
......@@ -153,10 +153,6 @@ def connected_to(role: nil, shard: nil, prevent_writes: false, &blk)
raise ArgumentError, "must provide a `shard` and/or `role`."
end
unless role
raise ArgumentError, "`connected_to` cannot accept a `shard` argument without a `role`."
end
with_role_and_shard(role, shard, prevent_writes, &blk)
end
......
......@@ -33,7 +33,7 @@ def initialize(env_name, name, configuration_hash)
end
def config
ActiveSupport::Deprecation.warn("DatabaseConfig#config will be removed in 7.0.0 in favor of DatabaseConfigurations#configuration_hash which returns a hash with symbol keys")
ActiveSupport::Deprecation.warn("DatabaseConfig#config will be removed in 7.0.0 in favor of DatabaseConfig#configuration_hash which returns a hash with symbol keys")
configuration_hash.stringify_keys
end
......@@ -109,6 +109,18 @@ def schema_cache_path
configuration_hash[:schema_cache_path]
end
def default_schema_cache_path
"db/schema_cache.yml"
end
def lazy_schema_cache_path
schema_cache_path || default_schema_cache_path
end
def primary? # :nodoc:
Base.configurations.primary?(name)
end
# Determines whether to dump the schema for a database.
def schema_dump
configuration_hash.fetch(:schema_dump, true)
......
......@@ -12,7 +12,7 @@ def validate_each(record, attribute, value)
super(record, attribute, value)
klass = record.class
if klass.deterministic_encrypted_attributes&.each do |attribute_name|
klass.deterministic_encrypted_attributes&.each do |attribute_name|
encrypted_type = klass.type_for_attribute(attribute_name)
[ encrypted_type, *encrypted_type.previous_types ].each do |type|
encrypted_value = type.serialize(value)
......@@ -21,7 +21,6 @@ def validate_each(record, attribute, value)
end
end
end
end
end
end
end
......
......@@ -326,7 +326,7 @@ class StrictLoadingViolationError < ActiveRecordError
# # The system must fail on Friday so that our support department
# # won't be out of job. We silently rollback this transaction
# # without telling the user.
# raise ActiveRecord::Rollback, "Call tech support!"
# raise ActiveRecord::Rollback
# end
# end
# # ActiveRecord::Rollback is the only exception that won't be passed on
......
......@@ -245,18 +245,19 @@ class TooManyRecords < ActiveRecordError
#
# === Validating the presence of a parent model
#
# If you want to validate that a child record is associated with a parent
# record, you can use the +validates_presence_of+ method and the +:inverse_of+
# key as this example illustrates:
#
# class Member < ActiveRecord::Base
# has_many :posts, inverse_of: :member
# accepts_nested_attributes_for :posts
# The +belongs_to+ association validates the presence of the parent model
# by default. You can disable this behavior by specifying <code>optional: true</code>.
# This can be used, for example, when conditionally validating the presence
# of the parent model:
#
# class Veterinarian < ActiveRecord::Base
# has_many :patients, inverse_of: :veterinarian
# accepts_nested_attributes_for :patients
# end
#
# class Post < ActiveRecord::Base
# belongs_to :member, inverse_of: :posts
# validates_presence_of :member
# class Patient < ActiveRecord::Base
# belongs_to :veterinarian, inverse_of: :patients, optional: true
# validates :veterinarian, presence: true, unless: -> { awaiting_intake }
# end
#
# Note that if you do not specify the +:inverse_of+ option, then
......
......@@ -130,7 +130,7 @@ class Railtie < Rails::Railtie # :nodoc:
initializer "active_record.check_schema_cache_dump" do
check_schema_cache_dump_version = config.active_record.check_schema_cache_dump_version
if config.active_record.use_schema_cache_dump
if config.active_record.use_schema_cache_dump && !config.active_record.lazily_load_schema_cache
config.after_initialize do |app|
ActiveSupport.on_load(:active_record) do
db_config = ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).first
......
......@@ -10,6 +10,7 @@ module Reflection # :nodoc:
included do
class_attribute :_reflections, instance_writer: false, default: {}
class_attribute :aggregate_reflections, instance_writer: false, default: {}
class_attribute :automatic_scope_inversing, instance_writer: false, default: false
end
class << self
......@@ -633,7 +634,7 @@ def valid_inverse_reflection?(reflection)
reflection &&
foreign_key == reflection.foreign_key &&
klass <= reflection.active_record &&
can_find_inverse_of_automatically?(reflection)
can_find_inverse_of_automatically?(reflection, true)
end
# Checks to see if the reflection doesn't have any options that prevent
......@@ -642,14 +643,25 @@ def valid_inverse_reflection?(reflection)
# have <tt>has_many</tt>, <tt>has_one</tt>, <tt>belongs_to</tt> associations.
# Third, we must not have options such as <tt>:foreign_key</tt>
# which prevent us from correctly guessing the inverse association.
#
# Anything with a scope can additionally ruin our attempt at finding an
# inverse, so we exclude reflections with scopes.
def can_find_inverse_of_automatically?(reflection)
def can_find_inverse_of_automatically?(reflection, inverse_reflection = false)
reflection.options[:inverse_of] != false &&
!reflection.options[:through] &&
!reflection.options[:foreign_key] &&
scope_allows_automatic_inverse_of?(reflection, inverse_reflection)
end
# Scopes on the potential inverse reflection prevent automatic
# <tt>inverse_of</tt>, since the scope could exclude the owner record
# we would inverse from. Scopes on the reflection itself allow for
# automatic <tt>inverse_of</tt> as long as
# <tt>config.active_record.automatic_scope_inversing<tt> is set to
# +true+ (the default for new applications).
def scope_allows_automatic_inverse_of?(reflection, inverse_reflection)
if inverse_reflection
!reflection.scope
else
!reflection.scope || reflection.klass.automatic_scope_inversing
end
end
def derive_class_name
......@@ -736,7 +748,7 @@ def join_foreign_type
end
private
def can_find_inverse_of_automatically?(_)
def can_find_inverse_of_automatically?(*)
!polymorphic? && super
end
end
......
......@@ -679,7 +679,7 @@ def test_eager_with_has_many_and_limit_and_high_offset_and_multiple_array_condit
assert_queries(1) do
posts = Post.references(:authors, :comments).
merge(includes: [ :author, :comments ], limit: 2, offset: 10,
where: [ "authors.name = ? and comments.body = ?", "David", "go crazy" ]).to_a
where: [ "authors.name = ? and comments.body = ?", "David", "go wild" ]).to_a
assert_equal 0, posts.size
end
end
......@@ -687,7 +687,7 @@ def test_eager_with_has_many_and_limit_and_high_offset_and_multiple_array_condit
def test_eager_with_has_many_and_limit_and_high_offset_and_multiple_hash_conditions
assert_queries(1) do
posts = Post.all.merge!(includes: [ :author, :comments ], limit: 2, offset: 10,
where: { "authors.name" => "David", "comments.body" => "go crazy" }).to_a
where: { "authors.name" => "David", "comments.body" => "go wild" }).to_a
assert_equal 0, posts.size
end
end
......
......@@ -210,13 +210,13 @@ def test_has_one_through_nonpreload_eager_loading_through_polymorphic
end
def test_has_one_through_nonpreload_eager_loading_through_polymorphic_with_more_than_one_through_record
Sponsor.new(sponsor_club: clubs(:crazy_club), sponsorable: members(:groucho)).save!
Sponsor.new(sponsor_club: clubs(:outrageous_club), sponsorable: members(:groucho)).save!
members = assert_queries(1) do
Member.all.merge!(includes: :sponsor_club, where: ["members.name = ?", "Groucho Marx"], order: "clubs.name DESC").to_a # force fallback
end
assert_equal 1, members.size
assert_not_nil assert_no_queries { members[0].sponsor_club }
assert_equal clubs(:crazy_club), members[0].sponsor_club
assert_equal clubs(:outrageous_club), members[0].sponsor_club
end
def test_uninitialized_has_one_through_should_return_nil_for_unsaved_record
......
......@@ -22,9 +22,12 @@
require "models/author"
require "models/user"
require "models/room"
require "models/contract"
require "models/subscription"
require "models/book"
class AutomaticInverseFindingTests < ActiveRecord::TestCase
fixtures :ratings, :comments, :cars
fixtures :ratings, :comments, :cars, :books
def test_has_one_and_belongs_to_should_find_inverse_automatically_on_multiple_word_name
monkey_reflection = MixedCaseMonkey.reflect_on_association(:human)
......@@ -109,6 +112,51 @@ def test_has_one_and_belongs_to_with_custom_association_name_should_not_find_wro
assert_not_equal room_reflection, owner_reflection.inverse_of
end
def test_has_many_and_belongs_to_with_a_scope_and_automatic_scope_inversing_should_find_inverse_automatically
contacts_reflection = Company.reflect_on_association(:special_contracts)
company_reflection = SpecialContract.reflect_on_association(:company)
assert contacts_reflection.scope
assert_not company_reflection.scope
with_automatic_scope_inversing(contacts_reflection, company_reflection) do
assert_predicate contacts_reflection, :has_inverse?
assert_equal company_reflection, contacts_reflection.inverse_of
assert_not_equal contacts_reflection, company_reflection.inverse_of
end
end
def test_has_one_and_belongs_to_with_a_scope_and_automatic_scope_inversing_should_find_inverse_automatically
post_reflection = Author.reflect_on_association(:recent_post)
author_reflection = Post.reflect_on_association(:author)
assert post_reflection.scope
assert_not author_reflection.scope
with_automatic_scope_inversing(post_reflection, author_reflection) do
assert_predicate post_reflection, :has_inverse?
assert_equal author_reflection, post_reflection.inverse_of
assert_not_equal post_reflection, author_reflection.inverse_of
end
end
def test_has_many_with_scoped_belongs_to_does_not_find_inverse_automatically
book = books(:tlg)
book.update_attribute(:author_visibility, :invisible)
assert_nil book.subscriptions.new.book
subscription_reflection = Book.reflect_on_association(:subscriptions)
book_reflection = Subscription.reflect_on_association(:book)
assert_not subscription_reflection.scope
assert book_reflection.scope
with_automatic_scope_inversing(book_reflection, subscription_reflection) do
assert_nil book.subscriptions.new.book
end
end
def test_has_one_and_belongs_to_automatic_inverse_shares_objects
car = Car.first
bulb = Bulb.create!(car: car)
......
......@@ -68,6 +68,17 @@ def test_has_many_through_has_many_with_has_many_through_source_reflection_prelo
assert_no_queries do
assert_equal [general, general], author.tags
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
assert_queries(4) { Author.includes(:tags).first }
end
end
def test_has_many_through_has_many_with_has_many_through_source_reflection_preload_via_joins
......@@ -309,6 +320,17 @@ def test_has_many_through_has_many_through_with_belongs_to_source_reflection_pre
assert_no_queries do
assert_equal [general, general], author.tagging_tags
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
assert_queries(4) { Author.includes(:tagging_tags).first }
end
end