diff options
113 files changed, 1200 insertions, 548 deletions
diff --git a/Gemfile.lock b/Gemfile.lock index ce50c043e2..a291e94936 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -70,7 +70,7 @@ PATH i18n (>= 0.7, < 2) minitest (~> 5.1) tzinfo (~> 1.1) - zeitwerk (~> 1.2) + zeitwerk (~> 1.3, >= 1.3.1) rails (6.0.0.beta1) actioncable (= 6.0.0.beta1) actionmailbox (= 6.0.0.beta1) @@ -517,7 +517,7 @@ GEM websocket-extensions (0.1.3) xpath (3.2.0) nokogiri (~> 1.8) - zeitwerk (1.2.0) + zeitwerk (1.3.1) PLATFORMS java diff --git a/actionmailbox/lib/action_mailbox/engine.rb b/actionmailbox/lib/action_mailbox/engine.rb index 27334c037e..039f04ac2f 100644 --- a/actionmailbox/lib/action_mailbox/engine.rb +++ b/actionmailbox/lib/action_mailbox/engine.rb @@ -29,13 +29,11 @@ module ActionMailbox end end - initializer "action_mailbox.ingress" do - config.after_initialize do |app| + initializer "action_mailbox.ingress" do |app| + config.to_prepare do if ActionMailbox.ingress = app.config.action_mailbox.ingress.presence - config.to_prepare do - if ingress_controller_class = "ActionMailbox::Ingresses::#{ActionMailbox.ingress.to_s.classify}::InboundEmailsController".safe_constantize - ingress_controller_class.prepare - end + if ingress_controller_class = "ActionMailbox::Ingresses::#{ActionMailbox.ingress.to_s.classify}::InboundEmailsController".safe_constantize + ingress_controller_class.prepare end end end diff --git a/actionmailer/lib/action_mailer/mail_delivery_job.rb b/actionmailer/lib/action_mailer/mail_delivery_job.rb index 93778edfce..609c6a72d9 100644 --- a/actionmailer/lib/action_mailer/mail_delivery_job.rb +++ b/actionmailer/lib/action_mailer/mail_delivery_job.rb @@ -3,7 +3,7 @@ require "active_job" module ActionMailer - # The <tt>ActionMailer::NewDeliveryJob</tt> class is used when you + # The <tt>ActionMailer::MailDeliveryJob</tt> class is used when you # want to send emails outside of the request-response cycle. It supports # sending either parameterized or normal mail. # diff --git a/actionmailer/test/caching_test.rb b/actionmailer/test/caching_test.rb index 22f310f39f..b658c96ec7 100644 --- a/actionmailer/test/caching_test.rb +++ b/actionmailer/test/caching_test.rb @@ -124,7 +124,7 @@ class FunctionalFragmentCachingTest < BaseCachingTest assert_match expected_body, email.body.encoded assert_match expected_body, - @store.read("views/caching_mailer/fragment_cache:#{template_digest("caching_mailer/fragment_cache")}/caching") + @store.read("views/caching_mailer/fragment_cache:#{template_digest("caching_mailer/fragment_cache", "html")}/caching") end def test_fragment_caching_in_partials @@ -133,7 +133,7 @@ class FunctionalFragmentCachingTest < BaseCachingTest assert_match(expected_body, email.body.encoded) assert_match(expected_body, - @store.read("views/caching_mailer/_partial:#{template_digest("caching_mailer/_partial")}/caching")) + @store.read("views/caching_mailer/_partial:#{template_digest("caching_mailer/_partial", "html")}/caching")) end def test_skip_fragment_cache_digesting @@ -183,15 +183,15 @@ class FunctionalFragmentCachingTest < BaseCachingTest end assert_equal "caching_mailer", payload[:mailer] - assert_equal [ :views, "caching_mailer/fragment_cache:#{template_digest("caching_mailer/fragment_cache")}", :caching ], payload[:key] + assert_equal [ :views, "caching_mailer/fragment_cache:#{template_digest("caching_mailer/fragment_cache", "html")}", :caching ], payload[:key] ensure @mailer.enable_fragment_cache_logging = true end private - def template_digest(name) - ActionView::Digestor.digest(name: name, finder: @mailer.lookup_context) + def template_digest(name, format) + ActionView::Digestor.digest(name: name, format: format, finder: @mailer.lookup_context) end end diff --git a/actionpack/lib/action_controller/metal/etag_with_template_digest.rb b/actionpack/lib/action_controller/metal/etag_with_template_digest.rb index 640c75536e..2f1544c69c 100644 --- a/actionpack/lib/action_controller/metal/etag_with_template_digest.rb +++ b/actionpack/lib/action_controller/metal/etag_with_template_digest.rb @@ -51,7 +51,7 @@ module ActionController end def lookup_and_digest_template(template) - ActionView::Digestor.digest name: template, finder: lookup_context + ActionView::Digestor.digest name: template, format: nil, finder: lookup_context end end end diff --git a/actionpack/lib/action_dispatch/http/upload.rb b/actionpack/lib/action_dispatch/http/upload.rb index 827f022ca2..0da8f5c14e 100644 --- a/actionpack/lib/action_dispatch/http/upload.rb +++ b/actionpack/lib/action_dispatch/http/upload.rb @@ -20,7 +20,6 @@ module ActionDispatch # A +Tempfile+ object with the actual uploaded file. Note that some of # its interface is available directly. attr_accessor :tempfile - alias :to_io :tempfile # A string with the headers of the multipart request. attr_accessor :headers @@ -84,6 +83,10 @@ module ActionDispatch def eof? @tempfile.eof? end + + def to_io + @tempfile.to_io + end end end end diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb index 10d85037ae..bb8b43ad4d 100644 --- a/actionpack/lib/action_dispatch/testing/integration.rb +++ b/actionpack/lib/action_dispatch/testing/integration.rb @@ -194,7 +194,7 @@ module ActionDispatch # Adds request headers characteristic of XMLHttpRequest e.g. HTTP_X_REQUESTED_WITH. # The headers will be merged into the Rack env hash. # - +as+: Used for encoding the request with different content type. - # Supports `:json` by default and will set the approriate request headers. + # Supports `:json` by default and will set the appropriate request headers. # The headers will be merged into the Rack env hash. # # This method is rarely used directly. Use +#get+, +#post+, or other standard diff --git a/actionpack/test/controller/caching_test.rb b/actionpack/test/controller/caching_test.rb index 5543f9120f..f09e812147 100644 --- a/actionpack/test/controller/caching_test.rb +++ b/actionpack/test/controller/caching_test.rb @@ -212,7 +212,7 @@ CACHED assert_equal expected_body, @response.body assert_equal "This bit's fragment cached", - @store.read("views/functional_caching/fragment_cached:#{template_digest("functional_caching/fragment_cached")}/fragment") + @store.read("views/functional_caching/fragment_cached:#{template_digest("functional_caching/fragment_cached", "html")}/fragment") end def test_fragment_caching_in_partials @@ -221,7 +221,7 @@ CACHED assert_match(/Old fragment caching in a partial/, @response.body) assert_match("Old fragment caching in a partial", - @store.read("views/functional_caching/_partial:#{template_digest("functional_caching/_partial")}/test.host/functional_caching/html_fragment_cached_with_partial")) + @store.read("views/functional_caching/_partial:#{template_digest("functional_caching/_partial", "html")}/test.host/functional_caching/html_fragment_cached_with_partial")) end def test_skipping_fragment_cache_digesting @@ -251,7 +251,7 @@ CACHED assert_match(/Some inline content/, @response.body) assert_match(/Some cached content/, @response.body) assert_match("Some cached content", - @store.read("views/functional_caching/inline_fragment_cached:#{template_digest("functional_caching/inline_fragment_cached")}/test.host/functional_caching/inline_fragment_cached")) + @store.read("views/functional_caching/inline_fragment_cached:#{template_digest("functional_caching/inline_fragment_cached", "html")}/test.host/functional_caching/inline_fragment_cached")) end def test_fragment_cache_instrumentation @@ -271,36 +271,39 @@ CACHED end def test_html_formatted_fragment_caching - get :formatted_fragment_cached, format: "html" + format = "html" + get :formatted_fragment_cached, format: format assert_response :success expected_body = "<body>\n<p>ERB</p>\n</body>\n" assert_equal expected_body, @response.body assert_equal "<p>ERB</p>", - @store.read("views/functional_caching/formatted_fragment_cached:#{template_digest("functional_caching/formatted_fragment_cached")}/fragment") + @store.read("views/functional_caching/formatted_fragment_cached:#{template_digest("functional_caching/formatted_fragment_cached", format)}/fragment") end def test_xml_formatted_fragment_caching - get :formatted_fragment_cached, format: "xml" + format = "xml" + get :formatted_fragment_cached, format: format assert_response :success expected_body = "<body>\n <p>Builder</p>\n</body>\n" assert_equal expected_body, @response.body assert_equal " <p>Builder</p>\n", - @store.read("views/functional_caching/formatted_fragment_cached:#{template_digest("functional_caching/formatted_fragment_cached")}/fragment") + @store.read("views/functional_caching/formatted_fragment_cached:#{template_digest("functional_caching/formatted_fragment_cached", format)}/fragment") end def test_fragment_caching_with_variant - get :formatted_fragment_cached_with_variant, format: "html", params: { v: :phone } + format = "html" + get :formatted_fragment_cached_with_variant, format: format, params: { v: :phone } assert_response :success expected_body = "<body>\n<p>PHONE</p>\n</body>\n" assert_equal expected_body, @response.body assert_equal "<p>PHONE</p>", - @store.read("views/functional_caching/formatted_fragment_cached_with_variant:#{template_digest("functional_caching/formatted_fragment_cached_with_variant")}/fragment") + @store.read("views/functional_caching/formatted_fragment_cached_with_variant:#{template_digest("functional_caching/formatted_fragment_cached_with_variant", format)}/fragment") end def test_fragment_caching_with_html_partials_in_xml @@ -309,8 +312,8 @@ CACHED end private - def template_digest(name) - ActionView::Digestor.digest(name: name, finder: @controller.lookup_context) + def template_digest(name, format) + ActionView::Digestor.digest(name: name, format: format, finder: @controller.lookup_context) end end diff --git a/actionpack/test/dispatch/debug_exceptions_test.rb b/actionpack/test/dispatch/debug_exceptions_test.rb index 6914fb66f9..c33b0e1c14 100644 --- a/actionpack/test/dispatch/debug_exceptions_test.rb +++ b/actionpack/test/dispatch/debug_exceptions_test.rb @@ -39,7 +39,7 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest def call(env) env["action_dispatch.show_detailed_exceptions"] = @detailed req = ActionDispatch::Request.new(env) - template = ActionView::Template.new(File.read(__FILE__), __FILE__, ActionView::Template::Handlers::Raw.new, {}) + template = ActionView::Template.new(File.read(__FILE__), __FILE__, ActionView::Template::Handlers::Raw.new, format: :html) case req.path when "/pass" diff --git a/actionpack/test/dispatch/uploaded_file_test.rb b/actionpack/test/dispatch/uploaded_file_test.rb index 21169fcb5c..03e5274541 100644 --- a/actionpack/test/dispatch/uploaded_file_test.rb +++ b/actionpack/test/dispatch/uploaded_file_test.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true require "abstract_unit" +require "tempfile" +require "stringio" module ActionDispatch class UploadedFileTest < ActiveSupport::TestCase @@ -11,109 +13,118 @@ module ActionDispatch end def test_original_filename - uf = Http::UploadedFile.new(filename: "foo", tempfile: Object.new) + uf = Http::UploadedFile.new(filename: "foo", tempfile: Tempfile.new) assert_equal "foo", uf.original_filename end def test_filename_is_different_object file_str = "foo" - uf = Http::UploadedFile.new(filename: file_str, tempfile: Object.new) + uf = Http::UploadedFile.new(filename: file_str, tempfile: Tempfile.new) assert_not_equal file_str.object_id, uf.original_filename.object_id end def test_filename_should_be_in_utf_8 - uf = Http::UploadedFile.new(filename: "foo", tempfile: Object.new) + uf = Http::UploadedFile.new(filename: "foo", tempfile: Tempfile.new) assert_equal "UTF-8", uf.original_filename.encoding.to_s end def test_filename_should_always_be_in_utf_8 uf = Http::UploadedFile.new(filename: "foo".encode(Encoding::SHIFT_JIS), - tempfile: Object.new) + tempfile: Tempfile.new) assert_equal "UTF-8", uf.original_filename.encoding.to_s end def test_content_type - uf = Http::UploadedFile.new(type: "foo", tempfile: Object.new) + uf = Http::UploadedFile.new(type: "foo", tempfile: Tempfile.new) assert_equal "foo", uf.content_type end def test_headers - uf = Http::UploadedFile.new(head: "foo", tempfile: Object.new) + uf = Http::UploadedFile.new(head: "foo", tempfile: Tempfile.new) assert_equal "foo", uf.headers end def test_tempfile - uf = Http::UploadedFile.new(tempfile: "foo") - assert_equal "foo", uf.tempfile + tf = Tempfile.new + uf = Http::UploadedFile.new(tempfile: tf) + assert_equal tf, uf.tempfile end - def test_to_io_returns_the_tempfile - tf = Object.new + def test_to_io_returns_file + tf = Tempfile.new uf = Http::UploadedFile.new(tempfile: tf) - assert_equal tf, uf.to_io + assert_equal tf.to_io, uf.to_io end def test_delegates_path_to_tempfile - tf = Class.new { def path; "thunderhorse" end } - uf = Http::UploadedFile.new(tempfile: tf.new) - assert_equal "thunderhorse", uf.path + tf = Tempfile.new + uf = Http::UploadedFile.new(tempfile: tf) + assert_equal tf.path, uf.path end def test_delegates_open_to_tempfile - tf = Class.new { def open; "thunderhorse" end } - uf = Http::UploadedFile.new(tempfile: tf.new) - assert_equal "thunderhorse", uf.open + tf = Tempfile.new + tf.close + uf = Http::UploadedFile.new(tempfile: tf) + assert_equal tf, uf.open + assert_not tf.closed? end def test_delegates_close_to_tempfile - tf = Class.new { def close(unlink_now = false); "thunderhorse" end } - uf = Http::UploadedFile.new(tempfile: tf.new) - assert_equal "thunderhorse", uf.close + tf = Tempfile.new + uf = Http::UploadedFile.new(tempfile: tf) + uf.close + assert tf.closed? end def test_close_accepts_parameter - tf = Class.new { def close(unlink_now = false); "thunderhorse: #{unlink_now}" end } - uf = Http::UploadedFile.new(tempfile: tf.new) - assert_equal "thunderhorse: true", uf.close(true) + tf = Tempfile.new + uf = Http::UploadedFile.new(tempfile: tf) + uf.close(true) + assert tf.closed? + assert_nil tf.path end def test_delegates_read_to_tempfile - tf = Class.new { def read(length = nil, buffer = nil); "thunderhorse" end } - uf = Http::UploadedFile.new(tempfile: tf.new) + tf = Tempfile.new + tf << "thunderhorse" + tf.rewind + uf = Http::UploadedFile.new(tempfile: tf) assert_equal "thunderhorse", uf.read end def test_delegates_read_to_tempfile_with_params - tf = Class.new { def read(length = nil, buffer = nil); [length, buffer] end } - uf = Http::UploadedFile.new(tempfile: tf.new) - assert_equal %w{ thunder horse }, uf.read(*%w{ thunder horse }) - end - - def test_delegate_respects_respond_to? - tf = Class.new { def read; yield end; private :read } - uf = Http::UploadedFile.new(tempfile: tf.new) - assert_raises(NoMethodError) do - uf.read - end + tf = Tempfile.new + tf << "thunderhorse" + tf.rewind + uf = Http::UploadedFile.new(tempfile: tf) + assert_equal "thunder", uf.read(7) + assert_equal "horse", uf.read(5, String.new) end def test_delegate_eof_to_tempfile - tf = Class.new { def eof?; true end; } - uf = Http::UploadedFile.new(tempfile: tf.new) - assert_predicate uf, :eof? + tf = Tempfile.new + tf << "thunderhorse" + uf = Http::UploadedFile.new(tempfile: tf) + assert_equal true, uf.eof? + tf.rewind + assert_equal false, uf.eof? end def test_delegate_to_path_to_tempfile - tf = Class.new { def to_path; "/any/file/path" end; } - uf = Http::UploadedFile.new(tempfile: tf.new) - assert_equal "/any/file/path", uf.to_path + tf = Tempfile.new + uf = Http::UploadedFile.new(tempfile: tf) + assert_equal tf.to_path, uf.to_path end - def test_respond_to? - tf = Class.new { def read; yield end } - uf = Http::UploadedFile.new(tempfile: tf.new) - assert_respond_to uf, :headers - assert_respond_to uf, :read + def test_io_copy_stream + tf = Tempfile.new + tf << "thunderhorse" + tf.rewind + uf = Http::UploadedFile.new(tempfile: tf) + result = StringIO.new + IO.copy_stream(uf, result) + assert_equal "thunderhorse", result.string end end end diff --git a/actiontext/app/helpers/action_text/tag_helper.rb b/actiontext/app/helpers/action_text/tag_helper.rb index 8434f2c611..1dc6202ae1 100644 --- a/actiontext/app/helpers/action_text/tag_helper.rb +++ b/actiontext/app/helpers/action_text/tag_helper.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "action_view/helpers/tags/placeholderable" + module ActionText module TagHelper cattr_accessor(:id, instance_accessor: false) { 0 } @@ -35,6 +37,8 @@ end module ActionView::Helpers class Tags::ActionText < Tags::Base + include Tags::Placeholderable + delegate :dom_id, to: ActionView::RecordIdentifier def render diff --git a/actiontext/test/template/form_helper_test.rb b/actiontext/test/template/form_helper_test.rb index a8c7a4dae2..cf7e4c0c69 100644 --- a/actiontext/test/template/form_helper_test.rb +++ b/actiontext/test/template/form_helper_test.rb @@ -5,6 +5,26 @@ require "test_helper" class ActionText::FormHelperTest < ActionView::TestCase tests ActionText::TagHelper + def form_with(*) + @output_buffer = super + end + + teardown do + I18n.backend.reload! + end + + setup do + I18n.backend.store_translations("placeholder", + activerecord: { + attributes: { + message: { + title: "Story title" + } + } + } + ) + end + test "form with rich text area" do form_with model: Message.new, scope: :message do |form| form.rich_text_area :content @@ -61,7 +81,33 @@ class ActionText::FormHelperTest < ActionView::TestCase output_buffer end - def form_with(*) - @output_buffer = super + test "form with rich text area having placeholder without locale" do + form_with model: Message.new, scope: :message do |form| + form.rich_text_area :content, placeholder: true + end + + assert_dom_equal \ + '<form action="/messages" accept-charset="UTF-8" data-remote="true" method="post">' \ + '<input type="hidden" name="message[content]" id="message_content_trix_input_message" />' \ + '<trix-editor placeholder="Content" id="message_content" input="message_content_trix_input_message" class="trix-content" data-direct-upload-url="http://test.host/rails/active_storage/direct_uploads" data-blob-url-template="http://test.host/rails/active_storage/blobs/:signed_id/:filename">' \ + "</trix-editor>" \ + "</form>", + output_buffer + end + + test "form with rich text area having placeholder with locale" do + I18n.with_locale :placeholder do + form_with model: Message.new, scope: :message do |form| + form.rich_text_area :title, placeholder: true + end + end + + assert_dom_equal \ + '<form action="/messages" accept-charset="UTF-8" data-remote="true" method="post">' \ + '<input type="hidden" name="message[title]" id="message_title_trix_input_message" />' \ + '<trix-editor placeholder="Story title" id="message_title" input="message_title_trix_input_message" class="trix-content" data-direct-upload-url="http://test.host/rails/active_storage/direct_uploads" data-blob-url-template="http://test.host/rails/active_storage/blobs/:signed_id/:filename">' \ + "</trix-editor>" \ + "</form>", + output_buffer end end diff --git a/actiontext/test/test_helper.rb b/actiontext/test/test_helper.rb index fd1c859349..196fba8c99 100644 --- a/actiontext/test/test_helper.rb +++ b/actiontext/test/test_helper.rb @@ -14,6 +14,9 @@ Minitest.backtrace_filter = Minitest::BacktraceFilter.new require "rails/test_unit/reporter" Rails::TestUnitReporter.executable = "bin/test" +# Disable available locale checks to allow to add locale after initialized. +I18n.enforce_available_locales = false + # Load fixtures from the engine if ActiveSupport::TestCase.respond_to?(:fixture_path=) ActiveSupport::TestCase.fixture_path = File.expand_path("fixtures", __dir__) diff --git a/actionview/app/assets/javascripts/rails-ujs/utils/event.coffee b/actionview/app/assets/javascripts/rails-ujs/utils/event.coffee index a7eee52060..768d9683d4 100644 --- a/actionview/app/assets/javascripts/rails-ujs/utils/event.coffee +++ b/actionview/app/assets/javascripts/rails-ujs/utils/event.coffee @@ -27,7 +27,7 @@ if typeof CustomEvent isnt 'function' # obj:: # a native DOM element # name:: -# string that corrspends to the event you want to trigger +# string that corresponds to the event you want to trigger # e.g. 'click', 'submit' # data:: # data you want to pass when you dispatch an event diff --git a/actionview/lib/action_view/base.rb b/actionview/lib/action_view/base.rb index c0d2d258c5..c4565ca272 100644 --- a/actionview/lib/action_view/base.rb +++ b/actionview/lib/action_view/base.rb @@ -213,6 +213,8 @@ module ActionView #:nodoc: context.lookup_context when Array ActionView::LookupContext.new(context) + when ActionView::PathSet + ActionView::LookupContext.new(context) when nil ActionView::LookupContext.new([]) else @@ -251,12 +253,13 @@ module ActionView #:nodoc: else ActiveSupport::Deprecation.warn <<~eowarn ActionView::Base instances should be constructed with a lookup context, - assigments, and a controller. + assignments, and a controller. eowarn @lookup_context = self.class.build_lookup_context(lookup_context) end @view_renderer = ActionView::Renderer.new @lookup_context + @current_template = nil @cache_hit = {} assign(assigns) @@ -264,12 +267,13 @@ module ActionView #:nodoc: _prepare_context end - def run(method, locals, buffer, &block) - _old_output_buffer, _old_virtual_path = @output_buffer, @virtual_path + def run(method, template, locals, buffer, &block) + _old_output_buffer, _old_virtual_path, _old_template = @output_buffer, @virtual_path, @current_template + @current_template = template @output_buffer = buffer send(method, locals, buffer, &block) ensure - @output_buffer, @virtual_path = _old_output_buffer, _old_virtual_path + @output_buffer, @virtual_path, @current_template = _old_output_buffer, _old_virtual_path, _old_template end def compiled_method_container @@ -284,7 +288,7 @@ module ActionView #:nodoc: self.class end - def in_context(options, locals) + def in_rendering_context(options) old_view_renderer = @view_renderer old_lookup_context = @lookup_context diff --git a/actionview/lib/action_view/digestor.rb b/actionview/lib/action_view/digestor.rb index 6d2e471a44..9fa8d7eab1 100644 --- a/actionview/lib/action_view/digestor.rb +++ b/actionview/lib/action_view/digestor.rb @@ -18,11 +18,11 @@ module ActionView # * <tt>name</tt> - Template name # * <tt>finder</tt> - An instance of <tt>ActionView::LookupContext</tt> # * <tt>dependencies</tt> - An array of dependent views - def digest(name:, finder:, dependencies: nil) + def digest(name:, format:, finder:, dependencies: nil) if dependencies.nil? || dependencies.empty? - cache_key = "#{name}.#{finder.rendered_format}" + cache_key = "#{name}.#{format}" else - cache_key = [ name, finder.rendered_format, dependencies ].flatten.compact.join(".") + cache_key = [ name, format, dependencies ].flatten.compact.join(".") end # this is a correctly done double-checked locking idiom @@ -48,8 +48,6 @@ module ActionView logical_name = name.gsub(%r|/_|, "/") if template = find_template(finder, logical_name, [], partial, []) - finder.rendered_format ||= template.formats.first - if node = seen[template.identifier] # handle cycles in the tree node else @@ -73,9 +71,7 @@ module ActionView private def find_template(finder, name, prefixes, partial, keys) finder.disable_cache do - format = finder.rendered_format - result = finder.find_all(name, prefixes, partial, keys, formats: [format]).first if format - result || finder.find_all(name, prefixes, partial, keys).first + finder.find_all(name, prefixes, partial, keys).first end end end diff --git a/actionview/lib/action_view/helpers/cache_helper.rb b/actionview/lib/action_view/helpers/cache_helper.rb index b1a14250c3..6b69b71947 100644 --- a/actionview/lib/action_view/helpers/cache_helper.rb +++ b/actionview/lib/action_view/helpers/cache_helper.rb @@ -216,13 +216,13 @@ module ActionView end end - def digest_path_from_virtual(virtual_path) # :nodoc: - digest = Digestor.digest(name: virtual_path, finder: lookup_context, dependencies: view_cache_dependencies) + def digest_path_from_template(template) # :nodoc: + digest = Digestor.digest(name: template.virtual_path, format: template.formats.first, finder: lookup_context, dependencies: view_cache_dependencies) if digest.present? - "#{virtual_path}:#{digest}" + "#{template.virtual_path}:#{digest}" else - virtual_path + template.virtual_path end end @@ -234,7 +234,7 @@ module ActionView if virtual_path || digest_path name = controller.url_for(name).split("://").last if name.is_a?(Hash) - digest_path ||= digest_path_from_virtual(virtual_path) + digest_path ||= digest_path_from_template(@current_template) [ digest_path, name ] else diff --git a/actionview/lib/action_view/helpers/rendering_helper.rb b/actionview/lib/action_view/helpers/rendering_helper.rb index 7323963c72..7ead691113 100644 --- a/actionview/lib/action_view/helpers/rendering_helper.rb +++ b/actionview/lib/action_view/helpers/rendering_helper.rb @@ -27,7 +27,7 @@ module ActionView def render(options = {}, locals = {}, &block) case options when Hash - in_context(options, locals) do |renderer| + in_rendering_context(options) do |renderer| if block_given? view_renderer.render_partial(self, options.merge(partial: options[:layout]), &block) else diff --git a/actionview/lib/action_view/layouts.rb b/actionview/lib/action_view/layouts.rb index 3e6d352c15..08f66bf435 100644 --- a/actionview/lib/action_view/layouts.rb +++ b/actionview/lib/action_view/layouts.rb @@ -322,7 +322,7 @@ module ActionView end class_eval <<-RUBY, __FILE__, __LINE__ + 1 - def _layout(formats) + def _layout(lookup_context, formats) if _conditional_layout? #{layout_definition} else @@ -388,8 +388,8 @@ module ActionView case name when String then _normalize_layout(name) when Proc then name - when true then Proc.new { |formats| _default_layout(formats, true) } - when :default then Proc.new { |formats| _default_layout(formats, false) } + when true then Proc.new { |lookup_context, formats| _default_layout(lookup_context, formats, true) } + when :default then Proc.new { |lookup_context, formats| _default_layout(lookup_context, formats, false) } when false, nil then nil else raise ArgumentError, @@ -411,9 +411,9 @@ module ActionView # # ==== Returns # * <tt>template</tt> - The template object for the default layout (or +nil+) - def _default_layout(formats, require_layout = false) + def _default_layout(lookup_context, formats, require_layout = false) begin - value = _layout(formats) if action_has_layout? + value = _layout(lookup_context, formats) if action_has_layout? rescue NameError => e raise e, "Could not render layout: #{e.message}" end diff --git a/actionview/lib/action_view/lookup_context.rb b/actionview/lib/action_view/lookup_context.rb index 125ab4dbe3..10cd61bbd6 100644 --- a/actionview/lib/action_view/lookup_context.rb +++ b/actionview/lib/action_view/lookup_context.rb @@ -16,6 +16,8 @@ module ActionView # only once during the request, it speeds up all cache accesses. class LookupContext #:nodoc: attr_accessor :prefixes, :rendered_format + deprecate :rendered_format + deprecate :rendered_format= mattr_accessor :fallbacks, default: FallbackFileSystemResolver.instances @@ -250,7 +252,6 @@ module ActionView @digest_cache = nil @cache = true @prefixes = prefixes - @rendered_format = nil @details = initialize_details({}, details) @view_paths = build_view_paths(view_paths) diff --git a/actionview/lib/action_view/renderer/abstract_renderer.rb b/actionview/lib/action_view/renderer/abstract_renderer.rb index ae366ce54a..f1b4c9b92d 100644 --- a/actionview/lib/action_view/renderer/abstract_renderer.rb +++ b/actionview/lib/action_view/renderer/abstract_renderer.rb @@ -27,6 +27,53 @@ module ActionView raise NotImplementedError end + class RenderedCollection # :nodoc: + def self.empty(format) + EmptyCollection.new format + end + + attr_reader :rendered_templates + + def initialize(rendered_templates, spacer) + @rendered_templates = rendered_templates + @spacer = spacer + end + + def body + @rendered_templates.map(&:body).join(@spacer.body).html_safe + end + + def format + rendered_templates.first.format + end + + class EmptyCollection + attr_reader :format + + def initialize(format) + @format = format + end + + def body; nil; end + end + end + + class RenderedTemplate # :nodoc: + attr_reader :body, :layout, :template + + def initialize(body, layout, template) + @body = body + @layout = layout + @template = template + end + + def format + template.formats.first + end + + EMPTY_SPACER = Struct.new(:body).new + end + private def extract_details(options) # :doc: @@ -49,5 +96,13 @@ module ActionView @lookup_context.formats = formats | @lookup_context.formats end + + def build_rendered_template(content, template, layout = nil) + RenderedTemplate.new content, layout, template + end + + def build_rendered_collection(templates, spacer) + RenderedCollection.new templates, spacer + end end end diff --git a/actionview/lib/action_view/renderer/partial_renderer.rb b/actionview/lib/action_view/renderer/partial_renderer.rb index f8a6f13ae9..ed8d5cf54e 100644 --- a/actionview/lib/action_view/renderer/partial_renderer.rb +++ b/actionview/lib/action_view/renderer/partial_renderer.rb @@ -314,14 +314,6 @@ module ActionView template = nil end - @lookup_context.rendered_format ||= begin - if template && template.formats.first - template.formats.first - else - formats.first - end - end - if @collection render_collection(context, template) else @@ -334,10 +326,13 @@ module ActionView def render_collection(view, template) identifier = (template && template.identifier) || @path instrument(:collection, identifier: identifier, count: @collection.size) do |payload| - return nil if @collection.blank? + return RenderedCollection.empty(@lookup_context.formats.first) if @collection.blank? - if @options.key?(:spacer_template) - spacer = find_template(@options[:spacer_template], @locals.keys).render(view, @locals) + spacer = if @options.key?(:spacer_template) + spacer_template = find_template(@options[:spacer_template], @locals.keys) + build_rendered_template(spacer_template.render(view, @locals), spacer_template) + else + RenderedTemplate::EMPTY_SPACER end collection_body = if template @@ -347,7 +342,7 @@ module ActionView else collection_without_template(view) end - collection_body.join(spacer).html_safe + build_rendered_collection(collection_body, spacer) end end @@ -369,7 +364,7 @@ module ActionView content = layout.render(view, locals) { content } if layout payload[:cache_hit] = view.view_renderer.cache_hits[template.virtual_path] - content + build_rendered_template(content, template, layout) end end @@ -460,7 +455,7 @@ module ActionView content = template.render(view, locals) content = layout.render(view, locals) { content } if layout partial_iteration.iterate! - content + build_rendered_template(content, template, layout) end end @@ -482,7 +477,7 @@ module ActionView template = (cache[path] ||= find_template(path, keys + [as, counter, iteration])) content = template.render(view, locals) partial_iteration.iterate! - content + build_rendered_template(content, template) end end diff --git a/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb b/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb index 388f9e5e56..ed59033e27 100644 --- a/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb +++ b/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb @@ -40,7 +40,7 @@ module ActionView rendered_partials = @collection.empty? ? [] : yield index = 0 - fetch_or_cache_partial(cached_partials, order_by: keyed_collection.each_key) do + fetch_or_cache_partial(cached_partials, template, order_by: keyed_collection.each_key) do # This block is called once # for every cache miss while preserving order. rendered_partials[index].tap { index += 1 } @@ -54,7 +54,7 @@ module ActionView def collection_by_cache_keys(view, template) seed = callable_cache_key? ? @options[:cached] : ->(i) { i } - digest_path = view.digest_path_from_virtual(template.virtual_path) + digest_path = view.digest_path_from_template(template) @collection.each_with_object({}) do |item, hash| hash[expanded_cache_key(seed.call(item), view, template, digest_path)] = item @@ -81,11 +81,13 @@ module ActionView # # If the partial is not already cached it will also be # written back to the underlying cache store. - def fetch_or_cache_partial(cached_partials, order_by:) + def fetch_or_cache_partial(cached_partials, template, order_by:) order_by.map do |cache_key| - cached_partials.fetch(cache_key) do + if content = cached_partials[cache_key] + build_rendered_template(content, template) + else yield.tap do |rendered_partial| - collection_cache.write(cache_key, rendered_partial) + collection_cache.write(cache_key, rendered_partial.body) end end end diff --git a/actionview/lib/action_view/renderer/renderer.rb b/actionview/lib/action_view/renderer/renderer.rb index 3f3a97529d..485eb1a5b4 100644 --- a/actionview/lib/action_view/renderer/renderer.rb +++ b/actionview/lib/action_view/renderer/renderer.rb @@ -19,10 +19,14 @@ module ActionView # Main render entry point shared by Action View and Action Controller. def render(context, options) + render_to_object(context, options).body + end + + def render_to_object(context, options) # :nodoc: if options.key?(:partial) - render_partial(context, options) + render_partial_to_object(context, options) else - render_template(context, options) + render_template_to_object(context, options) end end @@ -41,16 +45,24 @@ module ActionView # Direct access to template rendering. def render_template(context, options) #:nodoc: - TemplateRenderer.new(@lookup_context).render(context, options) + render_template_to_object(context, options).body end # Direct access to partial rendering. def render_partial(context, options, &block) #:nodoc: - PartialRenderer.new(@lookup_context).render(context, options, block) + render_partial_to_object(context, options, &block).body end def cache_hits # :nodoc: @cache_hits ||= {} end + + def render_template_to_object(context, options) #:nodoc: + TemplateRenderer.new(@lookup_context).render(context, options) + end + + def render_partial_to_object(context, options, &block) #:nodoc: + PartialRenderer.new(@lookup_context).render(context, options, block) + end end end diff --git a/actionview/lib/action_view/renderer/streaming_template_renderer.rb b/actionview/lib/action_view/renderer/streaming_template_renderer.rb index f414620923..279ef3c680 100644 --- a/actionview/lib/action_view/renderer/streaming_template_renderer.rb +++ b/actionview/lib/action_view/renderer/streaming_template_renderer.rb @@ -44,7 +44,7 @@ module ActionView # object that responds to each. This object is initialized with a block # that knows how to render the template. def render_template(view, template, layout_name = nil, locals = {}) #:nodoc: - return [super] unless layout_name && template.supports_streaming? + return [super.body] unless layout_name && template.supports_streaming? locals ||= {} layout = layout_name && find_layout(layout_name, locals.keys, [formats.first]) diff --git a/actionview/lib/action_view/renderer/template_renderer.rb b/actionview/lib/action_view/renderer/template_renderer.rb index c36baeffcd..87f6cf3de3 100644 --- a/actionview/lib/action_view/renderer/template_renderer.rb +++ b/actionview/lib/action_view/renderer/template_renderer.rb @@ -10,8 +10,6 @@ module ActionView prepend_formats(template.formats) - @lookup_context.rendered_format ||= (template.formats.first || formats.first) - render_template(context, template, options[:layout], options[:locals] || {}) end @@ -31,7 +29,12 @@ module ActionView @lookup_context.with_fallbacks.find_file(options[:file], nil, false, keys, @details) elsif options.key?(:inline) handler = Template.handler_for_extension(options[:type] || "erb") - Template.new(options[:inline], "inline template", handler, locals: keys) + format = if handler.respond_to?(:default_format) + handler.default_format + else + @lookup_context.formats.first + end + Template::Inline.new(options[:inline], "inline template", handler, locals: keys, format: format) elsif options.key?(:template) if options[:template].respond_to?(:render) options[:template] @@ -46,23 +49,24 @@ module ActionView # Renders the given template. A string representing the layout can be # supplied as well. def render_template(view, template, layout_name, locals) - render_with_layout(view, layout_name, locals) do |layout| + render_with_layout(view, layout_name, template, locals) do |layout| instrument(:template, identifier: template.identifier, layout: layout.try(:virtual_path)) do template.render(view, locals) { |*name| view._layout_for(*name) } end end end - def render_with_layout(view, path, locals) + def render_with_layout(view, path, template, locals) layout = path && find_layout(path, locals.keys, [formats.first]) content = yield(layout) - if layout + body = if layout view.view_flow.set(:layout, content) layout.render(view, locals) { |*name| view._layout_for(*name) } else content end + build_rendered_template(body, template, layout) end # This is the method which actually finds the layout using details in the lookup @@ -89,7 +93,7 @@ module ActionView raise unless template_exists?(layout, nil, false, [], all_details) end when Proc - resolve_layout(layout.call(formats), keys, formats) + resolve_layout(layout.call(@lookup_context, formats), keys, formats) else layout end diff --git a/actionview/lib/action_view/rendering.rb b/actionview/lib/action_view/rendering.rb index b798e80b04..e5e2771323 100644 --- a/actionview/lib/action_view/rendering.rb +++ b/actionview/lib/action_view/rendering.rb @@ -26,6 +26,13 @@ module ActionView extend ActiveSupport::Concern include ActionView::ViewPaths + attr_reader :rendered_format + + def initialize + @rendered_format = nil + super + end + # Overwrite process to setup I18n proxy. def process(*) #:nodoc: old_config, I18n.config = I18n.config, I18nProxy.new(I18n.config, lookup_context) @@ -96,10 +103,6 @@ module ActionView _render_template(options) end - def rendered_format - Template::Types[lookup_context.rendered_format] - end - private # Find and render a template based on the options given. @@ -109,17 +112,21 @@ module ActionView context = view_context context.assign assigns if assigns - lookup_context.rendered_format = nil if options[:formats] lookup_context.variants = variant if variant - context.view_renderer.render(context, options) + rendered_template = context.in_rendering_context(options) do |renderer| + renderer.render_to_object(context, options) + end + + @rendered_format = Template::Types[rendered_template.format] + + rendered_template.body end # Assign the rendered format to look up context. def _process_format(format) super lookup_context.formats = [format.to_sym] - lookup_context.rendered_format = lookup_context.formats.first end # Normalize args by converting render "foo" to render :action => "foo" and diff --git a/actionview/lib/action_view/template.rb b/actionview/lib/action_view/template.rb index 37f476e554..8ba03027d3 100644 --- a/actionview/lib/action_view/template.rb +++ b/actionview/lib/action_view/template.rb @@ -115,20 +115,24 @@ module ActionView autoload :Error autoload :Handlers autoload :HTML + autoload :Inline autoload :Text autoload :Types end extend Template::Handlers - attr_accessor :locals, :formats, :variants, :virtual_path + attr_accessor :locals, :variants, :virtual_path attr_reader :source, :identifier, :handler, :original_encoding, :updated_at - attr_reader :variable + attr_reader :variable, :formats - def initialize(source, identifier, handler, details) - format = details[:format] || (handler.default_format if handler.respond_to?(:default_format)) + def initialize(source, identifier, handler, format: nil, **details) + unless format + ActiveSupport::Deprecation.warn "ActionView::Template#initialize requires a format parameter" + format = :html + end @source = source @identifier = identifier @@ -145,11 +149,15 @@ module ActionView end @updated_at = details[:updated_at] || Time.now - @formats = Array(format).map { |f| f.respond_to?(:ref) ? f.ref : f } + @formats = Array(format) @variants = [details[:variant]] @compile_mutex = Mutex.new end + def formats=(_) + end + deprecate :formats= + # Returns whether the underlying handler supports streaming. If so, # a streaming buffer *may* be passed when it starts rendering. def supports_streaming? @@ -165,7 +173,7 @@ module ActionView def render(view, locals, buffer = ActionView::OutputBuffer.new, &block) instrument_render_template do compile!(view) - view.run(method_name, locals, buffer, &block) + view.run(method_name, self, locals, buffer, &block) end rescue => e handle_render_error(view, e) diff --git a/actionview/lib/action_view/template/handlers/erb/erubi.rb b/actionview/lib/action_view/template/handlers/erb/erubi.rb index 20510c3062..247b6d75d1 100644 --- a/actionview/lib/action_view/template/handlers/erb/erubi.rb +++ b/actionview/lib/action_view/template/handlers/erb/erubi.rb @@ -27,7 +27,7 @@ module ActionView include action_view_erb_handler_context._routes.url_helpers class_eval("define_method(:_template) { |local_assigns, output_buffer| #{src} }", @filename || "(erubi)", 0) }.empty - view.run(:_template, {}, ActionView::OutputBuffer.new) + view.run(:_template, nil, {}, ActionView::OutputBuffer.new) end private diff --git a/actionview/lib/action_view/template/inline.rb b/actionview/lib/action_view/template/inline.rb new file mode 100644 index 0000000000..44658487ea --- /dev/null +++ b/actionview/lib/action_view/template/inline.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module ActionView #:nodoc: + class Template #:nodoc: + class Inline < Template #:nodoc: + # This finalizer is needed (and exactly with a proc inside another proc) + # otherwise templates leak in development. + Finalizer = proc do |method_name, mod| # :nodoc: + proc do + mod.module_eval do + remove_possible_method method_name + end + end + end + + def compile(mod) + super + ObjectSpace.define_finalizer(self, Finalizer[method_name, mod]) + end + end + end +end diff --git a/actionview/lib/action_view/template/resolver.rb b/actionview/lib/action_view/template/resolver.rb index 3b4594942b..220794e443 100644 --- a/actionview/lib/action_view/template/resolver.rb +++ b/actionview/lib/action_view/template/resolver.rb @@ -196,7 +196,6 @@ module ActionView cached = nil templates.each do |t| t.locals = locals - t.formats = details[:formats] || [:html] if t.formats.empty? t.variants = details[:variants] || [] if t.variants.empty? t.virtual_path ||= (cached ||= build_path(*path_info)) end @@ -225,7 +224,7 @@ 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) + handler, format, variant = extract_handler_and_format_and_variant(template, formats.first) FileTemplate.new(File.expand_path(template), handler, virtual_path: path.virtual, @@ -292,7 +291,7 @@ module ActionView # Extract handler, formats and variant from path. If a format cannot be found neither # from the path, or the handler, we should return the array of formats given # to the resolver. - def extract_handler_and_format_and_variant(path) + def extract_handler_and_format_and_variant(path, query_format) pieces = File.basename(path).split(".") pieces.shift @@ -300,9 +299,18 @@ module ActionView handler = Template.handler_for_extension(extension) format, variant = pieces.last.split(EXTENSIONS[:variants], 2) if pieces.last - format &&= Template::Types[format] + format = if format + Template::Types[format]&.ref + else + if handler.respond_to?(:default_format) # default_format can return nil + handler.default_format + else + query_format + end + end - [handler, format, variant] + # Template::Types[format] and handler.default_format can return nil + [handler, format || query_format, variant] end end diff --git a/actionview/lib/action_view/testing/resolvers.rb b/actionview/lib/action_view/testing/resolvers.rb index d6203b95c5..275e2e9182 100644 --- a/actionview/lib/action_view/testing/resolvers.rb +++ b/actionview/lib/action_view/testing/resolvers.rb @@ -34,7 +34,7 @@ module ActionView #:nodoc: @hash.each do |_path, array| source, updated_at = array next unless query.match?(_path) - handler, format, variant = extract_handler_and_format_and_variant(_path) + handler, format, variant = extract_handler_and_format_and_variant(_path, :html) templates << Template.new(source, _path, handler, virtual_path: path.virtual, format: format, @@ -49,7 +49,7 @@ module ActionView #:nodoc: class NullResolver < PathResolver def query(path, exts, _, _) - handler, format, variant = extract_handler_and_format_and_variant(path) + handler, format, variant = extract_handler_and_format_and_variant(path, :html) [ActionView::Template.new("Template generated by Null Resolver", path.virtual, handler, virtual_path: path.virtual, format: format, variant: variant)] end end diff --git a/actionview/test/abstract_unit.rb b/actionview/test/abstract_unit.rb index b649b3c9dd..e0a1f59755 100644 --- a/actionview/test/abstract_unit.rb +++ b/actionview/test/abstract_unit.rb @@ -60,7 +60,7 @@ module RenderERBUtils string.strip, "test template", ActionView::Template.handler_for_extension(:erb), - {}) + format: :html) view = ActionView::Base.with_empty_template_cache template.render(view.empty, {}).strip diff --git a/actionview/test/actionpack/controller/render_test.rb b/actionview/test/actionpack/controller/render_test.rb index 727d3fbc1a..52c3c54d96 100644 --- a/actionview/test/actionpack/controller/render_test.rb +++ b/actionview/test/actionpack/controller/render_test.rb @@ -174,6 +174,10 @@ class TestController < ActionController::Base render inline: "<%= controller_name %>" end + def inline_rendered_format_without_format + render inline: "test" + end + # :ported: def render_custom_code render plain: "hello world", status: 404 @@ -659,6 +663,7 @@ class RenderTest < ActionController::TestCase get :hello_world_from_rxml_using_action, to: "test#hello_world_from_rxml_using_action" get :hello_world_from_rxml_using_template, to: "test#hello_world_from_rxml_using_template" get :hello_world_with_layout_false, to: "test#hello_world_with_layout_false" + get :inline_rendered_format_without_format, to: "test#inline_rendered_format_without_format" get :layout_overriding_layout, to: "test#layout_overriding_layout" get :layout_test, to: "test#layout_test" get :layout_test_with_different_layout, to: "test#layout_test_with_different_layout" @@ -1015,6 +1020,12 @@ class RenderTest < ActionController::TestCase assert_equal "<wrapper>\n<html>\n <p>Hello </p>\n<p>This is grand!</p>\n</html>\n</wrapper>\n", @response.body end + def test_rendered_format_without_format + get :inline_rendered_format_without_format + assert_equal "test", @response.body + assert_equal "text/html", @response.content_type + end + def test_partials_list get :partials_list assert_equal "goodbyeHello: davidHello: marygoodbye\n", @response.body diff --git a/actionview/test/activerecord/relation_cache_test.rb b/actionview/test/activerecord/relation_cache_test.rb index a6befc3ee5..6fe83dcb9a 100644 --- a/actionview/test/activerecord/relation_cache_test.rb +++ b/actionview/test/activerecord/relation_cache_test.rb @@ -11,13 +11,14 @@ class RelationCacheTest < ActionView::TestCase lookup_context = ActionView::LookupContext.new(view_paths, {}, ["test"]) @view_renderer = ActionView::Renderer.new(lookup_context) @virtual_path = "path" + @current_template = lookup_context.find "test/hello_world" controller.cache_store = ActiveSupport::Cache::MemoryStore.new end def test_cache_relation_other cache(Project.all) { concat("Hello World") } - assert_equal "Hello World", controller.cache_store.read("views/path/projects-#{Project.count}") + assert_equal "Hello World", controller.cache_store.read("views/test/hello_world:fa9482a68ce25bf7589b8eddad72f736/projects-#{Project.count}") end def view_cache_dependencies; []; end diff --git a/actionview/test/template/digestor_test.rb b/actionview/test/template/digestor_test.rb index ddaa7febb3..91861edf11 100644 --- a/actionview/test/template/digestor_test.rb +++ b/actionview/test/template/digestor_test.rb @@ -7,9 +7,8 @@ require "action_view/dependency_tracker" class FixtureFinder < ActionView::LookupContext FIXTURES_DIR = File.expand_path("../fixtures/digestor", __dir__) - def initialize(details = {}) - super(ActionView::PathSet.new(["digestor", "digestor/api"]), details, []) - @rendered_format = :html + def self.build(details = {}) + new(ActionView::PathSet.new(["digestor", "digestor/api"]), details, []) end end @@ -146,13 +145,12 @@ class TemplateDigestorTest < ActionView::TestCase end def test_nested_template_deps_with_non_default_rendered_format - finder.rendered_format = nil nested_deps = [{ "comments/comments" => ["comments/comment"] }] assert_equal nested_deps, nested_dependencies("messages/thread") end def test_template_formats_of_nested_deps_with_non_default_rendered_format - finder.rendered_format = nil + @finder = finder.with_prepended_formats([:json]) assert_equal [:json], tree_template_formats("messages/thread").uniq end @@ -161,12 +159,10 @@ class TemplateDigestorTest < ActionView::TestCase end def test_template_dependencies_with_fallback_from_js_to_html_format - finder.rendered_format = :js assert_equal ["comments/comment"], dependencies("comments/show") end def test_template_digest_with_fallback_from_js_to_html_format - finder.rendered_format = :js assert_digest_difference("comments/show") do change_template("comments/_comment") end @@ -219,14 +215,14 @@ class TemplateDigestorTest < ActionView::TestCase def test_details_are_included_in_cache_key # Cache the template digest. - @finder = FixtureFinder.new(formats: [:html]) + @finder = FixtureFinder.build(formats: [:html]) old_digest = digest("events/_event") # Change the template; the cached digest remains unchanged. change_template("events/_event") # The details are changed, so a new cache key is generated. - @finder = FixtureFinder.new + @finder = FixtureFinder.build # The cache is busted. assert_not_equal old_digest, digest("events/_event") @@ -343,9 +339,14 @@ class TemplateDigestorTest < ActionView::TestCase finder_options = options.extract!(:variants, :format) finder.variants = finder_options[:variants] || [] - finder.rendered_format = finder_options[:format] if finder_options[:format] - ActionView::Digestor.digest(name: template_name, finder: finder, dependencies: (options[:dependencies] || [])) + finder_with_formats = if finder_options[:format] + finder.with_prepended_formats(Array(finder_options[:format])) + else + finder + end + + ActionView::Digestor.digest(name: template_name, format: finder_options[:format], finder: finder_with_formats, dependencies: (options[:dependencies] || [])) end def dependencies(template_name) @@ -371,7 +372,7 @@ class TemplateDigestorTest < ActionView::TestCase end def finder - @finder ||= FixtureFinder.new + @finder ||= FixtureFinder.build end def change_template(template_name, variant = nil) diff --git a/actionview/test/template/form_helper/form_with_test.rb b/actionview/test/template/form_helper/form_with_test.rb index f84c9b2b73..42069340f1 100644 --- a/actionview/test/template/form_helper/form_with_test.rb +++ b/actionview/test/template/form_helper/form_with_test.rb @@ -994,7 +994,7 @@ class FormWithActsLikeFormForTest < FormWithTest end def test_submit_with_object_as_new_record_and_locale_strings - with_locale :submit do + I18n.with_locale :submit do @post.persisted = false @post.stub(:to_key, nil) do form_with(model: @post) do |f| @@ -1011,7 +1011,7 @@ class FormWithActsLikeFormForTest < FormWithTest end def test_submit_with_object_as_existing_record_and_locale_strings - with_locale :submit do + I18n.with_locale :submit do form_with(model: @post) do |f| concat f.submit end @@ -1025,7 +1025,7 @@ class FormWithActsLikeFormForTest < FormWithTest end def test_submit_without_object_and_locale_strings - with_locale :submit do + I18n.with_locale :submit do form_with(scope: :post) do |f| concat f.submit class: "extra" end @@ -1039,7 +1039,7 @@ class FormWithActsLikeFormForTest < FormWithTest end def test_submit_with_object_which_is_overwritten_by_scope_option - with_locale :submit do + I18n.with_locale :submit do form_with(model: @post, scope: :another_post) do |f| concat f.submit end @@ -1054,7 +1054,7 @@ class FormWithActsLikeFormForTest < FormWithTest def test_submit_with_object_which_is_namespaced blog_post = Blog::Post.new("And his name will be forty and four.", 44) - with_locale :submit do + I18n.with_locale :submit do form_with(model: blog_post) do |f| concat f.submit end @@ -2357,11 +2357,4 @@ class FormWithActsLikeFormForTest < FormWithTest def protect_against_forgery? false end - - def with_locale(testing_locale = :label) - old_locale, I18n.locale = I18n.locale, testing_locale - yield - ensure - I18n.locale = old_locale - end end diff --git a/actionview/test/template/form_helper_test.rb b/actionview/test/template/form_helper_test.rb index 5972946074..91052e5ae2 100644 --- a/actionview/test/template/form_helper_test.rb +++ b/actionview/test/template/form_helper_test.rb @@ -203,31 +203,31 @@ class FormHelperTest < ActionView::TestCase end def test_label_with_locales_strings - with_locale :label do + I18n.with_locale :label do assert_dom_equal('<label for="post_body">Write entire text here</label>', label("post", "body")) end end def test_label_with_human_attribute_name - with_locale :label do + I18n.with_locale :label do assert_dom_equal('<label for="post_cost">Total cost</label>', label(:post, :cost)) end end def test_label_with_human_attribute_name_and_options - with_locale :label do + I18n.with_locale :label do assert_dom_equal('<label for="post_language_spanish">Espanol</label>', label(:post, :language, value: "spanish")) end end def test_label_with_locales_symbols - with_locale :label do + I18n.with_locale :label do assert_dom_equal('<label for="post_body">Write entire text here</label>', label(:post, :body)) end end def test_label_with_locales_and_options - with_locale :label do + I18n.with_locale :label do assert_dom_equal( '<label for="post_body" class="post_body">Write entire text here</label>', label(:post, :body, class: "post_body") @@ -236,13 +236,13 @@ class FormHelperTest < ActionView::TestCase end def test_label_with_locales_and_value - with_locale :label do + I18n.with_locale :label do assert_dom_equal('<label for="post_color_red">Rojo</label>', label(:post, :color, value: "red")) end end def test_label_with_locales_and_nested_attributes - with_locale :label do + I18n.with_locale :label do form_for(@post, html: { id: "create-post" }) do |f| f.fields_for(:comments) do |cf| concat cf.label(:body) @@ -258,7 +258,7 @@ class FormHelperTest < ActionView::TestCase end def test_label_with_locales_fallback_and_nested_attributes - with_locale :label do + I18n.with_locale :label do form_for(@post, html: { id: "create-post" }) do |f| f.fields_for(:tags) do |cf| concat cf.label(:value) @@ -358,7 +358,7 @@ class FormHelperTest < ActionView::TestCase end def test_label_with_block_and_builder - with_locale :label do + I18n.with_locale :label do assert_dom_equal( '<label for="post_body"><b>Write entire text here</b></label>', label(:post, :body) { |b| raw("<b>#{b.translation}</b>") } @@ -381,7 +381,7 @@ class FormHelperTest < ActionView::TestCase end def test_label_with_to_model_and_overridden_model_name - with_locale :label do + I18n.with_locale :label do assert_dom_equal( %{<label for="post_delegator_title">Delegate model_name title</label>}, label(:post_delegator, :title) @@ -390,19 +390,19 @@ class FormHelperTest < ActionView::TestCase end def test_text_field_placeholder_without_locales - with_locale :placeholder do + I18n.with_locale :placeholder do assert_dom_equal('<input id="post_body" name="post[body]" placeholder="Body" type="text" value="Back to the hill and over it again!" />', text_field(:post, :body, placeholder: true)) end end def test_text_field_placeholder_with_locales - with_locale :placeholder do + I18n.with_locale :placeholder do assert_dom_equal('<input id="post_title" name="post[title]" placeholder="What is this about?" type="text" value="Hello World" />', text_field(:post, :title, placeholder: true)) end end def test_text_field_placeholder_with_locales_and_to_model - with_locale :placeholder do + I18n.with_locale :placeholder do assert_dom_equal( '<input id="post_delegator_title" name="post_delegator[title]" placeholder="Delegate model_name title" type="text" value="Hello World" />', text_field(:post_delegator, :title, placeholder: true) @@ -411,7 +411,7 @@ class FormHelperTest < ActionView::TestCase end def test_text_field_placeholder_with_human_attribute_name - with_locale :placeholder do + I18n.with_locale :placeholder do assert_dom_equal('<input id="post_cost" name="post[cost]" placeholder="Total cost" type="text" />', text_field(:post, :cost, placeholder: true)) end end @@ -424,25 +424,25 @@ class FormHelperTest < ActionView::TestCase end def test_text_field_placeholder_with_string_value - with_locale :placeholder do + I18n.with_locale :placeholder do assert_dom_equal('<input id="post_cost" name="post[cost]" placeholder="HOW MUCH?" type="text" />', text_field(:post, :cost, placeholder: "HOW MUCH?")) end end def test_text_field_placeholder_with_human_attribute_name_and_value - with_locale :placeholder do + I18n.with_locale :placeholder do assert_dom_equal('<input id="post_cost" name="post[cost]" placeholder="Pounds" type="text" />', text_field(:post, :cost, placeholder: :uk)) end end def test_text_field_placeholder_with_locales_and_value - with_locale :placeholder do + I18n.with_locale :placeholder do assert_dom_equal('<input id="post_written_on" name="post[written_on]" placeholder="Escrito en" type="text" value="2004-06-15" />', text_field(:post, :written_on, placeholder: :spanish)) end end def test_text_field_placeholder_with_locales_and_nested_attributes - with_locale :placeholder do + I18n.with_locale :placeholder do form_for(@post, html: { id: "create-post" }) do |f| f.fields_for(:comments) do |cf| concat cf.text_field(:body, placeholder: true) @@ -458,7 +458,7 @@ class FormHelperTest < ActionView::TestCase end def test_text_field_placeholder_with_locales_fallback_and_nested_attributes - with_locale :placeholder do + I18n.with_locale :placeholder do form_for(@post, html: { id: "create-post" }) do |f| f.fields_for(:tags) do |cf| concat cf.text_field(:value, placeholder: true) @@ -861,7 +861,7 @@ class FormHelperTest < ActionView::TestCase end def test_text_area_placeholder_without_locales - with_locale :placeholder do + I18n.with_locale :placeholder do assert_dom_equal( %{<textarea id="post_body" name="post[body]" placeholder="Body">\nBack to the hill and over it again!</textarea>}, text_area(:post, :body, placeholder: true) @@ -870,7 +870,7 @@ class FormHelperTest < ActionView::TestCase end def test_text_area_placeholder_with_locales - with_locale :placeholder do + I18n.with_locale :placeholder do assert_dom_equal( %{<textarea id="post_title" name="post[title]" placeholder="What is this about?">\nHello World</textarea>}, text_area(:post, :title, placeholder: true) @@ -879,7 +879,7 @@ class FormHelperTest < ActionView::TestCase end def test_text_area_placeholder_with_human_attribute_name - with_locale :placeholder do + I18n.with_locale :placeholder do assert_dom_equal( %{<textarea id="post_cost" name="post[cost]" placeholder="Total cost">\n</textarea>}, text_area(:post, :cost, placeholder: true) @@ -888,7 +888,7 @@ class FormHelperTest < ActionView::TestCase end def test_text_area_placeholder_with_string_value - with_locale :placeholder do + I18n.with_locale :placeholder do assert_dom_equal( %{<textarea id="post_cost" name="post[cost]" placeholder="HOW MUCH?">\n</textarea>}, text_area(:post, :cost, placeholder: "HOW MUCH?") @@ -897,7 +897,7 @@ class FormHelperTest < ActionView::TestCase end def test_text_area_placeholder_with_human_attribute_name_and_value - with_locale :placeholder do + I18n.with_locale :placeholder do assert_dom_equal( %{<textarea id="post_cost" name="post[cost]" placeholder="Pounds">\n</textarea>}, text_area(:post, :cost, placeholder: :uk) @@ -906,7 +906,7 @@ class FormHelperTest < ActionView::TestCase end def test_text_area_placeholder_with_locales_and_value - with_locale :placeholder do + I18n.with_locale :placeholder do assert_dom_equal( %{<textarea id="post_written_on" name="post[written_on]" placeholder="Escrito en">\n2004-06-15</textarea>}, text_area(:post, :written_on, placeholder: :spanish) @@ -915,7 +915,7 @@ class FormHelperTest < ActionView::TestCase end def test_text_area_placeholder_with_locales_and_nested_attributes - with_locale :placeholder do + I18n.with_locale :placeholder do form_for(@post, html: { id: "create-post" }) do |f| f.fields_for(:comments) do |cf| concat cf.text_area(:body, placeholder: true) @@ -931,7 +931,7 @@ class FormHelperTest < ActionView::TestCase end def test_text_area_placeholder_with_locales_fallback_and_nested_attributes - with_locale :placeholder do + I18n.with_locale :placeholder do form_for(@post, html: { id: "create-post" }) do |f| f.fields_for(:tags) do |cf| concat cf.text_area(:value, placeholder: true) @@ -2260,7 +2260,7 @@ class FormHelperTest < ActionView::TestCase end def test_submit_with_object_as_new_record_and_locale_strings - with_locale :submit do + I18n.with_locale :submit do @post.persisted = false @post.stub(:to_key, nil) do form_for(@post) do |f| @@ -2277,7 +2277,7 @@ class FormHelperTest < ActionView::TestCase end def test_submit_with_object_as_existing_record_and_locale_strings - with_locale :submit do + I18n.with_locale :submit do form_for(@post) do |f| concat f.submit end @@ -2291,7 +2291,7 @@ class FormHelperTest < ActionView::TestCase end def test_submit_without_object_and_locale_strings - with_locale :submit do + I18n.with_locale :submit do form_for(:post) do |f| concat f.submit class: "extra" end @@ -2305,7 +2305,7 @@ class FormHelperTest < ActionView::TestCase end def test_submit_with_object_which_is_overwritten_by_as_option - with_locale :submit do + I18n.with_locale :submit do form_for(@post, as: :another_post) do |f| concat f.submit end @@ -2320,7 +2320,7 @@ class FormHelperTest < ActionView::TestCase def test_submit_with_object_which_is_namespaced blog_post = Blog::Post.new("And his name will be forty and four.", 44) - with_locale :submit do + I18n.with_locale :submit do form_for(blog_post) do |f| concat f.submit end @@ -3554,7 +3554,6 @@ class FormHelperTest < ActionView::TestCase end private - def hidden_fields(options = {}) method = options[:method] @@ -3593,13 +3592,6 @@ class FormHelperTest < ActionView::TestCase false end - def with_locale(testing_locale = :label) - old_locale, I18n.locale = I18n.locale, testing_locale - yield - ensure - I18n.locale = old_locale - end - def with_default_enforce_utf8(value) old_value = ActionView::Helpers::FormTagHelper.default_enforce_utf8 ActionView::Helpers::FormTagHelper.default_enforce_utf8 = value diff --git a/actionview/test/template/lookup_context_test.rb b/actionview/test/template/lookup_context_test.rb index 290f832794..5298afb694 100644 --- a/actionview/test/template/lookup_context_test.rb +++ b/actionview/test/template/lookup_context_test.rb @@ -17,6 +17,16 @@ class LookupContextTest < ActiveSupport::TestCase I18n.locale = :en end + test "rendered_format is deprecated" do + assert_deprecated do + @lookup_context.rendered_format = "foo" + end + + assert_deprecated do + assert_equal "foo", @lookup_context.rendered_format + end + end + test "allows to override default_formats with ActionView::Base.default_formats" do formats = ActionView::Base.default_formats ActionView::Base.default_formats = [:foo, :bar] diff --git a/actionview/test/template/render_test.rb b/actionview/test/template/render_test.rb index 543df7a71a..1f6dbfc4a5 100644 --- a/actionview/test/template/render_test.rb +++ b/actionview/test/template/render_test.rb @@ -69,11 +69,6 @@ module RenderTestCases assert_match "<error>No Comment</error>", @view.render(template: "comments/empty", formats: [:xml]) end - def test_rendered_format_without_format - @view.render(inline: "test") - assert_equal :html, @view.lookup_context.rendered_format - end - def test_render_partial_implicitly_use_format_of_the_rendered_template @view.lookup_context.formats = [:json] assert_equal "Hello world", @view.render(template: "test/one", formats: [:html]) @@ -365,6 +360,10 @@ module RenderTestCases assert_deprecated do ActionView::Base.new ["/a"] end + + assert_deprecated do + ActionView::Base.new ActionView::PathSet.new ["/a"] + end end def test_without_compiled_method_container_is_deprecated @@ -743,10 +742,17 @@ class CachedCollectionViewRenderTest < ActiveSupport::TestCase end teardown do - GC.start I18n.reload! end + test "template body written to cache" do + customer = Customer.new("david", 1) + key = cache_key(customer, "test/_customer") + assert_nil ActionView::PartialRenderer.collection_cache.read(key) + @view.render(partial: "test/customer", collection: [customer], cached: true) + assert_equal "Hello: david", ActionView::PartialRenderer.collection_cache.read(key) + end + test "collection caching does not cache by default" do customer = Customer.new("david", 1) key = cache_key(customer, "test/_customer") @@ -790,7 +796,7 @@ class CachedCollectionViewRenderTest < ActiveSupport::TestCase private def cache_key(*names, virtual_path) - digest = ActionView::Digestor.digest name: virtual_path, finder: @view.lookup_context, dependencies: [] + digest = ActionView::Digestor.digest name: virtual_path, format: :html, finder: @view.lookup_context, dependencies: [] @view.combined_fragment_cache_key([ "#{virtual_path}:#{digest}", *names ]) end end diff --git a/actionview/test/template/template_test.rb b/actionview/test/template/template_test.rb index a069c8f2d0..36caef28c2 100644 --- a/actionview/test/template/template_test.rb +++ b/actionview/test/template/template_test.rb @@ -38,7 +38,8 @@ class TestERBTemplate < ActiveSupport::TestCase "<%= @virtual_path %>", "partial", ERBHandler, - virtual_path: "partial" + virtual_path: "partial", + format: :html ) end @@ -55,7 +56,8 @@ class TestERBTemplate < ActiveSupport::TestCase end end - def new_template(body = "<%= hello %>", details = { format: :html }) + def new_template(body = "<%= hello %>", details = {}) + details = { format: :html }.merge details ActionView::Template.new(body.dup, "hello template", details.fetch(:handler) { ERBHandler }, { virtual_path: "hello" }.merge!(details)) end diff --git a/activejob/lib/active_job/core.rb b/activejob/lib/active_job/core.rb index 2ce008e3da..283125698d 100644 --- a/activejob/lib/active_job/core.rb +++ b/activejob/lib/active_job/core.rb @@ -40,7 +40,7 @@ module ActiveJob # Timezone to be used during the job. attr_accessor :timezone - # Track when a job was enqueded + # Track when a job was enqueued attr_accessor :enqueued_at # These methods will be included into any Active Job object, adding diff --git a/activemodel/lib/active_model/type/helpers/numeric.rb b/activemodel/lib/active_model/type/helpers/numeric.rb index 444847a210..1d8171e25b 100644 --- a/activemodel/lib/active_model/type/helpers/numeric.rb +++ b/activemodel/lib/active_model/type/helpers/numeric.rb @@ -26,15 +26,18 @@ module ActiveModel private def number_to_non_number?(old_value, new_value_before_type_cast) - old_value != nil && non_numeric_string?(new_value_before_type_cast) + old_value != nil && non_numeric_string?(new_value_before_type_cast.to_s) end def non_numeric_string?(value) # 'wibble'.to_i will give zero, we want to make sure # that we aren't marking int zero to string zero as # changed. - !/\A[-+]?\d+/.match?(value.to_s) + !NUMERIC_REGEX.match?(value) end + + NUMERIC_REGEX = /\A\s*[+-]?\d/ + private_constant :NUMERIC_REGEX end end end diff --git a/activemodel/lib/active_model/type/integer.rb b/activemodel/lib/active_model/type/integer.rb index 5878b94171..1e1061ff60 100644 --- a/activemodel/lib/active_model/type/integer.rb +++ b/activemodel/lib/active_model/type/integer.rb @@ -18,17 +18,9 @@ module ActiveModel :integer end - def deserialize(value) - return if value.nil? - value.to_i - end - def serialize(value) - result = super - if result - ensure_in_range(result) - end - result + return if value.is_a?(::String) && non_numeric_string?(value) + ensure_in_range(super) end private @@ -39,9 +31,10 @@ module ActiveModel end def ensure_in_range(value) - unless range.cover?(value) + if value && !range.cover?(value) raise ActiveModel::RangeError, "#{value} is out of range for #{self.class} with limit #{_limit} bytes" end + value end def max_value diff --git a/activemodel/lib/active_model/type/time.rb b/activemodel/lib/active_model/type/time.rb index 16d3efb728..61847a4ce7 100644 --- a/activemodel/lib/active_model/type/time.rb +++ b/activemodel/lib/active_model/type/time.rb @@ -13,10 +13,6 @@ module ActiveModel :time end - def serialize(value) - super || value - end - def user_input_in_time_zone(value) return unless value.present? diff --git a/activemodel/test/cases/type/integer_test.rb b/activemodel/test/cases/type/integer_test.rb index 9bd0110099..6c02c01237 100644 --- a/activemodel/test/cases/type/integer_test.rb +++ b/activemodel/test/cases/type/integer_test.rb @@ -50,11 +50,19 @@ module ActiveModel assert_equal 7200, type.cast(2.hours) end + test "casting string for database" do + type = Type::Integer.new + assert_nil type.serialize("wibble") + assert_equal 5, type.serialize("5wibble") + assert_equal 5, type.serialize(" +5") + assert_equal(-5, type.serialize(" -5")) + end + test "casting empty string" do type = Type::Integer.new assert_nil type.cast("") assert_nil type.serialize("") - assert_equal 0, type.deserialize("") + assert_nil type.deserialize("") end test "changed?" do diff --git a/activemodel/test/cases/validations/conditional_validation_test.rb b/activemodel/test/cases/validations/conditional_validation_test.rb index 1704db9a48..9674068aff 100644 --- a/activemodel/test/cases/validations/conditional_validation_test.rb +++ b/activemodel/test/cases/validations/conditional_validation_test.rb @@ -49,7 +49,7 @@ class ConditionalValidationTest < ActiveModel::TestCase assert_empty t.errors[:title] end - def test_unless_validation_using_array_of_true_and_felse_methods + def test_unless_validation_using_array_of_true_and_false_methods Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", unless: [:condition_is_true, :condition_is_false]) t = Topic.new("title" => "uhohuhoh", "content" => "whatever") assert_predicate t, :valid? @@ -111,14 +111,14 @@ class ConditionalValidationTest < ActiveModel::TestCase assert_equal ["hoo 5"], t.errors["title"] end - def test_validation_using_conbining_if_true_and_unless_true_conditions + def test_validation_using_combining_if_true_and_unless_true_conditions Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", if: :condition_is_true, unless: :condition_is_true) t = Topic.new("title" => "uhohuhoh", "content" => "whatever") assert_predicate t, :valid? assert_empty t.errors[:title] end - def test_validation_using_conbining_if_true_and_unless_false_conditions + def test_validation_using_combining_if_true_and_unless_false_conditions Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", if: :condition_is_true, unless: :condition_is_false) t = Topic.new("title" => "uhohuhoh", "content" => "whatever") assert_predicate t, :invalid? diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 8d8aa89368..7e153a6642 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,47 @@ +* Fix prepared statements caching to be enabled even when query caching is enabled. + + *Ryuta Kamizono* + +* Ensure `update_all` series cares about optimistic locking. + + *Ryuta Kamizono* + +* Don't allow `where` with non numeric string matches to 0 values. + + *Ryuta Kamizono* + +* Introduce `ActiveRecord::Relation#destroy_by` and `ActiveRecord::Relation#delete_by`. + + `destroy_by` allows relation to find all the records matching the condition and perform + `destroy_all` on the matched records. + + Example: + + Person.destroy_by(name: 'David') + Person.destroy_by(name: 'David', rating: 4) + + david = Person.find_by(name: 'David') + david.posts.destroy_by(id: [1, 2, 3]) + + `delete_by` allows relation to find all the records matching the condition and perform + `delete_all` on the matched records. + + Example: + + Person.delete_by(name: 'David') + Person.delete_by(name: 'David', rating: 4) + + david = Person.find_by(name: 'David') + david.posts.delete_by(id: [1, 2, 3]) + + *Abhay Nikam* + +* Don't allow `where` with invalid value matches to nil values. + + Fixes #33624. + + *Ryuta Kamizono* + * SQLite3: Implement `add_foreign_key` and `remove_foreign_key`. *Ryuta Kamizono* diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb index 041e62077c..7048ff43b8 100644 --- a/activerecord/lib/active_record/associations/preloader/association.rb +++ b/activerecord/lib/active_record/associations/preloader/association.rb @@ -115,7 +115,7 @@ module ActiveRecord def build_scope scope = klass.scope_for_association - if reflection.type + if reflection.type && !reflection.through_reflection? scope.where!(reflection.type => model.polymorphic_name) end diff --git a/activerecord/lib/active_record/associations/preloader/through_association.rb b/activerecord/lib/active_record/associations/preloader/through_association.rb index a6b7ab80a2..25254e652a 100644 --- a/activerecord/lib/active_record/associations/preloader/through_association.rb +++ b/activerecord/lib/active_record/associations/preloader/through_association.rb @@ -7,10 +7,9 @@ module ActiveRecord def run(preloader) already_loaded = owners.first.association(through_reflection.name).loaded? through_scope = through_scope() - reflection_scope = target_reflection_scope through_preloaders = preloader.preload(owners, through_reflection.name, through_scope) middle_records = through_preloaders.flat_map(&:preloaded_records) - preloaders = preloader.preload(middle_records, source_reflection.name, reflection_scope) + preloaders = preloader.preload(middle_records, source_reflection.name, scope) @preloaded_records = preloaders.flat_map(&:preloaded_records) owners.each do |owner| @@ -25,18 +24,18 @@ module ActiveRecord owner.association(through_reflection.name).reset if through_scope end result = through_records.flat_map do |record| - association = record.association(source_reflection.name) - target = association.target - association.reset if preload_scope - target + record.association(source_reflection.name).target end result.compact! - if reflection_scope - result.sort_by! { |rhs| preload_index[rhs] } if reflection_scope.order_values.any? - result.uniq! if reflection_scope.distinct_value - end + result.sort_by! { |rhs| preload_index[rhs] } if scope.order_values.any? + result.uniq! if scope.distinct_value associate_records_to_owner(owner, result) end + unless scope.empty_scope? + middle_records.each do |owner| + owner.association(source_reflection.name).reset + end + end end private @@ -91,16 +90,6 @@ module ActiveRecord scope unless scope.empty_scope? end - - def target_reflection_scope - if preload_scope - reflection_scope.merge(preload_scope) - elsif reflection.scope - reflection_scope - else - nil - end - end end end end diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb index 5407af85ea..ef5444dfc3 100644 --- a/activerecord/lib/active_record/callbacks.rb +++ b/activerecord/lib/active_record/callbacks.rb @@ -95,7 +95,7 @@ module ActiveRecord # # private # def delete_parents - # self.class.where(parent_id: id).delete_all + # self.class.delete_by(parent_id: id) # end # end # @@ -324,7 +324,7 @@ module ActiveRecord private - def create_or_update(*) + def create_or_update(**) _run_save_callbacks { super } end @@ -332,7 +332,7 @@ module ActiveRecord _run_create_callbacks { super } end - def _update_record(*) + def _update_record _run_update_callbacks { super } end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb index aa2ecee74a..b5e6d03cf5 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -20,9 +20,22 @@ module ActiveRecord raise "Passing bind parameters with an arel AST is forbidden. " \ "The values must be stored on the AST directly" end - sql, binds = visitor.compile(arel_or_sql_string.ast, collector) - [sql.freeze, binds || []] + + if prepared_statements + sql, binds = visitor.compile(arel_or_sql_string.ast, collector) + + if binds.length > bind_params_length + unprepared_statement do + sql, binds = to_sql_and_binds(arel_or_sql_string) + visitor.preparable = false + end + end + else + sql = visitor.compile(arel_or_sql_string.ast, collector) + end + [sql.freeze, binds] else + visitor.preparable = false if prepared_statements [arel_or_sql_string.dup.freeze, binds] end end @@ -47,13 +60,8 @@ module ActiveRecord arel = arel_from_relation(arel) sql, binds = to_sql_and_binds(arel, binds) - if !prepared_statements || (arel.is_a?(String) && preparable.nil?) - preparable = false - elsif binds.length > bind_params_length - sql, binds = unprepared_statement { to_sql_and_binds(arel) } - preparable = false - else - preparable = visitor.preparable + if preparable.nil? + preparable = prepared_statements ? visitor.preparable : false end if prepared_statements && preparable diff --git a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb index d950099bab..93b1c4e632 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb @@ -97,9 +97,8 @@ module ActiveRecord arel = arel_from_relation(arel) sql, binds = to_sql_and_binds(arel, binds) - if binds.length > bind_params_length - sql, binds = unprepared_statement { to_sql_and_binds(arel) } - preparable = false + if preparable.nil? + preparable = prepared_statements ? visitor.preparable : false end cache_sql(sql, name, binds) { super(sql, name, binds, preparable: preparable) } diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb index a27751fd70..11d4b4a503 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -208,7 +208,7 @@ module ActiveRecord ## # :method: column - # :call-seq: column(name, type, options = {}) + # :call-seq: column(name, type, **options) # # Appends a column or columns of a specified type. # @@ -364,7 +364,7 @@ module ActiveRecord # t.references :tagger, polymorphic: true # t.references :taggable, polymorphic: { default: 'Photo' }, index: false # end - def column(name, type, options = {}) + def column(name, type, **options) name = name.to_s type = type.to_sym if type options = options.dup @@ -541,7 +541,7 @@ module ActiveRecord # t.column(:name, :string) # # See TableDefinition#column for details of the options you can use. - def column(column_name, type, options = {}) + def column(column_name, type, **options) index_options = options.delete(:index) @base.add_column(name, column_name, type, options) index(column_name, index_options.is_a?(Hash) ? index_options : {}) if index_options diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb index 78e153bcc9..b2cc60f363 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -584,7 +584,7 @@ module ActiveRecord # # Defines a column with a database-specific type. # add_column(:shapes, :triangle, 'polygon') # # ALTER TABLE "shapes" ADD "triangle" polygon - def add_column(table_name, column_name, type, options = {}) + def add_column(table_name, column_name, type, **options) at = create_alter_table table_name at.add_column(column_name, type, options) execute schema_creation.accept at @@ -852,7 +852,7 @@ module ActiveRecord # [<tt>:null</tt>] # Whether the column allows nulls. Defaults to true. # - # ====== Create a user_id bigint column without a index + # ====== Create a user_id bigint column without an index # # add_reference(:products, :user, index: false) # diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index 3c9510e469..f205d77ddb 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -102,7 +102,7 @@ module ActiveRecord end def self.build_read_query_regexp(*parts) # :nodoc: - parts = parts.map { |part| /\A\s*#{part}/i } + parts = parts.map { |part| /\A[\(\s]*#{part}/i } Regexp.union(*parts) end @@ -506,6 +506,10 @@ module ActiveRecord @connection end + def default_uniqueness_comparison(attribute, value) # :nodoc: + case_sensitive_comparison(attribute, value) + end + def case_sensitive_comparison(attribute, value) # :nodoc: attribute.eq(value) end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb index 246c6b5123..37506a97e2 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -431,30 +431,6 @@ module ActiveRecord table_options end - # Maps logical Rails types to MySQL-specific data types. - def type_to_sql(type, limit: nil, precision: nil, scale: nil, unsigned: nil, **) # :nodoc: - sql = \ - case type.to_s - when "integer" - integer_to_sql(limit) - when "text" - text_to_sql(limit) - when "blob" - binary_to_sql(limit) - when "binary" - if (0..0xfff) === limit - "varbinary(#{limit})" - else - binary_to_sql(limit) - end - else - super - end - - sql = "#{sql} unsigned" if unsigned && type != :primary_key - sql - end - # SHOW VARIABLES LIKE 'name' def show_variable(name) query_value("SELECT @@#{name}", "SCHEMA") @@ -818,37 +794,6 @@ module ActiveRecord MismatchedForeignKey.new(options) end - def integer_to_sql(limit) # :nodoc: - case limit - when 1; "tinyint" - when 2; "smallint" - when 3; "mediumint" - when nil, 4; "int" - when 5..8; "bigint" - else raise(ActiveRecordError, "No integer type has byte size #{limit}. Use a decimal with scale 0 instead.") - end - end - - def text_to_sql(limit) # :nodoc: - case limit - when 0..0xff; "tinytext" - when nil, 0x100..0xffff; "text" - when 0x10000..0xffffff; "mediumtext" - when 0x1000000..0xffffffff; "longtext" - else raise(ActiveRecordError, "No text type has byte length #{limit}") - end - end - - def binary_to_sql(limit) # :nodoc: - case limit - when 0..0xff; "tinyblob" - when nil, 0x100..0xffff; "blob" - when 0x10000..0xffffff; "mediumblob" - when 0x1000000..0xffffffff; "longblob" - else raise(ActiveRecordError, "No binary type has byte length #{limit}") - end - end - def version_string full_version.match(/^(?:5\.5\.5-)?(\d+\.\d+\.\d+)/)[1] end diff --git a/activerecord/lib/active_record/connection_adapters/determine_if_preparable_visitor.rb b/activerecord/lib/active_record/connection_adapters/determine_if_preparable_visitor.rb index 883747b84b..1df4dea2d8 100644 --- a/activerecord/lib/active_record/connection_adapters/determine_if_preparable_visitor.rb +++ b/activerecord/lib/active_record/connection_adapters/determine_if_preparable_visitor.rb @@ -3,7 +3,7 @@ module ActiveRecord module ConnectionAdapters module DetermineIfPreparableVisitor - attr_reader :preparable + attr_accessor :preparable def accept(*) @preparable = true diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb index 8cb6b64fec..d21535a709 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb @@ -64,16 +64,6 @@ module ActiveRecord case type when :virtual type = options[:type] - when :text, :blob, :binary - case (size = options[:size])&.to_s - when "tiny", "medium", "long" - sql_type = @conn.native_database_types[type][:name] - type = "#{size}#{sql_type}" - else - raise ArgumentError, <<~MSG unless size.nil? - #{size.inspect} is invalid :size value. Only :tiny, :medium, and :long are allowed. - MSG - end when :primary_key type = :integer options[:limit] ||= 8 diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb index e9484a08de..4018f0815c 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb @@ -97,6 +97,30 @@ module ActiveRecord MySQL::SchemaDumper.create(self, options) end + # Maps logical Rails types to MySQL-specific data types. + def type_to_sql(type, limit: nil, precision: nil, scale: nil, size: limit_to_size(limit, type), unsigned: nil, **) + sql = + case type.to_s + when "integer" + integer_to_sql(limit) + when "text" + type_with_size_to_sql("text", size) + when "blob" + type_with_size_to_sql("blob", size) + when "binary" + if (0..0xfff) === limit + "varbinary(#{limit})" + else + type_with_size_to_sql("blob", size) + end + else + super + end + + sql = "#{sql} unsigned" if unsigned && type != :primary_key + sql + end + private CHARSETS_OF_4BYTES_MAXLEN = ["utf8mb4", "utf16", "utf16le", "utf32"] @@ -197,6 +221,40 @@ module ActiveRecord schema, name = nil, schema unless name [schema, name] end + + def type_with_size_to_sql(type, size) + case size&.to_s + when nil, "tiny", "medium", "long" + "#{size}#{type}" + else + raise ArgumentError, + "#{size.inspect} is invalid :size value. Only :tiny, :medium, and :long are allowed." + end + end + + def limit_to_size(limit, type) + case type.to_s + when "text", "blob", "binary" + case limit + when 0..0xff; "tiny" + when nil, 0x100..0xffff; nil + when 0x10000..0xffffff; "medium" + when 0x1000000..0xffffffff; "long" + else raise ActiveRecordError, "No #{type} type has byte size #{limit}" + end + end + end + + def integer_to_sql(limit) + case limit + when 1; "tinyint" + when 2; "smallint" + when 3; "mediumint" + when nil, 4; "int" + when 5..8; "bigint" + else raise ActiveRecordError, "No integer type has byte size #{limit}. Use a decimal with scale 0 instead." + end + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb index d85f9ab3ef..aa7701e038 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb @@ -64,7 +64,7 @@ module ActiveRecord end def type_cast_single_for_database(value) - infinity?(value) ? value : @subtype.serialize(value) + infinity?(value) ? value : @subtype.serialize(@subtype.cast(value)) end def extract_bounds(value) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb index bc9b8dbfcf..28abdbd073 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb @@ -13,9 +13,12 @@ module ActiveRecord :uuid end - def cast(value) - value.to_s[ACCEPTABLE_UUID, 0] - end + private + + def cast_value(value) + casted = value.to_s + casted if casted.match?(ACCEPTABLE_UUID) + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb index 0895d06356..d40e0ef1f0 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb @@ -138,7 +138,7 @@ module ActiveRecord end def encode_range(range) - "[#{type_cast_range_value(range.first)},#{type_cast_range_value(range.last)}#{range.exclude_end? ? ')' : ']'}" + "[#{type_cast_range_value(range.begin)},#{type_cast_range_value(range.end)}#{range.exclude_end? ? ')' : ']'}" end def determine_encoding_of_strings_in_array(value) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb index 946436f7f9..d694a4f47d 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -548,14 +548,14 @@ module ActiveRecord # The hard limit is 1GB, because of a 32-bit size field, and TOAST. case limit when nil, 0..0x3fffffff; super(type) - else raise(ActiveRecordError, "No binary type has byte size #{limit}.") + else raise ActiveRecordError, "No binary type has byte size #{limit}. The limit on binary can be at most 1GB - 1byte." end when "text" # PostgreSQL doesn't support limits on text columns. # The hard limit is 1GB, according to section 8.3 in the manual. case limit when nil, 0..0x3fffffff; super(type) - else raise(ActiveRecordError, "The limit on text can be at most 1GB - 1byte.") + else raise ActiveRecordError, "No text type has byte size #{limit}. The limit on text can be at most 1GB - 1byte." end when "integer" case limit diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index 4b2e9ed81c..c20274420f 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -1323,7 +1323,7 @@ module ActiveRecord def record_version_state_after_migrating(version) if down? migrated.delete(version) - ActiveRecord::SchemaMigration.where(version: version.to_s).delete_all + ActiveRecord::SchemaMigration.delete_by(version: version.to_s) else migrated << version ActiveRecord::SchemaMigration.create!(version: version.to_s) diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index 510a275b4e..10148d0dca 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -161,7 +161,7 @@ module ActiveRecord # # Delete multiple rows # Todo.delete([2,3,4]) def delete(id_or_array) - where(primary_key => id_or_array).delete_all + delete_by(primary_key => id_or_array) end def _insert_record(values) # :nodoc: @@ -707,10 +707,10 @@ module ActiveRecord ) end - def create_or_update(*args, &block) + def create_or_update(**, &block) _raise_readonly_record_error if readonly? return false if destroyed? - result = new_record? ? _create_record(&block) : _update_record(*args, &block) + result = new_record? ? _create_record(&block) : _update_record(&block) result != false end diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb index 2a27f53f06..b3eeb60571 100644 --- a/activerecord/lib/active_record/querying.rb +++ b/activerecord/lib/active_record/querying.rb @@ -7,7 +7,7 @@ module ActiveRecord delegate :first_or_create, :first_or_create!, :first_or_initialize, to: :all delegate :find_or_create_by, :find_or_create_by!, :create_or_find_by, :create_or_find_by!, :find_or_initialize_by, to: :all delegate :find_by, :find_by!, to: :all - delegate :destroy_all, :delete_all, :update_all, to: :all + delegate :destroy_all, :delete_all, :update_all, :destroy_by, :delete_by, to: :all delegate :find_each, :find_in_batches, :in_batches, to: :all delegate :select, :group, :order, :except, :reorder, :limit, :offset, :joins, :left_joins, :left_outer_joins, :or, :where, :rewhere, :preload, :eager_load, :includes, :from, :lock, :readonly, :extending, diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index feaa20ccc6..d612ff53c1 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -372,6 +372,12 @@ module ActiveRecord stmt.wheres = arel.constraints if updates.is_a?(Hash) + if klass.locking_enabled? && + !updates.key?(klass.locking_column) && + !updates.key?(klass.locking_column.to_sym) + attr = arel_attribute(klass.locking_column) + updates[attr.name] = _increment_attribute(attr) + end stmt.set _substitute_values(updates) else stmt.set Arel.sql(klass.sanitize_sql_for_assignment(updates, table.name)) @@ -394,10 +400,7 @@ module ActiveRecord updates = {} counters.each do |counter_name, value| attr = arel_attribute(counter_name) - bind = predicate_builder.build_bind_attribute(attr.name, value.abs) - expr = table.coalesce(Arel::Nodes::UnqualifiedColumn.new(attr), 0) - expr = value < 0 ? expr - bind : expr + bind - updates[counter_name] = expr.expr + updates[attr.name] = _increment_attribute(attr, value) end if touch @@ -433,12 +436,7 @@ module ActiveRecord # Person.where(name: 'David').touch_all # # => "UPDATE \"people\" SET \"updated_at\" = '2018-01-04 22:55:23.132670' WHERE \"people\".\"name\" = 'David'" def touch_all(*names, time: nil) - if klass.locking_enabled? - names << { time: time } - update_counters(klass.locking_column => 1, touch: names) - else - update_all klass.touch_attributes_with_time(*names, time: time) - end + update_all klass.touch_attributes_with_time(*names, time: time) end # Destroys the records by instantiating each @@ -507,6 +505,32 @@ module ActiveRecord affected end + # Finds and destroys all records matching the specified conditions. + # This is short-hand for <tt>relation.where(condition).destroy_all</tt>. + # Returns the collection of objects that were destroyed. + # + # If no record is found, returns empty array. + # + # Person.destroy_by(id: 13) + # Person.destroy_by(name: 'Spartacus', rating: 4) + # Person.destroy_by("published_at < ?", 2.weeks.ago) + def destroy_by(*args) + where(*args).destroy_all + end + + # Finds and deletes all records matching the specified conditions. + # This is short-hand for <tt>relation.where(condition).delete_all</tt>. + # Returns the number of rows affected. + # + # If no record is found, returns <tt>0</tt> as zero rows were affected. + # + # Person.delete_by(id: 13) + # Person.delete_by(name: 'Spartacus', rating: 4) + # Person.delete_by("published_at < ?", 2.weeks.ago) + def delete_by(*args) + where(*args).delete_all + end + # Causes the records to be loaded from the database if they have not # been loaded already. You can use this if for some reason you need # to explicitly load some records before actually using them. The @@ -683,6 +707,13 @@ module ActiveRecord end end + def _increment_attribute(attribute, value = 1) + bind = predicate_builder.build_bind_attribute(attribute.name, value.abs) + expr = table.coalesce(Arel::Nodes::UnqualifiedColumn.new(attribute), 0) + expr = value < 0 ? expr - bind : expr + bind + expr.expr + end + def exec_queries(&block) skip_query_cache_if_necessary do @records = diff --git a/activerecord/lib/active_record/relation/query_attribute.rb b/activerecord/lib/active_record/relation/query_attribute.rb index 1dd6462d8d..cd18f27330 100644 --- a/activerecord/lib/active_record/relation/query_attribute.rb +++ b/activerecord/lib/active_record/relation/query_attribute.rb @@ -18,8 +18,10 @@ module ActiveRecord end def nil? - !value_before_type_cast.is_a?(StatementCache::Substitute) && - (value_before_type_cast.nil? || value_for_database.nil?) + unless value_before_type_cast.is_a?(StatementCache::Substitute) + value_before_type_cast.nil? || + type.respond_to?(:subtype, true) && value_for_database.nil? + end rescue ::RangeError end diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 3566a57ddc..f69b85af66 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -1068,8 +1068,10 @@ module ActiveRecord def arel_column(field) field = klass.attribute_alias(field) if klass.attribute_alias?(field) + from = from_clause.name || from_clause.value - if klass.columns_hash.key?(field) && !from_clause.value + if klass.columns_hash.key?(field) && + (!from || from == table.name || from == connection.quote_table_name(table.name)) arel_attribute(field) else yield @@ -1157,9 +1159,9 @@ module ActiveRecord order_args.map! do |arg| case arg when Symbol - field = arg.to_s - arel_column(field) { - Arel.sql(connection.quote_table_name(field)) + arg = arg.to_s + arel_column(arg) { + Arel.sql(connection.quote_table_name(arg)) }.asc when Hash arg.map { |field, dir| diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb index 2345db7138..04a1c03474 100644 --- a/activerecord/lib/active_record/timestamp.rb +++ b/activerecord/lib/active_record/timestamp.rb @@ -101,8 +101,8 @@ module ActiveRecord super end - def _update_record(*args, touch: true, **options) - if touch && should_record_timestamps? + def _update_record + if @_touch_record && should_record_timestamps? current_time = current_time_from_proper_timezone timestamp_attributes_for_update_in_model.each do |column| @@ -110,7 +110,13 @@ module ActiveRecord _write_attribute(column, current_time) end end - super(*args) + + super + end + + def create_or_update(touch: true, **) + @_touch_record = touch + super end def should_record_timestamps? diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index fb745af125..88ae62c681 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -12,7 +12,7 @@ module ActiveRecord raise ArgumentError, "#{options[:scope]} is not supported format for :scope option. " \ "Pass a symbol or an array of symbols instead: `scope: :user_id`" end - super({ case_sensitive: true }.merge!(options)) + super @klass = options[:class] end @@ -62,6 +62,8 @@ module ActiveRecord if bind.nil? attr.eq(bind) + elsif !options.key?(:case_sensitive) + klass.connection.default_uniqueness_comparison(attr, bind) elsif options[:case_sensitive] klass.connection.case_sensitive_comparison(attr, bind) else diff --git a/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb b/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb index ed3f669331..a5b53f76b4 100644 --- a/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb +++ b/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb @@ -204,6 +204,14 @@ class Mysql2AdapterTest < ActiveRecord::Mysql2TestCase end end + def test_doesnt_error_when_a_read_query_with_leading_chars_is_called_while_preventing_writes + @conn.execute("INSERT INTO `engines` (`car_id`) VALUES ('138853948594')") + + @conn.while_preventing_writes do + assert_equal 1, @conn.execute("(\n( SELECT `engines`.* FROM `engines` WHERE `engines`.`car_id` = '138853948594' ) )").entries.count + end + end + private def with_example_table(definition = "id int auto_increment primary key, number int, data varchar(255)", &block) diff --git a/activerecord/test/cases/adapters/postgresql/hstore_test.rb b/activerecord/test/cases/adapters/postgresql/hstore_test.rb index 4b061a9375..cd45975f70 100644 --- a/activerecord/test/cases/adapters/postgresql/hstore_test.rb +++ b/activerecord/test/cases/adapters/postgresql/hstore_test.rb @@ -2,6 +2,7 @@ require "cases/helper" require "support/schema_dumping_helper" +require "support/stubs/strong_parameters" class PostgresqlHstoreTest < ActiveRecord::PostgreSQLTestCase include SchemaDumpingHelper @@ -11,12 +12,6 @@ class PostgresqlHstoreTest < ActiveRecord::PostgreSQLTestCase store_accessor :settings, :language, :timezone end - class FakeParameters - def to_unsafe_h - { "hi" => "hi" } - end - end - def setup @connection = ActiveRecord::Base.connection @@ -344,7 +339,7 @@ class PostgresqlHstoreTest < ActiveRecord::PostgreSQLTestCase end def test_supports_to_unsafe_h_values - assert_equal("\"hi\"=>\"hi\"", @type.serialize(FakeParameters.new)) + assert_equal "\"hi\"=>\"hi\"", @type.serialize(ProtectedParams.new("hi" => "hi")) end private diff --git a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb index 34b4fc344b..fbd3cbf90f 100644 --- a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb +++ b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb @@ -432,6 +432,16 @@ module ActiveRecord end end + def test_doesnt_error_when_a_read_query_with_leading_chars_is_called_while_preventing_writes + with_example_table do + @connection.execute("INSERT INTO ex (data) VALUES ('138853948594')") + + @connection.while_preventing_writes do + assert_equal 1, @connection.execute("(\n( SELECT * FROM ex WHERE data = '138853948594' ) )").entries.count + end + end + end + private def with_example_table(definition = "id serial primary key, number integer, data character varying(255)", &block) diff --git a/activerecord/test/cases/adapters/postgresql/range_test.rb b/activerecord/test/cases/adapters/postgresql/range_test.rb index 478cd5aa76..068f1e8bea 100644 --- a/activerecord/test/cases/adapters/postgresql/range_test.rb +++ b/activerecord/test/cases/adapters/postgresql/range_test.rb @@ -375,6 +375,22 @@ class PostgresqlRangeTest < ActiveRecord::PostgreSQLTestCase assert_equal(-Float::INFINITY...Float::INFINITY, record.float_range) end + if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.6.0") + def test_endless_range_values + record = PostgresqlRange.create!( + int4_range: eval("1.."), + int8_range: eval("10.."), + float_range: eval("0.5..") + ) + + record = PostgresqlRange.find(record.id) + + assert_equal 1...Float::INFINITY, record.int4_range + assert_equal 10...Float::INFINITY, record.int8_range + assert_equal 0.5...Float::INFINITY, record.float_range + end + end + private def assert_equal_round_trip(range, attribute, value) round_trip(range, attribute, value) diff --git a/activerecord/test/cases/adapters/postgresql/uuid_test.rb b/activerecord/test/cases/adapters/postgresql/uuid_test.rb index 9912763c1b..d2d8ea8042 100644 --- a/activerecord/test/cases/adapters/postgresql/uuid_test.rb +++ b/activerecord/test/cases/adapters/postgresql/uuid_test.rb @@ -114,6 +114,22 @@ class PostgresqlUUIDTest < ActiveRecord::PostgreSQLTestCase assert_equal "foobar", uuid.guid_before_type_cast end + def test_invalid_uuid_dont_match_to_nil + UUIDType.create! + assert_empty UUIDType.where(guid: "") + assert_empty UUIDType.where(guid: "foobar") + end + + class DuckUUID + def initialize(uuid) + @uuid = uuid + end + + def to_s + @uuid + end + end + def test_acceptable_uuid_regex # Valid uuids ["A0EEBC99-9C0B-4EF8-BB6D-6BB9BD380A11", @@ -125,9 +141,11 @@ class PostgresqlUUIDTest < ActiveRecord::PostgreSQLTestCase # so we shouldn't block it either. (Pay attention to "fb6d" – the "f" here # is invalid – it must be one of 8, 9, A, B, a, b according to the spec.) "{a0eebc99-9c0b-4ef8-fb6d-6bb9bd380a11}", + # Support Object-Oriented UUIDs which respond to #to_s + DuckUUID.new("A0EEBC99-9C0B-4EF8-BB6D-6BB9BD380A11"), ].each do |valid_uuid| uuid = UUIDType.new guid: valid_uuid - assert_not_nil uuid.guid + assert_instance_of String, uuid.guid end # Invalid uuids diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb index 5c41c14171..806cfbfc00 100644 --- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb @@ -625,6 +625,16 @@ module ActiveRecord end end + def test_doesnt_error_when_a_read_query_with_leading_chars_is_called_while_preventing_writes + with_example_table "id int, data string" do + @conn.execute("INSERT INTO ex (data) VALUES ('138853948594')") + + @conn.while_preventing_writes do + assert_equal 1, @conn.execute(" SELECT data from ex WHERE data = '138853948594'").count + end + end + end + private def assert_logged(logs) diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index 7d669198ca..6a7efe2121 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -2073,10 +2073,12 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_associations_order_should_be_priority_over_throughs_order - david = authors(:david) + original = authors(:david) expected = [12, 10, 9, 8, 7, 6, 5, 3, 2, 1] - assert_equal expected, david.comments_desc.map(&:id) - assert_equal expected, Author.includes(:comments_desc).find(david.id).comments_desc.map(&:id) + assert_equal expected, original.comments_desc.map(&:id) + preloaded = Author.includes(:comments_desc).find(original.id) + assert_equal expected, preloaded.comments_desc.map(&:id) + assert_equal original.posts_sorted_by_id.first.comments.map(&:id), preloaded.posts_sorted_by_id.first.comments.map(&:id) end def test_dynamic_find_should_respect_association_order_for_through diff --git a/activerecord/test/cases/associations/nested_through_associations_test.rb b/activerecord/test/cases/associations/nested_through_associations_test.rb index 5821744530..0b83fd8421 100644 --- a/activerecord/test/cases/associations/nested_through_associations_test.rb +++ b/activerecord/test/cases/associations/nested_through_associations_test.rb @@ -610,6 +610,12 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase assert_equal hotel, Hotel.joins(:cake_designers, :drink_designers).take end + def test_has_many_through_reset_source_reflection_after_loading_is_complete + preloaded = Category.preload(:ordered_post_comments).find(1, 2).last + original = Category.find(2) + assert_equal original.ordered_post_comments.ids, preloaded.ordered_post_comments.ids + end + private def assert_includes_and_joins_equal(query, expected, association) diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb index d341dd0083..12ff6d4826 100644 --- a/activerecord/test/cases/attribute_methods_test.rb +++ b/activerecord/test/cases/attribute_methods_test.rb @@ -433,6 +433,10 @@ class AttributeMethodsTest < ActiveRecord::TestCase end assert_equal true, Topic.new(author_name: "Name").author_name? + + ActiveModel::Type::Boolean::FALSE_VALUES.each do |value| + assert_predicate Topic.new(author_name: value), :author_name? + end end test "number attribute predicate" do @@ -711,6 +715,10 @@ class AttributeMethodsTest < ActiveRecord::TestCase record.written_on = "Jan 01 00:00:00 2014" assert_equal record, YAML.load(YAML.dump(record)) end + ensure + # NOTE: Reset column info because global topics + # don't have tz-aware attributes by default. + Topic.reset_column_information end test "setting a time zone-aware time in the current time zone" do diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 63528d09d5..866818b2ab 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -1055,23 +1055,23 @@ class BasicsTest < ActiveRecord::TestCase end def test_find_ordered_last - last = Developer.all.merge!(order: "developers.salary ASC").last - assert_equal last, Developer.all.merge!(order: "developers.salary ASC").to_a.last + last = Developer.order("developers.salary ASC").last + assert_equal last, Developer.order("developers.salary": "ASC").to_a.last end def test_find_reverse_ordered_last - last = Developer.all.merge!(order: "developers.salary DESC").last - assert_equal last, Developer.all.merge!(order: "developers.salary DESC").to_a.last + last = Developer.order("developers.salary DESC").last + assert_equal last, Developer.order("developers.salary": "DESC").to_a.last end def test_find_multiple_ordered_last - last = Developer.all.merge!(order: "developers.name, developers.salary DESC").last - assert_equal last, Developer.all.merge!(order: "developers.name, developers.salary DESC").to_a.last + last = Developer.order("developers.name, developers.salary DESC").last + assert_equal last, Developer.order(:"developers.name", "developers.salary": "DESC").to_a.last end def test_find_keeps_multiple_order_values - combined = Developer.all.merge!(order: "developers.name, developers.salary").to_a - assert_equal combined, Developer.all.merge!(order: ["developers.name", "developers.salary"]).to_a + combined = Developer.order("developers.name, developers.salary").to_a + assert_equal combined, Developer.order(:"developers.name", :"developers.salary").to_a end def test_find_keeps_multiple_group_values diff --git a/activerecord/test/cases/bind_parameter_test.rb b/activerecord/test/cases/bind_parameter_test.rb index ca97a5dc65..8c3fe0437c 100644 --- a/activerecord/test/cases/bind_parameter_test.rb +++ b/activerecord/test/cases/bind_parameter_test.rb @@ -34,6 +34,49 @@ if ActiveRecord::Base.connection.prepared_statements ActiveSupport::Notifications.unsubscribe(@subscription) end + def test_statement_cache + @connection.clear_cache! + + topics = Topic.where(id: 1) + assert_equal [1], topics.map(&:id) + assert_includes statement_cache, to_sql_key(topics.arel) + end + + def test_statement_cache_with_query_cache + @connection.enable_query_cache! + @connection.clear_cache! + + topics = Topic.where(id: 1) + assert_equal [1], topics.map(&:id) + assert_includes statement_cache, to_sql_key(topics.arel) + ensure + @connection.disable_query_cache! + end + + def test_statement_cache_with_find + @connection.clear_cache! + + topics = Topic.where(id: 1).limit(1) + assert_equal 1, Topic.find(1).id + assert_includes statement_cache, to_sql_key(topics.arel) + end + + def test_statement_cache_with_in_clause + @connection.clear_cache! + + topics = Topic.where(id: [1, 3]) + assert_equal [1, 3], topics.map(&:id) + assert_not_includes statement_cache, to_sql_key(topics.arel) + end + + def test_statement_cache_with_sql_string_literal + @connection.clear_cache! + + topics = Topic.where("topics.id = ?", 1) + assert_equal [1], topics.map(&:id) + assert_not_includes statement_cache, to_sql_key(topics.arel) + end + def test_too_many_binds bind_params_length = @connection.send(:bind_params_length) @@ -45,7 +88,8 @@ if ActiveRecord::Base.connection.prepared_statements end def test_too_many_binds_with_query_cache - Topic.connection.enable_query_cache! + @connection.enable_query_cache! + bind_params_length = @connection.send(:bind_params_length) topics = Topic.where(id: (1 .. bind_params_length + 1).to_a) assert_equal Topic.count, topics.count @@ -53,7 +97,7 @@ if ActiveRecord::Base.connection.prepared_statements topics = Topic.where.not(id: (1 .. bind_params_length + 1).to_a) assert_equal 0, topics.count ensure - Topic.connection.disable_query_cache! + @connection.disable_query_cache! end def test_bind_from_join_in_subquery @@ -90,6 +134,15 @@ if ActiveRecord::Base.connection.prepared_statements end private + def to_sql_key(arel) + sql = @connection.to_sql(arel) + @connection.respond_to?(:sql_key, true) ? @connection.send(:sql_key, sql) : sql + end + + def statement_cache + @connection.instance_variable_get(:@statements).send(:cache) + end + def assert_logs_binds(binds) payload = { name: "SQL", diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb index b001667ac9..8ac2d55218 100644 --- a/activerecord/test/cases/calculations_test.rb +++ b/activerecord/test/cases/calculations_test.rb @@ -19,6 +19,7 @@ require "models/developer" require "models/post" require "models/comment" require "models/rating" +require "support/stubs/strong_parameters" class CalculationsTest < ActiveRecord::TestCase fixtures :companies, :accounts, :topics, :speedometers, :minivans, :books, :posts, :comments @@ -278,6 +279,18 @@ class CalculationsTest < ActiveRecord::TestCase assert_equal 3, Account.joins(:firm).distinct.order(:firm_id).limit(3).offset(2).count end + def test_distinct_joins_count_with_group_by + expected = { nil => 4, 1 => 1, 2 => 1, 4 => 1, 5 => 1, 7 => 1 } + assert_equal expected, Post.left_joins(:comments).group(:post_id).distinct.count(:author_id) + assert_equal expected, Post.left_joins(:comments).group(:post_id).distinct.select(:author_id).count + assert_equal expected, Post.left_joins(:comments).group(:post_id).count("DISTINCT posts.author_id") + assert_equal expected, Post.left_joins(:comments).group(:post_id).select("DISTINCT posts.author_id").count + + expected = { nil => 6, 1 => 1, 2 => 1, 4 => 1, 5 => 1, 7 => 1 } + assert_equal expected, Post.left_joins(:comments).group(:post_id).distinct.count(:all) + assert_equal expected, Post.left_joins(:comments).group(:post_id).distinct.select(:author_id).count(:all) + end + def test_distinct_count_with_group_by_and_order_and_limit assert_equal({ 6 => 2 }, Account.group(:firm_id).distinct.order("1 DESC").limit(1).count) end @@ -511,8 +524,10 @@ class CalculationsTest < ActiveRecord::TestCase end def test_should_count_field_of_root_table_with_conflicting_group_by_column - assert_equal({ 1 => 1 }, Firm.joins(:accounts).group(:firm_id).count) - assert_equal({ 1 => 1 }, Firm.joins(:accounts).group("accounts.firm_id").count) + expected = { 1 => 2, 2 => 1, 4 => 5, 5 => 2, 7 => 1 } + assert_equal expected, Post.joins(:comments).group(:post_id).count + assert_equal expected, Post.joins(:comments).group("comments.post_id").count + assert_equal expected, Post.joins(:comments).group(:post_id).select("DISTINCT posts.author_id").count(:all) end def test_count_with_no_parameters_isnt_deprecated @@ -883,26 +898,7 @@ class CalculationsTest < ActiveRecord::TestCase end def test_having_with_strong_parameters - protected_params = Class.new do - attr_reader :permitted - alias :permitted? :permitted - - def initialize(parameters) - @parameters = parameters - @permitted = false - end - - def to_h - @parameters - end - - def permit! - @permitted = true - self - end - end - - params = protected_params.new(credit_limit: "50") + params = ProtectedParams.new(credit_limit: "50") assert_raises(ActiveModel::ForbiddenAttributesError) do Account.group(:id).having(params) diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb index dfd74bfcb4..a2a501a794 100644 --- a/activerecord/test/cases/dirty_test.rb +++ b/activerecord/test/cases/dirty_test.rb @@ -352,7 +352,7 @@ class DirtyTest < ActiveRecord::TestCase Person.where(id: person.id).update_all(first_name: "baz") end - old_lock_version = person.lock_version + old_lock_version = person.lock_version + 1 with_partial_writes Person, true do assert_no_queries { 2.times { person.save! } } diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index 4040682280..f9792bf8d3 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -226,14 +226,14 @@ class FinderTest < ActiveRecord::TestCase end def test_exists_with_strong_parameters - assert_equal false, Subscriber.exists?(Parameters.new(nick: "foo").permit!) + assert_equal false, Subscriber.exists?(ProtectedParams.new(nick: "foo").permit!) Subscriber.create!(nick: "foo") - assert_equal true, Subscriber.exists?(Parameters.new(nick: "foo").permit!) + assert_equal true, Subscriber.exists?(ProtectedParams.new(nick: "foo").permit!) assert_raises(ActiveModel::ForbiddenAttributesError) do - Subscriber.exists?(Parameters.new(nick: "foo")) + Subscriber.exists?(ProtectedParams.new(nick: "foo")) end end diff --git a/activerecord/test/cases/forbidden_attributes_protection_test.rb b/activerecord/test/cases/forbidden_attributes_protection_test.rb index 101fa118c8..e7e31b6d2d 100644 --- a/activerecord/test/cases/forbidden_attributes_protection_test.rb +++ b/activerecord/test/cases/forbidden_attributes_protection_test.rb @@ -1,48 +1,12 @@ # frozen_string_literal: true require "cases/helper" -require "active_support/core_ext/hash/indifferent_access" - require "models/company" require "models/person" require "models/ship" require "models/ship_part" require "models/treasure" - -class ProtectedParams - attr_accessor :permitted - alias :permitted? :permitted - - delegate :keys, :key?, :has_key?, :empty?, to: :@parameters - - def initialize(attributes) - @parameters = attributes.with_indifferent_access - @permitted = false - end - - def permit! - @permitted = true - self - end - - def [](key) - @parameters[key] - end - - def to_h - @parameters - end - - def stringify_keys - dup - end - - def dup - super.tap do |duplicate| - duplicate.instance_variable_set :@permitted, @permitted - end - end -end +require "support/stubs/strong_parameters" class ForbiddenAttributesProtectionTest < ActiveRecord::TestCase def test_forbidden_attributes_cannot_be_used_for_mass_assignment diff --git a/activerecord/test/cases/migration/column_attributes_test.rb b/activerecord/test/cases/migration/column_attributes_test.rb index 3022121f4c..6f9190c110 100644 --- a/activerecord/test/cases/migration/column_attributes_test.rb +++ b/activerecord/test/cases/migration/column_attributes_test.rb @@ -177,10 +177,8 @@ module ActiveRecord if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter) def test_out_of_range_limit_should_raise assert_raise(ActiveRecordError) { add_column :test_models, :integer_too_big, :integer, limit: 10 } - - unless current_adapter?(:PostgreSQLAdapter) - assert_raise(ActiveRecordError) { add_column :test_models, :text_too_big, :text, limit: 0xfffffffff } - end + assert_raise(ActiveRecordError) { add_column :test_models, :text_too_big, :text, limit: 0xfffffffff } + assert_raise(ActiveRecordError) { add_column :test_models, :binary_too_big, :binary, limit: 0xfffffffff } end end end diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index 02031e51ef..0ecd93412e 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -600,6 +600,18 @@ class MigrationTest < ActiveRecord::TestCase end end + def test_decimal_scale_without_precision_should_raise + e = assert_raise(ArgumentError) do + Person.connection.create_table :test_decimal_scales, force: true do |t| + t.decimal :scaleonly, scale: 10 + end + end + + assert_equal "Error adding decimal column: precision cannot be empty if scale is specified", e.message + ensure + Person.connection.drop_table :test_decimal_scales, if_exists: true + end + if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter) def test_out_of_range_integer_limit_should_raise e = assert_raise(ActiveRecord::ActiveRecordError, "integer limit didn't raise") do @@ -608,13 +620,11 @@ class MigrationTest < ActiveRecord::TestCase end end - assert_match(/No integer type has byte size 10/, e.message) + assert_includes e.message, "No integer type has byte size 10" ensure Person.connection.drop_table :test_integer_limits, if_exists: true end - end - if current_adapter?(:Mysql2Adapter) def test_out_of_range_text_limit_should_raise e = assert_raise(ActiveRecord::ActiveRecordError, "text limit didn't raise") do Person.connection.create_table :test_text_limits, force: true do |t| @@ -622,11 +632,25 @@ class MigrationTest < ActiveRecord::TestCase end end - assert_match(/No text type has byte length #{0xfffffffff}/, e.message) + assert_includes e.message, "No text type has byte size #{0xfffffffff}" + ensure + Person.connection.drop_table :test_text_limits, if_exists: true + end + + def test_out_of_range_binary_limit_should_raise + e = assert_raise(ActiveRecord::ActiveRecordError) do + Person.connection.create_table :test_text_limits, force: true do |t| + t.binary :bigbinary, limit: 0xfffffffff + end + end + + assert_includes e.message, "No binary type has byte size #{0xfffffffff}" ensure Person.connection.drop_table :test_text_limits, if_exists: true end + end + if current_adapter?(:Mysql2Adapter) def test_invalid_text_size_should_raise e = assert_raise(ArgumentError) do Person.connection.create_table :test_text_sizes, force: true do |t| @@ -634,7 +658,7 @@ class MigrationTest < ActiveRecord::TestCase end end - assert_match(/#{0xfffffffff} is invalid :size value\. Only :tiny, :medium, and :long are allowed\./, e.message) + assert_equal "#{0xfffffffff} is invalid :size value. Only :tiny, :medium, and :long are allowed.", e.message ensure Person.connection.drop_table :test_text_sizes, if_exists: true end diff --git a/activerecord/test/cases/relation/delegation_test.rb b/activerecord/test/cases/relation/delegation_test.rb index 63ae438de3..4de3b1300c 100644 --- a/activerecord/test/cases/relation/delegation_test.rb +++ b/activerecord/test/cases/relation/delegation_test.rb @@ -50,7 +50,7 @@ module ActiveRecord :first_or_create, :first_or_create!, :first_or_initialize, :find_or_create_by, :find_or_create_by!, :create_or_find_by, :create_or_find_by!, :find_or_initialize_by, :find_by, :find_by!, - :destroy_all, :delete_all, :update_all, + :destroy_all, :delete_all, :update_all, :delete_by, :destroy_by, :find_each, :find_in_batches, :in_batches, :select, :group, :order, :except, :reorder, :limit, :offset, :joins, :left_joins, :left_outer_joins, :or, :where, :rewhere, :preload, :eager_load, :includes, :from, :lock, :readonly, :extending, diff --git a/activerecord/test/cases/relation/update_all_test.rb b/activerecord/test/cases/relation/update_all_test.rb index bb6912148c..0500574f28 100644 --- a/activerecord/test/cases/relation/update_all_test.rb +++ b/activerecord/test/cases/relation/update_all_test.rb @@ -138,14 +138,6 @@ class UpdateAllTest < ActiveRecord::TestCase assert_equal new_time, developer.updated_at end - def test_touch_all_updates_locking_column - person = people(:david) - - assert_difference -> { person.reload.lock_version }, +1 do - Person.where(first_name: "David").touch_all - end - end - def test_update_on_relation topic1 = TopicWithCallbacks.create! title: "arel", author_name: nil topic2 = TopicWithCallbacks.create! title: "activerecord", author_name: nil @@ -186,6 +178,69 @@ class UpdateAllTest < ActiveRecord::TestCase end end + def test_update_all_cares_about_optimistic_locking + david = people(:david) + + travel 5.seconds do + now = Time.now.utc + assert_not_equal now, david.updated_at + + people = Person.where(id: people(:michael, :david, :susan)) + expected = people.pluck(:lock_version) + expected.map! { |version| version + 1 } + people.update_all(updated_at: now) + + assert_equal [now] * 3, people.pluck(:updated_at) + assert_equal expected, people.pluck(:lock_version) + + assert_raises(ActiveRecord::StaleObjectError) do + david.touch(time: now) + end + end + end + + def test_update_counters_cares_about_optimistic_locking + david = people(:david) + + travel 5.seconds do + now = Time.now.utc + assert_not_equal now, david.updated_at + + people = Person.where(id: people(:michael, :david, :susan)) + expected = people.pluck(:lock_version) + expected.map! { |version| version + 1 } + people.update_counters(touch: [time: now]) + + assert_equal [now] * 3, people.pluck(:updated_at) + assert_equal expected, people.pluck(:lock_version) + + assert_raises(ActiveRecord::StaleObjectError) do + david.touch(time: now) + end + end + end + + def test_touch_all_cares_about_optimistic_locking + david = people(:david) + + travel 5.seconds do + now = Time.now.utc + assert_not_equal now, david.updated_at + + people = Person.where(id: people(:michael, :david, :susan)) + expected = people.pluck(:lock_version) + expected.map! { |version| version + 1 } + people.touch_all(time: now) + + assert_equal [now] * 3, people.pluck(:updated_at) + assert_equal expected, people.pluck(:lock_version) + + assert_raises(ActiveRecord::StaleObjectError) do + david.touch(time: now) + end + end + end + # Oracle UPDATE does not support ORDER BY unless current_adapter?(:OracleAdapter) def test_update_all_ignores_order_without_limit_from_association diff --git a/activerecord/test/cases/relation/where_test.rb b/activerecord/test/cases/relation/where_test.rb index d49ed092b2..b045184d7d 100644 --- a/activerecord/test/cases/relation/where_test.rb +++ b/activerecord/test/cases/relation/where_test.rb @@ -14,6 +14,7 @@ require "models/price_estimate" require "models/topic" require "models/treasure" require "models/vertex" +require "support/stubs/strong_parameters" module ActiveRecord class WhereTest < ActiveRecord::TestCase @@ -50,8 +51,13 @@ module ActiveRecord assert_equal [chef], chefs.to_a end - def test_where_with_casted_value_is_nil - assert_equal 4, Topic.where(last_read: "").count + def test_where_with_invalid_value + topics(:first).update!(parent_id: 0, written_on: nil, bonus_time: nil, last_read: nil) + assert_empty Topic.where(parent_id: Object.new) + assert_empty Topic.where(parent_id: "not-a-number") + assert_empty Topic.where(written_on: "") + assert_empty Topic.where(bonus_time: "") + assert_empty Topic.where(last_read: "") end def test_rewhere_on_root @@ -334,27 +340,8 @@ module ActiveRecord end def test_where_with_strong_parameters - protected_params = Class.new do - attr_reader :permitted - alias :permitted? :permitted - - def initialize(parameters) - @parameters = parameters - @permitted = false - end - - def to_h - @parameters - end - - def permit! - @permitted = true - self - end - end - author = authors(:david) - params = protected_params.new(name: author.name) + params = ProtectedParams.new(name: author.name) assert_raises(ActiveModel::ForbiddenAttributesError) { Author.where(params) } assert_equal author, Author.where(params.permit!).first end diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index c82f93a3f0..2de0a81c99 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -182,6 +182,43 @@ class RelationTest < ActiveRecord::TestCase end end + def test_select_with_original_table_name_in_from + relation = Comment.joins(:post).select(:id).order(:id) + subquery = Comment.from(Comment.table_name).joins(:post).select(:id).order(:id) + assert_equal relation.map(&:id), subquery.map(&:id) + end + + def test_pluck_with_original_table_name_in_from + relation = Comment.joins(:post).order(:id) + subquery = Comment.from(Comment.table_name).joins(:post).order(:id) + assert_equal relation.pluck(:id), subquery.pluck(:id) + end + + def test_select_with_quoted_original_table_name_in_from + relation = Comment.joins(:post).select(:id).order(:id) + subquery = Comment.from(Comment.quoted_table_name).joins(:post).select(:id).order(:id) + assert_equal relation.map(&:id), subquery.map(&:id) + end + + def test_pluck_with_quoted_original_table_name_in_from + relation = Comment.joins(:post).order(:id) + subquery = Comment.from(Comment.quoted_table_name).joins(:post).order(:id) + assert_equal relation.pluck(:id), subquery.pluck(:id) + end + + def test_select_with_subquery_in_from_uses_original_table_name + relation = Comment.joins(:post).select(:id).order(:id) + # Avoid subquery flattening by adding distinct to work with SQLite < 3.20.0. + subquery = Comment.from(Comment.all.distinct, Comment.quoted_table_name).joins(:post).select(:id).order(:id) + assert_equal relation.map(&:id), subquery.map(&:id) + end + + def test_pluck_with_subquery_in_from_uses_original_table_name + relation = Comment.joins(:post).order(:id) + subquery = Comment.from(Comment.all, Comment.quoted_table_name).joins(:post).order(:id) + assert_equal relation.pluck(:id), subquery.pluck(:id) + end + def test_select_with_subquery_in_from_does_not_use_original_table_name relation = Comment.group(:type).select("COUNT(post_id) AS post_count, type") subquery = Comment.from(relation).select("type", "post_count") @@ -1686,6 +1723,24 @@ class RelationTest < ActiveRecord::TestCase assert_predicate topics, :loaded? end + def test_delete_by + david = authors(:david) + + assert_difference("Post.count", -3) { david.posts.delete_by(body: "hello") } + + deleted = Author.delete_by(id: david.id) + assert_equal 1, deleted + end + + def test_destroy_by + david = authors(:david) + + assert_difference("Post.count", -3) { david.posts.destroy_by(body: "hello") } + + destroyed = Author.destroy_by(id: david.id) + assert_equal [david], destroyed + end + test "find_by with hash conditions returns the first matching record" do assert_equal posts(:eager_other), Post.order(:id).find_by(author_id: 2) end @@ -1886,6 +1941,30 @@ class RelationTest < ActiveRecord::TestCase assert_equal p2.first.comments, comments end + def test_unscope_with_merge + p0 = Post.where(author_id: 0) + p1 = Post.where(author_id: 1, comments_count: 1) + + assert_equal [posts(:authorless)], p0 + assert_equal [posts(:thinking)], p1 + + comments = Comment.merge(p0).unscope(where: :author_id).where(post: p1) + + assert_not_equal p0.first.comments, comments + assert_equal p1.first.comments, comments + end + + def test_unscope_with_unknown_column + comment = comments(:greetings) + comment.update!(comments: 1) + + comments = Comment.where(comments: 1).unscope(where: :unknown_column) + assert_equal [comment], comments + + comments = Comment.where(comments: 1).unscope(where: { comments: :unknown_column }) + assert_equal [comment], comments + end + def test_unscope_specific_where_value posts = Post.where(title: "Welcome to the weblog", body: "Such a lovely day") diff --git a/activerecord/test/models/category.rb b/activerecord/test/models/category.rb index 2ccc00bed9..8c86879dc6 100644 --- a/activerecord/test/models/category.rb +++ b/activerecord/test/models/category.rb @@ -26,6 +26,7 @@ class Category < ActiveRecord::Base has_many :categorizations has_many :special_categorizations has_many :post_comments, through: :posts, source: :comments + has_many :ordered_post_comments, -> { order(id: :desc) }, through: :posts, source: :comments has_many :authors, through: :categorizations has_many :authors_with_select, -> { select "authors.*, categorizations.post_id" }, through: :categorizations, source: :author diff --git a/activerecord/test/models/topic.rb b/activerecord/test/models/topic.rb index 75890c327a..0c8880a20e 100644 --- a/activerecord/test/models/topic.rb +++ b/activerecord/test/models/topic.rb @@ -99,7 +99,7 @@ class Topic < ActiveRecord::Base end def destroy_children - self.class.where("parent_id = #{id}").delete_all + self.class.delete_by(parent_id: id) end def set_email_address diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 349b8afc48..1dfea0f506 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -94,17 +94,18 @@ ActiveRecord::Schema.define do end create_table :books, id: :integer, force: true do |t| + default_zero = { default: 0 } t.references :author t.string :format t.column :name, :string - t.column :status, :integer, default: 0 - t.column :read_status, :integer, default: 0 + t.column :status, :integer, **default_zero + t.column :read_status, :integer, **default_zero t.column :nullable_status, :integer - t.column :language, :integer, default: 0 - t.column :author_visibility, :integer, default: 0 - t.column :illustrator_visibility, :integer, default: 0 - t.column :font_size, :integer, default: 0 - t.column :difficulty, :integer, default: 0 + t.column :language, :integer, **default_zero + t.column :author_visibility, :integer, **default_zero + t.column :illustrator_visibility, :integer, **default_zero + t.column :font_size, :integer, **default_zero + t.column :difficulty, :integer, **default_zero t.column :cover, :string, default: "hard" end diff --git a/activerecord/test/support/stubs/strong_parameters.rb b/activerecord/test/support/stubs/strong_parameters.rb index 84f93a28b9..da8f9892f9 100644 --- a/activerecord/test/support/stubs/strong_parameters.rb +++ b/activerecord/test/support/stubs/strong_parameters.rb @@ -1,6 +1,10 @@ # frozen_string_literal: true -class Parameters +require "active_support/core_ext/hash/indifferent_access" + +class ProtectedParams + delegate :keys, :key?, :has_key?, :empty?, to: :@parameters + def initialize(parameters = {}) @parameters = parameters.with_indifferent_access @permitted = false @@ -15,7 +19,22 @@ class Parameters self end + def [](key) + @parameters[key] + end + def to_h @parameters.to_h end + alias to_unsafe_h to_h + + def stringify_keys + dup + end + + def dup + super.tap do |duplicate| + duplicate.instance_variable_set :@permitted, @permitted + end + end end diff --git a/activestorage/lib/active_storage.rb b/activestorage/lib/active_storage.rb index e542c4b2ca..5c5da551ae 100644 --- a/activestorage/lib/active_storage.rb +++ b/activestorage/lib/active_storage.rb @@ -26,6 +26,7 @@ require "active_record" require "active_support" require "active_support/rails" +require "active_support/core_ext/numeric/time" require "active_storage/version" require "active_storage/errors" diff --git a/activesupport/activesupport.gemspec b/activesupport/activesupport.gemspec index 2fa0623a9c..c92691afaf 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.2" + s.add_dependency "zeitwerk", "~> 1.3", ">= 1.3.1" end diff --git a/activesupport/lib/active_support/dependencies/zeitwerk_integration.rb b/activesupport/lib/active_support/dependencies/zeitwerk_integration.rb index 23c237796e..ca4385b7c2 100644 --- a/activesupport/lib/active_support/dependencies/zeitwerk_integration.rb +++ b/activesupport/lib/active_support/dependencies/zeitwerk_integration.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "active_support/core_ext/string/inflections" + module ActiveSupport module Dependencies module ZeitwerkIntegration # :nodoc: all @@ -11,11 +13,11 @@ module ActiveSupport end def constantize(cpath) - Inflector.constantize(cpath) + ActiveSupport::Inflector.constantize(cpath) end def safe_constantize(cpath) - Inflector.safe_constantize(cpath) + ActiveSupport::Inflector.safe_constantize(cpath) end def autoloaded_constants @@ -28,9 +30,19 @@ module ActiveSupport end def verbose=(verbose) - l = verbose ? (logger || Rails.logger).method(:debug) : nil + l = verbose ? logger || Rails.logger : nil Rails.autoloaders.each { |autoloader| autoloader.logger = l } end + + def unhook! + :no_op + end + end + + module Inflector + def self.camelize(basename, _abspath) + basename.camelize + end end class << self @@ -43,6 +55,10 @@ module ActiveSupport private def setup_autoloaders + Rails.autoloaders.each do |autoloader| + autoloader.inflector = Inflector + end + Dependencies.autoload_paths.each do |autoload_path| # Zeitwerk only accepts existing directories in `push_dir` to # prevent misconfigurations. @@ -69,6 +85,7 @@ module ActiveSupport end def decorate_dependencies + Dependencies.unhook! Dependencies.singleton_class.prepend(Decorations) Object.class_eval { alias_method :require_dependency, :require } end diff --git a/guides/source/active_record_basics.md b/guides/source/active_record_basics.md index 4cf4111bf0..5c1de97aa8 100644 --- a/guides/source/active_record_basics.md +++ b/guides/source/active_record_basics.md @@ -309,12 +309,12 @@ user = User.find_by(name: 'David') user.destroy ``` -If you'd like to delete several records in bulk, you may use `destroy_all` -method: +If you'd like to delete several records in bulk, you may use `destroy_by` +or `destroy_all` method: ```ruby # find and delete all users named David -User.where(name: 'David').destroy_all +User.destroy_by(name: 'David') # delete all users User.destroy_all diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index ebdee446f9..614737c342 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -239,13 +239,12 @@ Skipping Callbacks Just as with validations, it is also possible to skip callbacks by using the following methods: -* `decrement` +* `decrement!` * `decrement_counter` * `delete` * `delete_all` -* `increment` +* `increment!` * `increment_counter` -* `toggle` * `update_column` * `update_columns` * `update_all` @@ -310,7 +309,7 @@ end ### Using `:if` and `:unless` with a `Proc` -Finally, it is possible to associate `:if` and `:unless` with a `Proc` object. This option is best suited when writing short validation methods, usually one-liners: +It is possible to associate `:if` and `:unless` with a `Proc` object. This option is best suited when writing short validation methods, usually one-liners: ```ruby class Order < ApplicationRecord @@ -338,6 +337,20 @@ class Comment < ApplicationRecord end ``` +### Combining Callback Conditions + +When multiple conditions define whether or not a callback should happen, an `Array` can be used. Moreover, you can apply both `:if` and `:unless` to the same callback. + +```ruby +class Comment < ApplicationRecord + after_create :send_email_to_author, + if: [Proc.new { |c| c.user.allow_send_email? }, :author_wants_emails?], + unless: Proc.new { |c| c.article.ignore_comments? } +end +``` + +The callback only runs when all the `:if` conditions and none of the `:unless` conditions are evaluated to `true`. + Callback Classes ---------------- diff --git a/guides/source/active_record_querying.md b/guides/source/active_record_querying.md index fd1dcf22c0..cb738f0657 100644 --- a/guides/source/active_record_querying.md +++ b/guides/source/active_record_querying.md @@ -623,6 +623,8 @@ To select only a subset of fields from the result set, you can specify the subse For example, to select only `viewable_by` and `locked` columns: ```ruby +Client.select(:viewable_by, :locked) +# OR Client.select("viewable_by, locked") ``` diff --git a/guides/source/active_record_validations.md b/guides/source/active_record_validations.md index 0fda7c5cfd..0c57802188 100644 --- a/guides/source/active_record_validations.md +++ b/guides/source/active_record_validations.md @@ -934,7 +934,7 @@ end ### Using a Proc with `:if` and `:unless` -Finally, it's possible to associate `:if` and `:unless` with a `Proc` object +It is possible to associate `:if` and `:unless` with a `Proc` object which will be called. Using a `Proc` object gives you the ability to write an inline condition instead of a separate method. This option is best suited for one-liners. diff --git a/guides/source/getting_started.md b/guides/source/getting_started.md index be59cd0cfa..7ee0d8c916 100644 --- a/guides/source/getting_started.md +++ b/guides/source/getting_started.md @@ -683,7 +683,7 @@ If you look in the `db/migrate/YYYYMMDDHHMMSS_create_articles.rb` file (remember, yours will have a slightly different name), here's what you'll find: ```ruby -class CreateArticles < ActiveRecord::Migration[5.0] +class CreateArticles < ActiveRecord::Migration[6.0] def change create_table :articles do |t| t.string :title @@ -1555,7 +1555,7 @@ In addition to the model, Rails has also made a migration to create the corresponding database table: ```ruby -class CreateComments < ActiveRecord::Migration[5.0] +class CreateComments < ActiveRecord::Migration[6.0] def change create_table :comments do |t| t.string :commenter diff --git a/railties/lib/rails/application/finisher.rb b/railties/lib/rails/application/finisher.rb index 39e8ef6631..8d2c13d2a8 100644 --- a/railties/lib/rails/application/finisher.rb +++ b/railties/lib/rails/application/finisher.rb @@ -73,6 +73,9 @@ module Rails initializer :eager_load! do if config.eager_load ActiveSupport.run_load_hooks(:before_eager_load, self) + # Checks defined?(Zeitwerk) instead of zeitwerk_enabled? because we + # want to eager load any dependency managed by Zeitwerk regardless of + # the autoloading mode of the application. Zeitwerk::Loader.eager_load_all if defined?(Zeitwerk) config.eager_load_namespaces.each(&:eager_load!) end diff --git a/railties/lib/rails/autoloaders.rb b/railties/lib/rails/autoloaders.rb index b03499cf81..a6974cc207 100644 --- a/railties/lib/rails/autoloaders.rb +++ b/railties/lib/rails/autoloaders.rb @@ -24,6 +24,10 @@ module Rails end end + def logger=(logger) + each { |loader| loader.logger = logger } + end + def zeitwerk_enabled? Rails.configuration.autoloader == :zeitwerk end diff --git a/railties/lib/rails/generators/rails/app/templates/Gemfile.tt b/railties/lib/rails/generators/rails/app/templates/Gemfile.tt index 783254b54d..18de6948f0 100644 --- a/railties/lib/rails/generators/rails/app/templates/Gemfile.tt +++ b/railties/lib/rails/generators/rails/app/templates/Gemfile.tt @@ -28,7 +28,7 @@ ruby <%= "'#{RUBY_VERSION}'" -%> <% if depend_on_bootsnap? -%> # Reduces boot times through caching; required in config/boot.rb -gem 'bootsnap', '>= 1.4.0', require: false +gem 'bootsnap', '>= 1.4.1', require: false <%- end -%> <%- if options.api? -%> diff --git a/railties/test/application/loading_test.rb b/railties/test/application/loading_test.rb index bfa66770bd..9c98489590 100644 --- a/railties/test/application/loading_test.rb +++ b/railties/test/application/loading_test.rb @@ -34,6 +34,22 @@ class LoadingTest < ActiveSupport::TestCase assert_equal "omg", p.title end + test "constants without a matching file raise NameError" do + app_file "app/models/post.rb", <<-RUBY + class Post + NON_EXISTING_CONSTANT + end + RUBY + + boot_app + + e = assert_raise(NameError) { User } + assert_equal "uninitialized constant #{self.class}::User", e.message + + e = assert_raise(NameError) { Post } + assert_equal "uninitialized constant Post::NON_EXISTING_CONSTANT", e.message + end + test "concerns in app are autoloaded" do app_file "app/controllers/concerns/trackable.rb", <<-CONCERN module Trackable diff --git a/railties/test/application/rake/dbs_test.rb b/railties/test/application/rake/dbs_test.rb index 5879949b61..ba5704c53e 100644 --- a/railties/test/application/rake/dbs_test.rb +++ b/railties/test/application/rake/dbs_test.rb @@ -179,9 +179,10 @@ module ApplicationTests def db_fixtures_load(expected_database) Dir.chdir(app_path) do rails "generate", "model", "book", "title:string" + reload rails "db:migrate", "db:fixtures:load" + assert_match expected_database, ActiveRecord::Base.connection_config[:database] - require "#{app_path}/app/models/book" assert_equal 2, Book.count end end @@ -201,8 +202,9 @@ module ApplicationTests require "#{app_path}/config/environment" rails "generate", "model", "admin::book", "title:string" + reload rails "db:migrate", "db:fixtures:load" - require "#{app_path}/app/models/admin/book" + assert_equal 2, Admin::Book.count end diff --git a/railties/test/application/rake/multi_dbs_test.rb b/railties/test/application/rake/multi_dbs_test.rb index ef99365e75..d676e7486e 100644 --- a/railties/test/application/rake/multi_dbs_test.rb +++ b/railties/test/application/rake/multi_dbs_test.rb @@ -170,6 +170,7 @@ module ApplicationTests rails "generate", "model", "book", "title:string" rails "generate", "model", "dog", "name:string" write_models_for_animals + reload end test "db:create and db:drop works on all databases for env" do diff --git a/railties/test/application/zeitwerk_integration_test.rb b/railties/test/application/zeitwerk_integration_test.rb index c536c2f7f4..cddbf5a22d 100644 --- a/railties/test/application/zeitwerk_integration_test.rb +++ b/railties/test/application/zeitwerk_integration_test.rb @@ -47,6 +47,31 @@ class ZeitwerkIntegrationTest < ActiveSupport::TestCase assert_equal 0, Rails.autoloaders.count end + test "autoloaders inflect with Active Support" do + app_file "config/initializers/inflections.rb", <<-RUBY + ActiveSupport::Inflector.inflections(:en) do |inflect| + inflect.acronym 'RESTful' + end + RUBY + + app_file "app/controllers/restful_controller.rb", <<-RUBY + class RESTfulController < ApplicationController + end + RUBY + + boot + + basename = "restful_controller" + abspath = "#{Rails.root}/app/controllers/#{basename}.rb" + camelized = "RESTfulController" + + Rails.autoloaders.each do |autoloader| + assert_equal camelized, autoloader.inflector.camelize(basename, abspath) + end + + assert RESTfulController + end + test "constantize returns the value stored in the constant" do app_file "app/models/admin/user.rb", "class Admin::User; end" boot @@ -164,7 +189,7 @@ class ZeitwerkIntegrationTest < ActiveSupport::TestCase assert_equal %i(main_autoloader), $zeitwerk_integration_reload_test end - test "verbose = true sets the debug method of the dependencies logger if present" do + test "verbose = true sets the dependencies logger if present" do boot logger = Logger.new(File::NULL) @@ -172,17 +197,17 @@ class ZeitwerkIntegrationTest < ActiveSupport::TestCase ActiveSupport::Dependencies.verbose = true Rails.autoloaders.each do |autoloader| - assert_equal logger.method(:debug), autoloader.logger + assert_same logger, autoloader.logger end end - test "verbose = true sets the debug method of the Rails logger as fallback" do + test "verbose = true sets the Rails logger as fallback" do boot ActiveSupport::Dependencies.verbose = true Rails.autoloaders.each do |autoloader| - assert_equal Rails.logger.method(:debug), autoloader.logger + assert_same Rails.logger, autoloader.logger end end @@ -199,4 +224,34 @@ class ZeitwerkIntegrationTest < ActiveSupport::TestCase assert_nil autoloader.logger end end + + test "unhooks" do + boot + + assert_equal Module, Module.method(:const_missing).owner + assert_equal :no_op, deps.unhook! + end + + test "autoloaders.logger=" do + boot + + logger = ->(_msg) { } + Rails.autoloaders.logger = logger + + Rails.autoloaders.each do |autoloader| + assert_same logger, autoloader.logger + end + + Rails.autoloaders.logger = Rails.logger + + Rails.autoloaders.each do |autoloader| + assert_same Rails.logger, autoloader.logger + end + + Rails.autoloaders.logger = nil + + Rails.autoloaders.each do |autoloader| + assert_nil autoloader.logger + end + end end diff --git a/railties/test/isolation/abstract_unit.rb b/railties/test/isolation/abstract_unit.rb index 4442cdf4bf..3f1638a516 100644 --- a/railties/test/isolation/abstract_unit.rb +++ b/railties/test/isolation/abstract_unit.rb @@ -458,12 +458,19 @@ module TestHelpers end end end + + module Reload + def reload + ActiveSupport::Dependencies.clear + end + end end class ActiveSupport::TestCase include TestHelpers::Paths include TestHelpers::Rack include TestHelpers::Generation + include TestHelpers::Reload include ActiveSupport::Testing::Stream include ActiveSupport::Testing::MethodCallAssertions end |