diff options
19 files changed, 163 insertions, 39 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 727ddd6bb7..2af48f99db 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,11 @@ +* Add a warning for enum elements with 'not_' prefix. + + class Foo + enum status: [:sent, :not_sent] + end + + *Edu Depetris* + * Make currency symbols optional for money column type in PostgreSQL *Joel Schneider* diff --git a/activerecord/lib/active_record/database_configurations.rb b/activerecord/lib/active_record/database_configurations.rb index e122628b05..8baa0f5af6 100644 --- a/activerecord/lib/active_record/database_configurations.rb +++ b/activerecord/lib/active_record/database_configurations.rb @@ -105,18 +105,20 @@ module ActiveRecord return configs if configs.is_a?(Array) db_configs = configs.flat_map do |env_name, config| - if config.is_a?(Hash) && config.all? { |k, v| v.is_a?(Hash) } + if config.is_a?(Hash) && config.all? { |_, v| v.is_a?(Hash) } walk_configs(env_name.to_s, config) else build_db_config_from_raw_config(env_name.to_s, "primary", config) end - end.compact + end - if url = ENV["DATABASE_URL"] - merge_url_with_configs(url, db_configs) - else - db_configs + current_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call.to_s + + unless db_configs.find(&:for_current_env?) + db_configs << environment_url_config(current_env, "primary", {}) end + + merge_db_environment_variables(current_env, db_configs.compact) end def walk_configs(env_name, config) @@ -156,22 +158,22 @@ module ActiveRecord end end - def merge_url_with_configs(url, configs) - env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call.to_s + def merge_db_environment_variables(current_env, configs) + configs.map do |config| + next config if config.url_config? || config.env_name != current_env - if configs.find(&:for_current_env?) - configs.map do |config| - if config.url_config? - config - else - ActiveRecord::DatabaseConfigurations::UrlConfig.new(config.env_name, config.spec_name, url, config.config) - end - end - else - configs + [ActiveRecord::DatabaseConfigurations::UrlConfig.new(env, "primary", url)] + url_config = environment_url_config(current_env, config.spec_name, config.config) + url_config || config end end + def environment_url_config(env, spec_name, config) + url = ENV["DATABASE_URL"] + return unless url + + ActiveRecord::DatabaseConfigurations::UrlConfig.new(env, spec_name, url, config) + end + def method_missing(method, *args, &blk) case method when :each, :first diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb index 8077630aeb..fc49f752aa 100644 --- a/activerecord/lib/active_record/enum.rb +++ b/activerecord/lib/active_record/enum.rb @@ -200,6 +200,8 @@ module ActiveRecord # scope :active, -> { where(status: 0) } # scope :not_active, -> { where.not(status: 0) } if enum_scopes != false + klass.send(:detect_negative_condition!, value_method_name) + klass.send(:detect_enum_conflict!, name, value_method_name, true) klass.scope value_method_name, -> { where(attr => value) } @@ -261,5 +263,12 @@ module ActiveRecord source: source } end + + def detect_negative_condition!(method_name) + if method_name.start_with?("not_") && logger + logger.warn "An enum element in #{self.name} uses the prefix 'not_'." \ + " This will cause a conflict with auto generated negative scopes." + end + end end end diff --git a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb index a7e04007a9..e3efeb75b5 100644 --- a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb @@ -16,7 +16,7 @@ module ActiveRecord connection.create_database configuration["database"], creation_options establish_connection configuration rescue ActiveRecord::StatementInvalid => error - if error.cause.error_number == ER_DB_CREATE_EXISTS + if connection.error_number(error.cause) == ER_DB_CREATE_EXISTS raise DatabaseAlreadyExists else raise diff --git a/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb b/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb index 15a045ed23..95e57f42e3 100644 --- a/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb +++ b/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb @@ -347,6 +347,22 @@ module ActiveRecord assert_equal expected, actual end end + + def test_does_not_change_other_environments + ENV["DATABASE_URL"] = "postgres://localhost/foo" + config = { "production" => { "adapter" => "not_postgres", "database" => "not_foo", "host" => "localhost" }, "default_env" => {} } + + actual = resolve_spec(:production, config) + assert_equal config["production"].merge("name" => "production"), actual + + actual = resolve_spec(:default_env, config) + assert_equal({ + "host" => "localhost", + "database" => "foo", + "adapter" => "postgresql", + "name" => "default_env" + }, actual) + end end end end diff --git a/activerecord/test/cases/enum_test.rb b/activerecord/test/cases/enum_test.rb index ae0ce195b3..8673a99c45 100644 --- a/activerecord/test/cases/enum_test.rb +++ b/activerecord/test/cases/enum_test.rb @@ -3,6 +3,7 @@ require "cases/helper" require "models/author" require "models/book" +require "active_support/log_subscriber/test_helper" class EnumTest < ActiveRecord::TestCase fixtures :books, :authors, :author_addresses @@ -565,4 +566,25 @@ class EnumTest < ActiveRecord::TestCase assert_raises(NoMethodError) { klass.proposed } end + + test "enums with a negative condition log a warning" do + old_logger = ActiveRecord::Base.logger + logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new + + ActiveRecord::Base.logger = logger + + expected_message = "An enum element in Book uses the prefix 'not_'."\ + " This will cause a conflict with auto generated negative scopes." + + Class.new(ActiveRecord::Base) do + def self.name + "Book" + end + enum status: [:sent, :not_sent] + end + + assert_match(expected_message, logger.logged(:warn).first) + ensure + ActiveRecord::Base.logger = old_logger + end end diff --git a/activerecord/test/cases/tasks/mysql_rake_test.rb b/activerecord/test/cases/tasks/mysql_rake_test.rb index 258132835f..ac3c5bc26e 100644 --- a/activerecord/test/cases/tasks/mysql_rake_test.rb +++ b/activerecord/test/cases/tasks/mysql_rake_test.rb @@ -7,7 +7,10 @@ if current_adapter?(:Mysql2Adapter) module ActiveRecord class MysqlDBCreateTest < ActiveRecord::TestCase def setup - @connection = Class.new { def create_database(*); end }.new + @connection = Class.new do + def create_database(*); end + def error_number(_); end + end.new @configuration = { "adapter" => "mysql2", "database" => "my-app-db" @@ -90,9 +93,11 @@ if current_adapter?(:Mysql2Adapter) with_stubbed_connection_establish_connection do ActiveRecord::Base.connection.stub( :create_database, - proc { raise ActiveRecord::Tasks::DatabaseAlreadyExists } + proc { raise ActiveRecord::StatementInvalid } ) do - ActiveRecord::Tasks::DatabaseTasks.create @configuration + ActiveRecord::Base.connection.stub(:error_number, 1007) do + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end assert_equal "Database 'my-app-db' already exists\n", $stderr.string end diff --git a/activesupport/lib/active_support/dependencies.rb b/activesupport/lib/active_support/dependencies.rb index 32cb3a53f4..923d6ad228 100644 --- a/activesupport/lib/active_support/dependencies.rb +++ b/activesupport/lib/active_support/dependencies.rb @@ -20,6 +20,9 @@ module ActiveSupport #:nodoc: module Dependencies #:nodoc: extend self + UNBOUND_METHOD_MODULE_NAME = Module.instance_method(:name) + private_constant :UNBOUND_METHOD_MODULE_NAME + mattr_accessor :interlock, default: Interlock.new # :doc: @@ -658,7 +661,7 @@ module ActiveSupport #:nodoc: # Determine if the given constant has been automatically loaded. def autoloaded?(desc) - return false if desc.is_a?(Module) && desc.anonymous? + return false if desc.is_a?(Module) && real_mod_name(desc).nil? name = to_constant_name desc return false unless qualified_const_defined?(name) autoloaded_constants.include?(name) @@ -714,7 +717,7 @@ module ActiveSupport #:nodoc: when String then desc.sub(/^::/, "") when Symbol then desc.to_s when Module - desc.name || + real_mod_name(desc) || raise(ArgumentError, "Anonymous modules have no name to be referenced by") else raise TypeError, "Not a valid constant descriptor: #{desc.inspect}" end @@ -788,6 +791,14 @@ module ActiveSupport #:nodoc: def log(message) logger.debug("autoloading: #{message}") if logger && verbose end + + private + + # Returns the original name of a class or module even if `name` has been + # overridden. + def real_mod_name(mod) + UNBOUND_METHOD_MODULE_NAME.bind(mod).call + end end end diff --git a/activesupport/lib/active_support/dependencies/zeitwerk_integration.rb b/activesupport/lib/active_support/dependencies/zeitwerk_integration.rb index 821e3f971e..f75083a05a 100644 --- a/activesupport/lib/active_support/dependencies/zeitwerk_integration.rb +++ b/activesupport/lib/active_support/dependencies/zeitwerk_integration.rb @@ -28,7 +28,7 @@ module ActiveSupport end def autoloaded?(object) - cpath = object.is_a?(Module) ? object.name : object.to_s + cpath = object.is_a?(Module) ? real_mod_name(object) : object.to_s Rails.autoloaders.main.unloadable_cpath?(cpath) end diff --git a/activesupport/lib/active_support/hash_with_indifferent_access.rb b/activesupport/lib/active_support/hash_with_indifferent_access.rb index bbb7c36382..6acf64cb39 100644 --- a/activesupport/lib/active_support/hash_with_indifferent_access.rb +++ b/activesupport/lib/active_support/hash_with_indifferent_access.rb @@ -367,20 +367,20 @@ module ActiveSupport key.kind_of?(Symbol) ? key.to_s : key end - EMPTY_HASH = {}.freeze + def convert_value(value, for: nil) # :doc: + conversion = binding.local_variable_get(:for) - def convert_value(value, options = EMPTY_HASH) # :doc: if value.is_a? Hash - if options[:for] == :to_hash + if conversion == :to_hash value.to_hash else value.nested_under_indifferent_access end elsif value.is_a?(Array) - if options[:for] != :assignment || value.frozen? + if conversion != :assignment || value.frozen? value = value.dup end - value.map! { |e| convert_value(e, options) } + value.map! { |e| convert_value(e, for: conversion) } else value end diff --git a/activesupport/lib/active_support/notifications/fanout.rb b/activesupport/lib/active_support/notifications/fanout.rb index aa602329ec..dda71b880e 100644 --- a/activesupport/lib/active_support/notifications/fanout.rb +++ b/activesupport/lib/active_support/notifications/fanout.rb @@ -218,6 +218,7 @@ module ActiveSupport def finish(name, id, payload) stack = Thread.current[:_event_stack] event = stack.pop + event.payload = payload event.finish! @delegate.call event end diff --git a/activesupport/lib/active_support/notifications/instrumenter.rb b/activesupport/lib/active_support/notifications/instrumenter.rb index 7ab39c9bfb..24e1ab313a 100644 --- a/activesupport/lib/active_support/notifications/instrumenter.rb +++ b/activesupport/lib/active_support/notifications/instrumenter.rb @@ -52,7 +52,8 @@ module ActiveSupport end class Event - attr_reader :name, :time, :end, :transaction_id, :payload, :children + attr_reader :name, :time, :end, :transaction_id, :children + attr_accessor :payload def self.clock_gettime_supported? # :nodoc: defined?(Process::CLOCK_PROCESS_CPUTIME_ID) && diff --git a/activesupport/test/dependencies_test.rb b/activesupport/test/dependencies_test.rb index 003a0dbccb..6bad69f7f2 100644 --- a/activesupport/test/dependencies_test.rb +++ b/activesupport/test/dependencies_test.rb @@ -592,6 +592,13 @@ class DependenciesTest < ActiveSupport::TestCase nil_name = Module.new def nil_name.name() nil end assert_not ActiveSupport::Dependencies.autoloaded?(nil_name) + + invalid_constant_name = Module.new do + def self.name + "primary::SchemaMigration" + end + end + assert_not ActiveSupport::Dependencies.autoloaded?(invalid_constant_name) end ensure remove_constants(:ModuleFolder) diff --git a/activesupport/test/notifications_test.rb b/activesupport/test/notifications_test.rb index c9c63680e4..08277e5436 100644 --- a/activesupport/test/notifications_test.rb +++ b/activesupport/test/notifications_test.rb @@ -41,6 +41,27 @@ module Notifications assert_operator event.duration, :>, 0 end + def test_subscribe_to_events_where_payload_is_changed_during_instrumentation + @notifier.subscribe do |event| + assert_equal "success!", event.payload[:my_key] + end + + ActiveSupport::Notifications.instrument("foo") do |payload| + payload[:my_key] = "success!" + end + end + + def test_subscribe_to_events_can_handle_nested_hashes_in_the_paylaod + @notifier.subscribe do |event| + assert_equal "success!", event.payload[:some_key][:key_one] + assert_equal "great_success!", event.payload[:some_key][:key_two] + end + + ActiveSupport::Notifications.instrument("foo", some_key: { key_one: "success!" }) do |payload| + payload[:some_key][:key_two] = "great_success!" + end + end + def test_subscribe_via_top_level_api old_notifier = ActiveSupport::Notifications.notifier ActiveSupport::Notifications.notifier = ActiveSupport::Notifications::Fanout.new diff --git a/railties/lib/rails/engine/configuration.rb b/railties/lib/rails/engine/configuration.rb index 4143b3c881..612bd170c6 100644 --- a/railties/lib/rails/engine/configuration.rb +++ b/railties/lib/rails/engine/configuration.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require "rails/railtie/configuration" +require "yaml" module Rails class Engine @@ -40,7 +41,7 @@ module Rails paths.add "app", eager_load: true, glob: "{*,*/concerns}", - exclude: %w(assets javascript) + exclude: ["assets", webpacker_path] paths.add "app/assets", glob: "*" paths.add "app/controllers", eager_load: true paths.add "app/channels", eager_load: true, glob: "**/*_channel.rb" @@ -85,6 +86,14 @@ module Rails def autoload_paths @autoload_paths ||= paths.autoload_paths end + + def webpacker_path + if File.file?("#{Rails.root}/config/webpacker.yml") + YAML.load_file("#{Rails.root}/config/webpacker.yml")[Rails.env]["source_path"]&.gsub("app/", "") + else + "javascript" + end + end end 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 index ffe53497bf..2510ab906f 100644 --- 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 @@ -38,3 +38,8 @@ # 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/paths.rb b/railties/lib/rails/paths.rb index 838fe55acc..0664338e0b 100644 --- a/railties/lib/rails/paths.rb +++ b/railties/lib/rails/paths.rb @@ -223,12 +223,10 @@ module Rails private def files_in(path) - Dir.chdir(path) do - files = Dir.glob(@glob) - files -= @exclude if @exclude - files.map! { |file| File.join(path, file) } - files.sort - end + files = Dir.glob(@glob, base: path) + files -= @exclude if @exclude + files.map! { |file| File.join(path, file) } + files.sort end end end diff --git a/railties/test/application/configuration_test.rb b/railties/test/application/configuration_test.rb index 96678c395c..38dda20bec 100644 --- a/railties/test/application/configuration_test.rb +++ b/railties/test/application/configuration_test.rb @@ -1708,7 +1708,7 @@ module ApplicationTests app "development" ActiveSupport::Dependencies.autoload_paths.each do |path| assert_not_operator path, :ends_with?, "app/assets" - assert_not_operator path, :ends_with?, "app/javascript" + assert_not_operator path, :ends_with?, "app/#{Rails.configuration.webpacker_path}" end end diff --git a/railties/test/application/zeitwerk_integration_test.rb b/railties/test/application/zeitwerk_integration_test.rb index 9146222f73..ff8c06b479 100644 --- a/railties/test/application/zeitwerk_integration_test.rb +++ b/railties/test/application/zeitwerk_integration_test.rb @@ -98,6 +98,15 @@ class ZeitwerkIntegrationTest < ActiveSupport::TestCase assert_nil deps.safe_constantize("Admin") end + test "autoloaded? and overridden class names" do + invalid_constant_name = Module.new do + def self.name + "primary::SchemaMigration" + end + end + assert_not deps.autoloaded?(invalid_constant_name) + end + test "unloadable constants (main)" do app_file "app/models/user.rb", "class User; end" app_file "app/models/post.rb", "class Post; end" |