diff options
72 files changed, 1812 insertions, 425 deletions
diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index a30f178190..c544ec96cf 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,3 +1,8 @@ +* Expose ActionController::Parameters#each_key which allows iterating over + keys without allocating an array. + + *Richard Schneeman* + * Purpose metadata for signed/encrypted cookies. Rails can now thwart attacks that attempt to copy signed/encrypted value @@ -10,8 +15,6 @@ Enable `action_dispatch.use_cookies_with_metadata` to use this feature, which writes cookies with the new purpose and expiry metadata embedded. - Pull Request: #32937 - *Assain Jaleel* * Raises `ActionController::RespondToMismatchError` with confliciting `respond_to` invocations. @@ -39,7 +42,7 @@ *Aaron Kromer* -* Pass along arguments to underlying `get` method in `follow_redirect!` +* Pass along arguments to underlying `get` method in `follow_redirect!`. Now all arguments passed to `follow_redirect!` are passed to the underlying `get` method. This for example allows to set custom headers for the @@ -56,7 +59,7 @@ *Vinicius Stock* -* Introduce ActionDispatch::DebugExceptions.register_interceptor +* Introduce `ActionDispatch::DebugExceptions.register_interceptor`. Exception aware plugin authors can use the newly introduced `.register_interceptor` method to get the processed exception, instead of diff --git a/actionpack/lib/abstract_controller/caching/fragments.rb b/actionpack/lib/abstract_controller/caching/fragments.rb index febd8a67a6..95078a2a28 100644 --- a/actionpack/lib/abstract_controller/caching/fragments.rb +++ b/actionpack/lib/abstract_controller/caching/fragments.rb @@ -88,7 +88,11 @@ module AbstractController def combined_fragment_cache_key(key) head = self.class.fragment_cache_keys.map { |k| instance_exec(&k) } tail = key.is_a?(Hash) ? url_for(key).split("://").last : key - [ :views, (ENV["RAILS_CACHE_ID"] || ENV["RAILS_APP_VERSION"]), *head, *tail ].compact + + cache_key = [:views, ENV["RAILS_CACHE_ID"] || ENV["RAILS_APP_VERSION"], head, tail] + cache_key.flatten!(1) + cache_key.compact! + cache_key end # Writes +content+ to the location signified by diff --git a/actionpack/lib/action_controller/metal/force_ssl.rb b/actionpack/lib/action_controller/metal/force_ssl.rb index 8d53a30e93..26e6f72b66 100644 --- a/actionpack/lib/action_controller/metal/force_ssl.rb +++ b/actionpack/lib/action_controller/metal/force_ssl.rb @@ -5,8 +5,8 @@ require "active_support/core_ext/hash/slice" module ActionController # This module is deprecated in favor of +config.force_ssl+ in your environment - # config file. This will ensure all communication to non-whitelisted endpoints - # served by your application occurs over HTTPS. + # config file. This will ensure all endpoints not explicitly marked otherwise + # will have all communication served over HTTPS. module ForceSSL # :nodoc: extend ActiveSupport::Concern include AbstractController::Callbacks diff --git a/actionpack/lib/action_controller/metal/live.rb b/actionpack/lib/action_controller/metal/live.rb index 2f4c8fb83c..b1c2391afe 100644 --- a/actionpack/lib/action_controller/metal/live.rb +++ b/actionpack/lib/action_controller/metal/live.rb @@ -86,7 +86,7 @@ module ActionController # Note: SSEs are not currently supported by IE. However, they are supported # by Chrome, Firefox, Opera, and Safari. class SSE - WHITELISTED_OPTIONS = %w( retry event id ) + PERMITTED_OPTIONS = %w( retry event id ) def initialize(stream, options = {}) @stream = stream @@ -111,7 +111,7 @@ module ActionController def perform_write(json, options) current_options = @options.merge(options).stringify_keys - WHITELISTED_OPTIONS.each do |option_name| + PERMITTED_OPTIONS.each do |option_name| if (option_value = current_options[option_name]) @stream.write "#{option_name}: #{option_value}\n" end diff --git a/actionpack/lib/action_controller/metal/mime_responds.rb b/actionpack/lib/action_controller/metal/mime_responds.rb index 9e63e3e7b6..118da11990 100644 --- a/actionpack/lib/action_controller/metal/mime_responds.rb +++ b/actionpack/lib/action_controller/metal/mime_responds.rb @@ -11,7 +11,7 @@ module ActionController #:nodoc: # @people = Person.all # end # - # That action implicitly responds to all formats, but formats can also be whitelisted: + # That action implicitly responds to all formats, but formats can also be explicitly enumerated: # # def index # @people = Person.all diff --git a/actionpack/lib/action_controller/metal/request_forgery_protection.rb b/actionpack/lib/action_controller/metal/request_forgery_protection.rb index 7ed7b9d546..cb109c6ad8 100644 --- a/actionpack/lib/action_controller/metal/request_forgery_protection.rb +++ b/actionpack/lib/action_controller/metal/request_forgery_protection.rb @@ -45,7 +45,7 @@ module ActionController #:nodoc: # the same origin. Note however that any cross-origin third party domain # allowed via {CORS}[https://en.wikipedia.org/wiki/Cross-origin_resource_sharing] # will also be able to create XHR requests. Be sure to check your - # CORS whitelist before disabling forgery protection for XHR. + # CORS configuration before disabling forgery protection for XHR. # # CSRF protection is turned on with the <tt>protect_from_forgery</tt> method. # By default <tt>protect_from_forgery</tt> protects your session with diff --git a/actionpack/lib/action_controller/metal/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb index 7af29f8dca..a37f08d944 100644 --- a/actionpack/lib/action_controller/metal/strong_parameters.rb +++ b/actionpack/lib/action_controller/metal/strong_parameters.rb @@ -58,7 +58,7 @@ module ActionController # == Action Controller \Parameters # - # Allows you to choose which attributes should be whitelisted for mass updating + # Allows you to choose which attributes should be permitted for mass updating # and thus prevent accidentally exposing that which shouldn't be exposed. # Provides two methods for this purpose: #require and #permit. The former is # used to mark parameters as required. The latter is used to set the parameter @@ -133,6 +133,15 @@ module ActionController # Returns a hash that can be used as the JSON representation for the parameters. ## + # :method: each_key + # + # :call-seq: + # each_key() + # + # Calls block once for each key in the parameters, passing the key. + # If no block is given, an enumerator is returned instead. + + ## # :method: empty? # # :call-seq: @@ -204,7 +213,7 @@ module ActionController # # Returns a new array of the values of the parameters. delegate :keys, :key?, :has_key?, :values, :has_value?, :value?, :empty?, :include?, - :as_json, :to_s, to: :@parameters + :as_json, :to_s, :each_key, to: :@parameters # By default, never raise an UnpermittedParameters exception if these # params are present. The default includes both 'controller' and 'action' @@ -505,7 +514,7 @@ module ActionController # # Note that if you use +permit+ in a key that points to a hash, # it won't allow all the hash. You also need to specify which - # attributes inside the hash should be whitelisted. + # attributes inside the hash should be permitted. # # params = ActionController::Parameters.new({ # person: { @@ -904,15 +913,28 @@ module ActionController PERMITTED_SCALAR_TYPES.any? { |type| value.is_a?(type) } end - def permitted_scalar_filter(params, key) - if has_key?(key) && permitted_scalar?(self[key]) - params[key] = self[key] + # Adds existing keys to the params if their values are scalar. + # + # For example: + # + # puts self.keys #=> ["zipcode(90210i)"] + # params = {} + # + # permitted_scalar_filter(params, "zipcode") + # + # puts params.keys # => ["zipcode"] + def permitted_scalar_filter(params, permitted_key) + permitted_key = permitted_key.to_s + + if has_key?(permitted_key) && permitted_scalar?(self[permitted_key]) + params[permitted_key] = self[permitted_key] end - keys.grep(/\A#{Regexp.escape(key)}\(\d+[if]?\)\z/) do |k| - if permitted_scalar?(self[k]) - params[k] = self[k] - end + each_key do |key| + next unless key =~ /\(\d+[if]?\)\z/ + next unless $~.pre_match == permitted_key + + params[key] = self[key] if permitted_scalar?(self[key]) end end @@ -997,8 +1019,8 @@ module ActionController # # It provides an interface for protecting attributes from end-user # assignment. This makes Action Controller parameters forbidden - # to be used in Active Model mass assignment until they have been - # whitelisted. + # to be used in Active Model mass assignment until they have been explicitly + # enumerated. # # In addition, parameters can be marked as required and flow through a # predefined raise/rescue flow to end up as a <tt>400 Bad Request</tt> with no @@ -1034,7 +1056,7 @@ module ActionController # end # # In order to use <tt>accepts_nested_attributes_for</tt> with Strong \Parameters, you - # will need to specify which nested attributes should be whitelisted. You might want + # will need to specify which nested attributes should be permitted. You might want # to allow +:id+ and +:_destroy+, see ActiveRecord::NestedAttributes for more information. # # class Person @@ -1052,7 +1074,7 @@ module ActionController # private # # def person_params - # # It's mandatory to specify the nested attributes that should be whitelisted. + # # It's mandatory to specify the nested attributes that should be permitted. # # If you use `permit` with just the key that points to the nested attributes hash, # # it will return an empty hash. # params.require(:person).permit(:name, :age, pets_attributes: [ :id, :name, :category ]) diff --git a/actionpack/lib/action_dispatch/http/mime_negotiation.rb b/actionpack/lib/action_dispatch/http/mime_negotiation.rb index d7435fa8df..be129965d1 100644 --- a/actionpack/lib/action_dispatch/http/mime_negotiation.rb +++ b/actionpack/lib/action_dispatch/http/mime_negotiation.rb @@ -85,10 +85,7 @@ module ActionDispatch if variant.all? { |v| v.is_a?(Symbol) } @variant = ActiveSupport::ArrayInquirer.new(variant) else - raise ArgumentError, "request.variant must be set to a Symbol or an Array of Symbols. " \ - "For security reasons, never directly set the variant to a user-provided value, " \ - "like params[:variant].to_sym. Check user-provided value against a whitelist first, " \ - "then set the variant: request.variant = :tablet if params[:variant] == 'tablet'" + raise ArgumentError, "request.variant must be set to a Symbol or an Array of Symbols." end end diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index ff325afc54..07e3be4db8 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -553,10 +553,10 @@ module ActionDispatch # # match 'json_only', constraints: { format: 'json' }, via: :get # - # class Whitelist + # class PermitList # def matches?(request) request.remote_ip == '1.2.3.4' end # end - # match 'path', to: 'c#a', constraints: Whitelist.new, via: :get + # match 'path', to: 'c#a', constraints: PermitList.new, via: :get # # See <tt>Scoping#constraints</tt> for more examples with its scope # equivalent. diff --git a/actionpack/test/controller/parameters/always_permitted_parameters_test.rb b/actionpack/test/controller/parameters/always_permitted_parameters_test.rb index fe0e5e368d..974612fb7b 100644 --- a/actionpack/test/controller/parameters/always_permitted_parameters_test.rb +++ b/actionpack/test/controller/parameters/always_permitted_parameters_test.rb @@ -20,7 +20,7 @@ class AlwaysPermittedParametersTest < ActiveSupport::TestCase end end - test "permits parameters that are whitelisted" do + test "allows both explicitly listed and always-permitted parameters" do params = ActionController::Parameters.new( book: { pages: 65 }, format: "json") diff --git a/actionview/lib/action_view/helpers/sanitize_helper.rb b/actionview/lib/action_view/helpers/sanitize_helper.rb index cb0c99c4cf..f4fa133f55 100644 --- a/actionview/lib/action_view/helpers/sanitize_helper.rb +++ b/actionview/lib/action_view/helpers/sanitize_helper.rb @@ -10,7 +10,7 @@ module ActionView # These helper methods extend Action View making them callable within your template files. module SanitizeHelper extend ActiveSupport::Concern - # Sanitizes HTML input, stripping all tags and attributes that aren't whitelisted. + # Sanitizes HTML input, stripping all but known-safe tags and attributes. # # It also strips href/src attributes with unsafe protocols like # <tt>javascript:</tt>, while also protecting against attempts to use Unicode, @@ -40,7 +40,7 @@ module ActionView # # <%= sanitize @comment.body %> # - # Providing custom whitelisted tags and attributes: + # Providing custom lists of permitted tags and attributes: # # <%= sanitize @comment.body, tags: %w(strong em a), attributes: %w(href) %> # diff --git a/actionview/lib/action_view/template/handlers/erb.rb b/actionview/lib/action_view/template/handlers/erb.rb index b7b749f9da..270be0a380 100644 --- a/actionview/lib/action_view/template/handlers/erb.rb +++ b/actionview/lib/action_view/template/handlers/erb.rb @@ -14,7 +14,17 @@ module ActionView class_attribute :erb_implementation, default: Erubi # Do not escape templates of these mime types. - class_attribute :escape_whitelist, default: ["text/plain"] + class_attribute :escape_ignore_list, default: ["text/plain"] + + [self, singleton_class].each do |base| + base.send(:alias_method, :escape_whitelist, :escape_ignore_list) + base.send(:alias_method, :escape_whitelist=, :escape_ignore_list=) + + base.deprecate( + escape_whitelist: "use #escape_ignore_list instead", + :escape_whitelist= => "use #escape_ignore_list= instead" + ) + end ENCODING_TAG = Regexp.new("\\A(<%#{ENCODING_FLAG}-?%>)[ \\t]*") @@ -47,7 +57,7 @@ module ActionView self.class.erb_implementation.new( erb, - escape: (self.class.escape_whitelist.include? template.type), + escape: (self.class.escape_ignore_list.include? template.type), trim: (self.class.erb_trim_mode == "-") ).src end diff --git a/activejob/CHANGELOG.md b/activejob/CHANGELOG.md index 2417ea3a87..c47465cb43 100644 --- a/activejob/CHANGELOG.md +++ b/activejob/CHANGELOG.md @@ -1,3 +1,7 @@ +* Added `enqueue_retry.active_job`, `retry_stopped.active_job`, and `discard.active_job` hooks. + + *steves* + * Allow `assert_performed_with` to be called without a block. *bogdanvlviv* diff --git a/activejob/lib/active_job/arguments.rb b/activejob/lib/active_job/arguments.rb index 86bb0c5540..ba7f9456f9 100644 --- a/activejob/lib/active_job/arguments.rb +++ b/activejob/lib/active_job/arguments.rb @@ -24,18 +24,20 @@ module ActiveJob module Arguments extend self # :nodoc: - TYPE_WHITELIST = [ NilClass, String, Integer, Float, BigDecimal, TrueClass, FalseClass ] + PERMITTED_TYPES = [ NilClass, String, Integer, Float, BigDecimal, TrueClass, FalseClass ] - # Serializes a set of arguments. Whitelisted types are returned - # as-is. Arrays/Hashes are serialized element by element. - # All other types are serialized using GlobalID. + # Serializes a set of arguments. Intrinsic types that can safely be + # serialized without mutation are returned as-is. Arrays/Hashes are + # serialized element by element. All other types are serialized using + # GlobalID. def serialize(arguments) arguments.map { |argument| serialize_argument(argument) } end - # Deserializes a set of arguments. Whitelisted types are returned - # as-is. Arrays/Hashes are deserialized element by element. - # All other types are deserialized using GlobalID. + # Deserializes a set of arguments. Instrinsic types that can safely be + # deserialized without mutation are returned as-is. Arrays/Hashes are + # deserialized element by element. All other types are deserialized using + # GlobalID. def deserialize(arguments) arguments.map { |argument| deserialize_argument(argument) } rescue @@ -64,7 +66,7 @@ module ActiveJob def serialize_argument(argument) case argument - when *TYPE_WHITELIST + when *PERMITTED_TYPES argument when GlobalID::Identification convert_to_global_id_hash(argument) @@ -88,7 +90,7 @@ module ActiveJob case argument when String GlobalID::Locator.locate(argument) || argument - when *TYPE_WHITELIST + when *PERMITTED_TYPES argument when Array argument.map { |arg| deserialize_argument(arg) } diff --git a/activejob/lib/active_job/exceptions.rb b/activejob/lib/active_job/exceptions.rb index 1e57dbcb1c..9a14c33d80 100644 --- a/activejob/lib/active_job/exceptions.rb +++ b/activejob/lib/active_job/exceptions.rb @@ -44,14 +44,24 @@ module ActiveJob # end def retry_on(*exceptions, wait: 3.seconds, attempts: 5, queue: nil, priority: nil) rescue_from(*exceptions) do |error| + payload = { + job: self, + adapter: self.class.queue_adapter, + error: error, + wait: wait + } + if executions < attempts - logger.error "Retrying #{self.class} in #{wait} seconds, due to a #{error.class}. The original exception was #{error.cause.inspect}." - retry_job wait: determine_delay(wait), queue: queue, priority: priority + ActiveSupport::Notifications.instrument("enqueue_retry.active_job", payload) do + retry_job wait: determine_delay(wait), queue: queue, priority: priority + end else if block_given? - yield self, error + ActiveSupport::Notifications.instrument("retry_stopped.active_job", payload) do + yield self, error + end else - logger.error "Stopped retrying #{self.class} due to a #{error.class}, which reoccurred on #{executions} attempts. The original exception was #{error.cause.inspect}." + ActiveSupport::Notifications.instrument("retry_stopped.active_job", payload) raise error end end @@ -78,10 +88,16 @@ module ActiveJob # end def discard_on(*exceptions) rescue_from(*exceptions) do |error| - if block_given? - yield self, error - else - logger.error "Discarded #{self.class} due to a #{error.class}. The original exception was #{error.cause.inspect}." + payload = { + job: self, + adapter: self.class.queue_adapter, + error: error + } + + ActiveSupport::Notifications.instrument("discard.active_job", payload) do + if block_given? + yield self, error + end end end end diff --git a/activejob/lib/active_job/logging.rb b/activejob/lib/active_job/logging.rb index 9ffd60ad53..96a3e6bf48 100644 --- a/activejob/lib/active_job/logging.rb +++ b/activejob/lib/active_job/logging.rb @@ -88,6 +88,34 @@ module ActiveJob end end + def enqueue_retry(event) + job = event.payload[:job] + ex = event.payload[:error] + wait = event.payload[:wait] + + error do + "Retrying #{job.class} in #{wait} seconds, due to a #{ex.class}. The original exception was #{ex.cause.inspect}." + end + end + + def retry_stopped(event) + job = event.payload[:job] + ex = event.payload[:error] + + error do + "Stopped retrying #{job.class} due to a #{ex.class}, which reoccurred on #{job.executions} attempts. The original exception was #{ex.cause.inspect}." + end + end + + def discard(event) + job = event.payload[:job] + ex = event.payload[:error] + + error do + "Discarded #{job.class} due to a #{ex.class}. The original exception was #{ex.cause.inspect}." + end + end + private def queue_name(event) event.payload[:adapter].class.name.demodulize.remove("Adapter") + "(#{event.payload[:job].queue_name})" diff --git a/activejob/test/cases/logging_test.rb b/activejob/test/cases/logging_test.rb index a1107a07fd..4041f5f8c0 100644 --- a/activejob/test/cases/logging_test.rb +++ b/activejob/test/cases/logging_test.rb @@ -8,9 +8,11 @@ require "jobs/logging_job" require "jobs/overridden_logging_job" require "jobs/nested_job" require "jobs/rescue_job" +require "jobs/retry_job" require "models/person" class LoggingTest < ActiveSupport::TestCase + include ActiveJob::TestHelper include ActiveSupport::LogSubscriber::TestHelper include ActiveSupport::Logger::Severity @@ -59,13 +61,17 @@ class LoggingTest < ActiveSupport::TestCase end def test_uses_job_name_as_tag - LoggingJob.perform_later "Dummy" - assert_match(/\[LoggingJob\]/, @logger.messages) + perform_enqueued_jobs do + LoggingJob.perform_later "Dummy" + assert_match(/\[LoggingJob\]/, @logger.messages) + end end def test_uses_job_id_as_tag - LoggingJob.perform_later "Dummy" - assert_match(/\[LOGGING-JOB-ID\]/, @logger.messages) + perform_enqueued_jobs do + LoggingJob.perform_later "Dummy" + assert_match(/\[LOGGING-JOB-ID\]/, @logger.messages) + end end def test_logs_correct_queue_name @@ -78,19 +84,23 @@ class LoggingTest < ActiveSupport::TestCase end def test_globalid_parameter_logging - person = Person.new(123) - LoggingJob.perform_later person - assert_match(%r{Enqueued.*gid://aj/Person/123}, @logger.messages) - assert_match(%r{Dummy, here is it: #<Person:.*>}, @logger.messages) - assert_match(%r{Performing.*gid://aj/Person/123}, @logger.messages) + perform_enqueued_jobs do + person = Person.new(123) + LoggingJob.perform_later person + assert_match(%r{Enqueued.*gid://aj/Person/123}, @logger.messages) + assert_match(%r{Dummy, here is it: #<Person:.*>}, @logger.messages) + assert_match(%r{Performing.*gid://aj/Person/123}, @logger.messages) + end end def test_globalid_nested_parameter_logging - person = Person.new(123) - LoggingJob.perform_later(person: person) - assert_match(%r{Enqueued.*gid://aj/Person/123}, @logger.messages) - assert_match(%r{Dummy, here is it: .*#<Person:.*>}, @logger.messages) - assert_match(%r{Performing.*gid://aj/Person/123}, @logger.messages) + perform_enqueued_jobs do + person = Person.new(123) + LoggingJob.perform_later(person: person) + assert_match(%r{Enqueued.*gid://aj/Person/123}, @logger.messages) + assert_match(%r{Dummy, here is it: .*#<Person:.*>}, @logger.messages) + assert_match(%r{Performing.*gid://aj/Person/123}, @logger.messages) + end end def test_enqueue_job_logging @@ -102,22 +112,26 @@ class LoggingTest < ActiveSupport::TestCase end def test_perform_job_logging - LoggingJob.perform_later "Dummy" - assert_match(/Performing LoggingJob \(Job ID: .*?\) from .*? with arguments:.*Dummy/, @logger.messages) - assert_match(/Dummy, here is it: Dummy/, @logger.messages) - assert_match(/Performed LoggingJob \(Job ID: .*?\) from .*? in .*ms/, @logger.messages) + perform_enqueued_jobs do + LoggingJob.perform_later "Dummy" + assert_match(/Performing LoggingJob \(Job ID: .*?\) from .*? with arguments:.*Dummy/, @logger.messages) + assert_match(/Dummy, here is it: Dummy/, @logger.messages) + assert_match(/Performed LoggingJob \(Job ID: .*?\) from .*? in .*ms/, @logger.messages) + end end def test_perform_nested_jobs_logging - NestedJob.perform_later - assert_match(/\[LoggingJob\] \[.*?\]/, @logger.messages) - assert_match(/\[ActiveJob\] Enqueued NestedJob \(Job ID: .*\) to/, @logger.messages) - assert_match(/\[ActiveJob\] \[NestedJob\] \[NESTED-JOB-ID\] Performing NestedJob \(Job ID: .*?\) from/, @logger.messages) - assert_match(/\[ActiveJob\] \[NestedJob\] \[NESTED-JOB-ID\] Enqueued LoggingJob \(Job ID: .*?\) to .* with arguments: "NestedJob"/, @logger.messages) - assert_match(/\[ActiveJob\].*\[LoggingJob\] \[LOGGING-JOB-ID\] Performing LoggingJob \(Job ID: .*?\) from .* with arguments: "NestedJob"/, @logger.messages) - assert_match(/\[ActiveJob\].*\[LoggingJob\] \[LOGGING-JOB-ID\] Dummy, here is it: NestedJob/, @logger.messages) - assert_match(/\[ActiveJob\].*\[LoggingJob\] \[LOGGING-JOB-ID\] Performed LoggingJob \(Job ID: .*?\) from .* in/, @logger.messages) - assert_match(/\[ActiveJob\] \[NestedJob\] \[NESTED-JOB-ID\] Performed NestedJob \(Job ID: .*?\) from .* in/, @logger.messages) + perform_enqueued_jobs do + NestedJob.perform_later + assert_match(/\[LoggingJob\] \[.*?\]/, @logger.messages) + assert_match(/\[ActiveJob\] Enqueued NestedJob \(Job ID: .*\) to/, @logger.messages) + assert_match(/\[ActiveJob\] \[NestedJob\] \[NESTED-JOB-ID\] Performing NestedJob \(Job ID: .*?\) from/, @logger.messages) + assert_match(/\[ActiveJob\] \[NestedJob\] \[NESTED-JOB-ID\] Enqueued LoggingJob \(Job ID: .*?\) to .* with arguments: "NestedJob"/, @logger.messages) + assert_match(/\[ActiveJob\].*\[LoggingJob\] \[LOGGING-JOB-ID\] Performing LoggingJob \(Job ID: .*?\) from .* with arguments: "NestedJob"/, @logger.messages) + assert_match(/\[ActiveJob\].*\[LoggingJob\] \[LOGGING-JOB-ID\] Dummy, here is it: NestedJob/, @logger.messages) + assert_match(/\[ActiveJob\].*\[LoggingJob\] \[LOGGING-JOB-ID\] Performed LoggingJob \(Job ID: .*?\) from .* in/, @logger.messages) + assert_match(/\[ActiveJob\] \[NestedJob\] \[NESTED-JOB-ID\] Performed NestedJob \(Job ID: .*?\) from .* in/, @logger.messages) + end end def test_enqueue_at_job_logging @@ -151,4 +165,35 @@ class LoggingTest < ActiveSupport::TestCase assert_match(/Performing RescueJob \(Job ID: .*?\) from .*? with arguments:.*other/, @logger.messages) assert_match(/Error performing RescueJob \(Job ID: .*?\) from .*? in .*ms: RescueJob::OtherError \(Bad hair\):\n.*\brescue_job\.rb:\d+:in `perform'/, @logger.messages) end + + def test_enqueue_retry_logging + perform_enqueued_jobs do + RetryJob.perform_later "DefaultsError", 2 + assert_match(/Retrying RetryJob in \d+ seconds, due to a DefaultsError\. The original exception was nil\./, @logger.messages) + end + end + + def test_retry_stopped_logging + perform_enqueued_jobs do + RetryJob.perform_later "CustomCatchError", 6 + assert_match(/Stopped retrying RetryJob due to a CustomCatchError, which reoccurred on \d+ attempts\. The original exception was #<CustomCatchError: CustomCatchError>\./, @logger.messages) + end + end + + def test_retry_stopped_logging_without_block + perform_enqueued_jobs do + begin + RetryJob.perform_later "DefaultsError", 6 + rescue DefaultsError + assert_match(/Stopped retrying RetryJob due to a DefaultsError, which reoccurred on \d+ attempts\. The original exception was #<DefaultsError: DefaultsError>\./, @logger.messages) + end + end + end + + def test_discard_logging + perform_enqueued_jobs do + RetryJob.perform_later "DiscardableError", 2 + assert_match(/Discarded RetryJob due to a DiscardableError\. The original exception was nil\./, @logger.messages) + end + end end diff --git a/activemodel/lib/active_model/type/helpers/time_value.rb b/activemodel/lib/active_model/type/helpers/time_value.rb index cb6aa67a9d..da56073436 100644 --- a/activemodel/lib/active_model/type/helpers/time_value.rb +++ b/activemodel/lib/active_model/type/helpers/time_value.rb @@ -70,7 +70,13 @@ module ActiveModel # Doesn't handle time zones. def fast_string_to_time(string) if string =~ ISO_DATETIME - microsec = ($7.to_r * 1_000_000).to_i + microsec_part = $7 + if microsec_part && microsec_part.start_with?(".") && microsec_part.length == 7 + microsec_part[0] = "" + microsec = microsec_part.to_i + else + microsec = (microsec_part.to_r * 1_000_000).to_i + end new_time $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, microsec end end diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 71dcecd346..a7c2680015 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,50 @@ +* ActiveRecord::Base.configurations now returns an object. + + ActiveRecord::Base.configurations used to return a hash, but this + is an inflexible data model. In order to improve multiple-database + handling in Rails, we've changed this to return an object. Some methods + are provided to make the object behave hash-like in order to ease the + transition process. Since most applications don't manipulate the hash + we've decided to add backwards-compatible functionality that will throw + a deprecation warning if used, however calling `ActiveRecord::Base.configurations` + will use the new version internally and externally. + + For example, the following database.yml... + + ``` + development: + adapter: sqlite3 + database: db/development.sqlite3 + ``` + + Used to become a hash: + + ``` + { "development" => { "adapter" => "sqlite3", "database" => "db/development.sqlite3" } } + ``` + + Is now converted into the following object: + + ``` + #<ActiveRecord::DatabaseConfigurations:0x00007fd1acbdf800 @configurations=[ + #<ActiveRecord::DatabaseConfigurations::HashConfig:0x00007fd1acbded10 @env_name="development", + @spec_name="primary", @config={"adapter"=>"sqlite3", "database"=>"db/development.sqlite3"}> + ] + ``` + + Iterating over the database configurations has also changed. Instead of + calling hash methods on the `configurations` hash directly, a new method `configs_for` has + been provided that allows you to select the correct configuration. `env_name` is a required + argument, `spec_name` is optional as well as passing a block. These return an array of + database config objects for the requested environment and specification name respectively. + + ``` + ActiveRecord::Base.configurations.configs_for("development") + ActiveRecord::Base.configurations.configs_for("development", "primary") + ``` + + *Eileen M. Uchitelle*, *Aaron Patterson* + * Add database configuration to disable advisory locks. ``` diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index d198466dbf..d43378c64f 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -40,7 +40,6 @@ module ActiveRecord autoload :Core autoload :ConnectionHandling autoload :CounterCache - autoload :DatabaseConfigurations autoload :DynamicMatchers autoload :Enum autoload :InternalMetadata diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index e4b8b1a330..701f19a6ae 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -31,7 +31,7 @@ module ActiveRecord end } - BLACKLISTED_CLASS_METHODS = %w(private public protected allocate new name parent superclass) + RESTRICTED_CLASS_METHODS = %w(private public protected allocate new name parent superclass) class GeneratedAttributeMethods < Module #:nodoc: include Mutex_m @@ -123,7 +123,7 @@ module ActiveRecord # A class method is 'dangerous' if it is already (re)defined by Active Record, but # not by any ancestors. (So 'puts' is not dangerous but 'new' is.) def dangerous_class_method?(method_name) - BLACKLISTED_CLASS_METHODS.include?(method_name.to_s) || class_method_defined_within?(method_name, Base) + RESTRICTED_CLASS_METHODS.include?(method_name.to_s) || class_method_defined_within?(method_name, Base) end def class_method_defined_within?(name, klass, superklass = klass.superclass) # :nodoc: @@ -167,12 +167,14 @@ module ActiveRecord end end - # Regexp whitelist. Matches the following: + # Regexp for column names (with or without a table name prefix). Matches + # the following: # "#{table_name}.#{column_name}" # "#{column_name}" - COLUMN_NAME_WHITELIST = /\A(?:\w+\.)?\w+\z/i + COLUMN_NAME = /\A(?:\w+\.)?\w+\z/i - # Regexp whitelist. Matches the following: + # Regexp for column names with order (with or without a table name + # prefix, with or without various order modifiers). Matches the following: # "#{table_name}.#{column_name}" # "#{table_name}.#{column_name} #{direction}" # "#{table_name}.#{column_name} #{direction} NULLS FIRST" @@ -181,7 +183,7 @@ module ActiveRecord # "#{column_name} #{direction}" # "#{column_name} #{direction} NULLS FIRST" # "#{column_name} NULLS LAST" - COLUMN_NAME_ORDER_WHITELIST = / + COLUMN_NAME_WITH_ORDER = / \A (?:\w+\.)? \w+ @@ -190,12 +192,12 @@ module ActiveRecord \z /ix - def enforce_raw_sql_whitelist(args, whitelist: COLUMN_NAME_WHITELIST) # :nodoc: + def disallow_raw_sql!(args, permit: COLUMN_NAME) # :nodoc: unexpected = args.reject do |arg| arg.kind_of?(Arel::Node) || arg.is_a?(Arel::Nodes::SqlLiteral) || arg.is_a?(Arel::Attributes::Attribute) || - arg.to_s.split(/\s*,\s*/).all? { |part| whitelist.match?(part) } + arg.to_s.split(/\s*,\s*/).all? { |part| permit.match?(part) } end return if unexpected.none? @@ -449,14 +451,6 @@ module ActiveRecord defined?(@attributes) && @attributes.key?(attr_name) end - def attributes_with_values_for_create(attribute_names) - attributes_with_values(attributes_for_create(attribute_names)) - end - - def attributes_with_values_for_update(attribute_names) - attributes_with_values(attributes_for_update(attribute_names)) - end - def attributes_with_values(attribute_names) attribute_names.each_with_object({}) do |name, attrs| attrs[name] = _read_attribute(name) @@ -465,7 +459,8 @@ module ActiveRecord # Filters the primary keys and readonly attributes from the attribute names. def attributes_for_update(attribute_names) - attribute_names.reject do |name| + attribute_names &= self.class.column_names + attribute_names.delete_if do |name| readonly_attribute?(name) end end @@ -473,7 +468,8 @@ module ActiveRecord # Filters out the primary keys, from the attribute names, when the primary # key is to be generated (e.g. the id attribute has no value). def attributes_for_create(attribute_names) - attribute_names.reject do |name| + attribute_names &= self.class.column_names + attribute_names.delete_if do |name| pk_attribute?(name) && id.nil? end end diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index bc25837fab..ebc2252c50 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -164,20 +164,20 @@ module ActiveRecord result end - def _update_record(*) - affected_rows = partial_writes? ? super(keys_for_partial_write) : super + def _update_record(attribute_names = attribute_names_for_partial_writes) + affected_rows = super changes_applied affected_rows end - def _create_record(*) - id = partial_writes? ? super(keys_for_partial_write) : super + def _create_record(attribute_names = attribute_names_for_partial_writes) + id = super changes_applied id end - def keys_for_partial_write - changed_attribute_names_to_save & self.class.column_names + def attribute_names_for_partial_writes + partial_writes? ? changed_attribute_names_to_save : attribute_names end end end diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 5169f312f5..db097cb930 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -22,6 +22,7 @@ require "active_record/explain_subscriber" require "active_record/relation/delegation" require "active_record/attributes" require "active_record/type_caster" +require "active_record/database_configurations" module ActiveRecord #:nodoc: # = Active Record @@ -291,7 +292,6 @@ module ActiveRecord #:nodoc: extend Aggregations::ClassMethods include Core - include DatabaseConfigurations include Persistence include ReadonlyAttributes include ModelSchema diff --git a/activerecord/lib/active_record/connection_adapters/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/connection_specification.rb index 204691006c..2e7a78215a 100644 --- a/activerecord/lib/active_record/connection_adapters/connection_specification.rb +++ b/activerecord/lib/active_record/connection_adapters/connection_specification.rb @@ -114,8 +114,7 @@ module ActiveRecord class Resolver # :nodoc: attr_reader :configurations - # Accepts a hash two layers deep, keys on the first layer represent - # environments such as "production". Keys must be strings. + # Accepts a list of db config objects. def initialize(configurations) @configurations = configurations end @@ -136,33 +135,14 @@ module ActiveRecord # Resolver.new(configurations).resolve(:production) # # => { "host" => "localhost", "database" => "foo", "adapter" => "postgresql" } # - def resolve(config) - if config - resolve_connection config - elsif env = ActiveRecord::ConnectionHandling::RAILS_ENV.call - resolve_symbol_connection env.to_sym + def resolve(config_or_env, pool_name = nil) + if config_or_env + resolve_connection config_or_env, pool_name else raise AdapterNotSpecified end end - # Expands each key in @configurations hash into fully resolved hash - def resolve_all - config = configurations.dup - - if env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call - env_config = config[env] if config[env].is_a?(Hash) && !(config[env].key?("adapter") || config[env].key?("url")) - end - - config.merge! env_config if env_config - - config.each do |key, value| - config[key] = resolve(value) if value - end - - config - end - # Returns an instance of ConnectionSpecification for a given adapter. # Accepts a hash one layer deep that contains all connection information. # @@ -176,7 +156,9 @@ module ActiveRecord # # => { "host" => "localhost", "database" => "foo", "adapter" => "sqlite3" } # def spec(config) - spec = resolve(config).symbolize_keys + pool_name = config if config.is_a?(Symbol) + + spec = resolve(config, pool_name).symbolize_keys raise(AdapterNotSpecified, "database configuration does not specify adapter") unless spec.key?(:adapter) @@ -211,7 +193,6 @@ module ActiveRecord end private - # Returns fully resolved connection, accepts hash, string or symbol. # Always returns a hash. # @@ -232,29 +213,42 @@ module ActiveRecord # Resolver.new({}).resolve_connection("postgresql://localhost/foo") # # => { "host" => "localhost", "database" => "foo", "adapter" => "postgresql" } # - def resolve_connection(spec) - case spec + def resolve_connection(config_or_env, pool_name = nil) + case config_or_env when Symbol - resolve_symbol_connection spec + resolve_symbol_connection config_or_env, pool_name when String - resolve_url_connection spec + resolve_url_connection config_or_env when Hash - resolve_hash_connection spec + resolve_hash_connection config_or_env + else + resolve_connection config_or_env end end - # Takes the environment such as +:production+ or +:development+. + # Takes the environment such as +:production+ or +:development+ and a + # pool name the corresponds to the name given by the connection pool + # to the connection. That pool name is merged into the hash with the + # name key. + # # This requires that the @configurations was initialized with a key that # matches. # - # Resolver.new("production" => {}).resolve_symbol_connection(:production) - # # => {} + # configurations = #<ActiveRecord::DatabaseConfigurations:0x00007fd9fdace3e0 + # @configurations=[ + # #<ActiveRecord::DatabaseConfigurations::HashConfig:0x00007fd9fdace250 + # @env_name="production", @spec_name="primary", @config={"database"=>"my_db"}> + # ]> # - def resolve_symbol_connection(spec) - if config = configurations[spec.to_s] - resolve_connection(config).merge("name" => spec.to_s) + # Resolver.new(configurations).resolve_symbol_connection(:production, "primary") + # # => { "database" => "my_db" } + def resolve_symbol_connection(env_name, pool_name) + db_config = configurations.find_db_config(env_name) + + if db_config + resolve_connection(db_config.config).merge("name" => pool_name.to_s) else - raise(AdapterNotSpecified, "'#{spec}' database is not configured. Available: #{configurations.keys.inspect}") + raise(AdapterNotSpecified, "'#{env_name}' database is not configured. Available: #{configurations.configurations.map(&:env_name).join(", ")}") end end diff --git a/activerecord/lib/active_record/connection_handling.rb b/activerecord/lib/active_record/connection_handling.rb index ee0e651912..18114f9e1c 100644 --- a/activerecord/lib/active_record/connection_handling.rb +++ b/activerecord/lib/active_record/connection_handling.rb @@ -46,45 +46,18 @@ module ActiveRecord # # The exceptions AdapterNotSpecified, AdapterNotFound and +ArgumentError+ # may be returned on an error. - def establish_connection(config = nil) + def establish_connection(config_or_env = nil) raise "Anonymous class is not allowed." unless name - config ||= DEFAULT_ENV.call.to_sym - spec_name = self == Base ? "primary" : name - self.connection_specification_name = spec_name + config_or_env ||= DEFAULT_ENV.call.to_sym + pool_name = self == Base ? "primary" : name + self.connection_specification_name = pool_name resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new(Base.configurations) - spec = resolver.resolve(config).symbolize_keys - spec[:name] = spec_name + config_hash = resolver.resolve(config_or_env, pool_name).symbolize_keys + config_hash[:name] = pool_name - # use the primary config if a config is not passed in and - # it's a three tier config - spec = spec[spec_name.to_sym] if spec[spec_name.to_sym] - - connection_handler.establish_connection(spec) - end - - class MergeAndResolveDefaultUrlConfig # :nodoc: - def initialize(raw_configurations) - @raw_config = raw_configurations.dup - @env = DEFAULT_ENV.call.to_s - end - - # Returns fully resolved connection hashes. - # Merges connection information from `ENV['DATABASE_URL']` if available. - def resolve - ConnectionAdapters::ConnectionSpecification::Resolver.new(config).resolve_all - end - - private - def config - @raw_config.dup.tap do |cfg| - if url = ENV["DATABASE_URL"] - cfg[@env] ||= {} - cfg[@env]["url"] ||= url - end - end - end + connection_handler.establish_connection(config_hash) end # Returns the connection currently associated with the class. This can diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index c983bc0d93..9ec6ba14fd 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -26,7 +26,7 @@ module ActiveRecord ## # Contains the database configuration - as is typically stored in config/database.yml - - # as a Hash. + # as an ActiveRecord::DatabaseConfigurations object. # # For example, the following database.yml... # @@ -40,22 +40,18 @@ module ActiveRecord # # ...would result in ActiveRecord::Base.configurations to look like this: # - # { - # 'development' => { - # 'adapter' => 'sqlite3', - # 'database' => 'db/development.sqlite3' - # }, - # 'production' => { - # 'adapter' => 'sqlite3', - # 'database' => 'db/production.sqlite3' - # } - # } + # #<ActiveRecord::DatabaseConfigurations:0x00007fd1acbdf800 @configurations=[ + # #<ActiveRecord::DatabaseConfigurations::HashConfig:0x00007fd1acbded10 @env_name="development", + # @spec_name="primary", @config={"adapter"=>"sqlite3", "database"=>"db/development.sqlite3"}>, + # #<ActiveRecord::DatabaseConfigurations::HashConfig:0x00007fd1acbdea90 @env_name="production", + # @spec_name="primary", @config={"adapter"=>"mysql2", "database"=>"db/production.sqlite3"}> + # ]> def self.configurations=(config) - @@configurations = ActiveRecord::ConnectionHandling::MergeAndResolveDefaultUrlConfig.new(config).resolve + @@configurations = ActiveRecord::DatabaseConfigurations.new(config) end self.configurations = {} - # Returns fully resolved configurations hash + # Returns fully resolved ActiveRecord::DatabaseConfigurations object def self.configurations @@configurations end diff --git a/activerecord/lib/active_record/database_configurations.rb b/activerecord/lib/active_record/database_configurations.rb index ffeed45030..14b7cb040f 100644 --- a/activerecord/lib/active_record/database_configurations.rb +++ b/activerecord/lib/active_record/database_configurations.rb @@ -1,63 +1,168 @@ # frozen_string_literal: true +require "active_record/database_configurations/database_config" +require "active_record/database_configurations/hash_config" +require "active_record/database_configurations/url_config" + module ActiveRecord - module DatabaseConfigurations # :nodoc: - class DatabaseConfig - attr_reader :env_name, :spec_name, :config + # ActiveRecord::DatabaseConfigurations returns an array of DatabaseConfig + # objects (either a HashConfig or UrlConfig) that are constructed from the + # application's database configuration hash or url string. + class DatabaseConfigurations + attr_reader :configurations + + def initialize(configurations = {}) + @configurations = build_configs(configurations) + end + + # Collects the configs for the environment and optionally the specification + # name passed in. + # + # If a spec name is provided a single DatabaseConfiguration object will be + # returned, otherwise an array of DatabaseConfiguration objects will be + # returned that corresponds with the environment requested. + def configs_for(env = nil, spec = nil, &blk) + configs = env_with_configs(env) - def initialize(env_name, spec_name, config) - @env_name = env_name - @spec_name = spec_name - @config = config + if spec + configs.find do |db_config| + db_config.spec_name == spec + end + else + configs end end - # Selects the config for the specified environment and specification name + # Returns the config hash that corresponds with the environment + # + # If the application has multiple databases `default_hash` will + # the first config hash for the environment. # - # For example if passed :development, and :animals it will select the database - # under the :development and :animals configuration level - def self.config_for_env_and_spec(environment, specification_name, configs = ActiveRecord::Base.configurations) # :nodoc: - configs_for(environment, configs).find do |db_config| - db_config.spec_name == specification_name + # { database: "my_db", adapter: "mysql2" } + def default_hash(env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call.to_s) + default = find_db_config(env) + default.config if default + end + alias :[] :default_hash + + # Returns a single DatabaseConfig object based on the requested environment. + # + # If the application has multiple databases `select_db_config` will return + # the first DatabaseConfig for the environment. + def find_db_config(env) + configurations.find do |db_config| + db_config.env_name == env.to_s || + (db_config.for_current_env? && db_config.spec_name == env.to_s) end end - # Collects the configs for the environment passed in. + # Returns the DatabaseConfig object as a Hash. + def to_h + configs = configurations.reverse.inject({}) do |memo, db_config| + memo.merge(db_config.to_legacy_hash) + end + + Hash[configs.to_a.reverse] + end + + # Checks if the application's configurations are empty. # - # If a block is given returns the specification name and configuration - # otherwise returns an array of DatabaseConfig structs for the environment. - def self.configs_for(env, configs = ActiveRecord::Base.configurations, &blk) # :nodoc: - env_with_configs = db_configs(configs).select do |db_config| - db_config.env_name == env + # Aliased to blank? + def empty? + configurations.empty? + end + alias :blank? :empty? + + private + def env_with_configs(env = nil) + if env + configurations.select { |db_config| db_config.env_name == env } + else + configurations + end + end + + def build_configs(configs) + return configs.configurations if configs.is_a?(DatabaseConfigurations) + + build_db_config = configs.each_pair.flat_map do |env_name, config| + walk_configs(env_name, "primary", config) + end.compact + + if url = ENV["DATABASE_URL"] + build_url_config(url, build_db_config) + else + build_db_config + end end - if block_given? - env_with_configs.each do |env_with_config| - yield env_with_config.spec_name, env_with_config.config + def walk_configs(env_name, spec_name, config) + case config + when String + build_db_config_from_string(env_name, spec_name, config) + when Hash + build_db_config_from_hash(env_name, spec_name, config) end - else - env_with_configs end - end - # Given an env, spec and config creates DatabaseConfig structs with - # each attribute set. - def self.walk_configs(env_name, spec_name, config) # :nodoc: - if config["database"] || config["url"] || config["adapter"] - DatabaseConfig.new(env_name, spec_name, config) - else - config.each_pair.map do |sub_spec_name, sub_config| - walk_configs(env_name, sub_spec_name, sub_config) + def build_db_config_from_string(env_name, spec_name, config) + begin + url = config + uri = URI.parse(url) + if uri.try(:scheme) + ActiveRecord::DatabaseConfigurations::UrlConfig.new(env_name, spec_name, url) + end + rescue URI::InvalidURIError + ActiveRecord::DatabaseConfigurations::HashConfig.new(env_name, spec_name, config) end end - end - # Walks all the configs passed in and returns an array - # of DatabaseConfig structs for each configuration. - def self.db_configs(configs = ActiveRecord::Base.configurations) # :nodoc: - configs.each_pair.flat_map do |env_name, config| - walk_configs(env_name, "primary", config) + def build_db_config_from_hash(env_name, spec_name, config) + if url = config["url"] + config_without_url = config.dup + config_without_url.delete "url" + ActiveRecord::DatabaseConfigurations::UrlConfig.new(env_name, spec_name, url, config_without_url) + elsif config["database"] || (config.size == 1 && config.values.all? { |v| v.is_a? String }) + ActiveRecord::DatabaseConfigurations::HashConfig.new(env_name, spec_name, config) + else + config.each_pair.map do |sub_spec_name, sub_config| + walk_configs(env_name, sub_spec_name, sub_config) + end + end + end + + def build_url_config(url, configs) + env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call.to_s + + if original_config = configs.find(&:for_current_env?) + if original_config.url_config? + configs + else + configs.map do |config| + ActiveRecord::DatabaseConfigurations::UrlConfig.new(env, config.spec_name, url, config.config) + end + end + else + configs + [ActiveRecord::DatabaseConfigurations::UrlConfig.new(env, "primary", url)] + end + end + + def method_missing(method, *args, &blk) + if Hash.method_defined?(method) + ActiveSupport::Deprecation.warn \ + "Returning a hash from ActiveRecord::Base.configurations is deprecated. Therefore calling `#{method}` on the hash is also deprecated. Please switch to using the `configs_for` method instead to collect and iterate over database configurations." + end + + case method + when :each, :first + configurations.send(method, *args, &blk) + when :fetch + configs_for(args.first) + when :values + configurations.map(&:config) + else + super + end end - end end end diff --git a/activerecord/lib/active_record/database_configurations/database_config.rb b/activerecord/lib/active_record/database_configurations/database_config.rb new file mode 100644 index 0000000000..4a58115cd5 --- /dev/null +++ b/activerecord/lib/active_record/database_configurations/database_config.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module ActiveRecord + class DatabaseConfigurations + # ActiveRecord::Base.configurations will return either a HashConfig or + # UrlConfig respectively. It will never return a DatabaseConfig object, + # as this is the parent class for the types of database configuration objects. + class DatabaseConfig # :nodoc: + attr_reader :env_name, :spec_name + + def initialize(env_name, spec_name) + @env_name = env_name + @spec_name = spec_name + end + + def url_config? + false + end + + def to_legacy_hash + { env_name => config } + end + + def for_current_env? + env_name == ActiveRecord::ConnectionHandling::DEFAULT_ENV.call + end + end + end +end diff --git a/activerecord/lib/active_record/database_configurations/hash_config.rb b/activerecord/lib/active_record/database_configurations/hash_config.rb new file mode 100644 index 0000000000..2ee218c730 --- /dev/null +++ b/activerecord/lib/active_record/database_configurations/hash_config.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module ActiveRecord + class DatabaseConfigurations + # A HashConfig object is created for each database configuration entry that + # is created from a hash. + # + # A hash config: + # + # { "development" => { "database" => "db_name" } } + # + # Becomes: + # + # #<ActiveRecord::DatabaseConfigurations::HashConfig:0x00007fd1acbded10 + # @env_name="development", @spec_name="primary", @config={"db_name"}> + # + # Options are: + # + # <tt>:env_name</tt> - The Rails environment, ie "development" + # <tt>:spec_name</tt> - The specification name. In a standard two-tier + # database configuration this will default to "primary". In a multiple + # database three-tier database configuration this corresponds to the name + # used in the second tier, for example "primary_readonly". + # <tt>:config</tt> - The config hash. This is the hash that contains the + # database adapter, name, and other important information for database + # connections. + class HashConfig < DatabaseConfig + attr_reader :config + + def initialize(env_name, spec_name, config) + super(env_name, spec_name) + @config = config + end + end + end +end diff --git a/activerecord/lib/active_record/database_configurations/url_config.rb b/activerecord/lib/active_record/database_configurations/url_config.rb new file mode 100644 index 0000000000..c3d9798c37 --- /dev/null +++ b/activerecord/lib/active_record/database_configurations/url_config.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +module ActiveRecord + class DatabaseConfigurations + # A UrlConfig object is created for each database configuration + # entry that is created from a URL. This can either be a URL string + # or a hash with a URL in place of the config hash. + # + # A URL config: + # + # postgres://localhost/foo + # + # Becomes: + # + # #<ActiveRecord::DatabaseConfigurations::UrlConfig:0x00007fdc3238f340 + # @env_name="default_env", @spec_name="primary", + # @config={"adapter"=>"postgresql", "database"=>"foo", "host"=>"localhost"}, + # @url="postgres://localhost/foo"> + # + # Options are: + # + # <tt>:env_name</tt> - The Rails environment, ie "development" + # <tt>:spec_name</tt> - The specification name. In a standard two-tier + # database configuration this will default to "primary". In a multiple + # database three-tier database configuration this corresponds to the name + # used in the second tier, for example "primary_readonly". + # <tt>:url</tt> - The database URL. + # <tt>:config</tt> - The config hash. This is the hash that contains the + # database adapter, name, and other important information for database + # connections. + class UrlConfig < DatabaseConfig + attr_reader :url, :config + + def initialize(env_name, spec_name, url, config = {}) + super(env_name, spec_name) + @config = build_config(config, url) + @url = url + end + + def url_config? # :nodoc: + true + end + + private + def build_config(original_config, url) + if /^jdbc:/.match?(url) + hash = { "url" => url } + else + hash = ActiveRecord::ConnectionAdapters::ConnectionSpecification::ConnectionUrlResolver.new(url).to_hash + end + + if original_config[env_name] + original_config[env_name].merge(hash) + else + original_config.merge(hash) + end + end + end + end +end diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index 05963e5546..24af56cf0b 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -718,7 +718,6 @@ module ActiveRecord # Updates the associated record with values matching those of the instance attributes. # Returns the number of affected rows. def _update_record(attribute_names = self.attribute_names) - attribute_names &= self.class.column_names attribute_names = attributes_for_update(attribute_names) if attribute_names.empty? @@ -737,10 +736,12 @@ module ActiveRecord # Creates a record with values matching those of the instance attributes # and returns its id. def _create_record(attribute_names = self.attribute_names) - attribute_names &= self.class.column_names - attributes_values = attributes_with_values_for_create(attribute_names) + attribute_names = attributes_for_create(attribute_names) + + new_id = self.class._insert_record( + attributes_with_values(attribute_names) + ) - new_id = self.class._insert_record(attributes_values) self.id ||= new_id if self.class.primary_key @new_record = false diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index 8b7d18fb3d..15b0459422 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -26,7 +26,7 @@ db_namespace = namespace :db do ActiveRecord::Tasks::DatabaseTasks.for_each do |spec_name| desc "Create #{spec_name} database for current environment" task spec_name => :load_config do - db_config = ActiveRecord::DatabaseConfigurations.config_for_env_and_spec(Rails.env, spec_name) + db_config = ActiveRecord::Base.configurations.configs_for(Rails.env, spec_name) ActiveRecord::Tasks::DatabaseTasks.create(db_config.config) end end @@ -45,7 +45,7 @@ db_namespace = namespace :db do ActiveRecord::Tasks::DatabaseTasks.for_each do |spec_name| desc "Drop #{spec_name} database for current environment" task spec_name => [:load_config, :check_protected_environments] do - db_config = ActiveRecord::DatabaseConfigurations.config_for_env_and_spec(Rails.env, spec_name) + db_config = ActiveRecord::Base.configurations.configs_for(Rails.env, spec_name) ActiveRecord::Tasks::DatabaseTasks.drop(db_config.config) end end @@ -73,8 +73,8 @@ db_namespace = namespace :db do desc "Migrate the database (options: VERSION=x, VERBOSE=false, SCOPE=blog)." task migrate: :load_config do - ActiveRecord::DatabaseConfigurations.configs_for(Rails.env) do |spec_name, config| - ActiveRecord::Base.establish_connection(config) + ActiveRecord::Base.configurations.configs_for(Rails.env).each do |db_config| + ActiveRecord::Base.establish_connection(db_config.config) ActiveRecord::Tasks::DatabaseTasks.migrate end db_namespace["_dump"].invoke @@ -99,7 +99,7 @@ db_namespace = namespace :db do ActiveRecord::Tasks::DatabaseTasks.for_each do |spec_name| desc "Migrate #{spec_name} database for current environment" task spec_name => :load_config do - db_config = ActiveRecord::DatabaseConfigurations.config_for_env_and_spec(Rails.env, spec_name) + db_config = ActiveRecord::Base.configurations.configs_for(Rails.env, spec_name) ActiveRecord::Base.establish_connection(db_config.config) ActiveRecord::Tasks::DatabaseTasks.migrate end @@ -274,11 +274,10 @@ db_namespace = namespace :db do desc "Creates a db/schema.rb file that is portable against any DB supported by Active Record" task dump: :load_config do require "active_record/schema_dumper" - - ActiveRecord::DatabaseConfigurations.configs_for(Rails.env) do |spec_name, config| - filename = ActiveRecord::Tasks::DatabaseTasks.dump_filename(spec_name, :ruby) + ActiveRecord::Base.configurations.configs_for(Rails.env).each do |db_config| + filename = ActiveRecord::Tasks::DatabaseTasks.dump_filename(db_config.spec_name, :ruby) File.open(filename, "w:utf-8") do |file| - ActiveRecord::Base.establish_connection(config) + ActiveRecord::Base.establish_connection(db_config.config) ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, file) end end @@ -314,11 +313,10 @@ db_namespace = namespace :db do namespace :structure do desc "Dumps the database structure to db/structure.sql. Specify another file with SCHEMA=db/my_structure.sql" task dump: :load_config do - ActiveRecord::DatabaseConfigurations.configs_for(Rails.env) do |spec_name, config| - ActiveRecord::Base.establish_connection(config) - filename = ActiveRecord::Tasks::DatabaseTasks.dump_filename(spec_name, :sql) - ActiveRecord::Tasks::DatabaseTasks.structure_dump(config, filename) - + ActiveRecord::Base.configurations.configs_for(Rails.env).each do |db_config| + ActiveRecord::Base.establish_connection(db_config.config) + filename = ActiveRecord::Tasks::DatabaseTasks.dump_filename(db_config.spec_name, :sql) + ActiveRecord::Tasks::DatabaseTasks.structure_dump(db_config.config, filename) if ActiveRecord::SchemaMigration.table_exists? File.open(filename, "a") do |f| f.puts ActiveRecord::Base.connection.dump_schema_information @@ -356,22 +354,30 @@ db_namespace = namespace :db do begin should_reconnect = ActiveRecord::Base.connection_pool.active_connection? ActiveRecord::Schema.verbose = false - ActiveRecord::Tasks::DatabaseTasks.load_schema ActiveRecord::Base.configurations["test"], :ruby, ENV["SCHEMA"], "test" + ActiveRecord::Base.configurations.configs_for("test").each do |db_config| + filename = ActiveRecord::Tasks::DatabaseTasks.dump_filename(db_config.spec_name, :ruby) + ActiveRecord::Tasks::DatabaseTasks.load_schema(db_config.config, :ruby, filename, "test") + end ensure if should_reconnect - ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations[ActiveRecord::Tasks::DatabaseTasks.env]) + ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations.default_hash(ActiveRecord::Tasks::DatabaseTasks.env)) end end end # desc "Recreate the test database from an existent structure.sql file" task load_structure: %w(db:test:purge) do - ActiveRecord::Tasks::DatabaseTasks.load_schema ActiveRecord::Base.configurations["test"], :sql, ENV["SCHEMA"], "test" + ActiveRecord::Base.configurations.configs_for("test").each do |db_config| + filename = ActiveRecord::Tasks::DatabaseTasks.dump_filename(db_config.spec_name, :sql) + ActiveRecord::Tasks::DatabaseTasks.load_schema(db_config.config, :sql, filename, "test") + end end # desc "Empty the test database" task purge: %w(load_config check_protected_environments) do - ActiveRecord::Tasks::DatabaseTasks.purge ActiveRecord::Base.configurations["test"] + ActiveRecord::Base.configurations.configs_for("test").each do |db_config| + ActiveRecord::Tasks::DatabaseTasks.purge(db_config.config) + end end # desc 'Load the test schema' diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 6d2f75a3ae..b2110f727c 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -612,9 +612,21 @@ module ActiveRecord # returns either +nil+ or the inverse association name that it finds. def automatic_inverse_of - if can_find_inverse_of_automatically?(self) - inverse_name = ActiveSupport::Inflector.underscore(options[:as] || active_record.name.demodulize).to_sym + return unless can_find_inverse_of_automatically?(self) + inverse_name_candidates = + if options[:as] + [options[:as]] + else + active_record_name = active_record.name.demodulize + [active_record_name, ActiveSupport::Inflector.pluralize(active_record_name)] + end + + inverse_name_candidates.map! do |candidate| + ActiveSupport::Inflector.underscore(candidate).to_sym + end + + inverse_name_candidates.detect do |inverse_name| begin reflection = klass._reflect_on_association(inverse_name) rescue NameError @@ -623,9 +635,7 @@ module ActiveRecord reflection = false end - if valid_inverse_reflection?(reflection) - return inverse_name - end + valid_inverse_reflection?(reflection) end end diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 2d3e1eaa08..29a3ceab7d 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -375,8 +375,12 @@ module ActiveRecord @klass.connection.update stmt, "#{@klass} Update All" end - def update(attributes) # :nodoc: - each { |record| record.update(attributes) } + def update(id = :all, attributes) # :nodoc: + if id == :all + each { |record| record.update(attributes) } + else + klass.update(id, attributes) + end end def update_counters(counters) # :nodoc: diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index 40fe39fa9d..0fa5ba2e50 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -190,7 +190,7 @@ module ActiveRecord relation = apply_join_dependency relation.pluck(*column_names) else - enforce_raw_sql_whitelist(column_names) + disallow_raw_sql!(column_names) relation = spawn relation.select_values = column_names.map { |cn| @klass.has_attribute?(cn) || @klass.attribute_alias?(cn) ? arel_attribute(cn) : cn diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 52405f21a1..56497e11cb 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -1133,9 +1133,9 @@ module ActiveRecord end order_args.flatten! - @klass.enforce_raw_sql_whitelist( + @klass.disallow_raw_sql!( order_args.flat_map { |a| a.is_a?(Hash) ? a.keys : a }, - whitelist: AttributeMethods::ClassMethods::COLUMN_NAME_ORDER_WHITELIST + permit: AttributeMethods::ClassMethods::COLUMN_NAME_WITH_ORDER ) validate_order_args(order_args) diff --git a/activerecord/lib/active_record/result.rb b/activerecord/lib/active_record/result.rb index 7f1c2fd7eb..3b2556b1c8 100644 --- a/activerecord/lib/active_record/result.rb +++ b/activerecord/lib/active_record/result.rb @@ -140,6 +140,8 @@ module ActiveRecord # We freeze the strings to prevent them getting duped when # used as keys in ActiveRecord::Base's @attributes hash columns = @columns.map(&:-@) + length = columns.length + @rows.map { |row| # In the past we used Hash[columns.zip(row)] # though elegant, the verbose way is much more efficient @@ -148,8 +150,6 @@ module ActiveRecord hash = {} index = 0 - length = columns.length - while index < length hash[columns[index]] = row[index] index += 1 diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb index c6c268855e..3485d9e557 100644 --- a/activerecord/lib/active_record/sanitization.rb +++ b/activerecord/lib/active_record/sanitization.rb @@ -61,8 +61,8 @@ module ActiveRecord # # => "id ASC" def sanitize_sql_for_order(condition) if condition.is_a?(Array) && condition.first.to_s.include?("?") - enforce_raw_sql_whitelist([condition.first], - whitelist: AttributeMethods::ClassMethods::COLUMN_NAME_ORDER_WHITELIST + disallow_raw_sql!([condition.first], + permit: AttributeMethods::ClassMethods::COLUMN_NAME_WITH_ORDER ) # Ensure we aren't dealing with a subclass of String that might diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb index fd36c0abd2..3a1791f9c7 100644 --- a/activerecord/lib/active_record/tasks/database_tasks.rb +++ b/activerecord/lib/active_record/tasks/database_tasks.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "active_record/database_configurations" + module ActiveRecord module Tasks # :nodoc: class DatabaseAlreadyExists < StandardError; end # :nodoc: @@ -101,16 +103,21 @@ module ActiveRecord @env ||= Rails.env end + def spec + @spec ||= "primary" + end + def seed_loader @seed_loader ||= Rails.application end def current_config(options = {}) options.reverse_merge! env: env + options[:spec] ||= "primary" if options.has_key?(:config) @current_config = options[:config] else - @current_config ||= ActiveRecord::Base.configurations[options[:env]] + @current_config ||= ActiveRecord::Base.configurations.configs_for(options[:env], options[:spec]).config end end @@ -122,7 +129,7 @@ module ActiveRecord $stderr.puts "Database '#{configuration['database']}' already exists" if verbose? rescue Exception => error $stderr.puts error - $stderr.puts "Couldn't create database for #{configuration.inspect}" + $stderr.puts "Couldn't create '#{configuration['database']}' database. Please check your configuration." raise end @@ -135,8 +142,8 @@ module ActiveRecord end def for_each - databases = Rails.application.config.load_database_yaml - database_configs = ActiveRecord::DatabaseConfigurations.configs_for(Rails.env, databases) + databases = Rails.application.config.database_configuration + database_configs = ActiveRecord::DatabaseConfigurations.new(databases).configs_for(Rails.env) # if this is a single database application we don't want tasks for each primary database return if database_configs.count == 1 @@ -180,9 +187,11 @@ module ActiveRecord scope = ENV["SCOPE"] verbose_was, Migration.verbose = Migration.verbose, verbose? + Base.connection.migration_context.migrate(target_version) do |migration| scope.blank? || scope == migration.scope end + ActiveRecord::Base.clear_cache! ensure Migration.verbose = verbose_was @@ -198,8 +207,8 @@ module ActiveRecord ENV["VERSION"].to_i if ENV["VERSION"] && !ENV["VERSION"].empty? end - def charset_current(environment = env) - charset ActiveRecord::Base.configurations[environment] + def charset_current(environment = env, specification_name = spec) + charset ActiveRecord::Base.configurations.configs_for(environment, specification_name).config end def charset(*arguments) @@ -207,8 +216,8 @@ module ActiveRecord class_for_adapter(configuration["adapter"]).new(*arguments).charset end - def collation_current(environment = env) - collation ActiveRecord::Base.configurations[environment] + def collation_current(environment = env, specification_name = spec) + collation ActiveRecord::Base.configurations.configs_for(environment, specification_name).config end def collation(*arguments) @@ -342,14 +351,15 @@ module ActiveRecord environments << "test" if environment == "development" environments.each do |env| - ActiveRecord::DatabaseConfigurations.configs_for(env) do |spec_name, configuration| - yield configuration, spec_name, env + ActiveRecord::Base.configurations.configs_for(env).each do |db_config| + yield db_config.config, db_config.spec_name, env end end end def each_local_configuration - ActiveRecord::Base.configurations.each_value do |configuration| + ActiveRecord::Base.configurations.configs_for.each do |db_config| + configuration = db_config.config next unless configuration["database"] if local_database?(configuration) diff --git a/activerecord/lib/active_record/test_databases.rb b/activerecord/lib/active_record/test_databases.rb index 5b4efe22c9..16113eb04e 100644 --- a/activerecord/lib/active_record/test_databases.rb +++ b/activerecord/lib/active_record/test_databases.rb @@ -5,31 +5,32 @@ require "active_support/testing/parallelization" module ActiveRecord module TestDatabases # :nodoc: ActiveSupport::Testing::Parallelization.after_fork_hook do |i| - create_and_load_schema(i, spec_name: Rails.env) + create_and_load_schema(i, env_name: Rails.env) end - ActiveSupport::Testing::Parallelization.run_cleanup_hook do |_| - drop(spec_name: Rails.env) + ActiveSupport::Testing::Parallelization.run_cleanup_hook do + drop(env_name: Rails.env) end - def self.create_and_load_schema(i, spec_name:) + def self.create_and_load_schema(i, env_name:) old, ENV["VERBOSE"] = ENV["VERBOSE"], "false" - connection_spec = ActiveRecord::Base.configurations[spec_name] - - connection_spec["database"] += "-#{i}" - ActiveRecord::Tasks::DatabaseTasks.create(connection_spec) - ActiveRecord::Tasks::DatabaseTasks.load_schema(connection_spec) + ActiveRecord::Base.configurations.configs_for(env_name).each do |db_config| + db_config.config["database"] += "-#{i}" + ActiveRecord::Tasks::DatabaseTasks.create(db_config.config) + ActiveRecord::Tasks::DatabaseTasks.load_schema(db_config.config, ActiveRecord::Base.schema_format, nil, env_name, db_config.spec_name) + end ensure - ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations[Rails.env]) + ActiveRecord::Base.establish_connection(Rails.env.to_sym) ENV["VERBOSE"] = old end - def self.drop(spec_name:) + def self.drop(env_name:) old, ENV["VERBOSE"] = ENV["VERBOSE"], "false" - connection_spec = ActiveRecord::Base.configurations[spec_name] - ActiveRecord::Tasks::DatabaseTasks.drop(connection_spec) + ActiveRecord::Base.configurations.configs_for(env_name).each do |db_config| + ActiveRecord::Tasks::DatabaseTasks.drop(db_config.config) + end ensure ENV["VERBOSE"] = old end diff --git a/activerecord/lib/rails/generators/active_record/migration.rb b/activerecord/lib/rails/generators/active_record/migration.rb index 4ceb502c5d..4a17082d66 100644 --- a/activerecord/lib/rails/generators/active_record/migration.rb +++ b/activerecord/lib/rails/generators/active_record/migration.rb @@ -24,7 +24,9 @@ module ActiveRecord end def db_migrate_path - if defined?(Rails.application) && Rails.application + if migrations_paths = options[:migrations_paths] + migrations_paths + elsif defined?(Rails.application) && Rails.application Rails.application.config.paths["db/migrate"].to_ary.first else "db/migrate" diff --git a/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb b/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb index a07b00ef79..281b7afb50 100644 --- a/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb +++ b/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb @@ -8,6 +8,7 @@ module ActiveRecord argument :attributes, type: :array, default: [], banner: "field[:type][:index] field[:type][:index]" class_option :primary_key_type, type: :string, desc: "The type for primary key" + class_option :migrations_paths, type: :string, desc: "The migration path for your generated migrations. If this is not set it will default to db/migrate" def create_migration_file set_local_assigns! diff --git a/activerecord/test/cases/associations/inverse_associations_test.rb b/activerecord/test/cases/associations/inverse_associations_test.rb index da3a42e2b5..eb4dc73423 100644 --- a/activerecord/test/cases/associations/inverse_associations_test.rb +++ b/activerecord/test/cases/associations/inverse_associations_test.rb @@ -20,6 +20,8 @@ require "models/company" require "models/project" require "models/author" require "models/post" +require "models/department" +require "models/hotel" class AutomaticInverseFindingTests < ActiveRecord::TestCase fixtures :ratings, :comments, :cars @@ -724,6 +726,16 @@ class InversePolymorphicBelongsToTests < ActiveRecord::TestCase # fails because Interest does have the correct inverse_of assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Face.first.polymorphic_man = Interest.first } end + + def test_favors_has_one_associations_for_inverse_of + inverse_name = Post.reflect_on_association(:author).inverse_of.name + assert_equal :post, inverse_name + end + + def test_finds_inverse_of_for_plural_associations + inverse_name = Department.reflect_on_association(:hotel).inverse_of.name + assert_equal :departments, inverse_name + end end # NOTE - these tests might not be meaningful, ripped as they were from the parental_control plugin diff --git a/activerecord/test/cases/connection_adapters/connection_handler_test.rb b/activerecord/test/cases/connection_adapters/connection_handler_test.rb index b8e623f17b..8c204a2692 100644 --- a/activerecord/test/cases/connection_adapters/connection_handler_test.rb +++ b/activerecord/test/cases/connection_adapters/connection_handler_test.rb @@ -28,13 +28,16 @@ module ActiveRecord end def test_establish_connection_uses_spec_name + old_config = ActiveRecord::Base.configurations config = { "readonly" => { "adapter" => "sqlite3" } } - resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new(config) + ActiveRecord::Base.configurations = config + resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new(ActiveRecord::Base.configurations) spec = resolver.spec(:readonly) @handler.establish_connection(spec.to_hash) assert_not_nil @handler.retrieve_connection_pool("readonly") ensure + ActiveRecord::Base.configurations = old_config @handler.remove_connection("readonly") end 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 1b64324cc4..06c1c51724 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 @@ -18,11 +18,14 @@ module ActiveRecord end def resolve_config(config) - ActiveRecord::ConnectionHandling::MergeAndResolveDefaultUrlConfig.new(config).resolve + configs = ActiveRecord::DatabaseConfigurations.new(config) + configs.to_h end def resolve_spec(spec, config) - ConnectionSpecification::Resolver.new(resolve_config(config)).resolve(spec) + configs = ActiveRecord::DatabaseConfigurations.new(config) + resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new(configs) + resolver.resolve(spec, spec) end def test_resolver_with_database_uri_and_current_env_symbol_key diff --git a/activerecord/test/cases/connection_specification/resolver_test.rb b/activerecord/test/cases/connection_specification/resolver_test.rb index 5b80f16a44..72be14f507 100644 --- a/activerecord/test/cases/connection_specification/resolver_test.rb +++ b/activerecord/test/cases/connection_specification/resolver_test.rb @@ -7,11 +7,15 @@ module ActiveRecord class ConnectionSpecification class ResolverTest < ActiveRecord::TestCase def resolve(spec, config = {}) - Resolver.new(config).resolve(spec) + configs = ActiveRecord::DatabaseConfigurations.new(config) + resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new(configs) + resolver.resolve(spec, spec) end def spec(spec, config = {}) - Resolver.new(config).spec(spec) + configs = ActiveRecord::DatabaseConfigurations.new(config) + resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new(configs) + resolver.spec(spec) end def test_url_invalid_adapter diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb index 1f0e770a93..b1ebd20d6b 100644 --- a/activerecord/test/cases/dirty_test.rb +++ b/activerecord/test/cases/dirty_test.rb @@ -567,8 +567,6 @@ class DirtyTest < ActiveRecord::TestCase assert_not_nil pirate.previous_changes["updated_on"][1] assert_not pirate.previous_changes.key?("parrot_id") assert_not pirate.previous_changes.key?("created_on") - ensure - travel_back end class Testings < ActiveRecord::Base; end diff --git a/activerecord/test/cases/explain_subscriber_test.rb b/activerecord/test/cases/explain_subscriber_test.rb index 82cc891970..79a0630193 100644 --- a/activerecord/test/cases/explain_subscriber_test.rb +++ b/activerecord/test/cases/explain_subscriber_test.rb @@ -40,7 +40,7 @@ if ActiveRecord::Base.connection.supports_explain? assert_equal binds, queries[0][1] end - def test_collects_nothing_if_the_statement_is_not_whitelisted + def test_collects_nothing_if_the_statement_is_not_explainable SUBSCRIBER.finish(nil, nil, name: "SQL", sql: "SHOW max_identifier_length") assert_empty queries end diff --git a/activerecord/test/cases/legacy_configurations_test.rb b/activerecord/test/cases/legacy_configurations_test.rb new file mode 100644 index 0000000000..c36feb5116 --- /dev/null +++ b/activerecord/test/cases/legacy_configurations_test.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require "cases/helper" + +module ActiveRecord + class LegacyConfigurationsTest < ActiveRecord::TestCase + def test_can_turn_configurations_into_a_hash + assert ActiveRecord::Base.configurations.to_h.is_a?(Hash), "expected to be a hash but was not." + assert_equal ["arunit", "arunit2", "arunit_without_prepared_statements"].sort, ActiveRecord::Base.configurations.to_h.keys.sort + end + + def test_each_is_deprecated + assert_deprecated do + ActiveRecord::Base.configurations.each do |db_config| + assert_equal "primary", db_config.spec_name + end + end + end + + def test_first_is_deprecated + assert_deprecated do + db_config = ActiveRecord::Base.configurations.first + assert_equal "arunit", db_config.env_name + assert_equal "primary", db_config.spec_name + end + end + + def test_fetch_is_deprecated + assert_deprecated do + db_config = ActiveRecord::Base.configurations.fetch("arunit").first + assert_equal "arunit", db_config.env_name + assert_equal "primary", db_config.spec_name + end + end + + def test_values_are_deprecated + config_hashes = ActiveRecord::Base.configurations.configurations.map(&:config) + assert_deprecated do + assert_equal config_hashes, ActiveRecord::Base.configurations.values + end + end + end +end diff --git a/activerecord/test/cases/relation/delegation_test.rb b/activerecord/test/cases/relation/delegation_test.rb index 3f3d41980c..a8030c2d64 100644 --- a/activerecord/test/cases/relation/delegation_test.rb +++ b/activerecord/test/cases/relation/delegation_test.rb @@ -5,7 +5,7 @@ require "models/post" require "models/comment" module ActiveRecord - module DelegationWhitelistTests + module ArrayDelegationTests ARRAY_DELEGATES = [ :+, :-, :|, :&, :[], :shuffle, :all?, :collect, :compact, :detect, :each, :each_cons, :each_with_index, @@ -38,7 +38,7 @@ module ActiveRecord end class DelegationAssociationTest < ActiveRecord::TestCase - include DelegationWhitelistTests + include ArrayDelegationTests include DeprecatedArelDelegationTests def target @@ -47,7 +47,7 @@ module ActiveRecord end class DelegationRelationTest < ActiveRecord::TestCase - include DelegationWhitelistTests + include ArrayDelegationTests include DeprecatedArelDelegationTests def target diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index 5412ab5def..d03b412efb 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -1587,6 +1587,24 @@ class RelationTest < ActiveRecord::TestCase assert_equal "David", topic2.reload.author_name end + def test_update_with_ids_on_relation + topic1 = TopicWithCallbacks.create!(title: "arel", author_name: nil) + topic2 = TopicWithCallbacks.create!(title: "activerecord", author_name: nil) + topics = TopicWithCallbacks.none + topics.update( + [topic1.id, topic2.id], + [{ title: "adequaterecord" }, { title: "adequaterecord" }] + ) + + assert_equal TopicWithCallbacks.count, TopicWithCallbacks.topic_count + + assert_equal "adequaterecord", topic1.reload.title + assert_equal "adequaterecord", topic2.reload.title + # Testing that the before_update callbacks have run + assert_equal "David", topic1.reload.author_name + assert_equal "David", topic2.reload.author_name + end + def test_update_on_relation_passing_active_record_object_is_not_permitted topic = Topic.create!(title: "Foo", author_name: nil) assert_raises(ArgumentError) do diff --git a/activerecord/test/cases/tasks/database_tasks_test.rb b/activerecord/test/cases/tasks/database_tasks_test.rb index ee53d576a1..a39a7371fe 100644 --- a/activerecord/test/cases/tasks/database_tasks_test.rb +++ b/activerecord/test/cases/tasks/database_tasks_test.rb @@ -156,20 +156,24 @@ module ActiveRecord class DatabaseTasksCreateAllTest < ActiveRecord::TestCase def setup + @old_configurations = ActiveRecord::Base.configurations @configurations = { "development" => { "database" => "my-db" } } $stdout, @original_stdout = StringIO.new, $stdout $stderr, @original_stderr = StringIO.new, $stderr + + ActiveRecord::Base.configurations = @configurations end def teardown $stdout, $stderr = @original_stdout, @original_stderr + ActiveRecord::Base.configurations = @old_configurations end def test_ignores_configurations_without_databases @configurations["development"]["database"] = nil - with_stubbed_configurations_establish_connection do + ActiveRecord::Base.configurations do assert_not_called(ActiveRecord::Tasks::DatabaseTasks, :create) do ActiveRecord::Tasks::DatabaseTasks.create_all end @@ -179,7 +183,7 @@ module ActiveRecord def test_ignores_remote_databases @configurations["development"]["host"] = "my.server.tld" - with_stubbed_configurations_establish_connection do + ActiveRecord::Base.configurations do assert_not_called(ActiveRecord::Tasks::DatabaseTasks, :create) do ActiveRecord::Tasks::DatabaseTasks.create_all end @@ -189,7 +193,7 @@ module ActiveRecord def test_warning_for_remote_databases @configurations["development"]["host"] = "my.server.tld" - with_stubbed_configurations_establish_connection do + ActiveRecord::Base.configurations do ActiveRecord::Tasks::DatabaseTasks.create_all assert_match "This task only modifies local databases. my-db is on a remote host.", @@ -200,7 +204,7 @@ module ActiveRecord def test_creates_configurations_with_local_ip @configurations["development"]["host"] = "127.0.0.1" - with_stubbed_configurations_establish_connection do + ActiveRecord::Base.configurations do assert_called(ActiveRecord::Tasks::DatabaseTasks, :create) do ActiveRecord::Tasks::DatabaseTasks.create_all end @@ -210,7 +214,7 @@ module ActiveRecord def test_creates_configurations_with_local_host @configurations["development"]["host"] = "localhost" - with_stubbed_configurations_establish_connection do + ActiveRecord::Base.configurations do assert_called(ActiveRecord::Tasks::DatabaseTasks, :create) do ActiveRecord::Tasks::DatabaseTasks.create_all end @@ -220,40 +224,33 @@ module ActiveRecord def test_creates_configurations_with_blank_hosts @configurations["development"]["host"] = nil - with_stubbed_configurations_establish_connection do + ActiveRecord::Base.configurations do assert_called(ActiveRecord::Tasks::DatabaseTasks, :create) do ActiveRecord::Tasks::DatabaseTasks.create_all end end end - - private - - def with_stubbed_configurations_establish_connection - ActiveRecord::Base.stub(:configurations, @configurations) do - # To refrain from connecting to a newly created empty DB in - # sqlite3_mem tests - ActiveRecord::Base.connection_handler.stub( - :establish_connection, - nil - ) do - yield - end - end - end end class DatabaseTasksCreateCurrentTest < ActiveRecord::TestCase def setup + @old_configurations = ActiveRecord::Base.configurations + @configurations = { "development" => { "database" => "dev-db" }, "test" => { "database" => "test-db" }, - "production" => { "url" => "prod-db-url" } + "production" => { "url" => "abstract://prod-db-url" } } + + ActiveRecord::Base.configurations = @configurations + end + + def teardown + ActiveRecord::Base.configurations = @old_configurations end def test_creates_current_environment_database - with_stubbed_configurations_establish_connection do + ActiveRecord::Base.configurations do assert_called_with( ActiveRecord::Tasks::DatabaseTasks, :create, @@ -267,7 +264,7 @@ module ActiveRecord end def test_creates_current_environment_database_with_url - with_stubbed_configurations_establish_connection do + ActiveRecord::Base.configurations do assert_called_with( ActiveRecord::Tasks::DatabaseTasks, :create, @@ -281,7 +278,7 @@ module ActiveRecord end def test_creates_test_and_development_databases_when_env_was_not_specified - with_stubbed_configurations_establish_connection do + ActiveRecord::Base.configurations do assert_called_with( ActiveRecord::Tasks::DatabaseTasks, :create, @@ -301,7 +298,7 @@ module ActiveRecord old_env = ENV["RAILS_ENV"] ENV["RAILS_ENV"] = "development" - with_stubbed_configurations_establish_connection do + ActiveRecord::Base.configurations do assert_called_with( ActiveRecord::Tasks::DatabaseTasks, :create, @@ -328,29 +325,27 @@ module ActiveRecord end end end - - private - - def with_stubbed_configurations_establish_connection - ActiveRecord::Base.stub(:configurations, @configurations) do - ActiveRecord::Base.stub(:establish_connection, nil) do - yield - end - end - end end class DatabaseTasksCreateCurrentThreeTierTest < ActiveRecord::TestCase def setup + @old_configurations = ActiveRecord::Base.configurations + @configurations = { "development" => { "primary" => { "database" => "dev-db" }, "secondary" => { "database" => "secondary-dev-db" } }, "test" => { "primary" => { "database" => "test-db" }, "secondary" => { "database" => "secondary-test-db" } }, - "production" => { "primary" => { "url" => "prod-db-url" }, "secondary" => { "url" => "secondary-prod-db-url" } } + "production" => { "primary" => { "url" => "abstract://prod-db-url" }, "secondary" => { "url" => "abstract://secondary-prod-db-url" } } } + + ActiveRecord::Base.configurations = @configurations + end + + def teardown + ActiveRecord::Base.configurations = @old_configurations end def test_creates_current_environment_database - with_stubbed_configurations_establish_connection do + ActiveRecord::Base.configurations do assert_called_with( ActiveRecord::Tasks::DatabaseTasks, :create, @@ -367,7 +362,7 @@ module ActiveRecord end def test_creates_current_environment_database_with_url - with_stubbed_configurations_establish_connection do + ActiveRecord::Base.configurations do assert_called_with( ActiveRecord::Tasks::DatabaseTasks, :create, @@ -384,7 +379,7 @@ module ActiveRecord end def test_creates_test_and_development_databases_when_env_was_not_specified - with_stubbed_configurations_establish_connection do + ActiveRecord::Base.configurations do assert_called_with( ActiveRecord::Tasks::DatabaseTasks, :create, @@ -406,7 +401,7 @@ module ActiveRecord old_env = ENV["RAILS_ENV"] ENV["RAILS_ENV"] = "development" - with_stubbed_configurations_establish_connection do + ActiveRecord::Base.configurations do assert_called_with( ActiveRecord::Tasks::DatabaseTasks, :create, @@ -439,16 +434,6 @@ module ActiveRecord end end end - - private - - def with_stubbed_configurations_establish_connection - ActiveRecord::Base.stub(:configurations, @configurations) do - ActiveRecord::Base.stub(:establish_connection, nil) do - yield - end - end - end end class DatabaseTasksDropTest < ActiveRecord::TestCase @@ -467,20 +452,24 @@ module ActiveRecord class DatabaseTasksDropAllTest < ActiveRecord::TestCase def setup + @old_configurations = ActiveRecord::Base.configurations @configurations = { development: { "database" => "my-db" } } $stdout, @original_stdout = StringIO.new, $stdout $stderr, @original_stderr = StringIO.new, $stderr + + ActiveRecord::Base.configurations = @configurations end def teardown $stdout, $stderr = @original_stdout, @original_stderr + ActiveRecord::Base.configurations = @old_configurations end def test_ignores_configurations_without_databases @configurations[:development]["database"] = nil - ActiveRecord::Base.stub(:configurations, @configurations) do + ActiveRecord::Base.configurations do assert_not_called(ActiveRecord::Tasks::DatabaseTasks, :drop) do ActiveRecord::Tasks::DatabaseTasks.drop_all end @@ -490,7 +479,7 @@ module ActiveRecord def test_ignores_remote_databases @configurations[:development]["host"] = "my.server.tld" - ActiveRecord::Base.stub(:configurations, @configurations) do + ActiveRecord::Base.configurations do assert_not_called(ActiveRecord::Tasks::DatabaseTasks, :drop) do ActiveRecord::Tasks::DatabaseTasks.drop_all end @@ -500,7 +489,7 @@ module ActiveRecord def test_warning_for_remote_databases @configurations[:development]["host"] = "my.server.tld" - ActiveRecord::Base.stub(:configurations, @configurations) do + ActiveRecord::Base.configurations do ActiveRecord::Tasks::DatabaseTasks.drop_all assert_match "This task only modifies local databases. my-db is on a remote host.", @@ -511,7 +500,7 @@ module ActiveRecord def test_drops_configurations_with_local_ip @configurations[:development]["host"] = "127.0.0.1" - ActiveRecord::Base.stub(:configurations, @configurations) do + ActiveRecord::Base.configurations do assert_called(ActiveRecord::Tasks::DatabaseTasks, :drop) do ActiveRecord::Tasks::DatabaseTasks.drop_all end @@ -521,7 +510,7 @@ module ActiveRecord def test_drops_configurations_with_local_host @configurations[:development]["host"] = "localhost" - ActiveRecord::Base.stub(:configurations, @configurations) do + ActiveRecord::Base.configurations do assert_called(ActiveRecord::Tasks::DatabaseTasks, :drop) do ActiveRecord::Tasks::DatabaseTasks.drop_all end @@ -531,7 +520,7 @@ module ActiveRecord def test_drops_configurations_with_blank_hosts @configurations[:development]["host"] = nil - ActiveRecord::Base.stub(:configurations, @configurations) do + ActiveRecord::Base.configurations do assert_called(ActiveRecord::Tasks::DatabaseTasks, :drop) do ActiveRecord::Tasks::DatabaseTasks.drop_all end @@ -541,15 +530,22 @@ module ActiveRecord class DatabaseTasksDropCurrentTest < ActiveRecord::TestCase def setup + @old_configurations = ActiveRecord::Base.configurations @configurations = { "development" => { "database" => "dev-db" }, "test" => { "database" => "test-db" }, - "production" => { "url" => "prod-db-url" } + "production" => { "url" => "abstract://prod-db-url" } } + + ActiveRecord::Base.configurations = @configurations + end + + def teardown + ActiveRecord::Base.configurations = @old_configurations end def test_drops_current_environment_database - ActiveRecord::Base.stub(:configurations, @configurations) do + ActiveRecord::Base.configurations do assert_called_with(ActiveRecord::Tasks::DatabaseTasks, :drop, ["database" => "test-db"]) do ActiveRecord::Tasks::DatabaseTasks.drop_current( @@ -560,7 +556,7 @@ module ActiveRecord end def test_drops_current_environment_database_with_url - ActiveRecord::Base.stub(:configurations, @configurations) do + ActiveRecord::Base.configurations do assert_called_with(ActiveRecord::Tasks::DatabaseTasks, :drop, ["url" => "prod-db-url"]) do ActiveRecord::Tasks::DatabaseTasks.drop_current( @@ -571,7 +567,7 @@ module ActiveRecord end def test_drops_test_and_development_databases_when_env_was_not_specified - ActiveRecord::Base.stub(:configurations, @configurations) do + ActiveRecord::Base.configurations do assert_called_with( ActiveRecord::Tasks::DatabaseTasks, :drop, @@ -591,7 +587,7 @@ module ActiveRecord old_env = ENV["RAILS_ENV"] ENV["RAILS_ENV"] = "development" - ActiveRecord::Base.stub(:configurations, @configurations) do + ActiveRecord::Base.configurations do assert_called_with( ActiveRecord::Tasks::DatabaseTasks, :drop, @@ -612,15 +608,22 @@ module ActiveRecord class DatabaseTasksDropCurrentThreeTierTest < ActiveRecord::TestCase def setup + @old_configurations = ActiveRecord::Base.configurations @configurations = { "development" => { "primary" => { "database" => "dev-db" }, "secondary" => { "database" => "secondary-dev-db" } }, "test" => { "primary" => { "database" => "test-db" }, "secondary" => { "database" => "secondary-test-db" } }, - "production" => { "primary" => { "url" => "prod-db-url" }, "secondary" => { "url" => "secondary-prod-db-url" } } + "production" => { "primary" => { "url" => "abstract://prod-db-url" }, "secondary" => { "url" => "abstract://secondary-prod-db-url" } } } + + ActiveRecord::Base.configurations = @configurations + end + + def teardown + ActiveRecord::Base.configurations = @old_configurations end def test_drops_current_environment_database - ActiveRecord::Base.stub(:configurations, @configurations) do + ActiveRecord::Base.configurations do assert_called_with( ActiveRecord::Tasks::DatabaseTasks, :drop, @@ -637,7 +640,7 @@ module ActiveRecord end def test_drops_current_environment_database_with_url - ActiveRecord::Base.stub(:configurations, @configurations) do + ActiveRecord::Base.configurations do assert_called_with( ActiveRecord::Tasks::DatabaseTasks, :drop, @@ -654,7 +657,7 @@ module ActiveRecord end def test_drops_test_and_development_databases_when_env_was_not_specified - ActiveRecord::Base.stub(:configurations, @configurations) do + ActiveRecord::Base.configurations do assert_called_with( ActiveRecord::Tasks::DatabaseTasks, :drop, @@ -676,7 +679,7 @@ module ActiveRecord old_env = ENV["RAILS_ENV"] ENV["RAILS_ENV"] = "development" - ActiveRecord::Base.stub(:configurations, @configurations) do + ActiveRecord::Base.configurations do assert_called_with( ActiveRecord::Tasks::DatabaseTasks, :drop, @@ -848,12 +851,16 @@ module ActiveRecord class DatabaseTasksPurgeCurrentTest < ActiveRecord::TestCase def test_purges_current_environment_database + old_configurations = ActiveRecord::Base.configurations configurations = { "development" => { "database" => "dev-db" }, "test" => { "database" => "test-db" }, "production" => { "database" => "prod-db" } } - ActiveRecord::Base.stub(:configurations, configurations) do + + ActiveRecord::Base.configurations = configurations + + ActiveRecord::Base.configurations do assert_called_with( ActiveRecord::Tasks::DatabaseTasks, :purge, @@ -864,13 +871,17 @@ module ActiveRecord end end end + ensure + ActiveRecord::Base.configurations = old_configurations end end class DatabaseTasksPurgeAllTest < ActiveRecord::TestCase def test_purge_all_local_configurations + old_configurations = ActiveRecord::Base.configurations configurations = { development: { "database" => "my-db" } } - ActiveRecord::Base.stub(:configurations, configurations) do + ActiveRecord::Base.configurations = configurations + ActiveRecord::Base.configurations do assert_called_with( ActiveRecord::Tasks::DatabaseTasks, :purge, @@ -879,6 +890,8 @@ module ActiveRecord ActiveRecord::Tasks::DatabaseTasks.purge_all end end + ensure + ActiveRecord::Base.configurations = old_configurations end end diff --git a/activerecord/test/cases/tasks/legacy_database_tasks_test.rb b/activerecord/test/cases/tasks/legacy_database_tasks_test.rb new file mode 100644 index 0000000000..ffa55be878 --- /dev/null +++ b/activerecord/test/cases/tasks/legacy_database_tasks_test.rb @@ -0,0 +1,591 @@ +# frozen_string_literal: true + +require "cases/helper" +require "active_record/tasks/database_tasks" + +module ActiveRecord + class LegacyDatabaseTasksCreateAllTest < ActiveRecord::TestCase + def setup + @old_configurations = ActiveRecord::Base.configurations.to_h + + @configurations = { "development" => { "database" => "my-db" } } + + $stdout, @original_stdout = StringIO.new, $stdout + $stderr, @original_stderr = StringIO.new, $stderr + + ActiveRecord::Base.configurations = @configurations + end + + def teardown + $stdout, $stderr = @original_stdout, @original_stderr + ActiveRecord::Base.configurations = @old_configurations + end + + def test_ignores_configurations_without_databases + @configurations["development"]["database"] = nil + + ActiveRecord::Base.configurations.to_h do + assert_not_called(ActiveRecord::Tasks::DatabaseTasks, :create) do + ActiveRecord::Tasks::DatabaseTasks.create_all + end + end + end + + def test_ignores_remote_databases + @configurations["development"]["host"] = "my.server.tld" + + ActiveRecord::Base.configurations.to_h do + assert_not_called(ActiveRecord::Tasks::DatabaseTasks, :create) do + ActiveRecord::Tasks::DatabaseTasks.create_all + end + end + end + + def test_warning_for_remote_databases + @configurations["development"]["host"] = "my.server.tld" + + ActiveRecord::Base.configurations.to_h do + ActiveRecord::Tasks::DatabaseTasks.create_all + + assert_match "This task only modifies local databases. my-db is on a remote host.", + $stderr.string + end + end + + def test_creates_configurations_with_local_ip + @configurations["development"]["host"] = "127.0.0.1" + + ActiveRecord::Base.configurations.to_h do + assert_called(ActiveRecord::Tasks::DatabaseTasks, :create) do + ActiveRecord::Tasks::DatabaseTasks.create_all + end + end + end + + def test_creates_configurations_with_local_host + @configurations["development"]["host"] = "localhost" + + ActiveRecord::Base.configurations.to_h do + assert_called(ActiveRecord::Tasks::DatabaseTasks, :create) do + ActiveRecord::Tasks::DatabaseTasks.create_all + end + end + end + + def test_creates_configurations_with_blank_hosts + @configurations["development"]["host"] = nil + + ActiveRecord::Base.configurations.to_h do + assert_called(ActiveRecord::Tasks::DatabaseTasks, :create) do + ActiveRecord::Tasks::DatabaseTasks.create_all + end + end + end + end + + class LegacyDatabaseTasksCreateCurrentTest < ActiveRecord::TestCase + def setup + @old_configurations = ActiveRecord::Base.configurations.to_h + + @configurations = { + "development" => { "database" => "dev-db" }, + "test" => { "database" => "test-db" }, + "production" => { "url" => "abstract://prod-db-url" } + } + + ActiveRecord::Base.configurations = @configurations + end + + def teardown + ActiveRecord::Base.configurations = @old_configurations + end + + def test_creates_current_environment_database + ActiveRecord::Base.configurations.to_h do + assert_called_with( + ActiveRecord::Tasks::DatabaseTasks, + :create, + ["database" => "test-db"], + ) do + ActiveRecord::Tasks::DatabaseTasks.create_current( + ActiveSupport::StringInquirer.new("test") + ) + end + end + end + + def test_creates_current_environment_database_with_url + ActiveRecord::Base.configurations.to_h do + assert_called_with( + ActiveRecord::Tasks::DatabaseTasks, + :create, + ["url" => "prod-db-url"], + ) do + ActiveRecord::Tasks::DatabaseTasks.create_current( + ActiveSupport::StringInquirer.new("production") + ) + end + end + end + + def test_creates_test_and_development_databases_when_env_was_not_specified + ActiveRecord::Base.configurations.to_h do + assert_called_with( + ActiveRecord::Tasks::DatabaseTasks, + :create, + [ + ["database" => "dev-db"], + ["database" => "test-db"] + ], + ) do + ActiveRecord::Tasks::DatabaseTasks.create_current( + ActiveSupport::StringInquirer.new("development") + ) + end + end + end + + def test_creates_test_and_development_databases_when_rails_env_is_development + old_env = ENV["RAILS_ENV"] + ENV["RAILS_ENV"] = "development" + + ActiveRecord::Base.configurations.to_h do + assert_called_with( + ActiveRecord::Tasks::DatabaseTasks, + :create, + [ + ["database" => "dev-db"], + ["database" => "test-db"] + ], + ) do + ActiveRecord::Tasks::DatabaseTasks.create_current( + ActiveSupport::StringInquirer.new("development") + ) + end + end + ensure + ENV["RAILS_ENV"] = old_env + end + + def test_establishes_connection_for_the_given_environments + ActiveRecord::Tasks::DatabaseTasks.stub(:create, nil) do + assert_called_with(ActiveRecord::Base, :establish_connection, [:development]) do + ActiveRecord::Tasks::DatabaseTasks.create_current( + ActiveSupport::StringInquirer.new("development") + ) + end + end + end + end + + class LegacyDatabaseTasksCreateCurrentThreeTierTest < ActiveRecord::TestCase + def setup + @old_configurations = ActiveRecord::Base.configurations.to_h + + @configurations = { + "development" => { "primary" => { "database" => "dev-db" }, "secondary" => { "database" => "secondary-dev-db" } }, + "test" => { "primary" => { "database" => "test-db" }, "secondary" => { "database" => "secondary-test-db" } }, + "production" => { "primary" => { "url" => "abstract://prod-db-url" }, "secondary" => { "url" => "abstract://secondary-prod-db-url" } } + } + + ActiveRecord::Base.configurations = @configurations + end + + def teardown + ActiveRecord::Base.configurations = @old_configurations + end + + def test_creates_current_environment_database + ActiveRecord::Base.configurations.to_h do + assert_called_with( + ActiveRecord::Tasks::DatabaseTasks, + :create, + [ + ["database" => "test-db"], + ["database" => "secondary-test-db"] + ] + ) do + ActiveRecord::Tasks::DatabaseTasks.create_current( + ActiveSupport::StringInquirer.new("test") + ) + end + end + end + + def test_creates_current_environment_database_with_url + ActiveRecord::Base.configurations.to_h do + assert_called_with( + ActiveRecord::Tasks::DatabaseTasks, + :create, + [ + ["url" => "prod-db-url"], + ["url" => "secondary-prod-db-url"] + ] + ) do + ActiveRecord::Tasks::DatabaseTasks.create_current( + ActiveSupport::StringInquirer.new("production") + ) + end + end + end + + def test_creates_test_and_development_databases_when_env_was_not_specified + ActiveRecord::Base.configurations.to_h do + assert_called_with( + ActiveRecord::Tasks::DatabaseTasks, + :create, + [ + ["database" => "dev-db"], + ["database" => "secondary-dev-db"], + ["database" => "test-db"], + ["database" => "secondary-test-db"] + ] + ) do + ActiveRecord::Tasks::DatabaseTasks.create_current( + ActiveSupport::StringInquirer.new("development") + ) + end + end + end + + def test_creates_test_and_development_databases_when_rails_env_is_development + old_env = ENV["RAILS_ENV"] + ENV["RAILS_ENV"] = "development" + + ActiveRecord::Base.configurations.to_h do + assert_called_with( + ActiveRecord::Tasks::DatabaseTasks, + :create, + [ + ["database" => "dev-db"], + ["database" => "secondary-dev-db"], + ["database" => "test-db"], + ["database" => "secondary-test-db"] + ] + ) do + ActiveRecord::Tasks::DatabaseTasks.create_current( + ActiveSupport::StringInquirer.new("development") + ) + end + end + ensure + ENV["RAILS_ENV"] = old_env + end + + def test_establishes_connection_for_the_given_environments_config + ActiveRecord::Tasks::DatabaseTasks.stub(:create, nil) do + assert_called_with( + ActiveRecord::Base, + :establish_connection, + [:development] + ) do + ActiveRecord::Tasks::DatabaseTasks.create_current( + ActiveSupport::StringInquirer.new("development") + ) + end + end + end + end + + class LegacyDatabaseTasksDropAllTest < ActiveRecord::TestCase + def setup + @old_configurations = ActiveRecord::Base.configurations.to_h + + @configurations = { development: { "database" => "my-db" } } + + $stdout, @original_stdout = StringIO.new, $stdout + $stderr, @original_stderr = StringIO.new, $stderr + + ActiveRecord::Base.configurations = @configurations + end + + def teardown + $stdout, $stderr = @original_stdout, @original_stderr + ActiveRecord::Base.configurations = @old_configurations + end + + def test_ignores_configurations_without_databases + @configurations[:development]["database"] = nil + + ActiveRecord::Base.configurations.to_h do + assert_not_called(ActiveRecord::Tasks::DatabaseTasks, :drop) do + ActiveRecord::Tasks::DatabaseTasks.drop_all + end + end + end + + def test_ignores_remote_databases + @configurations[:development]["host"] = "my.server.tld" + + ActiveRecord::Base.configurations.to_h do + assert_not_called(ActiveRecord::Tasks::DatabaseTasks, :drop) do + ActiveRecord::Tasks::DatabaseTasks.drop_all + end + end + end + + def test_warning_for_remote_databases + @configurations[:development]["host"] = "my.server.tld" + + ActiveRecord::Base.configurations.to_h do + ActiveRecord::Tasks::DatabaseTasks.drop_all + + assert_match "This task only modifies local databases. my-db is on a remote host.", + $stderr.string + end + end + + def test_drops_configurations_with_local_ip + @configurations[:development]["host"] = "127.0.0.1" + + ActiveRecord::Base.configurations.to_h do + assert_called(ActiveRecord::Tasks::DatabaseTasks, :drop) do + ActiveRecord::Tasks::DatabaseTasks.drop_all + end + end + end + + def test_drops_configurations_with_local_host + @configurations[:development]["host"] = "localhost" + + ActiveRecord::Base.configurations.to_h do + assert_called(ActiveRecord::Tasks::DatabaseTasks, :drop) do + ActiveRecord::Tasks::DatabaseTasks.drop_all + end + end + end + + def test_drops_configurations_with_blank_hosts + @configurations[:development]["host"] = nil + + ActiveRecord::Base.configurations.to_h do + assert_called(ActiveRecord::Tasks::DatabaseTasks, :drop) do + ActiveRecord::Tasks::DatabaseTasks.drop_all + end + end + end + end + + class LegacyDatabaseTasksDropCurrentTest < ActiveRecord::TestCase + def setup + @old_configurations = ActiveRecord::Base.configurations.to_h + + @configurations = { + "development" => { "database" => "dev-db" }, + "test" => { "database" => "test-db" }, + "production" => { "url" => "abstract://prod-db-url" } + } + + ActiveRecord::Base.configurations = @configurations + end + + def teardown + ActiveRecord::Base.configurations = @old_configurations + end + + def test_drops_current_environment_database + ActiveRecord::Base.configurations.to_h do + assert_called_with(ActiveRecord::Tasks::DatabaseTasks, :drop, + ["database" => "test-db"]) do + ActiveRecord::Tasks::DatabaseTasks.drop_current( + ActiveSupport::StringInquirer.new("test") + ) + end + end + end + + def test_drops_current_environment_database_with_url + ActiveRecord::Base.configurations.to_h do + assert_called_with(ActiveRecord::Tasks::DatabaseTasks, :drop, + ["url" => "prod-db-url"]) do + ActiveRecord::Tasks::DatabaseTasks.drop_current( + ActiveSupport::StringInquirer.new("production") + ) + end + end + end + + def test_drops_test_and_development_databases_when_env_was_not_specified + ActiveRecord::Base.configurations.to_h do + assert_called_with( + ActiveRecord::Tasks::DatabaseTasks, + :drop, + [ + ["database" => "dev-db"], + ["database" => "test-db"] + ] + ) do + ActiveRecord::Tasks::DatabaseTasks.drop_current( + ActiveSupport::StringInquirer.new("development") + ) + end + end + end + + def test_drops_testand_development_databases_when_rails_env_is_development + old_env = ENV["RAILS_ENV"] + ENV["RAILS_ENV"] = "development" + + ActiveRecord::Base.configurations.to_h do + assert_called_with( + ActiveRecord::Tasks::DatabaseTasks, + :drop, + [ + ["database" => "dev-db"], + ["database" => "test-db"] + ] + ) do + ActiveRecord::Tasks::DatabaseTasks.drop_current( + ActiveSupport::StringInquirer.new("development") + ) + end + end + ensure + ENV["RAILS_ENV"] = old_env + end + end + + class LegacyDatabaseTasksDropCurrentThreeTierTest < ActiveRecord::TestCase + def setup + @old_configurations = ActiveRecord::Base.configurations.to_h + + @configurations = { + "development" => { "primary" => { "database" => "dev-db" }, "secondary" => { "database" => "secondary-dev-db" } }, + "test" => { "primary" => { "database" => "test-db" }, "secondary" => { "database" => "secondary-test-db" } }, + "production" => { "primary" => { "url" => "abstract://prod-db-url" }, "secondary" => { "url" => "abstract://secondary-prod-db-url" } } + } + + ActiveRecord::Base.configurations = @configurations + end + + def teardown + ActiveRecord::Base.configurations = @old_configurations + end + + def test_drops_current_environment_database + ActiveRecord::Base.configurations.to_h do + assert_called_with( + ActiveRecord::Tasks::DatabaseTasks, + :drop, + [ + ["database" => "test-db"], + ["database" => "secondary-test-db"] + ] + ) do + ActiveRecord::Tasks::DatabaseTasks.drop_current( + ActiveSupport::StringInquirer.new("test") + ) + end + end + end + + def test_drops_current_environment_database_with_url + ActiveRecord::Base.configurations.to_h do + assert_called_with( + ActiveRecord::Tasks::DatabaseTasks, + :drop, + [ + ["url" => "prod-db-url"], + ["url" => "secondary-prod-db-url"] + ] + ) do + ActiveRecord::Tasks::DatabaseTasks.drop_current( + ActiveSupport::StringInquirer.new("production") + ) + end + end + end + + def test_drops_test_and_development_databases_when_env_was_not_specified + ActiveRecord::Base.configurations.to_h do + assert_called_with( + ActiveRecord::Tasks::DatabaseTasks, + :drop, + [ + ["database" => "dev-db"], + ["database" => "secondary-dev-db"], + ["database" => "test-db"], + ["database" => "secondary-test-db"] + ] + ) do + ActiveRecord::Tasks::DatabaseTasks.drop_current( + ActiveSupport::StringInquirer.new("development") + ) + end + end + end + + def test_drops_testand_development_databases_when_rails_env_is_development + old_env = ENV["RAILS_ENV"] + ENV["RAILS_ENV"] = "development" + + ActiveRecord::Base.configurations.to_h do + assert_called_with( + ActiveRecord::Tasks::DatabaseTasks, + :drop, + [ + ["database" => "dev-db"], + ["database" => "secondary-dev-db"], + ["database" => "test-db"], + ["database" => "secondary-test-db"] + ] + ) do + ActiveRecord::Tasks::DatabaseTasks.drop_current( + ActiveSupport::StringInquirer.new("development") + ) + end + end + ensure + ENV["RAILS_ENV"] = old_env + end + end + + class LegacyDatabaseTasksPurgeCurrentTest < ActiveRecord::TestCase + def test_purges_current_environment_database + @old_configurations = ActiveRecord::Base.configurations.to_h + + configurations = { + "development" => { "database" => "dev-db" }, + "test" => { "database" => "test-db" }, + "production" => { "database" => "prod-db" } + } + + ActiveRecord::Base.configurations = configurations + + ActiveRecord::Base.configurations.to_h do + assert_called_with( + ActiveRecord::Tasks::DatabaseTasks, + :purge, + ["database" => "prod-db"] + ) do + assert_called_with(ActiveRecord::Base, :establish_connection, [:production]) do + ActiveRecord::Tasks::DatabaseTasks.purge_current("production") + end + end + end + ensure + ActiveRecord::Base.configurations = @old_configurations + end + end + + class LegacyDatabaseTasksPurgeAllTest < ActiveRecord::TestCase + def test_purge_all_local_configurations + @old_configurations = ActiveRecord::Base.configurations.to_h + + configurations = { development: { "database" => "my-db" } } + ActiveRecord::Base.configurations = configurations + + ActiveRecord::Base.configurations.to_h do + assert_called_with( + ActiveRecord::Tasks::DatabaseTasks, + :purge, + ["database" => "my-db"] + ) do + ActiveRecord::Tasks::DatabaseTasks.purge_all + end + end + ensure + ActiveRecord::Base.configurations = @old_configurations + end + end +end diff --git a/activerecord/test/cases/tasks/postgresql_rake_test.rb b/activerecord/test/cases/tasks/postgresql_rake_test.rb index e36c2b1e3f..8c6e8d79f3 100644 --- a/activerecord/test/cases/tasks/postgresql_rake_test.rb +++ b/activerecord/test/cases/tasks/postgresql_rake_test.rb @@ -112,7 +112,7 @@ if current_adapter?(:PostgreSQLAdapter) ActiveRecord::Base.stub(:connection, @connection) do ActiveRecord::Base.stub(:establish_connection, -> * { raise Exception }) do assert_raises(Exception) { ActiveRecord::Tasks::DatabaseTasks.create @configuration } - assert_match "Couldn't create database for #{@configuration.inspect}", $stderr.string + assert_match "Couldn't create '#{@configuration['database']}' database. Please check your configuration.", $stderr.string end end end diff --git a/activerecord/test/cases/tasks/sqlite_rake_test.rb b/activerecord/test/cases/tasks/sqlite_rake_test.rb index c42afd0e42..c1092b97c1 100644 --- a/activerecord/test/cases/tasks/sqlite_rake_test.rb +++ b/activerecord/test/cases/tasks/sqlite_rake_test.rb @@ -62,7 +62,7 @@ if current_adapter?(:SQLite3Adapter) def test_db_create_with_error_prints_message ActiveRecord::Base.stub(:establish_connection, proc { raise Exception }) do assert_raises(Exception) { ActiveRecord::Tasks::DatabaseTasks.create @configuration, "/rails/root" } - assert_match "Couldn't create database for #{@configuration.inspect}", $stderr.string + assert_match "Couldn't create '#{@configuration['database']}' database. Please check your configuration.", $stderr.string end end end diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb index 640cdb33b4..528585fb75 100644 --- a/activerecord/test/models/post.rb +++ b/activerecord/test/models/post.rb @@ -324,7 +324,7 @@ class FakeKlass table[name] end - def enforce_raw_sql_whitelist(*args) + def disallow_raw_sql!(*args) # noop end diff --git a/activesupport/lib/active_support/core_ext/object/try.rb b/activesupport/lib/active_support/core_ext/object/try.rb index c874691629..aa6896af32 100644 --- a/activesupport/lib/active_support/core_ext/object/try.rb +++ b/activesupport/lib/active_support/core_ext/object/try.rb @@ -4,19 +4,27 @@ require "delegate" module ActiveSupport module Tryable #:nodoc: - def try(*a, &b) - try!(*a, &b) if a.empty? || respond_to?(a.first) + def try(method_name = nil, *args, &b) + if method_name.nil? && block_given? + if b.arity == 0 + instance_eval(&b) + else + yield self + end + elsif respond_to?(method_name) + public_send(method_name, *args, &b) + end end - def try!(*a, &b) - if a.empty? && block_given? + def try!(method_name = nil, *args, &b) + if method_name.nil? && block_given? if b.arity == 0 instance_eval(&b) else yield self end else - public_send(*a, &b) + public_send(method_name, *args, &b) end end end diff --git a/activesupport/test/metadata/shared_metadata_tests.rb b/activesupport/test/metadata/shared_metadata_tests.rb index 08bb0c648e..cf571223e5 100644 --- a/activesupport/test/metadata/shared_metadata_tests.rb +++ b/activesupport/test/metadata/shared_metadata_tests.rb @@ -1,11 +1,6 @@ # frozen_string_literal: true module SharedMessageMetadataTests - def teardown - travel_back - super - end - def null_serializing? false end diff --git a/guides/source/6_0_release_notes.md b/guides/source/6_0_release_notes.md new file mode 100644 index 0000000000..f3ed21dc45 --- /dev/null +++ b/guides/source/6_0_release_notes.md @@ -0,0 +1,175 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON https://guides.rubyonrails.org.** + +Ruby on Rails 6.0 Release Notes +=============================== + +Highlights in Rails 6.0: + +* Parallel Testing + +These release notes cover only the major changes. To learn about various bug +fixes and changes, please refer to the change logs or check out the [list of +commits](https://github.com/rails/rails/commits/6-0-stable) in the main Rails +repository on GitHub. + +-------------------------------------------------------------------------------- + +Upgrading to Rails 6.0 +---------------------- + +If you're upgrading an existing application, it's a great idea to have good test +coverage before going in. You should also first upgrade to Rails 5.2 in case you +haven't and make sure your application still runs as expected before attempting +an update to Rails 6.0. A list of things to watch out for when upgrading is +available in the +[Upgrading Ruby on Rails](upgrading_ruby_on_rails.html#upgrading-from-rails-5-2-to-rails-6-0) +guide. + +Major Features +-------------- + +### Parallel Testing + +[Pull Request](https://github.com/rails/rails/pull/31900) + +[Parallel Testing](testing.html#parallel-testing) allows you to parallelize your +test suite. While forking processes is the default method, threading is +supported as well. Running tests in parallel reduces the time it takes +your entire test suite to run. + +Railties +-------- + +Please refer to the [Changelog][railties] for detailed changes. + +### Removals + +### Deprecations + +### Notable changes + +Action Cable +------------ + +Please refer to the [Changelog][action-cable] for detailed changes. + +### Removals + +### Deprecations + +### Notable changes + +Action Pack +----------- + +Please refer to the [Changelog][action-pack] for detailed changes. + +### Removals + +### Deprecations + +### Notable changes + +Action View +----------- + +Please refer to the [Changelog][action-view] for detailed changes. + +### Removals + +### Deprecations + +### Notable changes + +Action Mailer +------------- + +Please refer to the [Changelog][action-mailer] for detailed changes. + +### Removals + +### Deprecations + +### Notable changes + +Active Record +------------- + +Please refer to the [Changelog][active-record] for detailed changes. + +### Removals + +### Deprecations + +### Notable changes + +Active Storage +-------------- + +Please refer to the [Changelog][active-storage] for detailed changes. + +### Removals + +### Deprecations + +### Notable changes + +Active Model +------------ + +Please refer to the [Changelog][active-model] for detailed changes. + +### Removals + +### Deprecations + +### Notable changes + +Active Support +-------------- + +Please refer to the [Changelog][active-support] for detailed changes. + +### Removals + +### Deprecations + +### Notable changes + +Active Job +---------- + +Please refer to the [Changelog][active-job] for detailed changes. + +### Removals + +### Deprecations + +### Notable changes + +Ruby on Rails Guides +-------------------- + +Please refer to the [Changelog][guides] for detailed changes. + +### Notable changes + +Credits +------- + +See the +[full list of contributors to Rails](http://contributors.rubyonrails.org/) +for the many people who spent many hours making Rails, the stable and robust +framework it is. Kudos to all of them. + +[railties]: https://github.com/rails/rails/blob/6-0-stable/railties/CHANGELOG.md +[action-pack]: https://github.com/rails/rails/blob/6-0-stable/actionpack/CHANGELOG.md +[action-view]: https://github.com/rails/rails/blob/6-0-stable/actionview/CHANGELOG.md +[action-mailer]: https://github.com/rails/rails/blob/6-0-stable/actionmailer/CHANGELOG.md +[action-cable]: https://github.com/rails/rails/blob/6-0-stable/actioncable/CHANGELOG.md +[active-record]: https://github.com/rails/rails/blob/6-0-stable/activerecord/CHANGELOG.md +[active-storage]: https://github.com/rails/rails/blob/6-0-stable/activestorage/CHANGELOG.md +[active-model]: https://github.com/rails/rails/blob/6-0-stable/activemodel/CHANGELOG.md +[active-support]: https://github.com/rails/rails/blob/6-0-stable/activesupport/CHANGELOG.md +[active-job]: https://github.com/rails/rails/blob/6-0-stable/activejob/CHANGELOG.md +[guides]: https://github.com/rails/rails/blob/6-0-stable/guides/CHANGELOG.md diff --git a/guides/source/active_support_instrumentation.md b/guides/source/active_support_instrumentation.md index 3568c47dd8..8581817d71 100644 --- a/guides/source/active_support_instrumentation.md +++ b/guides/source/active_support_instrumentation.md @@ -458,6 +458,14 @@ Active Job | `:adapter` | QueueAdapter object processing the job | | `:job` | Job object | +### enqueue_retry.active_job + +| Key | Value | +| ------------ | -------------------------------------- | +| `:job` | Job object | +| `:adapter` | QueueAdapter object processing the job | +| `:error` | The error that caused the retry | + ### perform_start.active_job | Key | Value | @@ -472,6 +480,22 @@ Active Job | `:adapter` | QueueAdapter object processing the job | | `:job` | Job object | +### retry_stopped.active_job + +| Key | Value | +| ------------ | -------------------------------------- | +| `:adapter` | QueueAdapter object processing the job | +| `:job` | Job object | +| `:error` | The error that caused the retry | + +### discard.active_job + +| Key | Value | +| ------------ | -------------------------------------- | +| `:adapter` | QueueAdapter object processing the job | +| `:job` | Job object | +| `:error` | The error that caused the discard | + Action Cable ------------ diff --git a/guides/source/configuring.md b/guides/source/configuring.md index b20a2bb0d2..8c95187fa4 100644 --- a/guides/source/configuring.md +++ b/guides/source/configuring.md @@ -516,6 +516,9 @@ Defaults to `'signed cookie'`. signed and encrypted cookies use the AES-256-GCM cipher or the older AES-256-CBC cipher. It defaults to `true`. +* `config.action_dispatch.use_cookies_with_metadata` enables writing + cookies with the purpose and expiry metadata embedded. It defaults to `true`. + * `config.action_dispatch.perform_deep_munge` configures whether `deep_munge` method should be performed on the parameters. See [Security Guide](security.html#unsafe-query-generation) for more information. It defaults to `true`. @@ -908,7 +911,15 @@ $ echo $DATABASE_URL postgresql://localhost/my_database $ rails runner 'puts ActiveRecord::Base.configurations' -{"development"=>{"adapter"=>"postgresql", "host"=>"localhost", "database"=>"my_database"}} +#<ActiveRecord::DatabaseConfigurations:0x00007fd50e209a28> + +$ rails runner 'puts ActiveRecord::Base.configurations.inspect' +#<ActiveRecord::DatabaseConfigurations:0x00007fc8eab02880 @configurations=[ + #<ActiveRecord::DatabaseConfigurations::UrlConfig:0x00007fc8eab020b0 + @env_name="development", @spec_name="primary", + @config={"adapter"=>"postgresql", "database"=>"my_database", "host"=>"localhost"} + @url="postgresql://localhost/my_database"> + ] ``` Here the adapter, host, and database match the information in `ENV['DATABASE_URL']`. @@ -925,7 +936,15 @@ $ echo $DATABASE_URL postgresql://localhost/my_database $ rails runner 'puts ActiveRecord::Base.configurations' -{"development"=>{"adapter"=>"postgresql", "host"=>"localhost", "database"=>"my_database", "pool"=>5}} +#<ActiveRecord::DatabaseConfigurations:0x00007fd50e209a28> + +$ rails runner 'puts ActiveRecord::Base.configurations.inspect' +#<ActiveRecord::DatabaseConfigurations:0x00007fc8eab02880 @configurations=[ + #<ActiveRecord::DatabaseConfigurations::UrlConfig:0x00007fc8eab020b0 + @env_name="development", @spec_name="primary", + @config={"adapter"=>"postgresql", "database"=>"my_database", "host"=>"localhost", "pool"=>5} + @url="postgresql://localhost/my_database"> + ] ``` Since pool is not in the `ENV['DATABASE_URL']` provided connection information its information is merged in. Since `adapter` is duplicate, the `ENV['DATABASE_URL']` connection information wins. @@ -935,13 +954,21 @@ The only way to explicitly not use the connection information in `ENV['DATABASE_ ``` $ cat config/database.yml development: - url: sqlite3:NOT_my_database + url: sqlite3://NOT_my_database $ echo $DATABASE_URL postgresql://localhost/my_database $ rails runner 'puts ActiveRecord::Base.configurations' -{"development"=>{"adapter"=>"sqlite3", "database"=>"NOT_my_database"}} +#<ActiveRecord::DatabaseConfigurations:0x00007fd50e209a28> + +$ rails runner 'puts ActiveRecord::Base.configurations.inspect' +#<ActiveRecord::DatabaseConfigurations:0x00007fc8eab02880 @configurations=[ + #<ActiveRecord::DatabaseConfigurations::UrlConfig:0x00007fc8eab020b0 + @env_name="development", @spec_name="primary", + @config={"adapter"=>"sqlite3", "database"=>"NOT_my_database", "host"=>"localhost"} + @url="sqlite3://NOT_my_database"> + ] ``` Here the connection information in `ENV['DATABASE_URL']` is ignored, note the different adapter and database name. diff --git a/guides/source/documents.yaml b/guides/source/documents.yaml index 4dee34b1e7..8f2312458d 100644 --- a/guides/source/documents.yaml +++ b/guides/source/documents.yaml @@ -202,6 +202,11 @@ url: upgrading_ruby_on_rails.html description: This guide helps in upgrading applications to latest Ruby on Rails versions. - + name: Ruby on Rails 6.0 Release Notes + work_in_progress: true + url: 6_0_release_notes.html + description: Release notes for Rails 6.0. + - name: Ruby on Rails 5.2 Release Notes url: 5_2_release_notes.html description: Release notes for Rails 5.2. diff --git a/guides/source/form_helpers.md b/guides/source/form_helpers.md index 12cfe249b6..3660772fb9 100644 --- a/guides/source/form_helpers.md +++ b/guides/source/form_helpers.md @@ -40,7 +40,7 @@ When called without arguments like this, it creates a form tag which, when submi ``` You'll notice that the HTML contains an `input` element with type `hidden`. This `input` is important, because non-GET form cannot be successfully submitted without it. -The hidden input element with the name `authenticity_token` is a security feature of Rails called **cross-site request forgery protection**, and form helpers generate it for every non-GET form (provided that this security feature is enabled). You can read more about this in the [Security Guide](security.html#cross-site-request-forgery-csrf) guide. +The hidden input element with the name `authenticity_token` is a security feature of Rails called **cross-site request forgery protection**, and form helpers generate it for every non-GET form (provided that this security feature is enabled). You can read more about this in the [Securing Rails Applications](security.html#cross-site-request-forgery-csrf) guide. ### A Generic Search Form diff --git a/guides/source/upgrading_ruby_on_rails.md b/guides/source/upgrading_ruby_on_rails.md index 89de180508..befd4e08c0 100644 --- a/guides/source/upgrading_ruby_on_rails.md +++ b/guides/source/upgrading_ruby_on_rails.md @@ -69,13 +69,15 @@ Don't forget to review the difference, to see if there were any unexpected chang ### Configure Framework Defaults The new Rails version might have different configuration defaults than the previous version. However, after following the steps described above, your application would still run with configuration defaults from the *previous* Rails version. That's because the value for `config.load_defaults` in `config/application.rb` has not been changed yet. - + To allow you to upgrade to new defaults one by one, the update task has created a file `config/initializers/new_framework_defaults.rb`. Once your application is ready to run with new defaults, you can remove this file and flip the `config.load_defaults` value. Upgrading from Rails 5.2 to Rails 6.0 ------------------------------------- +For more information on changes made to Rails 6.0 please see the [release notes](6_0_release_notes.html). + ### Force SSL The `force_ssl` method on controllers has been deprecated and will be removed in @@ -83,6 +85,17 @@ Rails 6.1. You are encouraged to enable `config.force_ssl` to enforce HTTPS connections throughout your application. If you need to exempt certain endpoints from redirection, you can use `config.ssl_options` to configure that behavior. +### Purpose in signed or encrypted cookie is now embedded in the cookies values + +To improve security, Rails now embeds the purpose information in encrypted or signed cookies value. +Rails can now thwart attacks that attempt to copy signed/encrypted value +of a cookie and use it as the value of another cookie. + +This new embed information make those cookies incompatible with versions of Rails older than 6.0. + +If you require your cookies to be read by 5.2 and older, or you are still validating your 6.0 deploy and want +to allow you to rollback set +`Rails.application.config.action_dispatch.use_cookies_with_metadata` to `false`. Upgrading from Rails 5.1 to Rails 5.2 ------------------------------------- diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md index 2a8882171f..6a0e79a7b3 100644 --- a/railties/CHANGELOG.md +++ b/railties/CHANGELOG.md @@ -1,3 +1,22 @@ +* Emit warning for unknown inflection rule when generating model. + + *Yoshiyuki Kinjo* + +* Add `--migrations_paths` option to migration generator. + + If you're using multiple databases and have a folder for each database + for migrations (ex db/migrate and db/new_db_migrate) you can now pass the + `--migrations_paths` option to the generator to make sure the the migration + is inserted into the correct folder. + + ``` + rails g migration CreateHouses --migrations_paths=db/kingston_migrate + invoke active_record + create db/kingston_migrate/20180830151055_create_houses.rb + ``` + + *Eileen M. Uchitelle* + * Deprecate `rake routes` in favor of `rails routes`. *Yuji Yaginuma* diff --git a/railties/lib/rails/application/configuration.rb b/railties/lib/rails/application/configuration.rb index 9eb07219e0..f4cbd2b9d0 100644 --- a/railties/lib/rails/application/configuration.rb +++ b/railties/lib/rails/application/configuration.rb @@ -168,18 +168,6 @@ module Rails end end - # Loads the database YAML without evaluating ERB. People seem to - # write ERB that makes the database configuration depend on - # Rails configuration. But we want Rails configuration (specifically - # `rake` and `rails` tasks) to be generated based on information in - # the database yaml, so we need a method that loads the database - # yaml *without* the context of the Rails application. - def load_database_yaml # :nodoc: - path = paths["config/database"].existent.first - return {} unless path - YAML.load_file(path.to_s) - end - # Loads and returns the entire raw configuration of database from # values stored in <tt>config/database.yml</tt>. def database_configuration diff --git a/railties/lib/rails/generators/model_helpers.rb b/railties/lib/rails/generators/model_helpers.rb index 50078404b3..3676432d5c 100644 --- a/railties/lib/rails/generators/model_helpers.rb +++ b/railties/lib/rails/generators/model_helpers.rb @@ -7,6 +7,10 @@ module Rails module ModelHelpers # :nodoc: PLURAL_MODEL_NAME_WARN_MESSAGE = "[WARNING] The model name '%s' was recognized as a plural, using the singular '%s' instead. " \ "Override with --force-plural or setup custom inflection rules for this noun before running the generator." + IRREGULAR_MODEL_NAME_WARN_MESSAGE = <<~WARNING + [WARNING] Rails cannot recover singular form from its plural form '%s'. + Please setup custom inflection rules for this noun before running the generator in config/initializers/inflections.rb. + WARNING mattr_accessor :skip_warn def self.included(base) #:nodoc: @@ -19,11 +23,14 @@ module Rails singular = name.singularize unless ModelHelpers.skip_warn say PLURAL_MODEL_NAME_WARN_MESSAGE % [name, singular] - ModelHelpers.skip_warn = true end name.replace singular assign_names!(name) end + if name.singularize != name.pluralize.singularize && ! ModelHelpers.skip_warn + say IRREGULAR_MODEL_NAME_WARN_MESSAGE % [name.pluralize] + end + ModelHelpers.skip_warn = true end end end diff --git a/railties/test/application/rake/dbs_test.rb b/railties/test/application/rake/dbs_test.rb index 0594236b1f..b1038ba5d6 100644 --- a/railties/test/application/rake/dbs_test.rb +++ b/railties/test/application/rake/dbs_test.rb @@ -26,12 +26,12 @@ module ApplicationTests FileUtils.rm_rf("#{app_path}/config/database.yml") end - def db_create_and_drop(expected_database, environment_loaded: true) + def db_create_and_drop(expected_database) Dir.chdir(app_path) do output = rails("db:create") assert_match(/Created database/, output) assert File.exist?(expected_database) - assert_equal expected_database, ActiveRecord::Base.connection_config[:database] if environment_loaded + assert_equal expected_database, ActiveRecord::Base.connection_config[:database] output = rails("db:drop") assert_match(/Dropped database/, output) assert_not File.exist?(expected_database) @@ -52,17 +52,21 @@ module ApplicationTests test "db:create and db:drop respect environment setting" do app_file "config/database.yml", <<-YAML development: - database: <%= Rails.application.config.database %> + database: db/development.sqlite3 adapter: sqlite3 YAML app_file "config/environments/development.rb", <<-RUBY Rails.application.configure do - config.database = "db/development.sqlite3" + config.read_encrypted_secrets = true end RUBY - db_create_and_drop "db/development.sqlite3", environment_loaded: false + app "development" + + assert_equal true, Rails.application.config.read_encrypted_secrets + + db_create_and_drop "db/development.sqlite3" end def with_database_existing @@ -93,7 +97,7 @@ module ApplicationTests test "db:create failure because bad permissions" do with_bad_permissions do output = rails("db:create", allow_failure: true) - assert_match(/Couldn't create database/, output) + assert_match("Couldn't create '#{database_url_db_name}' database. Please check your configuration.", output) assert_equal 1, $?.exitstatus end end diff --git a/railties/test/application/rake/multi_dbs_test.rb b/railties/test/application/rake/multi_dbs_test.rb index 07d96fcb56..f86bb9641f 100644 --- a/railties/test/application/rake/multi_dbs_test.rb +++ b/railties/test/application/rake/multi_dbs_test.rb @@ -127,36 +127,36 @@ EOS test "db:create and db:drop works on all databases for env" do require "#{app_path}/config/environment" - ActiveRecord::Base.configurations[Rails.env].each do |namespace, config| - db_create_and_drop namespace, config["database"] + ActiveRecord::Base.configurations.configs_for(Rails.env).each do |db_config| + db_create_and_drop db_config.spec_name, db_config.config["database"] end end test "db:create:namespace and db:drop:namespace works on specified databases" do require "#{app_path}/config/environment" - ActiveRecord::Base.configurations[Rails.env].each do |namespace, config| - db_create_and_drop_namespace namespace, config["database"] + ActiveRecord::Base.configurations.configs_for(Rails.env).each do |db_config| + db_create_and_drop_namespace db_config.spec_name, db_config.config["database"] end end test "db:migrate and db:schema:dump and db:schema:load works on all databases" do require "#{app_path}/config/environment" - ActiveRecord::Base.configurations[Rails.env].each do |namespace, config| - db_migrate_and_schema_dump_and_load namespace, config["database"], "schema" + ActiveRecord::Base.configurations.configs_for(Rails.env).each do |db_config| + db_migrate_and_schema_dump_and_load db_config.spec_name, db_config.config["database"], "schema" end end test "db:migrate and db:structure:dump and db:structure:load works on all databases" do require "#{app_path}/config/environment" - ActiveRecord::Base.configurations[Rails.env].each do |namespace, config| - db_migrate_and_schema_dump_and_load namespace, config["database"], "structure" + ActiveRecord::Base.configurations.configs_for(Rails.env).each do |db_config| + db_migrate_and_schema_dump_and_load db_config.spec_name, db_config.config["database"], "structure" end end test "db:migrate:namespace works" do require "#{app_path}/config/environment" - ActiveRecord::Base.configurations[Rails.env].each do |namespace, config| - db_migrate_namespaced namespace, config["database"] + ActiveRecord::Base.configurations.configs_for(Rails.env).each do |db_config| + db_migrate_namespaced db_config.spec_name, db_config.config["database"] end end end diff --git a/railties/test/generators/migration_generator_test.rb b/railties/test/generators/migration_generator_test.rb index 88a939a55a..5c57d607fc 100644 --- a/railties/test/generators/migration_generator_test.rb +++ b/railties/test/generators/migration_generator_test.rb @@ -51,12 +51,12 @@ class MigrationGeneratorTest < Rails::Generators::TestCase end def test_add_migration_with_table_having_from_in_title - migration = "add_email_address_to_blacklisted_from_campaign" + migration = "add_email_address_to_excluded_from_campaign" run_generator [migration, "email_address:string"] assert_migration "db/migrate/#{migration}.rb" do |content| assert_method :change, content do |change| - assert_match(/add_column :blacklisted_from_campaigns, :email_address, :string/, change) + assert_match(/add_column :excluded_from_campaigns, :email_address, :string/, change) end end end @@ -254,6 +254,15 @@ class MigrationGeneratorTest < Rails::Generators::TestCase end end + def test_migrations_paths_puts_migrations_in_that_folder + run_generator ["create_books", "--migrations_paths=db/test_migrate"] + assert_migration "db/test_migrate/create_books.rb" do |content| + assert_method :change, content do |change| + assert_match(/create_table :books/, change) + end + end + end + def test_should_create_empty_migrations_if_name_not_start_with_add_or_remove_or_create migration = "delete_books" run_generator [migration, "title:string", "content:text"] diff --git a/railties/test/generators/model_generator_test.rb b/railties/test/generators/model_generator_test.rb index 8d933e82c3..7febdfae96 100644 --- a/railties/test/generators/model_generator_test.rb +++ b/railties/test/generators/model_generator_test.rb @@ -7,6 +7,11 @@ class ModelGeneratorTest < Rails::Generators::TestCase include GeneratorsTestHelper arguments %w(Account name:string age:integer) + def setup + super + Rails::Generators::ModelHelpers.skip_warn = false + end + def test_help_shows_invoked_generators_options content = run_generator ["--help"] assert_match(/ActiveRecord options:/, content) @@ -37,12 +42,24 @@ class ModelGeneratorTest < Rails::Generators::TestCase end def test_plural_names_are_singularized - content = run_generator ["accounts".freeze] + content = run_generator ["accounts"] assert_file "app/models/account.rb", /class Account < ApplicationRecord/ assert_file "test/models/account_test.rb", /class AccountTest/ assert_match(/\[WARNING\] The model name 'accounts' was recognized as a plural, using the singular 'account' instead\. Override with --force-plural or setup custom inflection rules for this noun before running the generator\./, content) end + def test_unknown_inflection_rule_are_warned + content = run_generator ["porsche"] + assert_match("[WARNING] Rails cannot recover singular form from its plural form 'porsches'.\nPlease setup custom inflection rules for this noun before running the generator in config/initializers/inflections.rb.", content) + assert_file "app/models/porsche.rb", /class Porsche < ApplicationRecord/ + + uncountable_content = run_generator ["sheep"] + assert_no_match("[WARNING] Rails cannot recover singular form from its plural form", uncountable_content) + + regular_content = run_generator ["account"] + assert_no_match("[WARNING] Rails cannot recover singular form from its plural form", regular_content) + end + def test_model_with_underscored_parent_option run_generator ["account", "--parent", "admin/account"] assert_file "app/models/account.rb", /class Account < Admin::Account/ diff --git a/railties/test/generators/resource_generator_test.rb b/railties/test/generators/resource_generator_test.rb index 63a2cd3869..7a470d0d91 100644 --- a/railties/test/generators/resource_generator_test.rb +++ b/railties/test/generators/resource_generator_test.rb @@ -7,7 +7,11 @@ class ResourceGeneratorTest < Rails::Generators::TestCase include GeneratorsTestHelper arguments %w(account) - setup :copy_routes + def setup + super + copy_routes + Rails::Generators::ModelHelpers.skip_warn = false + end def test_help_with_inherited_options content = run_generator ["--help"] |