aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Gemfile.lock4
-rw-r--r--actionmailbox/lib/action_mailbox/engine.rb10
-rw-r--r--actionmailer/lib/action_mailer/mail_delivery_job.rb2
-rw-r--r--actionmailer/test/caching_test.rb10
-rw-r--r--actionpack/lib/action_controller/metal/etag_with_template_digest.rb2
-rw-r--r--actionpack/lib/action_dispatch/http/upload.rb5
-rw-r--r--actionpack/lib/action_dispatch/testing/integration.rb2
-rw-r--r--actionpack/test/controller/caching_test.rb25
-rw-r--r--actionpack/test/dispatch/debug_exceptions_test.rb2
-rw-r--r--actionpack/test/dispatch/uploaded_file_test.rb105
-rw-r--r--actiontext/app/helpers/action_text/tag_helper.rb4
-rw-r--r--actiontext/test/template/form_helper_test.rb50
-rw-r--r--actiontext/test/test_helper.rb3
-rw-r--r--actionview/app/assets/javascripts/rails-ujs/utils/event.coffee2
-rw-r--r--actionview/lib/action_view/base.rb14
-rw-r--r--actionview/lib/action_view/digestor.rb12
-rw-r--r--actionview/lib/action_view/helpers/cache_helper.rb10
-rw-r--r--actionview/lib/action_view/helpers/rendering_helper.rb2
-rw-r--r--actionview/lib/action_view/layouts.rb10
-rw-r--r--actionview/lib/action_view/lookup_context.rb3
-rw-r--r--actionview/lib/action_view/renderer/abstract_renderer.rb55
-rw-r--r--actionview/lib/action_view/renderer/partial_renderer.rb25
-rw-r--r--actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb12
-rw-r--r--actionview/lib/action_view/renderer/renderer.rb20
-rw-r--r--actionview/lib/action_view/renderer/streaming_template_renderer.rb2
-rw-r--r--actionview/lib/action_view/renderer/template_renderer.rb18
-rw-r--r--actionview/lib/action_view/rendering.rb21
-rw-r--r--actionview/lib/action_view/template.rb20
-rw-r--r--actionview/lib/action_view/template/handlers/erb/erubi.rb2
-rw-r--r--actionview/lib/action_view/template/inline.rb22
-rw-r--r--actionview/lib/action_view/template/resolver.rb18
-rw-r--r--actionview/lib/action_view/testing/resolvers.rb4
-rw-r--r--actionview/test/abstract_unit.rb2
-rw-r--r--actionview/test/actionpack/controller/render_test.rb11
-rw-r--r--actionview/test/activerecord/relation_cache_test.rb3
-rw-r--r--actionview/test/template/digestor_test.rb25
-rw-r--r--actionview/test/template/form_helper/form_with_test.rb17
-rw-r--r--actionview/test/template/form_helper_test.rb72
-rw-r--r--actionview/test/template/lookup_context_test.rb10
-rw-r--r--actionview/test/template/render_test.rb20
-rw-r--r--actionview/test/template/template_test.rb6
-rw-r--r--activejob/lib/active_job/core.rb2
-rw-r--r--activemodel/lib/active_model/type/helpers/numeric.rb7
-rw-r--r--activemodel/lib/active_model/type/integer.rb15
-rw-r--r--activemodel/lib/active_model/type/time.rb4
-rw-r--r--activemodel/test/cases/type/integer_test.rb10
-rw-r--r--activemodel/test/cases/validations/conditional_validation_test.rb6
-rw-r--r--activerecord/CHANGELOG.md44
-rw-r--r--activerecord/lib/active_record/associations/preloader/association.rb2
-rw-r--r--activerecord/lib/active_record/associations/preloader/through_association.rb29
-rw-r--r--activerecord/lib/active_record/callbacks.rb6
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb26
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb5
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb6
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb4
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_adapter.rb6
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb55
-rw-r--r--activerecord/lib/active_record/connection_adapters/determine_if_preparable_visitor.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb10
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb58
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb9
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb4
-rw-r--r--activerecord/lib/active_record/migration.rb2
-rw-r--r--activerecord/lib/active_record/persistence.rb6
-rw-r--r--activerecord/lib/active_record/querying.rb2
-rw-r--r--activerecord/lib/active_record/relation.rb51
-rw-r--r--activerecord/lib/active_record/relation/query_attribute.rb6
-rw-r--r--activerecord/lib/active_record/relation/query_methods.rb10
-rw-r--r--activerecord/lib/active_record/timestamp.rb12
-rw-r--r--activerecord/lib/active_record/validations/uniqueness.rb4
-rw-r--r--activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb8
-rw-r--r--activerecord/test/cases/adapters/postgresql/hstore_test.rb9
-rw-r--r--activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb10
-rw-r--r--activerecord/test/cases/adapters/postgresql/range_test.rb16
-rw-r--r--activerecord/test/cases/adapters/postgresql/uuid_test.rb20
-rw-r--r--activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb10
-rw-r--r--activerecord/test/cases/associations/has_many_associations_test.rb8
-rw-r--r--activerecord/test/cases/associations/nested_through_associations_test.rb6
-rw-r--r--activerecord/test/cases/attribute_methods_test.rb8
-rw-r--r--activerecord/test/cases/base_test.rb16
-rw-r--r--activerecord/test/cases/bind_parameter_test.rb57
-rw-r--r--activerecord/test/cases/calculations_test.rb40
-rw-r--r--activerecord/test/cases/dirty_test.rb2
-rw-r--r--activerecord/test/cases/finder_test.rb6
-rw-r--r--activerecord/test/cases/forbidden_attributes_protection_test.rb38
-rw-r--r--activerecord/test/cases/migration/column_attributes_test.rb6
-rw-r--r--activerecord/test/cases/migration_test.rb34
-rw-r--r--activerecord/test/cases/relation/delegation_test.rb2
-rw-r--r--activerecord/test/cases/relation/update_all_test.rb71
-rw-r--r--activerecord/test/cases/relation/where_test.rb31
-rw-r--r--activerecord/test/cases/relations_test.rb79
-rw-r--r--activerecord/test/models/category.rb1
-rw-r--r--activerecord/test/models/topic.rb2
-rw-r--r--activerecord/test/schema/schema.rb15
-rw-r--r--activerecord/test/support/stubs/strong_parameters.rb21
-rw-r--r--activestorage/lib/active_storage.rb1
-rw-r--r--activesupport/activesupport.gemspec2
-rw-r--r--activesupport/lib/active_support/dependencies/zeitwerk_integration.rb23
-rw-r--r--guides/source/active_record_basics.md6
-rw-r--r--guides/source/active_record_callbacks.md21
-rw-r--r--guides/source/active_record_querying.md2
-rw-r--r--guides/source/active_record_validations.md2
-rw-r--r--guides/source/getting_started.md4
-rw-r--r--railties/lib/rails/application/finisher.rb3
-rw-r--r--railties/lib/rails/autoloaders.rb4
-rw-r--r--railties/lib/rails/generators/rails/app/templates/Gemfile.tt2
-rw-r--r--railties/test/application/loading_test.rb16
-rw-r--r--railties/test/application/rake/dbs_test.rb6
-rw-r--r--railties/test/application/rake/multi_dbs_test.rb1
-rw-r--r--railties/test/application/zeitwerk_integration_test.rb63
-rw-r--r--railties/test/isolation/abstract_unit.rb7
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