aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Gemfile.lock4
-rw-r--r--actionpack/lib/action_controller/metal/request_forgery_protection.rb2
-rw-r--r--actionpack/test/controller/new_base/content_negotiation_test.rb2
-rw-r--r--actiontext/lib/action_text/attribute.rb7
-rw-r--r--actiontext/test/dummy/app/models/message.rb3
-rw-r--r--actiontext/test/dummy/app/models/review.rb5
-rw-r--r--actiontext/test/dummy/db/migrate/20190317200724_create_reviews.rb8
-rw-r--r--actiontext/test/dummy/db/schema.rb8
-rw-r--r--actiontext/test/unit/model_test.rb16
-rw-r--r--actionview/app/assets/javascripts/rails-ujs/utils/ajax.coffee2
-rw-r--r--actionview/lib/action_view/base.rb2
-rw-r--r--actionview/lib/action_view/file_template.rb4
-rw-r--r--actionview/lib/action_view/helpers/tags/base.rb2
-rw-r--r--actionview/lib/action_view/template.rb11
-rw-r--r--actionview/lib/action_view/template/handlers/erb/erubi.rb2
-rw-r--r--actionview/lib/action_view/template/resolver.rb40
-rw-r--r--actionview/lib/action_view/testing/resolvers.rb6
-rw-r--r--actionview/test/template/digestor_test.rb2
-rw-r--r--actionview/test/template/lookup_context_test.rb50
-rw-r--r--actionview/test/ujs/public/test/call-remote.js4
-rw-r--r--activerecord/CHANGELOG.md25
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb116
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/quoting.rb14
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_adapter.rb5
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb42
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb51
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb4
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb18
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3/database_statements.rb38
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb18
-rw-r--r--activerecord/lib/active_record/database_configurations.rb2
-rw-r--r--activerecord/lib/active_record/insert_all.rb54
-rw-r--r--activerecord/lib/active_record/querying.rb2
-rw-r--r--activerecord/lib/active_record/relation.rb2
-rw-r--r--activerecord/lib/active_record/relation/query_methods.rb26
-rw-r--r--activerecord/lib/active_record/tasks/database_tasks.rb5
-rw-r--r--activerecord/lib/active_record/tasks/mysql_database_tasks.rb10
-rw-r--r--activerecord/lib/active_record/tasks/postgresql_database_tasks.rb12
-rw-r--r--activerecord/lib/active_record/tasks/sqlite_database_tasks.rb10
-rw-r--r--activerecord/lib/arel/insert_manager.rb6
-rw-r--r--activerecord/lib/arel/nodes.rb1
-rw-r--r--activerecord/lib/arel/nodes/select_core.rb6
-rw-r--r--activerecord/lib/arel/nodes/unary.rb1
-rw-r--r--activerecord/lib/arel/nodes/values.rb16
-rw-r--r--activerecord/lib/arel/nodes/values_list.rb19
-rw-r--r--activerecord/lib/arel/select_manager.rb7
-rw-r--r--activerecord/lib/arel/visitors/depth_first.rb3
-rw-r--r--activerecord/lib/arel/visitors/dot.rb5
-rw-r--r--activerecord/lib/arel/visitors/ibm_db.rb12
-rw-r--r--activerecord/lib/arel/visitors/informix.rb5
-rw-r--r--activerecord/lib/arel/visitors/mssql.rb13
-rw-r--r--activerecord/lib/arel/visitors/to_sql.rb36
-rw-r--r--activerecord/test/cases/adapter_test.rb22
-rw-r--r--activerecord/test/cases/adapters/mysql2/connection_test.rb11
-rw-r--r--activerecord/test/cases/adapters/mysql2/optimizer_hints_test.rb40
-rw-r--r--activerecord/test/cases/adapters/postgresql/connection_test.rb8
-rw-r--r--activerecord/test/cases/adapters/postgresql/optimizer_hints_test.rb44
-rw-r--r--activerecord/test/cases/adapters/sqlite3/connection_test.rb18
-rw-r--r--activerecord/test/cases/arel/insert_manager_test.rb19
-rw-r--r--activerecord/test/cases/arel/visitors/depth_first_test.rb2
-rw-r--r--activerecord/test/cases/arel/visitors/dot_test.rb2
-rw-r--r--activerecord/test/cases/arel/visitors/to_sql_test.rb4
-rw-r--r--activerecord/test/cases/bind_parameter_test.rb2
-rw-r--r--activerecord/test/cases/fixtures_test.rb8
-rw-r--r--activerecord/test/cases/helper.rb1
-rw-r--r--activerecord/test/cases/insert_all_test.rb6
-rw-r--r--activerecord/test/cases/invertible_migration_test.rb2
-rw-r--r--activestorage/db/update_migrate/20180723000244_add_foreign_key_constraint_to_active_storage_attachments_for_blob_id.rb4
-rw-r--r--activesupport/CHANGELOG.md18
-rw-r--r--activesupport/activesupport.gemspec2
-rw-r--r--activesupport/lib/active_support/core_ext/string/inflections.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/time/calculations.rb3
-rw-r--r--activesupport/lib/active_support/dependencies/zeitwerk_integration.rb9
-rw-r--r--activesupport/lib/active_support/evented_file_update_checker.rb4
-rw-r--r--activesupport/lib/active_support/inflector/transliterate.rb14
-rw-r--r--activesupport/test/core_ext/time_ext_test.rb2
-rw-r--r--activesupport/test/file_update_checker_shared_tests.rb12
-rw-r--r--guides/source/6_0_release_notes.md63
-rw-r--r--guides/source/active_record_querying.md9
-rw-r--r--railties/CHANGELOG.md8
-rw-r--r--railties/lib/rails/command/environment_argument.rb9
-rw-r--r--railties/lib/rails/commands/credentials/credentials_command.rb7
-rw-r--r--railties/lib/rails/commands/initializers/initializers_command.rb7
-rw-r--r--railties/lib/rails/commands/runner/runner_command.rb10
-rw-r--r--railties/lib/rails/commands/server/server_command.rb6
-rw-r--r--railties/test/application/runner_test.rb4
-rw-r--r--railties/test/application/zeitwerk_integration_test.rb33
-rw-r--r--railties/test/commands/credentials_test.rb14
-rw-r--r--railties/test/commands/initializers_test.rb20
-rw-r--r--railties/test/commands/server_test.rb10
-rw-r--r--railties/test/isolation/abstract_unit.rb2
91 files changed, 767 insertions, 458 deletions
diff --git a/Gemfile.lock b/Gemfile.lock
index b66c8933be..6a8f9040d7 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -70,7 +70,7 @@ PATH
i18n (>= 0.7, < 2)
minitest (~> 5.1)
tzinfo (~> 1.1)
- zeitwerk (~> 1.3, >= 1.3.3)
+ zeitwerk (~> 1.4)
rails (6.0.0.beta3)
actioncable (= 6.0.0.beta3)
actionmailbox (= 6.0.0.beta3)
@@ -517,7 +517,7 @@ GEM
websocket-extensions (0.1.3)
xpath (3.2.0)
nokogiri (~> 1.8)
- zeitwerk (1.3.3)
+ zeitwerk (1.4.0)
PLATFORMS
java
diff --git a/actionpack/lib/action_controller/metal/request_forgery_protection.rb b/actionpack/lib/action_controller/metal/request_forgery_protection.rb
index cb109c6ad8..4bf8d90b69 100644
--- a/actionpack/lib/action_controller/metal/request_forgery_protection.rb
+++ b/actionpack/lib/action_controller/metal/request_forgery_protection.rb
@@ -431,7 +431,7 @@ module ActionController #:nodoc:
The browser returned a 'null' origin for a request with origin-based forgery protection turned on. This usually
means you have the 'no-referrer' Referrer-Policy header enabled, or that the request came from a site that
refused to give its origin. This makes it impossible for Rails to verify the source of the requests. Likely the
- best solution is to change your referrer policy to something less strict like same-origin or strict-same-origin.
+ best solution is to change your referrer policy to something less strict like same-origin or strict-origin.
If you cannot change the referrer policy, you can disable origin checking with the
Rails.application.config.action_controller.forgery_protection_origin_check setting.
MSG
diff --git a/actionpack/test/controller/new_base/content_negotiation_test.rb b/actionpack/test/controller/new_base/content_negotiation_test.rb
index 6de91c57b7..00b2798aeb 100644
--- a/actionpack/test/controller/new_base/content_negotiation_test.rb
+++ b/actionpack/test/controller/new_base/content_negotiation_test.rb
@@ -25,7 +25,7 @@ module ContentNegotiation
assert_body "Hello world text/html!"
end
- test "A js or */* Accept header on xhr will return HTML" do
+ test "A js or */* Accept header on xhr will return JavaScript" do
get "/content_negotiation/basic/hello", headers: { "HTTP_ACCEPT" => "text/javascript, */*" }, xhr: true
assert_body "Hello world text/javascript!"
end
diff --git a/actiontext/lib/action_text/attribute.rb b/actiontext/lib/action_text/attribute.rb
index f226dd21bd..f9a604096c 100644
--- a/actiontext/lib/action_text/attribute.rb
+++ b/actiontext/lib/action_text/attribute.rb
@@ -34,14 +34,11 @@ module ActionText
end
CODE
- has_one :"rich_text_#{name}", -> { where(name: name) }, class_name: "ActionText::RichText", as: :record, inverse_of: :record, dependent: :destroy
+ has_one :"rich_text_#{name}", -> { where(name: name) },
+ class_name: "ActionText::RichText", as: :record, inverse_of: :record, autosave: true, dependent: :destroy
scope :"with_rich_text_#{name}", -> { includes("rich_text_#{name}") }
scope :"with_rich_text_#{name}_and_embeds", -> { includes("rich_text_#{name}": { embeds_attachments: :blob }) }
-
- after_save do
- public_send(name).save if public_send(name).changed?
- end
end
end
end
diff --git a/actiontext/test/dummy/app/models/message.rb b/actiontext/test/dummy/app/models/message.rb
index 9ea4dbfe78..7bce50753c 100644
--- a/actiontext/test/dummy/app/models/message.rb
+++ b/actiontext/test/dummy/app/models/message.rb
@@ -1,4 +1,7 @@
class Message < ApplicationRecord
has_rich_text :content
has_rich_text :body
+
+ has_one :review
+ accepts_nested_attributes_for :review
end
diff --git a/actiontext/test/dummy/app/models/review.rb b/actiontext/test/dummy/app/models/review.rb
new file mode 100644
index 0000000000..e54a37685d
--- /dev/null
+++ b/actiontext/test/dummy/app/models/review.rb
@@ -0,0 +1,5 @@
+class Review < ApplicationRecord
+ belongs_to :message
+
+ has_rich_text :content
+end
diff --git a/actiontext/test/dummy/db/migrate/20190317200724_create_reviews.rb b/actiontext/test/dummy/db/migrate/20190317200724_create_reviews.rb
new file mode 100644
index 0000000000..96e0eab287
--- /dev/null
+++ b/actiontext/test/dummy/db/migrate/20190317200724_create_reviews.rb
@@ -0,0 +1,8 @@
+class CreateReviews < ActiveRecord::Migration[6.0]
+ def change
+ create_table :reviews do |t|
+ t.belongs_to :message, null: false
+ t.string :author_name, null: false
+ end
+ end
+end
diff --git a/actiontext/test/dummy/db/schema.rb b/actiontext/test/dummy/db/schema.rb
index 60ccbd4873..03e99b29d2 100644
--- a/actiontext/test/dummy/db/schema.rb
+++ b/actiontext/test/dummy/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 2019_03_05_172303) do
+ActiveRecord::Schema.define(version: 2019_03_17_200724) do
create_table "action_text_rich_texts", force: :cascade do |t|
t.string "name", null: false
@@ -61,5 +61,11 @@ ActiveRecord::Schema.define(version: 2019_03_05_172303) do
t.datetime "updated_at", precision: 6, null: false
end
+ create_table "reviews", force: :cascade do |t|
+ t.integer "message_id", null: false
+ t.string "author_name", null: false
+ t.index ["message_id"], name: "index_reviews_on_message_id"
+ end
+
add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
end
diff --git a/actiontext/test/unit/model_test.rb b/actiontext/test/unit/model_test.rb
index d56363adc0..f4328ba2ce 100644
--- a/actiontext/test/unit/model_test.rb
+++ b/actiontext/test/unit/model_test.rb
@@ -49,8 +49,22 @@ class ActionText::ModelTest < ActiveSupport::TestCase
assert_equal "Hello world", message.content.to_plain_text
end
- test "save body" do
+ test "saving body" do
message = Message.create(subject: "Greetings", body: "<h1>Hello world</h1>")
assert_equal "Hello world", message.body.to_plain_text
end
+
+ test "saving content via nested attributes" do
+ message = Message.create! subject: "Greetings", content: "<h1>Hello world</h1>",
+ review_attributes: { author_name: "Marcia", content: "Nice work!" }
+ assert_equal "Nice work!", message.review.content.to_plain_text
+ end
+
+ test "updating content via nested attributes" do
+ message = Message.create! subject: "Greetings", content: "<h1>Hello world</h1>",
+ review_attributes: { author_name: "Marcia", content: "Nice work!" }
+
+ message.update! review_attributes: { id: message.review.id, content: "Great work!" }
+ assert_equal "Great work!", message.review.reload.content.to_plain_text
+ end
end
diff --git a/actionview/app/assets/javascripts/rails-ujs/utils/ajax.coffee b/actionview/app/assets/javascripts/rails-ujs/utils/ajax.coffee
index 019bda635a..5b223d50f6 100644
--- a/actionview/app/assets/javascripts/rails-ujs/utils/ajax.coffee
+++ b/actionview/app/assets/javascripts/rails-ujs/utils/ajax.coffee
@@ -69,7 +69,7 @@ processResponse = (response, type) ->
script.setAttribute('nonce', cspNonce())
script.text = response
document.head.appendChild(script).parentNode.removeChild(script)
- else if type.match(/\bxml\b/)
+ else if type.match(/\b(xml|html|svg)\b/)
parser = new DOMParser()
type = type.replace(/;.+/, '') # remove something like ';charset=utf-8'
try response = parser.parseFromString(response, type)
diff --git a/actionview/lib/action_view/base.rb b/actionview/lib/action_view/base.rb
index cf71b4803b..5253ef7b0c 100644
--- a/actionview/lib/action_view/base.rb
+++ b/actionview/lib/action_view/base.rb
@@ -267,7 +267,7 @@ module ActionView #:nodoc:
_prepare_context
end
- def run(method, template, locals, buffer, &block)
+ def _run(method, template, locals, buffer, &block)
_old_output_buffer, _old_virtual_path, _old_template = @output_buffer, @virtual_path, @current_template
@current_template = template
@output_buffer = buffer
diff --git a/actionview/lib/action_view/file_template.rb b/actionview/lib/action_view/file_template.rb
index dea02176eb..d838078f94 100644
--- a/actionview/lib/action_view/file_template.rb
+++ b/actionview/lib/action_view/file_template.rb
@@ -22,11 +22,11 @@ module ActionView
# to ensure that references to the template object can be marshalled as well. This means forgoing
# the marshalling of the compiler mutex and instantiating that again on unmarshalling.
def marshal_dump # :nodoc:
- [ @identifier, @handler, @compiled, @locals, @virtual_path, @updated_at, @format, @variant ]
+ [ @identifier, @handler, @compiled, @locals, @virtual_path, @format, @variant ]
end
def marshal_load(array) # :nodoc:
- @identifier, @handler, @compiled, @locals, @virtual_path, @updated_at, @format, @variant = *array
+ @identifier, @handler, @compiled, @locals, @virtual_path, @format, @variant = *array
@compile_mutex = Mutex.new
end
end
diff --git a/actionview/lib/action_view/helpers/tags/base.rb b/actionview/lib/action_view/helpers/tags/base.rb
index 0adecf362a..b58e1a6680 100644
--- a/actionview/lib/action_view/helpers/tags/base.rb
+++ b/actionview/lib/action_view/helpers/tags/base.rb
@@ -138,7 +138,7 @@ module ActionView
end
def sanitized_value(value)
- value.to_s.gsub(/[\s\.]/, "_").gsub(/[^-[[:word:]]]/, "").mb_chars.downcase.to_s
+ value.to_s.gsub(/[\s\.]/, "_").gsub(/[^-[[:word:]]]/, "").downcase
end
def select_content_tag(option_tags, options, html_options)
diff --git a/actionview/lib/action_view/template.rb b/actionview/lib/action_view/template.rb
index 2c27e11b9e..e733c6d376 100644
--- a/actionview/lib/action_view/template.rb
+++ b/actionview/lib/action_view/template.rb
@@ -122,10 +122,10 @@ module ActionView
extend Template::Handlers
- attr_reader :source, :identifier, :handler, :original_encoding, :updated_at
+ attr_reader :source, :identifier, :handler, :original_encoding
attr_reader :variable, :format, :variant, :locals, :virtual_path
- def initialize(source, identifier, handler, format: nil, variant: nil, locals: nil, virtual_path: nil, updated_at: Time.now)
+ def initialize(source, identifier, handler, format: nil, variant: nil, locals: nil, virtual_path: nil)
unless locals
ActiveSupport::Deprecation.warn "ActionView::Template#initialize requires a locals parameter"
locals = []
@@ -144,7 +144,6 @@ module ActionView
$1.to_sym
end
- @updated_at = updated_at
@format = format
@variant = variant
@compile_mutex = Mutex.new
@@ -173,7 +172,7 @@ module ActionView
def render(view, locals, buffer = ActionView::OutputBuffer.new, &block)
instrument_render_template do
compile!(view)
- view.run(method_name, self, locals, buffer, &block)
+ view._run(method_name, self, locals, buffer, &block)
end
rescue => e
handle_render_error(view, e)
@@ -261,11 +260,11 @@ module ActionView
# to ensure that references to the template object can be marshalled as well. This means forgoing
# the marshalling of the compiler mutex and instantiating that again on unmarshalling.
def marshal_dump # :nodoc:
- [ @source, @identifier, @handler, @compiled, @locals, @virtual_path, @updated_at, @format, @variant ]
+ [ @source, @identifier, @handler, @compiled, @locals, @virtual_path, @format, @variant ]
end
def marshal_load(array) # :nodoc:
- @source, @identifier, @handler, @compiled, @locals, @virtual_path, @updated_at, @format, @variant = *array
+ @source, @identifier, @handler, @compiled, @locals, @virtual_path, @format, @variant = *array
@compile_mutex = Mutex.new
end
diff --git a/actionview/lib/action_view/template/handlers/erb/erubi.rb b/actionview/lib/action_view/template/handlers/erb/erubi.rb
index 247b6d75d1..307b852440 100644
--- a/actionview/lib/action_view/template/handlers/erb/erubi.rb
+++ b/actionview/lib/action_view/template/handlers/erb/erubi.rb
@@ -27,7 +27,7 @@ module ActionView
include action_view_erb_handler_context._routes.url_helpers
class_eval("define_method(:_template) { |local_assigns, output_buffer| #{src} }", @filename || "(erubi)", 0)
}.empty
- view.run(:_template, nil, {}, ActionView::OutputBuffer.new)
+ view._run(:_template, nil, {}, ActionView::OutputBuffer.new)
end
private
diff --git a/actionview/lib/action_view/template/resolver.rb b/actionview/lib/action_view/template/resolver.rb
index 1dc9c9919a..07c44307ff 100644
--- a/actionview/lib/action_view/template/resolver.rb
+++ b/actionview/lib/action_view/template/resolver.rb
@@ -63,26 +63,11 @@ module ActionView
# Cache the templates returned by the block
def cache(key, name, prefix, partial, locals)
- if Resolver.caching?
- @data[key][name][prefix][partial][locals] ||= canonical_no_templates(yield)
- else
- fresh_templates = yield
- cached_templates = @data[key][name][prefix][partial][locals]
-
- if templates_have_changed?(cached_templates, fresh_templates)
- @data[key][name][prefix][partial][locals] = canonical_no_templates(fresh_templates)
- else
- cached_templates || NO_TEMPLATES
- end
- end
+ @data[key][name][prefix][partial][locals] ||= canonical_no_templates(yield)
end
def cache_query(query) # :nodoc:
- if Resolver.caching?
- @query_cache[query] ||= canonical_no_templates(yield)
- else
- yield
- end
+ @query_cache[query] ||= canonical_no_templates(yield)
end
def clear
@@ -112,19 +97,6 @@ module ActionView
def canonical_no_templates(templates)
templates.empty? ? NO_TEMPLATES : templates
end
-
- def templates_have_changed?(cached_templates, fresh_templates)
- # if either the old or new template list is empty, we don't need to (and can't)
- # compare modification times, and instead just check whether the lists are different
- if cached_templates.blank? || fresh_templates.blank?
- return fresh_templates.blank? != cached_templates.blank?
- end
-
- cached_templates_max_updated_at = cached_templates.map(&:updated_at).max
-
- # if a template has changed, it will be now be newer than all the cached templates
- fresh_templates.any? { |t| t.updated_at > cached_templates_max_updated_at }
- end
end
cattr_accessor :caching, default: true
@@ -218,8 +190,7 @@ module ActionView
virtual_path: path.virtual,
format: format,
variant: variant,
- locals: locals,
- updated_at: mtime(template)
+ locals: locals
)
end
end
@@ -272,11 +243,6 @@ module ActionView
entry.gsub(/[*?{}\[\]]/, '\\\\\\&')
end
- # Returns the file mtime from the filesystem.
- def mtime(p)
- File.mtime(p)
- end
-
# Extract handler, formats and variant from path. If a format cannot be found neither
# from the path, or the handler, we should return the array of formats given
# to the resolver.
diff --git a/actionview/lib/action_view/testing/resolvers.rb b/actionview/lib/action_view/testing/resolvers.rb
index 3ca8420c6c..a16dc0096e 100644
--- a/actionview/lib/action_view/testing/resolvers.rb
+++ b/actionview/lib/action_view/testing/resolvers.rb
@@ -31,16 +31,14 @@ module ActionView #:nodoc:
query = /^(#{Regexp.escape(path)})#{query}$/
templates = []
- @hash.each do |_path, array|
- source, updated_at = array
+ @hash.each do |_path, source|
next unless query.match?(_path)
handler, format, variant = extract_handler_and_format_and_variant(_path)
templates << Template.new(source, _path, handler,
virtual_path: path.virtual,
format: format,
variant: variant,
- locals: locals,
- updated_at: updated_at
+ locals: locals
)
end
diff --git a/actionview/test/template/digestor_test.rb b/actionview/test/template/digestor_test.rb
index 7affd3c005..4515afdfff 100644
--- a/actionview/test/template/digestor_test.rb
+++ b/actionview/test/template/digestor_test.rb
@@ -326,12 +326,14 @@ class TemplateDigestorTest < ActionView::TestCase
def assert_digest_difference(template_name, options = {})
previous_digest = digest(template_name, options)
+ finder.view_paths.each(&:clear_cache)
finder.digest_cache.clear
yield
assert_not_equal previous_digest, digest(template_name, options), "digest didn't change"
finder.digest_cache.clear
+ finder.view_paths.each(&:clear_cache)
end
def digest(template_name, options = {})
diff --git a/actionview/test/template/lookup_context_test.rb b/actionview/test/template/lookup_context_test.rb
index 62935620e9..537f8ee163 100644
--- a/actionview/test/template/lookup_context_test.rb
+++ b/actionview/test/template/lookup_context_test.rb
@@ -235,56 +235,6 @@ class LookupContextTest < ActiveSupport::TestCase
end
end
-class LookupContextWithFalseCaching < ActiveSupport::TestCase
- def setup
- @resolver = ActionView::FixtureResolver.new("test/_foo.erb" => ["Foo", Time.utc(2000)])
- @lookup_context = ActionView::LookupContext.new(@resolver, {})
- end
-
- test "templates are always found in the resolver but timestamp is checked before being compiled" do
- ActionView::Resolver.stub(:caching?, false) do
- template = @lookup_context.find("foo", %w(test), true)
- assert_equal "Foo", template.source
-
- # Now we are going to change the template, but it won't change the returned template
- # since the timestamp is the same.
- @resolver.data["test/_foo.erb"][0] = "Bar"
- template = @lookup_context.find("foo", %w(test), true)
- assert_equal "Foo", template.source
-
- # Now update the timestamp.
- @resolver.data["test/_foo.erb"][1] = Time.now.utc
- template = @lookup_context.find("foo", %w(test), true)
- assert_equal "Bar", template.source
- end
- end
-
- test "if no template was found in the second lookup, with no cache, raise error" do
- ActionView::Resolver.stub(:caching?, false) do
- template = @lookup_context.find("foo", %w(test), true)
- assert_equal "Foo", template.source
-
- @resolver.data.clear
- assert_raise ActionView::MissingTemplate do
- @lookup_context.find("foo", %w(test), true)
- end
- end
- end
-
- test "if no template was cached in the first lookup, retrieval should work in the second call" do
- ActionView::Resolver.stub(:caching?, false) do
- @resolver.data.clear
- assert_raise ActionView::MissingTemplate do
- @lookup_context.find("foo", %w(test), true)
- end
-
- @resolver.data["test/_foo.erb"] = ["Foo", Time.utc(2000)]
- template = @lookup_context.find("foo", %w(test), true)
- assert_equal "Foo", template.source
- end
- end
-end
-
class TestMissingTemplate < ActiveSupport::TestCase
def setup
@lookup_context = ActionView::LookupContext.new("/Path/to/views", {})
diff --git a/actionview/test/ujs/public/test/call-remote.js b/actionview/test/ujs/public/test/call-remote.js
index 778dc1b09a..0f92007007 100644
--- a/actionview/test/ujs/public/test/call-remote.js
+++ b/actionview/test/ujs/public/test/call-remote.js
@@ -128,14 +128,14 @@ asyncTest('execution of JS code does not modify current DOM', 1, function() {
})
})
-asyncTest('HTML content should be plain-text', 1, function() {
+asyncTest('HTML document should be parsed', 1, function() {
buildForm({ method: 'post', 'data-type': 'html' })
$('form').append('<input type="text" name="content_type" value="text/html">')
$('form').append('<input type="text" name="content" value="<p>hello</p>">')
submit(function(e, data, status, xhr) {
- ok(data === '<p>hello</p>', 'returned data should be a plain-text string')
+ ok(data instanceof HTMLDocument, 'returned data should be an HTML document')
})
})
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md
index 3d7508252a..148b3800a8 100644
--- a/activerecord/CHANGELOG.md
+++ b/activerecord/CHANGELOG.md
@@ -1,3 +1,28 @@
+* Support Optimizer Hints.
+
+ In most databases, there is a way to control the optimizer is by using optimizer hints,
+ which can be specified within individual statements.
+
+ Example (for MySQL):
+
+ Topic.optimizer_hints("MAX_EXECUTION_TIME(50000)", "NO_INDEX_MERGE(topics)")
+ # SELECT /*+ MAX_EXECUTION_TIME(50000) NO_INDEX_MERGE(topics) */ `topics`.* FROM `topics`
+
+ Example (for PostgreSQL with pg_hint_plan):
+
+ Topic.optimizer_hints("SeqScan(topics)", "Parallel(topics 8)")
+ # SELECT /*+ SeqScan(topics) Parallel(topics 8) */ "topics".* FROM "topics"
+
+ See also:
+
+ * https://dev.mysql.com/doc/refman/8.0/en/optimizer-hints.html
+ * https://pghintplan.osdn.jp/pg_hint_plan.html
+ * https://docs.oracle.com/en/database/oracle/oracle-database/12.2/tgsql/influencing-the-optimizer.html
+ * https://docs.microsoft.com/en-us/sql/t-sql/queries/hints-transact-sql-query?view=sql-server-2017
+ * https://www.ibm.com/support/knowledgecenter/en/SSEPGG_11.1.0/com.ibm.db2.luw.admin.perf.doc/doc/c0070117.html
+
+ *Ryuta Kamizono*
+
* Fix query attribute method on user-defined attribute to be aware of typecasted value.
For example, the following code no longer return false as casted non-empty string:
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
index 3c872d6c1b..6aacbe5f88 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
@@ -142,11 +142,6 @@ module ActiveRecord
exec_query(sql, name, binds)
end
- # Executes the truncate statement.
- def truncate(table_name, name = nil)
- raise NotImplementedError
- end
-
# Executes update +sql+ statement in the context of this connection using
# +binds+ as the bind substitutes. +name+ is logged along with
# the executed +sql+ statement.
@@ -181,6 +176,23 @@ module ActiveRecord
exec_delete(sql, name, binds)
end
+ # Executes the truncate statement.
+ def truncate(table_name, name = nil)
+ execute(build_truncate_statements(table_name), name)
+ end
+
+ def truncate_tables(*table_names) # :nodoc:
+ return if table_names.empty?
+
+ with_multi_statements do
+ disable_referential_integrity do
+ Array(build_truncate_statements(*table_names)).each do |sql|
+ execute_batch(sql, "Truncate Tables")
+ end
+ end
+ end
+ end
+
# Runs the given block in a database transaction, and returns the result
# of the block.
#
@@ -341,46 +353,20 @@ module ActiveRecord
# We keep this method to provide fallback
# for databases like sqlite that do not support bulk inserts.
def insert_fixture(fixture, table_name)
- fixture = fixture.stringify_keys
-
- columns = schema_cache.columns_hash(table_name)
- binds = fixture.map do |name, value|
- if column = columns[name]
- type = lookup_cast_type_from_column(column)
- Relation::QueryAttribute.new(name, value, type)
- else
- raise Fixture::FixtureError, %(table "#{table_name}" has no column named #{name.inspect}.)
- end
- end
-
- table = Arel::Table.new(table_name)
-
- values = binds.map do |bind|
- value = with_yaml_fallback(bind.value_for_database)
- [table[bind.name], value]
- end
-
- manager = Arel::InsertManager.new
- manager.into(table)
- manager.insert(values)
- execute manager.to_sql, "Fixture Insert"
+ execute(build_fixture_sql(Array.wrap(fixture), table_name), "Fixture Insert")
end
def insert_fixtures_set(fixture_set, tables_to_delete = [])
- fixture_inserts = fixture_set.map do |table_name, fixtures|
- next if fixtures.empty?
-
- build_fixture_sql(fixtures, table_name)
- end.compact
-
- table_deletes = tables_to_delete.map { |table| +"DELETE FROM #{quote_table_name table}" }
- total_sql = Array.wrap(combine_multi_statements(table_deletes + fixture_inserts))
-
- disable_referential_integrity do
- transaction(requires_new: true) do
- total_sql.each do |sql|
- execute sql, "Fixtures Load"
- yield if block_given?
+ fixture_inserts = build_fixture_statements(fixture_set)
+ table_deletes = tables_to_delete.map { |table| "DELETE FROM #{quote_table_name(table)}" }
+ total_sql = Array(combine_multi_statements(table_deletes + fixture_inserts))
+
+ with_multi_statements do
+ disable_referential_integrity do
+ transaction(requires_new: true) do
+ total_sql.each do |sql|
+ execute_batch(sql, "Fixtures Load")
+ end
end
end
end
@@ -416,14 +402,21 @@ module ActiveRecord
end
private
+ def execute_batch(sql, name = nil)
+ execute(sql, name)
+ end
+
+ DEFAULT_INSERT_VALUE = Arel.sql("DEFAULT").freeze
+ private_constant :DEFAULT_INSERT_VALUE
+
def default_insert_value(column)
- Arel.sql("DEFAULT")
+ DEFAULT_INSERT_VALUE
end
def build_fixture_sql(fixtures, table_name)
columns = schema_cache.columns_hash(table_name)
- values = fixtures.map do |fixture|
+ values_list = fixtures.map do |fixture|
fixture = fixture.stringify_keys
unknown_columns = fixture.keys - columns.keys
@@ -445,12 +438,43 @@ module ActiveRecord
table = Arel::Table.new(table_name)
manager = Arel::InsertManager.new
manager.into(table)
- columns.each_key { |column| manager.columns << table[column] }
- manager.values = manager.create_values_list(values)
+ if values_list.size == 1
+ values = values_list.shift
+ new_values = []
+ columns.each_key.with_index { |column, i|
+ unless values[i].equal?(DEFAULT_INSERT_VALUE)
+ new_values << values[i]
+ manager.columns << table[column]
+ end
+ }
+ values_list << new_values
+ else
+ columns.each_key { |column| manager.columns << table[column] }
+ end
+
+ manager.values = manager.create_values_list(values_list)
manager.to_sql
end
+ def build_fixture_statements(fixture_set)
+ fixture_set.map do |table_name, fixtures|
+ next if fixtures.empty?
+ build_fixture_sql(fixtures, table_name)
+ end.compact
+ end
+
+ def build_truncate_statements(*table_names)
+ truncate_tables = table_names.map do |table_name|
+ "TRUNCATE TABLE #{quote_table_name(table_name)}"
+ end
+ combine_multi_statements(truncate_tables)
+ end
+
+ def with_multi_statements
+ yield
+ end
+
def combine_multi_statements(total_sql)
total_sql.join(";\n")
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
index eefe621feb..9d24d839c1 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
@@ -138,15 +138,15 @@ module ActiveRecord
"'#{quote_string(value.to_s)}'"
end
- def type_casted_binds(binds) # :nodoc:
- if binds.first.is_a?(Array)
- binds.map { |column, value| type_cast(value, column) }
- else
- binds.map { |attr| type_cast(attr.value_for_database) }
+ private
+ def type_casted_binds(binds)
+ if binds.first.is_a?(Array)
+ binds.map { |column, value| type_cast(value, column) }
+ else
+ binds.map { |attr| type_cast(attr.value_for_database) }
+ end
end
- end
- private
def lookup_cast_type(sql_type)
type_map.lookup(sql_type)
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
index 202187a047..7aad306d50 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
@@ -384,6 +384,11 @@ module ActiveRecord
false
end
+ # Does this adapter support optimizer hints?
+ def supports_optimizer_hints?
+ false
+ end
+
def supports_lazy_transactions?
false
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 a518b897a0..8ca2cfa9ed 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
@@ -103,6 +103,11 @@ module ActiveRecord
mariadb? || version >= "5.7.5"
end
+ # See https://dev.mysql.com/doc/refman/8.0/en/optimizer-hints.html for more details.
+ def supports_optimizer_hints?
+ !mariadb? && version >= "5.7.7"
+ end
+
def supports_advisory_locks?
true
end
@@ -269,10 +274,6 @@ module ActiveRecord
show_variable "collation_database"
end
- def truncate(table_name, name = nil)
- execute "TRUNCATE TABLE #{quote_table_name(table_name)}", name
- end
-
def table_comment(table_name) # :nodoc:
scope = quoted_scope(table_name)
@@ -511,12 +512,6 @@ module ActiveRecord
index.using == :btree || super
end
- def insert_fixtures_set(fixture_set, tables_to_delete = [])
- with_multi_statements do
- super { discard_remaining_results }
- end
- end
-
def build_insert_sql(insert) # :nodoc:
sql = +"INSERT #{insert.into} #{insert.values_list}"
@@ -538,33 +533,6 @@ module ActiveRecord
end
end
- def combine_multi_statements(total_sql)
- total_sql.each_with_object([]) do |sql, total_sql_chunks|
- previous_packet = total_sql_chunks.last
- sql << ";\n"
- if max_allowed_packet_reached?(sql, previous_packet) || total_sql_chunks.empty?
- total_sql_chunks << sql
- else
- previous_packet << sql
- end
- end
- end
-
- def max_allowed_packet_reached?(current_packet, previous_packet)
- if current_packet.bytesize > max_allowed_packet
- raise ActiveRecordError, "Fixtures set is too large #{current_packet.bytesize}. Consider increasing the max_allowed_packet variable."
- elsif previous_packet.nil?
- false
- else
- (current_packet.bytesize + previous_packet.bytesize) > max_allowed_packet
- end
- end
-
- def max_allowed_packet
- bytes_margin = 2
- @max_allowed_packet ||= (show_variable("max_allowed_packet") - bytes_margin)
- end
-
def initialize_type_map(m = type_map)
super
diff --git a/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb b/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb
index 6adcc14545..1199c0ad1b 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb
@@ -11,7 +11,7 @@ module ActiveRecord
else
super
end
- discard_remaining_results
+ @connection.abandon_results!
result
end
@@ -69,22 +69,31 @@ module ActiveRecord
alias :exec_update :exec_delete
private
+ def execute_batch(sql, name = nil)
+ super
+ @connection.abandon_results!
+ end
+
def default_insert_value(column)
- Arel.sql("DEFAULT") unless column.auto_increment?
+ super unless column.auto_increment?
end
def last_inserted_id(result)
@connection.last_id
end
- def discard_remaining_results
- @connection.abandon_results!
- end
-
def supports_set_server_option?
@connection.respond_to?(:set_server_option)
end
+ def build_truncate_statements(*table_names)
+ if table_names.size == 1
+ super.first
+ else
+ super
+ end
+ end
+
def multi_statements_enabled?(flags)
if flags.is_a?(Array)
flags.include?("MULTI_STATEMENTS")
@@ -117,6 +126,36 @@ module ActiveRecord
end
end
+ def combine_multi_statements(total_sql)
+ total_sql.each_with_object([]) do |sql, total_sql_chunks|
+ previous_packet = total_sql_chunks.last
+ if max_allowed_packet_reached?(sql, previous_packet)
+ total_sql_chunks << +sql
+ else
+ previous_packet << ";\n"
+ previous_packet << sql
+ end
+ end
+ end
+
+ def max_allowed_packet_reached?(current_packet, previous_packet)
+ if current_packet.bytesize > max_allowed_packet
+ raise ActiveRecordError,
+ "Fixtures set is too large #{current_packet.bytesize}. Consider increasing the max_allowed_packet variable."
+ elsif previous_packet.nil?
+ true
+ else
+ (current_packet.bytesize + previous_packet.bytesize) > max_allowed_packet
+ end
+ end
+
+ def max_allowed_packet
+ @max_allowed_packet ||= begin
+ bytes_margin = 2
+ show_variable("max_allowed_packet") - bytes_margin
+ end
+ end
+
def exec_stmt_and_free(sql, name, binds, cache_stmt: false)
if preventing_writes? && write_query?(sql)
raise ActiveRecord::ReadOnlyError, "Write query attempted while in readonly mode: #{sql}"
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb
index 41633872e2..ae7dbd2868 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb
@@ -164,6 +164,10 @@ module ActiveRecord
end
private
+ def build_truncate_statements(*table_names)
+ "TRUNCATE TABLE #{table_names.map(&method(:quote_table_name)).join(", ")}"
+ end
+
# Returns the current ID of a table's sequence.
def last_insert_id_result(sequence_name)
exec_query("SELECT currval(#{quote(sequence_name)})", "SQL")
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
index 672f11cfcb..29f764e8f4 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
@@ -259,10 +259,6 @@ module ActiveRecord
@use_insert_returning = @config.key?(:insert_returning) ? self.class.type_cast_config_to_boolean(@config[:insert_returning]) : true
end
- def truncate(table_name, name = nil)
- exec_query "TRUNCATE TABLE #{quote_table_name(table_name)}", name, []
- end
-
# Is this connection alive and ready for queries?
def active?
@lock.synchronize do
@@ -351,6 +347,13 @@ module ActiveRecord
postgresql_version >= 90400
end
+ def supports_optimizer_hints?
+ unless defined?(@has_pg_hint_plan)
+ @has_pg_hint_plan = extension_available?("pg_hint_plan")
+ end
+ @has_pg_hint_plan
+ end
+
def supports_lazy_transactions?
true
end
@@ -381,9 +384,12 @@ module ActiveRecord
}
end
+ def extension_available?(name)
+ query_value("SELECT true FROM pg_available_extensions WHERE name = #{quote(name)}", "SCHEMA")
+ end
+
def extension_enabled?(name)
- res = exec_query("SELECT EXISTS(SELECT * FROM pg_available_extensions WHERE name = '#{name}' AND installed_version IS NOT NULL) as enabled", "SCHEMA")
- res.cast_values.first
+ query_value("SELECT installed_version IS NOT NULL FROM pg_available_extensions WHERE name = #{quote(name)}", "SCHEMA")
end
def extensions
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/database_statements.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/database_statements.rb
new file mode 100644
index 0000000000..84dcae49b9
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3/database_statements.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+module ActiveRecord
+ module ConnectionAdapters
+ module SQLite3
+ module DatabaseStatements
+ private
+ def execute_batch(sql, name = nil)
+ if preventing_writes? && write_query?(sql)
+ raise ActiveRecord::ReadOnlyError, "Write query attempted while in readonly mode: #{sql}"
+ end
+
+ materialize_transactions
+
+ log(sql, name) do
+ ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
+ @connection.execute_batch(sql)
+ end
+ end
+ end
+
+ def build_fixture_statements(fixture_set)
+ fixture_set.flat_map do |table_name, fixtures|
+ next if fixtures.empty?
+ fixtures.map { |fixture| build_fixture_sql([fixture], table_name) }
+ end.compact
+ end
+
+ def build_truncate_statements(*table_names)
+ truncate_tables = table_names.map do |table_name|
+ "DELETE FROM #{quote_table_name(table_name)}"
+ end
+ combine_multi_statements(truncate_tables)
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
index 3004caf82d..ff23a525b9 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
@@ -4,6 +4,7 @@ require "active_record/connection_adapters/abstract_adapter"
require "active_record/connection_adapters/statement_pool"
require "active_record/connection_adapters/sqlite3/explain_pretty_printer"
require "active_record/connection_adapters/sqlite3/quoting"
+require "active_record/connection_adapters/sqlite3/database_statements"
require "active_record/connection_adapters/sqlite3/schema_creation"
require "active_record/connection_adapters/sqlite3/schema_definitions"
require "active_record/connection_adapters/sqlite3/schema_dumper"
@@ -58,6 +59,7 @@ module ActiveRecord
include SQLite3::Quoting
include SQLite3::SchemaStatements
+ include SQLite3::DatabaseStatements
NATIVE_DATABASE_TYPES = {
primary_key: "integer PRIMARY KEY AUTOINCREMENT NOT NULL",
@@ -155,10 +157,6 @@ module ActiveRecord
@connection.close rescue nil
end
- def truncate(table_name, name = nil)
- execute "DELETE FROM #{quote_table_name(table_name)}", name
- end
-
def supports_index_sort_order?
true
end
@@ -386,18 +384,6 @@ module ActiveRecord
end
end
- def insert_fixtures_set(fixture_set, tables_to_delete = [])
- disable_referential_integrity do
- transaction(requires_new: true) do
- tables_to_delete.each { |table| delete "DELETE FROM #{quote_table_name(table)}", "Fixture Delete" }
-
- fixture_set.each do |table_name, rows|
- rows.each { |row| insert_fixture(row, table_name) }
- end
- end
- end
- end
-
def build_insert_sql(insert) # :nodoc:
sql = +"INSERT #{insert.into} #{insert.values_list}"
diff --git a/activerecord/lib/active_record/database_configurations.rb b/activerecord/lib/active_record/database_configurations.rb
index a6c702cbbc..4656045fe5 100644
--- a/activerecord/lib/active_record/database_configurations.rb
+++ b/activerecord/lib/active_record/database_configurations.rb
@@ -106,7 +106,7 @@ module ActiveRecord
build_db_config = configs.each_pair.flat_map do |env_name, config|
walk_configs(env_name.to_s, "primary", config)
- end.compact
+ end.flatten.compact
if url = ENV["DATABASE_URL"]
build_url_config(url, build_db_config)
diff --git a/activerecord/lib/active_record/insert_all.rb b/activerecord/lib/active_record/insert_all.rb
index 5baaea344b..98c98d61cd 100644
--- a/activerecord/lib/active_record/insert_all.rb
+++ b/activerecord/lib/active_record/insert_all.rb
@@ -2,12 +2,14 @@
module ActiveRecord
class InsertAll
- attr_reader :model, :connection, :inserts, :on_duplicate, :returning, :unique_by
+ attr_reader :model, :connection, :inserts, :keys
+ attr_reader :on_duplicate, :returning, :unique_by
def initialize(model, inserts, on_duplicate:, returning: nil, unique_by: nil)
raise ArgumentError, "Empty list of attributes passed" if inserts.blank?
- @model, @connection, @inserts, @on_duplicate, @returning, @unique_by = model, model.connection, inserts, on_duplicate, returning, unique_by
+ @model, @connection, @inserts, @keys = model, model.connection, inserts, inserts.first.keys.map(&:to_s).to_set
+ @on_duplicate, @returning, @unique_by = on_duplicate, returning, unique_by
@returning = (connection.supports_insert_returning? ? primary_keys : false) if @returning.nil?
@returning = false if @returning == []
@@ -21,10 +23,6 @@ module ActiveRecord
connection.exec_query to_sql, "Bulk Insert"
end
- def keys
- inserts.first.keys.map(&:to_s)
- end
-
def updatable_columns
keys - readonly_columns - unique_by_columns
end
@@ -37,6 +35,17 @@ module ActiveRecord
on_duplicate == :update
end
+ def map_key_with_value
+ inserts.map do |attributes|
+ attributes = attributes.stringify_keys
+ verify_attributes(attributes)
+
+ keys.map do |key|
+ yield key, attributes[key]
+ end
+ end
+ end
+
private
def ensure_valid_options_for_connection!
if returning && !connection.supports_insert_returning?
@@ -76,6 +85,12 @@ module ActiveRecord
Array.wrap(model.primary_key)
end
+ def verify_attributes(attributes)
+ if keys != attributes.keys.to_set
+ raise ArgumentError, "All objects being inserted must have the same keys"
+ end
+ end
+
class Builder
attr_reader :model
@@ -91,21 +106,11 @@ module ActiveRecord
end
def values_list
- columns = connection.schema_cache.columns_hash(model.table_name)
- keys = insert_all.keys.to_set
- types = keys.map { |key| [ key, connection.lookup_cast_type_from_column(columns[key]) ] }.to_h
-
- values_list = insert_all.inserts.map do |attributes|
- attributes = attributes.stringify_keys
+ types = extract_types_from_columns_on(model.table_name, keys: insert_all.keys)
- unless attributes.keys.to_set == keys
- raise ArgumentError, "All objects being inserted must have the same keys"
- end
-
- keys.map do |key|
- bind = Relation::QueryAttribute.new(key, attributes[key], types[key])
- connection.with_yaml_fallback(bind.value_for_database)
- end
+ values_list = insert_all.map_key_with_value do |key, value|
+ bind = Relation::QueryAttribute.new(key, value, types[key])
+ connection.with_yaml_fallback(bind.value_for_database)
end
Arel::InsertManager.new.create_values_list(values_list).to_sql
@@ -133,6 +138,15 @@ module ActiveRecord
quote_columns(insert_all.keys).join(",")
end
+ def extract_types_from_columns_on(table_name, keys:)
+ columns = connection.schema_cache.columns_hash(table_name)
+
+ unknown_column = (keys - columns.keys).first
+ raise UnknownAttributeError.new(model.new, unknown_column) if unknown_column
+
+ keys.map { |key| [ key, connection.lookup_cast_type_from_column(columns[key]) ] }.to_h
+ end
+
def quote_columns(columns)
columns.map(&connection.method(:quote_column_name))
end
diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb
index 7a8d0cb663..86021f0a80 100644
--- a/activerecord/lib/active_record/querying.rb
+++ b/activerecord/lib/active_record/querying.rb
@@ -14,7 +14,7 @@ module ActiveRecord
:find_each, :find_in_batches, :in_batches,
:select, :reselect, :order, :reorder, :group, :limit, :offset, :joins, :left_joins, :left_outer_joins,
:where, :rewhere, :preload, :eager_load, :includes, :from, :lock, :readonly, :extending, :or,
- :having, :create_with, :distinct, :references, :none, :unscope, :merge, :except, :only,
+ :having, :create_with, :distinct, :references, :none, :unscope, :optimizer_hints, :merge, :except, :only,
:count, :average, :minimum, :maximum, :sum, :calculate,
:pluck, :pick, :ids
].freeze # :nodoc:
diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb
index ac37f73b76..37179774fa 100644
--- a/activerecord/lib/active_record/relation.rb
+++ b/activerecord/lib/active_record/relation.rb
@@ -5,7 +5,7 @@ module ActiveRecord
class Relation
MULTI_VALUE_METHODS = [:includes, :eager_load, :preload, :select, :group,
:order, :joins, :left_outer_joins, :references,
- :extending, :unscope]
+ :extending, :unscope, :optimizer_hints]
SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :reordering,
:reverse_order, :distinct, :create_with, :skip_query_cache]
diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb
index 0f2cff4c9c..05ea0850d3 100644
--- a/activerecord/lib/active_record/relation/query_methods.rb
+++ b/activerecord/lib/active_record/relation/query_methods.rb
@@ -350,7 +350,7 @@ module ActiveRecord
VALID_UNSCOPING_VALUES = Set.new([:where, :select, :group, :order, :lock,
:limit, :offset, :joins, :left_outer_joins,
- :includes, :from, :readonly, :having])
+ :includes, :from, :readonly, :having, :optimizer_hints])
# Removes an unwanted relation that is already defined on a chain of relations.
# This is useful when passing around chains of relations and would like to
@@ -901,6 +901,29 @@ module ActiveRecord
self
end
+ # Specify optimizer hints to be used in the SELECT statement.
+ #
+ # Example (for MySQL):
+ #
+ # Topic.optimizer_hints("MAX_EXECUTION_TIME(50000)", "NO_INDEX_MERGE(topics)")
+ # # SELECT /*+ MAX_EXECUTION_TIME(50000) NO_INDEX_MERGE(topics) */ `topics`.* FROM `topics`
+ #
+ # Example (for PostgreSQL with pg_hint_plan):
+ #
+ # Topic.optimizer_hints("SeqScan(topics)", "Parallel(topics 8)")
+ # # SELECT /*+ SeqScan(topics) Parallel(topics 8) */ "topics".* FROM "topics"
+ def optimizer_hints(*args)
+ check_if_method_has_arguments!(:optimizer_hints, args)
+ spawn.optimizer_hints!(*args)
+ end
+
+ def optimizer_hints!(*args) # :nodoc:
+ args.flatten!
+
+ self.optimizer_hints_values += args
+ self
+ end
+
# Reverse the existing order clause on the relation.
#
# User.order('name ASC').reverse_order # generated SQL has 'ORDER BY name DESC'
@@ -977,6 +1000,7 @@ module ActiveRecord
build_select(arel)
+ arel.optimizer_hints(*optimizer_hints_values) unless optimizer_hints_values.empty?
arel.distinct(distinct_value)
arel.from(build_from) unless from_clause.empty?
arel.lock(lock_value) if lock_value
diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb
index a8433fa0db..155d2b0b98 100644
--- a/activerecord/lib/active_record/tasks/database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/database_tasks.rb
@@ -185,14 +185,15 @@ module ActiveRecord
def truncate_tables(configuration)
ActiveRecord::Base.connected_to(database: { truncation: configuration }) do
table_names = ActiveRecord::Base.connection.tables
- internal_table_names = [
+ table_names -= [
ActiveRecord::Base.schema_migrations_table_name,
ActiveRecord::Base.internal_metadata_table_name
]
- class_for_adapter(configuration["adapter"]).new(configuration).truncate_tables(*table_names.without(*internal_table_names))
+ ActiveRecord::Base.connection.truncate_tables(*table_names)
end
end
+ private :truncate_tables
def truncate_all(environment = env)
ActiveRecord::Base.configurations.configs_for(env_name: environment).each do |db_config|
diff --git a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb
index f2b4ead98d..1c1b29b5e1 100644
--- a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb
@@ -31,16 +31,6 @@ module ActiveRecord
connection.recreate_database configuration["database"], creation_options
end
- def truncate_tables(*table_names)
- return if table_names.empty?
-
- ActiveRecord::Base.connection.disable_referential_integrity do
- table_names.each do |table_name|
- ActiveRecord::Base.connection.truncate(table_name)
- end
- end
- end
-
def charset
connection.charset
end
diff --git a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb
index dc368eb97d..8acb11f75f 100644
--- a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb
@@ -48,18 +48,6 @@ module ActiveRecord
create true
end
- def truncate_tables(*table_names)
- return if table_names.empty?
-
- ActiveRecord::Base.connection.disable_referential_integrity do
- quoted_table_names = table_names.map do |table_name|
- ActiveRecord::Base.connection.quote_table_name(table_name)
- end
-
- ActiveRecord::Base.connection.execute "TRUNCATE TABLE #{quoted_table_names.join(", ")}"
- end
- end
-
def structure_dump(filename, extra_flags)
set_psql_env
diff --git a/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb b/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb
index cedbae6b7f..a82cea80ca 100644
--- a/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb
@@ -33,16 +33,6 @@ module ActiveRecord
create
end
- def truncate_tables(*table_names)
- return if table_names.empty?
-
- ActiveRecord::Base.connection.disable_referential_integrity do
- table_names.each do |table_name|
- ActiveRecord::Base.connection.truncate(table_name)
- end
- end
- end
-
def charset
connection.encoding
end
diff --git a/activerecord/lib/arel/insert_manager.rb b/activerecord/lib/arel/insert_manager.rb
index c90fc33a48..cb31e3060b 100644
--- a/activerecord/lib/arel/insert_manager.rb
+++ b/activerecord/lib/arel/insert_manager.rb
@@ -33,13 +33,13 @@ module Arel # :nodoc: all
@ast.columns << column
values << value
end
- @ast.values = create_values values, @ast.columns
+ @ast.values = create_values(values)
end
self
end
- def create_values(values, columns)
- Nodes::Values.new values, columns
+ def create_values(values)
+ Nodes::ValuesList.new([values])
end
def create_values_list(rows)
diff --git a/activerecord/lib/arel/nodes.rb b/activerecord/lib/arel/nodes.rb
index 5af0e532e2..2f6dd9bc45 100644
--- a/activerecord/lib/arel/nodes.rb
+++ b/activerecord/lib/arel/nodes.rb
@@ -45,7 +45,6 @@ require "arel/nodes/and"
require "arel/nodes/function"
require "arel/nodes/count"
require "arel/nodes/extract"
-require "arel/nodes/values"
require "arel/nodes/values_list"
require "arel/nodes/named_function"
diff --git a/activerecord/lib/arel/nodes/select_core.rb b/activerecord/lib/arel/nodes/select_core.rb
index 73461ff683..6585f9b3ec 100644
--- a/activerecord/lib/arel/nodes/select_core.rb
+++ b/activerecord/lib/arel/nodes/select_core.rb
@@ -4,12 +4,13 @@ module Arel # :nodoc: all
module Nodes
class SelectCore < Arel::Nodes::Node
attr_accessor :projections, :wheres, :groups, :windows
- attr_accessor :havings, :source, :set_quantifier
+ attr_accessor :havings, :source, :set_quantifier, :optimizer_hints
def initialize
super()
@source = JoinSource.new nil
+ @optimizer_hints = nil
# https://ronsavage.github.io/SQL/sql-92.bnf.html#set%20quantifier
@set_quantifier = nil
@projections = []
@@ -42,7 +43,7 @@ module Arel # :nodoc: all
def hash
[
- @source, @set_quantifier, @projections,
+ @source, @set_quantifier, @projections, @optimizer_hints,
@wheres, @groups, @havings, @windows
].hash
end
@@ -51,6 +52,7 @@ module Arel # :nodoc: all
self.class == other.class &&
self.source == other.source &&
self.set_quantifier == other.set_quantifier &&
+ self.optimizer_hints == other.optimizer_hints &&
self.projections == other.projections &&
self.wheres == other.wheres &&
self.groups == other.groups &&
diff --git a/activerecord/lib/arel/nodes/unary.rb b/activerecord/lib/arel/nodes/unary.rb
index 00639304e4..6d1ac36b0e 100644
--- a/activerecord/lib/arel/nodes/unary.rb
+++ b/activerecord/lib/arel/nodes/unary.rb
@@ -35,6 +35,7 @@ module Arel # :nodoc: all
Not
Offset
On
+ OptimizerHints
Ordering
RollUp
}.each do |name|
diff --git a/activerecord/lib/arel/nodes/values.rb b/activerecord/lib/arel/nodes/values.rb
deleted file mode 100644
index 650248dc04..0000000000
--- a/activerecord/lib/arel/nodes/values.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-# frozen_string_literal: true
-
-module Arel # :nodoc: all
- module Nodes
- class Values < Arel::Nodes::Binary
- alias :expressions :left
- alias :expressions= :left=
- alias :columns :right
- alias :columns= :right=
-
- def initialize(exprs, columns = [])
- super
- end
- end
- end
-end
diff --git a/activerecord/lib/arel/nodes/values_list.rb b/activerecord/lib/arel/nodes/values_list.rb
index 27109848e4..1a9d9ebf01 100644
--- a/activerecord/lib/arel/nodes/values_list.rb
+++ b/activerecord/lib/arel/nodes/values_list.rb
@@ -2,23 +2,8 @@
module Arel # :nodoc: all
module Nodes
- class ValuesList < Node
- attr_reader :rows
-
- def initialize(rows)
- @rows = rows
- super()
- end
-
- def hash
- @rows.hash
- end
-
- def eql?(other)
- self.class == other.class &&
- self.rows == other.rows
- end
- alias :== :eql?
+ class ValuesList < Unary
+ alias :rows :expr
end
end
end
diff --git a/activerecord/lib/arel/select_manager.rb b/activerecord/lib/arel/select_manager.rb
index 0da52ca91b..32286b67f4 100644
--- a/activerecord/lib/arel/select_manager.rb
+++ b/activerecord/lib/arel/select_manager.rb
@@ -146,6 +146,13 @@ module Arel # :nodoc: all
@ctx.projections = projections
end
+ def optimizer_hints(*hints)
+ unless hints.empty?
+ @ctx.optimizer_hints = Arel::Nodes::OptimizerHints.new(hints)
+ end
+ self
+ end
+
def distinct(value = true)
if value
@ctx.set_quantifier = Arel::Nodes::Distinct.new
diff --git a/activerecord/lib/arel/visitors/depth_first.rb b/activerecord/lib/arel/visitors/depth_first.rb
index 92d309453c..109afb7402 100644
--- a/activerecord/lib/arel/visitors/depth_first.rb
+++ b/activerecord/lib/arel/visitors/depth_first.rb
@@ -35,6 +35,8 @@ module Arel # :nodoc: all
alias :visit_Arel_Nodes_Ascending :unary
alias :visit_Arel_Nodes_Descending :unary
alias :visit_Arel_Nodes_UnqualifiedColumn :unary
+ alias :visit_Arel_Nodes_OptimizerHints :unary
+ alias :visit_Arel_Nodes_ValuesList :unary
def function(o)
visit o.expressions
@@ -102,7 +104,6 @@ module Arel # :nodoc: all
alias :visit_Arel_Nodes_Regexp :binary
alias :visit_Arel_Nodes_RightOuterJoin :binary
alias :visit_Arel_Nodes_TableAlias :binary
- alias :visit_Arel_Nodes_Values :binary
alias :visit_Arel_Nodes_When :binary
def visit_Arel_Nodes_StringJoin(o)
diff --git a/activerecord/lib/arel/visitors/dot.rb b/activerecord/lib/arel/visitors/dot.rb
index 6389c875cb..37803ce0c0 100644
--- a/activerecord/lib/arel/visitors/dot.rb
+++ b/activerecord/lib/arel/visitors/dot.rb
@@ -46,8 +46,8 @@ module Arel # :nodoc: all
visit_edge o, "distinct"
end
- def visit_Arel_Nodes_Values(o)
- visit_edge o, "expressions"
+ def visit_Arel_Nodes_ValuesList(o)
+ visit_edge o, "rows"
end
def visit_Arel_Nodes_StringJoin(o)
@@ -82,6 +82,7 @@ module Arel # :nodoc: all
alias :visit_Arel_Nodes_Offset :unary
alias :visit_Arel_Nodes_On :unary
alias :visit_Arel_Nodes_UnqualifiedColumn :unary
+ alias :visit_Arel_Nodes_OptimizerHints :unary
alias :visit_Arel_Nodes_Preceding :unary
alias :visit_Arel_Nodes_Following :unary
alias :visit_Arel_Nodes_Rows :unary
diff --git a/activerecord/lib/arel/visitors/ibm_db.rb b/activerecord/lib/arel/visitors/ibm_db.rb
index 73166054da..0ffc0725f7 100644
--- a/activerecord/lib/arel/visitors/ibm_db.rb
+++ b/activerecord/lib/arel/visitors/ibm_db.rb
@@ -4,6 +4,14 @@ module Arel # :nodoc: all
module Visitors
class IBM_DB < Arel::Visitors::ToSql
private
+ def visit_Arel_Nodes_SelectCore(o, collector)
+ collector = super
+ maybe_visit o.optimizer_hints, collector
+ end
+
+ def visit_Arel_Nodes_OptimizerHints(o, collector)
+ collector << "/* <OPTGUIDELINES>#{sanitize_as_sql_comment(o).join}</OPTGUIDELINES> */"
+ end
def visit_Arel_Nodes_Limit(o, collector)
collector << "FETCH FIRST "
@@ -16,6 +24,10 @@ module Arel # :nodoc: all
collector = visit [o.left, o.right, 0, 1], collector
collector << ")"
end
+
+ def collect_optimizer_hints(o, collector)
+ collector
+ end
end
end
end
diff --git a/activerecord/lib/arel/visitors/informix.rb b/activerecord/lib/arel/visitors/informix.rb
index 208fa15aef..cd43be8858 100644
--- a/activerecord/lib/arel/visitors/informix.rb
+++ b/activerecord/lib/arel/visitors/informix.rb
@@ -42,10 +42,15 @@ module Arel # :nodoc: all
collector
end
+ def visit_Arel_Nodes_OptimizerHints(o, collector)
+ collector << "/*+ #{sanitize_as_sql_comment(o).join(", ")} */"
+ end
+
def visit_Arel_Nodes_Offset(o, collector)
collector << "SKIP "
visit o.expr, collector
end
+
def visit_Arel_Nodes_Limit(o, collector)
collector << "FIRST "
visit o.expr, collector
diff --git a/activerecord/lib/arel/visitors/mssql.rb b/activerecord/lib/arel/visitors/mssql.rb
index fdd864b40d..85815baca2 100644
--- a/activerecord/lib/arel/visitors/mssql.rb
+++ b/activerecord/lib/arel/visitors/mssql.rb
@@ -76,6 +76,15 @@ module Arel # :nodoc: all
end
end
+ def visit_Arel_Nodes_SelectCore(o, collector)
+ collector = super
+ maybe_visit o.optimizer_hints, collector
+ end
+
+ def visit_Arel_Nodes_OptimizerHints(o, collector)
+ collector << "OPTION (#{sanitize_as_sql_comment(o).join(", ")})"
+ end
+
def get_offset_limit_clause(o)
first_row = o.offset ? o.offset.expr.to_i + 1 : 1
last_row = o.limit ? o.limit.expr.to_i - 1 + first_row : nil
@@ -103,6 +112,10 @@ module Arel # :nodoc: all
end
end
+ def collect_optimizer_hints(o, collector)
+ collector
+ end
+
def determine_order_by(orders, x)
if orders.any?
orders
diff --git a/activerecord/lib/arel/visitors/to_sql.rb b/activerecord/lib/arel/visitors/to_sql.rb
index d0dec63860..583f920290 100644
--- a/activerecord/lib/arel/visitors/to_sql.rb
+++ b/activerecord/lib/arel/visitors/to_sql.rb
@@ -159,7 +159,7 @@ module Arel # :nodoc: all
when Nodes::SqlLiteral, Nodes::BindParam
collector = visit(value, collector)
else
- collector << quote(value)
+ collector << quote(value).to_s
end
collector << COMMA unless k == row_len
end
@@ -169,25 +169,6 @@ module Arel # :nodoc: all
collector
end
- def visit_Arel_Nodes_Values(o, collector)
- collector << "VALUES ("
-
- len = o.expressions.length - 1
- o.expressions.each_with_index { |value, i|
- case value
- when Nodes::SqlLiteral, Nodes::BindParam
- collector = visit value, collector
- else
- collector << quote(value).to_s
- end
- unless i == len
- collector << COMMA
- end
- }
-
- collector << ")"
- end
-
def visit_Arel_Nodes_SelectStatement(o, collector)
if o.with
collector = visit o.with, collector
@@ -219,6 +200,7 @@ module Arel # :nodoc: all
def visit_Arel_Nodes_SelectCore(o, collector)
collector << "SELECT"
+ collector = collect_optimizer_hints(o, collector)
collector = maybe_visit o.set_quantifier, collector
collect_nodes_for o.projections, collector, SPACE
@@ -236,6 +218,10 @@ module Arel # :nodoc: all
collector
end
+ def visit_Arel_Nodes_OptimizerHints(o, collector)
+ collector << "/*+ #{sanitize_as_sql_comment(o).join(" ")} */"
+ end
+
def collect_nodes_for(nodes, collector, spacer, connector = COMMA)
unless nodes.empty?
collector << spacer
@@ -799,6 +785,16 @@ module Arel # :nodoc: all
@connection.quote_column_name(name)
end
+ def sanitize_as_sql_comment(o)
+ o.expr.map { |v|
+ v.gsub(%r{ (/ (?: | \g<1>) \*) \+? \s* | \s* (\* (?: | \g<2>) /) }x, "")
+ }
+ end
+
+ def collect_optimizer_hints(o, collector)
+ maybe_visit o.optimizer_hints, collector
+ end
+
def maybe_visit(thing, collector)
return collector unless thing
collector << " "
diff --git a/activerecord/test/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb
index d90003b0ba..2b20d842e8 100644
--- a/activerecord/test/cases/adapter_test.rb
+++ b/activerecord/test/cases/adapter_test.rb
@@ -452,6 +452,8 @@ module ActiveRecord
class AdapterTestWithoutTransaction < ActiveRecord::TestCase
self.use_transactional_tests = false
+ fixtures :posts, :authors, :author_addresses
+
def setup
@connection = ActiveRecord::Base.connection
end
@@ -482,6 +484,26 @@ module ActiveRecord
end
end
+ def test_truncate
+ assert_operator @connection.query_value("SELECT COUNT(*) FROM posts"), :>, 0
+
+ @connection.truncate("posts")
+
+ assert_equal 0, @connection.query_value("SELECT COUNT(*) FROM posts")
+ end
+
+ def test_truncate_tables
+ assert_operator @connection.query_value("SELECT COUNT(*) FROM posts"), :>, 0
+ assert_operator @connection.query_value("SELECT COUNT(*) FROM authors"), :>, 0
+ assert_operator @connection.query_value("SELECT COUNT(*) FROM author_addresses"), :>, 0
+
+ @connection.truncate_tables("author_addresses", "authors", "posts")
+
+ assert_equal 0, @connection.query_value("SELECT COUNT(*) FROM posts")
+ assert_equal 0, @connection.query_value("SELECT COUNT(*) FROM authors")
+ assert_equal 0, @connection.query_value("SELECT COUNT(*) FROM author_addresses")
+ end
+
# test resetting sequences in odd tables in PostgreSQL
if ActiveRecord::Base.connection.respond_to?(:reset_pk_sequence!)
require "models/movie"
diff --git a/activerecord/test/cases/adapters/mysql2/connection_test.rb b/activerecord/test/cases/adapters/mysql2/connection_test.rb
index 3103589186..9c6566106a 100644
--- a/activerecord/test/cases/adapters/mysql2/connection_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/connection_test.rb
@@ -28,17 +28,6 @@ class Mysql2ConnectionTest < ActiveRecord::Mysql2TestCase
end
end
- def test_truncate
- rows = ActiveRecord::Base.connection.exec_query("select count(*) from comments")
- count = rows.first.values.first
- assert_operator count, :>, 0
-
- ActiveRecord::Base.connection.truncate("comments")
- rows = ActiveRecord::Base.connection.exec_query("select count(*) from comments")
- count = rows.first.values.first
- assert_equal 0, count
- end
-
def test_no_automatic_reconnection_after_timeout
assert_predicate @connection, :active?
@connection.update("set @@wait_timeout=1")
diff --git a/activerecord/test/cases/adapters/mysql2/optimizer_hints_test.rb b/activerecord/test/cases/adapters/mysql2/optimizer_hints_test.rb
new file mode 100644
index 0000000000..b9794c5710
--- /dev/null
+++ b/activerecord/test/cases/adapters/mysql2/optimizer_hints_test.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/post"
+
+if supports_optimizer_hints?
+ class Mysql2OptimzerHintsTest < ActiveRecord::Mysql2TestCase
+ fixtures :posts
+
+ def test_optimizer_hints
+ assert_sql(%r{\ASELECT /\*\+ NO_RANGE_OPTIMIZATION\(posts index_posts_on_author_id\) \*/}) do
+ posts = Post.optimizer_hints("NO_RANGE_OPTIMIZATION(posts index_posts_on_author_id)")
+ posts = posts.select(:id).where(author_id: [0, 1])
+ assert_includes posts.explain, "| index | index_posts_on_author_id | index_posts_on_author_id |"
+ end
+ end
+
+ def test_optimizer_hints_is_sanitized
+ assert_sql(%r{\ASELECT /\*\+ NO_RANGE_OPTIMIZATION\(posts index_posts_on_author_id\) \*/}) do
+ posts = Post.optimizer_hints("/*+ NO_RANGE_OPTIMIZATION(posts index_posts_on_author_id) */")
+ posts = posts.select(:id).where(author_id: [0, 1])
+ assert_includes posts.explain, "| index | index_posts_on_author_id | index_posts_on_author_id |"
+ end
+
+ assert_sql(%r{\ASELECT /\*\+ `posts`\.\*, \*/}) do
+ posts = Post.optimizer_hints("**// `posts`.*, //**")
+ posts = posts.select(:id).where(author_id: [0, 1])
+ assert_equal({ "id" => 1 }, posts.first.as_json)
+ end
+ end
+
+ def test_optimizer_hints_with_unscope
+ assert_sql(%r{\ASELECT `posts`\.`id`}) do
+ posts = Post.optimizer_hints("/*+ NO_RANGE_OPTIMIZATION(posts index_posts_on_author_id) */")
+ posts = posts.select(:id).where(author_id: [0, 1])
+ posts.unscope(:optimizer_hints).load
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/connection_test.rb b/activerecord/test/cases/adapters/postgresql/connection_test.rb
index 40ab158c05..210758f462 100644
--- a/activerecord/test/cases/adapters/postgresql/connection_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/connection_test.rb
@@ -25,14 +25,6 @@ module ActiveRecord
super
end
- def test_truncate
- count = ActiveRecord::Base.connection.execute("select count(*) from comments").first["count"].to_i
- assert_operator count, :>, 0
- ActiveRecord::Base.connection.truncate("comments")
- count = ActiveRecord::Base.connection.execute("select count(*) from comments").first["count"].to_i
- assert_equal 0, count
- end
-
def test_encoding
assert_queries(1) do
assert_not_nil @connection.encoding
diff --git a/activerecord/test/cases/adapters/postgresql/optimizer_hints_test.rb b/activerecord/test/cases/adapters/postgresql/optimizer_hints_test.rb
new file mode 100644
index 0000000000..5e4bf232e1
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/optimizer_hints_test.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/post"
+
+if supports_optimizer_hints?
+ class PostgresqlOptimzerHintsTest < ActiveRecord::PostgreSQLTestCase
+ fixtures :posts
+
+ def setup
+ enable_extension!("pg_hint_plan", ActiveRecord::Base.connection)
+ end
+
+ def test_optimizer_hints
+ assert_sql(%r{\ASELECT /\*\+ SeqScan\(posts\) \*/}) do
+ posts = Post.optimizer_hints("SeqScan(posts)")
+ posts = posts.select(:id).where(author_id: [0, 1])
+ assert_includes posts.explain, "Seq Scan on posts"
+ end
+ end
+
+ def test_optimizer_hints_is_sanitized
+ assert_sql(%r{\ASELECT /\*\+ SeqScan\(posts\) \*/}) do
+ posts = Post.optimizer_hints("/*+ SeqScan(posts) */")
+ posts = posts.select(:id).where(author_id: [0, 1])
+ assert_includes posts.explain, "Seq Scan on posts"
+ end
+
+ assert_sql(%r{\ASELECT /\*\+ "posts"\.\*, \*/}) do
+ posts = Post.optimizer_hints("**// \"posts\".*, //**")
+ posts = posts.select(:id).where(author_id: [0, 1])
+ assert_equal({ "id" => 1 }, posts.first.as_json)
+ end
+ end
+
+ def test_optimizer_hints_with_unscope
+ assert_sql(%r{\ASELECT "posts"\."id"}) do
+ posts = Post.optimizer_hints("/*+ SeqScan(posts) */")
+ posts = posts.select(:id).where(author_id: [0, 1])
+ posts.unscope(:optimizer_hints).load
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/adapters/sqlite3/connection_test.rb b/activerecord/test/cases/adapters/sqlite3/connection_test.rb
deleted file mode 100644
index 3dabc8766a..0000000000
--- a/activerecord/test/cases/adapters/sqlite3/connection_test.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-# frozen_string_literal: true
-
-require "cases/helper"
-
-class SQLite3ConnectionTest < ActiveRecord::SQLite3TestCase
- fixtures :comments
-
- def test_truncate
- rows = ActiveRecord::Base.connection.exec_query("select count(*) from comments")
- count = rows.first.values.first
- assert_operator count, :>, 0
-
- ActiveRecord::Base.connection.truncate("comments")
- rows = ActiveRecord::Base.connection.exec_query("select count(*) from comments")
- count = rows.first.values.first
- assert_equal 0, count
- end
-end
diff --git a/activerecord/test/cases/arel/insert_manager_test.rb b/activerecord/test/cases/arel/insert_manager_test.rb
index 2376ad8d37..79b85742ee 100644
--- a/activerecord/test/cases/arel/insert_manager_test.rb
+++ b/activerecord/test/cases/arel/insert_manager_test.rb
@@ -11,19 +11,18 @@ module Arel
end
describe "insert" do
- it "can create a Values node" do
+ it "can create a ValuesList node" do
manager = Arel::InsertManager.new
- values = manager.create_values %w{ a b }, %w{ c d }
+ values = manager.create_values_list([%w{ a b }, %w{ c d }])
- assert_kind_of Arel::Nodes::Values, values
- assert_equal %w{ a b }, values.left
- assert_equal %w{ c d }, values.right
+ assert_kind_of Arel::Nodes::ValuesList, values
+ assert_equal [%w{ a b }, %w{ c d }], values.rows
end
it "allows sql literals" do
manager = Arel::InsertManager.new
manager.into Table.new(:users)
- manager.values = manager.create_values [Arel.sql("*")], %w{ a }
+ manager.values = manager.create_values([Arel.sql("*")])
manager.to_sql.must_be_like %{
INSERT INTO \"users\" VALUES (*)
}
@@ -186,9 +185,9 @@ module Arel
manager = Arel::InsertManager.new
manager.into table
- manager.values = Nodes::Values.new [1]
+ manager.values = Nodes::ValuesList.new([[1], [2]])
manager.to_sql.must_be_like %{
- INSERT INTO "users" VALUES (1)
+ INSERT INTO "users" VALUES (1), (2)
}
end
@@ -210,11 +209,11 @@ module Arel
manager = Arel::InsertManager.new
manager.into table
- manager.values = Nodes::Values.new [1, "aaron"]
+ manager.values = Nodes::ValuesList.new([[1, "aaron"], [2, "david"]])
manager.columns << table[:id]
manager.columns << table[:name]
manager.to_sql.must_be_like %{
- INSERT INTO "users" ("id", "name") VALUES (1, 'aaron')
+ INSERT INTO "users" ("id", "name") VALUES (1, 'aaron'), (2, 'david')
}
end
end
diff --git a/activerecord/test/cases/arel/visitors/depth_first_test.rb b/activerecord/test/cases/arel/visitors/depth_first_test.rb
index f94ad521d7..4a57608411 100644
--- a/activerecord/test/cases/arel/visitors/depth_first_test.rb
+++ b/activerecord/test/cases/arel/visitors/depth_first_test.rb
@@ -33,6 +33,7 @@ module Arel
Arel::Nodes::Ordering,
Arel::Nodes::StringJoin,
Arel::Nodes::UnqualifiedColumn,
+ Arel::Nodes::ValuesList,
Arel::Nodes::Limit,
Arel::Nodes::Else,
].each do |klass|
@@ -116,7 +117,6 @@ module Arel
Arel::Nodes::NotIn,
Arel::Nodes::Or,
Arel::Nodes::TableAlias,
- Arel::Nodes::Values,
Arel::Nodes::As,
Arel::Nodes::DeleteStatement,
Arel::Nodes::JoinSource,
diff --git a/activerecord/test/cases/arel/visitors/dot_test.rb b/activerecord/test/cases/arel/visitors/dot_test.rb
index 6b3c132f83..ade53c358e 100644
--- a/activerecord/test/cases/arel/visitors/dot_test.rb
+++ b/activerecord/test/cases/arel/visitors/dot_test.rb
@@ -37,6 +37,7 @@ module Arel
Arel::Nodes::Offset,
Arel::Nodes::Ordering,
Arel::Nodes::UnqualifiedColumn,
+ Arel::Nodes::ValuesList,
Arel::Nodes::Limit,
].each do |klass|
define_method("test_#{klass.name.gsub('::', '_')}") do
@@ -61,7 +62,6 @@ module Arel
Arel::Nodes::NotIn,
Arel::Nodes::Or,
Arel::Nodes::TableAlias,
- Arel::Nodes::Values,
Arel::Nodes::As,
Arel::Nodes::DeleteStatement,
Arel::Nodes::JoinSource,
diff --git a/activerecord/test/cases/arel/visitors/to_sql_test.rb b/activerecord/test/cases/arel/visitors/to_sql_test.rb
index 4bfa799a96..625e37f1c0 100644
--- a/activerecord/test/cases/arel/visitors/to_sql_test.rb
+++ b/activerecord/test/cases/arel/visitors/to_sql_test.rb
@@ -23,9 +23,9 @@ module Arel
sql.must_be_like "?"
end
- it "does not quote BindParams used as part of a Values" do
+ it "does not quote BindParams used as part of a ValuesList" do
bp = Nodes::BindParam.new(1)
- values = Nodes::Values.new([bp])
+ values = Nodes::ValuesList.new([[bp]])
sql = compile values
sql.must_be_like "VALUES (?)"
end
diff --git a/activerecord/test/cases/bind_parameter_test.rb b/activerecord/test/cases/bind_parameter_test.rb
index eb8b45431f..85685d1d00 100644
--- a/activerecord/test/cases/bind_parameter_test.rb
+++ b/activerecord/test/cases/bind_parameter_test.rb
@@ -184,7 +184,7 @@ if ActiveRecord::Base.connection.prepared_statements
name: "SQL",
sql: "select * from topics where id = ?",
binds: binds,
- type_casted_binds: @connection.type_casted_binds(binds)
+ type_casted_binds: @connection.send(:type_casted_binds, binds)
}
event = ActiveSupport::Notifications::Event.new(
diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb
index b4f28fbfd6..0cb868da6e 100644
--- a/activerecord/test/cases/fixtures_test.rb
+++ b/activerecord/test/cases/fixtures_test.rb
@@ -209,7 +209,7 @@ class FixturesTest < ActiveRecord::TestCase
conn = ActiveRecord::Base.connection
mysql_margin = 2
packet_size = 1024
- bytes_needed_to_have_a_1024_bytes_fixture = 858
+ bytes_needed_to_have_a_1024_bytes_fixture = 906
fixtures = {
"traffic_lights" => [
{ "location" => "US", "state" => ["NY"], "long_state" => ["a" * bytes_needed_to_have_a_1024_bytes_fixture] },
@@ -496,11 +496,7 @@ class FixturesTest < ActiveRecord::TestCase
ActiveRecord::FixtureSet.create_fixtures(FIXTURES_ROOT + "/naked/yml", "parrots")
end
- if current_adapter?(:SQLite3Adapter)
- assert_equal(%(table "parrots" has no column named "arrr".), e.message)
- else
- assert_equal(%(table "parrots" has no columns named "arrr", "foobar".), e.message)
- end
+ assert_equal(%(table "parrots" has no columns named "arrr", "foobar".), e.message)
end
def test_yaml_file_with_symbol_columns
diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb
index a9ec667eba..f95d082907 100644
--- a/activerecord/test/cases/helper.rb
+++ b/activerecord/test/cases/helper.rb
@@ -64,6 +64,7 @@ end
supports_insert_on_duplicate_skip?
supports_insert_on_duplicate_update?
supports_insert_conflict_target?
+ supports_optimizer_hints?
].each do |method_name|
define_method method_name do
ActiveRecord::Base.connection.public_send(method_name)
diff --git a/activerecord/test/cases/insert_all_test.rb b/activerecord/test/cases/insert_all_test.rb
index 63245719c0..a3e920969d 100644
--- a/activerecord/test/cases/insert_all_test.rb
+++ b/activerecord/test/cases/insert_all_test.rb
@@ -136,4 +136,10 @@ class InsertAllTest < ActiveRecord::TestCase
assert_equal ["Out of the Silent Planet", "Perelandra"], Book.where(isbn: "1974522598").order(:name).pluck(:name)
end
+
+ def test_insert_all_raises_on_unknown_attribute
+ assert_raise ActiveRecord::UnknownAttributeError do
+ Book.insert_all! [{ unknown_attribute: "Test" }]
+ end
+ end
end
diff --git a/activerecord/test/cases/invertible_migration_test.rb b/activerecord/test/cases/invertible_migration_test.rb
index 6cf17ac15d..d68cc40107 100644
--- a/activerecord/test/cases/invertible_migration_test.rb
+++ b/activerecord/test/cases/invertible_migration_test.rb
@@ -308,6 +308,8 @@ module ActiveRecord
migration2 = DisableExtension1.new
migration3 = DisableExtension2.new
+ assert_equal true, Horse.connection.extension_available?("hstore")
+
migration1.migrate(:up)
migration2.migrate(:up)
assert_equal true, Horse.connection.extension_enabled?("hstore")
diff --git a/activestorage/db/update_migrate/20180723000244_add_foreign_key_constraint_to_active_storage_attachments_for_blob_id.rb b/activestorage/db/update_migrate/20180723000244_add_foreign_key_constraint_to_active_storage_attachments_for_blob_id.rb
index 6830203cd6..5472e3c87b 100644
--- a/activestorage/db/update_migrate/20180723000244_add_foreign_key_constraint_to_active_storage_attachments_for_blob_id.rb
+++ b/activestorage/db/update_migrate/20180723000244_add_foreign_key_constraint_to_active_storage_attachments_for_blob_id.rb
@@ -1,6 +1,8 @@
class AddForeignKeyConstraintToActiveStorageAttachmentsForBlobId < ActiveRecord::Migration[6.0]
def up
- unless foreign_key_exists?(:active_storage_attachments, column: :blob_id)
+ return if foreign_key_exists?(:active_storage_attachments, column: :blob_id)
+
+ if table_exists?(:active_storage_blobs)
add_foreign_key :active_storage_attachments, :active_storage_blobs, column: :blob_id
end
end
diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md
index 2610cc63a4..63e2e44597 100644
--- a/activesupport/CHANGELOG.md
+++ b/activesupport/CHANGELOG.md
@@ -1,3 +1,21 @@
+* Fix `Time#advance` to work with dates before 1001-03-07
+
+ Before:
+
+ Time.utc(1001, 3, 6).advance(years: -1) # => 1000-03-05 00:00:00 UTC
+
+ After
+
+ Time.utc(1001, 3, 6).advance(years: -1) # => 1000-03-06 00:00:00 UTC
+
+ Note that this doesn't affect `DateTime#advance` as that doesn't use a proleptic calendar.
+
+ *Andrew White*
+
+* In Zeitwerk mode, engines are now managed by the `main` autoloader. Engines may reference application constants, if the application is reloaded and we do not reload engines, they won't use the reloaded application code.
+
+ *Xavier Noria*
+
* Add support for supplying `locale` to `transliterate` and `parameterize`.
I18n.backend.store_translations(:de, i18n: { transliterate: { rule: { "ü" => "ue" } } })
diff --git a/activesupport/activesupport.gemspec b/activesupport/activesupport.gemspec
index 8409a0c420..51f5086cca 100644
--- a/activesupport/activesupport.gemspec
+++ b/activesupport/activesupport.gemspec
@@ -34,5 +34,5 @@ Gem::Specification.new do |s|
s.add_dependency "tzinfo", "~> 1.1"
s.add_dependency "minitest", "~> 5.1"
s.add_dependency "concurrent-ruby", "~> 1.0", ">= 1.0.2"
- s.add_dependency "zeitwerk", "~> 1.3", ">= 1.3.3"
+ s.add_dependency "zeitwerk", "~> 1.4"
end
diff --git a/activesupport/lib/active_support/core_ext/string/inflections.rb b/activesupport/lib/active_support/core_ext/string/inflections.rb
index e63b38b227..5eb8d9f99b 100644
--- a/activesupport/lib/active_support/core_ext/string/inflections.rb
+++ b/activesupport/lib/active_support/core_ext/string/inflections.rb
@@ -165,7 +165,7 @@ class String
# If the optional parameter +locale+ is specified,
# the word will be parameterized as a word of that language.
# By default, this parameter is set to <tt>nil</tt> and it will use
- # configured I18n.locale
+ # the configured <tt>I18n.locale</tt>.
#
# class Person
# def to_param
diff --git a/activesupport/lib/active_support/core_ext/time/calculations.rb b/activesupport/lib/active_support/core_ext/time/calculations.rb
index 120768dec5..f09a6271ad 100644
--- a/activesupport/lib/active_support/core_ext/time/calculations.rb
+++ b/activesupport/lib/active_support/core_ext/time/calculations.rb
@@ -170,8 +170,7 @@ class Time
options[:hours] = options.fetch(:hours, 0) + 24 * partial_days
end
- d = to_date.advance(options)
- d = d.gregorian if d.julian?
+ d = to_date.gregorian.advance(options)
time_advanced_by_date = change(year: d.year, month: d.month, day: d.day)
seconds_to_advance = \
options.fetch(:seconds, 0) +
diff --git a/activesupport/lib/active_support/dependencies/zeitwerk_integration.rb b/activesupport/lib/active_support/dependencies/zeitwerk_integration.rb
index 1e697e1ba5..c6fdade006 100644
--- a/activesupport/lib/active_support/dependencies/zeitwerk_integration.rb
+++ b/activesupport/lib/active_support/dependencies/zeitwerk_integration.rb
@@ -21,7 +21,11 @@ module ActiveSupport
end
def autoloaded_constants
- (Rails.autoloaders.main.loaded + Rails.autoloaders.once.loaded).to_a
+ cpaths = []
+ Rails.autoloaders.each do |autoloader|
+ cpaths.concat(autoloader.loaded_cpaths.to_a)
+ end
+ cpaths
end
def autoloaded?(object)
@@ -71,8 +75,7 @@ module ActiveSupport
end
def autoload_once?(autoload_path)
- Dependencies.autoload_once_paths.include?(autoload_path) ||
- Gem.path.any? { |gem_path| autoload_path.to_s.start_with?(gem_path) }
+ Dependencies.autoload_once_paths.include?(autoload_path)
end
def freeze_autoload_paths
diff --git a/activesupport/lib/active_support/evented_file_update_checker.rb b/activesupport/lib/active_support/evented_file_update_checker.rb
index 54bc5c0ae5..3893b0de0e 100644
--- a/activesupport/lib/active_support/evented_file_update_checker.rb
+++ b/activesupport/lib/active_support/evented_file_update_checker.rb
@@ -131,7 +131,9 @@ module ActiveSupport
ext = @ph.normalize_extension(file.extname)
file.dirname.ascend do |dir|
- if @dirs.fetch(dir, []).include?(ext)
+ matching = @dirs[dir]
+
+ if matching && (matching.empty? || matching.include?(ext))
break true
elsif dir == @lcsp || dir.root?
break false
diff --git a/activesupport/lib/active_support/inflector/transliterate.rb b/activesupport/lib/active_support/inflector/transliterate.rb
index fe778ff0e7..ec6e9ccb59 100644
--- a/activesupport/lib/active_support/inflector/transliterate.rb
+++ b/activesupport/lib/active_support/inflector/transliterate.rb
@@ -74,8 +74,8 @@ module ActiveSupport
#
# To use a custom separator, override the +separator+ argument.
#
- # parameterize("Donald E. Knuth", separator: '_') # => "donald_e_knuth"
- # parameterize("^très|Jolie__ ", separator: '_') # => "tres_jolie"
+ # parameterize("Donald E. Knuth", separator: '_') # => "donald_e_knuth"
+ # parameterize("^très|Jolie__ ", separator: '_') # => "tres_jolie"
#
# To preserve the case of the characters in a string, use the +preserve_case+ argument.
#
@@ -84,10 +84,14 @@ module ActiveSupport
#
# It preserves dashes and underscores unless they are used as separators:
#
- # parameterize("^très|Jolie__ ") # => "tres-jolie__"
- # parameterize("^très|Jolie-- ", separator: "_") # => "tres_jolie--"
- # parameterize("^très_Jolie-- ", separator: ".") # => "tres_jolie--"
+ # parameterize("^très|Jolie__ ") # => "tres-jolie__"
+ # parameterize("^très|Jolie-- ", separator: "_") # => "tres_jolie--"
+ # parameterize("^très_Jolie-- ", separator: ".") # => "tres_jolie--"
#
+ # If the optional parameter +locale+ is specified,
+ # the word will be parameterized as a word of that language.
+ # By default, this parameter is set to <tt>nil</tt> and it will use
+ # the configured <tt>I18n.locale<tt>.
def parameterize(string, separator: "-", preserve_case: false, locale: nil)
# Replace accented chars with their ASCII equivalents.
parameterized_string = transliterate(string, locale: locale)
diff --git a/activesupport/test/core_ext/time_ext_test.rb b/activesupport/test/core_ext/time_ext_test.rb
index 7078f3506d..590b81b770 100644
--- a/activesupport/test/core_ext/time_ext_test.rb
+++ b/activesupport/test/core_ext/time_ext_test.rb
@@ -514,6 +514,8 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase
assert_equal Time.local(1582, 10, 15, 15, 15, 10), Time.local(1582, 10, 14, 15, 15, 10).advance(days: 1)
assert_equal Time.local(1582, 10, 5, 15, 15, 10), Time.local(1582, 10, 4, 15, 15, 10).advance(days: 1)
assert_equal Time.local(1582, 10, 4, 15, 15, 10), Time.local(1582, 10, 5, 15, 15, 10).advance(days: -1)
+ assert_equal Time.local(999, 10, 4, 15, 15, 10), Time.local(1000, 10, 4, 15, 15, 10).advance(years: -1)
+ assert_equal Time.local(1000, 10, 4, 15, 15, 10), Time.local(999, 10, 4, 15, 15, 10).advance(years: 1)
end
def test_last_week
diff --git a/activesupport/test/file_update_checker_shared_tests.rb b/activesupport/test/file_update_checker_shared_tests.rb
index 72683816b3..84d89fa0a7 100644
--- a/activesupport/test/file_update_checker_shared_tests.rb
+++ b/activesupport/test/file_update_checker_shared_tests.rb
@@ -186,6 +186,18 @@ module FileUpdateCheckerSharedTests
assert_equal 1, i
end
+ test "should execute the block if files change in a watched directory any extensions" do
+ i = 0
+
+ checker = new_checker([], tmpdir => []) { i += 1 }
+
+ touch(tmpfile("foo.rb"))
+ wait
+
+ assert checker.execute_if_updated
+ assert_equal 1, i
+ end
+
test "should execute the block if files change in a watched directory several extensions" do
i = 0
diff --git a/guides/source/6_0_release_notes.md b/guides/source/6_0_release_notes.md
index b6a8536b38..0cf9ca09c7 100644
--- a/guides/source/6_0_release_notes.md
+++ b/guides/source/6_0_release_notes.md
@@ -77,10 +77,73 @@ Please refer to the [Changelog][railties] for detailed changes.
### Removals
+* Remove deprecated `after_bundle` helper inside plugins templates.
+ ([Commit](https://github.com/rails/rails/commit/4d51efe24e461a2a3ed562787308484cd48370c7))
+
+* Remove deprecated support to `config.ru` that uses the application
+ class as argument of `run`.
+ ([Commit](https://github.com/rails/rails/commit/553b86fc751c751db504bcbe2d033eb2bb5b6a0b))
+
+* Remove deprecated `environment` argument from the rails commands.
+ ([Commit](https://github.com/rails/rails/commit/e20589c9be09c7272d73492d4b0f7b24e5595571))
+
+* Remove deprecated `capify!` method in generators and templates.
+ ([Commit](https://github.com/rails/rails/commit/9d39f81d512e0d16a27e2e864ea2dd0e8dc41b17))
+
+* Remove deprecated `config.secret_token`.
+ ([Commit](https://github.com/rails/rails/commit/46ac5fe69a20d4539a15929fe48293e1809a26b0))
+
### Deprecations
+* Deprecate passing Rack server name as a regular argument to `rails server`.
+ ([Pull Request](https://github.com/rails/rails/pull/32058))
+
+* Deprecate support for using `HOST` environment to specify server IP.
+ ([Pull Request](https://github.com/rails/rails/pull/32540))
+
+* Deprecate accessing hashes returned by `config_for` by non-symbol keys.
+ ([Pull Request](https://github.com/rails/rails/pull/35198))
+
### Notable changes
+* Add an explicit option `--using` or `-u` for specifying the server for the
+ `rails server` command.
+ ([Pull Request](https://github.com/rails/rails/pull/32058))
+
+* Add ability to see the output of `rails routes` in expanded format.
+ ([Pull Request](https://github.com/rails/rails/pull/32130))
+
+* Run the seed database task using inline Active Job adapter.
+ ([Pull Request](https://github.com/rails/rails/pull/34953))
+
+* Add a command `rails db:system:change` to change the database of the application.
+ ([Pull Request](https://github.com/rails/rails/pull/34832))
+
+* Add `rails test:channels` command to test only Action Cable channels.
+ ([Pull Request](https://github.com/rails/rails/pull/34947))
+
+* Introduce guard against DNS rebinding attacks.
+ ([Pull Request](https://github.com/rails/rails/pull/33145))
+
+* Add ability to abort on failure while running generator commands.
+ ([Pull Request](https://github.com/rails/rails/pull/34420))
+
+* Make Webpacker the default JavaScript compiler for Rails 6.
+ ([Pull Request](https://github.com/rails/rails/pull/33079))
+
+* Add multiple database support for `rails db:migrate:status` command.
+ ([Pull Request](https://github.com/rails/rails/pull/34137))
+
+* Add ability to use different migration paths from multiple databases in
+ the generators.
+ ([Pull Request](https://github.com/rails/rails/pull/34021))
+
+* Add support for multi environment credentials=.
+ ([Pull Request](https://github.com/rails/rails/pull/33521))
+
+* Make `null_store` as default cache store in test environment.
+ ([Pull Request](https://github.com/rails/rails/pull/33773))
+
Action Cable
------------
diff --git a/guides/source/active_record_querying.md b/guides/source/active_record_querying.md
index 24dea0ec46..71a03e11d9 100644
--- a/guides/source/active_record_querying.md
+++ b/guides/source/active_record_querying.md
@@ -74,11 +74,13 @@ The methods are:
* `lock`
* `none`
* `offset`
+* `optimizer_hints`
* `order`
* `preload`
* `readonly`
* `references`
* `reorder`
+* `reselect`
* `reverse_order`
* `select`
* `where`
@@ -1731,10 +1733,13 @@ Client.find_by_sql("SELECT * FROM clients
### `select_all`
-`find_by_sql` has a close relative called `connection#select_all`. `select_all` will retrieve objects from the database using custom SQL just like `find_by_sql` but will not instantiate them. This method will return an instance of `ActiveRecord::Result` class and calling `to_hash` on this object would return you an array of hashes where each hash indicates a record.
+`find_by_sql` has a close relative called `connection#select_all`. `select_all` will retrieve
+objects from the database using custom SQL just like `find_by_sql` but will not instantiate them.
+This method will return an instance of `ActiveRecord::Result` class and calling `to_a` on this
+object would return you an array of hashes where each hash indicates a record.
```ruby
-Client.connection.select_all("SELECT first_name, created_at FROM clients WHERE id = '1'").to_hash
+Client.connection.select_all("SELECT first_name, created_at FROM clients WHERE id = '1'").to_a
# => [
# {"first_name"=>"Rafael", "created_at"=>"2012-11-10 23:23:45.281189"},
# {"first_name"=>"Eileen", "created_at"=>"2013-12-09 11:22:35.221282"}
diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md
index 8163b282aa..226b949b34 100644
--- a/railties/CHANGELOG.md
+++ b/railties/CHANGELOG.md
@@ -1,3 +1,7 @@
+* Add `-e/--environment` option to `rails initializers`.
+
+ *Yuji Yaginuma*
+
## Rails 6.0.0.beta3 (March 11, 2019) ##
* No changes.
@@ -75,9 +79,9 @@
*George Claghorn*
-* Introduce guard against DNS rebinding attacks
+* Introduce guard against DNS rebinding attacks.
- The `ActionDispatch::HostAuthorization` is a new middleware that prevent
+ The `ActionDispatch::HostAuthorization` is a new middleware that prevents
against DNS rebinding and other `Host` header attacks. It is included in
the development environment by default with the following configuration:
diff --git a/railties/lib/rails/command/environment_argument.rb b/railties/lib/rails/command/environment_argument.rb
index fdc5ee92d9..0cb3f1ce1e 100644
--- a/railties/lib/rails/command/environment_argument.rb
+++ b/railties/lib/rails/command/environment_argument.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
require "active_support"
+require "active_support/core_ext/class/attribute"
module Rails
module Command
@@ -8,16 +9,16 @@ module Rails
extend ActiveSupport::Concern
included do
- class_option :environment, aliases: "-e", type: :string,
- desc: "Specifies the environment to run this console under (test/development/production)."
+ class_attribute :environment_desc, default: "Specifies the environment to run this #{self.command_name} under (test/development/production)."
+ class_option :environment, aliases: "-e", type: :string, desc: environment_desc
end
private
- def extract_environment_option_from_argument
+ def extract_environment_option_from_argument(default_environment: Rails::Command.environment)
if options[:environment]
self.options = options.merge(environment: acceptable_environment(options[:environment]))
else
- self.options = options.merge(environment: Rails::Command.environment)
+ self.options = options.merge(environment: default_environment)
end
end
diff --git a/railties/lib/rails/commands/credentials/credentials_command.rb b/railties/lib/rails/commands/credentials/credentials_command.rb
index 54ccd97506..a22b1f3f84 100644
--- a/railties/lib/rails/commands/credentials/credentials_command.rb
+++ b/railties/lib/rails/commands/credentials/credentials_command.rb
@@ -2,14 +2,15 @@
require "active_support"
require "rails/command/helpers/editor"
+require "rails/command/environment_argument"
module Rails
module Command
class CredentialsCommand < Rails::Command::Base # :nodoc:
include Helpers::Editor
+ include EnvironmentArgument
- class_option :environment, aliases: "-e", type: :string,
- desc: "Uses credentials from config/credentials/:environment.yml.enc encrypted by config/credentials/:environment.key key"
+ self.environment_desc = "Uses credentials from config/credentials/:environment.yml.enc encrypted by config/credentials/:environment.key key"
no_commands do
def help
@@ -20,6 +21,7 @@ module Rails
end
def edit
+ extract_environment_option_from_argument(default_environment: nil)
require_application!
ensure_editor_available(command: "bin/rails credentials:edit") || (return)
@@ -37,6 +39,7 @@ module Rails
end
def show
+ extract_environment_option_from_argument(default_environment: nil)
require_application!
say credentials.read.presence || missing_credentials_message
diff --git a/railties/lib/rails/commands/initializers/initializers_command.rb b/railties/lib/rails/commands/initializers/initializers_command.rb
index 33596177af..bd2f3bed67 100644
--- a/railties/lib/rails/commands/initializers/initializers_command.rb
+++ b/railties/lib/rails/commands/initializers/initializers_command.rb
@@ -1,10 +1,17 @@
# frozen_string_literal: true
+require "rails/command/environment_argument"
+
module Rails
module Command
class InitializersCommand < Base # :nodoc:
+ include EnvironmentArgument
+
desc "initializers", "Print out all defined initializers in the order they are invoked by Rails."
def perform
+ extract_environment_option_from_argument
+ ENV["RAILS_ENV"] = options[:environment]
+
require_application_and_environment!
Rails.application.initializers.tsort_each do |initializer|
diff --git a/railties/lib/rails/commands/runner/runner_command.rb b/railties/lib/rails/commands/runner/runner_command.rb
index cb693bcf34..40fb5e4d89 100644
--- a/railties/lib/rails/commands/runner/runner_command.rb
+++ b/railties/lib/rails/commands/runner/runner_command.rb
@@ -1,11 +1,13 @@
# frozen_string_literal: true
+require "rails/command/environment_argument"
+
module Rails
module Command
class RunnerCommand < Base # :nodoc:
- class_option :environment, aliases: "-e", type: :string,
- default: Rails::Command.environment.dup,
- desc: "The environment for the runner to operate under (test/development/production)"
+ include EnvironmentArgument
+
+ self.environment_desc = "The environment for the runner to operate under (test/development/production)"
no_commands do
def help
@@ -19,6 +21,8 @@ module Rails
end
def perform(code_or_file = nil, *command_argv)
+ extract_environment_option_from_argument
+
unless code_or_file
help
exit 1
diff --git a/railties/lib/rails/commands/server/server_command.rb b/railties/lib/rails/commands/server/server_command.rb
index 43f7b76fd6..982b83ead5 100644
--- a/railties/lib/rails/commands/server/server_command.rb
+++ b/railties/lib/rails/commands/server/server_command.rb
@@ -6,6 +6,7 @@ require "rails"
require "active_support/deprecation"
require "active_support/core_ext/string/filters"
require "rails/dev_caching"
+require "rails/command/environment_argument"
module Rails
class Server < ::Rack::Server
@@ -91,6 +92,8 @@ module Rails
module Command
class ServerCommand < Base # :nodoc:
+ include EnvironmentArgument
+
# Hard-coding a bunch of handlers here as we don't have a public way of
# querying them from the Rack::Handler registry.
RACK_SERVERS = %w(cgi fastcgi webrick lsws scgi thin puma unicorn)
@@ -109,8 +112,6 @@ module Rails
desc: "Uses a custom rackup configuration.", banner: :file
class_option :daemon, aliases: "-d", type: :boolean, default: false,
desc: "Runs server as a Daemon."
- class_option :environment, aliases: "-e", type: :string,
- desc: "Specifies the environment to run this server under (development/test/production).", banner: :name
class_option :using, aliases: "-u", type: :string,
desc: "Specifies the Rack server used to run the application (thin/puma/webrick).", banner: :name
class_option :pid, aliases: "-P", type: :string, default: DEFAULT_PID_PATH,
@@ -130,6 +131,7 @@ module Rails
end
def perform
+ extract_environment_option_from_argument
set_application_directory!
prepare_restart
diff --git a/railties/test/application/runner_test.rb b/railties/test/application/runner_test.rb
index d590b6f6fb..dfb9540093 100644
--- a/railties/test/application/runner_test.rb
+++ b/railties/test/application/runner_test.rb
@@ -109,6 +109,10 @@ module ApplicationTests
assert_match "production", rails("runner", "-e", "production", "puts Rails.env")
end
+ def test_environment_option_is_properly_expanded
+ assert_match "production", rails("runner", "-e", "prod", "puts Rails.env")
+ end
+
def test_runner_detects_syntax_errors
output = rails("runner", "puts 'hello world", allow_failure: true)
assert_not_predicate $?, :success?
diff --git a/railties/test/application/zeitwerk_integration_test.rb b/railties/test/application/zeitwerk_integration_test.rb
index cddbf5a22d..c82b37d07d 100644
--- a/railties/test/application/zeitwerk_integration_test.rb
+++ b/railties/test/application/zeitwerk_integration_test.rb
@@ -149,22 +149,27 @@ class ZeitwerkIntegrationTest < ActiveSupport::TestCase
assert $zeitwerk_integration_test_extras
end
- test "autoload paths that are below Gem.path go to the once autoloader" do
- app_dir "extras"
- add_to_config 'config.autoload_paths << "#{Rails.root}/extras"'
-
- # Mocks Gem.path to include the extras directory.
- Gem.singleton_class.prepend(
- Module.new do
- def path
- super + ["#{Rails.root}/extras"]
- end
- end
- )
+ test "autoload_paths are set as root dirs of main, and in the same order" do
boot
- assert_not_includes Rails.autoloaders.main.dirs, "#{app_path}/extras"
- assert_includes Rails.autoloaders.once.dirs, "#{app_path}/extras"
+ existing_autoload_paths = deps.autoload_paths.select { |dir| File.directory?(dir) }
+ assert_equal existing_autoload_paths, Rails.autoloaders.main.dirs
+ end
+
+ test "autoload_once_paths go to the once autoloader, and in the same order" do
+ extras = %w(e1 e2 e3)
+ extras.each do |extra|
+ app_dir extra
+ add_to_config %(config.autoload_once_paths << "\#{Rails.root}/#{extra}")
+ end
+
+ boot
+
+ extras = extras.map { |extra| "#{app_path}/#{extra}" }
+ extras.each do |extra|
+ assert_not_includes Rails.autoloaders.main.dirs, extra
+ end
+ assert_equal extras, Rails.autoloaders.once.dirs
end
test "clear reloads the main autoloader, and does not reload the once one" do
diff --git a/railties/test/commands/credentials_test.rb b/railties/test/commands/credentials_test.rb
index 10d0ba1cae..3654e96aed 100644
--- a/railties/test/commands/credentials_test.rb
+++ b/railties/test/commands/credentials_test.rb
@@ -63,6 +63,14 @@ class Rails::Command::CredentialsCommandTest < ActiveSupport::TestCase
end
end
+ test "edit command properly expand environment option" do
+ assert_match(/access_key_id: 123/, run_edit_command(environment: "prod"))
+ Dir.chdir(app_path) do
+ assert File.exist?("config/credentials/production.key")
+ assert File.exist?("config/credentials/production.yml.enc")
+ end
+ end
+
test "edit command does not raise when an initializer tries to access non-existent credentials" do
app_file "config/initializers/raise_when_loaded.rb", <<-RUBY
Rails.application.credentials.missing_key!
@@ -95,6 +103,12 @@ class Rails::Command::CredentialsCommandTest < ActiveSupport::TestCase
assert_match(/access_key_id: 123/, run_show_command(environment: "production"))
end
+ test "show command properly expand environment option" do
+ run_edit_command(environment: "production")
+
+ assert_match(/access_key_id: 123/, run_show_command(environment: "prod"))
+ end
+
private
def run_edit_command(editor: "cat", environment: nil, **options)
switch_env("EDITOR", editor) do
diff --git a/railties/test/commands/initializers_test.rb b/railties/test/commands/initializers_test.rb
index bdfbb3021c..793365ef3d 100644
--- a/railties/test/commands/initializers_test.rb
+++ b/railties/test/commands/initializers_test.rb
@@ -25,8 +25,24 @@ class Rails::Command::InitializersTest < ActiveSupport::TestCase
assert final_output.include?("set_added_test_module")
end
+
+ test "prints out initializers only specified in environment option" do
+ add_to_config <<-RUBY
+ initializer(:set_added_development_module) { } if Rails.env.development?
+ initializer(:set_added_production_module) { } if Rails.env.production?
+ RUBY
+
+ output = run_initializers_command.split("\n")
+ assert_includes output, "AppTemplate::Application.set_added_development_module"
+ assert_not_includes output, "AppTemplate::Application.set_added_production_module"
+
+ output = run_initializers_command(["-e", "production"]).split("\n")
+ assert_not_includes output, "AppTemplate::Application.set_added_development_module"
+ assert_includes output, "AppTemplate::Application.set_added_production_module"
+ end
+
private
- def run_initializers_command
- rails "initializers"
+ def run_initializers_command(args = [])
+ rails "initializers", args
end
end
diff --git a/railties/test/commands/server_test.rb b/railties/test/commands/server_test.rb
index 25b89ecbd8..b78370a233 100644
--- a/railties/test/commands/server_test.rb
+++ b/railties/test/commands/server_test.rb
@@ -22,6 +22,12 @@ class Rails::Command::ServerCommandTest < ActiveSupport::TestCase
assert_nil options[:server]
end
+ def test_environment_option_is_properly_expanded
+ args = ["-e", "prod"]
+ options = parse_arguments(args)
+ assert_equal "production", options[:environment]
+ end
+
def test_explicit_using_option
args = ["-u", "thin"]
options = parse_arguments(args)
@@ -285,6 +291,8 @@ class Rails::Command::ServerCommandTest < ActiveSupport::TestCase
end
def parse_arguments(args = [])
- Rails::Command::ServerCommand.new([], args).server_options
+ command = Rails::Command::ServerCommand.new([], args)
+ command.send(:extract_environment_option_from_argument)
+ command.server_options
end
end
diff --git a/railties/test/isolation/abstract_unit.rb b/railties/test/isolation/abstract_unit.rb
index 14cdf1ab7c..3fcfaa9623 100644
--- a/railties/test/isolation/abstract_unit.rb
+++ b/railties/test/isolation/abstract_unit.rb
@@ -123,6 +123,8 @@ module TestHelpers
adapter: sqlite3
pool: 5
timeout: 5000
+ variables:
+ statement_timeout: 1000
development:
primary:
<<: *default