aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.github/issue_template.md9
-rw-r--r--.github/pull_request_template.md8
-rw-r--r--README.md4
-rw-r--r--actioncable/app/javascript/action_cable/connection_monitor.js7
-rw-r--r--actionmailer/CHANGELOG.md10
-rw-r--r--actionmailer/lib/action_mailer/base.rb11
-rw-r--r--actionmailer/lib/action_mailer/test_helper.rb19
-rw-r--r--actionmailer/test/base_test.rb6
-rw-r--r--actionmailer/test/mailers/base_mailer.rb7
-rw-r--r--actionmailer/test/test_helper_test.rb51
-rw-r--r--actionpack/CHANGELOG.md7
-rw-r--r--actionpack/lib/action_dispatch/journey/router.rb3
-rw-r--r--actionpack/lib/action_dispatch/routing/mapper.rb16
-rw-r--r--actionpack/test/controller/resources_test.rb22
-rw-r--r--actionpack/test/dispatch/routing_test.rb16
-rw-r--r--actionview/CHANGELOG.md18
-rw-r--r--actionview/lib/action_view/helpers/text_helper.rb2
-rw-r--r--actionview/test/template/text_helper_test.rb4
-rw-r--r--activejob/CHANGELOG.md12
-rw-r--r--activejob/README.md10
-rw-r--r--activejob/activejob.gemspec2
-rw-r--r--activejob/lib/active_job/arguments.rb12
-rw-r--r--activejob/lib/active_job/base.rb2
-rw-r--r--activejob/lib/active_job/callbacks.rb2
-rw-r--r--activejob/lib/active_job/core.rb11
-rw-r--r--activejob/lib/active_job/exceptions.rb8
-rw-r--r--activejob/lib/active_job/execution.rb2
-rw-r--r--activejob/lib/active_job/queue_adapters.rb6
-rw-r--r--activejob/lib/active_job/queue_adapters/test_adapter.rb10
-rw-r--r--activejob/lib/active_job/test_helper.rb34
-rw-r--r--activejob/test/cases/argument_serialization_test.rb10
-rw-r--r--activejob/test/cases/exceptions_test.rb22
-rw-r--r--activejob/test/cases/test_helper_test.rb56
-rw-r--r--activejob/test/jobs/retry_job.rb9
-rw-r--r--activejob/test/support/stubs/strong_parameters.rb15
-rw-r--r--activemodel/CHANGELOG.md4
-rw-r--r--activemodel/lib/active_model/dirty.rb4
-rw-r--r--activemodel/lib/active_model/errors.rb11
-rw-r--r--activemodel/test/cases/errors_test.rb24
-rw-r--r--activerecord/CHANGELOG.md20
-rw-r--r--activerecord/lib/active_record/associations/collection_association.rb6
-rw-r--r--activerecord/lib/active_record/associations/has_many_association.rb8
-rw-r--r--activerecord/lib/active_record/associations/has_many_through_association.rb22
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_adapter.rb18
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb30
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb16
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb47
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb10
-rw-r--r--activerecord/lib/active_record/connection_handling.rb2
-rw-r--r--activerecord/lib/active_record/core.rb4
-rw-r--r--activerecord/lib/active_record/errors.rb10
-rw-r--r--activerecord/lib/active_record/integration.rb2
-rw-r--r--activerecord/lib/active_record/migration.rb10
-rw-r--r--activerecord/lib/active_record/query_cache.rb15
-rw-r--r--activerecord/lib/active_record/relation/where_clause.rb4
-rw-r--r--activerecord/lib/arel/nodes/equality.rb7
-rw-r--r--activerecord/lib/arel/predications.rb8
-rw-r--r--activerecord/lib/arel/visitors/depth_first.rb2
-rw-r--r--activerecord/lib/arel/visitors/dot.rb2
-rw-r--r--activerecord/lib/arel/visitors/ibm_db.rb6
-rw-r--r--activerecord/lib/arel/visitors/mssql.rb25
-rw-r--r--activerecord/lib/arel/visitors/mysql.rb11
-rw-r--r--activerecord/lib/arel/visitors/oracle.rb6
-rw-r--r--activerecord/lib/arel/visitors/oracle12.rb6
-rw-r--r--activerecord/lib/arel/visitors/postgresql.rb12
-rw-r--r--activerecord/lib/arel/visitors/sqlite.rb12
-rw-r--r--activerecord/lib/arel/visitors/to_sql.rb33
-rw-r--r--activerecord/test/cases/adapter_test.rb12
-rw-r--r--activerecord/test/cases/adapters/postgresql/composite_test.rb14
-rw-r--r--activerecord/test/cases/adapters/postgresql/connection_test.rb2
-rw-r--r--activerecord/test/cases/adapters/postgresql/enum_test.rb2
-rw-r--r--activerecord/test/cases/adapters/postgresql/foreign_table_test.rb8
-rw-r--r--activerecord/test/cases/adapters/postgresql/range_test.rb2
-rw-r--r--activerecord/test/cases/adapters/postgresql/rename_table_test.rb2
-rw-r--r--activerecord/test/cases/adapters/postgresql/schema_test.rb14
-rw-r--r--activerecord/test/cases/adapters/postgresql/uuid_test.rb8
-rw-r--r--activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb14
-rw-r--r--activerecord/test/cases/arel/visitors/ibm_db_test.rb39
-rw-r--r--activerecord/test/cases/arel/visitors/informix_test.rb39
-rw-r--r--activerecord/test/cases/arel/visitors/mssql_test.rb39
-rw-r--r--activerecord/test/cases/arel/visitors/mysql_test.rb39
-rw-r--r--activerecord/test/cases/arel/visitors/oracle12_test.rb39
-rw-r--r--activerecord/test/cases/arel/visitors/oracle_test.rb39
-rw-r--r--activerecord/test/cases/arel/visitors/postgres_test.rb39
-rw-r--r--activerecord/test/cases/arel/visitors/sqlite_test.rb45
-rw-r--r--activerecord/test/cases/arel/visitors/to_sql_test.rb37
-rw-r--r--activerecord/test/cases/associations/has_many_through_associations_test.rb10
-rw-r--r--activerecord/test/cases/attribute_methods_test.rb2
-rw-r--r--activerecord/test/cases/calculations_test.rb4
-rw-r--r--activerecord/test/cases/connection_adapters/connection_handlers_multi_db_test.rb72
-rw-r--r--activerecord/test/cases/query_cache_test.rb16
-rw-r--r--activerecord/test/cases/relation/where_clause_test.rb4
-rw-r--r--activerecord/test/cases/statement_invalid_test.rb42
-rw-r--r--activerecord/test/cases/view_test.rb6
-rw-r--r--activerecord/test/schema/mysql2_specific_schema.rb42
-rw-r--r--activerecord/test/schema/oracle_specific_schema.rb24
-rw-r--r--activesupport/lib/active_support/core_ext/file/atomic.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/hash/slice.rb5
-rw-r--r--activesupport/lib/active_support/notifications/instrumenter.rb15
-rw-r--r--activesupport/lib/active_support/testing/parallelization.rb4
-rw-r--r--activesupport/test/core_ext/file_test.rb14
-rw-r--r--activesupport/test/dependencies_test.rb4
-rw-r--r--guides/assets/stylesheets/main.rtl.css762
-rw-r--r--guides/rails_guides.rb13
-rw-r--r--guides/rails_guides/generator.rb25
-rw-r--r--guides/source/active_job_basics.md2
-rw-r--r--guides/source/caching_with_rails.md6
-rw-r--r--guides/source/command_line.md50
-rw-r--r--guides/source/configuring.md2
-rw-r--r--guides/source/development_dependencies_install.md2
-rw-r--r--railties/lib/rails/command.rb19
-rw-r--r--railties/lib/rails/generators/rails/app/templates/bin/setup.tt2
-rw-r--r--railties/lib/rails/generators/rails/app/templates/test/test_helper.rb.tt2
-rw-r--r--railties/test/application/rake_test.rb5
-rw-r--r--railties/test/application/test_runner_test.rb14
-rw-r--r--railties/test/generators/actions_test.rb8
118 files changed, 2160 insertions, 295 deletions
diff --git a/.github/issue_template.md b/.github/issue_template.md
index 2ff6a271db..87e666a7dd 100644
--- a/.github/issue_template.md
+++ b/.github/issue_template.md
@@ -1,13 +1,12 @@
### Steps to reproduce
-
-(Guidelines for creating a bug report are [available
-here](http://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html#creating-a-bug-report))
+<!-- (Guidelines for creating a bug report are [available
+here](http://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html#creating-a-bug-report)) -->
### Expected behavior
-Tell us what should happen
+<!-- Tell us what should happen -->
### Actual behavior
-Tell us what happens instead
+<!-- Tell us what happens instead -->
### System configuration
**Rails version**:
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index a36687ec99..85c0e29ff0 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -1,13 +1,13 @@
### Summary
-Provide a general description of the code changes in your pull
+<!-- Provide a general description of the code changes in your pull
request... were there any bugs you had fixed? If so, mention them. If
these bugs have open GitHub issues, be sure to tag them here as well,
-to keep the conversation linked together.
+to keep the conversation linked together. -->
### Other Information
-If there's anything else that's important and relevant to your pull
+<!-- If there's anything else that's important and relevant to your pull
request, mention that information here. This could include
benchmarks, or other information.
@@ -18,4 +18,4 @@ Finally, if your pull request affects documentation or any non-code
changes, guidelines for those changes are [available
here](http://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html#contributing-to-the-rails-documentation)
-Thanks for contributing to Rails!
+Thanks for contributing to Rails! -->
diff --git a/README.md b/README.md
index 4854d7de0e..56d2b9909c 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# Welcome to Rails
-## What's Rails
+## What's Rails?
Rails is a web-application framework that includes everything needed to
create database-backed web applications according to the
@@ -45,7 +45,7 @@ or to generate the body of an email. In Rails, View generation is handled by [Ac
[Active Record](activerecord/README.rdoc), [Active Model](activemodel/README.rdoc), [Action Pack](actionpack/README.rdoc), and [Action View](actionview/README.rdoc) can each be used independently outside Rails.
In addition to that, Rails also comes with [Action Mailer](actionmailer/README.rdoc), a library
to generate and send emails; [Active Job](activejob/README.md), a
-framework for declaring jobs and making them run on a variety of queueing
+framework for declaring jobs and making them run on a variety of queuing
backends; [Action Cable](actioncable/README.md), a framework to
integrate WebSockets with a Rails application; [Active Storage](activestorage/README.md), a library to attach cloud
and local files to Rails applications;
diff --git a/actioncable/app/javascript/action_cable/connection_monitor.js b/actioncable/app/javascript/action_cable/connection_monitor.js
index 8d6ac1f682..cd1e4602d8 100644
--- a/actioncable/app/javascript/action_cable/connection_monitor.js
+++ b/actioncable/app/javascript/action_cable/connection_monitor.js
@@ -75,8 +75,8 @@ class ConnectionMonitor {
}
getPollInterval() {
- const {min, max} = this.constructor.pollInterval
- const interval = 5 * Math.log(this.reconnectAttempts + 1)
+ const {min, max, multiplier} = this.constructor.pollInterval
+ const interval = multiplier * Math.log(this.reconnectAttempts + 1)
return Math.round(clamp(interval, min, max) * 1000)
}
@@ -117,7 +117,8 @@ class ConnectionMonitor {
ConnectionMonitor.pollInterval = {
min: 3,
- max: 30
+ max: 30,
+ multiplier: 5
}
ConnectionMonitor.staleThreshold = 6 // Server::Connections::BEAT_INTERVAL * 2 (missed two pings)
diff --git a/actionmailer/CHANGELOG.md b/actionmailer/CHANGELOG.md
index db6fc7ee9c..ec6f249974 100644
--- a/actionmailer/CHANGELOG.md
+++ b/actionmailer/CHANGELOG.md
@@ -1,3 +1,13 @@
+* Fix ActionMailer assertions not working when a Mail defines
+ a custom delivery job class
+
+ *Edouard Chin*
+
+* Mails with multipart `format` blocks with implicit render now also check for
+ a template name in options hash instead of only using the action name.
+
+ *Marcus Ilgner*
+
* Allow ActionMailer classes to configure the parameterized delivery job
Example:
```
diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb
index 509d859ac3..2bb1078bb8 100644
--- a/actionmailer/lib/action_mailer/base.rb
+++ b/actionmailer/lib/action_mailer/base.rb
@@ -942,9 +942,7 @@ module ActionMailer
def collect_responses(headers)
if block_given?
- collector = ActionMailer::Collector.new(lookup_context) { render(action_name) }
- yield(collector)
- collector.responses
+ collect_responses_from_block(headers, &Proc.new)
elsif headers[:body]
collect_responses_from_text(headers)
else
@@ -952,6 +950,13 @@ module ActionMailer
end
end
+ def collect_responses_from_block(headers)
+ templates_name = headers[:template_name] || action_name
+ collector = ActionMailer::Collector.new(lookup_context) { render(templates_name) }
+ yield(collector)
+ collector.responses
+ end
+
def collect_responses_from_text(headers)
[{
body: headers.delete(:body),
diff --git a/actionmailer/lib/action_mailer/test_helper.rb b/actionmailer/lib/action_mailer/test_helper.rb
index a4751916af..b3403e8331 100644
--- a/actionmailer/lib/action_mailer/test_helper.rb
+++ b/actionmailer/lib/action_mailer/test_helper.rb
@@ -34,7 +34,7 @@ module ActionMailer
def assert_emails(number, &block)
if block_given?
original_count = ActionMailer::Base.deliveries.size
- perform_enqueued_jobs(only: [ActionMailer::DeliveryJob, ActionMailer::Parameterized::DeliveryJob], &block)
+ perform_enqueued_jobs(only: ->(job) { delivery_job_filter(job) }, &block)
new_count = ActionMailer::Base.deliveries.size
assert_equal number, new_count - original_count, "#{number} emails expected, but #{new_count - original_count} were sent"
else
@@ -90,7 +90,7 @@ module ActionMailer
# end
# end
def assert_enqueued_emails(number, &block)
- assert_enqueued_jobs number, only: [ ActionMailer::DeliveryJob, ActionMailer::Parameterized::DeliveryJob ], &block
+ assert_enqueued_jobs(number, only: ->(job) { delivery_job_filter(job) }, &block)
end
# Asserts that a specific email has been enqueued, optionally
@@ -125,10 +125,10 @@ module ActionMailer
# end
def assert_enqueued_email_with(mailer, method, args: nil, queue: "mailers", &block)
if args.is_a? Hash
- job = ActionMailer::Parameterized::DeliveryJob
+ job = mailer.parameterized_delivery_job
args = [mailer.to_s, method.to_s, "deliver_now", args]
else
- job = ActionMailer::DeliveryJob
+ job = mailer.delivery_job
args = [mailer.to_s, method.to_s, "deliver_now", *args]
end
@@ -151,7 +151,16 @@ module ActionMailer
# end
# end
def assert_no_enqueued_emails(&block)
- assert_no_enqueued_jobs only: [ ActionMailer::DeliveryJob, ActionMailer::Parameterized::DeliveryJob ], &block
+ assert_enqueued_emails 0, &block
end
+
+ private
+
+ def delivery_job_filter(job)
+ job_class = job.is_a?(Hash) ? job.fetch(:job) : job.class
+
+ Base.descendants.map(&:delivery_job).include?(job_class) ||
+ Base.descendants.map(&:parameterized_delivery_job).include?(job_class)
+ end
end
end
diff --git a/actionmailer/test/base_test.rb b/actionmailer/test/base_test.rb
index cbbee9fae8..86c0172772 100644
--- a/actionmailer/test/base_test.rb
+++ b/actionmailer/test/base_test.rb
@@ -544,6 +544,12 @@ class BaseTest < ActiveSupport::TestCase
assert_equal("TEXT Implicit Multipart", mail.text_part.body.decoded)
end
+ test "you can specify a different template for multipart render" do
+ mail = BaseMailer.implicit_different_template_with_block("explicit_multipart_templates").deliver
+ assert_equal("HTML Explicit Multipart Templates", mail.html_part.body.decoded)
+ assert_equal("TEXT Explicit Multipart Templates", mail.text_part.body.decoded)
+ end
+
test "should raise if missing template in implicit render" do
assert_raises ActionView::MissingTemplate do
BaseMailer.implicit_different_template("missing_template").deliver_now
diff --git a/actionmailer/test/mailers/base_mailer.rb b/actionmailer/test/mailers/base_mailer.rb
index a3101207dc..c1bb48cc96 100644
--- a/actionmailer/test/mailers/base_mailer.rb
+++ b/actionmailer/test/mailers/base_mailer.rb
@@ -111,6 +111,13 @@ class BaseMailer < ActionMailer::Base
mail(template_name: template_name)
end
+ def implicit_different_template_with_block(template_name = "")
+ mail(template_name: template_name) do |format|
+ format.text
+ format.html
+ end
+ end
+
def explicit_different_template(template_name = "")
mail do |format|
format.text { render template: "#{mailer_name}/#{template_name}" }
diff --git a/actionmailer/test/test_helper_test.rb b/actionmailer/test/test_helper_test.rb
index d31170706b..c700db9408 100644
--- a/actionmailer/test/test_helper_test.rb
+++ b/actionmailer/test/test_helper_test.rb
@@ -24,6 +24,17 @@ class TestHelperMailer < ActionMailer::Base
end
end
+class CustomDeliveryJob < ActionMailer::DeliveryJob
+end
+
+class CustomParameterizedDeliveryJob < ActionMailer::Parameterized::DeliveryJob
+end
+
+class CustomDeliveryMailer < TestHelperMailer
+ self.delivery_job = CustomDeliveryJob
+ self.parameterized_delivery_job = CustomParameterizedDeliveryJob
+end
+
class TestHelperMailerTest < ActionMailer::TestCase
include ActiveSupport::Testing::Stream
@@ -69,6 +80,26 @@ class TestHelperMailerTest < ActionMailer::TestCase
end
end
+ def test_assert_emails_with_custom_delivery_job
+ assert_nothing_raised do
+ assert_emails(1) do
+ silence_stream($stdout) do
+ CustomDeliveryMailer.test.deliver_later
+ end
+ end
+ end
+ end
+
+ def test_assert_emails_with_custom_parameterized_delivery_job
+ assert_nothing_raised do
+ assert_emails(1) do
+ silence_stream($stdout) do
+ CustomDeliveryMailer.with(foo: "bar").test_parameter_args.deliver_later
+ end
+ end
+ end
+ end
+
def test_assert_emails_with_enqueued_emails
assert_nothing_raised do
assert_emails 1 do
@@ -201,6 +232,16 @@ class TestHelperMailerTest < ActionMailer::TestCase
assert_match(/2 .* but 1/, error.message)
end
+ def test_assert_enqueued_emails_with_custom_delivery_job
+ assert_nothing_raised do
+ assert_enqueued_emails(1) do
+ silence_stream($stdout) do
+ CustomDeliveryMailer.test.deliver_later
+ end
+ end
+ end
+ end
+
def test_assert_enqueued_emails_too_many_sent
error = assert_raise ActiveSupport::TestCase::Assertion do
assert_enqueued_emails 1 do
@@ -252,6 +293,16 @@ class TestHelperMailerTest < ActionMailer::TestCase
end
end
+ def test_assert_enqueued_email_with_when_mailer_has_custom_delivery_job
+ assert_nothing_raised do
+ assert_enqueued_email_with CustomDeliveryMailer, :test do
+ silence_stream($stdout) do
+ CustomDeliveryMailer.test.deliver_later
+ end
+ end
+ end
+ end
+
def test_assert_enqueued_email_with_with_no_block
assert_nothing_raised do
silence_stream($stdout) do
diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md
index 90cf989100..740c6db06f 100644
--- a/actionpack/CHANGELOG.md
+++ b/actionpack/CHANGELOG.md
@@ -1,3 +1,10 @@
+* Raise an error on root route naming conflicts.
+
+ Raises an ArgumentError when multiple root routes are defined in the
+ same context instead of assigning nil names to subsequent roots.
+
+ *Gannon McGibbon*
+
* Allow rescue from parameter parse errors:
```
diff --git a/actionpack/lib/action_dispatch/journey/router.rb b/actionpack/lib/action_dispatch/journey/router.rb
index 30af3ff930..89a164f968 100644
--- a/actionpack/lib/action_dispatch/journey/router.rb
+++ b/actionpack/lib/action_dispatch/journey/router.rb
@@ -15,9 +15,6 @@ require "action_dispatch/journey/path/pattern"
module ActionDispatch
module Journey # :nodoc:
class Router # :nodoc:
- class RoutingError < ::StandardError # :nodoc:
- end
-
attr_accessor :routes
def initialize(routes)
diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb
index 99f3b4c2cd..421e2023c2 100644
--- a/actionpack/lib/action_dispatch/routing/mapper.rb
+++ b/actionpack/lib/action_dispatch/routing/mapper.rb
@@ -656,7 +656,7 @@ module ActionDispatch
# Query if the following named route was already defined.
def has_named_route?(name)
- @set.named_routes.key? name
+ @set.named_routes.key?(name)
end
private
@@ -1171,10 +1171,16 @@ module ActionDispatch
end
def actions
+ if @except
+ available_actions - Array(@except).map(&:to_sym)
+ else
+ available_actions
+ end
+ end
+
+ def available_actions
if @only
Array(@only).map(&:to_sym)
- elsif @except
- default_actions - Array(@except).map(&:to_sym)
else
default_actions
end
@@ -1946,9 +1952,7 @@ module ActionDispatch
end
def match_root_route(options)
- name = has_named_route?(name_for_action(:root, nil)) ? nil : :root
- args = ["/", { as: name, via: :get }.merge!(options)]
-
+ args = ["/", { as: :root, via: :get }.merge(options)]
match(*args)
end
end
diff --git a/actionpack/test/controller/resources_test.rb b/actionpack/test/controller/resources_test.rb
index d336b96eff..d2146f12a5 100644
--- a/actionpack/test/controller/resources_test.rb
+++ b/actionpack/test/controller/resources_test.rb
@@ -853,6 +853,28 @@ class ResourcesTest < ActionController::TestCase
end
end
+ def test_resource_has_show_action_but_does_not_have_destroy_action
+ with_routing do |set|
+ set.draw do
+ resources :products, only: [:show, :destroy], except: :destroy
+ end
+
+ assert_resource_allowed_routes("products", {}, { id: "1" }, :show, [:index, :new, :create, :edit, :update, :destroy])
+ assert_resource_allowed_routes("products", { format: "xml" }, { id: "1" }, :show, [:index, :new, :create, :edit, :update, :destroy])
+ end
+ end
+
+ def test_singleton_resource_has_show_action_but_does_not_have_destroy_action
+ with_routing do |set|
+ set.draw do
+ resource :account, only: [:show, :destroy], except: :destroy
+ end
+
+ assert_singleton_resource_allowed_routes("accounts", {}, :show, [:new, :create, :edit, :update, :destroy])
+ assert_singleton_resource_allowed_routes("accounts", { format: "xml" }, :show, [:new, :create, :edit, :update, :destroy])
+ end
+ end
+
def test_resource_has_only_create_action_and_named_route
with_routing do |set|
set.draw do
diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb
index affc2d8497..4dffbd0db1 100644
--- a/actionpack/test/dispatch/routing_test.rb
+++ b/actionpack/test/dispatch/routing_test.rb
@@ -3698,15 +3698,25 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
end
- def test_multiple_roots
+ def test_multiple_roots_raises_error
+ ex = assert_raises(ArgumentError) {
+ draw do
+ root "pages#index", constraints: { host: "www.example.com" }
+ root "admin/pages#index", constraints: { host: "admin.example.com" }
+ end
+ }
+ assert_match(/Invalid route name, already in use: 'root'/, ex.message)
+ end
+
+ def test_multiple_named_roots
draw do
namespace :foo do
root "pages#index", constraints: { host: "www.example.com" }
- root "admin/pages#index", constraints: { host: "admin.example.com" }
+ root "admin/pages#index", constraints: { host: "admin.example.com" }, as: :admin_root
end
root "pages#index", constraints: { host: "www.example.com" }
- root "admin/pages#index", constraints: { host: "admin.example.com" }
+ root "admin/pages#index", constraints: { host: "admin.example.com" }, as: :admin_root
end
get "http://www.example.com/foo"
diff --git a/actionview/CHANGELOG.md b/actionview/CHANGELOG.md
index 1f537e725a..237a25ba4f 100644
--- a/actionview/CHANGELOG.md
+++ b/actionview/CHANGELOG.md
@@ -1,3 +1,21 @@
+* Prevent `ActionView::TextHelper#word_wrap` from unexpectedly stripping white space from the _left_ side of lines.
+
+ For example, given input like this:
+
+ ```
+ This is a paragraph with an initial indent,
+ followed by additional lines that are not indented,
+ and finally terminated with a blockquote:
+ "A pithy saying"
+ ```
+
+ Calling `word_wrap` should not trim the indents on the first and last lines.
+
+ Fixes #34487
+
+ *Lyle Mullican*
+
+
* Add allocations to template rendering instrumentation.
Adds the allocations for template and partial rendering to the server output on render.
diff --git a/actionview/lib/action_view/helpers/text_helper.rb b/actionview/lib/action_view/helpers/text_helper.rb
index a338d076e4..3d378dcb2f 100644
--- a/actionview/lib/action_view/helpers/text_helper.rb
+++ b/actionview/lib/action_view/helpers/text_helper.rb
@@ -259,7 +259,7 @@ module ActionView
# # => Once\r\nupon\r\na\r\ntime
def word_wrap(text, line_width: 80, break_sequence: "\n")
text.split("\n").collect! do |line|
- line.length > line_width ? line.gsub(/(.{1,#{line_width}})(\s+|$)/, "\\1#{break_sequence}").strip : line
+ line.length > line_width ? line.gsub(/(.{1,#{line_width}})(\s+|$)/, "\\1#{break_sequence}").rstrip : line
end * break_sequence
end
diff --git a/actionview/test/template/text_helper_test.rb b/actionview/test/template/text_helper_test.rb
index 2925489f5d..e961a770e6 100644
--- a/actionview/test/template/text_helper_test.rb
+++ b/actionview/test/template/text_helper_test.rb
@@ -361,6 +361,10 @@ class TextHelperTest < ActionView::TestCase
assert_equal("my very very\nvery long\nstring\n\nwith another\nline", word_wrap("my very very very long string\n\nwith another line", line_width: 15))
end
+ def test_word_wrap_with_leading_spaces
+ assert_equal(" This is a paragraph\nthat includes some\nindented lines:\n Like this sample\n blockquote", word_wrap(" This is a paragraph that includes some\nindented lines:\n Like this sample\n blockquote", line_width: 25))
+ end
+
def test_word_wrap_does_not_modify_the_options_hash
options = { line_width: 15 }
passed_options = options.dup
diff --git a/activejob/CHANGELOG.md b/activejob/CHANGELOG.md
index 8bbecd5a5a..8e18faeb02 100644
--- a/activejob/CHANGELOG.md
+++ b/activejob/CHANGELOG.md
@@ -1,3 +1,15 @@
+* Keep executions for each specific declaration
+
+ Each `retry_on` declaration has now its own specific executions counter. Before it was
+ shared between all executions of a job.
+
+ *Alberto Almagro*
+
+* Allow all assertion helpers that have a `only` and `except` keyword to accept
+ Procs.
+
+ *Edouard Chin*
+
* Restore HashWithIndifferentAccess support to ActiveJob::Arguments.deserialize.
*Gannon McGibbon*
diff --git a/activejob/README.md b/activejob/README.md
index d49fcfe3c2..a2a5289ab7 100644
--- a/activejob/README.md
+++ b/activejob/README.md
@@ -1,7 +1,7 @@
# Active Job -- Make work happen later
Active Job is a framework for declaring jobs and making them run on a variety
-of queueing backends. These jobs can be everything from regularly scheduled
+of queuing backends. These jobs can be everything from regularly scheduled
clean-ups, to billing charges, to mailings. Anything that can be chopped up into
small units of work and run in parallel, really.
@@ -20,7 +20,7 @@ switch between them without having to rewrite your jobs.
## Usage
-To learn how to use your preferred queueing backend see its adapter
+To learn how to use your preferred queuing backend see its adapter
documentation at
[ActiveJob::QueueAdapters](http://api.rubyonrails.org/classes/ActiveJob/QueueAdapters.html).
@@ -39,7 +39,7 @@ end
Enqueue a job like so:
```ruby
-MyJob.perform_later record # Enqueue a job to be performed as soon as the queueing system is free.
+MyJob.perform_later record # Enqueue a job to be performed as soon as the queuing system is free.
```
```ruby
@@ -82,9 +82,9 @@ This works with any class that mixes in GlobalID::Identification, which
by default has been mixed into Active Record classes.
-## Supported queueing systems
+## Supported queuing systems
-Active Job has built-in adapters for multiple queueing backends (Sidekiq,
+Active Job has built-in adapters for multiple queuing backends (Sidekiq,
Resque, Delayed Job and others). To get an up-to-date list of the adapters
see the API Documentation for [ActiveJob::QueueAdapters](http://api.rubyonrails.org/classes/ActiveJob/QueueAdapters.html).
diff --git a/activejob/activejob.gemspec b/activejob/activejob.gemspec
index cd7ac210ec..20b9d4ccdd 100644
--- a/activejob/activejob.gemspec
+++ b/activejob/activejob.gemspec
@@ -7,7 +7,7 @@ Gem::Specification.new do |s|
s.name = "activejob"
s.version = version
s.summary = "Job framework with pluggable queues."
- s.description = "Declare job classes that can be run by a variety of queueing backends."
+ s.description = "Declare job classes that can be run by a variety of queuing backends."
s.required_ruby_version = ">= 2.4.1"
diff --git a/activejob/lib/active_job/arguments.rb b/activejob/lib/active_job/arguments.rb
index ffc57dae84..fa58c50ed0 100644
--- a/activejob/lib/active_job/arguments.rb
+++ b/activejob/lib/active_job/arguments.rb
@@ -75,14 +75,14 @@ module ActiveJob
when Array
argument.map { |arg| serialize_argument(arg) }
when ActiveSupport::HashWithIndifferentAccess
- result = serialize_hash(argument)
- result[WITH_INDIFFERENT_ACCESS_KEY] = serialize_argument(true)
- result
+ serialize_indifferent_hash(argument)
when Hash
symbol_keys = argument.each_key.grep(Symbol).map(&:to_s)
result = serialize_hash(argument)
result[SYMBOL_KEYS_KEY] = symbol_keys
result
+ when -> (arg) { arg.respond_to?(:permitted?) }
+ serialize_indifferent_hash(argument.to_h)
else
Serializers.serialize(argument)
end
@@ -148,6 +148,12 @@ module ActiveJob
end
end
+ def serialize_indifferent_hash(indifferent_hash)
+ result = serialize_hash(indifferent_hash)
+ result[WITH_INDIFFERENT_ACCESS_KEY] = serialize_argument(true)
+ result
+ end
+
def transform_symbol_keys(hash, symbol_keys)
# NOTE: HashWithIndifferentAccess#transform_keys always
# returns stringified keys with indifferent access
diff --git a/activejob/lib/active_job/base.rb b/activejob/lib/active_job/base.rb
index 95b1062701..ed41fac4b8 100644
--- a/activejob/lib/active_job/base.rb
+++ b/activejob/lib/active_job/base.rb
@@ -40,7 +40,7 @@ module ActiveJob #:nodoc:
# Records that are passed in are serialized/deserialized using Global
# ID. More information can be found in Arguments.
#
- # To enqueue a job to be performed as soon as the queueing system is free:
+ # To enqueue a job to be performed as soon as the queuing system is free:
#
# ProcessPhotoJob.perform_later(photo)
#
diff --git a/activejob/lib/active_job/callbacks.rb b/activejob/lib/active_job/callbacks.rb
index 334b24fb3b..61317c7cfc 100644
--- a/activejob/lib/active_job/callbacks.rb
+++ b/activejob/lib/active_job/callbacks.rb
@@ -130,7 +130,7 @@ module ActiveJob
set_callback(:enqueue, :after, *filters, &blk)
end
- # Defines a callback that will get called around the enqueueing
+ # Defines a callback that will get called around the enqueuing
# of the job.
#
# class VideoProcessJob < ActiveJob::Base
diff --git a/activejob/lib/active_job/core.rb b/activejob/lib/active_job/core.rb
index 62bb5861bb..4ab62f89b0 100644
--- a/activejob/lib/active_job/core.rb
+++ b/activejob/lib/active_job/core.rb
@@ -28,6 +28,12 @@ module ActiveJob
# Number of times this job has been executed (which increments on every retry, like after an exception).
attr_accessor :executions
+ # Hash that contains the number of times this job handled errors for each specific retry_on declaration.
+ # Keys are the string representation of the exceptions listed in the retry_on declaration,
+ # while its associated value holds the number of executions where the corresponding retry_on
+ # declaration handled one of its listed exceptions.
+ attr_accessor :exception_executions
+
# I18n.locale to be used during the job.
attr_accessor :locale
@@ -75,10 +81,11 @@ module ActiveJob
@queue_name = self.class.queue_name
@priority = self.class.priority
@executions = 0
+ @exception_executions = Hash.new(0)
end
# Returns a hash with the job data that can safely be passed to the
- # queueing adapter.
+ # queuing adapter.
def serialize
{
"job_class" => self.class.name,
@@ -88,6 +95,7 @@ module ActiveJob
"priority" => priority,
"arguments" => serialize_arguments_if_needed(arguments),
"executions" => executions,
+ "exception_executions" => exception_executions,
"locale" => I18n.locale.to_s,
"timezone" => Time.zone.try(:name)
}
@@ -126,6 +134,7 @@ module ActiveJob
self.priority = job_data["priority"]
self.serialized_arguments = job_data["arguments"]
self.executions = job_data["executions"]
+ self.exception_executions = job_data["exception_executions"]
self.locale = job_data["locale"] || I18n.locale.to_s
self.timezone = job_data["timezone"] || Time.zone.try(:name)
end
diff --git a/activejob/lib/active_job/exceptions.rb b/activejob/lib/active_job/exceptions.rb
index bc9e168971..53984a4e49 100644
--- a/activejob/lib/active_job/exceptions.rb
+++ b/activejob/lib/active_job/exceptions.rb
@@ -9,7 +9,6 @@ module ActiveJob
module ClassMethods
# Catch the exception and reschedule job for re-execution after so many seconds, for a specific number of attempts.
- # The number of attempts includes the total executions of a job, not just the retried executions.
# If the exception keeps getting raised beyond the specified number of attempts, the exception is allowed to
# bubble up to the underlying queuing system, which may have its own retry mechanism or place it in a
# holding queue for inspection.
@@ -22,8 +21,7 @@ module ActiveJob
# as a computing proc that the number of executions so far as an argument, or as a symbol reference of
# <tt>:exponentially_longer</tt>, which applies the wait algorithm of <tt>(executions ** 4) + 2</tt>
# (first wait 3s, then 18s, then 83s, etc)
- # * <tt>:attempts</tt> - Re-enqueues the job the specified number of times (default: 5 attempts),
- # attempts here refers to the total number of times the job is executed, not just retried executions
+ # * <tt>:attempts</tt> - Re-enqueues the job the specified number of times (default: 5 attempts)
# * <tt>:queue</tt> - Re-enqueues the job on a different queue
# * <tt>:priority</tt> - Re-enqueues the job with a different priority
#
@@ -46,7 +44,9 @@ module ActiveJob
# end
def retry_on(*exceptions, wait: 3.seconds, attempts: 5, queue: nil, priority: nil)
rescue_from(*exceptions) do |error|
- if executions < attempts
+ exception_executions[exceptions.to_s] += 1
+
+ if exception_executions[exceptions.to_s] < attempts
retry_job wait: determine_delay(wait), queue: queue, priority: priority, error: error
else
if block_given?
diff --git a/activejob/lib/active_job/execution.rb b/activejob/lib/active_job/execution.rb
index f5a343311f..e96dbcd4c9 100644
--- a/activejob/lib/active_job/execution.rb
+++ b/activejob/lib/active_job/execution.rb
@@ -26,7 +26,7 @@ module ActiveJob
end
end
- # Performs the job immediately. The job is not sent to the queueing adapter
+ # Performs the job immediately. The job is not sent to the queuing adapter
# but directly executed by blocking the execution of others until it's finished.
#
# MyJob.new(*args).perform_now
diff --git a/activejob/lib/active_job/queue_adapters.rb b/activejob/lib/active_job/queue_adapters.rb
index 3e3a474fbb..525e79e302 100644
--- a/activejob/lib/active_job/queue_adapters.rb
+++ b/activejob/lib/active_job/queue_adapters.rb
@@ -3,7 +3,7 @@
module ActiveJob
# == Active Job adapters
#
- # Active Job has adapters for the following queueing backends:
+ # Active Job has adapters for the following queuing backends:
#
# * {Backburner}[https://github.com/nesquena/backburner]
# * {Delayed Job}[https://github.com/collectiveidea/delayed_job]
@@ -52,7 +52,7 @@ module ActiveJob
#
# No: The adapter will run jobs at the next opportunity and cannot use perform_later.
#
- # N/A: The adapter does not support queueing.
+ # N/A: The adapter does not support queuing.
#
# NOTE:
# queue_classic supports job scheduling since version 3.1.
@@ -74,7 +74,7 @@ module ActiveJob
#
# No: Does not allow the priority of jobs to be configured.
#
- # N/A: The adapter does not support queueing, and therefore sorting them.
+ # N/A: The adapter does not support queuing, and therefore sorting them.
#
# ==== Timeout
#
diff --git a/activejob/lib/active_job/queue_adapters/test_adapter.rb b/activejob/lib/active_job/queue_adapters/test_adapter.rb
index f73ad444ba..c134257ebc 100644
--- a/activejob/lib/active_job/queue_adapters/test_adapter.rb
+++ b/activejob/lib/active_job/queue_adapters/test_adapter.rb
@@ -65,11 +65,17 @@ module ActiveJob
def filtered_job_class?(job)
if filter
- !Array(filter).include?(job.class)
+ !filter_as_proc(filter).call(job)
elsif reject
- Array(reject).include?(job.class)
+ filter_as_proc(reject).call(job)
end
end
+
+ def filter_as_proc(filter)
+ return filter if filter.is_a?(Proc)
+
+ ->(job) { Array(filter).include?(job.class) }
+ end
end
end
end
diff --git a/activejob/lib/active_job/test_helper.rb b/activejob/lib/active_job/test_helper.rb
index 0deb68d0d2..e0a71f4da8 100644
--- a/activejob/lib/active_job/test_helper.rb
+++ b/activejob/lib/active_job/test_helper.rb
@@ -107,6 +107,9 @@ module ActiveJob
# end
# end
#
+ # +:only+ and +:except+ options accepts Class, Array of Class or Proc. When passed a Proc,
+ # a hash containing the job's class and it's argument are passed as argument.
+ #
# Asserts the number of times a job is enqueued to a specific queue by passing +:queue+ option.
#
# def test_logging_job
@@ -163,6 +166,9 @@ module ActiveJob
# end
# end
#
+ # +:only+ and +:except+ options accepts Class, Array of Class or Proc. When passed a Proc,
+ # a hash containing the job's class and it's argument are passed as argument.
+ #
# Asserts that no jobs are enqueued to a specific queue by passing +:queue+ option
#
# def test_no_logging
@@ -243,6 +249,18 @@ module ActiveJob
# end
# end
#
+ # A proc may also be specified. When passed a Proc, the job's instance will be passed as argument.
+ #
+ # def test_hello_and_logging_jobs
+ # assert_nothing_raised do
+ # assert_performed_jobs(1, only: ->(job) { job.is_a?(HelloJob) }) do
+ # HelloJob.perform_later('jeremy')
+ # LoggingJob.perform_later('stewie')
+ # RescueJob.perform_later('david')
+ # end
+ # end
+ # end
+ #
# If the +:queue+ option is specified,
# then only the job(s) enqueued to a specific queue will be performed.
#
@@ -305,6 +323,9 @@ module ActiveJob
# end
# end
#
+ # +:only+ and +:except+ options accepts Class, Array of Class or Proc. When passed a Proc,
+ # an instance of the job will be passed as argument.
+ #
# If the +:queue+ option is specified,
# then only the job(s) enqueued to a specific queue will not be performed.
#
@@ -505,6 +526,9 @@ module ActiveJob
# assert_performed_jobs 1
# end
#
+ # +:only+ and +:except+ options accepts Class, Array of Class or Proc. When passed a Proc,
+ # an instance of the job will be passed as argument.
+ #
# If the +:queue+ option is specified,
# then only the job(s) enqueued to a specific queue will be performed.
#
@@ -569,9 +593,9 @@ module ActiveJob
job_class = job.fetch(:job)
if only
- next false unless Array(only).include?(job_class)
+ next false unless filter_as_proc(only).call(job)
elsif except
- next false if Array(except).include?(job_class)
+ next false if filter_as_proc(except).call(job)
end
if queue
@@ -584,6 +608,12 @@ module ActiveJob
end
end
+ def filter_as_proc(filter)
+ return filter if filter.is_a?(Proc)
+
+ ->(job) { Array(filter).include?(job.fetch(:job)) }
+ end
+
def enqueued_jobs_with(only: nil, except: nil, queue: nil, &block)
jobs_with(enqueued_jobs, only: only, except: except, queue: queue, &block)
end
diff --git a/activejob/test/cases/argument_serialization_test.rb b/activejob/test/cases/argument_serialization_test.rb
index 8b2981926f..e4e14016d9 100644
--- a/activejob/test/cases/argument_serialization_test.rb
+++ b/activejob/test/cases/argument_serialization_test.rb
@@ -5,6 +5,7 @@ require "active_job/arguments"
require "models/person"
require "active_support/core_ext/hash/indifferent_access"
require "jobs/kwargs_job"
+require "support/stubs/strong_parameters"
class ArgumentSerializationTest < ActiveSupport::TestCase
setup do
@@ -49,6 +50,15 @@ class ArgumentSerializationTest < ActiveSupport::TestCase
assert_arguments_roundtrip([a: 1, "b" => 2])
end
+ test "serialize a ActionController::Parameters" do
+ parameters = Parameters.new(a: 1)
+
+ assert_equal(
+ { "a" => 1, "_aj_hash_with_indifferent_access" => true },
+ ActiveJob::Arguments.serialize([parameters]).first
+ )
+ end
+
test "serialize a hash" do
symbol_key = { a: 1 }
string_key = { "a" => 1 }
diff --git a/activejob/test/cases/exceptions_test.rb b/activejob/test/cases/exceptions_test.rb
index 37bb65538a..b5328b8d6a 100644
--- a/activejob/test/cases/exceptions_test.rb
+++ b/activejob/test/cases/exceptions_test.rb
@@ -30,6 +30,28 @@ class ExceptionsTest < ActiveJob::TestCase
end
end
+ test "keeps the same attempts counter when several exceptions are listed in the same declaration" do
+ exceptions_to_raise = %w(FirstRetryableErrorOfTwo FirstRetryableErrorOfTwo FirstRetryableErrorOfTwo
+ SecondRetryableErrorOfTwo SecondRetryableErrorOfTwo)
+
+ assert_raises SecondRetryableErrorOfTwo do
+ perform_enqueued_jobs do
+ ExceptionRetryJob.perform_later(exceptions_to_raise)
+ end
+ end
+ end
+
+ test "keeps a separate attempts counter for each individual declaration" do
+ exceptions_to_raise = %w(FirstRetryableErrorOfTwo FirstRetryableErrorOfTwo FirstRetryableErrorOfTwo
+ DefaultsError DefaultsError)
+
+ assert_nothing_raised do
+ perform_enqueued_jobs do
+ ExceptionRetryJob.perform_later(exceptions_to_raise)
+ end
+ end
+ end
+
test "failed retry job when exception kept occurring against defaults" do
perform_enqueued_jobs do
begin
diff --git a/activejob/test/cases/test_helper_test.rb b/activejob/test/cases/test_helper_test.rb
index 8dc32037ff..046033921d 100644
--- a/activejob/test/cases/test_helper_test.rb
+++ b/activejob/test/cases/test_helper_test.rb
@@ -114,6 +114,16 @@ class EnqueuedJobsTest < ActiveJob::TestCase
end
end
+ def test_assert_enqueued_jobs_with_only_option_as_proc
+ assert_nothing_raised do
+ assert_enqueued_jobs(1, only: ->(job) { job.fetch(:job).name == "HelloJob" }) do
+ HelloJob.perform_later("jeremy")
+ LoggingJob.perform_later
+ LoggingJob.perform_later
+ end
+ end
+ end
+
def test_assert_enqueued_jobs_with_except_option
assert_nothing_raised do
assert_enqueued_jobs 1, except: LoggingJob do
@@ -124,6 +134,16 @@ class EnqueuedJobsTest < ActiveJob::TestCase
end
end
+ def test_assert_enqueued_jobs_with_except_option_as_proc
+ assert_nothing_raised do
+ assert_enqueued_jobs(1, except: ->(job) { job.fetch(:job).name == "LoggingJob" }) do
+ HelloJob.perform_later("jeremy")
+ LoggingJob.perform_later
+ LoggingJob.perform_later
+ end
+ end
+ end
+
def test_assert_enqueued_jobs_with_only_and_except_option
error = assert_raise ArgumentError do
assert_enqueued_jobs 1, only: HelloJob, except: HelloJob do
@@ -911,6 +931,15 @@ class PerformedJobsTest < ActiveJob::TestCase
end
end
+ def test_assert_performed_jobs_with_only_option_as_proc
+ assert_nothing_raised do
+ assert_performed_jobs(1, only: ->(job) { job.is_a?(HelloJob) }) do
+ HelloJob.perform_later("jeremy")
+ LoggingJob.perform_later("bogdan")
+ end
+ end
+ end
+
def test_assert_performed_jobs_without_block_with_only_option
HelloJob.perform_later("jeremy")
LoggingJob.perform_later("bogdan")
@@ -920,6 +949,15 @@ class PerformedJobsTest < ActiveJob::TestCase
assert_performed_jobs 1, only: HelloJob
end
+ def test_assert_performed_jobs_without_block_with_only_option_as_proc
+ HelloJob.perform_later("jeremy")
+ LoggingJob.perform_later("bogdan")
+
+ perform_enqueued_jobs
+
+ assert_performed_jobs(1, only: ->(job) { job.fetch(:job).name == "HelloJob" })
+ end
+
def test_assert_performed_jobs_without_block_with_only_option_failure
LoggingJob.perform_later("jeremy")
LoggingJob.perform_later("bogdan")
@@ -942,6 +980,15 @@ class PerformedJobsTest < ActiveJob::TestCase
end
end
+ def test_assert_performed_jobs_with_except_option_as_proc
+ assert_nothing_raised do
+ assert_performed_jobs(1, except: ->(job) { job.is_a?(HelloJob) }) do
+ HelloJob.perform_later("jeremy")
+ LoggingJob.perform_later("bogdan")
+ end
+ end
+ end
+
def test_assert_performed_jobs_without_block_with_except_option
HelloJob.perform_later("jeremy")
LoggingJob.perform_later("bogdan")
@@ -951,6 +998,15 @@ class PerformedJobsTest < ActiveJob::TestCase
assert_performed_jobs 1, except: HelloJob
end
+ def test_assert_performed_jobs_without_block_with_except_option_as_proc
+ HelloJob.perform_later("jeremy")
+ LoggingJob.perform_later("bogdan")
+
+ perform_enqueued_jobs
+
+ assert_performed_jobs(1, except: ->(job) { job.fetch(:job).name == "HelloJob" })
+ end
+
def test_assert_performed_jobs_without_block_with_except_option_failure
HelloJob.perform_later("jeremy")
HelloJob.perform_later("bogdan")
diff --git a/activejob/test/jobs/retry_job.rb b/activejob/test/jobs/retry_job.rb
index 68dc17e16c..2d19d4c41e 100644
--- a/activejob/test/jobs/retry_job.rb
+++ b/activejob/test/jobs/retry_job.rb
@@ -39,3 +39,12 @@ class RetryJob < ActiveJob::Base
end
end
end
+
+class ExceptionRetryJob < ActiveJob::Base
+ retry_on FirstRetryableErrorOfTwo, SecondRetryableErrorOfTwo, attempts: 4
+ retry_on DefaultsError
+
+ def perform(exceptions)
+ raise exceptions.shift.constantize.new unless exceptions.empty?
+ end
+end
diff --git a/activejob/test/support/stubs/strong_parameters.rb b/activejob/test/support/stubs/strong_parameters.rb
new file mode 100644
index 0000000000..acba3a4504
--- /dev/null
+++ b/activejob/test/support/stubs/strong_parameters.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class Parameters
+ def initialize(parameters = {})
+ @parameters = parameters.with_indifferent_access
+ end
+
+ def permitted?
+ true
+ end
+
+ def to_h
+ @parameters.to_h
+ end
+end
diff --git a/activemodel/CHANGELOG.md b/activemodel/CHANGELOG.md
index 6048911217..0bc8728e36 100644
--- a/activemodel/CHANGELOG.md
+++ b/activemodel/CHANGELOG.md
@@ -1,3 +1,7 @@
+* Add `#slice!` method to `ActiveModel::Errors`.
+
+ *Daniel López Prat*
+
* Fix numericality validator to still use value before type cast except Active Record.
Fixes #33651, #33686.
diff --git a/activemodel/lib/active_model/dirty.rb b/activemodel/lib/active_model/dirty.rb
index f0e1242554..0d9e761b1e 100644
--- a/activemodel/lib/active_model/dirty.rb
+++ b/activemodel/lib/active_model/dirty.rb
@@ -141,7 +141,9 @@ module ActiveModel
@mutations_from_database = nil
end
- def changes_applied # :nodoc:
+ # Clears dirty data and moves +changes+ to +previously_changed+ and
+ # +mutations_from_database+ to +mutations_before_last_save+ respectively.
+ def changes_applied
unless defined?(@attributes)
@previously_changed = changes
end
diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb
index 9de6b609a3..969effdc20 100644
--- a/activemodel/lib/active_model/errors.rb
+++ b/activemodel/lib/active_model/errors.rb
@@ -112,6 +112,17 @@ module ActiveModel
@details.merge!(other.details) { |_, ary1, ary2| ary1 + ary2 }
end
+ # Removes all errors except the given keys. Returns a hash containing the removed errors.
+ #
+ # person.errors.keys # => [:name, :age, :gender, :city]
+ # person.errors.slice!(:age, :gender) # => { :name=>["cannot be nil"], :city=>["cannot be nil"] }
+ # person.errors.keys # => [:age, :gender]
+ def slice!(*keys)
+ keys = keys.map(&:to_sym)
+ @details.slice!(*keys)
+ @messages.slice!(*keys)
+ end
+
# Clear the error messages.
#
# person.errors.full_messages # => ["name cannot be nil"]
diff --git a/activemodel/test/cases/errors_test.rb b/activemodel/test/cases/errors_test.rb
index 185b5a24ae..f9015b869d 100644
--- a/activemodel/test/cases/errors_test.rb
+++ b/activemodel/test/cases/errors_test.rb
@@ -411,6 +411,30 @@ class ErrorsTest < ActiveModel::TestCase
assert_equal({ name: [{ error: :blank }, { error: :invalid }] }, person.errors.details)
end
+ test "slice! removes all errors except the given keys" do
+ person = Person.new
+ person.errors.add(:name, "cannot be nil")
+ person.errors.add(:age, "cannot be nil")
+ person.errors.add(:gender, "cannot be nil")
+ person.errors.add(:city, "cannot be nil")
+
+ person.errors.slice!(:age, "gender")
+
+ assert_equal [:age, :gender], person.errors.keys
+ end
+
+ test "slice! returns the deleted errors" do
+ person = Person.new
+ person.errors.add(:name, "cannot be nil")
+ person.errors.add(:age, "cannot be nil")
+ person.errors.add(:gender, "cannot be nil")
+ person.errors.add(:city, "cannot be nil")
+
+ removed_errors = person.errors.slice!(:age, "gender")
+
+ assert_equal({ name: ["cannot be nil"], city: ["cannot be nil"] }, removed_errors)
+ end
+
test "errors are marshalable" do
errors = ActiveModel::Errors.new(Person.new)
errors.add(:name, :invalid)
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md
index 2a237f86cf..24a64e9deb 100644
--- a/activerecord/CHANGELOG.md
+++ b/activerecord/CHANGELOG.md
@@ -1,3 +1,23 @@
+* Move `ActiveRecord::StatementInvalid` SQL to error property and include binds as separate error property.
+
+ `ActiveRecord::ConnectionAdapters::AbstractAdapter#translate_exception_class` now requires `binds` to be passed as the last argument.
+
+ `ActiveRecord::ConnectionAdapters::AbstractAdapter#translate_exception` now requires `message`, `sql`, and `binds` to be passed as keyword arguments.
+
+ Subclasses of `ActiveRecord::StatementInvalid` must now provide `sql:` and `binds:` arguments to `super`.
+
+ Example:
+
+ ```
+ class MySubclassedError < ActiveRecord::StatementInvalid
+ def initialize(message, sql:, binds:)
+ super(message, sql: sql, binds: binds)
+ end
+ end
+ ```
+
+ *Gannon McGibbon*
+
* Add an `:if_not_exists` option to `create_table`.
Example:
diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb
index 840d900bbc..48bb9ab066 100644
--- a/activerecord/lib/active_record/associations/collection_association.rb
+++ b/activerecord/lib/active_record/associations/collection_association.rb
@@ -413,9 +413,9 @@ module ActiveRecord
end
def replace_records(new_target, original_target)
- delete(target - new_target)
+ delete(difference(target, new_target))
- unless concat(new_target - target)
+ unless concat(difference(new_target, target))
@target = original_target
raise RecordNotSaved, "Failed to replace #{reflection.name} because one or more of the " \
"new records could not be saved."
@@ -425,7 +425,7 @@ module ActiveRecord
end
def replace_common_records_in_memory(new_target, original_target)
- common_records = new_target & original_target
+ common_records = intersection(new_target, original_target)
common_records.each do |record|
skip_callbacks = true
replace_on_target(record, @target.index(record), skip_callbacks)
diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb
index cf85a87fa7..e224d3456a 100644
--- a/activerecord/lib/active_record/associations/has_many_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_association.rb
@@ -130,6 +130,14 @@ module ActiveRecord
end
saved_successfully
end
+
+ def difference(a, b)
+ a - b
+ end
+
+ def intersection(a, b)
+ a & b
+ end
end
end
end
diff --git a/activerecord/lib/active_record/associations/has_many_through_association.rb b/activerecord/lib/active_record/associations/has_many_through_association.rb
index f84ac65fa2..2322a49931 100644
--- a/activerecord/lib/active_record/associations/has_many_through_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_through_association.rb
@@ -163,6 +163,28 @@ module ActiveRecord
end
end
+ def difference(a, b)
+ distribution = distribution(b)
+
+ a.reject { |record| mark_occurrence(distribution, record) }
+ end
+
+ def intersection(a, b)
+ distribution = distribution(b)
+
+ a.select { |record| mark_occurrence(distribution, record) }
+ end
+
+ def mark_occurrence(distribution, record)
+ distribution[record] > 0 && distribution[record] -= 1
+ end
+
+ def distribution(array)
+ array.each_with_object(Hash.new(0)) do |record, distribution|
+ distribution[record] += 1
+ end
+ end
+
def through_records_for(record)
attributes = construct_join_attributes(record)
candidates = Array.wrap(through_association.target)
diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
index 0fe868478c..bd095fbc73 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
@@ -580,14 +580,12 @@ module ActiveRecord
$1.to_i if sql_type =~ /\((.*)\)/
end
- def translate_exception_class(e, sql)
- begin
- message = "#{e.class.name}: #{e.message}: #{sql}"
- rescue Encoding::CompatibilityError
- message = "#{e.class.name}: #{e.message.force_encoding sql.encoding}: #{sql}"
- end
+ def translate_exception_class(e, sql, binds)
+ message = "#{e.class.name}: #{e.message}"
- exception = translate_exception(e, message)
+ exception = translate_exception(
+ e, message: message, sql: sql, binds: binds
+ )
exception.set_backtrace e.backtrace
exception
end
@@ -606,18 +604,18 @@ module ActiveRecord
yield
end
rescue => e
- raise translate_exception_class(e, sql)
+ raise translate_exception_class(e, sql, binds)
end
end
end
- def translate_exception(exception, message)
+ def translate_exception(exception, message:, sql:, binds:)
# override in derived class
case exception
when RuntimeError
exception
else
- ActiveRecord::StatementInvalid.new(message)
+ ActiveRecord::StatementInvalid.new(message, sql: sql, binds: binds)
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
index 13c799b64a..2d7f34ef24 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
@@ -642,34 +642,34 @@ module ActiveRecord
ER_QUERY_INTERRUPTED = 1317
ER_QUERY_TIMEOUT = 3024
- def translate_exception(exception, message)
+ def translate_exception(exception, message:, sql:, binds:)
case error_number(exception)
when ER_DUP_ENTRY
- RecordNotUnique.new(message)
+ RecordNotUnique.new(message, sql: sql, binds: binds)
when ER_NO_REFERENCED_ROW, ER_ROW_IS_REFERENCED, ER_ROW_IS_REFERENCED_2, ER_NO_REFERENCED_ROW_2
- InvalidForeignKey.new(message)
+ InvalidForeignKey.new(message, sql: sql, binds: binds)
when ER_CANNOT_ADD_FOREIGN
- mismatched_foreign_key(message)
+ mismatched_foreign_key(message, sql: sql, binds: binds)
when ER_CANNOT_CREATE_TABLE
if message.include?("errno: 150")
- mismatched_foreign_key(message)
+ mismatched_foreign_key(message, sql: sql, binds: binds)
else
super
end
when ER_DATA_TOO_LONG
- ValueTooLong.new(message)
+ ValueTooLong.new(message, sql: sql, binds: binds)
when ER_OUT_OF_RANGE
- RangeError.new(message)
+ RangeError.new(message, sql: sql, binds: binds)
when ER_NOT_NULL_VIOLATION, ER_DO_NOT_HAVE_DEFAULT
- NotNullViolation.new(message)
+ NotNullViolation.new(message, sql: sql, binds: binds)
when ER_LOCK_DEADLOCK
- Deadlocked.new(message)
+ Deadlocked.new(message, sql: sql, binds: binds)
when ER_LOCK_WAIT_TIMEOUT
- LockWaitTimeout.new(message)
+ LockWaitTimeout.new(message, sql: sql, binds: binds)
when ER_QUERY_TIMEOUT
- StatementTimeout.new(message)
+ StatementTimeout.new(message, sql: sql, binds: binds)
when ER_QUERY_INTERRUPTED
- QueryCanceled.new(message)
+ QueryCanceled.new(message, sql: sql, binds: binds)
else
super
end
@@ -800,11 +800,13 @@ module ActiveRecord
Arel::Visitors::MySQL.new(self)
end
- def mismatched_foreign_key(message)
- parts = message.scan(/`(\w+)`[ $)]/).flatten
+ def mismatched_foreign_key(message, sql:, binds:)
+ parts = sql.scan(/`(\w+)`[ $)]/).flatten
MismatchedForeignKey.new(
self,
message: message,
+ sql: sql,
+ binds: binds,
table: parts[0],
foreign_key: parts[1],
target_table: parts[2],
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb
index 83c21ba6ea..203087bc36 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb
@@ -36,7 +36,7 @@ module ActiveRecord
def query_conditions_for_initial_load
known_type_names = @store.keys.map { |n| "'#{n}'" }
known_type_types = %w('r' 'e' 'd')
- <<-SQL % [known_type_names.join(", "), known_type_types.join(", ")]
+ <<~SQL % [known_type_names.join(", "), known_type_types.join(", ")]
WHERE
t.typname IN (%s)
OR t.typtype IN (%s)
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
index fae3ddbad4..16260fe565 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
@@ -68,7 +68,7 @@ module ActiveRecord
table = quoted_scope(table_name)
index = quoted_scope(index_name)
- query_value(<<-SQL, "SCHEMA").to_i > 0
+ query_value(<<~SQL, "SCHEMA").to_i > 0
SELECT COUNT(*)
FROM pg_class t
INNER JOIN pg_index d ON t.oid = d.indrelid
@@ -85,7 +85,7 @@ module ActiveRecord
def indexes(table_name) # :nodoc:
scope = quoted_scope(table_name)
- result = query(<<-SQL, "SCHEMA")
+ result = query(<<~SQL, "SCHEMA")
SELECT distinct i.relname, d.indisunique, d.indkey, pg_get_indexdef(d.indexrelid), t.oid,
pg_catalog.obj_description(i.oid, 'pg_class') AS comment
FROM pg_class t
@@ -124,7 +124,7 @@ module ActiveRecord
# add info on sort order (only desc order is explicitly specified, asc is the default)
# and non-default opclasses
- expressions.scan(/(?<column>\w+)\s?(?<opclass>\w+_ops)?\s?(?<desc>DESC)?\s?(?<nulls>NULLS (?:FIRST|LAST))?/).each do |column, opclass, desc, nulls|
+ expressions.scan(/(?<column>\w+)"?\s?(?<opclass>\w+_ops)?\s?(?<desc>DESC)?\s?(?<nulls>NULLS (?:FIRST|LAST))?/).each do |column, opclass, desc, nulls|
opclasses[column] = opclass.to_sym if opclass
if nulls
orders[column] = [desc, nulls].compact.join(" ")
@@ -196,7 +196,7 @@ module ActiveRecord
# Returns an array of schema names.
def schema_names
- query_values(<<-SQL, "SCHEMA")
+ query_values(<<~SQL, "SCHEMA")
SELECT nspname
FROM pg_namespace
WHERE nspname !~ '^pg_.*'
@@ -302,7 +302,7 @@ module ActiveRecord
def pk_and_sequence_for(table) #:nodoc:
# First try looking for a sequence with a dependency on the
# given table's primary key.
- result = query(<<-end_sql, "SCHEMA")[0]
+ result = query(<<~SQL, "SCHEMA")[0]
SELECT attr.attname, nsp.nspname, seq.relname
FROM pg_class seq,
pg_attribute attr,
@@ -319,10 +319,10 @@ module ActiveRecord
AND cons.contype = 'p'
AND dep.classid = 'pg_class'::regclass
AND dep.refobjid = #{quote(quote_table_name(table))}::regclass
- end_sql
+ SQL
if result.nil? || result.empty?
- result = query(<<-end_sql, "SCHEMA")[0]
+ result = query(<<~SQL, "SCHEMA")[0]
SELECT attr.attname, nsp.nspname,
CASE
WHEN pg_get_expr(def.adbin, def.adrelid) !~* 'nextval' THEN NULL
@@ -339,7 +339,7 @@ module ActiveRecord
WHERE t.oid = #{quote(quote_table_name(table))}::regclass
AND cons.contype = 'p'
AND pg_get_expr(def.adbin, def.adrelid) ~* 'nextval|uuid_generate'
- end_sql
+ SQL
end
pk = result.shift
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
index f701292be3..1cef8339f5 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
@@ -89,10 +89,11 @@ module ActiveRecord
# :singleton-method:
# PostgreSQL allows the creation of "unlogged" tables, which do not record
# data in the PostgreSQL Write-Ahead Log. This can make the tables faster,
- # bug significantly increases the risk of data loss if the database
+ # but significantly increases the risk of data loss if the database
# crashes. As a result, this should not be used in production
- # environments. If you would like all created tables to be unlogged you
- # can add the following line to your test.rb file:
+ # environments. If you would like all created tables to be unlogged in
+ # the test environment you can add the following line to your test.rb
+ # file:
#
# ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.create_unlogged_tables = true
class_attribute :create_unlogged_tables, default: false
@@ -440,28 +441,28 @@ module ActiveRecord
LOCK_NOT_AVAILABLE = "55P03"
QUERY_CANCELED = "57014"
- def translate_exception(exception, message)
+ def translate_exception(exception, message:, sql:, binds:)
return exception unless exception.respond_to?(:result)
case exception.result.try(:error_field, PG::PG_DIAG_SQLSTATE)
when UNIQUE_VIOLATION
- RecordNotUnique.new(message)
+ RecordNotUnique.new(message, sql: sql, binds: binds)
when FOREIGN_KEY_VIOLATION
- InvalidForeignKey.new(message)
+ InvalidForeignKey.new(message, sql: sql, binds: binds)
when VALUE_LIMIT_VIOLATION
- ValueTooLong.new(message)
+ ValueTooLong.new(message, sql: sql, binds: binds)
when NUMERIC_VALUE_OUT_OF_RANGE
- RangeError.new(message)
+ RangeError.new(message, sql: sql, binds: binds)
when NOT_NULL_VIOLATION
- NotNullViolation.new(message)
+ NotNullViolation.new(message, sql: sql, binds: binds)
when SERIALIZATION_FAILURE
- SerializationFailure.new(message)
+ SerializationFailure.new(message, sql: sql, binds: binds)
when DEADLOCK_DETECTED
- Deadlocked.new(message)
+ Deadlocked.new(message, sql: sql, binds: binds)
when LOCK_NOT_AVAILABLE
- LockWaitTimeout.new(message)
+ LockWaitTimeout.new(message, sql: sql, binds: binds)
when QUERY_CANCELED
- QueryCanceled.new(message)
+ QueryCanceled.new(message, sql: sql, binds: binds)
else
super
end
@@ -589,13 +590,13 @@ module ActiveRecord
initializer = OID::TypeMapInitializer.new(type_map)
if supports_ranges?
- query = <<-SQL
+ query = <<~SQL
SELECT t.oid, t.typname, t.typelem, t.typdelim, t.typinput, r.rngsubtype, t.typtype, t.typbasetype
FROM pg_type as t
LEFT JOIN pg_range as r ON oid = rngtypid
SQL
else
- query = <<-SQL
+ query = <<~SQL
SELECT t.oid, t.typname, t.typelem, t.typdelim, t.typinput, t.typtype, t.typbasetype
FROM pg_type as t
SQL
@@ -641,7 +642,7 @@ module ActiveRecord
def exec_cache(sql, name, binds)
materialize_transactions
- stmt_key = prepare_statement(sql)
+ stmt_key = prepare_statement(sql, binds)
type_casted_binds = type_casted_binds(binds)
log(sql, name, binds, type_casted_binds, stmt_key) do
@@ -695,7 +696,7 @@ module ActiveRecord
# Prepare the statement if it hasn't been prepared, return
# the statement key.
- def prepare_statement(sql)
+ def prepare_statement(sql, binds)
@lock.synchronize do
sql_key = sql_key(sql)
unless @statements.key? sql_key
@@ -703,7 +704,7 @@ module ActiveRecord
begin
@connection.prepare nextkey, sql
rescue => e
- raise translate_exception_class(e, sql)
+ raise translate_exception_class(e, sql, binds)
end
# Clear the queue
@connection.get_last_result
@@ -775,7 +776,7 @@ module ActiveRecord
# - format_type includes the column size constraint, e.g. varchar(50)
# - ::regclass is a function that gives the id for a table name
def column_definitions(table_name)
- query(<<-end_sql, "SCHEMA")
+ query(<<~SQL, "SCHEMA")
SELECT a.attname, format_type(a.atttypid, a.atttypmod),
pg_get_expr(d.adbin, d.adrelid), a.attnotnull, a.atttypid, a.atttypmod,
c.collname, col_description(a.attrelid, a.attnum) AS comment
@@ -786,7 +787,7 @@ module ActiveRecord
WHERE a.attrelid = #{quote(quote_table_name(table_name))}::regclass
AND a.attnum > 0 AND NOT a.attisdropped
ORDER BY a.attnum
- end_sql
+ SQL
end
def extract_table_ref_from_insert_sql(sql)
@@ -801,7 +802,7 @@ module ActiveRecord
def can_perform_case_insensitive_comparison_for?(column)
@case_insensitive_cache ||= {}
@case_insensitive_cache[column.sql_type] ||= begin
- sql = <<-end_sql
+ sql = <<~SQL
SELECT exists(
SELECT * FROM pg_proc
WHERE proname = 'lower'
@@ -813,7 +814,7 @@ module ActiveRecord
WHERE proname = 'lower'
AND castsource = #{quote column.sql_type}::regtype
)
- end_sql
+ SQL
execute_and_clear(sql, "SCHEMA", []) do |result|
result.getvalue(0, 0)
end
@@ -839,7 +840,7 @@ module ActiveRecord
"bool" => PG::TextDecoder::Boolean,
}
known_coder_types = coders_by_name.keys.map { |n| quote(n) }
- query = <<-SQL % known_coder_types.join(", ")
+ query = <<~SQL % known_coder_types.join(", ")
SELECT t.oid, t.typname
FROM pg_type as t
WHERE t.typname IN (%s)
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb
index 48277f0ae2..8650c07bab 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb
@@ -11,7 +11,7 @@ module ActiveRecord
# See https://www.sqlite.org/fileformat2.html#intschema
next if row["name"].starts_with?("sqlite_")
- index_sql = query_value(<<-SQL, "SCHEMA")
+ index_sql = query_value(<<~SQL, "SCHEMA")
SELECT sql
FROM sqlite_master
WHERE name = #{quote(row['name'])} AND type = 'index'
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
index 3312c3de01..b41bf2fc66 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
@@ -529,18 +529,18 @@ module ActiveRecord
@sqlite_version ||= SQLite3Adapter::Version.new(query_value("SELECT sqlite_version(*)"))
end
- def translate_exception(exception, message)
+ def translate_exception(exception, message:, sql:, binds:)
case exception.message
# SQLite 3.8.2 returns a newly formatted error message:
# UNIQUE constraint failed: *table_name*.*column_name*
# Older versions of SQLite return:
# column *column_name* is not unique
when /column(s)? .* (is|are) not unique/, /UNIQUE constraint failed: .*/
- RecordNotUnique.new(message)
+ RecordNotUnique.new(message, sql: sql, binds: binds)
when /.* may not be NULL/, /NOT NULL constraint failed: .*/
- NotNullViolation.new(message)
+ NotNullViolation.new(message, sql: sql, binds: binds)
when /FOREIGN KEY constraint failed/i
- InvalidForeignKey.new(message)
+ InvalidForeignKey.new(message, sql: sql, binds: binds)
else
super
end
@@ -550,7 +550,7 @@ module ActiveRecord
def table_structure_with_collation(table_name, basic_structure)
collation_hash = {}
- sql = <<-SQL
+ sql = <<~SQL
SELECT sql FROM
(SELECT * FROM sqlite_master UNION ALL
SELECT * FROM sqlite_temp_master)
diff --git a/activerecord/lib/active_record/connection_handling.rb b/activerecord/lib/active_record/connection_handling.rb
index 777cb8a402..3ce9aad5fc 100644
--- a/activerecord/lib/active_record/connection_handling.rb
+++ b/activerecord/lib/active_record/connection_handling.rb
@@ -107,7 +107,7 @@ module ActiveRecord
# end
def connected_to(database: nil, role: nil, &blk)
if database && role
- raise ArgumentError, "connected_to can only accept a database or role argument, but not both arguments."
+ raise ArgumentError, "connected_to can only accept a `database` or a `role` argument, but not both arguments."
elsif database
if database.is_a?(Hash)
role, database = database.first
diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb
index 50f3087c51..8f4d292a4b 100644
--- a/activerecord/lib/active_record/core.rb
+++ b/activerecord/lib/active_record/core.rb
@@ -129,11 +129,11 @@ module ActiveRecord
self.filter_attributes = []
def self.connection_handler
- ActiveRecord::RuntimeRegistry.connection_handler || default_connection_handler
+ Thread.current.thread_variable_get("ar_connection_handler") || default_connection_handler
end
def self.connection_handler=(handler)
- ActiveRecord::RuntimeRegistry.connection_handler = handler
+ Thread.current.thread_variable_set("ar_connection_handler", handler)
end
self.default_connection_handler = ConnectionAdapters::ConnectionHandler.new
diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb
index f61bc7b9e8..7e6d4c1d46 100644
--- a/activerecord/lib/active_record/errors.rb
+++ b/activerecord/lib/active_record/errors.rb
@@ -97,9 +97,13 @@ module ActiveRecord
#
# Wraps the underlying database error as +cause+.
class StatementInvalid < ActiveRecordError
- def initialize(message = nil)
+ def initialize(message = nil, sql: nil, binds: nil)
super(message || $!.try(:message))
+ @sql = sql
+ @binds = binds
end
+
+ attr_reader :sql, :binds
end
# Defunct wrapper class kept for compatibility.
@@ -118,7 +122,7 @@ module ActiveRecord
# Raised when a foreign key constraint cannot be added because the column type does not match the referenced column type.
class MismatchedForeignKey < StatementInvalid
- def initialize(adapter = nil, message: nil, table: nil, foreign_key: nil, target_table: nil, primary_key: nil)
+ def initialize(adapter = nil, message: nil, sql: nil, binds: nil, table: nil, foreign_key: nil, target_table: nil, primary_key: nil)
@adapter = adapter
if table
msg = +<<~EOM
@@ -135,7 +139,7 @@ module ActiveRecord
if message
msg << "\nOriginal message: #{message}"
end
- super(msg)
+ super(msg, sql: sql, binds: binds)
end
private
diff --git a/activerecord/lib/active_record/integration.rb b/activerecord/lib/active_record/integration.rb
index 456689ec6d..33c4066b89 100644
--- a/activerecord/lib/active_record/integration.rb
+++ b/activerecord/lib/active_record/integration.rb
@@ -20,7 +20,7 @@ module ActiveRecord
# Indicates whether to use a stable #cache_key method that is accompanied
# by a changing version in the #cache_version method.
#
- # This is +false+, by default until Rails 6.0.
+ # This is +true+, by default on Rails 5.2 and above.
class_attribute :cache_versioning, instance_writer: false, default: false
end
diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb
index 6e5a610642..24782f8748 100644
--- a/activerecord/lib/active_record/migration.rb
+++ b/activerecord/lib/active_record/migration.rb
@@ -23,7 +23,7 @@ module ActiveRecord
# t.string :zipcode
# end
#
- # execute <<-SQL
+ # execute <<~SQL
# ALTER TABLE distributors
# ADD CONSTRAINT zipchk
# CHECK (char_length(zipcode) = 5) NO INHERIT;
@@ -41,7 +41,7 @@ module ActiveRecord
# t.string :zipcode
# end
#
- # execute <<-SQL
+ # execute <<~SQL
# ALTER TABLE distributors
# ADD CONSTRAINT zipchk
# CHECK (char_length(zipcode) = 5) NO INHERIT;
@@ -49,7 +49,7 @@ module ActiveRecord
# end
#
# def down
- # execute <<-SQL
+ # execute <<~SQL
# ALTER TABLE distributors
# DROP CONSTRAINT zipchk
# SQL
@@ -68,7 +68,7 @@ module ActiveRecord
#
# reversible do |dir|
# dir.up do
- # execute <<-SQL
+ # execute <<~SQL
# ALTER TABLE distributors
# ADD CONSTRAINT zipchk
# CHECK (char_length(zipcode) = 5) NO INHERIT;
@@ -76,7 +76,7 @@ module ActiveRecord
# end
#
# dir.down do
- # execute <<-SQL
+ # execute <<~SQL
# ALTER TABLE distributors
# DROP CONSTRAINT zipchk
# SQL
diff --git a/activerecord/lib/active_record/query_cache.rb b/activerecord/lib/active_record/query_cache.rb
index 28194c7c46..43a21e629e 100644
--- a/activerecord/lib/active_record/query_cache.rb
+++ b/activerecord/lib/active_record/query_cache.rb
@@ -26,15 +26,22 @@ module ActiveRecord
end
def self.run
- ActiveRecord::Base.connection_handler.connection_pool_list.
- reject { |p| p.query_cache_enabled }.each { |p| p.enable_query_cache! }
+ pools = []
+
+ ActiveRecord::Base.connection_handlers.each do |key, handler|
+ pools << handler.connection_pool_list.reject { |p| p.query_cache_enabled }.each { |p| p.enable_query_cache! }
+ end
+
+ pools.flatten
end
def self.complete(pools)
pools.each { |pool| pool.disable_query_cache! }
- ActiveRecord::Base.connection_handler.connection_pool_list.each do |pool|
- pool.release_connection if pool.active_connection? && !pool.connection.transaction_open?
+ ActiveRecord::Base.connection_handlers.each do |_, handler|
+ handler.connection_pool_list.each do |pool|
+ pool.release_connection if pool.active_connection? && !pool.connection.transaction_open?
+ end
end
end
diff --git a/activerecord/lib/active_record/relation/where_clause.rb b/activerecord/lib/active_record/relation/where_clause.rb
index a502713e56..e225628bae 100644
--- a/activerecord/lib/active_record/relation/where_clause.rb
+++ b/activerecord/lib/active_record/relation/where_clause.rb
@@ -125,6 +125,10 @@ module ActiveRecord
raise ArgumentError, "Invalid argument for .where.not(), got nil."
when Arel::Nodes::In
Arel::Nodes::NotIn.new(node.left, node.right)
+ when Arel::Nodes::IsNotDistinctFrom
+ Arel::Nodes::IsDistinctFrom.new(node.left, node.right)
+ when Arel::Nodes::IsDistinctFrom
+ Arel::Nodes::IsNotDistinctFrom.new(node.left, node.right)
when Arel::Nodes::Equality
Arel::Nodes::NotEqual.new(node.left, node.right)
when String
diff --git a/activerecord/lib/arel/nodes/equality.rb b/activerecord/lib/arel/nodes/equality.rb
index 2aa85a977e..551d56c2ff 100644
--- a/activerecord/lib/arel/nodes/equality.rb
+++ b/activerecord/lib/arel/nodes/equality.rb
@@ -7,5 +7,12 @@ module Arel # :nodoc: all
alias :operand1 :left
alias :operand2 :right
end
+
+ %w{
+ IsDistinctFrom
+ IsNotDistinctFrom
+ }.each do |name|
+ const_set name, Class.new(Equality)
+ end
end
end
diff --git a/activerecord/lib/arel/predications.rb b/activerecord/lib/arel/predications.rb
index e83a6f162f..77502dd199 100644
--- a/activerecord/lib/arel/predications.rb
+++ b/activerecord/lib/arel/predications.rb
@@ -18,6 +18,14 @@ module Arel # :nodoc: all
Nodes::Equality.new self, quoted_node(other)
end
+ def is_not_distinct_from(other)
+ Nodes::IsNotDistinctFrom.new self, quoted_node(other)
+ end
+
+ def is_distinct_from(other)
+ Nodes::IsDistinctFrom.new self, quoted_node(other)
+ end
+
def eq_any(others)
grouping_any :eq, others
end
diff --git a/activerecord/lib/arel/visitors/depth_first.rb b/activerecord/lib/arel/visitors/depth_first.rb
index 8f65d303ac..92d309453c 100644
--- a/activerecord/lib/arel/visitors/depth_first.rb
+++ b/activerecord/lib/arel/visitors/depth_first.rb
@@ -95,6 +95,8 @@ module Arel # :nodoc: all
alias :visit_Arel_Nodes_NotEqual :binary
alias :visit_Arel_Nodes_NotIn :binary
alias :visit_Arel_Nodes_NotRegexp :binary
+ alias :visit_Arel_Nodes_IsNotDistinctFrom :binary
+ alias :visit_Arel_Nodes_IsDistinctFrom :binary
alias :visit_Arel_Nodes_Or :binary
alias :visit_Arel_Nodes_OuterJoin :binary
alias :visit_Arel_Nodes_Regexp :binary
diff --git a/activerecord/lib/arel/visitors/dot.rb b/activerecord/lib/arel/visitors/dot.rb
index 9054f0159b..6389c875cb 100644
--- a/activerecord/lib/arel/visitors/dot.rb
+++ b/activerecord/lib/arel/visitors/dot.rb
@@ -195,6 +195,8 @@ module Arel # :nodoc: all
alias :visit_Arel_Nodes_JoinSource :binary
alias :visit_Arel_Nodes_LessThan :binary
alias :visit_Arel_Nodes_LessThanOrEqual :binary
+ alias :visit_Arel_Nodes_IsNotDistinctFrom :binary
+ alias :visit_Arel_Nodes_IsDistinctFrom :binary
alias :visit_Arel_Nodes_Matches :binary
alias :visit_Arel_Nodes_NotEqual :binary
alias :visit_Arel_Nodes_NotIn :binary
diff --git a/activerecord/lib/arel/visitors/ibm_db.rb b/activerecord/lib/arel/visitors/ibm_db.rb
index 0a06aef60b..73166054da 100644
--- a/activerecord/lib/arel/visitors/ibm_db.rb
+++ b/activerecord/lib/arel/visitors/ibm_db.rb
@@ -10,6 +10,12 @@ module Arel # :nodoc: all
collector = visit o.expr, collector
collector << " ROWS ONLY"
end
+
+ def is_distinct_from(o, collector)
+ collector << "DECODE("
+ collector = visit [o.left, o.right, 0, 1], collector
+ collector << ")"
+ end
end
end
end
diff --git a/activerecord/lib/arel/visitors/mssql.rb b/activerecord/lib/arel/visitors/mssql.rb
index d564e19089..fdd864b40d 100644
--- a/activerecord/lib/arel/visitors/mssql.rb
+++ b/activerecord/lib/arel/visitors/mssql.rb
@@ -12,6 +12,31 @@ module Arel # :nodoc: all
private
+ def visit_Arel_Nodes_IsNotDistinctFrom(o, collector)
+ right = o.right
+
+ if right.nil?
+ collector = visit o.left, collector
+ collector << " IS NULL"
+ else
+ collector << "EXISTS (VALUES ("
+ collector = visit o.left, collector
+ collector << ") INTERSECT VALUES ("
+ collector = visit right, collector
+ collector << "))"
+ end
+ end
+
+ def visit_Arel_Nodes_IsDistinctFrom(o, collector)
+ if o.right.nil?
+ collector = visit o.left, collector
+ collector << " IS NOT NULL"
+ else
+ collector << "NOT "
+ visit_Arel_Nodes_IsNotDistinctFrom o, collector
+ end
+ end
+
def visit_Arel_Visitors_MSSQL_RowNumber(o, collector)
collector << "ROW_NUMBER() OVER (ORDER BY "
inject_join(o.children, collector, ", ") << ") as _row_num"
diff --git a/activerecord/lib/arel/visitors/mysql.rb b/activerecord/lib/arel/visitors/mysql.rb
index 4e7b2456aa..dd77cfdf66 100644
--- a/activerecord/lib/arel/visitors/mysql.rb
+++ b/activerecord/lib/arel/visitors/mysql.rb
@@ -37,6 +37,17 @@ module Arel # :nodoc: all
collector
end
+ def visit_Arel_Nodes_IsNotDistinctFrom(o, collector)
+ collector = visit o.left, collector
+ collector << " <=> "
+ visit o.right, collector
+ end
+
+ def visit_Arel_Nodes_IsDistinctFrom(o, collector)
+ collector << "NOT "
+ visit_Arel_Nodes_IsNotDistinctFrom o, collector
+ end
+
# In the simple case, MySQL allows us to place JOINs directly into the UPDATE
# query. However, this does not allow for LIMIT, OFFSET and ORDER. To support
# these, we must use a subquery.
diff --git a/activerecord/lib/arel/visitors/oracle.rb b/activerecord/lib/arel/visitors/oracle.rb
index 30a1529d46..f96bf65ee5 100644
--- a/activerecord/lib/arel/visitors/oracle.rb
+++ b/activerecord/lib/arel/visitors/oracle.rb
@@ -148,6 +148,12 @@ module Arel # :nodoc: all
def visit_Arel_Nodes_BindParam(o, collector)
collector.add_bind(o.value) { |i| ":a#{i}" }
end
+
+ def is_distinct_from(o, collector)
+ collector << "DECODE("
+ collector = visit [o.left, o.right, 0, 1], collector
+ collector << ")"
+ end
end
end
end
diff --git a/activerecord/lib/arel/visitors/oracle12.rb b/activerecord/lib/arel/visitors/oracle12.rb
index 7061f06087..b092aa95e0 100644
--- a/activerecord/lib/arel/visitors/oracle12.rb
+++ b/activerecord/lib/arel/visitors/oracle12.rb
@@ -56,6 +56,12 @@ module Arel # :nodoc: all
def visit_Arel_Nodes_BindParam(o, collector)
collector.add_bind(o.value) { |i| ":a#{i}" }
end
+
+ def is_distinct_from(o, collector)
+ collector << "DECODE("
+ collector = visit [o.left, o.right, 0, 1], collector
+ collector << ")"
+ end
end
end
end
diff --git a/activerecord/lib/arel/visitors/postgresql.rb b/activerecord/lib/arel/visitors/postgresql.rb
index c5110fa89c..920776b4dc 100644
--- a/activerecord/lib/arel/visitors/postgresql.rb
+++ b/activerecord/lib/arel/visitors/postgresql.rb
@@ -77,6 +77,18 @@ module Arel # :nodoc: all
grouping_parentheses o, collector
end
+ def visit_Arel_Nodes_IsNotDistinctFrom(o, collector)
+ collector = visit o.left, collector
+ collector << " IS NOT DISTINCT FROM "
+ visit o.right, collector
+ end
+
+ def visit_Arel_Nodes_IsDistinctFrom(o, collector)
+ collector = visit o.left, collector
+ collector << " IS DISTINCT FROM "
+ visit o.right, collector
+ end
+
# Used by Lateral visitor to enclose select queries in parentheses
def grouping_parentheses(o, collector)
if o.expr.is_a? Nodes::SelectStatement
diff --git a/activerecord/lib/arel/visitors/sqlite.rb b/activerecord/lib/arel/visitors/sqlite.rb
index cb1d2424ad..af6f7e856a 100644
--- a/activerecord/lib/arel/visitors/sqlite.rb
+++ b/activerecord/lib/arel/visitors/sqlite.rb
@@ -22,6 +22,18 @@ module Arel # :nodoc: all
def visit_Arel_Nodes_False(o, collector)
collector << "0"
end
+
+ def visit_Arel_Nodes_IsNotDistinctFrom(o, collector)
+ collector = visit o.left, collector
+ collector << " IS "
+ visit o.right, collector
+ end
+
+ def visit_Arel_Nodes_IsDistinctFrom(o, collector)
+ collector = visit o.left, collector
+ collector << " IS NOT "
+ visit o.right, collector
+ end
end
end
end
diff --git a/activerecord/lib/arel/visitors/to_sql.rb b/activerecord/lib/arel/visitors/to_sql.rb
index 7efd74dbc9..f9fe4404eb 100644
--- a/activerecord/lib/arel/visitors/to_sql.rb
+++ b/activerecord/lib/arel/visitors/to_sql.rb
@@ -641,6 +641,26 @@ module Arel # :nodoc: all
end
end
+ def visit_Arel_Nodes_IsNotDistinctFrom(o, collector)
+ if o.right.nil?
+ collector = visit o.left, collector
+ collector << " IS NULL"
+ else
+ collector = is_distinct_from(o, collector)
+ collector << " = 0"
+ end
+ end
+
+ def visit_Arel_Nodes_IsDistinctFrom(o, collector)
+ if o.right.nil?
+ collector = visit o.left, collector
+ collector << " IS NOT NULL"
+ else
+ collector = is_distinct_from(o, collector)
+ collector << " = 1"
+ end
+ end
+
def visit_Arel_Nodes_NotEqual(o, collector)
right = o.right
@@ -873,6 +893,19 @@ module Arel # :nodoc: all
collector
end
end
+
+ def is_distinct_from(o, collector)
+ collector << "CASE WHEN "
+ collector = visit o.left, collector
+ collector << " = "
+ collector = visit o.right, collector
+ collector << " OR ("
+ collector = visit o.left, collector
+ collector << " IS NULL AND "
+ collector = visit o.right, collector
+ collector << " IS NULL)"
+ collector << " THEN 0 ELSE 1 END"
+ end
end
end
end
diff --git a/activerecord/test/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb
index 64c2b51f83..d3ed59bccd 100644
--- a/activerecord/test/cases/adapter_test.rb
+++ b/activerecord/test/cases/adapter_test.rb
@@ -286,18 +286,6 @@ module ActiveRecord
assert_equal "special_db_type", @connection.type_to_sql(:special_db_type)
end
- unless current_adapter?(:PostgreSQLAdapter)
- def test_log_invalid_encoding
- error = assert_raises RuntimeError do
- @connection.send :log, "SELECT 'ы' FROM DUAL" do
- raise (+"ы").force_encoding(Encoding::ASCII_8BIT)
- end
- end
-
- assert_equal "ы", error.message
- end
- end
-
def test_supports_multi_insert_is_deprecated
assert_deprecated { @connection.supports_multi_insert? }
end
diff --git a/activerecord/test/cases/adapters/postgresql/composite_test.rb b/activerecord/test/cases/adapters/postgresql/composite_test.rb
index b0ce2694a3..683066cdb3 100644
--- a/activerecord/test/cases/adapters/postgresql/composite_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/composite_test.rb
@@ -15,13 +15,13 @@ module PostgresqlCompositeBehavior
@connection = ActiveRecord::Base.connection
@connection.transaction do
- @connection.execute <<-SQL
- CREATE TYPE full_address AS
- (
- city VARCHAR(90),
- street VARCHAR(90)
- );
- SQL
+ @connection.execute <<~SQL
+ CREATE TYPE full_address AS
+ (
+ city VARCHAR(90),
+ street VARCHAR(90)
+ );
+ SQL
@connection.create_table("postgresql_composites") do |t|
t.column :address, :full_address
end
diff --git a/activerecord/test/cases/adapters/postgresql/connection_test.rb b/activerecord/test/cases/adapters/postgresql/connection_test.rb
index 70aa189893..f0daea4266 100644
--- a/activerecord/test/cases/adapters/postgresql/connection_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/connection_test.rb
@@ -230,7 +230,7 @@ module ActiveRecord
def test_get_and_release_advisory_lock
lock_id = 5295901941911233559
- list_advisory_locks = <<-SQL
+ list_advisory_locks = <<~SQL
SELECT locktype,
(classid::bigint << 32) | objid::bigint AS lock_id
FROM pg_locks
diff --git a/activerecord/test/cases/adapters/postgresql/enum_test.rb b/activerecord/test/cases/adapters/postgresql/enum_test.rb
index 6789ff63e7..416a2b141b 100644
--- a/activerecord/test/cases/adapters/postgresql/enum_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/enum_test.rb
@@ -13,7 +13,7 @@ class PostgresqlEnumTest < ActiveRecord::PostgreSQLTestCase
def setup
@connection = ActiveRecord::Base.connection
@connection.transaction do
- @connection.execute <<-SQL
+ @connection.execute <<~SQL
CREATE TYPE mood AS ENUM ('sad', 'ok', 'happy');
SQL
@connection.create_table("postgresql_enums") do |t|
diff --git a/activerecord/test/cases/adapters/postgresql/foreign_table_test.rb b/activerecord/test/cases/adapters/postgresql/foreign_table_test.rb
index 4fa315ad23..69339c8a31 100644
--- a/activerecord/test/cases/adapters/postgresql/foreign_table_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/foreign_table_test.rb
@@ -22,18 +22,18 @@ if ActiveRecord::Base.connection.supports_foreign_tables?
enable_extension!("postgres_fdw", @connection)
foreign_db_config = ARTest.connection_config["arunit2"]
- @connection.execute <<-SQL
+ @connection.execute <<~SQL
CREATE SERVER foreign_server
FOREIGN DATA WRAPPER postgres_fdw
OPTIONS (dbname '#{foreign_db_config["database"]}')
SQL
- @connection.execute <<-SQL
+ @connection.execute <<~SQL
CREATE USER MAPPING FOR CURRENT_USER
SERVER foreign_server
SQL
- @connection.execute <<-SQL
+ @connection.execute <<~SQL
CREATE FOREIGN TABLE foreign_professors (
id int,
name character varying NOT NULL
@@ -45,7 +45,7 @@ if ActiveRecord::Base.connection.supports_foreign_tables?
def teardown
disable_extension!("postgres_fdw", @connection)
- @connection.execute <<-SQL
+ @connection.execute <<~SQL
DROP SERVER IF EXISTS foreign_server CASCADE
SQL
end
diff --git a/activerecord/test/cases/adapters/postgresql/range_test.rb b/activerecord/test/cases/adapters/postgresql/range_test.rb
index 433598500d..3727fe1edd 100644
--- a/activerecord/test/cases/adapters/postgresql/range_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/range_test.rb
@@ -394,7 +394,7 @@ _SQL
end
def insert_range(values)
- @connection.execute <<-SQL
+ @connection.execute <<~SQL
INSERT INTO postgresql_ranges (
id,
date_range,
diff --git a/activerecord/test/cases/adapters/postgresql/rename_table_test.rb b/activerecord/test/cases/adapters/postgresql/rename_table_test.rb
index 100d247113..7eccaf4aa2 100644
--- a/activerecord/test/cases/adapters/postgresql/rename_table_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/rename_table_test.rb
@@ -27,7 +27,7 @@ class PostgresqlRenameTableTest < ActiveRecord::PostgreSQLTestCase
private
def num_indices_named(name)
- @connection.execute(<<-SQL).values.length
+ @connection.execute(<<~SQL).values.length
SELECT 1 FROM "pg_index"
JOIN "pg_class" ON "pg_index"."indexrelid" = "pg_class"."oid"
WHERE "pg_class"."relname" = '#{name}'
diff --git a/activerecord/test/cases/adapters/postgresql/schema_test.rb b/activerecord/test/cases/adapters/postgresql/schema_test.rb
index a36d066c80..931a9bd0be 100644
--- a/activerecord/test/cases/adapters/postgresql/schema_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/schema_test.rb
@@ -146,7 +146,7 @@ class SchemaTest < ActiveRecord::PostgreSQLTestCase
def test_habtm_table_name_with_schema
ActiveRecord::Base.connection.drop_schema "music", if_exists: true
ActiveRecord::Base.connection.create_schema "music"
- ActiveRecord::Base.connection.execute <<-SQL
+ ActiveRecord::Base.connection.execute <<~SQL
CREATE TABLE music.albums (id serial primary key);
CREATE TABLE music.songs (id serial primary key);
CREATE TABLE music.albums_songs (album_id integer, song_id integer);
@@ -507,6 +507,7 @@ class SchemaIndexOpclassTest < ActiveRecord::PostgreSQLTestCase
@connection = ActiveRecord::Base.connection
@connection.create_table "trains" do |t|
t.string :name
+ t.string :position
t.text :description
end
end
@@ -530,6 +531,17 @@ class SchemaIndexOpclassTest < ActiveRecord::PostgreSQLTestCase
assert_match(/opclass: \{ description: :text_pattern_ops \}/, output)
end
+
+ def test_opclass_class_parsing_on_non_reserved_and_cannot_be_function_or_type_keyword
+ @connection.enable_extension("pg_trgm")
+ @connection.execute "CREATE INDEX trains_position ON trains USING gin(position gin_trgm_ops)"
+ @connection.execute "CREATE INDEX trains_name_and_position ON trains USING btree(name, position text_pattern_ops)"
+
+ output = dump_table_schema "trains"
+
+ assert_match(/opclass: :gin_trgm_ops/, output)
+ assert_match(/opclass: \{ position: :text_pattern_ops \}/, output)
+ end
end
class SchemaIndexNullsOrderTest < ActiveRecord::PostgreSQLTestCase
diff --git a/activerecord/test/cases/adapters/postgresql/uuid_test.rb b/activerecord/test/cases/adapters/postgresql/uuid_test.rb
index 71d07e2f4c..9912763c1b 100644
--- a/activerecord/test/cases/adapters/postgresql/uuid_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/uuid_test.rb
@@ -198,10 +198,10 @@ class PostgresqlUUIDGenerationTest < ActiveRecord::PostgreSQLTestCase
# Create custom PostgreSQL function to generate UUIDs
# to test dumping tables which columns have defaults with custom functions
- connection.execute <<-SQL
- CREATE OR REPLACE FUNCTION my_uuid_generator() RETURNS uuid
- AS $$ SELECT * FROM #{uuid_function} $$
- LANGUAGE SQL VOLATILE;
+ connection.execute <<~SQL
+ CREATE OR REPLACE FUNCTION my_uuid_generator() RETURNS uuid
+ AS $$ SELECT * FROM #{uuid_function} $$
+ LANGUAGE SQL VOLATILE;
SQL
# Create such a table with custom function as default value generator
diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb
index 89052019f8..8536dcf183 100644
--- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb
+++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb
@@ -55,11 +55,11 @@ module ActiveRecord
owner = Owner.create!(name: "hello".encode("ascii-8bit"))
owner.reload
select = Owner.columns.map { |c| "typeof(#{c.name})" }.join ", "
- result = Owner.connection.exec_query <<-esql
+ result = Owner.connection.exec_query <<~SQL
SELECT #{select}
FROM #{Owner.table_name}
WHERE #{Owner.primary_key} = #{owner.id}
- esql
+ SQL
assert_not(result.rows.first.include?("blob"), "should not store blobs")
ensure
@@ -160,13 +160,13 @@ module ActiveRecord
end
def test_quote_binary_column_escapes_it
- DualEncoding.connection.execute(<<-eosql)
+ DualEncoding.connection.execute(<<~SQL)
CREATE TABLE IF NOT EXISTS dual_encodings (
id integer PRIMARY KEY AUTOINCREMENT,
name varchar(255),
data binary
)
- eosql
+ SQL
str = (+"\x80").force_encoding("ASCII-8BIT")
binary = DualEncoding.new name: "いただきます!", data: str
binary.save!
@@ -261,7 +261,7 @@ module ActiveRecord
end
def test_tables_logs_name
- sql = <<-SQL
+ sql = <<~SQL
SELECT name FROM sqlite_master WHERE name <> 'sqlite_sequence' AND type IN ('table')
SQL
assert_logged [[sql.squish, "SCHEMA", []]] do
@@ -271,7 +271,7 @@ module ActiveRecord
def test_table_exists_logs_name
with_example_table do
- sql = <<-SQL
+ sql = <<~SQL
SELECT name FROM sqlite_master WHERE name <> 'sqlite_sequence' AND name = 'ex' AND type IN ('table')
SQL
assert_logged [[sql.squish, "SCHEMA", []]] do
@@ -585,7 +585,7 @@ module ActiveRecord
end
def with_example_table(definition = nil, table_name = "ex", &block)
- definition ||= <<-SQL
+ definition ||= <<~SQL
id integer PRIMARY KEY AUTOINCREMENT,
number integer
SQL
diff --git a/activerecord/test/cases/arel/visitors/ibm_db_test.rb b/activerecord/test/cases/arel/visitors/ibm_db_test.rb
index 7163cb34d3..2ddbec3266 100644
--- a/activerecord/test/cases/arel/visitors/ibm_db_test.rb
+++ b/activerecord/test/cases/arel/visitors/ibm_db_test.rb
@@ -29,6 +29,45 @@ module Arel
sql = compile(stmt)
sql.must_be_like "UPDATE \"users\" WHERE \"users\".\"id\" IN (SELECT \"users\".\"id\" FROM \"users\" FETCH FIRST 1 ROWS ONLY)"
end
+
+ describe "Nodes::IsNotDistinctFrom" do
+ it "should construct a valid generic SQL statement" do
+ test = Table.new(:users)[:name].is_not_distinct_from "Aaron Patterson"
+ compile(test).must_be_like %{
+ DECODE("users"."name", 'Aaron Patterson', 0, 1) = 0
+ }
+ end
+
+ it "should handle column names on both sides" do
+ test = Table.new(:users)[:first_name].is_not_distinct_from Table.new(:users)[:last_name]
+ compile(test).must_be_like %{
+ DECODE("users"."first_name", "users"."last_name", 0, 1) = 0
+ }
+ end
+
+ it "should handle nil" do
+ @table = Table.new(:users)
+ val = Nodes.build_quoted(nil, @table[:active])
+ sql = compile Nodes::IsNotDistinctFrom.new(@table[:name], val)
+ sql.must_be_like %{ "users"."name" IS NULL }
+ end
+ end
+
+ describe "Nodes::IsDistinctFrom" do
+ it "should handle column names on both sides" do
+ test = Table.new(:users)[:first_name].is_distinct_from Table.new(:users)[:last_name]
+ compile(test).must_be_like %{
+ DECODE("users"."first_name", "users"."last_name", 0, 1) = 1
+ }
+ end
+
+ it "should handle nil" do
+ @table = Table.new(:users)
+ val = Nodes.build_quoted(nil, @table[:active])
+ sql = compile Nodes::IsDistinctFrom.new(@table[:name], val)
+ sql.must_be_like %{ "users"."name" IS NOT NULL }
+ end
+ end
end
end
end
diff --git a/activerecord/test/cases/arel/visitors/informix_test.rb b/activerecord/test/cases/arel/visitors/informix_test.rb
index b0b031cca3..b6c2dd6ae7 100644
--- a/activerecord/test/cases/arel/visitors/informix_test.rb
+++ b/activerecord/test/cases/arel/visitors/informix_test.rb
@@ -54,6 +54,45 @@ module Arel
sql = compile(stmt)
sql.must_be_like 'SELECT FROM "posts" INNER JOIN "comments"'
end
+
+ describe "Nodes::IsNotDistinctFrom" do
+ it "should construct a valid generic SQL statement" do
+ test = Table.new(:users)[:name].is_not_distinct_from "Aaron Patterson"
+ compile(test).must_be_like %{
+ CASE WHEN "users"."name" = 'Aaron Patterson' OR ("users"."name" IS NULL AND 'Aaron Patterson' IS NULL) THEN 0 ELSE 1 END = 0
+ }
+ end
+
+ it "should handle column names on both sides" do
+ test = Table.new(:users)[:first_name].is_not_distinct_from Table.new(:users)[:last_name]
+ compile(test).must_be_like %{
+ CASE WHEN "users"."first_name" = "users"."last_name" OR ("users"."first_name" IS NULL AND "users"."last_name" IS NULL) THEN 0 ELSE 1 END = 0
+ }
+ end
+
+ it "should handle nil" do
+ @table = Table.new(:users)
+ val = Nodes.build_quoted(nil, @table[:active])
+ sql = compile Nodes::IsNotDistinctFrom.new(@table[:name], val)
+ sql.must_be_like %{ "users"."name" IS NULL }
+ end
+ end
+
+ describe "Nodes::IsDistinctFrom" do
+ it "should handle column names on both sides" do
+ test = Table.new(:users)[:first_name].is_distinct_from Table.new(:users)[:last_name]
+ compile(test).must_be_like %{
+ CASE WHEN "users"."first_name" = "users"."last_name" OR ("users"."first_name" IS NULL AND "users"."last_name" IS NULL) THEN 0 ELSE 1 END = 1
+ }
+ end
+
+ it "should handle nil" do
+ @table = Table.new(:users)
+ val = Nodes.build_quoted(nil, @table[:active])
+ sql = compile Nodes::IsDistinctFrom.new(@table[:name], val)
+ sql.must_be_like %{ "users"."name" IS NOT NULL }
+ end
+ end
end
end
end
diff --git a/activerecord/test/cases/arel/visitors/mssql_test.rb b/activerecord/test/cases/arel/visitors/mssql_test.rb
index 340376c3d6..74f34b4dad 100644
--- a/activerecord/test/cases/arel/visitors/mssql_test.rb
+++ b/activerecord/test/cases/arel/visitors/mssql_test.rb
@@ -94,6 +94,45 @@ module Arel
sql = compile(stmt)
sql.must_be_like "SELECT COUNT(1) as count_id FROM (SELECT _t.* FROM (SELECT ROW_NUMBER() OVER (ORDER BY ) as _row_num) as _t WHERE _row_num BETWEEN 1 AND 10) AS subquery"
end
+
+ describe "Nodes::IsNotDistinctFrom" do
+ it "should construct a valid generic SQL statement" do
+ test = Table.new(:users)[:name].is_not_distinct_from "Aaron Patterson"
+ compile(test).must_be_like %{
+ EXISTS (VALUES ("users"."name") INTERSECT VALUES ('Aaron Patterson'))
+ }
+ end
+
+ it "should handle column names on both sides" do
+ test = Table.new(:users)[:first_name].is_not_distinct_from Table.new(:users)[:last_name]
+ compile(test).must_be_like %{
+ EXISTS (VALUES ("users"."first_name") INTERSECT VALUES ("users"."last_name"))
+ }
+ end
+
+ it "should handle nil" do
+ @table = Table.new(:users)
+ val = Nodes.build_quoted(nil, @table[:active])
+ sql = compile Nodes::IsNotDistinctFrom.new(@table[:name], val)
+ sql.must_be_like %{ "users"."name" IS NULL }
+ end
+ end
+
+ describe "Nodes::IsDistinctFrom" do
+ it "should handle column names on both sides" do
+ test = Table.new(:users)[:first_name].is_distinct_from Table.new(:users)[:last_name]
+ compile(test).must_be_like %{
+ NOT EXISTS (VALUES ("users"."first_name") INTERSECT VALUES ("users"."last_name"))
+ }
+ end
+
+ it "should handle nil" do
+ @table = Table.new(:users)
+ val = Nodes.build_quoted(nil, @table[:active])
+ sql = compile Nodes::IsDistinctFrom.new(@table[:name], val)
+ sql.must_be_like %{ "users"."name" IS NOT NULL }
+ end
+ end
end
end
end
diff --git a/activerecord/test/cases/arel/visitors/mysql_test.rb b/activerecord/test/cases/arel/visitors/mysql_test.rb
index dae98b0583..5f37587957 100644
--- a/activerecord/test/cases/arel/visitors/mysql_test.rb
+++ b/activerecord/test/cases/arel/visitors/mysql_test.rb
@@ -65,6 +65,45 @@ module Arel
}
end
end
+
+ describe "Nodes::IsNotDistinctFrom" do
+ it "should construct a valid generic SQL statement" do
+ test = Table.new(:users)[:name].is_not_distinct_from "Aaron Patterson"
+ compile(test).must_be_like %{
+ "users"."name" <=> 'Aaron Patterson'
+ }
+ end
+
+ it "should handle column names on both sides" do
+ test = Table.new(:users)[:first_name].is_not_distinct_from Table.new(:users)[:last_name]
+ compile(test).must_be_like %{
+ "users"."first_name" <=> "users"."last_name"
+ }
+ end
+
+ it "should handle nil" do
+ @table = Table.new(:users)
+ val = Nodes.build_quoted(nil, @table[:active])
+ sql = compile Nodes::IsNotDistinctFrom.new(@table[:name], val)
+ sql.must_be_like %{ "users"."name" <=> NULL }
+ end
+ end
+
+ describe "Nodes::IsDistinctFrom" do
+ it "should handle column names on both sides" do
+ test = Table.new(:users)[:first_name].is_distinct_from Table.new(:users)[:last_name]
+ compile(test).must_be_like %{
+ NOT "users"."first_name" <=> "users"."last_name"
+ }
+ end
+
+ it "should handle nil" do
+ @table = Table.new(:users)
+ val = Nodes.build_quoted(nil, @table[:active])
+ sql = compile Nodes::IsDistinctFrom.new(@table[:name], val)
+ sql.must_be_like %{ NOT "users"."name" <=> NULL }
+ end
+ end
end
end
end
diff --git a/activerecord/test/cases/arel/visitors/oracle12_test.rb b/activerecord/test/cases/arel/visitors/oracle12_test.rb
index 83a2ee36ca..4ce5cab4db 100644
--- a/activerecord/test/cases/arel/visitors/oracle12_test.rb
+++ b/activerecord/test/cases/arel/visitors/oracle12_test.rb
@@ -56,6 +56,45 @@ module Arel
}
end
end
+
+ describe "Nodes::IsNotDistinctFrom" do
+ it "should construct a valid generic SQL statement" do
+ test = Table.new(:users)[:name].is_not_distinct_from "Aaron Patterson"
+ compile(test).must_be_like %{
+ DECODE("users"."name", 'Aaron Patterson', 0, 1) = 0
+ }
+ end
+
+ it "should handle column names on both sides" do
+ test = Table.new(:users)[:first_name].is_not_distinct_from Table.new(:users)[:last_name]
+ compile(test).must_be_like %{
+ DECODE("users"."first_name", "users"."last_name", 0, 1) = 0
+ }
+ end
+
+ it "should handle nil" do
+ @table = Table.new(:users)
+ val = Nodes.build_quoted(nil, @table[:active])
+ sql = compile Nodes::IsNotDistinctFrom.new(@table[:name], val)
+ sql.must_be_like %{ "users"."name" IS NULL }
+ end
+ end
+
+ describe "Nodes::IsDistinctFrom" do
+ it "should handle column names on both sides" do
+ test = Table.new(:users)[:first_name].is_distinct_from Table.new(:users)[:last_name]
+ compile(test).must_be_like %{
+ DECODE("users"."first_name", "users"."last_name", 0, 1) = 1
+ }
+ end
+
+ it "should handle nil" do
+ @table = Table.new(:users)
+ val = Nodes.build_quoted(nil, @table[:active])
+ sql = compile Nodes::IsDistinctFrom.new(@table[:name], val)
+ sql.must_be_like %{ "users"."name" IS NOT NULL }
+ end
+ end
end
end
end
diff --git a/activerecord/test/cases/arel/visitors/oracle_test.rb b/activerecord/test/cases/arel/visitors/oracle_test.rb
index e1dfe40cf9..893edc7f74 100644
--- a/activerecord/test/cases/arel/visitors/oracle_test.rb
+++ b/activerecord/test/cases/arel/visitors/oracle_test.rb
@@ -192,6 +192,45 @@ module Arel
}
end
end
+
+ describe "Nodes::IsNotDistinctFrom" do
+ it "should construct a valid generic SQL statement" do
+ test = Table.new(:users)[:name].is_not_distinct_from "Aaron Patterson"
+ compile(test).must_be_like %{
+ DECODE("users"."name", 'Aaron Patterson', 0, 1) = 0
+ }
+ end
+
+ it "should handle column names on both sides" do
+ test = Table.new(:users)[:first_name].is_not_distinct_from Table.new(:users)[:last_name]
+ compile(test).must_be_like %{
+ DECODE("users"."first_name", "users"."last_name", 0, 1) = 0
+ }
+ end
+
+ it "should handle nil" do
+ @table = Table.new(:users)
+ val = Nodes.build_quoted(nil, @table[:active])
+ sql = compile Nodes::IsNotDistinctFrom.new(@table[:name], val)
+ sql.must_be_like %{ "users"."name" IS NULL }
+ end
+ end
+
+ describe "Nodes::IsDistinctFrom" do
+ it "should handle column names on both sides" do
+ test = Table.new(:users)[:first_name].is_distinct_from Table.new(:users)[:last_name]
+ compile(test).must_be_like %{
+ DECODE("users"."first_name", "users"."last_name", 0, 1) = 1
+ }
+ end
+
+ it "should handle nil" do
+ @table = Table.new(:users)
+ val = Nodes.build_quoted(nil, @table[:active])
+ sql = compile Nodes::IsDistinctFrom.new(@table[:name], val)
+ sql.must_be_like %{ "users"."name" IS NOT NULL }
+ end
+ end
end
end
end
diff --git a/activerecord/test/cases/arel/visitors/postgres_test.rb b/activerecord/test/cases/arel/visitors/postgres_test.rb
index f7f2c76b6f..0f9efb20b4 100644
--- a/activerecord/test/cases/arel/visitors/postgres_test.rb
+++ b/activerecord/test/cases/arel/visitors/postgres_test.rb
@@ -276,6 +276,45 @@ module Arel
}
end
end
+
+ describe "Nodes::IsNotDistinctFrom" do
+ it "should construct a valid generic SQL statement" do
+ test = Table.new(:users)[:name].is_not_distinct_from "Aaron Patterson"
+ compile(test).must_be_like %{
+ "users"."name" IS NOT DISTINCT FROM 'Aaron Patterson'
+ }
+ end
+
+ it "should handle column names on both sides" do
+ test = Table.new(:users)[:first_name].is_not_distinct_from Table.new(:users)[:last_name]
+ compile(test).must_be_like %{
+ "users"."first_name" IS NOT DISTINCT FROM "users"."last_name"
+ }
+ end
+
+ it "should handle nil" do
+ @table = Table.new(:users)
+ val = Nodes.build_quoted(nil, @table[:active])
+ sql = compile Nodes::IsNotDistinctFrom.new(@table[:name], val)
+ sql.must_be_like %{ "users"."name" IS NOT DISTINCT FROM NULL }
+ end
+ end
+
+ describe "Nodes::IsDistinctFrom" do
+ it "should handle column names on both sides" do
+ test = Table.new(:users)[:first_name].is_distinct_from Table.new(:users)[:last_name]
+ compile(test).must_be_like %{
+ "users"."first_name" IS DISTINCT FROM "users"."last_name"
+ }
+ end
+
+ it "should handle nil" do
+ @table = Table.new(:users)
+ val = Nodes.build_quoted(nil, @table[:active])
+ sql = compile Nodes::IsDistinctFrom.new(@table[:name], val)
+ sql.must_be_like %{ "users"."name" IS DISTINCT FROM NULL }
+ end
+ end
end
end
end
diff --git a/activerecord/test/cases/arel/visitors/sqlite_test.rb b/activerecord/test/cases/arel/visitors/sqlite_test.rb
index 6650b6ff3a..ee4e07a675 100644
--- a/activerecord/test/cases/arel/visitors/sqlite_test.rb
+++ b/activerecord/test/cases/arel/visitors/sqlite_test.rb
@@ -6,7 +6,11 @@ module Arel
module Visitors
class SqliteTest < Arel::Spec
before do
- @visitor = SQLite.new Table.engine.connection_pool
+ @visitor = SQLite.new Table.engine.connection
+ end
+
+ def compile(node)
+ @visitor.accept(node, Collectors::SQLString.new).value
end
it "defaults limit to -1" do
@@ -27,6 +31,45 @@ module Arel
node = Nodes::False.new()
assert_equal "0", @visitor.accept(node, Collectors::SQLString.new).value
end
+
+ describe "Nodes::IsNotDistinctFrom" do
+ it "should construct a valid generic SQL statement" do
+ test = Table.new(:users)[:name].is_not_distinct_from "Aaron Patterson"
+ compile(test).must_be_like %{
+ "users"."name" IS 'Aaron Patterson'
+ }
+ end
+
+ it "should handle column names on both sides" do
+ test = Table.new(:users)[:first_name].is_not_distinct_from Table.new(:users)[:last_name]
+ compile(test).must_be_like %{
+ "users"."first_name" IS "users"."last_name"
+ }
+ end
+
+ it "should handle nil" do
+ @table = Table.new(:users)
+ val = Nodes.build_quoted(nil, @table[:active])
+ sql = compile Nodes::IsNotDistinctFrom.new(@table[:name], val)
+ sql.must_be_like %{ "users"."name" IS NULL }
+ end
+ end
+
+ describe "Nodes::IsDistinctFrom" do
+ it "should handle column names on both sides" do
+ test = Table.new(:users)[:first_name].is_distinct_from Table.new(:users)[:last_name]
+ compile(test).must_be_like %{
+ "users"."first_name" IS NOT "users"."last_name"
+ }
+ end
+
+ it "should handle nil" do
+ @table = Table.new(:users)
+ val = Nodes.build_quoted(nil, @table[:active])
+ sql = compile Nodes::IsDistinctFrom.new(@table[:name], val)
+ sql.must_be_like %{ "users"."name" IS NOT NULL }
+ end
+ end
end
end
end
diff --git a/activerecord/test/cases/arel/visitors/to_sql_test.rb b/activerecord/test/cases/arel/visitors/to_sql_test.rb
index 680c26a9ab..4bfa799a96 100644
--- a/activerecord/test/cases/arel/visitors/to_sql_test.rb
+++ b/activerecord/test/cases/arel/visitors/to_sql_test.rb
@@ -155,6 +155,43 @@ module Arel
end
end
+ describe "Nodes::IsNotDistinctFrom" do
+ it "should construct a valid generic SQL statement" do
+ test = Table.new(:users)[:name].is_not_distinct_from "Aaron Patterson"
+ compile(test).must_be_like %{
+ CASE WHEN "users"."name" = 'Aaron Patterson' OR ("users"."name" IS NULL AND 'Aaron Patterson' IS NULL) THEN 0 ELSE 1 END = 0
+ }
+ end
+
+ it "should handle column names on both sides" do
+ test = Table.new(:users)[:first_name].is_not_distinct_from Table.new(:users)[:last_name]
+ compile(test).must_be_like %{
+ CASE WHEN "users"."first_name" = "users"."last_name" OR ("users"."first_name" IS NULL AND "users"."last_name" IS NULL) THEN 0 ELSE 1 END = 0
+ }
+ end
+
+ it "should handle nil" do
+ val = Nodes.build_quoted(nil, @table[:active])
+ sql = compile Nodes::IsNotDistinctFrom.new(@table[:name], val)
+ sql.must_be_like %{ "users"."name" IS NULL }
+ end
+ end
+
+ describe "Nodes::IsDistinctFrom" do
+ it "should handle column names on both sides" do
+ test = Table.new(:users)[:first_name].is_distinct_from Table.new(:users)[:last_name]
+ compile(test).must_be_like %{
+ CASE WHEN "users"."first_name" = "users"."last_name" OR ("users"."first_name" IS NULL AND "users"."last_name" IS NULL) THEN 0 ELSE 1 END = 1
+ }
+ end
+
+ it "should handle nil" do
+ val = Nodes.build_quoted(nil, @table[:active])
+ sql = compile Nodes::IsDistinctFrom.new(@table[:name], val)
+ sql.must_be_like %{ "users"."name" IS NOT NULL }
+ end
+ end
+
it "should visit string subclass" do
[
Class.new(String).new(":'("),
diff --git a/activerecord/test/cases/associations/has_many_through_associations_test.rb b/activerecord/test/cases/associations/has_many_through_associations_test.rb
index 7b405c74c4..cf514957ca 100644
--- a/activerecord/test/cases/associations/has_many_through_associations_test.rb
+++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb
@@ -586,6 +586,16 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
assert_not_includes posts(:welcome).reload.people.reload, people(:michael)
end
+ def test_replace_association_with_duplicates
+ post = posts(:thinking)
+ person = people(:david)
+
+ assert_difference "post.people.count", 2 do
+ post.people = [person]
+ post.people = [person, person]
+ end
+ end
+
def test_replace_order_is_preserved
posts(:welcome).people.clear
posts(:welcome).people = [people(:david), people(:michael)]
diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb
index 0dbdd56ae6..12a4027a4e 100644
--- a/activerecord/test/cases/attribute_methods_test.rb
+++ b/activerecord/test/cases/attribute_methods_test.rb
@@ -449,7 +449,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
end
test "custom field attribute predicate" do
- object = Company.find_by_sql(<<-SQL).first
+ object = Company.find_by_sql(<<~SQL).first
SELECT c1.*, c2.type as string_value, c2.rating as int_value
FROM companies c1, companies c2
WHERE c1.firm_id = c2.id
diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb
index 5c9ed42173..97bce90c8b 100644
--- a/activerecord/test/cases/calculations_test.rb
+++ b/activerecord/test/cases/calculations_test.rb
@@ -218,8 +218,8 @@ class CalculationsTest < ActiveRecord::TestCase
Account.select("credit_limit, firm_name").count
}
- assert_match %r{accounts}i, e.message
- assert_match "credit_limit, firm_name", e.message
+ assert_match %r{accounts}i, e.sql
+ assert_match "credit_limit, firm_name", e.sql
end
def test_apply_distinct_in_count
diff --git a/activerecord/test/cases/connection_adapters/connection_handlers_multi_db_test.rb b/activerecord/test/cases/connection_adapters/connection_handlers_multi_db_test.rb
index 0e4ea62113..393b577c1d 100644
--- a/activerecord/test/cases/connection_adapters/connection_handlers_multi_db_test.rb
+++ b/activerecord/test/cases/connection_adapters/connection_handlers_multi_db_test.rb
@@ -153,6 +153,20 @@ module ActiveRecord
ENV["RAILS_ENV"] = previous_env
end
+ def test_switching_connections_with_database_and_role_raises
+ error = assert_raises(ArgumentError) do
+ ActiveRecord::Base.connected_to(database: :readonly, role: :writing) { }
+ end
+ assert_equal "connected_to can only accept a `database` or a `role` argument, but not both arguments.", error.message
+ end
+
+ def test_switching_connections_without_database_and_role_raises
+ error = assert_raises(ArgumentError) do
+ ActiveRecord::Base.connected_to { }
+ end
+ assert_equal "must provide a `database` or a `role`.", error.message
+ end
+
def test_switching_connections_with_database_symbol
previous_env, ENV["RAILS_ENV"] = ENV["RAILS_ENV"], "default_env"
@@ -207,6 +221,27 @@ module ActiveRecord
ActiveRecord::Base.configurations = @prev_configs
ActiveRecord::Base.establish_connection(:arunit)
end
+
+ def test_connects_to_returns_array_of_established_connections
+ config = {
+ "development" => { "adapter" => "sqlite3", "database" => "db/primary.sqlite3" },
+ "development_readonly" => { "adapter" => "sqlite3", "database" => "db/readonly.sqlite3" }
+ }
+ @prev_configs, ActiveRecord::Base.configurations = ActiveRecord::Base.configurations, config
+
+ result = ActiveRecord::Base.connects_to database: { writing: :development, reading: :development_readonly }
+
+ assert_equal(
+ [
+ ActiveRecord::Base.connection_handlers[:writing].retrieve_connection_pool("primary"),
+ ActiveRecord::Base.connection_handlers[:reading].retrieve_connection_pool("primary")
+ ],
+ result
+ )
+ ensure
+ ActiveRecord::Base.configurations = @prev_configs
+ ActiveRecord::Base.establish_connection(:arunit)
+ end
end
def test_connection_pools
@@ -245,6 +280,43 @@ module ActiveRecord
assert_nil @rw_handler.retrieve_connection_pool("foo")
assert_nil @ro_handler.retrieve_connection_pool("foo")
end
+
+ def test_connection_handlers_are_per_thread_and_not_per_fiber
+ original_handlers = ActiveRecord::Base.connection_handlers
+
+ ActiveRecord::Base.connection_handlers = { writing: ActiveRecord::Base.default_connection_handler, reading: ActiveRecord::ConnectionAdapters::ConnectionHandler.new }
+
+ reading_handler = ActiveRecord::Base.connection_handlers[:reading]
+
+ reading = ActiveRecord::Base.with_handler(:reading) do
+ Person.connection_handler
+ end
+
+ assert_not_equal reading, ActiveRecord::Base.connection_handler
+ assert_equal reading, reading_handler
+ ensure
+ ActiveRecord::Base.connection_handlers = original_handlers
+ end
+
+ def test_connection_handlers_swapping_connections_in_fiber
+ original_handlers = ActiveRecord::Base.connection_handlers
+
+ ActiveRecord::Base.connection_handlers = { writing: ActiveRecord::Base.default_connection_handler, reading: ActiveRecord::ConnectionAdapters::ConnectionHandler.new }
+
+ reading_handler = ActiveRecord::Base.connection_handlers[:reading]
+
+ enum = Enumerator.new do |r|
+ r << ActiveRecord::Base.connection_handler
+ end
+
+ reading = ActiveRecord::Base.with_handler(:reading) do
+ enum.next
+ end
+
+ assert_equal reading, reading_handler
+ ensure
+ ActiveRecord::Base.connection_handlers = original_handlers
+ end
end
end
end
diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb
index 565190c476..02ead8d914 100644
--- a/activerecord/test/cases/query_cache_test.rb
+++ b/activerecord/test/cases/query_cache_test.rb
@@ -55,6 +55,22 @@ class QueryCacheTest < ActiveRecord::TestCase
assert_cache :off
end
+ def test_query_cache_is_applied_to_connections_in_all_handlers
+ ActiveRecord::Base.connected_to(role: :reading) do
+ ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations["arunit"])
+ end
+
+ mw = middleware { |env|
+ ro_conn = ActiveRecord::Base.connection_handlers[:reading].connection_pool_list.first.connection
+ assert_predicate ActiveRecord::Base.connection, :query_cache_enabled
+ assert_predicate ro_conn, :query_cache_enabled
+ }
+
+ mw.call({})
+ ensure
+ ActiveRecord::Base.connection_handlers = { writing: ActiveRecord::Base.default_connection_handler }
+ end
+
def test_query_cache_across_threads
with_temporary_connection_pool do
begin
diff --git a/activerecord/test/cases/relation/where_clause_test.rb b/activerecord/test/cases/relation/where_clause_test.rb
index 8703d238a0..0b06cec40b 100644
--- a/activerecord/test/cases/relation/where_clause_test.rb
+++ b/activerecord/test/cases/relation/where_clause_test.rb
@@ -92,12 +92,16 @@ class ActiveRecord::Relation
original = WhereClause.new([
table["id"].in([1, 2, 3]),
table["id"].eq(1),
+ table["id"].is_not_distinct_from(1),
+ table["id"].is_distinct_from(2),
"sql literal",
random_object
])
expected = WhereClause.new([
table["id"].not_in([1, 2, 3]),
table["id"].not_eq(1),
+ table["id"].is_distinct_from(1),
+ table["id"].is_not_distinct_from(2),
Arel::Nodes::Not.new(Arel::Nodes::SqlLiteral.new("sql literal")),
Arel::Nodes::Not.new(random_object)
])
diff --git a/activerecord/test/cases/statement_invalid_test.rb b/activerecord/test/cases/statement_invalid_test.rb
new file mode 100644
index 0000000000..16ea69c1bd
--- /dev/null
+++ b/activerecord/test/cases/statement_invalid_test.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/book"
+
+module ActiveRecord
+ class StatementInvalidTest < ActiveRecord::TestCase
+ fixtures :books
+
+ class MockDatabaseError < StandardError
+ def result
+ 0
+ end
+
+ def error_number
+ 0
+ end
+ end
+
+ test "message contains no sql" do
+ sql = Book.where(author_id: 96, cover: "hard").to_sql
+ error = assert_raises(ActiveRecord::StatementInvalid) do
+ Book.connection.send(:log, sql, Book.name) do
+ raise MockDatabaseError
+ end
+ end
+ assert_not error.message.include?("SELECT")
+ end
+
+ test "statement and binds are set on select" do
+ sql = Book.where(author_id: 96, cover: "hard").to_sql
+ binds = [Minitest::Mock.new, Minitest::Mock.new]
+ error = assert_raises(ActiveRecord::StatementInvalid) do
+ Book.connection.send(:log, sql, Book.name, binds) do
+ raise MockDatabaseError
+ end
+ end
+ assert_equal error.sql, sql
+ assert_equal error.binds, binds
+ end
+ end
+end
diff --git a/activerecord/test/cases/view_test.rb b/activerecord/test/cases/view_test.rb
index 7e2d66c62a..88e5e4a421 100644
--- a/activerecord/test/cases/view_test.rb
+++ b/activerecord/test/cases/view_test.rb
@@ -20,7 +20,7 @@ module ViewBehavior
def setup
super
@connection = ActiveRecord::Base.connection
- create_view "ebooks'", <<-SQL
+ create_view "ebooks'", <<~SQL
SELECT id, name, status FROM books WHERE format = 'ebook'
SQL
end
@@ -106,7 +106,7 @@ if ActiveRecord::Base.connection.supports_views?
setup do
@connection = ActiveRecord::Base.connection
- @connection.execute <<-SQL
+ @connection.execute <<~SQL
CREATE VIEW paperbacks
AS SELECT name, status FROM books WHERE format = 'paperback'
SQL
@@ -169,7 +169,7 @@ if ActiveRecord::Base.connection.supports_views?
setup do
@connection = ActiveRecord::Base.connection
- @connection.execute <<-SQL
+ @connection.execute <<~SQL
CREATE VIEW printed_books
AS SELECT id, name, status, format FROM books WHERE format = 'paperback'
SQL
diff --git a/activerecord/test/schema/mysql2_specific_schema.rb b/activerecord/test/schema/mysql2_specific_schema.rb
index ccca9a2d9b..61e9bc9af7 100644
--- a/activerecord/test/schema/mysql2_specific_schema.rb
+++ b/activerecord/test/schema/mysql2_specific_schema.rb
@@ -54,33 +54,25 @@ ActiveRecord::Schema.define do
t.binary :binary_column, limit: 1
end
- ActiveRecord::Base.connection.execute <<-SQL
-DROP PROCEDURE IF EXISTS ten;
-SQL
-
- ActiveRecord::Base.connection.execute <<-SQL
-CREATE PROCEDURE ten() SQL SECURITY INVOKER
-BEGIN
- select 10;
-END
-SQL
+ create_table :enum_tests, id: false, force: true do |t|
+ t.column :enum_column, "ENUM('text','blob','tiny','medium','long','unsigned','bigint')"
+ end
- ActiveRecord::Base.connection.execute <<-SQL
-DROP PROCEDURE IF EXISTS topics;
-SQL
+ execute "DROP PROCEDURE IF EXISTS ten"
- ActiveRecord::Base.connection.execute <<-SQL
-CREATE PROCEDURE topics(IN num INT) SQL SECURITY INVOKER
-BEGIN
- select * from topics limit num;
-END
-SQL
+ execute <<~SQL
+ CREATE PROCEDURE ten() SQL SECURITY INVOKER
+ BEGIN
+ SELECT 10;
+ END
+ SQL
- ActiveRecord::Base.connection.drop_table "enum_tests", if_exists: true
+ execute "DROP PROCEDURE IF EXISTS topics"
- ActiveRecord::Base.connection.execute <<-SQL
-CREATE TABLE enum_tests (
- enum_column ENUM('text','blob','tiny','medium','long','unsigned','bigint')
-)
-SQL
+ execute <<~SQL
+ CREATE PROCEDURE topics(IN num INT) SQL SECURITY INVOKER
+ BEGIN
+ SELECT * FROM topics LIMIT num;
+ END
+ SQL
end
diff --git a/activerecord/test/schema/oracle_specific_schema.rb b/activerecord/test/schema/oracle_specific_schema.rb
index bc1e45ca80..08c6e24555 100644
--- a/activerecord/test/schema/oracle_specific_schema.rb
+++ b/activerecord/test/schema/oracle_specific_schema.rb
@@ -7,23 +7,21 @@ ActiveRecord::Schema.define do
execute "drop table defaults" rescue nil
execute "drop sequence defaults_seq" rescue nil
- execute <<-SQL
-create table test_oracle_defaults (
- id integer not null primary key,
- test_char char(1) default 'X' not null,
- test_string varchar2(20) default 'hello' not null,
- test_int integer default 3 not null
-)
+ execute <<~SQL
+ create table test_oracle_defaults (
+ id integer not null primary key,
+ test_char char(1) default 'X' not null,
+ test_string varchar2(20) default 'hello' not null,
+ test_int integer default 3 not null
+ )
SQL
- execute <<-SQL
-create sequence test_oracle_defaults_seq minvalue 10000
- SQL
+ execute "create sequence test_oracle_defaults_seq minvalue 10000"
execute "create sequence companies_nonstd_seq minvalue 10000"
- execute <<-SQL
- CREATE TABLE defaults (
+ execute <<~SQL
+ CREATE TABLE defaults (
id integer not null,
modified_date date default sysdate,
modified_date_function date default sysdate,
@@ -34,7 +32,7 @@ create sequence test_oracle_defaults_seq minvalue 10000
char1 varchar2(1) default 'Y',
char2 varchar2(50) default 'a varchar field',
char3 clob default 'a text field'
- )
+ )
SQL
execute "create sequence defaults_seq minvalue 10000"
end
diff --git a/activesupport/lib/active_support/core_ext/file/atomic.rb b/activesupport/lib/active_support/core_ext/file/atomic.rb
index 8e288833b6..9deceb1bb4 100644
--- a/activesupport/lib/active_support/core_ext/file/atomic.rb
+++ b/activesupport/lib/active_support/core_ext/file/atomic.rb
@@ -29,7 +29,7 @@ class File
old_stat = if exist?(file_name)
# Get original file permissions
stat(file_name)
- elsif temp_dir != dirname(file_name)
+ else
# If not possible, probe which are the default permissions in the
# destination directory.
probe_stat_in(dirname(file_name))
diff --git a/activesupport/lib/active_support/core_ext/hash/slice.rb b/activesupport/lib/active_support/core_ext/hash/slice.rb
index 2bd0a56ea4..e07a5498ff 100644
--- a/activesupport/lib/active_support/core_ext/hash/slice.rb
+++ b/activesupport/lib/active_support/core_ext/hash/slice.rb
@@ -27,8 +27,9 @@ class Hash
# Replaces the hash with only the given keys.
# Returns a hash containing the removed key/value pairs.
#
- # { a: 1, b: 2, c: 3, d: 4 }.slice!(:a, :b)
- # # => {:c=>3, :d=>4}
+ # hash = { a: 1, b: 2, c: 3, d: 4 }
+ # hash.slice!(:a, :b) # => {:c=>3, :d=>4}
+ # hash # => {:a=>1, :b=>2}
def slice!(*keys)
omit = slice(*self.keys - keys)
hash = slice(*keys)
diff --git a/activesupport/lib/active_support/notifications/instrumenter.rb b/activesupport/lib/active_support/notifications/instrumenter.rb
index f8344912bb..a721187d2d 100644
--- a/activesupport/lib/active_support/notifications/instrumenter.rb
+++ b/activesupport/lib/active_support/notifications/instrumenter.rb
@@ -52,8 +52,12 @@ module ActiveSupport
end
class Event
- attr_reader :name, :time, :transaction_id, :payload, :children
- attr_accessor :end
+ attr_reader :name, :time, :end, :transaction_id, :payload, :children
+
+ def self.clock_gettime_supported? # :nodoc:
+ defined?(Process::CLOCK_PROCESS_CPUTIME_ID) &&
+ !Gem.win_platform?
+ end
def initialize(name, start, ending, transaction_id, payload)
@name = name
@@ -83,6 +87,11 @@ module ActiveSupport
@allocation_count_finish = now_allocations
end
+ def end=(ending)
+ ActiveSupport::Deprecation.deprecation_warning(:end=, :finish!)
+ @end = ending
+ end
+
# Returns the CPU time (in milliseconds) passed since the call to
# +start!+ and the call to +finish!+
def cpu_time
@@ -130,7 +139,7 @@ module ActiveSupport
Process.clock_gettime(Process::CLOCK_MONOTONIC)
end
- if defined?(Process::CLOCK_PROCESS_CPUTIME_ID)
+ if clock_gettime_supported?
def now_cpu
Process.clock_gettime(Process::CLOCK_PROCESS_CPUTIME_ID)
end
diff --git a/activesupport/lib/active_support/testing/parallelization.rb b/activesupport/lib/active_support/testing/parallelization.rb
index 9c8dffa9d8..8de01eb19b 100644
--- a/activesupport/lib/active_support/testing/parallelization.rb
+++ b/activesupport/lib/active_support/testing/parallelization.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require "drb"
-require "drb/unix"
+require "drb/unix" unless Gem.win_platform?
require "active_support/core_ext/module/attribute_accessors"
module ActiveSupport
@@ -15,6 +15,8 @@ module ActiveSupport
end
def record(reporter, result)
+ raise DRb::DRbConnError if result.is_a?(DRb::DRbUnknown)
+
reporter.synchronize do
reporter.record(result)
end
diff --git a/activesupport/test/core_ext/file_test.rb b/activesupport/test/core_ext/file_test.rb
index 9c97700e5d..186c863f91 100644
--- a/activesupport/test/core_ext/file_test.rb
+++ b/activesupport/test/core_ext/file_test.rb
@@ -59,6 +59,20 @@ class AtomicWriteTest < ActiveSupport::TestCase
File.unlink(file_name) rescue nil
end
+ def test_atomic_write_preserves_file_permissions_same_directory
+ Dir.mktmpdir do |temp_dir|
+ File.chmod 0700, temp_dir
+
+ probed_permissions = File.probe_stat_in(temp_dir).mode.to_s(8)
+
+ File.atomic_write(File.join(temp_dir, file_name), &:close)
+
+ actual_permissions = File.stat(File.join(temp_dir, file_name)).mode.to_s(8)
+
+ assert_equal actual_permissions, probed_permissions
+ end
+ end
+
def test_atomic_write_returns_result_from_yielded_block
block_return_value = File.atomic_write(file_name, Dir.pwd) do |file|
"Hello world!"
diff --git a/activesupport/test/dependencies_test.rb b/activesupport/test/dependencies_test.rb
index 9f2755a25c..b1b3070891 100644
--- a/activesupport/test/dependencies_test.rb
+++ b/activesupport/test/dependencies_test.rb
@@ -284,7 +284,7 @@ class DependenciesTest < ActiveSupport::TestCase
def test_module_with_nested_class_requiring_lib_class
with_autoloading_fixtures do
- ModuleFolder::NestedWithRequire
+ _ = ModuleFolder::NestedWithRequire # assignment to silence parse-time warning "possibly useless use of :: in void context"
assert defined?(ModuleFolder::LibClass)
assert_not ActiveSupport::Dependencies.autoloaded_constants.include?("ModuleFolder::LibClass")
@@ -297,7 +297,7 @@ class DependenciesTest < ActiveSupport::TestCase
def test_module_with_nested_class_and_parent_requiring_lib_class
with_autoloading_fixtures do
- NestedWithRequireParent
+ _ = NestedWithRequireParent # assignment to silence parse-time warning "possibly useless use of a constant in void context"
assert defined?(ModuleFolder::LibClass)
assert_not ActiveSupport::Dependencies.autoloaded_constants.include?("ModuleFolder::LibClass")
diff --git a/guides/assets/stylesheets/main.rtl.css b/guides/assets/stylesheets/main.rtl.css
new file mode 100644
index 0000000000..ea31d6017c
--- /dev/null
+++ b/guides/assets/stylesheets/main.rtl.css
@@ -0,0 +1,762 @@
+/* Guides.rubyonrails.org */
+/* Main.css */
+/* Created January 30, 2009 */
+/* Modified February 8, 2009
+--------------------------------------- */
+
+/* General
+--------------------------------------- */
+
+.right {float: right; margin-left: 1em;}
+.left {float: left; margin-right: 1em;}
+@media screen and (max-width: 480px) {
+ .right, .left { float: none; }
+}
+.small {font-size: smaller;}
+.large {font-size: larger;}
+.hide {display: none;}
+
+ul, ol { margin: 0 1.5em 1.5em 1.5em; }
+
+ul { list-style-type: disc; }
+ol { list-style-type: decimal; }
+
+dl { margin: 0 0 1.5em 0; }
+dl dt { font-weight: bold; }
+dd { margin-right: 1.5em;}
+
+pre, code {
+ font-size: 1em;
+ font-family: "Anonymous Pro", "Inconsolata", "Menlo", "Consolas", "Bitstream Vera Sans Mono", "Courier New", monospace;
+ line-height: 1.5;
+ margin: 1.5em 0;
+ overflow: auto;
+ color: #222;
+}
+
+p code {
+ background: #eee;
+ border-radius: 2px;
+ padding: 1px 3px;
+}
+
+pre, tt, code {
+ white-space: pre-wrap; /* css-3 */
+ white-space: -moz-pre-wrap !important; /* Mozilla, since 1999 */
+ white-space: -pre-wrap; /* Opera 4-6 */
+ white-space: -o-pre-wrap; /* Opera 7 */
+ word-wrap: break-word; /* Internet Explorer 5.5+ */
+}
+
+abbr, acronym { border-bottom: 1px dotted #666; }
+address { margin: 0 0 1.5em; font-style: italic; }
+del { color:#666; }
+
+blockquote { margin: 1.5em; color: #666; font-style: italic; }
+strong { font-weight: bold; }
+em, dfn { font-style: italic; }
+dfn { font-weight: bold; }
+sup, sub { line-height: 0; }
+p {margin: 0 0 1.5em;}
+
+label { font-weight: bold; }
+fieldset { padding:1.4em; margin: 0 0 1.5em 0; border: 1px solid #ccc; }
+legend { font-weight: bold; font-size:1.2em; }
+
+input.text, input.title,
+textarea, select {
+ margin:0.5em 0;
+ border:1px solid #bbb;
+}
+
+table {
+ margin: 0 0 1.5em;
+ border: 2px solid #CCC;
+ background: #FFF;
+ border-collapse: collapse;
+}
+
+table th, table td {
+ padding: 9px 10px;
+ border: 1px solid #CCC;
+ border-collapse: collapse;
+}
+
+table th {
+ border-bottom: 2px solid #CCC;
+ background: #EEE;
+ font-weight: bold;
+}
+
+img {
+ max-width: 100%;
+}
+
+
+/* Structure and Layout
+--------------------------------------- */
+
+body {
+ text-align: center;
+ font-family: Helvetica, Arial, sans-serif;
+ font-size: 87.5%;
+ line-height: 1.5em;
+ background: #fff;
+ color: #999;
+ direction: rtl;
+}
+
+.wrapper {
+ text-align: right;
+ margin: 0 auto;
+ max-width: 960px;
+ padding: 0 1em;
+}
+
+.red-button {
+ display: inline-block;
+ border-top: 1px solid rgba(255,255,255,.5);
+ background: #751913;
+ background: -webkit-gradient(linear, right top, right bottom, from(#c52f24), to(#751913));
+ background: -webkit-linear-gradient(top, #c52f24, #751913);
+ background: -moz-linear-gradient(top, #c52f24, #751913);
+ background: -ms-linear-gradient(top, #c52f24, #751913);
+ background: -o-linear-gradient(top, #c52f24, #751913);
+ padding: 9px 18px;
+ -webkit-border-radius: 11px;
+ -moz-border-radius: 11px;
+ border-radius: 11px;
+ -webkit-box-shadow: rgba(0,0,0,1) 0 1px 0;
+ -moz-box-shadow: rgba(0,0,0,1) 0 1px 0;
+ box-shadow: rgba(0,0,0,1) 0 1px 0;
+ text-shadow: rgba(0,0,0,.4) 0 1px 0;
+ color: white;
+ font-size: 15px;
+ font-family: Helvetica, Arial, Sans-Serif;
+ text-decoration: none;
+ vertical-align: middle;
+ cursor: pointer;
+}
+.red-button:active {
+ border-top: none;
+ padding-top: 10px;
+ background: -webkit-gradient(linear, right top, right bottom, from(#751913), to(#c52f24));
+ background: -webkit-linear-gradient(top, #751913, #c52f24);
+ background: -moz-linear-gradient(top, #751913, #c52f24);
+ background: -ms-linear-gradient(top, #751913, #c52f24);
+ background: -o-linear-gradient(top, #751913, #c52f24);
+}
+
+#topNav {
+ padding: 1em 0;
+ color: #565656;
+ background: #222;
+}
+
+.s-hidden {
+ display: none;
+}
+
+@media screen and (min-width: 1025px) {
+ .more-info-button {
+ display: none;
+ }
+ .more-info-links {
+ list-style: none;
+ display: inline;
+ margin: 0;
+ }
+
+ .more-info {
+ display: inline-block;
+ }
+ .more-info:after {
+ content: " |";
+ }
+
+ .more-info:last-child:after {
+ content: "";
+ }
+}
+
+@media screen and (max-width: 1024px) {
+ #topNav .wrapper { text-align: center; }
+ .more-info-button {
+ position: relative;
+ z-index: 25;
+ }
+
+ .more-info-label {
+ display: none;
+ }
+
+ .more-info-container {
+ position: absolute;
+ top: .5em;
+ z-index: 20;
+ margin: 0 auto;
+ right: 0;
+ left: 0;
+ width: 20em;
+ }
+
+ .more-info-links {
+ display: block;
+ list-style: none;
+ background-color: #c52f24;
+ border-radius: 5px;
+ padding-top: 5.25em;
+ border: 1px #980905 solid;
+ }
+ .more-info-links.s-hidden {
+ display: none;
+ }
+ .more-info {
+ padding: .75em;
+ border-top: 1px #980905 solid;
+ }
+ .more-info a, .more-info a:link, .more-info a:visited {
+ display: block;
+ color: white;
+ width: 100%;
+ height: 100%;
+ text-decoration: none;
+ text-transform: uppercase;
+ }
+}
+
+#header {
+ background: #c52f24 url(../images/header_tile.gif) repeat-x;
+ color: #FFF;
+ padding: 1.5em 0;
+ z-index: 99;
+}
+
+#feature {
+ background: #d5e9f6 url(../images/feature_tile.gif) repeat-x;
+ color: #333;
+ padding: 0.5em 0 1.5em;
+}
+
+#container {
+ color: #333;
+ padding: 0.5em 0 1.5em 0;
+}
+
+#mainCol {
+ max-width: 630px;
+ margin-right: 2em;
+}
+
+#subCol {
+ position: absolute;
+ z-index: 0;
+ top: 21px;
+ left: 0;
+ background: #FFF;
+ padding: 1em 1.5em 1em 1.25em;
+ width: 17em;
+ font-size: 0.9285em;
+ line-height: 1.3846em;
+ margin-left: 1em;
+}
+
+
+@media screen and (max-width: 800px) {
+ #subCol {
+ position: static;
+ width: inherit;
+ margin-right: -1em;
+ margin-left: 0;
+ padding-left: 1.25em;
+ }
+}
+
+#footer {
+ padding: 2em 0;
+ background: #222 url(../images/footer_tile.gif) repeat-x;
+}
+#footer .wrapper {
+ padding-right: 1em;
+ max-width: 960px;
+}
+
+#header .wrapper, #topNav .wrapper, #feature .wrapper {padding-right: 1em; max-width: 960px;}
+#feature .wrapper {max-width: 640px; padding-left: 23em; position: relative; z-index: 0;}
+
+@media screen and (max-width: 960px) {
+ #container .wrapper { padding-left: 23em; }
+}
+
+@media screen and (max-width: 800px) {
+ #feature .wrapper, #container .wrapper { padding-left: 0; }
+}
+
+/* Links
+--------------------------------------- */
+
+a, a:link, a:visited {
+ color: #ee3f3f;
+ text-decoration: underline;
+}
+
+#mainCol a, #subCol a, #feature a {color: #980905;}
+#mainCol a code, #subCol a code, #feature a code {color: #980905;}
+
+#mainCol a.anchorlink, #mainCol a.anchorlink code {color: #333;}
+#mainCol a.anchorlink { text-decoration: none; }
+#mainCol a.anchorlink:hover { text-decoration: underline; }
+
+/* Navigation
+--------------------------------------- */
+
+.nav {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+ float: left;
+ margin-top: 1.5em;
+ font-size: 1.2857em;
+}
+
+.nav .nav-item {color: #FFF; text-decoration: none;}
+.nav .nav-item:hover {text-decoration: underline;}
+
+.guides-index-large, .guides-index-small .guides-index-item {
+ padding: 0.5em 1.5em;
+ border-radius: 1em;
+ -webkit-border-radius: 1em;
+ -moz-border-radius: 1em;
+ background: #980905;
+ position: relative;
+ color: white;
+}
+
+.guides-index .guides-index-item {
+ background: #980905 url(../images/nav_arrow.gif) no-repeat left top;
+ padding-left: 1em;
+ position: relative;
+ z-index: 15;
+ padding-bottom: 0.125em;
+}
+
+.guides-index:hover .guides-index-item, .guides-index .guides-index-item:hover {
+ background-position: left -81px;
+ text-decoration: underline !important;
+}
+
+@media screen and (min-width: 481px) {
+ .nav {
+ float: left;
+ margin-top: 1.5em;
+ font-size: 1.2857em;
+ }
+ .nav>li {
+ display: inline;
+ margin-right: 0.5em;
+ }
+ .guides-index.guides-index-small {
+ display: none;
+ }
+}
+
+@media screen and (max-width: 480px) {
+ .nav {
+ float: none;
+ width: 100%;
+ text-align: center;
+ }
+ .nav .nav-item {
+ display: block;
+ margin: 0;
+ width: 100%;
+ background-color: #980905;
+ border: solid 1px #620c04;
+ border-top: 0;
+ padding: 15px 0;
+ text-align: center;
+ }
+ .nav .nav-item, .nav-item.guides-index-item {
+ text-transform: uppercase;
+ }
+ .nav .nav-item:first-child, .nav-item.guides-index-small {
+ border-top: solid 1px #620c04;
+ }
+ .guides-index.guides-index-small {
+ display: block;
+ margin-top: 1.5em;
+ }
+ .guides-index.guides-index-large {
+ display: none;
+ }
+ .guides-index-small .guides-index-item {
+ font: inherit;
+ padding-right: .75em;
+ font-size: .95em;
+ background-position: 96% 16px;
+ -webkit-appearance: none;
+ }
+ .guides-index-small .guides-index-item:hover{
+ background-position: 96% -65px;
+ }
+}
+
+#guides {
+ width: 37em;
+ display: block;
+ background: #980905;
+ border-radius: 1em;
+ color: #f1938c;
+ padding: 1.5em 2em;
+ position: absolute;
+ z-index: 10;
+ top: -0.25em;
+ left: 0;
+ padding-top: 2em;
+}
+
+#guides.visible {
+ display: block !important;
+}
+
+.guides-section dt, .guides-section dd {
+ font-weight: normal;
+ font-size: 0.722em;
+ margin: 0;
+ padding: 0;
+}
+.guides-section dt {
+ margin: 0.5em 0 0;
+ padding:0;
+}
+#guides a {
+ background: none !important;
+ color: #FFF;
+ text-decoration: none;
+}
+#guides a:hover {
+ text-decoration: underline;
+}
+.guides-section-container {
+ display: flex;
+ flex-direction: column;
+ flex-wrap: wrap;
+ width: 100%;
+ max-height: 35em;
+}
+
+.guides-section {
+ min-width: 5em;
+ margin: 0 2em 0.5em 0;
+ flex: auto;
+ max-width: 12em;
+}
+
+.guides-section dd {
+ line-height: 1.3;
+ margin-bottom: 0.5em;
+}
+
+#guides hr {
+ display: block;
+ border: none;
+ height: 1px;
+ color: #f1938c;
+ background: #f1938c;
+}
+
+/* Headings
+--------------------------------------- */
+
+h1 {
+ font-size: 2.5em;
+ line-height: 1em;
+ margin: 0.6em 0 .2em;
+ font-weight: bold;
+}
+
+h2 {
+ font-size: 2.1428em;
+ line-height: 1em;
+ margin: 0.7em 0 .2333em;
+ font-weight: bold;
+}
+
+@media screen and (max-width: 480px) {
+ h2 {
+ font-size: 1.45em;
+ }
+}
+
+h3 {
+ font-size: 1.7142em;
+ line-height: 1.286em;
+ margin: 0.875em 0 0.2916em;
+ font-weight: bold;
+}
+
+@media screen and (max-width: 480px) {
+ h3 {
+ font-size: 1.45em;
+ }
+}
+
+h4 {
+ font-size: 1.2857em;
+ line-height: 1.2em;
+ margin: 1.6667em 0 .3887em;
+ font-weight: bold;
+}
+
+h5 {
+ font-size: 1em;
+ line-height: 1.5em;
+ margin: 1em 0 .5em;
+ font-weight: bold;
+}
+
+h6 {
+ font-size: 1em;
+ line-height: 1.5em;
+ margin: 1em 0 .5em;
+ font-weight: normal;
+}
+
+.section {
+ padding-bottom: 0.25em;
+ border-bottom: 1px solid #999;
+}
+
+/* Content
+--------------------------------------- */
+
+.pic {
+ margin: 0 2em 2em 0;
+}
+
+#topNav strong {color: #999; margin-left: 0.5em;}
+#topNav strong a {color: #FFF;}
+
+#header h1 {
+ float: right;
+ background: url(../images/rails_guides_logo_1x.png) no-repeat;
+ width: 297px;
+ text-indent: -9999em;
+ margin: 0;
+ padding: 0;
+}
+
+@media
+only screen and (-webkit-min-device-pixel-ratio: 2),
+only screen and ( min--moz-device-pixel-ratio: 2),
+only screen and ( -o-min-device-pixel-ratio: 2/1),
+only screen and ( min-device-pixel-ratio: 2),
+only screen and ( min-resolution: 192dpi),
+only screen and ( min-resolution: 2dppx) {
+ #header h1 {
+ background: url(../images/rails_guides_logo_2x.png) no-repeat;
+ background-size: 160%;
+ }
+}
+
+@media screen and (max-width: 480px) {
+ #header h1 {
+ float: none;
+ }
+}
+
+#header h1 a {
+ text-decoration: none;
+ display: block;
+ height: 77px;
+}
+
+#feature p {
+ font-size: 1.2857em;
+ margin-bottom: 0.75em;
+}
+
+@media screen and (max-width: 480px) {
+ #feature p {
+ font-size: 1em;
+ }
+}
+
+#feature ul {margin-right: 0;}
+#feature ul li {
+ list-style: none;
+ background: url(../images/check_bullet.gif) no-repeat right 0.5em;
+ padding: 0.5em 1.75em 0.5em 1.75em;
+ font-size: 1.1428em;
+ font-weight: bold;
+}
+
+#mainCol dd, #subCol dd {
+ padding: 0.25em 0 1em;
+ border-bottom: 1px solid #CCC;
+ margin-bottom: 1em;
+ margin-right: 0;
+ /*padding-right: 28px;*/
+ padding-right: 0;
+}
+
+#mainCol dt, #subCol dt {
+ font-size: 1.2857em;
+ padding: 0.125em 0 0.25em 0;
+ margin-bottom: 0;
+}
+
+@media screen and (max-width: 480px) {
+ #mainCol dt, #subCol dt {
+ font-size: 1em;
+ }
+}
+
+#mainCol dd.work-in-progress, #subCol dd.work-in-progress {
+ background: #fff9d8 url(../images/tab_yellow.gif) no-repeat left top;
+ border: none;
+ padding: 1.25em 1em 1.25em 48px;
+ margin-right: 0;
+ margin-top: 0.25em;
+}
+
+#mainCol dd.kindle, #subCol dd.kindle {
+ background: #d5e9f6 url(../images/tab_info.gif) no-repeat left top;
+ border: none;
+ padding: 1.25em 1em 1.25em 48px;
+ margin-right: 0;
+ margin-top: 0.25em;
+}
+
+#mainCol div.warning, #subCol dd.warning {
+ background: #f9d9d8 url(../images/tab_red.gif) no-repeat left top;
+ border: none;
+ padding: 1.25em 1.25em 0.25em 48px;
+ margin-right: 0;
+ margin-top: 0.25em;
+}
+
+#subCol .chapters {color: #980905;}
+#subCol .chapters a {font-weight: bold;}
+#subCol .chapters ul a {font-weight: normal;}
+#subCol .chapters li {margin-bottom: 0.75em;}
+#subCol h3.chapter {margin-top: 0.25em;}
+#subCol h3.chapter img {vertical-align: text-bottom;}
+#subCol .chapters ul {margin-right: 0; margin-top: 0.5em;}
+#subCol .chapters ul li {
+ list-style: none;
+ padding: 0 1em 0 0;
+ background: url(../images/bullet.gif) no-repeat right 0.45em;
+ margin-right: 0;
+ font-size: 1em;
+ font-weight: normal;
+}
+
+#subCol li ul, li ol { margin:0 1.5em; }
+
+div.code_container {
+ background: #EEE url(../images/tab_grey.gif) no-repeat right top;
+ padding: 0.25em 48px 0.5em 1em;
+}
+
+.note {
+ background: #fff9d8 url(../images/tab_note.gif) no-repeat right top;
+ border: none;
+ padding: 1em 48px 0.25em 1em;
+ margin: 0.25em 0 1.5em 0;
+}
+
+.info {
+ background: #d5e9f6 url(../images/tab_info.gif) no-repeat right top;
+ border: none;
+ padding: 1em 48px 0.25em 1em;
+ margin: 0.25em 0 1.5em 0;
+}
+
+#mainCol div.todo {
+ background: #fff9d8 url(../images/tab_yellow.gif) no-repeat right top;
+ border: none;
+ padding: 1em 48px 0.25em 1em;
+ margin: 0.25em 0 1.5em 0;
+}
+
+.note code, .info code, .todo code {
+ background: #fff;
+}
+
+#mainCol ul li {
+ list-style:none;
+ background: url(../images/grey_bullet.gif) no-repeat right 0.5em;
+ padding-right: 1em;
+ margin-right: 0;
+}
+
+#subCol .content {
+ font-size: 0.7857em;
+ line-height: 1.5em;
+}
+
+#subCol .content li {
+ font-weight: normal;
+ background: none;
+ padding: 0 0 1em;
+ font-size: 1.1667em;
+}
+
+/* Clearing
+--------------------------------------- */
+
+.clearfix:after {
+ content: ".";
+ display: block;
+ height: 0;
+ clear: both;
+ visibility: hidden;
+}
+
+* html .clearfix {height: 1%;}
+.clearfix {display: block;}
+
+/* Same bottom margin for special boxes than for regular paragraphs, this way
+intermediate whitespace looks uniform. */
+div.code_container, div.important, div.caution, div.warning, div.note, div.info {
+ margin-bottom: 1.5em;
+}
+
+/* Remove bottom margin of paragraphs in special boxes, otherwise they get a
+spurious blank area below with the box background. */
+div.important p, div.caution p, div.warning p, div.note p, div.info p {
+ margin-bottom: 1em;
+}
+
+/* Edge Badge
+--------------------------------------- */
+
+#edge-badge {
+ position: fixed;
+ right: 0px;
+ top: 0px;
+ z-index: 100;
+ border: none;
+}
+
+/* Foundation v2.1.4 http://foundation.zurb.com */
+/* Artfully masterminded by ZURB */
+
+/* Mobile */
+@media only screen and (max-width: 767px) {
+ table.responsive { margin-bottom: 0; }
+
+ .pinned { position: absolute; right: 0; top: 0; background: #fff; width: 35%; overflow: hidden; overflow-x: scroll; border-left: 1px solid #ccc; border-right: 1px solid #ccc; }
+ .pinned table { border-left: none; border-right: none; width: 100%; }
+ .pinned table th, .pinned table td { white-space: nowrap; }
+ .pinned td:last-child { border-bottom: 0; }
+
+ div.table-wrapper { position: relative; margin-bottom: 20px; overflow: hidden; border-left: 1px solid #ccc; }
+ div.table-wrapper div.scrollable table { margin-right: 35%; }
+ div.table-wrapper div.scrollable { overflow: scroll; overflow-y: hidden; }
+
+ table.responsive td, table.responsive th { position: relative; white-space: nowrap; overflow: hidden; }
+ table.responsive th:first-child, table.responsive td:first-child, table.responsive td:first-child, table.responsive.pinned td { display: none; }
+
+}
diff --git a/guides/rails_guides.rb b/guides/rails_guides.rb
index f2d4d6f647..28a7c94ca9 100644
--- a/guides/rails_guides.rb
+++ b/guides/rails_guides.rb
@@ -20,10 +20,11 @@ version = env_value["RAILS_VERSION"]
edge = `git rev-parse HEAD`.strip unless version
RailsGuides::Generator.new(
- edge: edge,
- version: version,
- all: env_flag["ALL"],
- only: env_value["ONLY"],
- kindle: env_flag["KINDLE"],
- language: env_value["GUIDES_LANGUAGE"]
+ edge: edge,
+ version: version,
+ all: env_flag["ALL"],
+ only: env_value["ONLY"],
+ kindle: env_flag["KINDLE"],
+ language: env_value["GUIDES_LANGUAGE"],
+ direction: env_value["DIRECTION"].to_sym
).generate
diff --git a/guides/rails_guides/generator.rb b/guides/rails_guides/generator.rb
index c83538ad48..1f0a2063f2 100644
--- a/guides/rails_guides/generator.rb
+++ b/guides/rails_guides/generator.rb
@@ -17,13 +17,14 @@ module RailsGuides
class Generator
GUIDES_RE = /\.(?:erb|md)\z/
- def initialize(edge:, version:, all:, only:, kindle:, language:)
- @edge = edge
- @version = version
- @all = all
- @only = only
- @kindle = kindle
- @language = language
+ def initialize(edge:, version:, all:, only:, kindle:, language:, direction: :ltr)
+ @edge = edge
+ @version = version
+ @all = all
+ @only = only
+ @kindle = kindle
+ @language = language
+ @direction = direction
if @kindle
check_for_kindlegen
@@ -116,6 +117,14 @@ module RailsGuides
def copy_assets
FileUtils.cp_r(Dir.glob("#{@guides_dir}/assets/*"), @output_dir)
+
+ if @direction == :rtl
+ overwrite_css_with_right_to_left_direction
+ end
+ end
+
+ def overwrite_css_with_right_to_left_direction
+ FileUtils.mv("#{@output_dir}/stylesheets/main.rtl.css", "#{@output_dir}/stylesheets/main.css")
end
def output_file_for(guide)
@@ -198,7 +207,7 @@ module RailsGuides
def check_fragment_identifiers(html, anchors)
html.scan(/<a\s+href="#([^"]+)/).flatten.each do |fragment_identifier|
next if fragment_identifier == "mainCol" # in layout, jumps to some DIV
- unless anchors.member?(fragment_identifier)
+ unless anchors.member?(CGI.unescape(fragment_identifier))
guess = anchors.min { |a, b|
Levenshtein.distance(fragment_identifier, a) <=> Levenshtein.distance(fragment_identifier, b)
}
diff --git a/guides/source/active_job_basics.md b/guides/source/active_job_basics.md
index 4dc69ef911..39239852ca 100644
--- a/guides/source/active_job_basics.md
+++ b/guides/source/active_job_basics.md
@@ -289,7 +289,7 @@ style if the code inside your block is so short that it fits in a single line.
For example, you could send metrics for every job enqueued:
```ruby
-class ApplicationJob
+class ApplicationJob < ActiveJob::Base
before_enqueue { |job| $statsd.increment "#{job.class.name.underscore}.enqueue" }
end
```
diff --git a/guides/source/caching_with_rails.md b/guides/source/caching_with_rails.md
index 321eee637f..67b097f2ae 100644
--- a/guides/source/caching_with_rails.md
+++ b/guides/source/caching_with_rails.md
@@ -302,7 +302,7 @@ class Product < ApplicationRecord
end
```
-NOTE: Notice that in this example we used the `cache_key` method, so the resulting cache key will be something like `products/233-20140225082222765838000/competing_price`. `cache_key` generates a string based on the model's `id` and `updated_at` attributes. This is a common convention and has the benefit of invalidating the cache whenever the product is updated. In general, when you use low-level caching for instance level information, you need to generate a cache key.
+NOTE: Notice that in this example we used the `cache_key_with_version` method, so the resulting cache key will be something like `products/233-20140225082222765838000/competing_price`. `cache_key_with_version` generates a string based on the model's `id` and `updated_at` attributes. This is a common convention and has the benefit of invalidating the cache whenever the product is updated. In general, when you use low-level caching for instance level information, you need to generate a cache key.
### SQL Caching
@@ -563,7 +563,7 @@ class ProductsController < ApplicationController
# If the request is stale according to the given timestamp and etag value
# (i.e. it needs to be processed again) then execute this block
- if stale?(last_modified: @product.updated_at.utc, etag: @product.cache_key)
+ if stale?(last_modified: @product.updated_at.utc, etag: @product.cache_key_with_version)
respond_to do |wants|
# ... normal response processing
end
@@ -577,7 +577,7 @@ class ProductsController < ApplicationController
end
```
-Instead of an options hash, you can also simply pass in a model. Rails will use the `updated_at` and `cache_key` methods for setting `last_modified` and `etag`:
+Instead of an options hash, you can also simply pass in a model. Rails will use the `updated_at` and `cache_key_with_version` methods for setting `last_modified` and `etag`:
```ruby
class ProductsController < ApplicationController
diff --git a/guides/source/command_line.md b/guides/source/command_line.md
index 5fd3ad17de..bbebf97c3f 100644
--- a/guides/source/command_line.md
+++ b/guides/source/command_line.md
@@ -36,35 +36,35 @@ $ rails --help
Usage: rails COMMAND [ARGS]
The most common rails commands are:
-generate Generate new code (short-cut alias: "g")
-console Start the Rails console (short-cut alias: "c")
-server Start the Rails server (short-cut alias: "s")
-...
+ generate Generate new code (short-cut alias: "g")
+ console Start the Rails console (short-cut alias: "c")
+ server Start the Rails server (short-cut alias: "s")
+ ...
All commands can be run with -h (or --help) for more information.
In addition to those commands, there are:
-about List versions of all Rails ...
-assets:clean[keep] Remove old compiled assets
-assets:clobber Remove compiled assets
-assets:environment Load asset compile environment
-assets:precompile Compile all the assets ...
-...
-db:fixtures:load Loads fixtures into the ...
-db:migrate Migrate the database ...
-db:migrate:status Display status of migrations
-db:rollback Rolls the schema back to ...
-db:schema:cache:clear Clears a db/schema_cache.yml file
-db:schema:cache:dump Creates a db/schema_cache.yml file
-db:schema:dump Creates a db/schema.rb file ...
-db:schema:load Loads a schema.rb file ...
-db:seed Loads the seed data ...
-db:structure:dump Dumps the database structure ...
-db:structure:load Recreates the databases ...
-db:version Retrieves the current schema ...
-...
-restart Restart app by touching ...
-tmp:create Creates tmp directories ...
+ about List versions of all Rails ...
+ assets:clean[keep] Remove old compiled assets
+ assets:clobber Remove compiled assets
+ assets:environment Load asset compile environment
+ assets:precompile Compile all the assets ...
+ ...
+ db:fixtures:load Loads fixtures into the ...
+ db:migrate Migrate the database ...
+ db:migrate:status Display status of migrations
+ db:rollback Rolls the schema back to ...
+ db:schema:cache:clear Clears a db/schema_cache.yml file
+ db:schema:cache:dump Creates a db/schema_cache.yml file
+ db:schema:dump Creates a db/schema.rb file ...
+ db:schema:load Loads a schema.rb file ...
+ db:seed Loads the seed data ...
+ db:structure:dump Dumps the database structure ...
+ db:structure:load Recreates the databases ...
+ db:version Retrieves the current schema ...
+ ...
+ restart Restart app by touching ...
+ tmp:create Creates tmp directories ...
```
Let's create a simple Rails application to step through each of these commands in context.
diff --git a/guides/source/configuring.md b/guides/source/configuring.md
index 1ede31c1cd..960a43406b 100644
--- a/guides/source/configuring.md
+++ b/guides/source/configuring.md
@@ -731,7 +731,7 @@ There are a few configuration options available in Active Support:
`config.active_job` provides the following configuration options:
-* `config.active_job.queue_adapter` sets the adapter for the queueing backend. The default adapter is `:async`. For an up-to-date list of built-in adapters see the [ActiveJob::QueueAdapters API documentation](http://api.rubyonrails.org/classes/ActiveJob/QueueAdapters.html).
+* `config.active_job.queue_adapter` sets the adapter for the queuing backend. The default adapter is `:async`. For an up-to-date list of built-in adapters see the [ActiveJob::QueueAdapters API documentation](http://api.rubyonrails.org/classes/ActiveJob/QueueAdapters.html).
```ruby
# Be sure to have the adapter's gem in your Gemfile
diff --git a/guides/source/development_dependencies_install.md b/guides/source/development_dependencies_install.md
index d52946be08..b3baf726e3 100644
--- a/guides/source/development_dependencies_install.md
+++ b/guides/source/development_dependencies_install.md
@@ -156,7 +156,7 @@ To install all run:
# portmaster databases/redis
```
-Or install everyting through ports (these packages are located under the
+Or install everything through ports (these packages are located under the
`databases` folder).
NOTE: If you run into troubles during the installation of MySQL, please see
diff --git a/railties/lib/rails/command.rb b/railties/lib/rails/command.rb
index 6d99ac9936..f09aa3ae0d 100644
--- a/railties/lib/rails/command.rb
+++ b/railties/lib/rails/command.rb
@@ -83,20 +83,21 @@ module Rails
end
def print_commands # :nodoc:
- sorted_groups.each { |b, n| print_list(b, n) }
+ commands.each { |command| puts(" #{command}") }
end
- def sorted_groups # :nodoc:
- lookup!
+ private
+ COMMANDS_IN_USAGE = %w(generate console server test test:system dbconsole new)
+ private_constant :COMMANDS_IN_USAGE
- groups = (subclasses - hidden_commands).group_by { |c| c.namespace.split(":").first }
- groups.transform_values! { |commands| commands.flat_map(&:printing_commands).sort }
+ def commands
+ lookup!
- rails = groups.delete("rails")
- [[ "rails", rails ]] + groups.sort.to_a
- end
+ visible_commands = (subclasses - hidden_commands).flat_map(&:printing_commands)
+
+ (visible_commands - COMMANDS_IN_USAGE).sort
+ end
- private
def command_type # :doc:
@command_type ||= "command"
end
diff --git a/railties/lib/rails/generators/rails/app/templates/bin/setup.tt b/railties/lib/rails/generators/rails/app/templates/bin/setup.tt
index a18e03e7db..3f73bae3da 100644
--- a/railties/lib/rails/generators/rails/app/templates/bin/setup.tt
+++ b/railties/lib/rails/generators/rails/app/templates/bin/setup.tt
@@ -23,7 +23,7 @@ FileUtils.chdir APP_ROOT do
# puts "\n== Copying sample files =="
# unless File.exist?('config/database.yml')
- # cp 'config/database.yml.sample', 'config/database.yml'
+ # FileUtils.cp 'config/database.yml.sample', 'config/database.yml'
# end
puts "\n== Preparing database =="
diff --git a/railties/lib/rails/generators/rails/app/templates/test/test_helper.rb.tt b/railties/lib/rails/generators/rails/app/templates/test/test_helper.rb.tt
index c918b57eca..c06cd525d7 100644
--- a/railties/lib/rails/generators/rails/app/templates/test/test_helper.rb.tt
+++ b/railties/lib/rails/generators/rails/app/templates/test/test_helper.rb.tt
@@ -4,7 +4,7 @@ require 'rails/test_help'
class ActiveSupport::TestCase
# Run tests in parallel with specified workers
-<% if defined?(JRUBY_VERSION) -%>
+<% if defined?(JRUBY_VERSION) || Gem.win_platform? -%>
parallelize(workers: 2, with: :threads)
<%- else -%>
parallelize(workers: 2)
diff --git a/railties/test/application/rake_test.rb b/railties/test/application/rake_test.rb
index c38b91ef03..44e3b0f66b 100644
--- a/railties/test/application/rake_test.rb
+++ b/railties/test/application/rake_test.rb
@@ -160,7 +160,10 @@ module ApplicationTests
def test_scaffold_tests_pass_by_default
rails "generate", "scaffold", "user", "username:string", "password:string"
- with_rails_env("test") { rails("db:migrate") }
+ with_rails_env("test") do
+ rails("db:migrate")
+ rails("webpacker:compile")
+ end
output = rails("test")
assert_match(/7 runs, 9 assertions, 0 failures, 0 errors/, output)
diff --git a/railties/test/application/test_runner_test.rb b/railties/test/application/test_runner_test.rb
index 0d7645e794..140703e118 100644
--- a/railties/test/application/test_runner_test.rb
+++ b/railties/test/application/test_runner_test.rb
@@ -586,6 +586,20 @@ module ApplicationTests
assert_match "1 runs, 0 assertions, 0 failures, 1 errors", output
end
+ def test_run_in_parallel_with_unknown_object
+ create_scaffold
+ app_file "config/environments/test.rb", <<-RUBY
+ Rails.application.configure do
+ config.action_controller.allow_forgery_protection = true
+ config.action_dispatch.show_exceptions = false
+ end
+ RUBY
+
+ output = run_test_command("-n test_should_create_user")
+
+ assert_match "ActionController::InvalidAuthenticityToken", output
+ end
+
def test_raise_error_when_specified_file_does_not_exist
error = capture(:stderr) { run_test_command("test/not_exists.rb", stderr: true) }
assert_match(%r{cannot load such file.+test/not_exists\.rb}, error)
diff --git a/railties/test/generators/actions_test.rb b/railties/test/generators/actions_test.rb
index 6d53230eab..a2b35124c5 100644
--- a/railties/test/generators/actions_test.rb
+++ b/railties/test/generators/actions_test.rb
@@ -308,7 +308,7 @@ class ActionsTest < Rails::Generators::TestCase
end
end
- def test_rails_should_run_rake_command_with_default_env
+ def test_rake_should_run_rake_command_with_default_env
assert_called_with(generator, :run, ["rake log:clear RAILS_ENV=development", verbose: false]) do
with_rails_env nil do
action :rake, "log:clear"
@@ -316,13 +316,13 @@ class ActionsTest < Rails::Generators::TestCase
end
end
- def test_rails_with_env_option_should_run_rake_command_in_env
+ def test_rake_with_env_option_should_run_rake_command_in_env
assert_called_with(generator, :run, ["rake log:clear RAILS_ENV=production", verbose: false]) do
action :rake, "log:clear", env: "production"
end
end
- test "rails command with RAILS_ENV variable should run rake command in env" do
+ test "rake with RAILS_ENV variable should run rake command in env" do
assert_called_with(generator, :run, ["rake log:clear RAILS_ENV=production", verbose: false]) do
with_rails_env "production" do
action :rake, "log:clear"
@@ -338,7 +338,7 @@ class ActionsTest < Rails::Generators::TestCase
end
end
- test "rails command with sudo option should run rake command with sudo" do
+ test "rake with sudo option should run rake command with sudo" do
assert_called_with(generator, :run, ["sudo rake log:clear RAILS_ENV=development", verbose: false]) do
with_rails_env nil do
action :rake, "log:clear", sudo: true