diff options
40 files changed, 344 insertions, 168 deletions
diff --git a/Gemfile.lock b/Gemfile.lock index 5e7f3d2b74..6a8f9040d7 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -70,7 +70,7 @@ PATH i18n (>= 0.7, < 2) minitest (~> 5.1) tzinfo (~> 1.1) - zeitwerk (~> 1.3, >= 1.3.4) + zeitwerk (~> 1.4) rails (6.0.0.beta3) actioncable (= 6.0.0.beta3) actionmailbox (= 6.0.0.beta3) @@ -517,7 +517,7 @@ GEM websocket-extensions (0.1.3) xpath (3.2.0) nokogiri (~> 1.8) - zeitwerk (1.3.4) + zeitwerk (1.4.0) PLATFORMS java diff --git a/actionpack/lib/action_dispatch/http/mime_type.rb b/actionpack/lib/action_dispatch/http/mime_type.rb index c3e0ea3c89..962d10d81b 100644 --- a/actionpack/lib/action_dispatch/http/mime_type.rb +++ b/actionpack/lib/action_dispatch/http/mime_type.rb @@ -170,6 +170,7 @@ module Mime def parse(accept_header) if !accept_header.include?(",") accept_header = accept_header.split(PARAMETER_SEPARATOR_REGEXP).first + return [] unless accept_header parse_trailing_star(accept_header) || [Mime::Type.lookup(accept_header)].compact else list, index = [], 0 @@ -221,7 +222,18 @@ module Mime attr_reader :hash + MIME_NAME = "[a-zA-Z0-9][a-zA-Z0-9#{Regexp.escape('!#$&-^_.+')}]{0,126}" + MIME_PARAMETER_KEY = "[a-zA-Z0-9][a-zA-Z0-9#{Regexp.escape('!#$&-^_.+')}]{0,126}" + MIME_PARAMETER_VALUE = "#{Regexp.escape('"')}?[a-zA-Z0-9][a-zA-Z0-9#{Regexp.escape('!#$&-^_.+')}]{0,126}#{Regexp.escape('"')}?" + MIME_PARAMETER = "\s*\;\s+#{MIME_PARAMETER_KEY}(?:\=#{MIME_PARAMETER_VALUE})?" + MIME_REGEXP = /\A(?:\*\/\*|#{MIME_NAME}\/(?:\*|#{MIME_NAME})(?:\s*#{MIME_PARAMETER}\s*)*)\z/ + + class InvalidMimeType < StandardError; end + def initialize(string, symbol = nil, synonyms = []) + unless MIME_REGEXP.match?(string) + raise InvalidMimeType, "#{string.inspect} is not a valid MIME type" + end @symbol, @synonyms = symbol, synonyms @string = string @hash = [@string, @synonyms, @symbol].hash diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb index 1611a8b3dd..b69bcab05c 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -338,7 +338,7 @@ module ActionDispatch def update_cookies_from_jar request_jar = @request.cookie_jar.instance_variable_get(:@cookies) - set_cookies = request_jar.reject { |k, _| @delete_cookies.key?(k) } + set_cookies = request_jar.reject { |k, _| @delete_cookies.key?(k) || @set_cookies.key?(k) } @cookies.update set_cookies if set_cookies end diff --git a/actionpack/lib/action_dispatch/middleware/stack.rb b/actionpack/lib/action_dispatch/middleware/stack.rb index 7a2fcd6db7..f0c869fba0 100644 --- a/actionpack/lib/action_dispatch/middleware/stack.rb +++ b/actionpack/lib/action_dispatch/middleware/stack.rb @@ -34,7 +34,28 @@ module ActionDispatch end def build(app) - klass.new(app, *args, &block) + InstrumentationProxy.new(klass.new(app, *args, &block), inspect) + end + end + + # This class is used to instrument the execution of a single middleware. + # It proxies the `call` method transparently and instruments the method + # call. + class InstrumentationProxy + EVENT_NAME = "process_middleware.action_dispatch" + + def initialize(middleware, class_name) + @middleware = middleware + + @payload = { + middleware: class_name, + } + end + + def call(env) + ActiveSupport::Notifications.instrument(EVENT_NAME, @payload) do + @middleware.call(env) + end end end diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index d67044b4ac..da3ade652e 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -1440,6 +1440,9 @@ module ActionDispatch # Allows you to specify the default value for optional +format+ # segment or disable it by supplying +false+. # + # [:param] + # Allows you to override the default param name of +:id+ in the URL. + # # === Examples # # # routes call <tt>Admin::PostsController</tt> diff --git a/actionpack/lib/action_dispatch/testing/request_encoder.rb b/actionpack/lib/action_dispatch/testing/request_encoder.rb index 9889f61951..6c65bec62f 100644 --- a/actionpack/lib/action_dispatch/testing/request_encoder.rb +++ b/actionpack/lib/action_dispatch/testing/request_encoder.rb @@ -38,8 +38,8 @@ module ActionDispatch end def self.parser(content_type) - mime = Mime::Type.lookup(content_type) - encoder(mime ? mime.ref : nil).response_parser + type = Mime::Type.lookup(content_type).ref if content_type + encoder(type).response_parser end def self.encoder(name) diff --git a/actionpack/test/controller/new_base/content_negotiation_test.rb b/actionpack/test/controller/new_base/content_negotiation_test.rb index 6de91c57b7..00b2798aeb 100644 --- a/actionpack/test/controller/new_base/content_negotiation_test.rb +++ b/actionpack/test/controller/new_base/content_negotiation_test.rb @@ -25,7 +25,7 @@ module ContentNegotiation assert_body "Hello world text/html!" end - test "A js or */* Accept header on xhr will return HTML" do + test "A js or */* Accept header on xhr will return JavaScript" do get "/content_negotiation/basic/hello", headers: { "HTTP_ACCEPT" => "text/javascript, */*" }, xhr: true assert_body "Hello world text/javascript!" end diff --git a/actionpack/test/controller/show_exceptions_test.rb b/actionpack/test/controller/show_exceptions_test.rb index 2094aa1aed..8724f9bcdb 100644 --- a/actionpack/test/controller/show_exceptions_test.rb +++ b/actionpack/test/controller/show_exceptions_test.rb @@ -99,15 +99,16 @@ module ShowExceptions class ShowFailsafeExceptionsTest < ActionDispatch::IntegrationTest def test_render_failsafe_exception @app = ShowExceptionsOverriddenController.action(:boom) - @exceptions_app = @app.instance_variable_get(:@exceptions_app) - @app.instance_variable_set(:@exceptions_app, nil) + middleware = @app.instance_variable_get(:@middleware) + @exceptions_app = middleware.instance_variable_get(:@exceptions_app) + middleware.instance_variable_set(:@exceptions_app, nil) $stderr = StringIO.new get "/", headers: { "HTTP_ACCEPT" => "text/json" } assert_response :internal_server_error assert_equal "text/plain", response.content_type.to_s ensure - @app.instance_variable_set(:@exceptions_app, @exceptions_app) + middleware.instance_variable_set(:@exceptions_app, @exceptions_app) $stderr = STDERR end end diff --git a/actionpack/test/dispatch/cookies_test.rb b/actionpack/test/dispatch/cookies_test.rb index 4aaac1320e..2c67bb779f 100644 --- a/actionpack/test/dispatch/cookies_test.rb +++ b/actionpack/test/dispatch/cookies_test.rb @@ -123,6 +123,11 @@ class CookiesTest < ActionController::TestCase head :ok end + def set_cookie_if_not_present + cookies["user_name"] = "alice" unless cookies["user_name"].present? + head :ok + end + def logout cookies.delete("user_name") head :ok @@ -1128,6 +1133,14 @@ class CookiesTest < ActionController::TestCase assert_equal "bar", @controller.encrypted_cookie end + def test_cookie_override + get :set_cookie_if_not_present + assert_equal "alice", cookies["user_name"] + cookies["user_name"] = "bob" + get :set_cookie_if_not_present + assert_equal "bob", cookies["user_name"] + end + def test_signed_cookie_with_expires_set_relatively request.env["action_dispatch.use_cookies_with_metadata"] = true diff --git a/actionpack/test/dispatch/middleware_stack_test.rb b/actionpack/test/dispatch/middleware_stack_test.rb index 5f43e5a3c5..90f2eccd19 100644 --- a/actionpack/test/dispatch/middleware_stack_test.rb +++ b/actionpack/test/dispatch/middleware_stack_test.rb @@ -3,13 +3,24 @@ require "abstract_unit" class MiddlewareStackTest < ActiveSupport::TestCase - class FooMiddleware; end - class BarMiddleware; end - class BazMiddleware; end - class HiyaMiddleware; end - class BlockMiddleware + class Base + def initialize(app) + @app = app + end + + def call(env) + @app.call(env) + end + end + + class FooMiddleware < Base; end + class BarMiddleware < Base; end + class BazMiddleware < Base; end + class HiyaMiddleware < Base; end + class BlockMiddleware < Base attr_reader :block - def initialize(&block) + def initialize(app, &block) + super(app) @block = block end end @@ -109,6 +120,24 @@ class MiddlewareStackTest < ActiveSupport::TestCase assert_equal @stack.last, @stack.last end + test "instruments the execution of middlewares" do + app = @stack.build(proc { |env| [200, {}, []] }) + env = {} + + events = [] + + subscriber = proc do |*args| + events << ActiveSupport::Notifications::Event.new(*args) + end + + ActiveSupport::Notifications.subscribed(subscriber, "process_middleware.action_dispatch") do + app.call(env) + end + + assert_equal 2, events.count + assert_equal ["MiddlewareStackTest::BarMiddleware", "MiddlewareStackTest::FooMiddleware"], events.map { |e| e.payload[:middleware] } + end + test "includes a middleware" do assert_equal true, @stack.include?(ActionDispatch::MiddlewareStack::Middleware.new(BarMiddleware, nil, nil)) end diff --git a/actionpack/test/dispatch/mime_type_test.rb b/actionpack/test/dispatch/mime_type_test.rb index 45d91883c0..50f6c06fee 100644 --- a/actionpack/test/dispatch/mime_type_test.rb +++ b/actionpack/test/dispatch/mime_type_test.rb @@ -174,4 +174,51 @@ class MimeTypeTest < ActiveSupport::TestCase assert_not (Mime[:js] !~ "application/javascript") assert Mime[:html] =~ "application/xhtml+xml" end + + test "can be initialized with wildcards" do + assert_equal "*/*", Mime::Type.new("*/*").to_s + assert_equal "text/*", Mime::Type.new("text/*").to_s + assert_equal "video/*", Mime::Type.new("video/*").to_s + end + + test "can be initialized with parameters" do + assert_equal "text/html; parameter", Mime::Type.new("text/html; parameter").to_s + assert_equal "text/html; parameter=abc", Mime::Type.new("text/html; parameter=abc").to_s + assert_equal 'text/html; parameter="abc"', Mime::Type.new('text/html; parameter="abc"').to_s + assert_equal 'text/html; parameter=abc; parameter2="xyz"', Mime::Type.new('text/html; parameter=abc; parameter2="xyz"').to_s + end + + test "invalid mime types raise error" do + assert_raises Mime::Type::InvalidMimeType do + Mime::Type.new("too/many/slash") + end + + assert_raises Mime::Type::InvalidMimeType do + Mime::Type.new("missingslash") + end + + assert_raises Mime::Type::InvalidMimeType do + Mime::Type.new("improper/semicolon;") + end + + assert_raises Mime::Type::InvalidMimeType do + Mime::Type.new('improper/semicolon; parameter=abc; parameter2="xyz";') + end + + assert_raises Mime::Type::InvalidMimeType do + Mime::Type.new("text/html, text/plain") + end + + assert_raises Mime::Type::InvalidMimeType do + Mime::Type.new("*/html") + end + + assert_raises Mime::Type::InvalidMimeType do + Mime::Type.new("") + end + + assert_raises Mime::Type::InvalidMimeType do + Mime::Type.new(nil) + end + end end diff --git a/actionview/lib/action_view/file_template.rb b/actionview/lib/action_view/file_template.rb index d838078f94..dea02176eb 100644 --- a/actionview/lib/action_view/file_template.rb +++ b/actionview/lib/action_view/file_template.rb @@ -22,11 +22,11 @@ module ActionView # to ensure that references to the template object can be marshalled as well. This means forgoing # the marshalling of the compiler mutex and instantiating that again on unmarshalling. def marshal_dump # :nodoc: - [ @identifier, @handler, @compiled, @locals, @virtual_path, @format, @variant ] + [ @identifier, @handler, @compiled, @locals, @virtual_path, @updated_at, @format, @variant ] end def marshal_load(array) # :nodoc: - @identifier, @handler, @compiled, @locals, @virtual_path, @format, @variant = *array + @identifier, @handler, @compiled, @locals, @virtual_path, @updated_at, @format, @variant = *array @compile_mutex = Mutex.new end end diff --git a/actionview/lib/action_view/template.rb b/actionview/lib/action_view/template.rb index e733c6d376..6e3af1536a 100644 --- a/actionview/lib/action_view/template.rb +++ b/actionview/lib/action_view/template.rb @@ -122,10 +122,10 @@ module ActionView extend Template::Handlers - attr_reader :source, :identifier, :handler, :original_encoding + attr_reader :source, :identifier, :handler, :original_encoding, :updated_at attr_reader :variable, :format, :variant, :locals, :virtual_path - def initialize(source, identifier, handler, format: nil, variant: nil, locals: nil, virtual_path: nil) + def initialize(source, identifier, handler, format: nil, variant: nil, locals: nil, virtual_path: nil, updated_at: nil) unless locals ActiveSupport::Deprecation.warn "ActionView::Template#initialize requires a locals parameter" locals = [] @@ -144,12 +144,19 @@ module ActionView $1.to_sym end + if updated_at + ActiveSupport::Deprecation.warn "ActionView::Template#updated_at is deprecated" + @updated_at = updated_at + else + @updated_at = Time.now + end @format = format @variant = variant @compile_mutex = Mutex.new end deprecate :original_encoding + deprecate :updated_at deprecate def virtual_path=(_); end deprecate def locals=(_); end deprecate def formats=(_); end @@ -260,11 +267,11 @@ module ActionView # to ensure that references to the template object can be marshalled as well. This means forgoing # the marshalling of the compiler mutex and instantiating that again on unmarshalling. def marshal_dump # :nodoc: - [ @source, @identifier, @handler, @compiled, @locals, @virtual_path, @format, @variant ] + [ @source, @identifier, @handler, @compiled, @locals, @virtual_path, @updated_at, @format, @variant ] end def marshal_load(array) # :nodoc: - @source, @identifier, @handler, @compiled, @locals, @virtual_path, @format, @variant = *array + @source, @identifier, @handler, @compiled, @locals, @virtual_path, @updated_at, @format, @variant = *array @compile_mutex = Mutex.new end diff --git a/actionview/lib/action_view/template/resolver.rb b/actionview/lib/action_view/template/resolver.rb index 07c44307ff..1c577348e5 100644 --- a/actionview/lib/action_view/template/resolver.rb +++ b/actionview/lib/action_view/template/resolver.rb @@ -184,17 +184,21 @@ module ActionView template_paths = reject_files_external_to_app(template_paths) unless outside_app_allowed template_paths.map do |template| - handler, format, variant = extract_handler_and_format_and_variant(template) - - FileTemplate.new(File.expand_path(template), handler, - virtual_path: path.virtual, - format: format, - variant: variant, - locals: locals - ) + build_template(template, path.virtual, locals) end end + def build_template(template, virtual_path, locals) + handler, format, variant = extract_handler_and_format_and_variant(template) + + FileTemplate.new(File.expand_path(template), handler, + virtual_path: virtual_path, + format: format, + variant: variant, + locals: locals + ) + end + def reject_files_external_to_app(files) files.reject { |filename| !inside_path?(@path, filename) } end @@ -385,5 +389,9 @@ module ActionView def self.instances [new(""), new("/")] end + + def build_template(template, virtual_path, locals) + super(template, nil, locals) + end end end diff --git a/actionview/test/template/fallback_file_system_resolver_test.rb b/actionview/test/template/fallback_file_system_resolver_test.rb new file mode 100644 index 0000000000..304cdb8a03 --- /dev/null +++ b/actionview/test/template/fallback_file_system_resolver_test.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require "abstract_unit" + +class FallbackFileSystemResolverTest < ActiveSupport::TestCase + def setup + @root_resolver = ActionView::FallbackFileSystemResolver.new("/") + end + + def test_should_have_no_virtual_path + templates = @root_resolver.find_all("hello_world.erb", "#{FIXTURE_LOAD_PATH}/test", false, locale: [], formats: [:html], variants: [], handlers: [:erb]) + assert_equal 1, templates.size + assert_equal "Hello world!", templates[0].source + assert_nil templates[0].virtual_path + end +end diff --git a/actionview/test/template/html_test.rb b/actionview/test/template/html_test.rb index c5fc8f906c..17f21cbbc5 100644 --- a/actionview/test/template/html_test.rb +++ b/actionview/test/template/html_test.rb @@ -8,9 +8,9 @@ class HTMLTest < ActiveSupport::TestCase end test "formats returns string for recognized MIME type when MIME does not have symbol" do - foo = Mime::Type.lookup("foo") + foo = Mime::Type.lookup("text/foo") assert_nil foo.to_sym - assert_equal "foo", ActionView::Template::HTML.new("", foo).format + assert_equal "text/foo", ActionView::Template::HTML.new("", foo).format end test "formats returns string for unknown MIME type" do diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index fd8c1da842..af7e46e649 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -24,7 +24,7 @@ module ActiveRecord RESTRICTED_CLASS_METHODS = %w(private public protected allocate new name parent superclass) - class GeneratedAttributeMethods < Module #:nodoc: + class GeneratedAttributeMethodsBuilder < Module #:nodoc: include Mutex_m end @@ -35,7 +35,8 @@ module ActiveRecord end def initialize_generated_modules # :nodoc: - @generated_attribute_methods = GeneratedAttributeMethods.new + @generated_attribute_methods = const_set(:GeneratedAttributeMethods, GeneratedAttributeMethodsBuilder.new) + private_constant :GeneratedAttributeMethods @attribute_methods_generated = false include @generated_attribute_methods @@ -88,7 +89,7 @@ module ActiveRecord # If ThisClass < ... < SomeSuperClass < ... < Base and SomeSuperClass # defines its own attribute method, then we don't want to overwrite that. defined = method_defined_within?(method_name, superclass, Base) && - ! superclass.instance_method(method_name).owner.is_a?(GeneratedAttributeMethods) + ! superclass.instance_method(method_name).owner.is_a?(GeneratedAttributeMethodsBuilder) defined || super end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index 9d24d839c1..2877530917 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -138,6 +138,10 @@ module ActiveRecord "'#{quote_string(value.to_s)}'" end + def sanitize_as_sql_comment(value) # :nodoc: + value.to_s.gsub(%r{ (/ (?: | \g<1>) \*) \+? \s* | \s* (\* (?: | \g<2>) /) }x, "") + end + private def type_casted_binds(binds) if binds.first.is_a?(Array) diff --git a/activerecord/lib/active_record/database_configurations.rb b/activerecord/lib/active_record/database_configurations.rb index a6c702cbbc..4656045fe5 100644 --- a/activerecord/lib/active_record/database_configurations.rb +++ b/activerecord/lib/active_record/database_configurations.rb @@ -106,7 +106,7 @@ module ActiveRecord build_db_config = configs.each_pair.flat_map do |env_name, config| walk_configs(env_name.to_s, "primary", config) - end.compact + end.flatten.compact if url = ENV["DATABASE_URL"] build_url_config(url, build_db_config) diff --git a/activerecord/lib/active_record/insert_all.rb b/activerecord/lib/active_record/insert_all.rb index 3833cb2fcf..98c98d61cd 100644 --- a/activerecord/lib/active_record/insert_all.rb +++ b/activerecord/lib/active_record/insert_all.rb @@ -2,12 +2,14 @@ module ActiveRecord class InsertAll - attr_reader :model, :connection, :inserts, :on_duplicate, :returning, :unique_by + attr_reader :model, :connection, :inserts, :keys + attr_reader :on_duplicate, :returning, :unique_by def initialize(model, inserts, on_duplicate:, returning: nil, unique_by: nil) raise ArgumentError, "Empty list of attributes passed" if inserts.blank? - @model, @connection, @inserts, @on_duplicate, @returning, @unique_by = model, model.connection, inserts, on_duplicate, returning, unique_by + @model, @connection, @inserts, @keys = model, model.connection, inserts, inserts.first.keys.map(&:to_s).to_set + @on_duplicate, @returning, @unique_by = on_duplicate, returning, unique_by @returning = (connection.supports_insert_returning? ? primary_keys : false) if @returning.nil? @returning = false if @returning == [] @@ -21,10 +23,6 @@ module ActiveRecord connection.exec_query to_sql, "Bulk Insert" end - def keys - inserts.first.keys.map(&:to_s) - end - def updatable_columns keys - readonly_columns - unique_by_columns end @@ -37,6 +35,17 @@ module ActiveRecord on_duplicate == :update end + def map_key_with_value + inserts.map do |attributes| + attributes = attributes.stringify_keys + verify_attributes(attributes) + + keys.map do |key| + yield key, attributes[key] + end + end + end + private def ensure_valid_options_for_connection! if returning && !connection.supports_insert_returning? @@ -76,6 +85,12 @@ module ActiveRecord Array.wrap(model.primary_key) end + def verify_attributes(attributes) + if keys != attributes.keys.to_set + raise ArgumentError, "All objects being inserted must have the same keys" + end + end + class Builder attr_reader :model @@ -91,29 +106,11 @@ module ActiveRecord end def values_list - columns = connection.schema_cache.columns_hash(model.table_name) - - column_names = columns.keys.to_set - keys = insert_all.keys.to_set - unknown_columns = keys - column_names + types = extract_types_from_columns_on(model.table_name, keys: insert_all.keys) - unless unknown_columns.empty? - raise UnknownAttributeError.new(model.new, unknown_columns.first) - end - - types = keys.map { |key| [ key, connection.lookup_cast_type_from_column(columns[key]) ] }.to_h - - values_list = insert_all.inserts.map do |attributes| - attributes = attributes.stringify_keys - - unless attributes.keys.to_set == keys - raise ArgumentError, "All objects being inserted must have the same keys" - end - - keys.map do |key| - bind = Relation::QueryAttribute.new(key, attributes[key], types[key]) - connection.with_yaml_fallback(bind.value_for_database) - end + values_list = insert_all.map_key_with_value do |key, value| + bind = Relation::QueryAttribute.new(key, value, types[key]) + connection.with_yaml_fallback(bind.value_for_database) end Arel::InsertManager.new.create_values_list(values_list).to_sql @@ -141,6 +138,15 @@ module ActiveRecord quote_columns(insert_all.keys).join(",") end + def extract_types_from_columns_on(table_name, keys:) + columns = connection.schema_cache.columns_hash(table_name) + + unknown_column = (keys - columns.keys).first + raise UnknownAttributeError.new(model.new, unknown_column) if unknown_column + + keys.map { |key| [ key, connection.lookup_cast_type_from_column(columns[key]) ] }.to_h + end + def quote_columns(columns) columns.map(&connection.method(:quote_column_name)) end diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 37179774fa..6d954a2b2e 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -342,6 +342,8 @@ module ActiveRecord # trigger Active Record callbacks or validations. However, values passed to #update_all will still go through # Active Record's normal type casting and serialization. # + # Note: As Active Record callbacks are not triggered, this method will not automatically update +updated_at+/+updated_on+ columns. + # # ==== Parameters # # * +updates+ - A string, array, or hash representing the SET part of an SQL statement. @@ -416,10 +418,10 @@ module ActiveRecord update_all updates end - # Touches all records in the current relation without instantiating records first with the updated_at/on attributes + # Touches all records in the current relation without instantiating records first with the +updated_at+/+updated_on+ attributes # set to the current time or the time specified. # This method can be passed attribute names and an optional time argument. - # If attribute names are passed, they are updated along with updated_at/on attributes. + # If attribute names are passed, they are updated along with +updated_at+/+updated_on+ attributes. # If no time argument is passed, the current time is used as default. # # === Examples diff --git a/activerecord/lib/arel/visitors/ibm_db.rb b/activerecord/lib/arel/visitors/ibm_db.rb index 0ffc0725f7..5cf958f5f0 100644 --- a/activerecord/lib/arel/visitors/ibm_db.rb +++ b/activerecord/lib/arel/visitors/ibm_db.rb @@ -10,7 +10,8 @@ module Arel # :nodoc: all end def visit_Arel_Nodes_OptimizerHints(o, collector) - collector << "/* <OPTGUIDELINES>#{sanitize_as_sql_comment(o).join}</OPTGUIDELINES> */" + hints = o.expr.map { |v| sanitize_as_sql_comment(v) }.join + collector << "/* <OPTGUIDELINES>#{hints}</OPTGUIDELINES> */" end def visit_Arel_Nodes_Limit(o, collector) diff --git a/activerecord/lib/arel/visitors/informix.rb b/activerecord/lib/arel/visitors/informix.rb index cd43be8858..1a4ad1c8d8 100644 --- a/activerecord/lib/arel/visitors/informix.rb +++ b/activerecord/lib/arel/visitors/informix.rb @@ -43,7 +43,8 @@ module Arel # :nodoc: all end def visit_Arel_Nodes_OptimizerHints(o, collector) - collector << "/*+ #{sanitize_as_sql_comment(o).join(", ")} */" + hints = o.expr.map { |v| sanitize_as_sql_comment(v) }.join(", ") + collector << "/*+ #{hints} */" end def visit_Arel_Nodes_Offset(o, collector) diff --git a/activerecord/lib/arel/visitors/mssql.rb b/activerecord/lib/arel/visitors/mssql.rb index 85815baca2..8475139870 100644 --- a/activerecord/lib/arel/visitors/mssql.rb +++ b/activerecord/lib/arel/visitors/mssql.rb @@ -82,7 +82,8 @@ module Arel # :nodoc: all end def visit_Arel_Nodes_OptimizerHints(o, collector) - collector << "OPTION (#{sanitize_as_sql_comment(o).join(", ")})" + hints = o.expr.map { |v| sanitize_as_sql_comment(v) }.join(", ") + collector << "OPTION (#{hints})" end def get_offset_limit_clause(o) @@ -106,7 +107,7 @@ module Arel # :nodoc: all collector = visit o.relation, collector if o.wheres.any? collector << " WHERE " - inject_join o.wheres, collector, AND + inject_join o.wheres, collector, " AND " else collector end diff --git a/activerecord/lib/arel/visitors/postgresql.rb b/activerecord/lib/arel/visitors/postgresql.rb index 920776b4dc..8296f1cdc1 100644 --- a/activerecord/lib/arel/visitors/postgresql.rb +++ b/activerecord/lib/arel/visitors/postgresql.rb @@ -3,11 +3,6 @@ module Arel # :nodoc: all module Visitors class PostgreSQL < Arel::Visitors::ToSql - CUBE = "CUBE" - ROLLUP = "ROLLUP" - GROUPING_SETS = "GROUPING SETS" - LATERAL = "LATERAL" - private def visit_Arel_Nodes_Matches(o, collector) @@ -57,23 +52,22 @@ module Arel # :nodoc: all end def visit_Arel_Nodes_Cube(o, collector) - collector << CUBE + collector << "CUBE" grouping_array_or_grouping_element o, collector end def visit_Arel_Nodes_RollUp(o, collector) - collector << ROLLUP + collector << "ROLLUP" grouping_array_or_grouping_element o, collector end def visit_Arel_Nodes_GroupingSet(o, collector) - collector << GROUPING_SETS + collector << "GROUPING SETS" grouping_array_or_grouping_element o, collector end def visit_Arel_Nodes_Lateral(o, collector) - collector << LATERAL - collector << SPACE + collector << "LATERAL " grouping_parentheses o, collector end diff --git a/activerecord/lib/arel/visitors/to_sql.rb b/activerecord/lib/arel/visitors/to_sql.rb index 583f920290..1630226085 100644 --- a/activerecord/lib/arel/visitors/to_sql.rb +++ b/activerecord/lib/arel/visitors/to_sql.rb @@ -9,59 +9,6 @@ module Arel # :nodoc: all end class ToSql < Arel::Visitors::Visitor - ## - # This is some roflscale crazy stuff. I'm roflscaling this because - # building SQL queries is a hotspot. I will explain the roflscale so that - # others will not rm this code. - # - # In YARV, string literals in a method body will get duped when the byte - # code is executed. Let's take a look: - # - # > puts RubyVM::InstructionSequence.new('def foo; "bar"; end').disasm - # - # == disasm: <RubyVM::InstructionSequence:foo@<compiled>>===== - # 0000 trace 8 - # 0002 trace 1 - # 0004 putstring "bar" - # 0006 trace 16 - # 0008 leave - # - # The `putstring` bytecode will dup the string and push it on the stack. - # In many cases in our SQL visitor, that string is never mutated, so there - # is no need to dup the literal. - # - # If we change to a constant lookup, the string will not be duped, and we - # can reduce the objects in our system: - # - # > puts RubyVM::InstructionSequence.new('BAR = "bar"; def foo; BAR; end').disasm - # - # == disasm: <RubyVM::InstructionSequence:foo@<compiled>>======== - # 0000 trace 8 - # 0002 trace 1 - # 0004 getinlinecache 11, <ic:0> - # 0007 getconstant :BAR - # 0009 setinlinecache <ic:0> - # 0011 trace 16 - # 0013 leave - # - # `getconstant` should be a hash lookup, and no object is duped when the - # value of the constant is pushed on the stack. Hence the crazy - # constants below. - # - # `matches` and `doesNotMatch` operate case-insensitively via Visitor subclasses - # specialized for specific databases when necessary. - # - - WHERE = " WHERE " # :nodoc: - SPACE = " " # :nodoc: - COMMA = ", " # :nodoc: - GROUP_BY = " GROUP BY " # :nodoc: - ORDER_BY = " ORDER BY " # :nodoc: - WINDOW = " WINDOW " # :nodoc: - AND = " AND " # :nodoc: - - DISTINCT = "DISTINCT" # :nodoc: - def initialize(connection) super() @connection = connection @@ -161,10 +108,10 @@ module Arel # :nodoc: all else collector << quote(value).to_s end - collector << COMMA unless k == row_len + collector << ", " unless k == row_len end collector << ")" - collector << COMMA unless i == len + collector << ", " unless i == len } collector end @@ -172,7 +119,7 @@ module Arel # :nodoc: all def visit_Arel_Nodes_SelectStatement(o, collector) if o.with collector = visit o.with, collector - collector << SPACE + collector << " " end collector = o.cores.inject(collector) { |c, x| @@ -180,11 +127,11 @@ module Arel # :nodoc: all } unless o.orders.empty? - collector << ORDER_BY + collector << " ORDER BY " len = o.orders.length - 1 o.orders.each_with_index { |x, i| collector = visit(x, collector) - collector << COMMA unless len == i + collector << ", " unless len == i } end @@ -203,26 +150,27 @@ module Arel # :nodoc: all collector = collect_optimizer_hints(o, collector) collector = maybe_visit o.set_quantifier, collector - collect_nodes_for o.projections, collector, SPACE + collect_nodes_for o.projections, collector, " " if o.source && !o.source.empty? collector << " FROM " collector = visit o.source, collector end - collect_nodes_for o.wheres, collector, WHERE, AND - collect_nodes_for o.groups, collector, GROUP_BY - collect_nodes_for o.havings, collector, " HAVING ", AND - collect_nodes_for o.windows, collector, WINDOW + collect_nodes_for o.wheres, collector, " WHERE ", " AND " + collect_nodes_for o.groups, collector, " GROUP BY " + collect_nodes_for o.havings, collector, " HAVING ", " AND " + collect_nodes_for o.windows, collector, " WINDOW " collector end def visit_Arel_Nodes_OptimizerHints(o, collector) - collector << "/*+ #{sanitize_as_sql_comment(o).join(" ")} */" + hints = o.expr.map { |v| sanitize_as_sql_comment(v) }.join(" ") + collector << "/*+ #{hints} */" end - def collect_nodes_for(nodes, collector, spacer, connector = COMMA) + def collect_nodes_for(nodes, collector, spacer, connector = ", ") unless nodes.empty? collector << spacer inject_join nodes, collector, connector @@ -234,7 +182,7 @@ module Arel # :nodoc: all end def visit_Arel_Nodes_Distinct(o, collector) - collector << DISTINCT + collector << "DISTINCT" end def visit_Arel_Nodes_DistinctOn(o, collector) @@ -243,12 +191,12 @@ module Arel # :nodoc: all def visit_Arel_Nodes_With(o, collector) collector << "WITH " - inject_join o.children, collector, COMMA + inject_join o.children, collector, ", " end def visit_Arel_Nodes_WithRecursive(o, collector) collector << "WITH RECURSIVE " - inject_join o.children, collector, COMMA + inject_join o.children, collector, ", " end def visit_Arel_Nodes_Union(o, collector) @@ -281,13 +229,13 @@ module Arel # :nodoc: all collect_nodes_for o.partitions, collector, "PARTITION BY " if o.orders.any? - collector << SPACE if o.partitions.any? + collector << " " if o.partitions.any? collector << "ORDER BY " collector = inject_join o.orders, collector, ", " end if o.framing - collector << SPACE if o.partitions.any? || o.orders.any? + collector << " " if o.partitions.any? || o.orders.any? collector = visit o.framing, collector end @@ -492,8 +440,8 @@ module Arel # :nodoc: all collector = visit o.left, collector end if o.right.any? - collector << SPACE if o.left - collector = inject_join o.right, collector, SPACE + collector << " " if o.left + collector = inject_join o.right, collector, " " end collector end @@ -513,7 +461,7 @@ module Arel # :nodoc: all def visit_Arel_Nodes_FullOuterJoin(o, collector) collector << "FULL OUTER JOIN " collector = visit o.left, collector - collector << SPACE + collector << " " visit o.right, collector end @@ -527,7 +475,7 @@ module Arel # :nodoc: all def visit_Arel_Nodes_RightOuterJoin(o, collector) collector << "RIGHT OUTER JOIN " collector = visit o.left, collector - collector << SPACE + collector << " " visit o.right, collector end @@ -535,7 +483,7 @@ module Arel # :nodoc: all collector << "INNER JOIN " collector = visit o.left, collector if o.right - collector << SPACE + collector << " " visit(o.right, collector) else collector @@ -785,10 +733,9 @@ module Arel # :nodoc: all @connection.quote_column_name(name) end - def sanitize_as_sql_comment(o) - o.expr.map { |v| - v.gsub(%r{ (/ (?: | \g<1>) \*) \+? \s* | \s* (\* (?: | \g<2>) /) }x, "") - } + def sanitize_as_sql_comment(value) + return value if Arel::Nodes::SqlLiteral === value + @connection.sanitize_as_sql_comment(value) end def collect_optimizer_hints(o, collector) 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 dd79bcf542..cb2c74f1ca 100644 --- a/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb +++ b/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb @@ -8,7 +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 :database, type: :string, aliases: %i(db), desc: "The database for your migration. By default, the current environment's primary database is used." + class_option :database, type: :string, aliases: %i(--db), desc: "The database for your migration. By default, the current environment's primary database is used." def create_migration_file set_local_assigns! diff --git a/activerecord/lib/rails/generators/active_record/model/model_generator.rb b/activerecord/lib/rails/generators/active_record/model/model_generator.rb index eac504f9f1..c71bbdcab8 100644 --- a/activerecord/lib/rails/generators/active_record/model/model_generator.rb +++ b/activerecord/lib/rails/generators/active_record/model/model_generator.rb @@ -14,7 +14,7 @@ module ActiveRecord class_option :parent, type: :string, desc: "The parent class for the generated model" class_option :indexes, type: :boolean, default: true, desc: "Add indexes for references and belongs_to columns" class_option :primary_key_type, type: :string, desc: "The type for primary key" - class_option :database, type: :string, aliases: %i(db), desc: "The database for your model's migration. By default, the current environment's primary database is used." + class_option :database, type: :string, aliases: %i(--db), desc: "The database for your model's migration. By default, the current environment's primary database is used." # creates the migration file for the model. def create_migration_file diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb index f51c87ef2b..9fd62dcf72 100644 --- a/activerecord/test/cases/attribute_methods_test.rb +++ b/activerecord/test/cases/attribute_methods_test.rb @@ -1083,7 +1083,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase test "generated attribute methods ancestors have correct class" do mod = Topic.send(:generated_attribute_methods) - assert_match %r(GeneratedAttributeMethods), mod.inspect + assert_match %r(Topic::GeneratedAttributeMethods), mod.inspect end private diff --git a/activestorage/db/update_migrate/20180723000244_add_foreign_key_constraint_to_active_storage_attachments_for_blob_id.rb b/activestorage/db/update_migrate/20180723000244_add_foreign_key_constraint_to_active_storage_attachments_for_blob_id.rb index 6830203cd6..5472e3c87b 100644 --- a/activestorage/db/update_migrate/20180723000244_add_foreign_key_constraint_to_active_storage_attachments_for_blob_id.rb +++ b/activestorage/db/update_migrate/20180723000244_add_foreign_key_constraint_to_active_storage_attachments_for_blob_id.rb @@ -1,6 +1,8 @@ class AddForeignKeyConstraintToActiveStorageAttachmentsForBlobId < ActiveRecord::Migration[6.0] def up - unless foreign_key_exists?(:active_storage_attachments, column: :blob_id) + return if foreign_key_exists?(:active_storage_attachments, column: :blob_id) + + if table_exists?(:active_storage_blobs) add_foreign_key :active_storage_attachments, :active_storage_blobs, column: :blob_id end end diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index b2330f2c9d..63e2e44597 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,3 +1,17 @@ +* Fix `Time#advance` to work with dates before 1001-03-07 + + Before: + + Time.utc(1001, 3, 6).advance(years: -1) # => 1000-03-05 00:00:00 UTC + + After + + Time.utc(1001, 3, 6).advance(years: -1) # => 1000-03-06 00:00:00 UTC + + Note that this doesn't affect `DateTime#advance` as that doesn't use a proleptic calendar. + + *Andrew White* + * In Zeitwerk mode, engines are now managed by the `main` autoloader. Engines may reference application constants, if the application is reloaded and we do not reload engines, they won't use the reloaded application code. *Xavier Noria* diff --git a/activesupport/activesupport.gemspec b/activesupport/activesupport.gemspec index e55d73c717..51f5086cca 100644 --- a/activesupport/activesupport.gemspec +++ b/activesupport/activesupport.gemspec @@ -34,5 +34,5 @@ Gem::Specification.new do |s| s.add_dependency "tzinfo", "~> 1.1" s.add_dependency "minitest", "~> 5.1" s.add_dependency "concurrent-ruby", "~> 1.0", ">= 1.0.2" - s.add_dependency "zeitwerk", "~> 1.3", ">= 1.3.4" + s.add_dependency "zeitwerk", "~> 1.4" end diff --git a/activesupport/lib/active_support/core_ext/time/calculations.rb b/activesupport/lib/active_support/core_ext/time/calculations.rb index 120768dec5..f09a6271ad 100644 --- a/activesupport/lib/active_support/core_ext/time/calculations.rb +++ b/activesupport/lib/active_support/core_ext/time/calculations.rb @@ -170,8 +170,7 @@ class Time options[:hours] = options.fetch(:hours, 0) + 24 * partial_days end - d = to_date.advance(options) - d = d.gregorian if d.julian? + d = to_date.gregorian.advance(options) time_advanced_by_date = change(year: d.year, month: d.month, day: d.day) seconds_to_advance = \ options.fetch(:seconds, 0) + diff --git a/activesupport/lib/active_support/dependencies/zeitwerk_integration.rb b/activesupport/lib/active_support/dependencies/zeitwerk_integration.rb index e00307d257..c6fdade006 100644 --- a/activesupport/lib/active_support/dependencies/zeitwerk_integration.rb +++ b/activesupport/lib/active_support/dependencies/zeitwerk_integration.rb @@ -21,7 +21,11 @@ module ActiveSupport end def autoloaded_constants - (Rails.autoloaders.main.loaded + Rails.autoloaders.once.loaded).to_a + cpaths = [] + Rails.autoloaders.each do |autoloader| + cpaths.concat(autoloader.loaded_cpaths.to_a) + end + cpaths end def autoloaded?(object) diff --git a/activesupport/test/core_ext/time_ext_test.rb b/activesupport/test/core_ext/time_ext_test.rb index 7078f3506d..590b81b770 100644 --- a/activesupport/test/core_ext/time_ext_test.rb +++ b/activesupport/test/core_ext/time_ext_test.rb @@ -514,6 +514,8 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase assert_equal Time.local(1582, 10, 15, 15, 15, 10), Time.local(1582, 10, 14, 15, 15, 10).advance(days: 1) assert_equal Time.local(1582, 10, 5, 15, 15, 10), Time.local(1582, 10, 4, 15, 15, 10).advance(days: 1) assert_equal Time.local(1582, 10, 4, 15, 15, 10), Time.local(1582, 10, 5, 15, 15, 10).advance(days: -1) + assert_equal Time.local(999, 10, 4, 15, 15, 10), Time.local(1000, 10, 4, 15, 15, 10).advance(years: -1) + assert_equal Time.local(1000, 10, 4, 15, 15, 10), Time.local(999, 10, 4, 15, 15, 10).advance(years: 1) end def test_last_week diff --git a/guides/source/active_support_instrumentation.md b/guides/source/active_support_instrumentation.md index c1c3832b79..89e0e3afa8 100644 --- a/guides/source/active_support_instrumentation.md +++ b/guides/source/active_support_instrumentation.md @@ -203,6 +203,15 @@ INFO. Additional keys may be added by the caller. | ------- | ---------------- | | `:keys` | Unpermitted keys | +Action Dispatch +--------------- + +### process_middleware.action_dispatch + +| Key | Value | +| ------------- | ---------------------- | +| `:middleware` | Name of the middleware | + Action View ----------- @@ -424,7 +433,7 @@ INFO. Cache stores may add their own keys ``` Active Job --------- +---------- ### enqueue_at.active_job diff --git a/railties/test/generators/migration_generator_test.rb b/railties/test/generators/migration_generator_test.rb index 5812cbdfc9..acc5fc3b25 100644 --- a/railties/test/generators/migration_generator_test.rb +++ b/railties/test/generators/migration_generator_test.rb @@ -265,6 +265,17 @@ class MigrationGeneratorTest < Rails::Generators::TestCase end end + def test_database_puts_migrations_in_configured_folder_with_aliases + with_secondary_database_configuration do + run_generator ["create_books", "--db=secondary"] + assert_migration "db/secondary_migrate/create_books.rb" do |content| + assert_method :change, content do |change| + assert_match(/create_table :books/, change) + end + 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 134659f285..bdb430369e 100644 --- a/railties/test/generators/model_generator_test.rb +++ b/railties/test/generators/model_generator_test.rb @@ -403,6 +403,17 @@ class ModelGeneratorTest < Rails::Generators::TestCase end end + def test_database_puts_migrations_in_configured_folder_with_aliases + with_secondary_database_configuration do + run_generator ["account", "--db=secondary"] + assert_migration "db/secondary_migrate/create_accounts.rb" do |content| + assert_method :change, content do |change| + assert_match(/create_table :accounts/, change) + end + end + end + end + def test_required_belongs_to_adds_required_association run_generator ["account", "supplier:references{required}"] diff --git a/railties/test/generators/scaffold_generator_test.rb b/railties/test/generators/scaffold_generator_test.rb index f672e301a7..715ad938f4 100644 --- a/railties/test/generators/scaffold_generator_test.rb +++ b/railties/test/generators/scaffold_generator_test.rb @@ -479,6 +479,14 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase end end + def test_scaffold_generator_database_with_aliases + with_secondary_database_configuration do + run_generator ["posts", "--db=secondary"] + + assert_migration "db/secondary_migrate/create_posts.rb" + end + end + def test_scaffold_generator_password_digest run_generator ["user", "name", "password:digest"] diff --git a/railties/test/isolation/abstract_unit.rb b/railties/test/isolation/abstract_unit.rb index 14cdf1ab7c..3fcfaa9623 100644 --- a/railties/test/isolation/abstract_unit.rb +++ b/railties/test/isolation/abstract_unit.rb @@ -123,6 +123,8 @@ module TestHelpers adapter: sqlite3 pool: 5 timeout: 5000 + variables: + statement_timeout: 1000 development: primary: <<: *default |
