aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--actionmailer/lib/action_mailer/base.rb30
-rw-r--r--actionpack/CHANGELOG.md12
-rw-r--r--actionpack/lib/abstract_controller/callbacks.rb1
-rw-r--r--actionpack/lib/action_dispatch/http/response.rb8
-rw-r--r--actionpack/lib/action_dispatch/testing/integration.rb2
-rw-r--r--actionpack/lib/action_dispatch/testing/test_response.rb13
-rw-r--r--actionpack/test/abstract_unit.rb31
-rw-r--r--actionpack/test/controller/caching_test.rb58
-rw-r--r--actionpack/test/controller/filters_test.rb25
-rw-r--r--actionpack/test/controller/integration_test.rb34
-rw-r--r--actionpack/test/controller/new_base/metal_test.rb6
-rw-r--r--actionpack/test/controller/new_base/middleware_test.rb2
-rw-r--r--actionpack/test/controller/test_case_test.rb46
-rw-r--r--actionpack/test/dispatch/routing_test.rb12
-rw-r--r--actionpack/test/fixtures/collection_cache/index.html.erb1
-rw-r--r--actionpack/test/fixtures/customers/_commented_customer.html.erb4
-rw-r--r--actionpack/test/fixtures/customers/_customer.html.erb3
-rw-r--r--actionpack/test/fixtures/symlink_parent/symlinked_layout.erb5
-rw-r--r--actionview/CHANGELOG.md25
-rw-r--r--actionview/lib/action_view/dependency_tracker.rb19
-rw-r--r--actionview/lib/action_view/helpers/cache_helper.rb31
-rw-r--r--actionview/lib/action_view/helpers/form_helper.rb3
-rw-r--r--actionview/lib/action_view/railtie.rb6
-rw-r--r--actionview/lib/action_view/renderer/partial_renderer.rb8
-rw-r--r--actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb70
-rw-r--r--actionview/lib/action_view/template.rb14
-rw-r--r--actionview/lib/action_view/template/handlers/erb.rb18
-rw-r--r--actionview/test/actionpack/controller/render_test.rb4
-rw-r--r--actionview/test/fixtures/test/_cached_customer.erb3
-rw-r--r--actionview/test/fixtures/test/_cached_customer_as.erb3
-rw-r--r--actionview/test/template/dependency_tracker_test.rb1
-rw-r--r--actionview/test/template/render_test.rb38
-rw-r--r--activejob/lib/active_job/queue_adapter.rb2
-rw-r--r--activejob/lib/active_job/queue_adapters/test_adapter.rb52
-rw-r--r--activejob/lib/active_job/test_helper.rb6
-rw-r--r--activejob/test/cases/test_case_test.rb2
-rw-r--r--activemodel/lib/active_model/naming.rb2
-rw-r--r--activemodel/test/cases/serializers/json_serialization_test.rb4
-rw-r--r--activerecord/CHANGELOG.md38
-rw-r--r--activerecord/lib/active_record/associations.rb4
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb70
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/transaction.rb8
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_adapter.rb1
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb39
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb9
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb37
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb160
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb3
-rw-r--r--activerecord/lib/active_record/core.rb2
-rw-r--r--activerecord/lib/active_record/fixtures.rb4
-rw-r--r--activerecord/lib/active_record/locking/optimistic.rb2
-rw-r--r--activerecord/lib/active_record/migration.rb2
-rw-r--r--activerecord/lib/active_record/railtie.rb3
-rw-r--r--activerecord/lib/active_record/relation/query_methods.rb14
-rw-r--r--activerecord/lib/active_record/transactions.rb39
-rw-r--r--activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb4
-rw-r--r--activerecord/test/cases/adapters/postgresql/referential_integrity_test.rb89
-rw-r--r--activerecord/test/cases/associations/has_many_associations_test.rb25
-rw-r--r--activerecord/test/cases/fixtures_test.rb20
-rw-r--r--activerecord/test/cases/migration/change_table_test.rb35
-rw-r--r--activerecord/test/cases/primary_keys_test.rb10
-rw-r--r--activerecord/test/cases/relations_test.rb11
-rw-r--r--activerecord/test/cases/schema_dumper_test.rb5
-rw-r--r--activerecord/test/cases/transaction_callbacks_test.rb60
-rw-r--r--activerecord/test/schema/postgresql_specific_schema.rb10
-rw-r--r--activerecord/test/schema/schema.rb1
-rw-r--r--activesupport/CHANGELOG.md5
-rw-r--r--activesupport/lib/active_support/cache.rb40
-rw-r--r--activesupport/lib/active_support/cache/mem_cache_store.rb17
-rw-r--r--activesupport/lib/active_support/core_ext/string/filters.rb2
-rw-r--r--activesupport/test/caching_test.rb9
-rw-r--r--activesupport/test/core_ext/string_ext_test.rb9
-rw-r--r--guides/rails_guides/levenshtein.rb2
-rw-r--r--guides/source/active_support_core_extensions.md2
-rw-r--r--guides/source/association_basics.md3
-rw-r--r--guides/source/command_line.md6
-rw-r--r--guides/source/layouts_and_rendering.md36
-rw-r--r--railties/CHANGELOG.md7
-rw-r--r--railties/lib/rails/paths.rb2
80 files changed, 1131 insertions, 320 deletions
diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb
index 7eae76f93b..3e5125f72e 100644
--- a/actionmailer/lib/action_mailer/base.rb
+++ b/actionmailer/lib/action_mailer/base.rb
@@ -25,7 +25,7 @@ module ActionMailer
# layout 'mailer'
# end
#
- # class Notifier < ApplicationMailer
+ # class NotifierMailer < ApplicationMailer
# default from: 'no-reply@example.com',
# return_path: 'system@example.com'
#
@@ -88,7 +88,7 @@ module ActionMailer
#
# To define a template to be used with a mailing, create an <tt>.erb</tt> file with the same
# name as the method in your mailer model. For example, in the mailer defined above, the template at
- # <tt>app/views/notifier/welcome.text.erb</tt> would be used to generate the email.
+ # <tt>app/views/notifier_mailer/welcome.text.erb</tt> would be used to generate the email.
#
# Variables defined in the methods of your mailer model are accessible as instance variables in their
# corresponding view.
@@ -137,20 +137,20 @@ module ActionMailer
# Once a mailer action and template are defined, you can deliver your message or create it and save it
# for delivery later:
#
- # Notifier.welcome(User.first).deliver_now # sends the email
- # mail = Notifier.welcome(User.first) # => an ActionMailer::MessageDelivery object
+ # NotifierMailer.welcome(User.first).deliver_now # sends the email
+ # mail = NotifierMailer.welcome(User.first) # => an ActionMailer::MessageDelivery object
# mail.deliver_now # sends the email
#
# The <tt>ActionMailer::MessageDelivery</tt> class is a wrapper around a <tt>Mail::Message</tt> object. If
# you want direct access to the <tt>Mail::Message</tt> object you can call the <tt>message</tt> method on
# the <tt>ActionMailer::MessageDelivery</tt> object.
#
- # Notifier.welcome(User.first).message # => a Mail::Message object
+ # NotifierMailer.welcome(User.first).message # => a Mail::Message object
#
# Action Mailer is nicely integrated with Active Job so you can send emails in the background (example: outside
# of the request-response cycle, so the user doesn't have to wait on it):
#
- # Notifier.welcome(User.first).deliver_later # enqueue the email sending to Active Job
+ # NotifierMailer.welcome(User.first).deliver_later # enqueue the email sending to Active Job
#
# You never instantiate your mailer class. Rather, you just call the method you defined on the class itself.
#
@@ -179,7 +179,7 @@ module ActionMailer
#
# Sending attachment in emails is easy:
#
- # class Notifier < ApplicationMailer
+ # class NotifierMailer < ApplicationMailer
# def welcome(recipient)
# attachments['free_book.pdf'] = File.read('path/to/file.pdf')
# mail(to: recipient, subject: "New account information")
@@ -195,7 +195,7 @@ module ActionMailer
# If you need to send attachments with no content, you need to create an empty view for it,
# or add an empty body parameter like this:
#
- # class Notifier < ApplicationMailer
+ # class NotifierMailer < ApplicationMailer
# def welcome(recipient)
# attachments['free_book.pdf'] = File.read('path/to/file.pdf')
# mail(to: recipient, subject: "New account information", body: "")
@@ -207,7 +207,7 @@ module ActionMailer
# You can also specify that a file should be displayed inline with other HTML. This is useful
# if you want to display a corporate logo or a photo.
#
- # class Notifier < ApplicationMailer
+ # class NotifierMailer < ApplicationMailer
# def welcome(recipient)
# attachments.inline['photo.png'] = File.read('path/to/photo.png')
# mail(to: recipient, subject: "Here is what we look like")
@@ -246,7 +246,7 @@ module ActionMailer
# Action Mailer provides some intelligent defaults for your emails, these are usually specified in a
# default method inside the class definition:
#
- # class Notifier < ApplicationMailer
+ # class NotifierMailer < ApplicationMailer
# default sender: 'system@example.com'
# end
#
@@ -264,7 +264,7 @@ module ActionMailer
# As you can pass in any header, you need to either quote the header as a string, or pass it in as
# an underscored symbol, so the following will work:
#
- # class Notifier < ApplicationMailer
+ # class NotifierMailer < ApplicationMailer
# default 'Content-Transfer-Encoding' => '7bit',
# content_description: 'This is a description'
# end
@@ -272,7 +272,7 @@ module ActionMailer
# Finally, Action Mailer also supports passing <tt>Proc</tt> objects into the default hash, so you
# can define methods that evaluate as the message is being generated:
#
- # class Notifier < ApplicationMailer
+ # class NotifierMailer < ApplicationMailer
# default 'X-Special-Header' => Proc.new { my_method }
#
# private
@@ -297,7 +297,7 @@ module ActionMailer
# This may be useful, for example, when you want to add default inline attachments for all
# messages sent out by a certain mailer class:
#
- # class Notifier < ApplicationMailer
+ # class NotifierMailer < ApplicationMailer
# before_action :add_inline_attachment!
#
# def welcome
@@ -325,9 +325,9 @@ module ActionMailer
# <tt>ActionMailer::Base.preview_path</tt>. Since most emails do something interesting
# with database data, you'll need to write some scenarios to load messages with fake data:
#
- # class NotifierPreview < ActionMailer::Preview
+ # class NotifierMailerPreview < ActionMailer::Preview
# def welcome
- # Notifier.welcome(User.first)
+ # NotifierMailer.welcome(User.first)
# end
# end
#
diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md
index 7659c35eae..0134bf7cab 100644
--- a/actionpack/CHANGELOG.md
+++ b/actionpack/CHANGELOG.md
@@ -1,3 +1,15 @@
+* Fix regression in functional tests. Responses should have default headers
+ assigned.
+
+ See #18423.
+
+ *Jeremy Kemper*, *Yves Senn*
+
+* Deprecate AbstractController#skip_action_callback in favor of individual skip_callback methods
+ (which can be made to raise an error if no callback was removed).
+
+ *Iain Beeston*
+
* Alias the `ActionDispatch::Request#uuid` method to `ActionDispatch::Request#request_id`.
Due to implementation, `config.log_tags = [:request_id]` also works in substitute
for `config.log_tags = [:uuid]`.
diff --git a/actionpack/lib/abstract_controller/callbacks.rb b/actionpack/lib/abstract_controller/callbacks.rb
index 69f490b327..f4fd1db36c 100644
--- a/actionpack/lib/abstract_controller/callbacks.rb
+++ b/actionpack/lib/abstract_controller/callbacks.rb
@@ -63,6 +63,7 @@ module AbstractController
# impossible to skip a callback defined using an anonymous proc
# using #skip_action_callback
def skip_action_callback(*names)
+ ActiveSupport::Deprecation.warn('`skip_action_callback` is deprecated and will be removed in the next major version of Rails. Please use skip_before_action, skip_after_action or skip_around_action instead.')
skip_before_action(*names)
skip_after_action(*names)
skip_around_action(*names)
diff --git a/actionpack/lib/action_dispatch/http/response.rb b/actionpack/lib/action_dispatch/http/response.rb
index 4061ea71a3..a895d1ab18 100644
--- a/actionpack/lib/action_dispatch/http/response.rb
+++ b/actionpack/lib/action_dispatch/http/response.rb
@@ -113,10 +113,10 @@ module ActionDispatch # :nodoc:
# The underlying body, as a streamable object.
attr_reader :stream
- def initialize(status = 200, header = {}, body = [])
+ def initialize(status = 200, header = {}, body = [], default_headers: self.class.default_headers)
super()
- header = merge_default_headers(header, self.class.default_headers)
+ header = merge_default_headers(header, default_headers)
self.body, self.header, self.status = body, header, status
@@ -308,9 +308,7 @@ module ActionDispatch # :nodoc:
end
def merge_default_headers(original, default)
- return original unless default.respond_to?(:merge)
-
- default.merge(original)
+ default.respond_to?(:merge) ? default.merge(original) : original
end
def build_buffer(response, body)
diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb
index 9b00616968..2fe37c5bd4 100644
--- a/actionpack/lib/action_dispatch/testing/integration.rb
+++ b/actionpack/lib/action_dispatch/testing/integration.rb
@@ -369,7 +369,7 @@ module ActionDispatch
@request_count += 1
@request = ActionDispatch::Request.new(session.last_request.env)
response = _mock_session.last_response
- @response = ActionDispatch::TestResponse.new(response.status, response.headers, response.body)
+ @response = ActionDispatch::TestResponse.from_response(response)
@html_document = nil
@url_options = nil
diff --git a/actionpack/lib/action_dispatch/testing/test_response.rb b/actionpack/lib/action_dispatch/testing/test_response.rb
index 369ea1467c..a9b88ac5fd 100644
--- a/actionpack/lib/action_dispatch/testing/test_response.rb
+++ b/actionpack/lib/action_dispatch/testing/test_response.rb
@@ -7,11 +7,7 @@ module ActionDispatch
# See Response for more information on controller response objects.
class TestResponse < Response
def self.from_response(response)
- new.tap do |resp|
- resp.status = response.status
- resp.headers = response.headers
- resp.body = response.body
- end
+ new response.status, response.headers, response.body, default_headers: nil
end
# Was the response successful?
@@ -25,12 +21,5 @@ module ActionDispatch
# Was there a server-side error?
alias_method :error?, :server_error?
-
- def merge_default_headers(original, *args)
- # Default headers are already applied, no need to merge them a second time.
- # This makes sure that default headers, removed in controller actions, will
- # not be reapplied to the test response.
- original
- end
end
end
diff --git a/actionpack/test/abstract_unit.rb b/actionpack/test/abstract_unit.rb
index 2d69ceed3a..918589f916 100644
--- a/actionpack/test/abstract_unit.rb
+++ b/actionpack/test/abstract_unit.rb
@@ -54,23 +54,8 @@ I18n.enforce_available_locales = false
# Register danish language for testing
I18n.backend.store_translations 'da', {}
I18n.backend.store_translations 'pt-BR', {}
-ORIGINAL_LOCALES = I18n.available_locales.map(&:to_s).sort
FIXTURE_LOAD_PATH = File.join(File.dirname(__FILE__), 'fixtures')
-FIXTURES = Pathname.new(FIXTURE_LOAD_PATH)
-
-module RackTestUtils
- def body_to_string(body)
- if body.respond_to?(:each)
- str = ""
- body.each {|s| str << s }
- str
- else
- body
- end
- end
- extend self
-end
SharedTestRoutes = ActionDispatch::Routing::RouteSet.new
@@ -129,22 +114,6 @@ class RoutedRackApp
end
end
-class BasicController
- attr_accessor :request
-
- def config
- @config ||= ActiveSupport::InheritableOptions.new(ActionController::Base.config).tap do |config|
- # VIEW TODO: View tests should not require a controller
- public_dir = File.expand_path("../fixtures/public", __FILE__)
- config.assets_dir = public_dir
- config.javascripts_dir = "#{public_dir}/javascripts"
- config.stylesheets_dir = "#{public_dir}/stylesheets"
- config.assets = ActiveSupport::InheritableOptions.new({ :prefix => "assets" })
- config
- end
- end
-end
-
class ActionDispatch::IntegrationTest < ActiveSupport::TestCase
include ActionDispatch::SharedRoutes
diff --git a/actionpack/test/controller/caching_test.rb b/actionpack/test/controller/caching_test.rb
index 4760ec1698..2d6607041d 100644
--- a/actionpack/test/controller/caching_test.rb
+++ b/actionpack/test/controller/caching_test.rb
@@ -1,5 +1,6 @@
require 'fileutils'
require 'abstract_unit'
+require 'lib/controller/fake_models'
CACHE_DIR = 'test_cache'
# Don't change '/../temp/' cavalierly or you might hose something you don't want hosed
@@ -349,3 +350,60 @@ class ViewCacheDependencyTest < ActionController::TestCase
assert_equal %w(trombone flute), HasDependenciesController.new.view_cache_dependencies
end
end
+
+class CollectionCacheController < ActionController::Base
+ def index
+ @customers = [Customer.new('david', params[:id] || 1)]
+ end
+
+ def index_ordered
+ @customers = [Customer.new('david', 1), Customer.new('david', 2), Customer.new('david', 3)]
+ render 'index'
+ end
+
+ def index_explicit_render
+ @customers = [Customer.new('david', 1)]
+ render partial: 'customers/customer', collection: @customers
+ end
+
+ def index_with_comment
+ @customers = [Customer.new('david', 1)]
+ render partial: 'customers/commented_customer', collection: @customers, as: :customer
+ end
+end
+
+class AutomaticCollectionCacheTest < ActionController::TestCase
+ def setup
+ super
+ @controller = CollectionCacheController.new
+ @controller.perform_caching = true
+ @controller.cache_store = ActiveSupport::Cache::MemoryStore.new
+ end
+
+ def test_collection_fetches_cached_views
+ get :index
+
+ ActionView::PartialRenderer.expects(:collection_with_template).never
+ get :index
+ end
+
+ def test_preserves_order_when_reading_from_cache_plus_rendering
+ get :index, params: { id: 2 }
+ get :index_ordered
+
+ assert_select ':root', "david, 1\n david, 2\n david, 3"
+ end
+
+ def test_explicit_render_call_with_options
+ get :index_explicit_render
+
+ assert_select ':root', "david, 1"
+ end
+
+ def test_caching_works_with_beginning_comment
+ get :index_with_comment
+
+ ActionView::PartialRenderer.expects(:collection_with_template).never
+ get :index_with_comment
+ end
+end
diff --git a/actionpack/test/controller/filters_test.rb b/actionpack/test/controller/filters_test.rb
index b9fb6be4e3..26b94f0db8 100644
--- a/actionpack/test/controller/filters_test.rb
+++ b/actionpack/test/controller/filters_test.rb
@@ -26,7 +26,6 @@ class ActionController::Base
end
class FilterTest < ActionController::TestCase
-
class TestController < ActionController::Base
before_action :ensure_login
after_action :clean_up
@@ -967,8 +966,15 @@ class ControllerWithAllTypesOfFilters < PostsController
end
class ControllerWithTwoLessFilters < ControllerWithAllTypesOfFilters
- skip_action_callback :around_again
- skip_action_callback :after
+ skip_around_action :around_again
+ skip_after_action :after
+end
+
+class SkipFilterUsingSkipActionCallback < ControllerWithAllTypesOfFilters
+ ActiveSupport::Deprecation.silence do
+ skip_action_callback :around_again
+ skip_action_callback :after
+ end
end
class YieldingAroundFiltersTest < ActionController::TestCase
@@ -1055,6 +1061,19 @@ class YieldingAroundFiltersTest < ActionController::TestCase
assert_equal 3, controller.instance_variable_get(:@try)
end
+ def test_skipping_with_skip_action_callback
+ test_process(SkipFilterUsingSkipActionCallback,'no_raise')
+ assert_equal 'before around (before yield) around (after yield)', assigns['ran_filter'].join(' ')
+ end
+
+ def test_deprecated_skip_action_callback
+ assert_deprecated do
+ Class.new(PostsController) do
+ skip_action_callback :clean_up
+ end
+ end
+ end
+
protected
def test_process(controller, action = "show")
@controller = controller.is_a?(Class) ? controller.new : controller
diff --git a/actionpack/test/controller/integration_test.rb b/actionpack/test/controller/integration_test.rb
index 9ab1549e8e..438c044da2 100644
--- a/actionpack/test/controller/integration_test.rb
+++ b/actionpack/test/controller/integration_test.rb
@@ -397,9 +397,9 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest
redirect_to action_url('get')
end
- def remove_default_header
- response.headers.except! 'X-Frame-Options'
- head :ok
+ def remove_header
+ response.headers.delete params[:header]
+ head :ok, 'c' => '3'
end
end
@@ -640,25 +640,27 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest
end
end
- def test_removed_default_headers_on_test_response_are_not_reapplied
+ def test_respect_removal_of_default_headers_by_a_controller_action
with_test_route_set do
- begin
- header_to_remove = 'X-Frame-Options'
- original_default_headers = ActionDispatch::Response.default_headers
- ActionDispatch::Response.default_headers = {
- 'X-Content-Type-Options' => 'nosniff',
- header_to_remove => 'SAMEORIGIN',
- }
- get '/remove_default_header'
- assert_includes headers, 'X-Content-Type-Options'
- assert_not_includes headers, header_to_remove, "Should not contain removed default header"
- ensure
- ActionDispatch::Response.default_headers = original_default_headers
+ with_default_headers 'a' => '1', 'b' => '2' do
+ get '/remove_header', params: { header: 'a' }
end
end
+
+ assert_not_includes @response.headers, 'a', 'Response should not include default header removed by the controller action'
+ assert_includes @response.headers, 'b'
+ assert_includes @response.headers, 'c'
end
private
+ def with_default_headers(headers)
+ original = ActionDispatch::Response.default_headers
+ ActionDispatch::Response.default_headers = headers
+ yield
+ ensure
+ ActionDispatch::Response.default_headers = original
+ end
+
def with_test_route_set
with_routing do |set|
controller = ::IntegrationProcessTest::IntegrationController.clone
diff --git a/actionpack/test/controller/new_base/metal_test.rb b/actionpack/test/controller/new_base/metal_test.rb
index 45a6619eb4..537b93387a 100644
--- a/actionpack/test/controller/new_base/metal_test.rb
+++ b/actionpack/test/controller/new_base/metal_test.rb
@@ -18,8 +18,6 @@ module MetalTest
end
class TestMiddleware < ActiveSupport::TestCase
- include RackTestUtils
-
def setup
@app = Rack::Builder.new do
use MetalTest::MetalMiddleware
@@ -31,14 +29,14 @@ module MetalTest
env = Rack::MockRequest.env_for("/authed")
response = @app.call(env)
- assert_equal "Hello World", body_to_string(response[2])
+ assert_equal ["Hello World"], response[2]
end
test "it can return a response using the normal AC::Metal techniques" do
env = Rack::MockRequest.env_for("/")
response = @app.call(env)
- assert_equal "Not authed!", body_to_string(response[2])
+ assert_equal ["Not authed!"], response[2]
assert_equal 401, response[0]
end
end
diff --git a/actionpack/test/controller/new_base/middleware_test.rb b/actionpack/test/controller/new_base/middleware_test.rb
index 6b7b5e10e3..a30e937bb3 100644
--- a/actionpack/test/controller/new_base/middleware_test.rb
+++ b/actionpack/test/controller/new_base/middleware_test.rb
@@ -75,7 +75,7 @@ module MiddlewareTest
test "middleware that is 'use'd is called as part of the Rack application" do
result = @app.call(env_for("/"))
- assert_equal "Hello World", RackTestUtils.body_to_string(result[2])
+ assert_equal ["Hello World"], result[2]
assert_equal "Success", result[1]["Middleware-Test"]
end
diff --git a/actionpack/test/controller/test_case_test.rb b/actionpack/test/controller/test_case_test.rb
index ca854040b7..e348749f78 100644
--- a/actionpack/test/controller/test_case_test.rb
+++ b/actionpack/test/controller/test_case_test.rb
@@ -965,6 +965,52 @@ XML
end
end
+class ResponseDefaultHeadersTest < ActionController::TestCase
+ class TestController < ActionController::Base
+ def remove_header
+ headers.delete params[:header]
+ head :ok, 'C' => '3'
+ end
+ end
+
+ setup do
+ @original = ActionDispatch::Response.default_headers
+ @defaults = { 'A' => '1', 'B' => '2' }
+ ActionDispatch::Response.default_headers = @defaults
+ end
+
+ teardown do
+ ActionDispatch::Response.default_headers = @original
+ end
+
+ def setup
+ super
+ @controller = TestController.new
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ @request.env['PATH_INFO'] = nil
+ @routes = ActionDispatch::Routing::RouteSet.new.tap do |r|
+ r.draw do
+ get ':controller(/:action(/:id))'
+ end
+ end
+ end
+
+ test "response contains default headers" do
+ # Response headers start out with the defaults
+ assert_equal @defaults, response.headers
+
+ get :remove_header, params: { header: 'A' }
+ assert_response :ok
+
+ # After a request, the response in the test case doesn't have the
+ # defaults merged on top again.
+ assert_not_includes response.headers, 'A'
+ assert_includes response.headers, 'B'
+ assert_includes response.headers, 'C'
+ end
+end
+
module EngineControllerTests
class Engine < ::Rails::Engine
isolate_namespace EngineControllerTests
diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb
index deb289bd57..d65d2e8e8f 100644
--- a/actionpack/test/dispatch/routing_test.rb
+++ b/actionpack/test/dispatch/routing_test.rb
@@ -3489,13 +3489,19 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
def test_head_fetch_with_mount_on_root
draw do
get '/home' => 'test#index'
- mount lambda { |env| [404, {"Content-Type" => "text/html"}, ["testing"]] }, at: '/'
+ mount lambda { |env| [200, {}, [env['REQUEST_METHOD']]] }, at: '/'
end
+
+ # HEAD request matches `get /home` rather than the lower-precedence
+ # Rack app mounted at `/`
head '/home'
assert_response :success
+ assert_equal 'test#index', @response.body
- head '/'
- assert_response :not_found
+ # But the Rack app can still respond to its own HEAD requests.
+ head '/foobar'
+ assert_response :ok
+ assert_equal 'HEAD', @response.body
end
private
diff --git a/actionpack/test/fixtures/collection_cache/index.html.erb b/actionpack/test/fixtures/collection_cache/index.html.erb
new file mode 100644
index 0000000000..521b1450df
--- /dev/null
+++ b/actionpack/test/fixtures/collection_cache/index.html.erb
@@ -0,0 +1 @@
+<%= render @customers %> \ No newline at end of file
diff --git a/actionpack/test/fixtures/customers/_commented_customer.html.erb b/actionpack/test/fixtures/customers/_commented_customer.html.erb
new file mode 100644
index 0000000000..d5f6e3b491
--- /dev/null
+++ b/actionpack/test/fixtures/customers/_commented_customer.html.erb
@@ -0,0 +1,4 @@
+<%# I'm a comment %>
+<% cache customer do %>
+ <%= customer.name %>, <%= customer.id %>
+<% end %> \ No newline at end of file
diff --git a/actionpack/test/fixtures/customers/_customer.html.erb b/actionpack/test/fixtures/customers/_customer.html.erb
new file mode 100644
index 0000000000..67e9f6d411
--- /dev/null
+++ b/actionpack/test/fixtures/customers/_customer.html.erb
@@ -0,0 +1,3 @@
+<% cache customer do %>
+ <%= customer.name %>, <%= customer.id %>
+<% end %> \ No newline at end of file
diff --git a/actionpack/test/fixtures/symlink_parent/symlinked_layout.erb b/actionpack/test/fixtures/symlink_parent/symlinked_layout.erb
deleted file mode 100644
index bda57d0fae..0000000000
--- a/actionpack/test/fixtures/symlink_parent/symlinked_layout.erb
+++ /dev/null
@@ -1,5 +0,0 @@
-This is my layout
-
-<%= yield %>
-
-End.
diff --git a/actionview/CHANGELOG.md b/actionview/CHANGELOG.md
index 5557285ef5..8f47171e62 100644
--- a/actionview/CHANGELOG.md
+++ b/actionview/CHANGELOG.md
@@ -1,3 +1,28 @@
+* Collection rendering automatically caches and fetches multiple partials.
+
+ Collections rendered as:
+
+ ```ruby
+ <%= render @notifications %>
+ <%= render partial: 'notifications/notification', collection: @notifications, as: :notification %>
+ ```
+
+ will now read several partials from cache at once, if the template starts with a cache call:
+
+ ```ruby
+ # notifications/_notification.html.erb
+ <% cache notification do %>
+ <%# ... %>
+ <% end %>
+ ```
+
+ *Kasper Timm Hansen*
+
+* Fixed a dependency tracker bug that caused template dependencies not
+ count layouts as dependencies for partials.
+
+ *Juho Leinonen*
+
* Extracted `ActionView::Helpers::RecordTagHelper` to external gem
(`record_tag_helper`) and added removal notices.
diff --git a/actionview/lib/action_view/dependency_tracker.rb b/actionview/lib/action_view/dependency_tracker.rb
index e34bdd4a46..7a7e116dbb 100644
--- a/actionview/lib/action_view/dependency_tracker.rb
+++ b/actionview/lib/action_view/dependency_tracker.rb
@@ -76,6 +76,12 @@ module ActionView
(?:#{STRING}|#{VARIABLE_OR_METHOD_CHAIN}) # finally, the dependency name of interest
/xm
+ LAYOUT_DEPENDENCY = /\A
+ (?:\s*\(?\s*) # optional opening paren surrounded by spaces
+ (?:.*?#{LAYOUT_HASH_KEY}) # check if the line has layout key declaration
+ (?:#{STRING}|#{VARIABLE_OR_METHOD_CHAIN}) # finally, the dependency name of interest
+ /xm
+
def self.call(name, template)
new(name, template).dependencies
end
@@ -106,15 +112,20 @@ module ActionView
render_calls = source.split(/\brender\b/).drop(1)
render_calls.each do |arguments|
- arguments.scan(RENDER_ARGUMENTS) do
- add_dynamic_dependency(render_dependencies, Regexp.last_match[:dynamic])
- add_static_dependency(render_dependencies, Regexp.last_match[:static])
- end
+ add_dependencies(render_dependencies, arguments, LAYOUT_DEPENDENCY)
+ add_dependencies(render_dependencies, arguments, RENDER_ARGUMENTS)
end
render_dependencies.uniq
end
+ def add_dependencies(render_dependencies, arguments, pattern)
+ arguments.scan(pattern) do
+ add_dynamic_dependency(render_dependencies, Regexp.last_match[:dynamic])
+ add_static_dependency(render_dependencies, Regexp.last_match[:static])
+ end
+ end
+
def add_dynamic_dependency(dependencies, dependency)
if dependency
dependencies << "#{dependency.pluralize}/#{dependency.singularize}"
diff --git a/actionview/lib/action_view/helpers/cache_helper.rb b/actionview/lib/action_view/helpers/cache_helper.rb
index 9dadfb5ce1..0e2a5f90f4 100644
--- a/actionview/lib/action_view/helpers/cache_helper.rb
+++ b/actionview/lib/action_view/helpers/cache_helper.rb
@@ -110,6 +110,29 @@ module ActionView
# <%= some_helper_method(person) %>
#
# Now all you'll have to do is change that timestamp when the helper method changes.
+ #
+ # === Automatic Collection Caching
+ #
+ # When rendering collections such as:
+ #
+ # <%= render @notifications %>
+ # <%= render partial: 'notifications/notification', collection: @notifications %>
+ #
+ # If the notifications/_notification partial starts with a cache call like so:
+ #
+ # <% cache notification do %>
+ # <%= notification.name %>
+ # <% end %>
+ #
+ # The collection can then automatically use any cached renders for that
+ # template by reading them at once instead of one by one.
+ #
+ # See ActionView::Template::Handlers::ERB.resource_cache_call_pattern for more
+ # information on what cache calls make a template eligible for this collection caching.
+ #
+ # The automatic cache multi read can be turned off like so:
+ #
+ # <%= render @notifications, cache: false %>
def cache(name = {}, options = nil, &block)
if controller.perform_caching
safe_concat(fragment_for(cache_fragment_name(name, options), options, &block))
@@ -161,6 +184,14 @@ module ActionView
end
end
+ # Given a key (as described in ActionController::Caching::Fragments.expire_fragment),
+ # returns a key suitable for use in reading, writing, or expiring a
+ # cached fragment. All keys are prefixed with <tt>views/</tt> and uses
+ # ActiveSupport::Cache.expand_cache_key for the expansion.
+ def fragment_cache_key(key)
+ ActiveSupport::Cache.expand_cache_key(key.is_a?(Hash) ? url_for(key).split("://").last : key, :views)
+ end
+
private
def fragment_name_with_digest(name) #:nodoc:
diff --git a/actionview/lib/action_view/helpers/form_helper.rb b/actionview/lib/action_view/helpers/form_helper.rb
index b76b35bf3c..b0793d0b91 100644
--- a/actionview/lib/action_view/helpers/form_helper.rb
+++ b/actionview/lib/action_view/helpers/form_helper.rb
@@ -1246,7 +1246,7 @@ module ActionView
# Admin: <%= person_form.check_box :admin %>
# <% end %>
#
- # In the above block, the a +FormBuilder+ object is yielded as the
+ # In the above block, a +FormBuilder+ object is yielded as the
# +person_form+ variable. This allows you to generate the +text_field+
# and +check_box+ fields by specifying their eponymous methods, which
# modify the underlying template and associates the +@person+ model object
@@ -1267,6 +1267,7 @@ module ActionView
# )
# )
# end
+ # end
#
# The above code creates a new method +div_radio_button+ which wraps a div
# around the new radio button. Note that when options are passed in, you
diff --git a/actionview/lib/action_view/railtie.rb b/actionview/lib/action_view/railtie.rb
index ee3dce24b7..9a26cba574 100644
--- a/actionview/lib/action_view/railtie.rb
+++ b/actionview/lib/action_view/railtie.rb
@@ -36,6 +36,12 @@ module ActionView
end
end
+ initializer "action_view.collection_caching" do |app|
+ ActiveSupport.on_load(:action_controller) do
+ PartialRenderer.collection_cache = app.config.action_controller.cache_store
+ end
+ end
+
initializer "action_view.setup_action_pack" do |app|
ActiveSupport.on_load(:action_controller) do
ActionView::RoutingUrlFor.include(ActionDispatch::Routing::UrlFor)
diff --git a/actionview/lib/action_view/renderer/partial_renderer.rb b/actionview/lib/action_view/renderer/partial_renderer.rb
index 5ff15411cf..56b8ab1e2d 100644
--- a/actionview/lib/action_view/renderer/partial_renderer.rb
+++ b/actionview/lib/action_view/renderer/partial_renderer.rb
@@ -1,3 +1,4 @@
+require 'action_view/renderer/partial_renderer/collection_caching'
require 'thread_safe'
module ActionView
@@ -280,6 +281,8 @@ module ActionView
# <%- end -%>
# <% end %>
class PartialRenderer < AbstractRenderer
+ include CollectionCaching
+
PREFIXED_PARTIAL_NAMES = ThreadSafe::Cache.new do |h, k|
h[k] = ThreadSafe::Cache.new
end
@@ -321,8 +324,9 @@ module ActionView
spacer = find_template(@options[:spacer_template], @locals.keys).render(@view, @locals)
end
- result = @template ? collection_with_template : collection_without_template
- result.join(spacer).html_safe
+ cache_collection_render do
+ @template ? collection_with_template : collection_without_template
+ end.join(spacer).html_safe
end
def render_partial
diff --git a/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb b/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb
new file mode 100644
index 0000000000..b77c884e66
--- /dev/null
+++ b/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb
@@ -0,0 +1,70 @@
+require 'active_support/core_ext/object/try'
+
+module ActionView
+ module CollectionCaching # :nodoc:
+ extend ActiveSupport::Concern
+
+ included do
+ # Fallback cache store if Action View is used without Rails.
+ # Otherwise overriden in Railtie to use Rails.cache.
+ mattr_accessor(:collection_cache) { ActiveSupport::Cache::MemoryStore.new }
+ end
+
+ private
+ def cache_collection_render
+ return yield unless cache_collection?
+
+ keyed_collection = collection_by_cache_keys
+ partial_cache = collection_cache.read_multi(*keyed_collection.keys)
+
+ @collection = keyed_collection.reject { |key, _| partial_cache.key?(key) }.values
+ rendered_partials = @collection.any? ? yield.dup : []
+
+ fetch_or_cache_partial(partial_cache, order_by: keyed_collection.each_key) do
+ rendered_partials.shift
+ end
+ end
+
+ def cache_collection?
+ @options.fetch(:cache, automatic_cache_eligible?)
+ end
+
+ def automatic_cache_eligible?
+ single_template_render? && !callable_cache_key? &&
+ @template.eligible_for_collection_caching?(as: @options[:as])
+ end
+
+ def single_template_render?
+ @template # Template is only set when a collection renders one template.
+ end
+
+ def callable_cache_key?
+ @options[:cache].respond_to?(:call)
+ end
+
+ def collection_by_cache_keys
+ seed = callable_cache_key? ? @options[:cache] : ->(i) { i }
+
+ @collection.each_with_object({}) do |item, hash|
+ hash[expanded_cache_key(seed.call(item))] = item
+ end
+ end
+
+ def expanded_cache_key(key)
+ key = @view.fragment_cache_key(@view.cache_fragment_name(key))
+ key.frozen? ? key.dup : key # #read_multi & #write may require mutability, Dalli 2.6.0.
+ end
+
+ def fetch_or_cache_partial(cached_partials, order_by:)
+ cache_options = @options[:cache_options] || @locals[:cache_options] || {}
+
+ order_by.map do |key|
+ cached_partials.fetch(key) do
+ yield.tap do |rendered_partial|
+ collection_cache.write(key, rendered_partial, cache_options)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/actionview/lib/action_view/template.rb b/actionview/lib/action_view/template.rb
index 305d9c6ee3..377ceb534a 100644
--- a/actionview/lib/action_view/template.rb
+++ b/actionview/lib/action_view/template.rb
@@ -130,6 +130,7 @@ module ActionView
@source = source
@identifier = identifier
@handler = handler
+ @cache_name = extract_resource_cache_call_name
@compiled = false
@original_encoding = nil
@locals = details[:locals] || []
@@ -165,6 +166,10 @@ module ActionView
@type ||= Types[@formats.first] if @formats.first
end
+ def eligible_for_collection_caching?(as: nil)
+ @cache_name == (as || inferred_cache_name).to_s
+ end
+
# Receives a view object and return a template similar to self by using @virtual_path.
#
# This method is useful if you have a template object but it does not contain its source
@@ -345,5 +350,14 @@ module ActionView
payload = { virtual_path: @virtual_path, identifier: @identifier }
ActiveSupport::Notifications.instrument("#{action}.action_view", payload, &block)
end
+
+ def extract_resource_cache_call_name
+ $1 if @handler.respond_to?(:resource_cache_call_pattern) &&
+ @source =~ @handler.resource_cache_call_pattern
+ end
+
+ def inferred_cache_name
+ @inferred_cache_name ||= @virtual_path.split('/').last.sub('_', '')
+ end
end
end
diff --git a/actionview/lib/action_view/template/handlers/erb.rb b/actionview/lib/action_view/template/handlers/erb.rb
index 85a100ed4c..88a8570706 100644
--- a/actionview/lib/action_view/template/handlers/erb.rb
+++ b/actionview/lib/action_view/template/handlers/erb.rb
@@ -123,6 +123,24 @@ module ActionView
).src
end
+ # Returns Regexp to extract a cached resource's name from a cache call at the
+ # first line of a template.
+ # The extracted cache name is expected in $1.
+ #
+ # <% cache notification do %> # => notification
+ #
+ # The pattern should support templates with a beginning comment:
+ #
+ # <%# Still extractable even though there's a comment %>
+ # <% cache notification do %> # => notification
+ #
+ # But fail to extract a name if a resource association is cached.
+ #
+ # <% cache notification.event do %> # => nil
+ def resource_cache_call_pattern
+ /\A(?:<%#.*%>\n?)?<% cache\(?\s*(\w+\.?)/
+ end
+
private
def valid_encoding(string, encoding)
diff --git a/actionview/test/actionpack/controller/render_test.rb b/actionview/test/actionpack/controller/render_test.rb
index fb826af044..8b47536a18 100644
--- a/actionview/test/actionpack/controller/render_test.rb
+++ b/actionview/test/actionpack/controller/render_test.rb
@@ -31,6 +31,10 @@ class Customer < Struct.new(:name, :id)
def persisted?
id.present?
end
+
+ def cache_key
+ name.to_s
+ end
end
module Quiz
diff --git a/actionview/test/fixtures/test/_cached_customer.erb b/actionview/test/fixtures/test/_cached_customer.erb
new file mode 100644
index 0000000000..52f35a3497
--- /dev/null
+++ b/actionview/test/fixtures/test/_cached_customer.erb
@@ -0,0 +1,3 @@
+<% cache cached_customer do %>
+ Hello: <%= cached_customer.name %>
+<% end %> \ No newline at end of file
diff --git a/actionview/test/fixtures/test/_cached_customer_as.erb b/actionview/test/fixtures/test/_cached_customer_as.erb
new file mode 100644
index 0000000000..fca8d19e34
--- /dev/null
+++ b/actionview/test/fixtures/test/_cached_customer_as.erb
@@ -0,0 +1,3 @@
+<% cache buyer do %>
+ <%= greeting %>: <%= customer.name %>
+<% end %> \ No newline at end of file
diff --git a/actionview/test/template/dependency_tracker_test.rb b/actionview/test/template/dependency_tracker_test.rb
index d74da4c318..672b4747ec 100644
--- a/actionview/test/template/dependency_tracker_test.rb
+++ b/actionview/test/template/dependency_tracker_test.rb
@@ -60,7 +60,6 @@ class ERBTrackerTest < Minitest::Test
end
def test_dependency_of_template_partial_with_layout
- skip # FIXME: Needs to be fixed properly, right now we can only match one dependency per line. Need multiple!
template = FakeTemplate.new("<%# render partial: 'messages/show', layout: 'messages/layout' %>", :erb)
tracker = make_tracker("multiple/_dependencies", template)
diff --git a/actionview/test/template/render_test.rb b/actionview/test/template/render_test.rb
index 94af1dc3ad..22665b6844 100644
--- a/actionview/test/template/render_test.rb
+++ b/actionview/test/template/render_test.rb
@@ -598,3 +598,41 @@ class LazyViewRenderTest < ActiveSupport::TestCase
silence_warnings { Encoding.default_external = old }
end
end
+
+class CachedCollectionViewRenderTest < CachedViewRenderTest
+ class CachedCustomer < Customer; end
+
+ teardown do
+ ActionView::PartialRenderer.collection_cache.clear
+ end
+
+ test "with custom key" do
+ customer = Customer.new("david")
+ key = ActionController::Base.new.fragment_cache_key([customer, 'key'])
+
+ ActionView::PartialRenderer.collection_cache.write(key, 'Hello')
+
+ assert_equal "Hello",
+ @view.render(partial: "test/customer", collection: [customer], cache: ->(item) { [item, 'key'] })
+ end
+
+ test "automatic caching with inferred cache name" do
+ customer = CachedCustomer.new("david")
+ key = ActionController::Base.new.fragment_cache_key(customer)
+
+ ActionView::PartialRenderer.collection_cache.write(key, 'Cached')
+
+ assert_equal "Cached",
+ @view.render(partial: "test/cached_customer", collection: [customer])
+ end
+
+ test "automatic caching with as name" do
+ customer = CachedCustomer.new("david")
+ key = ActionController::Base.new.fragment_cache_key(customer)
+
+ ActionView::PartialRenderer.collection_cache.write(key, 'Cached')
+
+ assert_equal "Cached",
+ @view.render(partial: "test/cached_customer_as", collection: [customer], as: :buyer)
+ end
+end
diff --git a/activejob/lib/active_job/queue_adapter.rb b/activejob/lib/active_job/queue_adapter.rb
index d610d30e01..aa3ebdbc7b 100644
--- a/activejob/lib/active_job/queue_adapter.rb
+++ b/activejob/lib/active_job/queue_adapter.rb
@@ -17,8 +17,6 @@ module ActiveJob
def queue_adapter=(name_or_adapter)
@@queue_adapter = \
case name_or_adapter
- when :test
- ActiveJob::QueueAdapters::TestAdapter.new
when Symbol, String
load_adapter(name_or_adapter)
else
diff --git a/activejob/lib/active_job/queue_adapters/test_adapter.rb b/activejob/lib/active_job/queue_adapters/test_adapter.rb
index c9e2bdca27..fd7c0b207a 100644
--- a/activejob/lib/active_job/queue_adapters/test_adapter.rb
+++ b/activejob/lib/active_job/queue_adapters/test_adapter.rb
@@ -10,40 +10,39 @@ module ActiveJob
#
# Rails.application.config.active_job.queue_adapter = :test
class TestAdapter
- delegate :name, to: :class
- attr_accessor(:perform_enqueued_jobs, :perform_enqueued_at_jobs, :filter)
- attr_writer(:enqueued_jobs, :performed_jobs)
+ class << self
+ attr_accessor(:perform_enqueued_jobs, :perform_enqueued_at_jobs, :filter)
+ attr_writer(:enqueued_jobs, :performed_jobs)
- def initialize
- self.perform_enqueued_jobs = false
- self.perform_enqueued_at_jobs = false
- end
+ # Provides a store of all the enqueued jobs with the TestAdapter so you can check them.
+ def enqueued_jobs
+ @enqueued_jobs ||= []
+ end
- # Provides a store of all the enqueued jobs with the TestAdapter so you can check them.
- def enqueued_jobs
- @enqueued_jobs ||= []
- end
+ # Provides a store of all the performed jobs with the TestAdapter so you can check them.
+ def performed_jobs
+ @performed_jobs ||= []
+ end
- # Provides a store of all the performed jobs with the TestAdapter so you can check them.
- def performed_jobs
- @performed_jobs ||= []
- end
+ def enqueue(job) #:nodoc:
+ return if filtered?(job)
- def enqueue(job) #:nodoc:
- return if filtered?(job)
+ job_data = job_to_hash(job)
+ enqueue_or_perform(perform_enqueued_jobs, job, job_data)
+ end
- job_data = { job: job.class, args: job.serialize['arguments'], queue: job.queue_name }
- enqueue_or_perform(perform_enqueued_jobs, job, job_data)
- end
+ def enqueue_at(job, timestamp) #:nodoc:
+ return if filtered?(job)
- def enqueue_at(job, timestamp) #:nodoc:
- return if filtered?(job)
+ job_data = job_to_hash(job, at: timestamp)
+ enqueue_or_perform(perform_enqueued_at_jobs, job, job_data)
+ end
- job_data = { job: job.class, args: job.serialize['arguments'], queue: job.queue_name, at: timestamp }
- enqueue_or_perform(perform_enqueued_at_jobs, job, job_data)
- end
+ private
- private
+ def job_to_hash(job, extras = {})
+ { job: job.class, args: job.serialize.fetch('arguments'), queue: job.queue_name }.merge!(extras)
+ end
def enqueue_or_perform(perform, job, job_data)
if perform
@@ -57,6 +56,7 @@ module ActiveJob
def filtered?(job)
filter && !Array(filter).include?(job.class)
end
+ end
end
end
end
diff --git a/activejob/lib/active_job/test_helper.rb b/activejob/lib/active_job/test_helper.rb
index 25bc99a4f8..66508114d1 100644
--- a/activejob/lib/active_job/test_helper.rb
+++ b/activejob/lib/active_job/test_helper.rb
@@ -7,10 +7,12 @@ module ActiveJob
included do
def before_setup
- @old_queue_adapter = queue_adapter
+ @old_queue_adapter = queue_adapter
ActiveJob::Base.queue_adapter = :test
clear_enqueued_jobs
clear_performed_jobs
+ queue_adapter.perform_enqueued_jobs = false
+ queue_adapter.perform_enqueued_at_jobs = false
super
end
@@ -281,7 +283,7 @@ module ActiveJob
def enqueued_jobs_size(only: nil)
if only
- enqueued_jobs.select { |job| job[:job] == only }.size
+ enqueued_jobs.select { |job| job.fetch(:job) == only }.size
else
enqueued_jobs.size
end
diff --git a/activejob/test/cases/test_case_test.rb b/activejob/test/cases/test_case_test.rb
index 1d0fdbd22d..7d1702990e 100644
--- a/activejob/test/cases/test_case_test.rb
+++ b/activejob/test/cases/test_case_test.rb
@@ -9,6 +9,6 @@ class ActiveJobTestCaseTest < ActiveJob::TestCase
end
def test_set_test_adapter
- assert_instance_of ActiveJob::QueueAdapters::TestAdapter, self.queue_adapter
+ assert_equal ActiveJob::QueueAdapters::TestAdapter, self.queue_adapter
end
end
diff --git a/activemodel/lib/active_model/naming.rb b/activemodel/lib/active_model/naming.rb
index 2bc3eeaa19..22010b517c 100644
--- a/activemodel/lib/active_model/naming.rb
+++ b/activemodel/lib/active_model/naming.rb
@@ -130,7 +130,7 @@ module ActiveModel
#
# Equivalent to +to_s+.
delegate :==, :===, :<=>, :=~, :"!~", :eql?, :to_s,
- :to_str, to: :name
+ :to_str, :as_json, to: :name
# Returns a new ActiveModel::Name instance. By default, the +namespace+
# and +name+ option will take the namespace and name of the given class
diff --git a/activemodel/test/cases/serializers/json_serialization_test.rb b/activemodel/test/cases/serializers/json_serialization_test.rb
index e2eb91eeb0..d765a47636 100644
--- a/activemodel/test/cases/serializers/json_serialization_test.rb
+++ b/activemodel/test/cases/serializers/json_serialization_test.rb
@@ -195,4 +195,8 @@ class JsonSerializationTest < ActiveModel::TestCase
assert_no_match %r{"awesome":}, json
assert_no_match %r{"preferences":}, json
end
+
+ test "Class.model_name should be json encodable" do
+ assert_match %r{"Contact"}, Contact.model_name.to_json
+ end
end
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md
index 131fdc62aa..b66db6a754 100644
--- a/activerecord/CHANGELOG.md
+++ b/activerecord/CHANGELOG.md
@@ -1,3 +1,41 @@
+* Add `SchemaMigration.create_table` support any unicode charsets for MySQL.
+
+ *Ryuta Kamizono*
+
+* PostgreSQL, no longer disables user triggers if system triggers can't be
+ disabled. Disabling user triggers does not fulfill what the method promises.
+ Rails currently requires superuser privileges for this method.
+
+ If you absolutely rely on this behavior, consider patching
+ `disable_referential_integrity`.
+
+ *Yves Senn*
+
+* Restore aborted transaction state when `disable_referential_integrity` fails
+ due to missing permissions.
+
+ *Toby Ovod-Everett*, *Yves Senn*
+
+* PostgreSQL, print warning message if `disable_referential_integrity` fails
+ due to missing permissions.
+
+ *Andrey Nering*, *Yves Senn*
+
+* Allow `:limit` option for MySQL bigint primary key support.
+
+ Example:
+
+ create_table :foos, id: :primary_key, limit: 8 do |t|
+ end
+
+ # or
+
+ create_table :foos, id: false do |t|
+ t.primary_key :id, limit: 8
+ end
+
+ *Ryuta Kamizono*
+
* `belongs_to` will now trigger a validation error by default if the association is not present.
You can turn this off on a per-association basis with `optional: true`.
(Note this new default only applies to new Rails apps that will be generated with
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb
index 0b33ee881b..cecb2dbd0b 100644
--- a/activerecord/lib/active_record/associations.rb
+++ b/activerecord/lib/active_record/associations.rb
@@ -1019,7 +1019,7 @@ module ActiveRecord
# can affect what it does.
#
# Note that <tt>:dependent</tt> option is ignored for +has_one+ <tt>:through</tt> associations.
- #
+ #
# === Delete or destroy?
#
# +has_many+ and +has_and_belongs_to_many+ associations have the methods <tt>destroy</tt>,
@@ -1529,8 +1529,6 @@ module ActiveRecord
# NOTE: <tt>required</tt> is set to <tt>true</tt> by default and is deprecated. If
# you don't want to have association presence validated, use <tt>optional: true</tt>.
#
- #
- #
# Option examples:
# belongs_to :firm, foreign_key: "client_of"
# belongs_to :person, primary_key: "name", foreign_key: "person_name"
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 a2777fcd0a..0acc815d51 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
@@ -142,6 +142,41 @@ module ActiveRecord
end
end
+ module ColumnMethods
+ # Appends a primary key definition to the table definition.
+ # Can be called multiple times, but this is probably not a good idea.
+ def primary_key(name, type = :primary_key, **options)
+ column(name, type, options.merge(primary_key: true))
+ end
+
+ # Appends a column or columns of a specified type.
+ #
+ # t.string(:goat)
+ # t.string(:goat, :sheep)
+ #
+ # See TableDefinition#column
+ [
+ :bigint,
+ :binary,
+ :boolean,
+ :date,
+ :datetime,
+ :decimal,
+ :float,
+ :integer,
+ :string,
+ :text,
+ :time,
+ :timestamp,
+ ].each do |column_type|
+ module_eval <<-CODE, __FILE__, __LINE__ + 1
+ def #{column_type}(*args, **options)
+ args.each { |name| column(name, :#{column_type}, options) }
+ end
+ CODE
+ end
+ end
+
# Represents the schema of an SQL table in an abstract way. This class
# provides methods for manipulating the schema representation.
#
@@ -163,6 +198,8 @@ module ActiveRecord
# The table definitions
# The Columns are stored as a ColumnDefinition in the +columns+ attribute.
class TableDefinition
+ include ColumnMethods
+
# An array of ColumnDefinition objects, representing the column changes
# that have been defined.
attr_accessor :indexes
@@ -181,12 +218,6 @@ module ActiveRecord
def columns; @columns_hash.values; end
- # Appends a primary key definition to the table definition.
- # Can be called multiple times, but this is probably not a good idea.
- def primary_key(name, type = :primary_key, options = {})
- column(name, type, options.merge(:primary_key => true))
- end
-
# Returns a ColumnDefinition for the column with name +name+.
def [](name)
@columns_hash[name.to_s]
@@ -343,14 +374,6 @@ module ActiveRecord
@columns_hash.delete name.to_s
end
- [:string, :text, :integer, :bigint, :float, :decimal, :datetime, :timestamp, :time, :date, :binary, :boolean].each do |column_type|
- define_method column_type do |*args|
- options = args.extract_options!
- column_names = args
- column_names.each { |name| column(name, column_type, options) }
- end
- end
-
# Adds index options to the indexes hash, keyed by column name
# This is primarily used to track indexes that need to be created after the table
#
@@ -462,6 +485,7 @@ module ActiveRecord
# Available transformations are:
#
# change_table :table do |t|
+ # t.primary_key
# t.column
# t.index
# t.rename_index
@@ -474,6 +498,7 @@ module ActiveRecord
# t.string
# t.text
# t.integer
+ # t.bigint
# t.float
# t.decimal
# t.datetime
@@ -490,6 +515,8 @@ module ActiveRecord
# end
#
class Table
+ include ColumnMethods
+
attr_reader :name
def initialize(table_name, base)
@@ -640,21 +667,6 @@ module ActiveRecord
end
alias :remove_belongs_to :remove_references
- # Adds a column or columns of a specified type.
- #
- # t.string(:goat)
- # t.string(:goat, :sheep)
- #
- # See SchemaStatements#add_column
- [:string, :text, :integer, :float, :decimal, :datetime, :timestamp, :time, :date, :binary, :boolean].each do |column_type|
- define_method column_type do |*args|
- options = args.extract_options!
- args.each do |column_name|
- @base.add_column(name, column_name, column_type, options)
- end
- end
- end
-
def foreign_key(*args) # :nodoc:
@base.add_foreign_key(name, *args)
end
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 0438c95bd7..d42f9a894b 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
@@ -770,7 +770,7 @@ module ActiveRecord
# # Check a foreign key exists
# foreign_key_exists?(:accounts, :branches)
#
- # # Check a foreign key on specified column exists
+ # # Check a foreign key on a specified column exists
# foreign_key_exists?(:accounts, column: :owner_id)
#
# # Check a foreign key with a custom name exists
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb
index 11440e30d4..3a1e4a4a88 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb
@@ -77,6 +77,10 @@ module ActiveRecord
@state.set_state(:committed)
end
+ def before_commit_records
+ records.uniq.each(&:before_committed!)
+ end
+
def commit_records
ite = records.uniq
while record = ite.shift
@@ -159,13 +163,15 @@ module ActiveRecord
def commit_transaction
inner_transaction = @stack.pop
- inner_transaction.commit
if current_transaction.joinable?
+ inner_transaction.commit
inner_transaction.records.each do |r|
r.add_to_transaction
end
else
+ inner_transaction.before_commit_records
+ inner_transaction.commit
inner_transaction.commit_records
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
index c307189980..ae42e8ef8d 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
@@ -478,7 +478,6 @@ module ActiveRecord
message = "#{e.class.name}: #{e.message.force_encoding sql.encoding}: #{sql}"
end
- @logger.error message if @logger
exception = translate_exception(e, message)
exception.set_backtrace e.backtrace
exception
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 8db4bcd7e3..c084431588 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
@@ -6,13 +6,31 @@ module ActiveRecord
class AbstractMysqlAdapter < AbstractAdapter
include Savepoints
- class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition
- def primary_key(name, type = :primary_key, options = {})
- options[:auto_increment] ||= type == :bigint
+ module ColumnMethods
+ def primary_key(name, type = :primary_key, **options)
+ options[:auto_increment] = true if type == :bigint
super
end
end
+ class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition
+ include ColumnMethods
+
+ def new_column_definition(name, type, options) # :nodoc:
+ column = super
+ case column.type
+ when :primary_key
+ column.type = :integer
+ column.auto_increment = true
+ end
+ column
+ end
+ end
+
+ class Table < ActiveRecord::ConnectionAdapters::Table
+ include ColumnMethods
+ end
+
class SchemaCreation < AbstractAdapter::SchemaCreation
def visit_AddColumn(o)
add_column_position!(super, column_options(o))
@@ -57,6 +75,10 @@ module ActiveRecord
end
end
+ def update_table_definition(table_name, base) # :nodoc:
+ Table.new(table_name, base)
+ end
+
def schema_creation
SchemaCreation.new self
end
@@ -76,6 +98,7 @@ module ActiveRecord
def prepare_column_options(column)
spec = super
spec.delete(:precision) if /time/ === column.sql_type && column.precision == 0
+ spec.delete(:limit) if :boolean === column.type
spec
end
@@ -212,6 +235,16 @@ module ActiveRecord
end
end
+ MAX_INDEX_LENGTH_FOR_CHARSETS_OF_4BYTES_MAXLEN = 191
+ CHARSETS_OF_4BYTES_MAXLEN = ['utf8mb4', 'utf16', 'utf16le', 'utf32']
+ def initialize_schema_migrations_table
+ if CHARSETS_OF_4BYTES_MAXLEN.include?(charset)
+ ActiveRecord::SchemaMigration.create_table(MAX_INDEX_LENGTH_FOR_CHARSETS_OF_4BYTES_MAXLEN)
+ else
+ ActiveRecord::SchemaMigration.create_table
+ end
+ end
+
# Returns true, since this connection adapter supports migrations.
def supports_migrations?
true
diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
index fac6f81540..21631be25c 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
@@ -37,15 +37,6 @@ module ActiveRecord
configure_connection
end
- MAX_INDEX_LENGTH_FOR_UTF8MB4 = 191
- def initialize_schema_migrations_table
- if charset == 'utf8mb4'
- ActiveRecord::SchemaMigration.create_table(MAX_INDEX_LENGTH_FOR_UTF8MB4)
- else
- ActiveRecord::SchemaMigration.create_table
- end
- end
-
def supports_explain?
true
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb b/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb
index 52b307c432..c1835380f9 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb
@@ -8,20 +8,39 @@ module ActiveRecord
def disable_referential_integrity # :nodoc:
if supports_disable_referential_integrity?
+ original_exception = nil
+
begin
- execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} DISABLE TRIGGER ALL" }.join(";"))
- rescue
- execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} DISABLE TRIGGER USER" }.join(";"))
+ transaction(requires_new: true) do
+ execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} DISABLE TRIGGER ALL" }.join(";"))
+ end
+ rescue => e
+ original_exception = e
end
- end
- yield
- ensure
- if supports_disable_referential_integrity?
+
+ begin
+ yield
+ rescue ActiveRecord::InvalidForeignKey => e
+ warn <<-WARNING
+WARNING: Rails was not able to disable referential integrity.
+
+This is most likely caused due to missing permissions.
+Rails needs superuser privileges to disable referential integrity.
+
+ cause: #{original_exception.try(:message)}
+
+ WARNING
+ raise e
+ end
+
begin
- execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} ENABLE TRIGGER ALL" }.join(";"))
+ transaction(require_new: true) do
+ execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} ENABLE TRIGGER ALL" }.join(";"))
+ end
rescue
- execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} ENABLE TRIGGER USER" }.join(";"))
end
+ else
+ yield
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb
index b9078d4c86..022dbdfa27 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb
@@ -2,90 +2,129 @@ module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module ColumnMethods
- def xml(*args)
- options = args.extract_options!
- column(args[0], :xml, options)
+ # Defines the primary key field.
+ # Use of the native PostgreSQL UUID type is supported, and can be used
+ # by defining your tables as such:
+ #
+ # create_table :stuffs, id: :uuid do |t|
+ # t.string :content
+ # t.timestamps
+ # end
+ #
+ # By default, this will use the +uuid_generate_v4()+ function from the
+ # +uuid-ossp+ extension, which MUST be enabled on your database. To enable
+ # the +uuid-ossp+ extension, you can use the +enable_extension+ method in your
+ # migrations. To use a UUID primary key without +uuid-ossp+ enabled, you can
+ # set the +:default+ option to +nil+:
+ #
+ # create_table :stuffs, id: false do |t|
+ # t.primary_key :id, :uuid, default: nil
+ # t.uuid :foo_id
+ # t.timestamps
+ # end
+ #
+ # You may also pass a different UUID generation function from +uuid-ossp+
+ # or another library.
+ #
+ # Note that setting the UUID primary key default value to +nil+ will
+ # require you to assure that you always provide a UUID value before saving
+ # a record (as primary keys cannot be +nil+). This might be done via the
+ # +SecureRandom.uuid+ method and a +before_save+ callback, for instance.
+ def primary_key(name, type = :primary_key, **options)
+ options[:default] = options.fetch(:default, 'uuid_generate_v4()') if type == :uuid
+ super
+ end
+
+ def bigserial(*args, **options)
+ args.each { |name| column(name, :bigserial, options) }
+ end
+
+ def bit(*args, **options)
+ args.each { |name| column(name, :bit, options) }
end
- def tsvector(*args)
- options = args.extract_options!
- column(args[0], :tsvector, options)
+ def bit_varying(*args, **options)
+ args.each { |name| column(name, :bit_varying, options) }
end
- def int4range(name, options = {})
- column(name, :int4range, options)
+ def cidr(*args, **options)
+ args.each { |name| column(name, :cidr, options) }
end
- def int8range(name, options = {})
- column(name, :int8range, options)
+ def citext(*args, **options)
+ args.each { |name| column(name, :citext, options) }
end
- def tsrange(name, options = {})
- column(name, :tsrange, options)
+ def daterange(*args, **options)
+ args.each { |name| column(name, :daterange, options) }
end
- def tstzrange(name, options = {})
- column(name, :tstzrange, options)
+ def hstore(*args, **options)
+ args.each { |name| column(name, :hstore, options) }
end
- def numrange(name, options = {})
- column(name, :numrange, options)
+ def inet(*args, **options)
+ args.each { |name| column(name, :inet, options) }
end
- def daterange(name, options = {})
- column(name, :daterange, options)
+ def int4range(*args, **options)
+ args.each { |name| column(name, :int4range, options) }
end
- def hstore(name, options = {})
- column(name, :hstore, options)
+ def int8range(*args, **options)
+ args.each { |name| column(name, :int8range, options) }
end
- def ltree(name, options = {})
- column(name, :ltree, options)
+ def json(*args, **options)
+ args.each { |name| column(name, :json, options) }
end
- def inet(name, options = {})
- column(name, :inet, options)
+ def jsonb(*args, **options)
+ args.each { |name| column(name, :jsonb, options) }
end
- def cidr(name, options = {})
- column(name, :cidr, options)
+ def ltree(*args, **options)
+ args.each { |name| column(name, :ltree, options) }
end
- def macaddr(name, options = {})
- column(name, :macaddr, options)
+ def macaddr(*args, **options)
+ args.each { |name| column(name, :macaddr, options) }
end
- def uuid(name, options = {})
- column(name, :uuid, options)
+ def money(*args, **options)
+ args.each { |name| column(name, :money, options) }
end
- def json(name, options = {})
- column(name, :json, options)
+ def numrange(*args, **options)
+ args.each { |name| column(name, :numrange, options) }
end
- def jsonb(name, options = {})
- column(name, :jsonb, options)
+ def point(*args, **options)
+ args.each { |name| column(name, :point, options) }
end
- def citext(name, options = {})
- column(name, :citext, options)
+ def serial(*args, **options)
+ args.each { |name| column(name, :serial, options) }
end
- def point(name, options = {})
- column(name, :point, options)
+ def tsrange(*args, **options)
+ args.each { |name| column(name, :tsrange, options) }
end
- def bit(name, options = {})
- column(name, :bit, options)
+ def tstzrange(*args, **options)
+ args.each { |name| column(name, :tstzrange, options) }
end
- def bit_varying(name, options = {})
- column(name, :bit_varying, options)
+ def tsvector(*args, **options)
+ args.each { |name| column(name, :tsvector, options) }
end
- def money(name, options = {})
- column(name, :money, options)
+ def uuid(*args, **options)
+ args.each { |name| column(name, :uuid, options) }
+ end
+
+ def xml(*args, **options)
+ args.each { |name| column(name, :xml, options) }
end
end
@@ -96,39 +135,6 @@ module ActiveRecord
class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition
include ColumnMethods
- # Defines the primary key field.
- # Use of the native PostgreSQL UUID type is supported, and can be used
- # by defining your tables as such:
- #
- # create_table :stuffs, id: :uuid do |t|
- # t.string :content
- # t.timestamps
- # end
- #
- # By default, this will use the +uuid_generate_v4()+ function from the
- # +uuid-ossp+ extension, which MUST be enabled on your database. To enable
- # the +uuid-ossp+ extension, you can use the +enable_extension+ method in your
- # migrations. To use a UUID primary key without +uuid-ossp+ enabled, you can
- # set the +:default+ option to +nil+:
- #
- # create_table :stuffs, id: false do |t|
- # t.primary_key :id, :uuid, default: nil
- # t.uuid :foo_id
- # t.timestamps
- # end
- #
- # You may also pass a different UUID generation function from +uuid-ossp+
- # or another library.
- #
- # Note that setting the UUID primary key default value to +nil+ will
- # require you to assure that you always provide a UUID value before saving
- # a record (as primary keys cannot be +nil+). This might be done via the
- # +SecureRandom.uuid+ method and a +before_save+ callback, for instance.
- def primary_key(name, type = :primary_key, options = {})
- options[:default] = options.fetch(:default, 'uuid_generate_v4()') if type == :uuid
- super
- end
-
def new_column_definition(name, type, options) # :nodoc:
column = super
column.array = options[:array]
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
index 400b586c95..7e184dd510 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
@@ -121,6 +121,7 @@ module ActiveRecord
@config = config
@visitor = Arel::Visitors::SQLite.new self
+ @quoted_column_names = {}
if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true })
@prepared_statements = true
@@ -240,7 +241,7 @@ module ActiveRecord
end
def quote_column_name(name) #:nodoc:
- %Q("#{name.to_s.gsub('"', '""')}")
+ @quoted_column_names[name] ||= %Q("#{name.to_s.gsub('"', '""')}")
end
#--
diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb
index de48681746..d41872d767 100644
--- a/activerecord/lib/active_record/core.rb
+++ b/activerecord/lib/active_record/core.rb
@@ -488,7 +488,7 @@ module ActiveRecord
end
def has_transactional_callbacks? # :nodoc:
- !_rollback_callbacks.empty? || !_commit_callbacks.empty?
+ !_rollback_callbacks.empty? || !_commit_callbacks.empty? || !_before_commit_callbacks.empty?
end
# Updates the attributes on this particular ActiveRecord object so that
diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb
index 739be524da..75b0e1e08d 100644
--- a/activerecord/lib/active_record/fixtures.rb
+++ b/activerecord/lib/active_record/fixtures.rb
@@ -708,6 +708,10 @@ module ActiveRecord
def lhs_key
@association.through_reflection.foreign_key
end
+
+ def join_table
+ @association.through_reflection.table_name
+ end
end
private
diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb
index a58d8355aa..a09437b4b0 100644
--- a/activerecord/lib/active_record/locking/optimistic.rb
+++ b/activerecord/lib/active_record/locking/optimistic.rb
@@ -93,7 +93,7 @@ module ActiveRecord
self.class.primary_key => id,
lock_col => previous_lock_value,
).update_all(
- attribute_names.map do |name|
+ attributes_for_update(attribute_names).map do |name|
[name, _read_attribute(name)]
end.to_h
)
diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb
index 4a8e6bdd23..a83b90a95f 100644
--- a/activerecord/lib/active_record/migration.rb
+++ b/activerecord/lib/active_record/migration.rb
@@ -395,7 +395,7 @@ module ActiveRecord
def load_schema_if_pending!
if ActiveRecord::Migrator.needs_migration? || !ActiveRecord::Migrator.any_migrations?
- # Roundrip to Rake to allow plugins to hook into database initialization.
+ # Roundtrip to Rake to allow plugins to hook into database initialization.
FileUtils.cd Rails.root do
current_config = Base.connection_config
Base.clear_all_connections!
diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb
index f1bdbc845c..b6de90e89d 100644
--- a/activerecord/lib/active_record/railtie.rb
+++ b/activerecord/lib/active_record/railtie.rb
@@ -58,6 +58,9 @@ module ActiveRecord
require "active_record/railties/console_sandbox" if app.sandbox?
require "active_record/base"
console = ActiveSupport::Logger.new(STDERR)
+ console.formatter = Rails.logger.formatter
+ console.level = Rails.logger.level
+
Rails.logger.extend ActiveSupport::Logger.broadcast console
end
diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb
index 7514401072..69ce5cdc2a 100644
--- a/activerecord/lib/active_record/relation/query_methods.rb
+++ b/activerecord/lib/active_record/relation/query_methods.rb
@@ -999,11 +999,15 @@ module ActiveRecord
end
def arel_columns(columns)
- columns.map do |field|
- if (Symbol === field || String === field) && columns_hash.key?(field.to_s)
- arel_table[field]
- else
- field
+ if from_clause.value
+ columns
+ else
+ columns.map do |field|
+ if (Symbol === field || String === field) && columns_hash.key?(field.to_s)
+ arel_table[field]
+ else
+ field
+ end
end
end
end
diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb
index dd405c7796..598defce50 100644
--- a/activerecord/lib/active_record/transactions.rb
+++ b/activerecord/lib/active_record/transactions.rb
@@ -7,6 +7,10 @@ module ActiveRecord
included do
define_callbacks :commit, :rollback,
+ :before_commit,
+ :before_commit_without_transaction_enrollment,
+ :commit_without_transaction_enrollment,
+ :rollback_without_transaction_enrollment,
scope: [:kind, :name]
end
@@ -206,6 +210,11 @@ module ActiveRecord
connection.transaction(options, &block)
end
+ def before_commit(*args, &block) # :nodoc:
+ set_options_for_callbacks!(args)
+ set_callback(:before_commit, :before, *args, &block)
+ end
+
# This callback is called after a record has been created, updated, or destroyed.
#
# You can specify that the callback should only be fired by a certain action with
@@ -233,6 +242,21 @@ module ActiveRecord
set_callback(:rollback, :after, *args, &block)
end
+ def before_commit_without_transaction_enrollment(*args, &block) # :nodoc:
+ set_options_for_callbacks!(args)
+ set_callback(:before_commit_without_transaction_enrollment, :before, *args, &block)
+ end
+
+ def after_commit_without_transaction_enrollment(*args, &block) # :nodoc:
+ set_options_for_callbacks!(args)
+ set_callback(:commit_without_transaction_enrollment, :after, *args, &block)
+ end
+
+ def after_rollback_without_transaction_enrollment(*args, &block) # :nodoc:
+ set_options_for_callbacks!(args)
+ set_callback(:rollback_without_transaction_enrollment, :after, *args, &block)
+ end
+
def raise_in_transactional_callbacks
ActiveSupport::Deprecation.warn('ActiveRecord::Base.raise_in_transactional_callbacks is deprecated and will be removed without replacement.')
true
@@ -296,12 +320,20 @@ module ActiveRecord
clear_transaction_record_state
end
+ def before_committed! # :nodoc:
+ _run_before_commit_without_transaction_enrollment_callbacks
+ _run_before_commit_callbacks
+ end
+
# Call the +after_commit+ callbacks.
#
# Ensure that it is not called if the object was never persisted (failed create),
# but call it after the commit of a destroyed object.
def committed!(should_run_callbacks: true) #:nodoc:
- _run_commit_callbacks if should_run_callbacks && destroyed? || persisted?
+ if should_run_callbacks && destroyed? || persisted?
+ _run_commit_without_transaction_enrollment_callbacks
+ _run_commit_callbacks
+ end
ensure
force_clear_transaction_record_state
end
@@ -309,7 +341,10 @@ module ActiveRecord
# Call the +after_rollback+ callbacks. The +force_restore_state+ argument indicates if the record
# state should be rolled back to the beginning or just to the last savepoint.
def rolledback!(force_restore_state: false, should_run_callbacks: true) #:nodoc:
- _run_rollback_callbacks if should_run_callbacks
+ if should_run_callbacks
+ _run_rollback_without_transaction_enrollment_callbacks
+ _run_rollback_callbacks
+ end
ensure
restore_transaction_record_state(force_restore_state)
clear_transaction_record_state
diff --git a/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb b/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb
index 271b570eb5..417ccf6d11 100644
--- a/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb
@@ -2,7 +2,7 @@ require "cases/helper"
module ActiveRecord
module ConnectionAdapters
- class Mysql2Adapter
+ class AbstractMysqlAdapter
class SchemaMigrationsTest < ActiveRecord::TestCase
def test_renaming_index_on_foreign_key
connection.add_index "engines", "car_id"
@@ -28,7 +28,7 @@ module ActiveRecord
connection.initialize_schema_migrations_table
- assert connection.column_exists?(smtn, :version, :string, limit: Mysql2Adapter::MAX_INDEX_LENGTH_FOR_UTF8MB4)
+ assert connection.column_exists?(smtn, :version, :string, limit: AbstractMysqlAdapter::MAX_INDEX_LENGTH_FOR_CHARSETS_OF_4BYTES_MAXLEN)
ensure
execute("ALTER DATABASE #{database_name} DEFAULT CHARACTER SET #{original_charset} COLLATE #{original_collation}")
end
diff --git a/activerecord/test/cases/adapters/postgresql/referential_integrity_test.rb b/activerecord/test/cases/adapters/postgresql/referential_integrity_test.rb
new file mode 100644
index 0000000000..98291f1bbf
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/referential_integrity_test.rb
@@ -0,0 +1,89 @@
+require 'cases/helper'
+require 'support/connection_helper'
+
+class PostgreSQLReferentialIntegrityTest < ActiveRecord::TestCase
+ self.use_transactional_fixtures = false
+
+ include ConnectionHelper
+
+ module MissingSuperuserPrivileges
+ def execute(sql)
+ if sql.match(/DISABLE TRIGGER ALL/) || sql.match(/ENABLE TRIGGER ALL/)
+ super "BROKEN;" rescue nil # put transaction in broken state
+ raise ActiveRecord::StatementInvalid, 'PG::InsufficientPrivilege'
+ else
+ super
+ end
+ end
+ end
+
+ def setup
+ @connection = ActiveRecord::Base.connection
+ end
+
+ def teardown
+ reset_connection
+ if ActiveRecord::Base.connection.is_a?(MissingSuperuserPrivileges)
+ raise "MissingSuperuserPrivileges patch was not removed"
+ end
+ end
+
+ def test_should_reraise_invalid_foreign_key_exception_and_show_warning
+ @connection.extend MissingSuperuserPrivileges
+
+ warning = capture(:stderr) do
+ e = assert_raises(ActiveRecord::InvalidForeignKey) do
+ @connection.disable_referential_integrity do
+ raise ActiveRecord::InvalidForeignKey, 'Should be re-raised'
+ end
+ end
+ assert_equal 'Should be re-raised', e.message
+ end
+ assert_match (/WARNING: Rails was not able to disable referential integrity/), warning
+ assert_match (/cause: PG::InsufficientPrivilege/), warning
+ end
+
+ def test_does_not_print_warning_if_no_invalid_foreign_key_exception_was_raised
+ @connection.extend MissingSuperuserPrivileges
+
+ warning = capture(:stderr) do
+ e = assert_raises(ActiveRecord::StatementInvalid) do
+ @connection.disable_referential_integrity do
+ raise ActiveRecord::StatementInvalid, 'Should be re-raised'
+ end
+ end
+ assert_equal 'Should be re-raised', e.message
+ end
+ assert warning.blank?, "expected no warnings but got:\n#{warning}"
+ end
+
+ def test_does_not_break_transactions
+ @connection.extend MissingSuperuserPrivileges
+
+ @connection.transaction do
+ @connection.disable_referential_integrity do
+ assert_transaction_is_not_broken
+ end
+ assert_transaction_is_not_broken
+ end
+ end
+
+ def test_does_not_break_nested_transactions
+ @connection.extend MissingSuperuserPrivileges
+
+ @connection.transaction do
+ @connection.transaction(requires_new: true) do
+ @connection.disable_referential_integrity do
+ assert_transaction_is_not_broken
+ end
+ end
+ assert_transaction_is_not_broken
+ end
+ end
+
+ private
+
+ def assert_transaction_is_not_broken
+ assert_equal "1", @connection.select_value("SELECT 1")
+ end
+end
diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb
index eebeaad7cf..897c52a49d 100644
--- a/activerecord/test/cases/associations/has_many_associations_test.rb
+++ b/activerecord/test/cases/associations/has_many_associations_test.rb
@@ -30,6 +30,7 @@ require 'models/college'
require 'models/student'
require 'models/pirate'
require 'models/ship'
+require 'models/ship_part'
require 'models/tyre'
require 'models/subscriber'
require 'models/subscription'
@@ -162,6 +163,30 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal college.students, Student.where(active: true, college_id: college.id)
end
+ def test_add_record_to_collection_should_change_its_updated_at
+ ship = Ship.create(name: 'dauntless')
+ part = ShipPart.create(name: 'cockpit')
+ updated_at = part.updated_at
+
+ ship.parts << part
+
+ assert_equal part.ship, ship
+ assert_not_equal part.updated_at, updated_at
+ end
+
+ def test_clear_collection_should_not_change_updated_at
+ # GH#17161: .clear calls delete_all (and returns the association),
+ # which is intended to not touch associated objects's updated_at field
+ ship = Ship.create(name: 'dauntless')
+ part = ShipPart.create(name: 'cockpit', ship_id: ship.id)
+
+ ship.parts.clear
+ part.reload
+
+ assert_equal nil, part.ship
+ assert !part.updated_at_changed?
+ end
+
def test_create_from_association_should_respect_default_scope
car = Car.create(:name => 'honda')
assert_equal 'honda', car.name
diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb
index f41245dfd2..7ef2ebc998 100644
--- a/activerecord/test/cases/fixtures_test.rb
+++ b/activerecord/test/cases/fixtures_test.rb
@@ -273,7 +273,7 @@ class HasManyThroughFixture < ActiveSupport::TestCase
Class.new(ActiveRecord::Base) { define_singleton_method(:name) { name } }
end
- def test_has_many_through
+ def test_has_many_through_with_default_table_name
pt = make_model "ParrotTreasure"
parrot = make_model "Parrot"
treasure = make_model "Treasure"
@@ -292,6 +292,24 @@ class HasManyThroughFixture < ActiveSupport::TestCase
assert_equal load_has_and_belongs_to_many['parrots_treasures'], rows['parrots_treasures']
end
+ def test_has_many_through_with_renamed_table
+ pt = make_model "ParrotTreasure"
+ parrot = make_model "Parrot"
+ treasure = make_model "Treasure"
+
+ pt.belongs_to :parrot, :class => parrot
+ pt.belongs_to :treasure, :class => treasure
+
+ parrot.has_many :parrot_treasures, :class => pt
+ parrot.has_many :treasures, :through => :parrot_treasures
+
+ parrots = File.join FIXTURES_ROOT, 'parrots'
+
+ fs = ActiveRecord::FixtureSet.new parrot.connection, "parrots", parrot, parrots
+ rows = fs.table_rows
+ assert_equal load_has_and_belongs_to_many['parrots_treasures'], rows['parrot_treasures']
+ end
+
def load_has_and_belongs_to_many
parrot = make_model "Parrot"
parrot.has_and_belongs_to_many :treasures
diff --git a/activerecord/test/cases/migration/change_table_test.rb b/activerecord/test/cases/migration/change_table_test.rb
index 7010af5434..2ffe7a1b0d 100644
--- a/activerecord/test/cases/migration/change_table_test.rb
+++ b/activerecord/test/cases/migration/change_table_test.rb
@@ -13,7 +13,7 @@ module ActiveRecord
end
def with_change_table
- yield ConnectionAdapters::Table.new(:delete_me, @connection)
+ yield ActiveRecord::Base.connection.update_table_definition(:delete_me, @connection)
end
def test_references_column_type_adds_id
@@ -100,6 +100,13 @@ module ActiveRecord
end
end
+ def test_primary_key_creates_primary_key_column
+ with_change_table do |t|
+ @connection.expect :add_column, nil, [:delete_me, :id, :primary_key, primary_key: true, first: true]
+ t.primary_key :id, first: true
+ end
+ end
+
def test_integer_creates_integer_column
with_change_table do |t|
@connection.expect :add_column, nil, [:delete_me, :foo, :integer, {}]
@@ -108,6 +115,14 @@ module ActiveRecord
end
end
+ def test_bigint_creates_bigint_column
+ with_change_table do |t|
+ @connection.expect :add_column, nil, [:delete_me, :foo, :bigint, {}]
+ @connection.expect :add_column, nil, [:delete_me, :bar, :bigint, {}]
+ t.bigint :foo, :bar
+ end
+ end
+
def test_string_creates_string_column
with_change_table do |t|
@connection.expect :add_column, nil, [:delete_me, :foo, :string, {}]
@@ -116,6 +131,24 @@ module ActiveRecord
end
end
+ if current_adapter?(:PostgreSQLAdapter)
+ def test_json_creates_json_column
+ with_change_table do |t|
+ @connection.expect :add_column, nil, [:delete_me, :foo, :json, {}]
+ @connection.expect :add_column, nil, [:delete_me, :bar, :json, {}]
+ t.json :foo, :bar
+ end
+ end
+
+ def test_xml_creates_xml_column
+ with_change_table do |t|
+ @connection.expect :add_column, nil, [:delete_me, :foo, :xml, {}]
+ @connection.expect :add_column, nil, [:delete_me, :bar, :xml, {}]
+ t.xml :foo, :bar
+ end
+ end
+ end
+
def test_column_creates_column
with_change_table do |t|
@connection.expect :add_column, nil, [:delete_me, :bar, :integer, {}]
diff --git a/activerecord/test/cases/primary_keys_test.rb b/activerecord/test/cases/primary_keys_test.rb
index b45fbf0143..1ea1ef5e12 100644
--- a/activerecord/test/cases/primary_keys_test.rb
+++ b/activerecord/test/cases/primary_keys_test.rb
@@ -282,5 +282,15 @@ if current_adapter?(:PostgreSQLAdapter, :MysqlAdapter, :Mysql2Adapter)
assert_match %r{create_table "widgets", id: :bigint}, schema
end
end
+
+ if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
+ test "primary key column type with options" do
+ @connection.create_table(:widgets, id: :primary_key, limit: 8, force: true)
+ column = @connection.columns(:widgets).find { |c| c.name == 'id' }
+ assert column.auto_increment?
+ assert_equal :integer, column.type
+ assert_equal 8, column.limit
+ end
+ end
end
end
diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb
index dec3a37f42..0cf44388fa 100644
--- a/activerecord/test/cases/relations_test.rb
+++ b/activerecord/test/cases/relations_test.rb
@@ -157,6 +157,17 @@ class RelationTest < ActiveRecord::TestCase
end
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')
+ assert_equal(relation.map(&:post_count).sort,subquery.map(&:post_count).sort)
+ end
+
+ def test_group_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).group('type').average("post_count")
+ assert_equal(relation.map(&:post_count).sort,subquery.values.sort)
+ end
def test_finding_with_conditions
assert_equal ["David"], Author.where(:name => 'David').map(&:name)
diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb
index 7d8d6421a9..513f65f707 100644
--- a/activerecord/test/cases/schema_dumper_test.rb
+++ b/activerecord/test/cases/schema_dumper_test.rb
@@ -225,6 +225,11 @@ class SchemaDumperTest < ActiveRecord::TestCase
assert_match %r{t\.text\s+"long_text",\s+limit: 4294967295$}, output
end
+ def test_schema_does_not_include_limit_for_emulated_mysql_boolean_fields
+ output = standard_dump
+ assert_no_match %r{t\.boolean\s+"has_fun",.+limit: 1}, output
+ end
+
def test_schema_dumps_index_type
output = standard_dump
assert_match %r{add_index "key_tests", \["awesome"\], name: "index_key_tests_on_awesome", type: :fulltext}, output
diff --git a/activerecord/test/cases/transaction_callbacks_test.rb b/activerecord/test/cases/transaction_callbacks_test.rb
index f185cda263..e868022fed 100644
--- a/activerecord/test/cases/transaction_callbacks_test.rb
+++ b/activerecord/test/cases/transaction_callbacks_test.rb
@@ -400,3 +400,63 @@ class CallbacksOnMultipleActionsTest < ActiveRecord::TestCase
assert_equal [:update_and_destroy, :create_and_destroy], topic.history
end
end
+
+
+class TransactionEnrollmentCallbacksTest < ActiveRecord::TestCase
+
+ class TopicWithoutTransactionalEnrollmentCallbacks < ActiveRecord::Base
+ self.table_name = :topics
+
+ before_commit_without_transaction_enrollment { |r| r.history << :before_commit }
+ after_commit_without_transaction_enrollment { |r| r.history << :after_commit }
+ after_rollback_without_transaction_enrollment { |r| r.history << :rollback }
+
+ def history
+ @history ||= []
+ end
+ end
+
+ def setup
+ @topic = TopicWithoutTransactionalEnrollmentCallbacks.create!
+ end
+
+ def test_commit_does_not_run_transactions_callbacks_without_enrollment
+ @topic.transaction do
+ @topic.content = 'foo'
+ @topic.save!
+ end
+ assert @topic.history.empty?
+ end
+
+ def test_commit_run_transactions_callbacks_with_explicit_enrollment
+ @topic.transaction do
+ 2.times do
+ @topic.content = 'foo'
+ @topic.save!
+ end
+ @topic.class.connection.add_transaction_record(@topic)
+ end
+ assert_equal [:before_commit, :after_commit], @topic.history
+ end
+
+ def test_rollback_does_not_run_transactions_callbacks_without_enrollment
+ @topic.transaction do
+ @topic.content = 'foo'
+ @topic.save!
+ raise ActiveRecord::Rollback
+ end
+ assert @topic.history.empty?
+ end
+
+ def test_rollback_run_transactions_callbacks_with_explicit_enrollment
+ @topic.transaction do
+ 2.times do
+ @topic.content = 'foo'
+ @topic.save!
+ end
+ @topic.class.connection.add_transaction_record(@topic)
+ raise ActiveRecord::Rollback
+ end
+ assert_equal [:rollback], @topic.history
+ end
+end
diff --git a/activerecord/test/schema/postgresql_specific_schema.rb b/activerecord/test/schema/postgresql_specific_schema.rb
index 77d2f7fda2..f84be0e7f4 100644
--- a/activerecord/test/schema/postgresql_specific_schema.rb
+++ b/activerecord/test/schema/postgresql_specific_schema.rb
@@ -88,16 +88,6 @@ _SQL
end
end
- begin
- execute <<_SQL
- CREATE TABLE postgresql_xml_data_type (
- id SERIAL PRIMARY KEY,
- data xml
- );
-_SQL
- rescue #This version of PostgreSQL either has no XML support or is was not compiled with XML support: skipping table
- end
-
# This table is to verify if the :limit option is being ignored for text and binary columns
create_table :limitless_fields, force: true do |t|
t.binary :binary, limit: 100_000
diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb
index 08a16d5c9e..5e5f7a798e 100644
--- a/activerecord/test/schema/schema.rb
+++ b/activerecord/test/schema/schema.rb
@@ -680,6 +680,7 @@ ActiveRecord::Schema.define do
create_table :ship_parts, force: true do |t|
t.string :name
t.integer :ship_id
+ t.datetime :updated_at
end
create_table :speedometers, force: true, id: false do |t|
diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md
index 361571db98..35be823ea1 100644
--- a/activesupport/CHANGELOG.md
+++ b/activesupport/CHANGELOG.md
@@ -1,3 +1,8 @@
+* Fixed a problem where String#truncate_words would get stuck with a complex
+ string.
+
+ *Henrik Nygren*
+
* Fixed a roundtrip problem with AS::SafeBuffer where primitive-like strings
will be dumped as primitives:
diff --git a/activesupport/lib/active_support/cache.rb b/activesupport/lib/active_support/cache.rb
index 3a1e1ac3a1..fdef9cbfc0 100644
--- a/activesupport/lib/active_support/cache.rb
+++ b/activesupport/lib/active_support/cache.rb
@@ -325,19 +325,22 @@ module ActiveSupport
def read_multi(*names)
options = names.extract_options!
options = merged_options(options)
- results = {}
- names.each do |name|
- key = namespaced_key(name, options)
- entry = read_entry(key, options)
- if entry
- if entry.expired?
- delete_entry(key, options)
- else
- results[name] = entry.value
+
+ instrument_multi(:read, names, options) do |payload|
+ results = {}
+ names.each do |name|
+ key = namespaced_key(name, options)
+ entry = read_entry(key, options)
+ if entry
+ if entry.expired?
+ delete_entry(key, options)
+ else
+ results[name] = entry.value
+ end
end
end
+ results
end
- results
end
# Fetches data from the cache, using the given keys. If there is data in
@@ -527,16 +530,27 @@ module ActiveSupport
end
def instrument(operation, key, options = nil)
- log(operation, key, options)
+ log { "Cache #{operation}: #{key}#{options.blank? ? "" : " (#{options.inspect})"}" }
payload = { :key => key }
payload.merge!(options) if options.is_a?(Hash)
ActiveSupport::Notifications.instrument("cache_#{operation}.active_support", payload){ yield(payload) }
end
- def log(operation, key, options = nil)
+ def instrument_multi(operation, keys, options = nil)
+ log do
+ formatted_keys = keys.map { |k| "- #{k}" }.join("\n")
+ "Caches multi #{operation}:\n#{formatted_keys}#{options.blank? ? "" : " (#{options.inspect})"}"
+ end
+
+ payload = { key: keys }
+ payload.merge!(options) if options.is_a?(Hash)
+ ActiveSupport::Notifications.instrument("cache_#{operation}_multi.active_support", payload) { yield(payload) }
+ end
+
+ def log
return unless logger && logger.debug? && !silence?
- logger.debug("Cache #{operation}: #{key}#{options.blank? ? "" : " (#{options.inspect})"}")
+ logger.debug(yield)
end
def find_cached_entry(key, name, options)
diff --git a/activesupport/lib/active_support/cache/mem_cache_store.rb b/activesupport/lib/active_support/cache/mem_cache_store.rb
index 61b4f0b8b0..73ae3acea5 100644
--- a/activesupport/lib/active_support/cache/mem_cache_store.rb
+++ b/activesupport/lib/active_support/cache/mem_cache_store.rb
@@ -66,14 +66,17 @@ module ActiveSupport
def read_multi(*names)
options = names.extract_options!
options = merged_options(options)
- keys_to_names = Hash[names.map{|name| [escape_key(namespaced_key(name, options)), name]}]
- raw_values = @data.get_multi(keys_to_names.keys, :raw => true)
- values = {}
- raw_values.each do |key, value|
- entry = deserialize_entry(value)
- values[keys_to_names[key]] = entry.value unless entry.expired?
+
+ instrument_multi(:read, names, options) do
+ keys_to_names = Hash[names.map{|name| [escape_key(namespaced_key(name, options)), name]}]
+ raw_values = @data.get_multi(keys_to_names.keys, :raw => true)
+ values = {}
+ raw_values.each do |key, value|
+ entry = deserialize_entry(value)
+ values[keys_to_names[key]] = entry.value unless entry.expired?
+ end
+ values
end
- values
end
# Increment a cached value. This method uses the memcached incr atomic
diff --git a/activesupport/lib/active_support/core_ext/string/filters.rb b/activesupport/lib/active_support/core_ext/string/filters.rb
index 096292dc58..b88976eab2 100644
--- a/activesupport/lib/active_support/core_ext/string/filters.rb
+++ b/activesupport/lib/active_support/core_ext/string/filters.rb
@@ -93,7 +93,7 @@ class String
def truncate_words(words_count, options = {})
sep = options[:separator] || /\s+/
sep = Regexp.escape(sep.to_s) unless Regexp === sep
- if self =~ /\A((?:.+?#{sep}){#{words_count - 1}}.+?)#{sep}.*/m
+ if self =~ /\A((?>.+?#{sep}){#{words_count - 1}}.+?)#{sep}.*/m
$1 + (options[:omission] || '...')
else
dup
diff --git a/activesupport/test/caching_test.rb b/activesupport/test/caching_test.rb
index 98f3ea7a14..095e908907 100644
--- a/activesupport/test/caching_test.rb
+++ b/activesupport/test/caching_test.rb
@@ -1021,6 +1021,15 @@ class CacheStoreLoggerTest < ActiveSupport::TestCase
@cache.mute { @cache.fetch('foo') { 'bar' } }
assert @buffer.string.blank?
end
+
+ def test_multi_read_loggin
+ @cache.write 'hello', 'goodbye'
+ @cache.write 'world', 'earth'
+
+ @cache.read_multi('hello', 'world')
+
+ assert_match "Caches multi read:\n- hello\n- world", @buffer.string.tap { |l| p l }
+ end
end
class CacheEntryTest < ActiveSupport::TestCase
diff --git a/activesupport/test/core_ext/string_ext_test.rb b/activesupport/test/core_ext/string_ext_test.rb
index 24037c665a..cb24147ab3 100644
--- a/activesupport/test/core_ext/string_ext_test.rb
+++ b/activesupport/test/core_ext/string_ext_test.rb
@@ -249,6 +249,15 @@ class StringInflectionsTest < ActiveSupport::TestCase
assert_equal "Hello<br>Big<br>World!", "Hello<br>Big<br>World!".truncate_words(3, :omission => "[...]", :separator => '<br>')
end
+ def test_truncate_words_with_complex_string
+ Timeout.timeout(10) do
+ complex_string = "aa aa aaa aa aaa aaa aaa aa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaaa aaaaa aaaaa aaaaaa aa aa aa aaa aa aaa aa aa aa aa a aaa aaa \n a aaa <<s"
+ assert_equal complex_string.truncate_words(80), complex_string
+ end
+ rescue Timeout::Error
+ assert false
+ end
+
def test_truncate_multibyte
assert_equal "\354\225\204\353\246\254\353\236\221 \354\225\204\353\246\254 ...".force_encoding(Encoding::UTF_8),
"\354\225\204\353\246\254\353\236\221 \354\225\204\353\246\254 \354\225\204\353\235\274\353\246\254\354\230\244".force_encoding(Encoding::UTF_8).truncate(10)
diff --git a/guides/rails_guides/levenshtein.rb b/guides/rails_guides/levenshtein.rb
index 8a908a4339..36183fd321 100644
--- a/guides/rails_guides/levenshtein.rb
+++ b/guides/rails_guides/levenshtein.rb
@@ -7,11 +7,9 @@ module RailsGuides
t = str2
n = s.length
m = t.length
- max = n/2
return m if (0 == n)
return n if (0 == m)
- return n if (n - m).abs > max
d = (0..m).to_a
x = nil
diff --git a/guides/source/active_support_core_extensions.md b/guides/source/active_support_core_extensions.md
index 0fbd6ed7e1..0594b701dc 100644
--- a/guides/source/active_support_core_extensions.md
+++ b/guides/source/active_support_core_extensions.md
@@ -727,7 +727,7 @@ NOTE: Defined in `active_support/core_ext/module/introspection.rb`.
#### Qualified Constant Names
-The standard methods `const_defined?`, `const_get` , and `const_set` accept
+The standard methods `const_defined?`, `const_get`, and `const_set` accept
bare constant names. Active Support extends this API to be able to pass
relative qualified constant names.
diff --git a/guides/source/association_basics.md b/guides/source/association_basics.md
index 0079049d44..d6b2e75e1e 100644
--- a/guides/source/association_basics.md
+++ b/guides/source/association_basics.md
@@ -959,7 +959,8 @@ If you set the `:validate` option to `true`, then associated objects will be val
##### `:optional`
-If you set the `:optional` option to `true`, then associated object will be validated for presence. By default, this is `false`: associated objects will be validated for presence.
+If you set the `:optional` option to `true`, then the presence of the associated
+object won't be validated. By default, this option is set to `false`.
#### Scopes for `belongs_to`
diff --git a/guides/source/command_line.md b/guides/source/command_line.md
index 19ccdc5488..6f5a6b7957 100644
--- a/guides/source/command_line.md
+++ b/guides/source/command_line.md
@@ -338,6 +338,12 @@ You can specify the environment in which the `runner` command should operate usi
$ bin/rails runner -e staging "Model.long_running_method"
```
+You can even execute ruby code written in a file with runner.
+
+```bash
+$ bin/rails runner lib/code_to_be_run.rb
+```
+
### `rails destroy`
Think of `destroy` as the opposite of `generate`. It'll figure out what generate did, and undo it.
diff --git a/guides/source/layouts_and_rendering.md b/guides/source/layouts_and_rendering.md
index 329d501ce0..b0e71035c0 100644
--- a/guides/source/layouts_and_rendering.md
+++ b/guides/source/layouts_and_rendering.md
@@ -563,6 +563,42 @@ In this application:
* `OldArticlesController#show` will use no layout at all
* `OldArticlesController#index` will use the `old` layout
+##### Template Inheritance
+
+Similarly to the Layout Inheritance logic, if a template or partial is not found in the conventional path, the controller will look for a template or partial to render in its inheritance chain. For example:
+
+```ruby
+# in app/controllers/application_controller
+class ApplicationController < ActionController::Base
+end
+
+# in app/controllers/admin_controller
+class AdminController < ApplicationController
+end
+
+# in app/controllers/admin/products_controller
+class Admin::ProductsController < AdminController
+ def index
+ end
+end
+```
+
+The lookup order for a `admin/products#index` action will be:
+
+* `app/views/admin/products/`
+* `app/views/admin/`
+* `app/views/application/`
+
+This makes `app/views/application/` a great place for your shared partials, which can then be rendered in your ERb as such:
+
+```erb
+<%# app/views/admin/products/index.html.erb %>
+<%= render @products || "empty_list" %>
+
+<%# app/views/application/_empty_list.html.erb %>
+There are no items in this list <em>yet</em>.
+```
+
#### Avoiding Double Render Errors
Sooner or later, most Rails developers will see the error message "Can only render or redirect once per action". While this is annoying, it's relatively easy to fix. Usually it happens because of a fundamental misunderstanding of the way that `render` works.
diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md
index df9f8fe993..8306233dcd 100644
--- a/railties/CHANGELOG.md
+++ b/railties/CHANGELOG.md
@@ -1,3 +1,10 @@
+* Set Rails console to use log formatter and log level as specified for the
+ given environment.
+
+ Fixes #15470.
+
+ *Jacob Evelyn*
+
* Add `config/initializers/active_record_belongs_to_required_by_default.rb`
Newly generated Rails apps have a new initializer called
diff --git a/railties/lib/rails/paths.rb b/railties/lib/rails/paths.rb
index 5458036219..ebcaaaba46 100644
--- a/railties/lib/rails/paths.rb
+++ b/railties/lib/rails/paths.rb
@@ -7,7 +7,7 @@ module Rails
# root = Root.new "/rails"
# root.add "app/controllers", eager_load: true
#
- # The command above creates a new root object and add "app/controllers" as a path.
+ # The command above creates a new root object and adds "app/controllers" as a path.
# This means we can get a <tt>Rails::Paths::Path</tt> object back like below:
#
# path = root["app/controllers"]