aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.rubocop.yml5
-rw-r--r--Gemfile.lock4
-rw-r--r--README.md2
-rw-r--r--RELEASING_RAILS.md2
-rw-r--r--actioncable/karma.conf.js6
-rw-r--r--actionmailbox/app/controllers/action_mailbox/base_controller.rb2
-rw-r--r--actionmailbox/lib/action_mailbox/test_helper.rb12
-rw-r--r--actionpack/lib/action_dispatch/journey/path/pattern.rb4
-rw-r--r--actionpack/test/journey/path/pattern_test.rb9
-rw-r--r--activejob/lib/active_job/enqueuing.rb2
-rw-r--r--activemodel/lib/active_model/attribute_mutation_tracker.rb117
-rw-r--r--activemodel/lib/active_model/dirty.rb119
-rw-r--r--activerecord/CHANGELOG.md63
-rw-r--r--activerecord/lib/active_record/associations/association.rb2
-rw-r--r--activerecord/lib/active_record/attribute_methods/dirty.rb14
-rw-r--r--activerecord/lib/active_record/autosave_association.rb8
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb12
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb7
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb1
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb4
-rw-r--r--activerecord/lib/active_record/connection_adapters/column.rb30
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb4
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb6
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb11
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/type_metadata.rb13
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/column.rb45
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb33
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb17
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb5
-rw-r--r--activerecord/lib/active_record/connection_adapters/sql_type_metadata.rb19
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3/database_statements.rb80
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb80
-rw-r--r--activerecord/lib/active_record/insert_all.rb12
-rw-r--r--activerecord/lib/active_record/migration.rb34
-rw-r--r--activerecord/lib/active_record/persistence.rb2
-rw-r--r--activerecord/lib/active_record/reflection.rb4
-rw-r--r--activerecord/lib/active_record/relation.rb6
-rw-r--r--activerecord/lib/active_record/relation/calculations.rb45
-rw-r--r--activerecord/lib/active_record/relation/finder_methods.rb2
-rw-r--r--activerecord/lib/active_record/relation/query_methods.rb9
-rw-r--r--activerecord/lib/active_record/schema_dumper.rb5
-rw-r--r--activerecord/lib/active_record/scoping/named.rb2
-rw-r--r--activerecord/lib/arel/visitors/oracle.rb48
-rw-r--r--activerecord/lib/arel/visitors/oracle12.rb48
-rw-r--r--activerecord/test/cases/adapters/mysql2/count_deleted_rows_with_lock_test.rb28
-rw-r--r--activerecord/test/cases/adapters/postgresql/bytea_test.rb2
-rw-r--r--activerecord/test/cases/adapters/postgresql/datatype_test.rb2
-rw-r--r--activerecord/test/cases/arel/visitors/oracle12_test.rb21
-rw-r--r--activerecord/test/cases/arel/visitors/oracle_test.rb21
-rw-r--r--activerecord/test/cases/associations/eager_test.rb23
-rw-r--r--activerecord/test/cases/associations/has_many_through_associations_test.rb16
-rw-r--r--activerecord/test/cases/calculations_test.rb11
-rw-r--r--activerecord/test/cases/date_time_precision_test.rb2
-rw-r--r--activerecord/test/cases/insert_all_test.rb87
-rw-r--r--activerecord/test/cases/migration/column_attributes_test.rb6
-rw-r--r--activerecord/test/cases/migration_test.rb10
-rw-r--r--activerecord/test/cases/relation_test.rb4
-rw-r--r--activerecord/test/cases/scoping/relation_scoping_test.rb28
-rw-r--r--activerecord/test/cases/time_precision_test.rb2
-rw-r--r--activerecord/test/cases/touch_later_test.rb2
-rw-r--r--activerecord/test/cases/transactions_test.rb85
-rw-r--r--activerecord/test/models/post.rb4
-rw-r--r--activerecord/test/models/section.rb6
-rw-r--r--activerecord/test/models/seminar.rb6
-rw-r--r--activerecord/test/models/session.rb6
-rw-r--r--activerecord/test/schema/schema.rb19
-rw-r--r--activesupport/CHANGELOG.md14
-rw-r--r--activesupport/activesupport.gemspec2
-rw-r--r--activesupport/lib/active_support/dependencies/zeitwerk_integration.rb26
-rw-r--r--activesupport/lib/active_support/descendants_tracker.rb11
-rw-r--r--guides/source/6_0_release_notes.md227
-rw-r--r--guides/source/association_basics.md16
-rw-r--r--guides/source/contributing_to_ruby_on_rails.md6
-rw-r--r--guides/source/debugging_rails_applications.md7
-rw-r--r--guides/source/engines.md61
-rw-r--r--railties/CHANGELOG.md4
-rw-r--r--railties/Rakefile41
-rw-r--r--railties/lib/rails/application/finisher.rb2
-rw-r--r--railties/lib/rails/engine.rb8
-rw-r--r--railties/lib/rails/generators/rails/plugin/plugin_generator.rb15
-rw-r--r--railties/test/application/zeitwerk_integration_test.rb94
-rw-r--r--railties/test/generators/actions_test.rb2
-rw-r--r--railties/test/railties/engine_test.rb15
86 files changed, 1362 insertions, 541 deletions
diff --git a/.rubocop.yml b/.rubocop.yml
index 54dcaf80f9..3dbd4a27a6 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -10,7 +10,7 @@ AllCops:
- 'railties/test/fixtures/tmp/**/*'
- 'actionmailbox/test/dummy/**/*'
- 'actiontext/test/dummy/**/*'
- - 'node_modules/**/*'
+ - '**/node_modules/**/*'
Performance:
Exclude:
@@ -116,6 +116,9 @@ Layout/SpaceAroundOperators:
Layout/SpaceBeforeComma:
Enabled: true
+Layout/SpaceBeforeComment:
+ Enabled: true
+
Layout/SpaceBeforeFirstArg:
Enabled: true
diff --git a/Gemfile.lock b/Gemfile.lock
index 92dcee9542..b92603d799 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -71,7 +71,7 @@ PATH
i18n (>= 0.7, < 2)
minitest (~> 5.1)
tzinfo (~> 1.1)
- zeitwerk (~> 1.4, >= 1.4.3)
+ zeitwerk (~> 2.1, >= 2.1.2)
rails (6.0.0.beta3)
actioncable (= 6.0.0.beta3)
actionmailbox (= 6.0.0.beta3)
@@ -526,7 +526,7 @@ GEM
websocket-extensions (0.1.3)
xpath (3.2.0)
nokogiri (~> 1.8)
- zeitwerk (1.4.3)
+ zeitwerk (2.1.2)
PLATFORMS
java
diff --git a/README.md b/README.md
index 566136e2a2..98ade9b445 100644
--- a/README.md
+++ b/README.md
@@ -97,7 +97,7 @@ Everyone interacting in Rails and its sub-projects' codebases, issue trackers, c
## Code Status
-[![Build Status](https://travis-ci.org/rails/rails.svg?branch=master)](https://travis-ci.org/rails/rails)
+[![Build Status](https://badge.buildkite.com/ab1152b6a1f6a61d3ea4ec5b3eece8d4c2b830998459c75352.svg?branch=master)](https://buildkite.com/rails/rails)
## License
diff --git a/RELEASING_RAILS.md b/RELEASING_RAILS.md
index dd2742b403..d2d7a771bc 100644
--- a/RELEASING_RAILS.md
+++ b/RELEASING_RAILS.md
@@ -14,7 +14,7 @@ Today is mostly coordination tasks. Here are the things you must do today:
Do not release with a Red CI. You can find the CI status here:
```
-https://travis-ci.org/rails/rails
+https://buildkite.com/rails/rails
```
### Is Sam Ruby happy? If not, make him happy.
diff --git a/actioncable/karma.conf.js b/actioncable/karma.conf.js
index 845b38d74f..83e9c98af1 100644
--- a/actioncable/karma.conf.js
+++ b/actioncable/karma.conf.js
@@ -52,9 +52,9 @@ if (process.env.CI) {
}
function buildId() {
- const { TRAVIS_BUILD_NUMBER, TRAVIS_BUILD_ID } = process.env
- return TRAVIS_BUILD_NUMBER && TRAVIS_BUILD_ID
- ? `TRAVIS #${TRAVIS_BUILD_NUMBER} (${TRAVIS_BUILD_ID})`
+ const { BUILDKITE_JOB_ID } = process.env
+ return BUILDKITE_JOB_ID
+ ? `Buildkite ${BUILDKITE_JOB_ID}`
: ""
}
}
diff --git a/actionmailbox/app/controllers/action_mailbox/base_controller.rb b/actionmailbox/app/controllers/action_mailbox/base_controller.rb
index f0f1f555e6..92477b86a8 100644
--- a/actionmailbox/app/controllers/action_mailbox/base_controller.rb
+++ b/actionmailbox/app/controllers/action_mailbox/base_controller.rb
@@ -3,7 +3,7 @@
module ActionMailbox
# The base class for all Action Mailbox ingress controllers.
class BaseController < ActionController::Base
- skip_forgery_protection
+ skip_forgery_protection if default_protect_from_forgery
before_action :ensure_configured
diff --git a/actionmailbox/lib/action_mailbox/test_helper.rb b/actionmailbox/lib/action_mailbox/test_helper.rb
index 0ec9152844..d248fa8734 100644
--- a/actionmailbox/lib/action_mailbox/test_helper.rb
+++ b/actionmailbox/lib/action_mailbox/test_helper.rb
@@ -29,16 +29,16 @@ module ActionMailbox
create_inbound_email_from_fixture(*args).tap(&:route)
end
- # Create an +InboundEmail+ from fixture using the same arguments as +create_inbound_email_from_mail+
- # and immediately route it to processing.
+ # Create an +InboundEmail+ using the same arguments as +create_inbound_email_from_mail+ and immediately route it to
+ # processing.
def receive_inbound_email_from_mail(**kwargs)
create_inbound_email_from_mail(**kwargs).tap(&:route)
end
- # Create an +InboundEmail+ from fixture using the same arguments as +create_inbound_email_from_source+
- # and immediately route it to processing.
- def receive_inbound_email_from_source(**kwargs)
- create_inbound_email_from_source(**kwargs).tap(&:route)
+ # Create an +InboundEmail+ using the same arguments as +create_inbound_email_from_source+ and immediately route it
+ # to processing.
+ def receive_inbound_email_from_source(*args)
+ create_inbound_email_from_source(*args).tap(&:route)
end
end
end
diff --git a/actionpack/lib/action_dispatch/journey/path/pattern.rb b/actionpack/lib/action_dispatch/journey/path/pattern.rb
index 697f5b9d8b..a968df5f19 100644
--- a/actionpack/lib/action_dispatch/journey/path/pattern.rb
+++ b/actionpack/lib/action_dispatch/journey/path/pattern.rb
@@ -136,6 +136,10 @@ module ActionDispatch
Array.new(length - 1) { |i| self[i + 1] }
end
+ def named_captures
+ @names.zip(captures).to_h
+ end
+
def [](x)
idx = @offsets[x - 1] + x
@match[idx]
diff --git a/actionpack/test/journey/path/pattern_test.rb b/actionpack/test/journey/path/pattern_test.rb
index fcfaba96b0..2f39abcb92 100644
--- a/actionpack/test/journey/path/pattern_test.rb
+++ b/actionpack/test/journey/path/pattern_test.rb
@@ -280,6 +280,15 @@ module ActionDispatch
assert_equal "list", match[1]
assert_equal "rss", match[2]
end
+
+ def test_named_captures
+ path = Path::Pattern.from_string "/books(/:action(.:format))"
+
+ uri = "/books/list.rss"
+ match = path =~ uri
+ named_captures = { "action" => "list", "format" => "rss" }
+ assert_equal named_captures, match.named_captures
+ end
end
end
end
diff --git a/activejob/lib/active_job/enqueuing.rb b/activejob/lib/active_job/enqueuing.rb
index 5609d13f5f..c5ee76a69e 100644
--- a/activejob/lib/active_job/enqueuing.rb
+++ b/activejob/lib/active_job/enqueuing.rb
@@ -67,7 +67,7 @@ module ActiveJob
false
else
ActiveSupport::Deprecation.warn(
- "Rails 6.0 will return false when the enqueuing is aborted. Make sure your code doesn't depend on it" \
+ "Rails 6.1 will return false when the enqueuing is aborted. Make sure your code doesn't depend on it" \
" returning the instance of the job and set `config.active_job.return_false_on_aborted_enqueue = true`" \
" to remove the deprecations."
)
diff --git a/activemodel/lib/active_model/attribute_mutation_tracker.rb b/activemodel/lib/active_model/attribute_mutation_tracker.rb
index 6abf37bd44..d8cd48a53b 100644
--- a/activemodel/lib/active_model/attribute_mutation_tracker.rb
+++ b/activemodel/lib/active_model/attribute_mutation_tracker.rb
@@ -1,14 +1,15 @@
# frozen_string_literal: true
require "active_support/core_ext/hash/indifferent_access"
+require "active_support/core_ext/object/duplicable"
module ActiveModel
class AttributeMutationTracker # :nodoc:
OPTION_NOT_GIVEN = Object.new
- def initialize(attributes)
+ def initialize(attributes, forced_changes = Set.new)
@attributes = attributes
- @forced_changes = Set.new
+ @forced_changes = forced_changes
end
def changed_attribute_names
@@ -18,24 +19,22 @@ module ActiveModel
def changed_values
attr_names.each_with_object({}.with_indifferent_access) do |attr_name, result|
if changed?(attr_name)
- result[attr_name] = attributes[attr_name].original_value
+ result[attr_name] = original_value(attr_name)
end
end
end
def changes
attr_names.each_with_object({}.with_indifferent_access) do |attr_name, result|
- change = change_to_attribute(attr_name)
- if change
+ if change = change_to_attribute(attr_name)
result.merge!(attr_name => change)
end
end
end
def change_to_attribute(attr_name)
- attr_name = attr_name.to_s
if changed?(attr_name)
- [attributes[attr_name].original_value, attributes.fetch_value(attr_name)]
+ [original_value(attr_name), fetch_value(attr_name)]
end
end
@@ -44,29 +43,26 @@ module ActiveModel
end
def changed?(attr_name, from: OPTION_NOT_GIVEN, to: OPTION_NOT_GIVEN)
- attr_name = attr_name.to_s
- forced_changes.include?(attr_name) ||
- attributes[attr_name].changed? &&
- (OPTION_NOT_GIVEN == from || attributes[attr_name].original_value == from) &&
- (OPTION_NOT_GIVEN == to || attributes[attr_name].value == to)
+ attribute_changed?(attr_name) &&
+ (OPTION_NOT_GIVEN == from || original_value(attr_name) == from) &&
+ (OPTION_NOT_GIVEN == to || fetch_value(attr_name) == to)
end
def changed_in_place?(attr_name)
- attributes[attr_name.to_s].changed_in_place?
+ attributes[attr_name].changed_in_place?
end
def forget_change(attr_name)
- attr_name = attr_name.to_s
attributes[attr_name] = attributes[attr_name].forgetting_assignment
forced_changes.delete(attr_name)
end
def original_value(attr_name)
- attributes[attr_name.to_s].original_value
+ attributes[attr_name].original_value
end
def force_change(attr_name)
- forced_changes << attr_name.to_s
+ forced_changes << attr_name
end
private
@@ -75,45 +71,108 @@ module ActiveModel
def attr_names
attributes.keys
end
+
+ def attribute_changed?(attr_name)
+ forced_changes.include?(attr_name) || !!attributes[attr_name].changed?
+ end
+
+ def fetch_value(attr_name)
+ attributes.fetch_value(attr_name)
+ end
+ end
+
+ class ForcedMutationTracker < AttributeMutationTracker # :nodoc:
+ def initialize(attributes, forced_changes = {})
+ super
+ @finalized_changes = nil
+ end
+
+ def changed_in_place?(attr_name)
+ false
+ end
+
+ def change_to_attribute(attr_name)
+ if finalized_changes&.include?(attr_name)
+ finalized_changes[attr_name].dup
+ else
+ super
+ end
+ end
+
+ def forget_change(attr_name)
+ forced_changes.delete(attr_name)
+ end
+
+ def original_value(attr_name)
+ if changed?(attr_name)
+ forced_changes[attr_name]
+ else
+ fetch_value(attr_name)
+ end
+ end
+
+ def force_change(attr_name)
+ forced_changes[attr_name] = clone_value(attr_name) unless attribute_changed?(attr_name)
+ end
+
+ def finalize_changes
+ @finalized_changes = changes
+ end
+
+ private
+ attr_reader :finalized_changes
+
+ def attr_names
+ forced_changes.keys
+ end
+
+ def attribute_changed?(attr_name)
+ forced_changes.include?(attr_name)
+ end
+
+ def fetch_value(attr_name)
+ attributes.send(:_read_attribute, attr_name)
+ end
+
+ def clone_value(attr_name)
+ value = fetch_value(attr_name)
+ value.duplicable? ? value.clone : value
+ rescue TypeError, NoMethodError
+ value
+ end
end
class NullMutationTracker # :nodoc:
include Singleton
- def changed_attribute_names(*)
+ def changed_attribute_names
[]
end
- def changed_values(*)
+ def changed_values
{}
end
- def changes(*)
+ def changes
{}
end
def change_to_attribute(attr_name)
end
- def any_changes?(*)
+ def any_changes?
false
end
- def changed?(*)
+ def changed?(attr_name, **)
false
end
- def changed_in_place?(*)
+ def changed_in_place?(attr_name)
false
end
- def forget_change(*)
- end
-
- def original_value(*)
- end
-
- def force_change(*)
+ def original_value(attr_name)
end
end
end
diff --git a/activemodel/lib/active_model/dirty.rb b/activemodel/lib/active_model/dirty.rb
index 0d9e761b1e..ab2c9d04ae 100644
--- a/activemodel/lib/active_model/dirty.rb
+++ b/activemodel/lib/active_model/dirty.rb
@@ -1,7 +1,5 @@
# frozen_string_literal: true
-require "active_support/hash_with_indifferent_access"
-require "active_support/core_ext/object/duplicable"
require "active_model/attribute_mutation_tracker"
module ActiveModel
@@ -122,9 +120,6 @@ module ActiveModel
extend ActiveSupport::Concern
include ActiveModel::AttributeMethods
- OPTION_NOT_GIVEN = Object.new # :nodoc:
- private_constant :OPTION_NOT_GIVEN
-
included do
attribute_method_suffix "_changed?", "_change", "_will_change!", "_was"
attribute_method_suffix "_previously_changed?", "_previous_change"
@@ -145,10 +140,9 @@ module ActiveModel
# +mutations_from_database+ to +mutations_before_last_save+ respectively.
def changes_applied
unless defined?(@attributes)
- @previously_changed = changes
+ mutations_from_database.finalize_changes
end
@mutations_before_last_save = mutations_from_database
- @attributes_changed_by_setter = ActiveSupport::HashWithIndifferentAccess.new
forget_attribute_assignments
@mutations_from_database = nil
end
@@ -159,7 +153,7 @@ module ActiveModel
# person.name = 'bob'
# person.changed? # => true
def changed?
- changed_attributes.present?
+ mutations_from_database.any_changes?
end
# Returns an array with the name of the attributes with unsaved changes.
@@ -168,42 +162,37 @@ module ActiveModel
# person.name = 'bob'
# person.changed # => ["name"]
def changed
- changed_attributes.keys
+ mutations_from_database.changed_attribute_names
end
# Handles <tt>*_changed?</tt> for +method_missing+.
- def attribute_changed?(attr, from: OPTION_NOT_GIVEN, to: OPTION_NOT_GIVEN) # :nodoc:
- !!changes_include?(attr) &&
- (to == OPTION_NOT_GIVEN || to == _read_attribute(attr)) &&
- (from == OPTION_NOT_GIVEN || from == changed_attributes[attr])
+ def attribute_changed?(attr_name, **options) # :nodoc:
+ mutations_from_database.changed?(attr_name.to_s, options)
end
# Handles <tt>*_was</tt> for +method_missing+.
- def attribute_was(attr) # :nodoc:
- attribute_changed?(attr) ? changed_attributes[attr] : _read_attribute(attr)
+ def attribute_was(attr_name) # :nodoc:
+ mutations_from_database.original_value(attr_name.to_s)
end
# Handles <tt>*_previously_changed?</tt> for +method_missing+.
- def attribute_previously_changed?(attr) #:nodoc:
- previous_changes_include?(attr)
+ def attribute_previously_changed?(attr_name) # :nodoc:
+ mutations_before_last_save.changed?(attr_name.to_s)
end
# Restore all previous data of the provided attributes.
- def restore_attributes(attributes = changed)
- attributes.each { |attr| restore_attribute! attr }
+ def restore_attributes(attr_names = changed)
+ attr_names.each { |attr_name| restore_attribute!(attr_name) }
end
# Clears all dirty data: current changes and previous changes.
def clear_changes_information
- @previously_changed = ActiveSupport::HashWithIndifferentAccess.new
@mutations_before_last_save = nil
- @attributes_changed_by_setter = ActiveSupport::HashWithIndifferentAccess.new
forget_attribute_assignments
@mutations_from_database = nil
end
def clear_attribute_changes(attr_names)
- attributes_changed_by_setter.except!(*attr_names)
attr_names.each do |attr_name|
clear_attribute_change(attr_name)
end
@@ -216,13 +205,7 @@ module ActiveModel
# person.name = 'robert'
# person.changed_attributes # => {"name" => "bob"}
def changed_attributes
- # This should only be set by methods which will call changed_attributes
- # multiple times when it is known that the computed value cannot change.
- if defined?(@cached_changed_attributes)
- @cached_changed_attributes
- else
- attributes_changed_by_setter.reverse_merge(mutations_from_database.changed_values).freeze
- end
+ mutations_from_database.changed_values
end
# Returns a hash of changed attributes indicating their original
@@ -232,9 +215,7 @@ module ActiveModel
# person.name = 'bob'
# person.changes # => { "name" => ["bill", "bob"] }
def changes
- cache_changed_attributes do
- ActiveSupport::HashWithIndifferentAccess[changed.map { |attr| [attr, attribute_change(attr)] }]
- end
+ mutations_from_database.changes
end
# Returns a hash of attributes that were changed before the model was saved.
@@ -244,27 +225,23 @@ module ActiveModel
# person.save
# person.previous_changes # => {"name" => ["bob", "robert"]}
def previous_changes
- @previously_changed ||= ActiveSupport::HashWithIndifferentAccess.new
- @previously_changed.merge(mutations_before_last_save.changes)
+ mutations_before_last_save.changes
end
def attribute_changed_in_place?(attr_name) # :nodoc:
- mutations_from_database.changed_in_place?(attr_name)
+ mutations_from_database.changed_in_place?(attr_name.to_s)
end
private
def clear_attribute_change(attr_name)
- mutations_from_database.forget_change(attr_name)
+ mutations_from_database.forget_change(attr_name.to_s)
end
def mutations_from_database
- unless defined?(@mutations_from_database)
- @mutations_from_database = nil
- end
@mutations_from_database ||= if defined?(@attributes)
ActiveModel::AttributeMutationTracker.new(@attributes)
else
- NullMutationTracker.instance
+ ActiveModel::ForcedMutationTracker.new(self)
end
end
@@ -276,68 +253,28 @@ module ActiveModel
@mutations_before_last_save ||= ActiveModel::NullMutationTracker.instance
end
- def cache_changed_attributes
- @cached_changed_attributes = changed_attributes
- yield
- ensure
- clear_changed_attributes_cache
- end
-
- def clear_changed_attributes_cache
- remove_instance_variable(:@cached_changed_attributes) if defined?(@cached_changed_attributes)
- end
-
- # Returns +true+ if attr_name is changed, +false+ otherwise.
- def changes_include?(attr_name)
- attributes_changed_by_setter.include?(attr_name) || mutations_from_database.changed?(attr_name)
- end
- alias attribute_changed_by_setter? changes_include?
-
- # Returns +true+ if attr_name were changed before the model was saved,
- # +false+ otherwise.
- def previous_changes_include?(attr_name)
- previous_changes.include?(attr_name)
- end
-
# Handles <tt>*_change</tt> for +method_missing+.
- def attribute_change(attr)
- [changed_attributes[attr], _read_attribute(attr)] if attribute_changed?(attr)
+ def attribute_change(attr_name)
+ mutations_from_database.change_to_attribute(attr_name.to_s)
end
# Handles <tt>*_previous_change</tt> for +method_missing+.
- def attribute_previous_change(attr)
- previous_changes[attr]
+ def attribute_previous_change(attr_name)
+ mutations_before_last_save.change_to_attribute(attr_name.to_s)
end
# Handles <tt>*_will_change!</tt> for +method_missing+.
- def attribute_will_change!(attr)
- unless attribute_changed?(attr)
- begin
- value = _read_attribute(attr)
- value = value.duplicable? ? value.clone : value
- rescue TypeError, NoMethodError
- end
-
- set_attribute_was(attr, value)
- end
- mutations_from_database.force_change(attr)
+ def attribute_will_change!(attr_name)
+ mutations_from_database.force_change(attr_name.to_s)
end
# Handles <tt>restore_*!</tt> for +method_missing+.
- def restore_attribute!(attr)
- if attribute_changed?(attr)
- __send__("#{attr}=", changed_attributes[attr])
- clear_attribute_changes([attr])
+ def restore_attribute!(attr_name)
+ attr_name = attr_name.to_s
+ if attribute_changed?(attr_name)
+ __send__("#{attr_name}=", attribute_was(attr_name))
+ clear_attribute_change(attr_name)
end
end
-
- def attributes_changed_by_setter
- @attributes_changed_by_setter ||= ActiveSupport::HashWithIndifferentAccess.new
- end
-
- # Force an attribute to have a particular "before" value
- def set_attribute_was(attr, old_value)
- attributes_changed_by_setter[attr] = old_value
- end
end
end
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md
index f0396c3d53..1cf3f3352f 100644
--- a/activerecord/CHANGELOG.md
+++ b/activerecord/CHANGELOG.md
@@ -1,3 +1,66 @@
+* Fix circular `autosave: true` causes invalid records to be saved.
+
+ Prior to the fix, when there was a circular series of `autosave: true`
+ associations, the callback for a `has_many` association was run while
+ another instance of the same callback on the same association hadn't
+ finished running. When control returned to the first instance of the
+ callback, the instance variable had changed, and subsequent associated
+ records weren't saved correctly. Specifically, the ID field for the
+ `belongs_to` corresponding to the `has_many` was `nil`.
+
+ Fixes #28080.
+
+ *Larry Reid*
+
+* Raise `ArgumentError` for invalid `:limit` and `:precision` like as other options.
+
+ Before:
+
+ ```ruby
+ add_column :items, :attr1, :binary, size: 10 # => ArgumentError
+ add_column :items, :attr2, :decimal, scale: 10 # => ArgumentError
+ add_column :items, :attr3, :integer, limit: 10 # => ActiveRecordError
+ add_column :items, :attr4, :datetime, precision: 10 # => ActiveRecordError
+ ```
+
+ After:
+
+ ```ruby
+ add_column :items, :attr1, :binary, size: 10 # => ArgumentError
+ add_column :items, :attr2, :decimal, scale: 10 # => ArgumentError
+ add_column :items, :attr3, :integer, limit: 10 # => ArgumentError
+ add_column :items, :attr4, :datetime, precision: 10 # => ArgumentError
+ ```
+
+ *Ryuta Kamizono*
+
+* Association loading isn't to be affected by scoping consistently
+ whether preloaded / eager loaded or not, with the exception of `unscoped`.
+
+ Before:
+
+ ```ruby
+ Post.where("1=0").scoping do
+ Comment.find(1).post # => nil
+ Comment.preload(:post).find(1).post # => #<Post id: 1, ...>
+ Comment.eager_load(:post).find(1).post # => #<Post id: 1, ...>
+ end
+ ```
+
+ After:
+
+ ```ruby
+ Post.where("1=0").scoping do
+ Comment.find(1).post # => #<Post id: 1, ...>
+ Comment.preload(:post).find(1).post # => #<Post id: 1, ...>
+ Comment.eager_load(:post).find(1).post # => #<Post id: 1, ...>
+ end
+ ```
+
+ Fixes #34638, #35398.
+
+ *Ryuta Kamizono*
+
* Add `rails db:prepare` to migrate or setup a database.
Runs `db:migrate` if the database exists or `db:setup` if it doesn't.
diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb
index 0bb63b97ae..cf22b850b9 100644
--- a/activerecord/lib/active_record/associations/association.rb
+++ b/activerecord/lib/active_record/associations/association.rb
@@ -225,7 +225,7 @@ module ActiveRecord
# Can be overridden (i.e. in ThroughAssociation) to merge in other scopes (i.e. the
# through association's scope)
def target_scope
- AssociationRelation.create(klass, self).merge!(klass.all)
+ AssociationRelation.create(klass, self).merge!(klass.scope_for_association)
end
def scope_for_create
diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb
index 45e4b8adfa..920a7b2038 100644
--- a/activerecord/lib/active_record/attribute_methods/dirty.rb
+++ b/activerecord/lib/active_record/attribute_methods/dirty.rb
@@ -29,9 +29,7 @@ module ActiveRecord
# <tt>reload</tt> the record and clears changed attributes.
def reload(*)
super.tap do
- @previously_changed = ActiveSupport::HashWithIndifferentAccess.new
@mutations_before_last_save = nil
- @attributes_changed_by_setter = ActiveSupport::HashWithIndifferentAccess.new
@mutations_from_database = nil
end
end
@@ -51,7 +49,7 @@ module ActiveRecord
# +to+ When passed, this method will return false unless the value was
# changed to the given value
def saved_change_to_attribute?(attr_name, **options)
- mutations_before_last_save.changed?(attr_name, **options)
+ mutations_before_last_save.changed?(attr_name.to_s, options)
end
# Returns the change to an attribute during the last save. If the
@@ -63,7 +61,7 @@ module ActiveRecord
# invoked as +saved_change_to_name+ instead of
# <tt>saved_change_to_attribute("name")</tt>.
def saved_change_to_attribute(attr_name)
- mutations_before_last_save.change_to_attribute(attr_name)
+ mutations_before_last_save.change_to_attribute(attr_name.to_s)
end
# Returns the original value of an attribute before the last save.
@@ -73,7 +71,7 @@ module ActiveRecord
# invoked as +name_before_last_save+ instead of
# <tt>attribute_before_last_save("name")</tt>.
def attribute_before_last_save(attr_name)
- mutations_before_last_save.original_value(attr_name)
+ mutations_before_last_save.original_value(attr_name.to_s)
end
# Did the last call to +save+ have any changes to change?
@@ -101,7 +99,7 @@ module ActiveRecord
# +to+ When passed, this method will return false unless the value will be
# changed to the given value
def will_save_change_to_attribute?(attr_name, **options)
- mutations_from_database.changed?(attr_name, **options)
+ mutations_from_database.changed?(attr_name.to_s, options)
end
# Returns the change to an attribute that will be persisted during the
@@ -115,7 +113,7 @@ module ActiveRecord
# If the attribute will change, the result will be an array containing the
# original value and the new value about to be saved.
def attribute_change_to_be_saved(attr_name)
- mutations_from_database.change_to_attribute(attr_name)
+ mutations_from_database.change_to_attribute(attr_name.to_s)
end
# Returns the value of an attribute in the database, as opposed to the
@@ -127,7 +125,7 @@ module ActiveRecord
# saved. It can be invoked as +name_in_database+ instead of
# <tt>attribute_in_database("name")</tt>.
def attribute_in_database(attr_name)
- mutations_from_database.original_value(attr_name)
+ mutations_from_database.original_value(attr_name.to_s)
end
# Will the next call to +save+ have any changes to persist?
diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb
index fe94662543..50f29a81a6 100644
--- a/activerecord/lib/active_record/autosave_association.rb
+++ b/activerecord/lib/active_record/autosave_association.rb
@@ -382,10 +382,14 @@ module ActiveRecord
if association = association_instance_get(reflection.name)
autosave = reflection.options[:autosave]
+ # By saving the instance variable in a local variable,
+ # we make the whole callback re-entrant.
+ new_record_before_save = @new_record_before_save
+
# reconstruct the scope now that we know the owner's id
association.reset_scope
- if records = associated_records_to_validate_or_save(association, @new_record_before_save, autosave)
+ if records = associated_records_to_validate_or_save(association, new_record_before_save, autosave)
if autosave
records_to_destroy = records.select(&:marked_for_destruction?)
records_to_destroy.each { |record| association.destroy(record) }
@@ -397,7 +401,7 @@ module ActiveRecord
saved = true
- if autosave != false && (@new_record_before_save || record.new_record?)
+ if autosave != false && (new_record_before_save || record.new_record?)
if autosave
saved = association.insert_record(record, false)
elsif !reflection.nested?
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb
index 1305216be2..75e959045e 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb
@@ -5,20 +5,24 @@ require "active_support/deprecation"
module ActiveRecord
module ConnectionAdapters # :nodoc:
module DatabaseLimits
+ def max_identifier_length # :nodoc:
+ 64
+ end
+
# Returns the maximum length of a table alias.
def table_alias_length
- 255
+ max_identifier_length
end
# Returns the maximum length of a column name.
def column_name_length
- 64
+ max_identifier_length
end
deprecate :column_name_length
# Returns the maximum length of a table name.
def table_name_length
- 64
+ max_identifier_length
end
deprecate :table_name_length
@@ -33,7 +37,7 @@ module ActiveRecord
# Returns the maximum length of an index name.
def index_name_length
- 64
+ max_identifier_length
end
# Returns the maximum number of columns per table.
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 6aacbe5f88..ef19538447 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
@@ -131,7 +131,7 @@ module ActiveRecord
# +binds+ as the bind substitutes. +name+ is logged along with
# the executed +sql+ statement.
def exec_insert(sql, name = nil, binds = [], pk = nil, sequence_name = nil)
- sql, binds = sql_for_insert(sql, pk, sequence_name, binds)
+ sql, binds = sql_for_insert(sql, pk, binds)
exec_query(sql, name, binds)
end
@@ -427,8 +427,7 @@ module ActiveRecord
columns.map do |name, column|
if fixture.key?(name)
type = lookup_cast_type_from_column(column)
- bind = Relation::QueryAttribute.new(name, fixture[name], type)
- with_yaml_fallback(bind.value_for_database)
+ with_yaml_fallback(type.serialize(fixture[name]))
else
default_insert_value(column)
end
@@ -488,7 +487,7 @@ module ActiveRecord
exec_query(sql, name, binds, prepare: true)
end
- def sql_for_insert(sql, pk, sequence_name, binds)
+ def sql_for_insert(sql, pk, binds)
[sql, binds]
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
index 4861872129..688eea75e8 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
@@ -416,6 +416,7 @@ module ActiveRecord
#
# t.references(:user)
# t.belongs_to(:supplier, foreign_key: true)
+ # t.belongs_to(:supplier, foreign_key: true, type: :integer)
#
# See {connection.add_reference}[rdoc-ref:SchemaStatements#add_reference] for details of the options you can use.
def references(*args, **options)
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
index 9f0335b31d..4840307094 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
@@ -1097,7 +1097,7 @@ module ActiveRecord
if (0..6) === precision
column_type_sql << "(#{precision})"
else
- raise(ActiveRecordError, "No #{native[:name]} type has precision of #{precision}. The allowed range of precision is from 0 to 6")
+ raise ArgumentError, "No #{native[:name]} type has precision of #{precision}. The allowed range of precision is from 0 to 6"
end
elsif (type != :primary_key) && (limit ||= native.is_a?(Hash) && native[:limit])
column_type_sql << "(#{limit})"
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 f5da19f0f6..ca8bbc14da 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
@@ -516,8 +516,8 @@ module ActiveRecord
sql = +"INSERT #{insert.into} #{insert.values_list}"
if insert.skip_duplicates?
- any_column = quote_column_name(insert.model.columns.first.name)
- sql << " ON DUPLICATE KEY UPDATE #{any_column}=#{any_column}"
+ no_op_column = quote_column_name(insert.keys.first)
+ sql << " ON DUPLICATE KEY UPDATE #{no_op_column}=#{no_op_column}"
elsif insert.update_duplicates?
sql << " ON DUPLICATE KEY UPDATE "
sql << insert.updatable_columns.map { |column| "#{column}=VALUES(#{column})" }.join(",")
diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb
index 5d81de9fe1..279d0b9e84 100644
--- a/activerecord/lib/active_record/connection_adapters/column.rb
+++ b/activerecord/lib/active_record/connection_adapters/column.rb
@@ -5,7 +5,7 @@ module ActiveRecord
module ConnectionAdapters
# An abstract definition of a column in a table.
class Column
- attr_reader :name, :default, :sql_type_metadata, :null, :table_name, :default_function, :collation, :comment
+ attr_reader :name, :default, :sql_type_metadata, :null, :default_function, :collation, :comment
delegate :precision, :scale, :limit, :type, :sql_type, to: :sql_type_metadata, allow_nil: true
@@ -15,9 +15,8 @@ module ActiveRecord
# +default+ is the type-casted default value, such as +new+ in <tt>sales_stage varchar(20) default 'new'</tt>.
# +sql_type_metadata+ is various information about the type of the column
# +null+ determines if this column allows +NULL+ values.
- def initialize(name, default, sql_type_metadata = nil, null = true, table_name = nil, default_function = nil, collation = nil, comment: nil, **)
+ def initialize(name, default, sql_type_metadata = nil, null = true, default_function = nil, collation: nil, comment: nil, **)
@name = name.freeze
- @table_name = table_name
@sql_type_metadata = sql_type_metadata
@null = null
@default = default
@@ -44,7 +43,6 @@ module ActiveRecord
def init_with(coder)
@name = coder["name"]
- @table_name = coder["table_name"]
@sql_type_metadata = coder["sql_type_metadata"]
@null = coder["null"]
@default = coder["default"]
@@ -55,7 +53,6 @@ module ActiveRecord
def encode_with(coder)
coder["name"] = @name
- coder["table_name"] = @table_name
coder["sql_type_metadata"] = @sql_type_metadata
coder["null"] = @null
coder["default"] = @default
@@ -66,19 +63,26 @@ module ActiveRecord
def ==(other)
other.is_a?(Column) &&
- attributes_for_hash == other.attributes_for_hash
+ name == other.name &&
+ default == other.default &&
+ sql_type_metadata == other.sql_type_metadata &&
+ null == other.null &&
+ default_function == other.default_function &&
+ collation == other.collation &&
+ comment == other.comment
end
alias :eql? :==
def hash
- attributes_for_hash.hash
+ Column.hash ^
+ name.hash ^
+ default.hash ^
+ sql_type_metadata.hash ^
+ null.hash ^
+ default_function.hash ^
+ collation.hash ^
+ comment.hash
end
-
- protected
-
- def attributes_for_hash
- [self.class, name, default, sql_type_metadata, null, table_name, default_function, collation]
- end
end
class NullColumn < Column
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 a14394fe04..2132e5d248 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb
@@ -61,7 +61,9 @@ module ActiveRecord
def exec_delete(sql, name = nil, binds = [])
if without_prepared_statement?(binds)
- execute_and_free(sql, name) { @connection.affected_rows }
+ @lock.synchronize do
+ execute_and_free(sql, name) { @connection.affected_rows }
+ end
else
exec_stmt_and_free(sql, name, binds) { |stmt| stmt.affected_rows }
end
diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb
index a37557361a..234fb25fdf 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb
@@ -55,7 +55,7 @@ module ActiveRecord
end
def schema_collation(column)
- if column.collation && table_name = column.table_name
+ if column.collation
@table_collation_cache ||= {}
@table_collation_cache[table_name] ||=
@connection.exec_query("SHOW TABLE STATUS LIKE #{@connection.quote(table_name)}", "SCHEMA").first["Collation"]
@@ -65,13 +65,13 @@ module ActiveRecord
def extract_expression_for_virtual_column(column)
if @connection.mariadb? && @connection.database_version < "10.2.5"
- create_table_info = @connection.send(:create_table_info, column.table_name)
+ create_table_info = @connection.send(:create_table_info, table_name)
column_name = @connection.quote_column_name(column.name)
if %r/#{column_name} #{Regexp.quote(column.sql_type)}(?: COLLATE \w+)? AS \((?<expression>.+?)\) #{column.extra}/ =~ create_table_info
$~[:expression].inspect
end
else
- scope = @connection.send(:quoted_scope, column.table_name)
+ scope = @connection.send(:quoted_scope, table_name)
column_name = @connection.quote(column.name)
sql = "SELECT generation_expression FROM information_schema.columns" \
" WHERE table_schema = #{scope[:schema]}" \
diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb
index a1f90a0642..25a1fb234a 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb
@@ -121,6 +121,10 @@ module ActiveRecord
sql
end
+ def table_alias_length
+ 256 # https://dev.mysql.com/doc/refman/8.0/en/identifiers.html
+ end
+
private
CHARSETS_OF_4BYTES_MAXLEN = ["utf8mb4", "utf16", "utf16le", "utf32"]
@@ -170,9 +174,8 @@ module ActiveRecord
default,
type_metadata,
field[:Null] == "YES",
- table_name,
default_function,
- field[:Collation],
+ collation: field[:Collation],
comment: field[:Comment].presence
)
end
@@ -240,7 +243,7 @@ module ActiveRecord
when nil, 0x100..0xffff; nil
when 0x10000..0xffffff; "medium"
when 0x1000000..0xffffffff; "long"
- else raise ActiveRecordError, "No #{type} type has byte size #{limit}"
+ else raise ArgumentError, "No #{type} type has byte size #{limit}"
end
end
end
@@ -252,7 +255,7 @@ module ActiveRecord
when 3; "mediumint"
when nil, 4; "int"
when 5..8; "bigint"
- else raise ActiveRecordError, "No integer type has byte size #{limit}. Use a decimal with scale 0 instead."
+ else raise ArgumentError, "No integer type has byte size #{limit}. Use a decimal with scale 0 instead."
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/mysql/type_metadata.rb b/activerecord/lib/active_record/connection_adapters/mysql/type_metadata.rb
index 7ad0944d51..56479f27bf 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql/type_metadata.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql/type_metadata.rb
@@ -16,19 +16,16 @@ module ActiveRecord
def ==(other)
other.is_a?(MySQL::TypeMetadata) &&
- attributes_for_hash == other.attributes_for_hash
+ __getobj__ == other.__getobj__ &&
+ extra == other.extra
end
alias eql? ==
def hash
- attributes_for_hash.hash
+ TypeMetadata.hash ^
+ __getobj__.hash ^
+ extra.hash
end
-
- protected
-
- def attributes_for_hash
- [self.class, @type_metadata, extra]
- end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb
index 3ccc7271ab..ef98d2b37a 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb
@@ -2,42 +2,21 @@
module ActiveRecord
module ConnectionAdapters
- # PostgreSQL-specific extensions to column definitions in a table.
- class PostgreSQLColumn < Column #:nodoc:
- delegate :array, :oid, :fmod, to: :sql_type_metadata
- alias :array? :array
-
- def initialize(*, max_identifier_length: 63, **)
- super
- @max_identifier_length = max_identifier_length
- end
-
- def serial?
- return unless default_function
-
- if %r{\Anextval\('"?(?<sequence_name>.+_(?<suffix>seq\d*))"?'::regclass\)\z} =~ default_function
- sequence_name_from_parts(table_name, name, suffix) == sequence_name
+ module PostgreSQL
+ class Column < ConnectionAdapters::Column # :nodoc:
+ delegate :array, :oid, :fmod, to: :sql_type_metadata
+ alias :array? :array
+
+ def initialize(*, serial: nil, **)
+ super
+ @serial = serial
end
- end
- private
- attr_reader :max_identifier_length
-
- def sequence_name_from_parts(table_name, column_name, suffix)
- over_length = [table_name, column_name, suffix].map(&:length).sum + 2 - max_identifier_length
-
- if over_length > 0
- column_name_length = [(max_identifier_length - suffix.length - 2) / 2, column_name.length].min
- over_length -= column_name.length - column_name_length
- column_name = column_name[0, column_name_length - [over_length, 0].min]
- end
-
- if over_length > 0
- table_name = table_name[0, table_name.length - over_length]
- end
-
- "#{table_name}_#{column_name}_#{suffix}"
+ def serial?
+ @serial
end
+ end
end
+ PostgreSQLColumn = PostgreSQL::Column # :nodoc:
end
end
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 ae7dbd2868..d872bd662f 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb
@@ -110,7 +110,7 @@ module ActiveRecord
end
alias :exec_update :exec_delete
- def sql_for_insert(sql, pk, sequence_name, binds) # :nodoc:
+ def sql_for_insert(sql, pk, binds) # :nodoc:
if pk.nil?
# Extract the table from the insert sql. Yuck.
table_ref = extract_table_ref_from_insert_sql(sql)
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
index 41d1a8e4ab..c412d1f34c 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
@@ -548,21 +548,21 @@ module ActiveRecord
# The hard limit is 1GB, because of a 32-bit size field, and TOAST.
case limit
when nil, 0..0x3fffffff; super(type)
- else raise ActiveRecordError, "No binary type has byte size #{limit}. The limit on binary can be at most 1GB - 1byte."
+ else raise ArgumentError, "No binary type has byte size #{limit}. The limit on binary can be at most 1GB - 1byte."
end
when "text"
# PostgreSQL doesn't support limits on text columns.
# The hard limit is 1GB, according to section 8.3 in the manual.
case limit
when nil, 0..0x3fffffff; super(type)
- else raise ActiveRecordError, "No text type has byte size #{limit}. The limit on text can be at most 1GB - 1byte."
+ else raise ArgumentError, "No text type has byte size #{limit}. The limit on text can be at most 1GB - 1byte."
end
when "integer"
case limit
when 1, 2; "smallint"
when nil, 3, 4; "integer"
when 5..8; "bigint"
- else raise(ActiveRecordError, "No integer type has byte size #{limit}. Use a numeric with scale 0 instead.")
+ else raise ArgumentError, "No integer type has byte size #{limit}. Use a numeric with scale 0 instead."
end
else
super
@@ -650,16 +650,19 @@ module ActiveRecord
default_value = extract_value_from_default(default)
default_function = extract_default_function(default_value, default)
- PostgreSQLColumn.new(
+ if match = default_function&.match(/\Anextval\('"?(?<sequence_name>.+_(?<suffix>seq\d*))"?'::regclass\)\z/)
+ serial = sequence_name_from_parts(table_name, column_name, match[:suffix]) == match[:sequence_name]
+ end
+
+ PostgreSQL::Column.new(
column_name,
default_value,
type_metadata,
!notnull,
- table_name,
default_function,
- collation,
+ collation: collation,
comment: comment.presence,
- max_identifier_length: max_identifier_length
+ serial: serial
)
end
@@ -675,6 +678,22 @@ module ActiveRecord
PostgreSQLTypeMetadata.new(simple_type, oid: oid, fmod: fmod)
end
+ def sequence_name_from_parts(table_name, column_name, suffix)
+ over_length = [table_name, column_name, suffix].sum(&:length) + 2 - max_identifier_length
+
+ if over_length > 0
+ column_name_length = [(max_identifier_length - suffix.length - 2) / 2, column_name.length].min
+ over_length -= column_name.length - column_name_length
+ column_name = column_name[0, column_name_length - [over_length, 0].min]
+ end
+
+ if over_length > 0
+ table_name = table_name[0, table_name.length - over_length]
+ end
+
+ "#{table_name}_#{column_name}_#{suffix}"
+ end
+
def extract_foreign_key_action(specifier)
case specifier
when "c"; :cascade
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb b/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb
index cd69d28139..403b3ead98 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb
@@ -22,19 +22,20 @@ module ActiveRecord
def ==(other)
other.is_a?(PostgreSQLTypeMetadata) &&
- attributes_for_hash == other.attributes_for_hash
+ __getobj__ == other.__getobj__ &&
+ oid == other.oid &&
+ fmod == other.fmod &&
+ array == other.array
end
alias eql? ==
def hash
- attributes_for_hash.hash
+ PostgreSQLTypeMetadata.hash ^
+ __getobj__.hash ^
+ oid.hash ^
+ fmod.hash ^
+ array.hash
end
-
- protected
-
- def attributes_for_hash
- [self.class, @type_metadata, oid, fmod]
- end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
index 0ed4e61d18..91318a0af1 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
@@ -400,8 +400,6 @@ module ActiveRecord
def max_identifier_length
@max_identifier_length ||= query_value("SHOW max_identifier_length", "SCHEMA").to_i
end
- alias table_alias_length max_identifier_length
- alias index_name_length max_identifier_length
# Set the authorized user for this session
def session_auth=(user)
@@ -424,9 +422,10 @@ module ActiveRecord
}
# Returns the version of the connected PostgreSQL server.
- def get_database_version
+ def get_database_version # :nodoc:
@connection.server_version
end
+ alias :postgresql_version :database_version
def default_index_type?(index) # :nodoc:
index.using == :btree || super
diff --git a/activerecord/lib/active_record/connection_adapters/sql_type_metadata.rb b/activerecord/lib/active_record/connection_adapters/sql_type_metadata.rb
index 8489bcbf1d..df28df7a7c 100644
--- a/activerecord/lib/active_record/connection_adapters/sql_type_metadata.rb
+++ b/activerecord/lib/active_record/connection_adapters/sql_type_metadata.rb
@@ -16,19 +16,22 @@ module ActiveRecord
def ==(other)
other.is_a?(SqlTypeMetadata) &&
- attributes_for_hash == other.attributes_for_hash
+ sql_type == other.sql_type &&
+ type == other.type &&
+ limit == other.limit &&
+ precision == other.precision &&
+ scale == other.scale
end
alias eql? ==
def hash
- attributes_for_hash.hash
+ SqlTypeMetadata.hash ^
+ sql_type.hash ^
+ type.hash ^
+ limit.hash ^
+ precision.hash >> 1 ^
+ scale.hash >> 2
end
-
- protected
-
- def attributes_for_hash
- [self.class, sql_type, type, limit, precision, scale]
- end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/database_statements.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/database_statements.rb
index ffa75172b5..46ce1a15b5 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3/database_statements.rb
@@ -4,6 +4,82 @@ module ActiveRecord
module ConnectionAdapters
module SQLite3
module DatabaseStatements
+ READ_QUERY = ActiveRecord::ConnectionAdapters::AbstractAdapter.build_read_query_regexp(:begin, :commit, :explain, :select, :pragma, :release, :savepoint, :rollback) # :nodoc:
+ private_constant :READ_QUERY
+
+ def write_query?(sql) # :nodoc:
+ !READ_QUERY.match?(sql)
+ end
+
+ def execute(sql, name = nil) #:nodoc:
+ 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(sql)
+ end
+ end
+ end
+
+ def exec_query(sql, name = nil, binds = [], prepare: false)
+ if preventing_writes? && write_query?(sql)
+ raise ActiveRecord::ReadOnlyError, "Write query attempted while in readonly mode: #{sql}"
+ end
+
+ materialize_transactions
+
+ type_casted_binds = type_casted_binds(binds)
+
+ log(sql, name, binds, type_casted_binds) do
+ ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
+ # Don't cache statements if they are not prepared
+ unless prepare
+ stmt = @connection.prepare(sql)
+ begin
+ cols = stmt.columns
+ unless without_prepared_statement?(binds)
+ stmt.bind_params(type_casted_binds)
+ end
+ records = stmt.to_a
+ ensure
+ stmt.close
+ end
+ else
+ stmt = @statements[sql] ||= @connection.prepare(sql)
+ cols = stmt.columns
+ stmt.reset!
+ stmt.bind_params(type_casted_binds)
+ records = stmt.to_a
+ end
+
+ ActiveRecord::Result.new(cols, records)
+ end
+ end
+ end
+
+ def exec_delete(sql, name = "SQL", binds = [])
+ exec_query(sql, name, binds)
+ @connection.changes
+ end
+ alias :exec_update :exec_delete
+
+ def begin_db_transaction #:nodoc:
+ log("begin transaction", nil) { @connection.transaction }
+ end
+
+ def commit_db_transaction #:nodoc:
+ log("commit transaction", nil) { @connection.commit }
+ end
+
+ def exec_rollback_db_transaction #:nodoc:
+ log("rollback transaction", nil) { @connection.rollback }
+ end
+
+
private
def execute_batch(sql, name = nil)
if preventing_writes? && write_query?(sql)
@@ -19,6 +95,10 @@ module ActiveRecord
end
end
+ def last_inserted_id(result)
+ @connection.last_insert_row_id
+ end
+
def build_fixture_statements(fixture_set)
fixture_set.flat_map do |table_name, fixtures|
next if fixtures.empty?
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb
index e64e995e1a..e48f59b4f0 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb
@@ -105,7 +105,7 @@ module ActiveRecord
end
type_metadata = fetch_type_metadata(field["type"])
- Column.new(field["name"], default, type_metadata, field["notnull"].to_i == 0, table_name, nil, field["collation"])
+ Column.new(field["name"], default, type_metadata, field["notnull"].to_i == 0, collation: field["collation"])
end
def data_source_sql(name = nil, type: nil)
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
index 1801924c09..f5f5827d04 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
@@ -204,91 +204,11 @@ module ActiveRecord
#--
# DATABASE STATEMENTS ======================================
#++
-
- READ_QUERY = ActiveRecord::ConnectionAdapters::AbstractAdapter.build_read_query_regexp(:begin, :commit, :explain, :select, :pragma, :release, :savepoint, :rollback) # :nodoc:
- private_constant :READ_QUERY
-
- def write_query?(sql) # :nodoc:
- !READ_QUERY.match?(sql)
- end
-
def explain(arel, binds = [])
sql = "EXPLAIN QUERY PLAN #{to_sql(arel, binds)}"
SQLite3::ExplainPrettyPrinter.new.pp(exec_query(sql, "EXPLAIN", []))
end
- def exec_query(sql, name = nil, binds = [], prepare: false)
- if preventing_writes? && write_query?(sql)
- raise ActiveRecord::ReadOnlyError, "Write query attempted while in readonly mode: #{sql}"
- end
-
- materialize_transactions
-
- type_casted_binds = type_casted_binds(binds)
-
- log(sql, name, binds, type_casted_binds) do
- ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
- # Don't cache statements if they are not prepared
- unless prepare
- stmt = @connection.prepare(sql)
- begin
- cols = stmt.columns
- unless without_prepared_statement?(binds)
- stmt.bind_params(type_casted_binds)
- end
- records = stmt.to_a
- ensure
- stmt.close
- end
- else
- stmt = @statements[sql] ||= @connection.prepare(sql)
- cols = stmt.columns
- stmt.reset!
- stmt.bind_params(type_casted_binds)
- records = stmt.to_a
- end
-
- ActiveRecord::Result.new(cols, records)
- end
- end
- end
-
- def exec_delete(sql, name = "SQL", binds = [])
- exec_query(sql, name, binds)
- @connection.changes
- end
- alias :exec_update :exec_delete
-
- def last_inserted_id(result)
- @connection.last_insert_row_id
- end
-
- def execute(sql, name = nil) #:nodoc:
- 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(sql)
- end
- end
- end
-
- def begin_db_transaction #:nodoc:
- log("begin transaction", nil) { @connection.transaction }
- end
-
- def commit_db_transaction #:nodoc:
- log("commit transaction", nil) { @connection.commit }
- end
-
- def exec_rollback_db_transaction #:nodoc:
- log("rollback transaction", nil) { @connection.rollback }
- end
-
# SCHEMA STATEMENTS ========================================
def primary_keys(table_name) # :nodoc:
diff --git a/activerecord/lib/active_record/insert_all.rb b/activerecord/lib/active_record/insert_all.rb
index 4b02d40aa0..959e5bd4d7 100644
--- a/activerecord/lib/active_record/insert_all.rb
+++ b/activerecord/lib/active_record/insert_all.rb
@@ -21,7 +21,10 @@ module ActiveRecord
end
def execute
- connection.exec_query to_sql, "Bulk Insert"
+ message = "#{model} "
+ message += "Bulk " if inserts.many?
+ message += (on_duplicate == :update ? "Upsert" : "Insert")
+ connection.exec_query to_sql, message
end
def updatable_columns
@@ -111,7 +114,7 @@ module ActiveRecord
class Builder
attr_reader :model
- delegate :skip_duplicates?, :update_duplicates?, to: :insert_all
+ delegate :skip_duplicates?, :update_duplicates?, :keys, to: :insert_all
def initialize(insert_all)
@insert_all, @model, @connection = insert_all, insert_all.model, insert_all.connection
@@ -122,11 +125,10 @@ module ActiveRecord
end
def values_list
- types = extract_types_from_columns_on(model.table_name, keys: insert_all.keys)
+ types = extract_types_from_columns_on(model.table_name, keys: keys)
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)
+ connection.with_yaml_fallback(types[key].serialize(value))
end
Arel::InsertManager.new.create_values_list(values_list).to_sql
diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb
index 997b7f763a..ed0c6d48b8 100644
--- a/activerecord/lib/active_record/migration.rb
+++ b/activerecord/lib/active_record/migration.rb
@@ -6,7 +6,7 @@ require "zlib"
require "active_support/core_ext/module/attribute_accessors"
module ActiveRecord
- class MigrationError < ActiveRecordError#:nodoc:
+ class MigrationError < ActiveRecordError #:nodoc:
def initialize(message = nil)
message = "\n\n#{message}\n\n" if message
super
@@ -87,7 +87,7 @@ module ActiveRecord
class IrreversibleMigration < MigrationError
end
- class DuplicateMigrationVersionError < MigrationError#:nodoc:
+ class DuplicateMigrationVersionError < MigrationError #:nodoc:
def initialize(version = nil)
if version
super("Multiple migrations have the version number #{version}.")
@@ -97,7 +97,7 @@ module ActiveRecord
end
end
- class DuplicateMigrationNameError < MigrationError#:nodoc:
+ class DuplicateMigrationNameError < MigrationError #:nodoc:
def initialize(name = nil)
if name
super("Multiple migrations have the name #{name}.")
@@ -117,7 +117,7 @@ module ActiveRecord
end
end
- class IllegalMigrationNameError < MigrationError#:nodoc:
+ class IllegalMigrationNameError < MigrationError #:nodoc:
def initialize(name = nil)
if name
super("Illegal name for migration file: #{name}\n\t(only lower case letters, numbers, and '_' allowed).")
@@ -127,7 +127,7 @@ module ActiveRecord
end
end
- class PendingMigrationError < MigrationError#:nodoc:
+ class PendingMigrationError < MigrationError #:nodoc:
def initialize(message = nil)
if !message && defined?(Rails.env)
super("Migrations are pending. To resolve this issue, run:\n\n rails db:migrate RAILS_ENV=#{::Rails.env}")
@@ -520,10 +520,10 @@ module ActiveRecord
autoload :Compatibility, "active_record/migration/compatibility"
# This must be defined before the inherited hook, below
- class Current < Migration # :nodoc:
+ class Current < Migration #:nodoc:
end
- def self.inherited(subclass) # :nodoc:
+ def self.inherited(subclass) #:nodoc:
super
if subclass.superclass == Migration
raise StandardError, "Directly inheriting from ActiveRecord::Migration is not supported. " \
@@ -541,7 +541,7 @@ module ActiveRecord
ActiveRecord::VERSION::STRING.to_f
end
- MigrationFilenameRegexp = /\A([0-9]+)_([_a-z0-9]*)\.?([_a-z0-9]*)?\.rb\z/ # :nodoc:
+ MigrationFilenameRegexp = /\A([0-9]+)_([_a-z0-9]*)\.?([_a-z0-9]*)?\.rb\z/ #:nodoc:
# This class is used to verify that all migrations have been run before
# loading a web page if <tt>config.active_record.migration_error</tt> is set to :page_load
@@ -568,10 +568,10 @@ module ActiveRecord
end
class << self
- attr_accessor :delegate # :nodoc:
- attr_accessor :disable_ddl_transaction # :nodoc:
+ attr_accessor :delegate #:nodoc:
+ attr_accessor :disable_ddl_transaction #:nodoc:
- def nearest_delegate # :nodoc:
+ def nearest_delegate #:nodoc:
delegate || superclass.nearest_delegate
end
@@ -595,13 +595,13 @@ module ActiveRecord
end
end
- def maintain_test_schema! # :nodoc:
+ def maintain_test_schema! #:nodoc:
if ActiveRecord::Base.maintain_test_schema
suppress_messages { load_schema_if_pending! }
end
end
- def method_missing(name, *args, &block) # :nodoc:
+ def method_missing(name, *args, &block) #:nodoc:
nearest_delegate.send(name, *args, &block)
end
@@ -618,7 +618,7 @@ module ActiveRecord
end
end
- def disable_ddl_transaction # :nodoc:
+ def disable_ddl_transaction #:nodoc:
self.class.disable_ddl_transaction
end
@@ -693,7 +693,7 @@ module ActiveRecord
connection.respond_to?(:reverting) && connection.reverting
end
- ReversibleBlockHelper = Struct.new(:reverting) do # :nodoc:
+ ReversibleBlockHelper = Struct.new(:reverting) do #:nodoc:
def up
yield unless reverting
end
@@ -1006,7 +1006,7 @@ module ActiveRecord
end
end
- class MigrationContext # :nodoc:
+ class MigrationContext #:nodoc:
attr_reader :migrations_paths
def initialize(migrations_paths)
@@ -1165,7 +1165,7 @@ module ActiveRecord
end
end
- class Migrator # :nodoc:
+ class Migrator #:nodoc:
class << self
attr_accessor :migrations_paths
diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb
index bf2f61737b..e05490753f 100644
--- a/activerecord/lib/active_record/persistence.rb
+++ b/activerecord/lib/active_record/persistence.rb
@@ -178,7 +178,7 @@ module ActiveRecord
InsertAll.new(self, attributes, on_duplicate: :raise, returning: returning).execute
end
- # Updates or inserts (upserts) multiple records into the database in a
+ # Updates or inserts (upserts) a single record into the database in a
# single SQL INSERT statement. It does not instantiate any models nor does
# it trigger Active Record callbacks or validations. Though passed values
# go through Active Record's type casting and serialization.
diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb
index 3452cf971b..1312bf6f91 100644
--- a/activerecord/lib/active_record/reflection.rb
+++ b/activerecord/lib/active_record/reflection.rb
@@ -21,12 +21,12 @@ module ActiveRecord
def add_reflection(ar, name, reflection)
ar.clear_reflections_cache
- name = name.to_s
+ name = -name.to_s
ar._reflections = ar._reflections.except(name).merge!(name => reflection)
end
def add_aggregate_reflection(ar, name, reflection)
- ar.aggregate_reflections = ar.aggregate_reflections.merge(name.to_s => reflection)
+ ar.aggregate_reflections = ar.aggregate_reflections.merge(-name.to_s => reflection)
end
private
diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb
index c1ef98c063..cd62b0b881 100644
--- a/activerecord/lib/active_record/relation.rb
+++ b/activerecord/lib/active_record/relation.rb
@@ -721,6 +721,10 @@ module ActiveRecord
@loaded = true
end
+ def null_relation? # :nodoc:
+ is_a?(NullRelation)
+ end
+
private
def already_in_scope?
@delegate_to_klass && begin
@@ -770,7 +774,7 @@ module ActiveRecord
@records =
if eager_loading?
apply_join_dependency do |relation, join_dependency|
- if ActiveRecord::NullRelation === relation
+ if relation.null_relation?
[]
else
relation = join_dependency.apply_column_aliases(relation)
diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb
index 29b1dc97b7..801e312658 100644
--- a/activerecord/lib/active_record/relation/calculations.rb
+++ b/activerecord/lib/active_record/relation/calculations.rb
@@ -308,25 +308,22 @@ module ActiveRecord
end
def execute_grouped_calculation(operation, column_name, distinct) #:nodoc:
- group_attrs = group_values
+ group_fields = group_values
- if group_attrs.first.respond_to?(:to_sym)
- association = @klass._reflect_on_association(group_attrs.first)
- associated = group_attrs.size == 1 && association && association.belongs_to? # only count belongs_to associations
- group_fields = Array(associated ? association.foreign_key : group_attrs)
- else
- group_fields = group_attrs
+ if group_fields.size == 1 && group_fields.first.respond_to?(:to_sym)
+ association = klass._reflect_on_association(group_fields.first)
+ associated = association && association.belongs_to? # only count belongs_to associations
+ group_fields = Array(association.foreign_key) if associated
end
group_fields = arel_columns(group_fields)
- group_aliases = group_fields.map { |field| column_alias_for(field) }
+ group_aliases = group_fields.map { |field|
+ field = connection.visitor.compile(field) if Arel.arel_node?(field)
+ column_alias_for(field.to_s.downcase)
+ }
group_columns = group_aliases.zip(group_fields)
- if operation == "count" && column_name == :all
- aggregate_alias = "count_all"
- else
- aggregate_alias = column_alias_for([operation, column_name].join(" "))
- end
+ aggregate_alias = column_alias_for("#{operation}_#{column_name.to_s.downcase}")
select_values = [
operation_over_aggregate_column(
@@ -345,7 +342,7 @@ module ActiveRecord
}
relation = except(:group).distinct!(false)
- relation.group_values = group_fields
+ relation.group_values = group_aliases
relation.select_values = select_values
calculated_data = skip_query_cache_if_necessary { @klass.connection.select_all(relation.arel, nil) }
@@ -371,25 +368,23 @@ module ActiveRecord
end]
end
- # Converts the given keys to the value that the database adapter returns as
+ # Converts the given field to the value that the database adapter returns as
# a usable column name:
#
# column_alias_for("users.id") # => "users_id"
# column_alias_for("sum(id)") # => "sum_id"
# column_alias_for("count(distinct users.id)") # => "count_distinct_users_id"
# column_alias_for("count(*)") # => "count_all"
- def column_alias_for(keys)
- if keys.respond_to? :name
- keys = "#{keys.relation.name}.#{keys.name}"
- end
+ def column_alias_for(field)
+ return field if field.match?(/\A\w{,#{connection.table_alias_length}}\z/)
- table_name = keys.to_s.downcase
- table_name.gsub!(/\*/, "all")
- table_name.gsub!(/\W+/, " ")
- table_name.strip!
- table_name.gsub!(/ +/, "_")
+ column_alias = +field
+ column_alias.gsub!(/\*/, "all")
+ column_alias.gsub!(/\W+/, " ")
+ column_alias.strip!
+ column_alias.gsub!(/ +/, "_")
- @klass.connection.table_alias_for(table_name)
+ connection.table_alias_for(column_alias)
end
def type_for(field, &block)
diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb
index fcb0da1a42..9450e4d3c5 100644
--- a/activerecord/lib/active_record/relation/finder_methods.rb
+++ b/activerecord/lib/active_record/relation/finder_methods.rb
@@ -314,7 +314,7 @@ module ActiveRecord
relation = construct_relation_for_exists(conditions)
- skip_query_cache_if_necessary { connection.select_one(relation.arel, "#{name} Exists") } ? true : false
+ skip_query_cache_if_necessary { connection.select_one(relation.arel, "#{name} Exists?") } ? true : false
end
# This method is called whenever no records are found with either a single
diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb
index 7ea18c3069..90b5e9a118 100644
--- a/activerecord/lib/active_record/relation/query_methods.rb
+++ b/activerecord/lib/active_record/relation/query_methods.rb
@@ -252,9 +252,6 @@ module ActiveRecord
def _select!(*fields) # :nodoc:
fields.reject!(&:blank?)
fields.flatten!
- fields.map! do |field|
- klass.attribute_alias?(field) ? klass.attribute_alias(field).to_sym : field
- end
self.select_values += fields
self
end
@@ -1164,9 +1161,9 @@ module ActiveRecord
case field
when Symbol
field = field.to_s
- arel_column(field) { connection.quote_table_name(field) }
+ arel_column(field, &connection.method(:quote_table_name))
when String
- arel_column(field) { field }
+ arel_column(field, &:itself)
when Proc
field.call
else
@@ -1182,7 +1179,7 @@ module ActiveRecord
if klass.columns_hash.key?(field) && (!from || table_name_matches?(from))
arel_attribute(field)
else
- yield
+ yield field
end
end
diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb
index d475e77444..2f7cc07221 100644
--- a/activerecord/lib/active_record/schema_dumper.rb
+++ b/activerecord/lib/active_record/schema_dumper.rb
@@ -47,6 +47,7 @@ module ActiveRecord
end
private
+ attr_accessor :table_name
def initialize(connection, options = {})
@connection = connection
@@ -110,6 +111,8 @@ HEADER
def table(table, stream)
columns = @connection.columns(table)
begin
+ self.table_name = table
+
tbl = StringIO.new
# first dump primary key column
@@ -159,6 +162,8 @@ HEADER
stream.puts "# Could not dump table #{table.inspect} because of following #{e.class}"
stream.puts "# #{e.message}"
stream.puts
+ ensure
+ self.table_name = nil
end
end
diff --git a/activerecord/lib/active_record/scoping/named.rb b/activerecord/lib/active_record/scoping/named.rb
index 681a5c6250..cd9801b7a0 100644
--- a/activerecord/lib/active_record/scoping/named.rb
+++ b/activerecord/lib/active_record/scoping/named.rb
@@ -58,7 +58,7 @@ module ActiveRecord
end
def default_extensions # :nodoc:
- if scope = current_scope || build_default_scope
+ if scope = scope_for_association || build_default_scope
scope.extensions
else
[]
diff --git a/activerecord/lib/arel/visitors/oracle.rb b/activerecord/lib/arel/visitors/oracle.rb
index f96bf65ee5..500974dff5 100644
--- a/activerecord/lib/arel/visitors/oracle.rb
+++ b/activerecord/lib/arel/visitors/oracle.rb
@@ -87,6 +87,50 @@ module Arel # :nodoc: all
collector << " )"
end
+ def visit_Arel_Nodes_In(o, collector)
+ if Array === o.right && !o.right.empty?
+ o.right.delete_if { |value| unboundable?(value) }
+ end
+
+ if Array === o.right && o.right.empty?
+ collector << "1=0"
+ else
+ first = true
+ o.right.each_slice(in_clause_length) do |sliced_o_right|
+ collector << " OR " unless first
+ first = false
+
+ collector = visit o.left, collector
+ collector << " IN ("
+ visit(sliced_o_right, collector)
+ collector << ")"
+ end
+ end
+ collector
+ end
+
+ def visit_Arel_Nodes_NotIn(o, collector)
+ if Array === o.right && !o.right.empty?
+ o.right.delete_if { |value| unboundable?(value) }
+ end
+
+ if Array === o.right && o.right.empty?
+ collector << "1=1"
+ else
+ first = true
+ o.right.each_slice(in_clause_length) do |sliced_o_right|
+ collector << " AND " unless first
+ first = false
+
+ collector = visit o.left, collector
+ collector << " NOT IN ("
+ visit(sliced_o_right, collector)
+ collector << ")"
+ end
+ end
+ collector
+ end
+
def visit_Arel_Nodes_UpdateStatement(o, collector)
# Oracle does not allow ORDER BY/LIMIT in UPDATEs.
if o.orders.any? && o.limit.nil?
@@ -154,6 +198,10 @@ module Arel # :nodoc: all
collector = visit [o.left, o.right, 0, 1], collector
collector << ")"
end
+
+ def in_clause_length
+ 1000
+ end
end
end
end
diff --git a/activerecord/lib/arel/visitors/oracle12.rb b/activerecord/lib/arel/visitors/oracle12.rb
index 9a7fe4d626..8e0f07fca9 100644
--- a/activerecord/lib/arel/visitors/oracle12.rb
+++ b/activerecord/lib/arel/visitors/oracle12.rb
@@ -41,6 +41,50 @@ module Arel # :nodoc: all
collector << " )"
end
+ def visit_Arel_Nodes_In(o, collector)
+ if Array === o.right && !o.right.empty?
+ o.right.delete_if { |value| unboundable?(value) }
+ end
+
+ if Array === o.right && o.right.empty?
+ collector << "1=0"
+ else
+ first = true
+ o.right.each_slice(in_clause_length) do |sliced_o_right|
+ collector << " OR " unless first
+ first = false
+
+ collector = visit o.left, collector
+ collector << " IN ("
+ visit(sliced_o_right, collector)
+ collector << ")"
+ end
+ end
+ collector
+ end
+
+ def visit_Arel_Nodes_NotIn(o, collector)
+ if Array === o.right && !o.right.empty?
+ o.right.delete_if { |value| unboundable?(value) }
+ end
+
+ if Array === o.right && o.right.empty?
+ collector << "1=1"
+ else
+ first = true
+ o.right.each_slice(in_clause_length) do |sliced_o_right|
+ collector << " AND " unless first
+ first = false
+
+ collector = visit o.left, collector
+ collector << " NOT IN ("
+ visit(sliced_o_right, collector)
+ collector << ")"
+ end
+ end
+ collector
+ end
+
def visit_Arel_Nodes_UpdateStatement(o, collector)
# Oracle does not allow ORDER BY/LIMIT in UPDATEs.
if o.orders.any? && o.limit.nil?
@@ -62,6 +106,10 @@ module Arel # :nodoc: all
collector = visit [o.left, o.right, 0, 1], collector
collector << ")"
end
+
+ def in_clause_length
+ 1000
+ end
end
end
end
diff --git a/activerecord/test/cases/adapters/mysql2/count_deleted_rows_with_lock_test.rb b/activerecord/test/cases/adapters/mysql2/count_deleted_rows_with_lock_test.rb
new file mode 100644
index 0000000000..4d361e405c
--- /dev/null
+++ b/activerecord/test/cases/adapters/mysql2/count_deleted_rows_with_lock_test.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "support/connection_helper"
+require "models/author"
+require "models/bulb"
+
+module ActiveRecord
+ class CountDeletedRowsWithLockTest < ActiveRecord::Mysql2TestCase
+ test "delete and create in different threads synchronize correctly" do
+ Bulb.unscoped.delete_all
+ Bulb.create!(name: "Jimmy", color: "blue")
+
+ delete_thread = Thread.new do
+ Bulb.unscoped.delete_all
+ end
+
+ create_thread = Thread.new do
+ Author.create!(name: "Tommy")
+ end
+
+ delete_thread.join
+ create_thread.join
+
+ assert_equal 1, delete_thread.value
+ end
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/bytea_test.rb b/activerecord/test/cases/adapters/postgresql/bytea_test.rb
index 3988c2adca..531e6b2328 100644
--- a/activerecord/test/cases/adapters/postgresql/bytea_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/bytea_test.rb
@@ -35,7 +35,7 @@ class PostgresqlByteaTest < ActiveRecord::PostgreSQLTestCase
def test_binary_columns_are_limitless_the_upper_limit_is_one_GB
assert_equal "bytea", @connection.type_to_sql(:binary, limit: 100_000)
- assert_raise ActiveRecord::ActiveRecordError do
+ assert_raise ArgumentError do
@connection.type_to_sql(:binary, limit: 4294967295)
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/datatype_test.rb b/activerecord/test/cases/adapters/postgresql/datatype_test.rb
index b7535d5c9a..562cf1f2d1 100644
--- a/activerecord/test/cases/adapters/postgresql/datatype_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/datatype_test.rb
@@ -64,7 +64,7 @@ class PostgresqlDataTypeTest < ActiveRecord::PostgreSQLTestCase
def test_text_columns_are_limitless_the_upper_limit_is_one_GB
assert_equal "text", @connection.type_to_sql(:text, limit: 100_000)
- assert_raise ActiveRecord::ActiveRecordError do
+ assert_raise ArgumentError do
@connection.type_to_sql(:text, limit: 4294967295)
end
end
diff --git a/activerecord/test/cases/arel/visitors/oracle12_test.rb b/activerecord/test/cases/arel/visitors/oracle12_test.rb
index 4ce5cab4db..ebea12910d 100644
--- a/activerecord/test/cases/arel/visitors/oracle12_test.rb
+++ b/activerecord/test/cases/arel/visitors/oracle12_test.rb
@@ -8,6 +8,7 @@ module Arel
before do
@visitor = Oracle12.new Table.engine.connection
@table = Table.new(:users)
+ @attr = @table[:id]
end
def compile(node)
@@ -95,6 +96,26 @@ module Arel
sql.must_be_like %{ "users"."name" IS NOT NULL }
end
end
+
+ describe "Nodes::In" do
+ it "should know how to visit" do
+ ary = (1 .. 1001).to_a
+ node = @attr.in ary
+ compile(node).must_be_like %{
+ "users"."id" IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 256, 257, 258, 259, 260, 261, 262, 263, 264, 265, 266, 267, 268, 269, 270, 271, 272, 273, 274, 275, 276, 277, 278, 279, 280, 281, 282, 283, 284, 285, 286, 287, 288, 289, 290, 291, 292, 293, 294, 295, 296, 297, 298, 299, 300, 301, 302, 303, 304, 305, 306, 307, 308, 309, 310, 311, 312, 313, 314, 315, 316, 317, 318, 319, 320, 321, 322, 323, 324, 325, 326, 327, 328, 329, 330, 331, 332, 333, 334, 335, 336, 337, 338, 339, 340, 341, 342, 343, 344, 345, 346, 347, 348, 349, 350, 351, 352, 353, 354, 355, 356, 357, 358, 359, 360, 361, 362, 363, 364, 365, 366, 367, 368, 369, 370, 371, 372, 373, 374, 375, 376, 377, 378, 379, 380, 381, 382, 383, 384, 385, 386, 387, 388, 389, 390, 391, 392, 393, 394, 395, 396, 397, 398, 399, 400, 401, 402, 403, 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417, 418, 419, 420, 421, 422, 423, 424, 425, 426, 427, 428, 429, 430, 431, 432, 433, 434, 435, 436, 437, 438, 439, 440, 441, 442, 443, 444, 445, 446, 447, 448, 449, 450, 451, 452, 453, 454, 455, 456, 457, 458, 459, 460, 461, 462, 463, 464, 465, 466, 467, 468, 469, 470, 471, 472, 473, 474, 475, 476, 477, 478, 479, 480, 481, 482, 483, 484, 485, 486, 487, 488, 489, 490, 491, 492, 493, 494, 495, 496, 497, 498, 499, 500, 501, 502, 503, 504, 505, 506, 507, 508, 509, 510, 511, 512, 513, 514, 515, 516, 517, 518, 519, 520, 521, 522, 523, 524, 525, 526, 527, 528, 529, 530, 531, 532, 533, 534, 535, 536, 537, 538, 539, 540, 541, 542, 543, 544, 545, 546, 547, 548, 549, 550, 551, 552, 553, 554, 555, 556, 557, 558, 559, 560, 561, 562, 563, 564, 565, 566, 567, 568, 569, 570, 571, 572, 573, 574, 575, 576, 577, 578, 579, 580, 581, 582, 583, 584, 585, 586, 587, 588, 589, 590, 591, 592, 593, 594, 595, 596, 597, 598, 599, 600, 601, 602, 603, 604, 605, 606, 607, 608, 609, 610, 611, 612, 613, 614, 615, 616, 617, 618, 619, 620, 621, 622, 623, 624, 625, 626, 627, 628, 629, 630, 631, 632, 633, 634, 635, 636, 637, 638, 639, 640, 641, 642, 643, 644, 645, 646, 647, 648, 649, 650, 651, 652, 653, 654, 655, 656, 657, 658, 659, 660, 661, 662, 663, 664, 665, 666, 667, 668, 669, 670, 671, 672, 673, 674, 675, 676, 677, 678, 679, 680, 681, 682, 683, 684, 685, 686, 687, 688, 689, 690, 691, 692, 693, 694, 695, 696, 697, 698, 699, 700, 701, 702, 703, 704, 705, 706, 707, 708, 709, 710, 711, 712, 713, 714, 715, 716, 717, 718, 719, 720, 721, 722, 723, 724, 725, 726, 727, 728, 729, 730, 731, 732, 733, 734, 735, 736, 737, 738, 739, 740, 741, 742, 743, 744, 745, 746, 747, 748, 749, 750, 751, 752, 753, 754, 755, 756, 757, 758, 759, 760, 761, 762, 763, 764, 765, 766, 767, 768, 769, 770, 771, 772, 773, 774, 775, 776, 777, 778, 779, 780, 781, 782, 783, 784, 785, 786, 787, 788, 789, 790, 791, 792, 793, 794, 795, 796, 797, 798, 799, 800, 801, 802, 803, 804, 805, 806, 807, 808, 809, 810, 811, 812, 813, 814, 815, 816, 817, 818, 819, 820, 821, 822, 823, 824, 825, 826, 827, 828, 829, 830, 831, 832, 833, 834, 835, 836, 837, 838, 839, 840, 841, 842, 843, 844, 845, 846, 847, 848, 849, 850, 851, 852, 853, 854, 855, 856, 857, 858, 859, 860, 861, 862, 863, 864, 865, 866, 867, 868, 869, 870, 871, 872, 873, 874, 875, 876, 877, 878, 879, 880, 881, 882, 883, 884, 885, 886, 887, 888, 889, 890, 891, 892, 893, 894, 895, 896, 897, 898, 899, 900, 901, 902, 903, 904, 905, 906, 907, 908, 909, 910, 911, 912, 913, 914, 915, 916, 917, 918, 919, 920, 921, 922, 923, 924, 925, 926, 927, 928, 929, 930, 931, 932, 933, 934, 935, 936, 937, 938, 939, 940, 941, 942, 943, 944, 945, 946, 947, 948, 949, 950, 951, 952, 953, 954, 955, 956, 957, 958, 959, 960, 961, 962, 963, 964, 965, 966, 967, 968, 969, 970, 971, 972, 973, 974, 975, 976, 977, 978, 979, 980, 981, 982, 983, 984, 985, 986, 987, 988, 989, 990, 991, 992, 993, 994, 995, 996, 997, 998, 999, 1000) OR \"users\".\"id\" IN (1001)
+ }
+ end
+ end
+
+ describe "Nodes::NotIn" do
+ it "should know how to visit" do
+ ary = (1 .. 1001).to_a
+ node = @attr.not_in ary
+ compile(node).must_be_like %{
+ "users"."id" NOT IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 256, 257, 258, 259, 260, 261, 262, 263, 264, 265, 266, 267, 268, 269, 270, 271, 272, 273, 274, 275, 276, 277, 278, 279, 280, 281, 282, 283, 284, 285, 286, 287, 288, 289, 290, 291, 292, 293, 294, 295, 296, 297, 298, 299, 300, 301, 302, 303, 304, 305, 306, 307, 308, 309, 310, 311, 312, 313, 314, 315, 316, 317, 318, 319, 320, 321, 322, 323, 324, 325, 326, 327, 328, 329, 330, 331, 332, 333, 334, 335, 336, 337, 338, 339, 340, 341, 342, 343, 344, 345, 346, 347, 348, 349, 350, 351, 352, 353, 354, 355, 356, 357, 358, 359, 360, 361, 362, 363, 364, 365, 366, 367, 368, 369, 370, 371, 372, 373, 374, 375, 376, 377, 378, 379, 380, 381, 382, 383, 384, 385, 386, 387, 388, 389, 390, 391, 392, 393, 394, 395, 396, 397, 398, 399, 400, 401, 402, 403, 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417, 418, 419, 420, 421, 422, 423, 424, 425, 426, 427, 428, 429, 430, 431, 432, 433, 434, 435, 436, 437, 438, 439, 440, 441, 442, 443, 444, 445, 446, 447, 448, 449, 450, 451, 452, 453, 454, 455, 456, 457, 458, 459, 460, 461, 462, 463, 464, 465, 466, 467, 468, 469, 470, 471, 472, 473, 474, 475, 476, 477, 478, 479, 480, 481, 482, 483, 484, 485, 486, 487, 488, 489, 490, 491, 492, 493, 494, 495, 496, 497, 498, 499, 500, 501, 502, 503, 504, 505, 506, 507, 508, 509, 510, 511, 512, 513, 514, 515, 516, 517, 518, 519, 520, 521, 522, 523, 524, 525, 526, 527, 528, 529, 530, 531, 532, 533, 534, 535, 536, 537, 538, 539, 540, 541, 542, 543, 544, 545, 546, 547, 548, 549, 550, 551, 552, 553, 554, 555, 556, 557, 558, 559, 560, 561, 562, 563, 564, 565, 566, 567, 568, 569, 570, 571, 572, 573, 574, 575, 576, 577, 578, 579, 580, 581, 582, 583, 584, 585, 586, 587, 588, 589, 590, 591, 592, 593, 594, 595, 596, 597, 598, 599, 600, 601, 602, 603, 604, 605, 606, 607, 608, 609, 610, 611, 612, 613, 614, 615, 616, 617, 618, 619, 620, 621, 622, 623, 624, 625, 626, 627, 628, 629, 630, 631, 632, 633, 634, 635, 636, 637, 638, 639, 640, 641, 642, 643, 644, 645, 646, 647, 648, 649, 650, 651, 652, 653, 654, 655, 656, 657, 658, 659, 660, 661, 662, 663, 664, 665, 666, 667, 668, 669, 670, 671, 672, 673, 674, 675, 676, 677, 678, 679, 680, 681, 682, 683, 684, 685, 686, 687, 688, 689, 690, 691, 692, 693, 694, 695, 696, 697, 698, 699, 700, 701, 702, 703, 704, 705, 706, 707, 708, 709, 710, 711, 712, 713, 714, 715, 716, 717, 718, 719, 720, 721, 722, 723, 724, 725, 726, 727, 728, 729, 730, 731, 732, 733, 734, 735, 736, 737, 738, 739, 740, 741, 742, 743, 744, 745, 746, 747, 748, 749, 750, 751, 752, 753, 754, 755, 756, 757, 758, 759, 760, 761, 762, 763, 764, 765, 766, 767, 768, 769, 770, 771, 772, 773, 774, 775, 776, 777, 778, 779, 780, 781, 782, 783, 784, 785, 786, 787, 788, 789, 790, 791, 792, 793, 794, 795, 796, 797, 798, 799, 800, 801, 802, 803, 804, 805, 806, 807, 808, 809, 810, 811, 812, 813, 814, 815, 816, 817, 818, 819, 820, 821, 822, 823, 824, 825, 826, 827, 828, 829, 830, 831, 832, 833, 834, 835, 836, 837, 838, 839, 840, 841, 842, 843, 844, 845, 846, 847, 848, 849, 850, 851, 852, 853, 854, 855, 856, 857, 858, 859, 860, 861, 862, 863, 864, 865, 866, 867, 868, 869, 870, 871, 872, 873, 874, 875, 876, 877, 878, 879, 880, 881, 882, 883, 884, 885, 886, 887, 888, 889, 890, 891, 892, 893, 894, 895, 896, 897, 898, 899, 900, 901, 902, 903, 904, 905, 906, 907, 908, 909, 910, 911, 912, 913, 914, 915, 916, 917, 918, 919, 920, 921, 922, 923, 924, 925, 926, 927, 928, 929, 930, 931, 932, 933, 934, 935, 936, 937, 938, 939, 940, 941, 942, 943, 944, 945, 946, 947, 948, 949, 950, 951, 952, 953, 954, 955, 956, 957, 958, 959, 960, 961, 962, 963, 964, 965, 966, 967, 968, 969, 970, 971, 972, 973, 974, 975, 976, 977, 978, 979, 980, 981, 982, 983, 984, 985, 986, 987, 988, 989, 990, 991, 992, 993, 994, 995, 996, 997, 998, 999, 1000) AND \"users\".\"id\" NOT IN (1001)
+ }
+ end
+ end
end
end
end
diff --git a/activerecord/test/cases/arel/visitors/oracle_test.rb b/activerecord/test/cases/arel/visitors/oracle_test.rb
index 893edc7f74..f69b201855 100644
--- a/activerecord/test/cases/arel/visitors/oracle_test.rb
+++ b/activerecord/test/cases/arel/visitors/oracle_test.rb
@@ -8,6 +8,7 @@ module Arel
before do
@visitor = Oracle.new Table.engine.connection
@table = Table.new(:users)
+ @attr = @table[:id]
end
def compile(node)
@@ -231,6 +232,26 @@ module Arel
sql.must_be_like %{ "users"."name" IS NOT NULL }
end
end
+
+ describe "Nodes::In" do
+ it "should know how to visit" do
+ ary = (1 .. 1001).to_a
+ node = @attr.in ary
+ compile(node).must_be_like %{
+ "users"."id" IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 256, 257, 258, 259, 260, 261, 262, 263, 264, 265, 266, 267, 268, 269, 270, 271, 272, 273, 274, 275, 276, 277, 278, 279, 280, 281, 282, 283, 284, 285, 286, 287, 288, 289, 290, 291, 292, 293, 294, 295, 296, 297, 298, 299, 300, 301, 302, 303, 304, 305, 306, 307, 308, 309, 310, 311, 312, 313, 314, 315, 316, 317, 318, 319, 320, 321, 322, 323, 324, 325, 326, 327, 328, 329, 330, 331, 332, 333, 334, 335, 336, 337, 338, 339, 340, 341, 342, 343, 344, 345, 346, 347, 348, 349, 350, 351, 352, 353, 354, 355, 356, 357, 358, 359, 360, 361, 362, 363, 364, 365, 366, 367, 368, 369, 370, 371, 372, 373, 374, 375, 376, 377, 378, 379, 380, 381, 382, 383, 384, 385, 386, 387, 388, 389, 390, 391, 392, 393, 394, 395, 396, 397, 398, 399, 400, 401, 402, 403, 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417, 418, 419, 420, 421, 422, 423, 424, 425, 426, 427, 428, 429, 430, 431, 432, 433, 434, 435, 436, 437, 438, 439, 440, 441, 442, 443, 444, 445, 446, 447, 448, 449, 450, 451, 452, 453, 454, 455, 456, 457, 458, 459, 460, 461, 462, 463, 464, 465, 466, 467, 468, 469, 470, 471, 472, 473, 474, 475, 476, 477, 478, 479, 480, 481, 482, 483, 484, 485, 486, 487, 488, 489, 490, 491, 492, 493, 494, 495, 496, 497, 498, 499, 500, 501, 502, 503, 504, 505, 506, 507, 508, 509, 510, 511, 512, 513, 514, 515, 516, 517, 518, 519, 520, 521, 522, 523, 524, 525, 526, 527, 528, 529, 530, 531, 532, 533, 534, 535, 536, 537, 538, 539, 540, 541, 542, 543, 544, 545, 546, 547, 548, 549, 550, 551, 552, 553, 554, 555, 556, 557, 558, 559, 560, 561, 562, 563, 564, 565, 566, 567, 568, 569, 570, 571, 572, 573, 574, 575, 576, 577, 578, 579, 580, 581, 582, 583, 584, 585, 586, 587, 588, 589, 590, 591, 592, 593, 594, 595, 596, 597, 598, 599, 600, 601, 602, 603, 604, 605, 606, 607, 608, 609, 610, 611, 612, 613, 614, 615, 616, 617, 618, 619, 620, 621, 622, 623, 624, 625, 626, 627, 628, 629, 630, 631, 632, 633, 634, 635, 636, 637, 638, 639, 640, 641, 642, 643, 644, 645, 646, 647, 648, 649, 650, 651, 652, 653, 654, 655, 656, 657, 658, 659, 660, 661, 662, 663, 664, 665, 666, 667, 668, 669, 670, 671, 672, 673, 674, 675, 676, 677, 678, 679, 680, 681, 682, 683, 684, 685, 686, 687, 688, 689, 690, 691, 692, 693, 694, 695, 696, 697, 698, 699, 700, 701, 702, 703, 704, 705, 706, 707, 708, 709, 710, 711, 712, 713, 714, 715, 716, 717, 718, 719, 720, 721, 722, 723, 724, 725, 726, 727, 728, 729, 730, 731, 732, 733, 734, 735, 736, 737, 738, 739, 740, 741, 742, 743, 744, 745, 746, 747, 748, 749, 750, 751, 752, 753, 754, 755, 756, 757, 758, 759, 760, 761, 762, 763, 764, 765, 766, 767, 768, 769, 770, 771, 772, 773, 774, 775, 776, 777, 778, 779, 780, 781, 782, 783, 784, 785, 786, 787, 788, 789, 790, 791, 792, 793, 794, 795, 796, 797, 798, 799, 800, 801, 802, 803, 804, 805, 806, 807, 808, 809, 810, 811, 812, 813, 814, 815, 816, 817, 818, 819, 820, 821, 822, 823, 824, 825, 826, 827, 828, 829, 830, 831, 832, 833, 834, 835, 836, 837, 838, 839, 840, 841, 842, 843, 844, 845, 846, 847, 848, 849, 850, 851, 852, 853, 854, 855, 856, 857, 858, 859, 860, 861, 862, 863, 864, 865, 866, 867, 868, 869, 870, 871, 872, 873, 874, 875, 876, 877, 878, 879, 880, 881, 882, 883, 884, 885, 886, 887, 888, 889, 890, 891, 892, 893, 894, 895, 896, 897, 898, 899, 900, 901, 902, 903, 904, 905, 906, 907, 908, 909, 910, 911, 912, 913, 914, 915, 916, 917, 918, 919, 920, 921, 922, 923, 924, 925, 926, 927, 928, 929, 930, 931, 932, 933, 934, 935, 936, 937, 938, 939, 940, 941, 942, 943, 944, 945, 946, 947, 948, 949, 950, 951, 952, 953, 954, 955, 956, 957, 958, 959, 960, 961, 962, 963, 964, 965, 966, 967, 968, 969, 970, 971, 972, 973, 974, 975, 976, 977, 978, 979, 980, 981, 982, 983, 984, 985, 986, 987, 988, 989, 990, 991, 992, 993, 994, 995, 996, 997, 998, 999, 1000) OR \"users\".\"id\" IN (1001)
+ }
+ end
+ end
+
+ describe "Nodes::NotIn" do
+ it "should know how to visit" do
+ ary = (1 .. 1001).to_a
+ node = @attr.not_in ary
+ compile(node).must_be_like %{
+ "users"."id" NOT IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 256, 257, 258, 259, 260, 261, 262, 263, 264, 265, 266, 267, 268, 269, 270, 271, 272, 273, 274, 275, 276, 277, 278, 279, 280, 281, 282, 283, 284, 285, 286, 287, 288, 289, 290, 291, 292, 293, 294, 295, 296, 297, 298, 299, 300, 301, 302, 303, 304, 305, 306, 307, 308, 309, 310, 311, 312, 313, 314, 315, 316, 317, 318, 319, 320, 321, 322, 323, 324, 325, 326, 327, 328, 329, 330, 331, 332, 333, 334, 335, 336, 337, 338, 339, 340, 341, 342, 343, 344, 345, 346, 347, 348, 349, 350, 351, 352, 353, 354, 355, 356, 357, 358, 359, 360, 361, 362, 363, 364, 365, 366, 367, 368, 369, 370, 371, 372, 373, 374, 375, 376, 377, 378, 379, 380, 381, 382, 383, 384, 385, 386, 387, 388, 389, 390, 391, 392, 393, 394, 395, 396, 397, 398, 399, 400, 401, 402, 403, 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417, 418, 419, 420, 421, 422, 423, 424, 425, 426, 427, 428, 429, 430, 431, 432, 433, 434, 435, 436, 437, 438, 439, 440, 441, 442, 443, 444, 445, 446, 447, 448, 449, 450, 451, 452, 453, 454, 455, 456, 457, 458, 459, 460, 461, 462, 463, 464, 465, 466, 467, 468, 469, 470, 471, 472, 473, 474, 475, 476, 477, 478, 479, 480, 481, 482, 483, 484, 485, 486, 487, 488, 489, 490, 491, 492, 493, 494, 495, 496, 497, 498, 499, 500, 501, 502, 503, 504, 505, 506, 507, 508, 509, 510, 511, 512, 513, 514, 515, 516, 517, 518, 519, 520, 521, 522, 523, 524, 525, 526, 527, 528, 529, 530, 531, 532, 533, 534, 535, 536, 537, 538, 539, 540, 541, 542, 543, 544, 545, 546, 547, 548, 549, 550, 551, 552, 553, 554, 555, 556, 557, 558, 559, 560, 561, 562, 563, 564, 565, 566, 567, 568, 569, 570, 571, 572, 573, 574, 575, 576, 577, 578, 579, 580, 581, 582, 583, 584, 585, 586, 587, 588, 589, 590, 591, 592, 593, 594, 595, 596, 597, 598, 599, 600, 601, 602, 603, 604, 605, 606, 607, 608, 609, 610, 611, 612, 613, 614, 615, 616, 617, 618, 619, 620, 621, 622, 623, 624, 625, 626, 627, 628, 629, 630, 631, 632, 633, 634, 635, 636, 637, 638, 639, 640, 641, 642, 643, 644, 645, 646, 647, 648, 649, 650, 651, 652, 653, 654, 655, 656, 657, 658, 659, 660, 661, 662, 663, 664, 665, 666, 667, 668, 669, 670, 671, 672, 673, 674, 675, 676, 677, 678, 679, 680, 681, 682, 683, 684, 685, 686, 687, 688, 689, 690, 691, 692, 693, 694, 695, 696, 697, 698, 699, 700, 701, 702, 703, 704, 705, 706, 707, 708, 709, 710, 711, 712, 713, 714, 715, 716, 717, 718, 719, 720, 721, 722, 723, 724, 725, 726, 727, 728, 729, 730, 731, 732, 733, 734, 735, 736, 737, 738, 739, 740, 741, 742, 743, 744, 745, 746, 747, 748, 749, 750, 751, 752, 753, 754, 755, 756, 757, 758, 759, 760, 761, 762, 763, 764, 765, 766, 767, 768, 769, 770, 771, 772, 773, 774, 775, 776, 777, 778, 779, 780, 781, 782, 783, 784, 785, 786, 787, 788, 789, 790, 791, 792, 793, 794, 795, 796, 797, 798, 799, 800, 801, 802, 803, 804, 805, 806, 807, 808, 809, 810, 811, 812, 813, 814, 815, 816, 817, 818, 819, 820, 821, 822, 823, 824, 825, 826, 827, 828, 829, 830, 831, 832, 833, 834, 835, 836, 837, 838, 839, 840, 841, 842, 843, 844, 845, 846, 847, 848, 849, 850, 851, 852, 853, 854, 855, 856, 857, 858, 859, 860, 861, 862, 863, 864, 865, 866, 867, 868, 869, 870, 871, 872, 873, 874, 875, 876, 877, 878, 879, 880, 881, 882, 883, 884, 885, 886, 887, 888, 889, 890, 891, 892, 893, 894, 895, 896, 897, 898, 899, 900, 901, 902, 903, 904, 905, 906, 907, 908, 909, 910, 911, 912, 913, 914, 915, 916, 917, 918, 919, 920, 921, 922, 923, 924, 925, 926, 927, 928, 929, 930, 931, 932, 933, 934, 935, 936, 937, 938, 939, 940, 941, 942, 943, 944, 945, 946, 947, 948, 949, 950, 951, 952, 953, 954, 955, 956, 957, 958, 959, 960, 961, 962, 963, 964, 965, 966, 967, 968, 969, 970, 971, 972, 973, 974, 975, 976, 977, 978, 979, 980, 981, 982, 983, 984, 985, 986, 987, 988, 989, 990, 991, 992, 993, 994, 995, 996, 997, 998, 999, 1000) AND \"users\".\"id\" NOT IN (1001)
+ }
+ end
+ end
end
end
end
diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb
index cd9c8a5285..594d161fa3 100644
--- a/activerecord/test/cases/associations/eager_test.rb
+++ b/activerecord/test/cases/associations/eager_test.rb
@@ -1400,11 +1400,24 @@ class EagerAssociationTest < ActiveRecord::TestCase
assert_equal expected, FirstPost.unscoped.find(2)
end
- test "preload ignores the scoping" do
- assert_equal(
- Comment.find(1).post,
- Post.where("1 = 0").scoping { Comment.preload(:post).find(1).post }
- )
+ test "belongs_to association ignores the scoping" do
+ post = Comment.find(1).post
+
+ Post.where("1=0").scoping do
+ assert_equal post, Comment.find(1).post
+ assert_equal post, Comment.preload(:post).find(1).post
+ assert_equal post, Comment.eager_load(:post).find(1).post
+ end
+ end
+
+ test "has_many association ignores the scoping" do
+ comments = Post.find(1).comments.to_a
+
+ Comment.where("1=0").scoping do
+ assert_equal comments, Post.find(1).comments
+ assert_equal comments, Post.preload(:comments).find(1).comments
+ assert_equal comments, Post.eager_load(:comments).find(1).comments
+ end
end
test "deep preload" do
diff --git a/activerecord/test/cases/associations/has_many_through_associations_test.rb b/activerecord/test/cases/associations/has_many_through_associations_test.rb
index fa540c9dab..c13789f7ec 100644
--- a/activerecord/test/cases/associations/has_many_through_associations_test.rb
+++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb
@@ -33,6 +33,9 @@ require "models/organization"
require "models/user"
require "models/family"
require "models/family_tree"
+require "models/section"
+require "models/seminar"
+require "models/session"
class HasManyThroughAssociationsTest < ActiveRecord::TestCase
fixtures :posts, :readers, :people, :comments, :authors, :categories, :taggings, :tags,
@@ -1492,6 +1495,19 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
end
end
+ def test_circular_autosave_association_correctly_saves_multiple_records
+ cs180 = Seminar.new(name: "CS180")
+ fall = Session.new(name: "Fall")
+ sections = [
+ cs180.sections.build(short_name: "A"),
+ cs180.sections.build(short_name: "B"),
+ ]
+ fall.sections << sections
+ fall.save!
+ fall.reload
+ assert_equal sections, fall.sections.sort_by(&:id)
+ end
+
private
def make_model(name)
Class.new(ActiveRecord::Base) { define_singleton_method(:name) { name } }
diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb
index c4070fc341..16c2a3661d 100644
--- a/activerecord/test/cases/calculations_test.rb
+++ b/activerecord/test/cases/calculations_test.rb
@@ -363,6 +363,17 @@ class CalculationsTest < ActiveRecord::TestCase
assert_equal 60, c[2]
end
+ def test_should_calculate_grouped_with_longer_field
+ field = "a" * Account.connection.max_identifier_length
+
+ Account.update_all("#{field} = credit_limit")
+
+ c = Account.group(:firm_id).sum(field)
+ assert_equal 50, c[1]
+ assert_equal 105, c[6]
+ assert_equal 60, c[2]
+ end
+
def test_should_calculate_with_invalid_field
assert_equal 6, Account.calculate(:count, "*")
assert_equal 6, Account.calculate(:count, :all)
diff --git a/activerecord/test/cases/date_time_precision_test.rb b/activerecord/test/cases/date_time_precision_test.rb
index 9d1af9362d..79d63949ca 100644
--- a/activerecord/test/cases/date_time_precision_test.rb
+++ b/activerecord/test/cases/date_time_precision_test.rb
@@ -82,7 +82,7 @@ if subsecond_precision_supported?
end
def test_invalid_datetime_precision_raises_error
- assert_raises ActiveRecord::ActiveRecordError do
+ assert_raises ArgumentError do
@connection.create_table(:foos, force: true) do |t|
t.timestamps precision: 7
end
diff --git a/activerecord/test/cases/insert_all_test.rb b/activerecord/test/cases/insert_all_test.rb
index fc25701c80..f24c63031c 100644
--- a/activerecord/test/cases/insert_all_test.rb
+++ b/activerecord/test/cases/insert_all_test.rb
@@ -104,6 +104,44 @@ class InsertAllTest < ActiveRecord::TestCase
end
end
+ def test_insert_all_with_skip_duplicates_and_autonumber_id_not_given
+ skip unless supports_insert_on_duplicate_skip?
+
+ assert_difference "Book.count", 1 do
+ # These two books are duplicates according to an index on %i[author_id name]
+ # but their IDs are not specified so they will be assigned different IDs
+ # by autonumber. We will get an exception from MySQL if we attempt to skip
+ # one of these records by assigning its ID.
+ Book.insert_all [
+ { author_id: 8, name: "Refactoring" },
+ { author_id: 8, name: "Refactoring" }
+ ]
+ end
+ end
+
+ def test_insert_all_with_skip_duplicates_and_autonumber_id_given
+ skip unless supports_insert_on_duplicate_skip?
+
+ assert_difference "Book.count", 1 do
+ Book.insert_all [
+ { id: 200, author_id: 8, name: "Refactoring" },
+ { id: 201, author_id: 8, name: "Refactoring" }
+ ]
+ end
+ end
+
+ def test_skip_duplicates_strategy_does_not_secretly_upsert
+ skip unless supports_insert_on_duplicate_skip?
+
+ book = Book.create!(author_id: 8, name: "Refactoring", format: "EXPECTED")
+
+ assert_no_difference "Book.count" do
+ Book.insert(author_id: 8, name: "Refactoring", format: "UNEXPECTED")
+ end
+
+ assert_equal "EXPECTED", book.reload.format
+ end
+
def test_insert_all_will_raise_if_duplicates_are_skipped_only_for_a_certain_conflict_target
skip unless supports_insert_on_duplicate_skip? && supports_insert_conflict_target?
@@ -143,6 +181,42 @@ class InsertAllTest < ActiveRecord::TestCase
end
end
+ def test_insert_logs_message_including_model_name
+ skip unless supports_insert_conflict_target?
+
+ capture_log_output do |output|
+ Book.insert(name: "Rework", author_id: 1)
+ assert_match "Book Insert", output.string
+ end
+ end
+
+ def test_insert_all_logs_message_including_model_name
+ skip unless supports_insert_conflict_target?
+
+ capture_log_output do |output|
+ Book.insert_all [{ name: "Remote", author_id: 1 }, { name: "Renote", author_id: 1 }]
+ assert_match "Book Bulk Insert", output.string
+ end
+ end
+
+ def test_upsert_logs_message_including_model_name
+ skip unless supports_insert_on_duplicate_update?
+
+ capture_log_output do |output|
+ Book.upsert(name: "Remote", author_id: 1)
+ assert_match "Book Upsert", output.string
+ end
+ end
+
+ def test_upsert_all_logs_message_including_model_name
+ skip unless supports_insert_on_duplicate_update?
+
+ capture_log_output do |output|
+ Book.upsert_all [{ name: "Remote", author_id: 1 }, { name: "Renote", author_id: 1 }]
+ assert_match "Book Bulk Upsert", output.string
+ end
+ end
+
def test_upsert_all_updates_existing_records
skip unless supports_insert_on_duplicate_update?
@@ -186,4 +260,17 @@ class InsertAllTest < ActiveRecord::TestCase
Book.insert_all! [{ unknown_attribute: "Test" }]
end
end
+
+ private
+
+ def capture_log_output
+ output = StringIO.new
+ old_logger, ActiveRecord::Base.logger = ActiveRecord::Base.logger, ActiveSupport::Logger.new(output)
+
+ begin
+ yield output
+ ensure
+ ActiveRecord::Base.logger = old_logger
+ end
+ end
end
diff --git a/activerecord/test/cases/migration/column_attributes_test.rb b/activerecord/test/cases/migration/column_attributes_test.rb
index 6f9190c110..b6064500ee 100644
--- a/activerecord/test/cases/migration/column_attributes_test.rb
+++ b/activerecord/test/cases/migration/column_attributes_test.rb
@@ -176,9 +176,9 @@ module ActiveRecord
if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter)
def test_out_of_range_limit_should_raise
- assert_raise(ActiveRecordError) { add_column :test_models, :integer_too_big, :integer, limit: 10 }
- assert_raise(ActiveRecordError) { add_column :test_models, :text_too_big, :text, limit: 0xfffffffff }
- assert_raise(ActiveRecordError) { add_column :test_models, :binary_too_big, :binary, limit: 0xfffffffff }
+ assert_raise(ArgumentError) { add_column :test_models, :integer_too_big, :integer, limit: 10 }
+ assert_raise(ArgumentError) { add_column :test_models, :text_too_big, :text, limit: 0xfffffffff }
+ assert_raise(ArgumentError) { add_column :test_models, :binary_too_big, :binary, limit: 0xfffffffff }
end
end
end
diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb
index f10c26c308..8e8ed494d9 100644
--- a/activerecord/test/cases/migration_test.rb
+++ b/activerecord/test/cases/migration_test.rb
@@ -583,7 +583,7 @@ class MigrationTest < ActiveRecord::TestCase
if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter)
def test_out_of_range_integer_limit_should_raise
- e = assert_raise(ActiveRecord::ActiveRecordError, "integer limit didn't raise") do
+ e = assert_raise(ArgumentError) do
Person.connection.create_table :test_integer_limits, force: true do |t|
t.column :bigone, :integer, limit: 10
end
@@ -595,7 +595,7 @@ class MigrationTest < ActiveRecord::TestCase
end
def test_out_of_range_text_limit_should_raise
- e = assert_raise(ActiveRecord::ActiveRecordError, "text limit didn't raise") do
+ e = assert_raise(ArgumentError) do
Person.connection.create_table :test_text_limits, force: true do |t|
t.text :bigtext, limit: 0xfffffffff
end
@@ -607,15 +607,15 @@ class MigrationTest < ActiveRecord::TestCase
end
def test_out_of_range_binary_limit_should_raise
- e = assert_raise(ActiveRecord::ActiveRecordError) do
- Person.connection.create_table :test_text_limits, force: true do |t|
+ e = assert_raise(ArgumentError) do
+ Person.connection.create_table :test_binary_limits, force: true do |t|
t.binary :bigbinary, limit: 0xfffffffff
end
end
assert_includes e.message, "No binary type has byte size #{0xfffffffff}"
ensure
- Person.connection.drop_table :test_text_limits, if_exists: true
+ Person.connection.drop_table :test_binary_limits, if_exists: true
end
end
diff --git a/activerecord/test/cases/relation_test.rb b/activerecord/test/cases/relation_test.rb
index 00a7b3841f..3f370e5ede 100644
--- a/activerecord/test/cases/relation_test.rb
+++ b/activerecord/test/cases/relation_test.rb
@@ -101,6 +101,9 @@ module ActiveRecord
relation.merge!(relation)
assert_predicate relation, :empty_scope?
+
+ assert_not_predicate NullPost.all, :empty_scope?
+ assert_not_predicate FirstPost.all, :empty_scope?
end
def test_bad_constants_raise_errors
@@ -289,6 +292,7 @@ module ActiveRecord
klass.create!(description: "foo")
assert_equal ["foo"], klass.select(:description).from(klass.all).map(&:desc)
+ assert_equal ["foo"], klass.reselect(:description).from(klass.all).map(&:desc)
end
def test_relation_merging_with_merged_joins_as_strings
diff --git a/activerecord/test/cases/scoping/relation_scoping_test.rb b/activerecord/test/cases/scoping/relation_scoping_test.rb
index a95ab0f429..50b514d464 100644
--- a/activerecord/test/cases/scoping/relation_scoping_test.rb
+++ b/activerecord/test/cases/scoping/relation_scoping_test.rb
@@ -411,7 +411,19 @@ class HasManyScopingTest < ActiveRecord::TestCase
def test_nested_scope_finder
Comment.where("1=0").scoping do
- assert_equal 0, @welcome.comments.count
+ assert_equal 2, @welcome.comments.count
+ assert_equal "a comment...", @welcome.comments.what_are_you
+ end
+
+ Comment.where("1=1").scoping do
+ assert_equal 2, @welcome.comments.count
+ assert_equal "a comment...", @welcome.comments.what_are_you
+ end
+ end
+
+ def test_none_scoping
+ Comment.none.scoping do
+ assert_equal 2, @welcome.comments.count
assert_equal "a comment...", @welcome.comments.what_are_you
end
@@ -452,7 +464,19 @@ class HasAndBelongsToManyScopingTest < ActiveRecord::TestCase
def test_nested_scope_finder
Category.where("1=0").scoping do
- assert_equal 0, @welcome.categories.count
+ assert_equal 2, @welcome.categories.count
+ assert_equal "a category...", @welcome.categories.what_are_you
+ end
+
+ Category.where("1=1").scoping do
+ assert_equal 2, @welcome.categories.count
+ assert_equal "a category...", @welcome.categories.what_are_you
+ end
+ end
+
+ def test_none_scoping
+ Category.none.scoping do
+ assert_equal 2, @welcome.categories.count
assert_equal "a category...", @welcome.categories.what_are_you
end
diff --git a/activerecord/test/cases/time_precision_test.rb b/activerecord/test/cases/time_precision_test.rb
index 2f534ea110..1abd857216 100644
--- a/activerecord/test/cases/time_precision_test.rb
+++ b/activerecord/test/cases/time_precision_test.rb
@@ -75,7 +75,7 @@ if subsecond_precision_supported?
end
def test_invalid_time_precision_raises_error
- assert_raises ActiveRecord::ActiveRecordError do
+ assert_raises ArgumentError do
@connection.create_table(:foos, force: true) do |t|
t.time :start, precision: 7
t.time :finish, precision: 7
diff --git a/activerecord/test/cases/touch_later_test.rb b/activerecord/test/cases/touch_later_test.rb
index cd3d5ed7d1..f1a9cf2d05 100644
--- a/activerecord/test/cases/touch_later_test.rb
+++ b/activerecord/test/cases/touch_later_test.rb
@@ -10,7 +10,7 @@ require "models/tree"
class TouchLaterTest < ActiveRecord::TestCase
fixtures :nodes, :trees
- def test_touch_laster_raise_if_non_persisted
+ def test_touch_later_raise_if_non_persisted
invoice = Invoice.new
Invoice.transaction do
assert_not_predicate invoice, :persisted?
diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb
index 1009dd0f99..410f07d3ab 100644
--- a/activerecord/test/cases/transactions_test.rb
+++ b/activerecord/test/cases/transactions_test.rb
@@ -34,20 +34,21 @@ class TransactionTest < ActiveRecord::TestCase
end
}
- assert @first.reload
assert_not_predicate @first, :frozen?
end
def test_successful
- Topic.transaction do
- @first.approved = true
- @second.approved = false
- @first.save
- @second.save
+ assert_not_called(@first, :committed!) do
+ Topic.transaction do
+ @first.approved = true
+ @second.approved = false
+ @first.save
+ @second.save
+ end
end
- assert Topic.find(1).approved?, "First should have been approved"
- assert_not Topic.find(2).approved?, "Second should have been unapproved"
+ assert_predicate Topic.find(1), :approved?, "First should have been approved"
+ assert_not_predicate Topic.find(2), :approved?, "Second should have been unapproved"
end
def transaction_with_return
@@ -76,11 +77,13 @@ class TransactionTest < ActiveRecord::TestCase
end
end
- transaction_with_return
+ assert_not_called(@first, :committed!) do
+ transaction_with_return
+ end
assert committed
- assert Topic.find(1).approved?, "First should have been approved"
- assert_not Topic.find(2).approved?, "Second should have been unapproved"
+ assert_predicate Topic.find(1), :approved?, "First should have been approved"
+ assert_not_predicate Topic.find(2), :approved?, "Second should have been unapproved"
ensure
Topic.connection.class_eval do
remove_method :commit_db_transaction
@@ -99,9 +102,11 @@ class TransactionTest < ActiveRecord::TestCase
end
end
- Topic.transaction do
- @first.approved = true
- @first.save!
+ assert_not_called(@first, :committed!) do
+ Topic.transaction do
+ @first.approved = true
+ @first.save!
+ end
end
assert_equal 0, num
@@ -113,19 +118,21 @@ class TransactionTest < ActiveRecord::TestCase
end
def test_successful_with_instance_method
- @first.transaction do
- @first.approved = true
- @second.approved = false
- @first.save
- @second.save
+ assert_not_called(@first, :committed!) do
+ @first.transaction do
+ @first.approved = true
+ @second.approved = false
+ @first.save
+ @second.save
+ end
end
- assert Topic.find(1).approved?, "First should have been approved"
- assert_not Topic.find(2).approved?, "Second should have been unapproved"
+ assert_predicate Topic.find(1), :approved?, "First should have been approved"
+ assert_not_predicate Topic.find(2), :approved?, "Second should have been unapproved"
end
def test_failing_on_exception
- begin
+ assert_not_called(@first, :rolledback!) do
Topic.transaction do
@first.approved = true
@second.approved = false
@@ -137,11 +144,11 @@ class TransactionTest < ActiveRecord::TestCase
# caught it
end
- assert @first.approved?, "First should still be changed in the objects"
- assert_not @second.approved?, "Second should still be changed in the objects"
+ assert_predicate @first, :approved?, "First should still be changed in the objects"
+ assert_not_predicate @second, :approved?, "Second should still be changed in the objects"
- assert_not Topic.find(1).approved?, "First shouldn't have been approved"
- assert Topic.find(2).approved?, "Second should still be approved"
+ assert_not_predicate Topic.find(1), :approved?, "First shouldn't have been approved"
+ assert_predicate Topic.find(2), :approved?, "Second should still be approved"
end
def test_raising_exception_in_callback_rollbacks_in_save
@@ -150,8 +157,10 @@ class TransactionTest < ActiveRecord::TestCase
end
@first.approved = true
- e = assert_raises(RuntimeError) { @first.save }
- assert_equal "Make the transaction rollback", e.message
+ assert_not_called(@first, :rolledback!) do
+ e = assert_raises(RuntimeError) { @first.save }
+ assert_equal "Make the transaction rollback", e.message
+ end
assert_not_predicate Topic.find(1), :approved?
end
@@ -159,13 +168,15 @@ class TransactionTest < ActiveRecord::TestCase
def @first.before_save_for_transaction
raise ActiveRecord::Rollback
end
- assert_not @first.approved
+ assert_not_predicate @first, :approved?
- Topic.transaction do
- @first.approved = true
- @first.save!
+ assert_not_called(@first, :rolledback!) do
+ Topic.transaction do
+ @first.approved = true
+ @first.save!
+ end
end
- assert_not Topic.find(@first.id).approved?, "Should not commit the approved flag"
+ assert_not_predicate Topic.find(@first.id), :approved?, "Should not commit the approved flag"
end
def test_raising_exception_in_nested_transaction_restore_state_in_save
@@ -175,11 +186,13 @@ class TransactionTest < ActiveRecord::TestCase
raise "Make the transaction rollback"
end
- assert_raises(RuntimeError) do
- Topic.transaction { topic.save }
+ assert_not_called(topic, :rolledback!) do
+ assert_raises(RuntimeError) do
+ Topic.transaction { topic.save }
+ end
end
- assert topic.new_record?, "#{topic.inspect} should be new record"
+ assert_predicate topic, :new_record?, "#{topic.inspect} should be new record"
end
def test_transaction_state_is_cleared_when_record_is_persisted
diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb
index 61e5f14100..395b534c63 100644
--- a/activerecord/test/models/post.rb
+++ b/activerecord/test/models/post.rb
@@ -203,6 +203,10 @@ end
class SubAbstractStiPost < AbstractStiPost; end
+class NullPost < Post
+ default_scope { none }
+end
+
class FirstPost < ActiveRecord::Base
self.inheritance_column = :disabled
self.table_name = "posts"
diff --git a/activerecord/test/models/section.rb b/activerecord/test/models/section.rb
new file mode 100644
index 0000000000..f8b4cc7936
--- /dev/null
+++ b/activerecord/test/models/section.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+class Section < ActiveRecord::Base
+ belongs_to :session, inverse_of: :sections, autosave: true
+ belongs_to :seminar, inverse_of: :sections, autosave: true
+end
diff --git a/activerecord/test/models/seminar.rb b/activerecord/test/models/seminar.rb
new file mode 100644
index 0000000000..c18aa86433
--- /dev/null
+++ b/activerecord/test/models/seminar.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+class Seminar < ActiveRecord::Base
+ has_many :sections, inverse_of: :seminar, autosave: true, dependent: :destroy
+ has_many :sessions, through: :sections
+end
diff --git a/activerecord/test/models/session.rb b/activerecord/test/models/session.rb
new file mode 100644
index 0000000000..db66b5297e
--- /dev/null
+++ b/activerecord/test/models/session.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+class Session < ActiveRecord::Base
+ has_many :sections, inverse_of: :session, autosave: true, dependent: :destroy
+ has_many :seminars, through: :sections
+end
diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb
index ead4de2a13..7d9b8afeb6 100644
--- a/activerecord/test/schema/schema.rb
+++ b/activerecord/test/schema/schema.rb
@@ -19,6 +19,7 @@ ActiveRecord::Schema.define do
t.references :firm, index: false
t.string :firm_name
t.integer :credit_limit
+ t.integer "a" * max_identifier_length
end
create_table :admin_accounts, force: true do |t|
@@ -791,6 +792,24 @@ ActiveRecord::Schema.define do
t.integer :lock_version, default: 0
end
+ disable_referential_integrity do
+ create_table :seminars, force: :cascade do |t|
+ t.string :name
+ end
+
+ create_table :sessions, force: :cascade do |t|
+ t.date :start_date
+ t.date :end_date
+ t.string :name
+ end
+
+ create_table :sections, force: :cascade do |t|
+ t.string :short_name
+ t.belongs_to :session, foreign_key: true
+ t.belongs_to :seminar, foreign_key: true
+ end
+ end
+
create_table :shape_expressions, force: true do |t|
t.string :paint_type
t.integer :paint_id
diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md
index db9c7e96f3..57e03b5e12 100644
--- a/activesupport/CHANGELOG.md
+++ b/activesupport/CHANGELOG.md
@@ -1,3 +1,17 @@
+* The Zeitwerk compatibility interface for `ActiveSupport::Dependencies` no
+ longer implements `autoloaded_constants` or `autoloaded?` (undocumented,
+ anyway). Experience shows introspection does not have many use cases, and
+ troubleshooting is done by logging. With this design trade-off we are able
+ to use even less memory in all environments.
+
+ *Xavier Noria*
+
+* Depends on Zeitwerk 2, which stores less metadata if reloading is disabled
+ and hence uses less memory when `config.cache_classes` is `true`, a standard
+ setup in production.
+
+ *Xavier Noria*
+
* In `:zeitwerk` mode, eager load directories in engines and applications only
if present in their respective `config.eager_load_paths`.
diff --git a/activesupport/activesupport.gemspec b/activesupport/activesupport.gemspec
index bf0fe0f76d..dd2340cdd3 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.4", ">= 1.4.3"
+ s.add_dependency "zeitwerk", "~> 2.1", ">= 2.1.2"
end
diff --git a/activesupport/lib/active_support/dependencies/zeitwerk_integration.rb b/activesupport/lib/active_support/dependencies/zeitwerk_integration.rb
index a43d03cf09..f16a9f7e57 100644
--- a/activesupport/lib/active_support/dependencies/zeitwerk_integration.rb
+++ b/activesupport/lib/active_support/dependencies/zeitwerk_integration.rb
@@ -9,7 +9,11 @@ module ActiveSupport
module Decorations
def clear
Dependencies.unload_interlock do
- Rails.autoloaders.main.reload
+ begin
+ Rails.autoloaders.main.reload
+ rescue Zeitwerk::ReloadingDisabledError
+ raise "reloading is disabled because config.cache_classes is true"
+ end
end
end
@@ -21,17 +25,8 @@ module ActiveSupport
ActiveSupport::Inflector.safe_constantize(cpath)
end
- def autoloaded_constants
- cpaths = []
- Rails.autoloaders.each do |autoloader|
- cpaths.concat(autoloader.loaded_cpaths.to_a)
- end
- cpaths
- end
-
- def autoloaded?(object)
- cpath = object.is_a?(Module) ? object.name : object.to_s
- Rails.autoloaders.any? { |autoloader| autoloader.loaded?(cpath) }
+ def to_unload?(cpath)
+ Rails.autoloaders.main.to_unload?(cpath)
end
def verbose=(verbose)
@@ -51,15 +46,15 @@ module ActiveSupport
end
class << self
- def take_over
- setup_autoloaders
+ def take_over(enable_reloading:)
+ setup_autoloaders(enable_reloading)
freeze_paths
decorate_dependencies
end
private
- def setup_autoloaders
+ def setup_autoloaders(enable_reloading)
Dependencies.autoload_paths.each do |autoload_path|
# Zeitwerk only accepts existing directories in `push_dir` to
# prevent misconfigurations.
@@ -72,6 +67,7 @@ module ActiveSupport
autoloader.do_not_eager_load(autoload_path) unless eager_load?(autoload_path)
end
+ Rails.autoloaders.main.enable_reloading if enable_reloading
Rails.autoloaders.each(&:setup)
end
diff --git a/activesupport/lib/active_support/descendants_tracker.rb b/activesupport/lib/active_support/descendants_tracker.rb
index 2dca990712..fe0c6991aa 100644
--- a/activesupport/lib/active_support/descendants_tracker.rb
+++ b/activesupport/lib/active_support/descendants_tracker.rb
@@ -22,11 +22,18 @@ module ActiveSupport
def clear
if defined? ActiveSupport::Dependencies
+ # to_unload? is only defined in Zeitwerk mode.
+ to_unload = if Dependencies.respond_to?(:to_unload?)
+ ->(klass) { Dependencies.to_unload?(klass.name) }
+ else
+ ->(klass) { Dependencies.autoloaded?(klass) }
+ end
+
@@direct_descendants.each do |klass, descendants|
- if ActiveSupport::Dependencies.autoloaded?(klass)
+ if to_unload[klass]
@@direct_descendants.delete(klass)
else
- descendants.reject! { |v| ActiveSupport::Dependencies.autoloaded?(v) }
+ descendants.reject! { |v| to_unload[v] }
end
end
else
diff --git a/guides/source/6_0_release_notes.md b/guides/source/6_0_release_notes.md
index a4edaa1180..f1f41dc8fc 100644
--- a/guides/source/6_0_release_notes.md
+++ b/guides/source/6_0_release_notes.md
@@ -211,10 +211,237 @@ Please refer to the [Changelog][active-record] for detailed changes.
### Removals
+* Remove deprecated `#set_state` from the transaction object.
+ ([Commit](https://github.com/rails/rails/commit/6c745b0c5152a4437163a67707e02f4464493983))
+
+* Remove deprecated `#supports_statement_cache?` from the database adapters.
+ ([Commit](https://github.com/rails/rails/commit/5f3ed8784383fb4eb0f9959f31a9c28a991b7553))
+
+* Remove deprecated `#insert_fixtures` from the database adapters.
+ ([Commit](https://github.com/rails/rails/commit/400ba786e1d154448235f5f90183e48a1043eece))
+
+* Remove deprecated `ActiveRecord::ConnectionAdapters::SQLite3Adapter#valid_alter_table_type?`.
+ ([Commit](https://github.com/rails/rails/commit/45b4d5f81f0c0ca72c18d0dea4a3a7b2ecc589bf))
+
+* Remove support for passing the column name to `sum` when a block is passed.
+ ([Commit](https://github.com/rails/rails/commit/91ddb30083430622188d76eb9f29b78131df67f9))
+
+* Remove support for passing the column name to `count` when a block is passed.
+ ([Commit](https://github.com/rails/rails/commit/67356f2034ab41305af7218f7c8b2fee2d614129))
+
+* Remove support for delegation of missing methods in a relation to arel.
+ ([Commit](https://github.com/rails/rails/commit/d97980a16d76ad190042b4d8578109714e9c53d0))
+
+* Remove support for delegating missing methods in a relation to private methods of the class.
+ ([Commit](https://github.com/rails/rails/commit/a7becf147afc85c354e5cfa519911a948d25fc4d))
+
+* Remove support for specifying a timestamp name for `#cache_key`.
+ ([Commit](https://github.com/rails/rails/commit/0bef23e630f62e38f20b5ae1d1d5dbfb087050ea))
+
+* Remove deprecated `ActiveRecord::Migrator.migrations_path=`.
+ ([Commit](https://github.com/rails/rails/commit/90d7842186591cae364fab3320b524e4d31a7d7d))
+
+* Remove deprecated `expand_hash_conditions_for_aggregates`.
+ ([Commit](https://github.com/rails/rails/commit/27b252d6a85e300c7236d034d55ec8e44f57a83e))
+
+
### Deprecations
+* Deprecate mismatched case-sensitivity collation comparisons for uniqueness validator.
+ ([Commit](https://github.com/rails/rails/commit/9def05385f1cfa41924bb93daa187615e88c95b9))
+
+* Deprecate using class level querying methods if the receiver scope has leaked.
+ ([Pull Request](https://github.com/rails/rails/pull/35280))
+
+* Deprecate `config.activerecord.sqlite3.represent_boolean_as_integer`.
+ ([Commit](https://github.com/rails/rails/commit/f59b08119bc0c01a00561d38279b124abc82561b))
+
+* Deprecate passing `migrations_paths` to `connection.assume_migrated_upto_version`.
+ ([Commit](https://github.com/rails/rails/commit/c1b14aded27e063ead32fa911aa53163d7cfc21a))
+
+* Deprecate `ActiveRecord::Result#to_hash` in favor of `ActiveRecord::Result#to_a`.
+ ([Commit](https://github.com/rails/rails/commit/16510d609c601aa7d466809f3073ec3313e08937))
+
+* Deprecate methods in `DatabaseLimits`: `column_name_length`, `table_name_length`,
+ `columns_per_table`, `indexes_per_table`, `columns_per_multicolumn_index`,
+ `sql_query_length`, and `joins_per_query`.
+ ([Commit](https://github.com/rails/rails/commit/e0a1235f7df0fa193c7e299a5adee88db246b44f))
+
+* Deprecate `update_attributes`/`!` in favor of `update`/`!`.
+ ([Commit](https://github.com/rails/rails/commit/5645149d3a27054450bd1130ff5715504638a5f5))
+
### Notable changes
+* Bump the minimum sqlite3 version to 1.4.
+ ([Pull Request](https://github.com/rails/rails/pull/35844))
+
+* Add `rails db:prepare` to create a database if it doesn't exist, and run its migrations.
+ ([Pull Request](https://github.com/rails/rails/pull/35768))
+
+* Add `after_save_commit` callback as shortcut for `after_commit :hook, on: [ :create, :update ]`.
+ ([Pull Request](https://github.com/rails/rails/pull/35804))
+
+* Add `ActiveRecord::Relation#extract_associated` for extracting associated records from a relation.
+ ([Pull Request](https://github.com/rails/rails/pull/35784))
+
+* Add `ActiveRecord::Relation#annotate` for adding SQL comments to ActiveRecord::Relation queries.
+ ([Pull Request](https://github.com/rails/rails/pull/35617))
+
+* Add support for setting Optimizer Hints on databases.
+ ([Pull Request](https://github.com/rails/rails/pull/35615))
+
+* Add `insert_all`/`insert_all!`/`upsert_all` methods for doing bulk inserts.
+ ([Pull Request](https://github.com/rails/rails/pull/35631))
+
+* Add `rails db:seed:replant` that truncates tables of each database
+ for ther current environment and loads the seeds.
+ ([Pull Request](https://github.com/rails/rails/pull/34779))
+
+* Add `reselect` method, which is a short-hand for `unscope(:select).select(fields)`.
+ ([Pull Request](https://github.com/rails/rails/pull/33611))
+
+* Add negative scopes for all enum values.
+ ([Pull Request](https://github.com/rails/rails/pull/35381))
+
+* Add `#destroy_by` and `#delete_by` for conditional removals.
+ ([Pull Request](https://github.com/rails/rails/pull/35316))
+
+* Add the ability to automatically switch database connections.
+ ([Pull Request](https://github.com/rails/rails/pull/35073))
+
+* Add the ability to prevent writes to a database for the duration of a block.
+ ([Pull Request](https://github.com/rails/rails/pull/34505))
+
+* Add an API for switching connections to support multiple databases.
+ ([Pull Request](https://github.com/rails/rails/pull/34052))
+
+* Make timestamps with precision the default for migrations.
+ ([Pull Request](https://github.com/rails/rails/pull/34970))
+
+* Support `:size` option to change text and blob size in MySQL.
+ ([Pull Request](https://github.com/rails/rails/pull/35071))
+
+* Set both the foreign key and the foreign type columns to NULL for
+ polymorphic associations on `dependent: :nullify` strategy.
+ ([Pull Request](https://github.com/rails/rails/pull/28078))
+
+* Allow a permitted instance of `ActionController::Parameters` to be passed as an
+ argument to `ActiveRecord::Relation#exists?`.
+ ([Pull Request](https://github.com/rails/rails/pull/34891))
+
+* Add support in `#where` for endless ranges introduced in Ruby 2.6.
+ ([Pull Request](https://github.com/rails/rails/pull/34906))
+
+* Make `ROW_FORMAT=DYNAMIC` a default create table option for MySQL.
+ ([Pull Request](https://github.com/rails/rails/pull/34742))
+
+* Add the ability to disable scopes generated by `ActiveRecord.enum`.
+ ([Pull Request](https://github.com/rails/rails/pull/34605/files))
+
+* Make implicit ordering configurable for a column.
+ ([Pull Request](https://github.com/rails/rails/pull/34480))
+
+* Bump the minimum PostgreSQL version to 9.3, dropping support for 9.1 and 9.2.
+ ([Pull Request](https://github.com/rails/rails/pull/34520))
+
+* Make the values of an enum frozen, raising an error when attempting to modify them.
+ ([Pull Request](https://github.com/rails/rails/pull/34517))
+
+* Make the SQL of `ActiveRecord::StatementInvalid` errors its own error property
+ and include SQL binds as a separate error property.
+ ([Pull Request](https://github.com/rails/rails/pull/34468))
+
+* Add an `:if_not_exists` option to `create_table`.
+ ([Pull Request](https://github.com/rails/rails/pull/31382))
+
+* Add support for multiple databases to `rails db:schema:cache:dump`
+ and `rails db:schema:cache:clear`.
+ ([Pull Request](https://github.com/rails/rails/pull/34181))
+
+* Add support for hash and url configs in database hash of `ActiveRecord::Base.connected_to`.
+ ([Pull Request](https://github.com/rails/rails/pull/34196))
+
+* Add support for default expressions and expression indexes for MySQL.
+ ([Pull Request](https://github.com/rails/rails/pull/34307))
+
+* Add an `index` option for `change_table` migration helpers.
+ ([Pull Request](https://github.com/rails/rails/pull/23593))
+
+* Fix `transaction` reverting for migrations. Previously, commands inside of a `transaction`
+ in a reverted migration ran uninverted. This change fixes that.
+ ([Pull Request](https://github.com/rails/rails/pull/31604))
+
+* Allow `ActiveRecord::Base.configurations=` to be set with a symbolized hash.
+ ([Pull Request](https://github.com/rails/rails/pull/33968))
+
+* Fix the counter cache to only update if the record is actually saved.
+ ([Pull Request](https://github.com/rails/rails/pull/33913))
+
+* Add expression indexes support for the SQLite adapter.
+ ([Pull Request](https://github.com/rails/rails/pull/33874))
+
+* Allow subclasses to redefine autosave callbacks for associated records.
+ ([Pull Request](https://github.com/rails/rails/pull/33378))
+
+* Bump the minimum MySQL version to 5.5.8.
+ ([Pull Request](https://github.com/rails/rails/pull/33853))
+
+* Use the utf8mb4 character set by default in MySQL.
+ ([Pull Request](https://github.com/rails/rails/pull/33608))
+
+* Add the ability to filter out sensitive data in `#inspect`
+ ([Pull Request](https://github.com/rails/rails/pull/33756), [Pull Request](https://github.com/rails/rails/pull/34208))
+
+* Change `ActiveRecord::Base.configurations` to return an object instead of a hash.
+ ([Pull Request](https://github.com/rails/rails/pull/33637))
+
+* Add database configuration to disable advisory locks.
+ ([Pull Request](https://github.com/rails/rails/pull/33691))
+
+* Update SQLite3 adapter `alter_table` method to restore foreign keys.
+ ([Pull Request](https://github.com/rails/rails/pull/33585))
+
+* Allow the `:to_table` option of `remove_foreign_key` to be invertible.
+ ([Pull Request](https://github.com/rails/rails/pull/33530))
+
+* Fix default value for mysql time types with specified precision.
+ ([Pull Request](https://github.com/rails/rails/pull/33280))
+
+* Fix the `touch` option to behave consistently with `Persistence#touch` method.
+ ([Pull Request](https://github.com/rails/rails/pull/33107))
+
+* Raise an exception for duplicate column definitions in Migrations.
+ ([Pull Request](https://github.com/rails/rails/pull/33029))
+
+* Bump the minimum SQLite version to 3.8.
+ ([Pull Request](https://github.com/rails/rails/pull/32923))
+
+* Fix parent records to not get saved with duplicate children records.
+ ([Pull Request](https://github.com/rails/rails/pull/32952))
+
+* Ensure `Associations::CollectionAssociation#size` and `Associations::CollectionAssociation#empty?`
+ use loaded association ids if present.
+ ([Pull Request](https://github.com/rails/rails/pull/32617))
+
+* Add support to preload associations of polymorphic associations when not all the records have the requested associations.
+ ([Commit](https://github.com/rails/rails/commit/75ef18c67c29b1b51314b6c8a963cee53394080b))
+
+* Add `touch_all` method to `ActiveRecord::Relation`.
+ ([Pull Request](https://github.com/rails/rails/pull/31513))
+
+* Add `ActiveRecord::Base.base_class?` predicate.
+ ([Pull Request](https://github.com/rails/rails/pull/32417))
+
+* Add custom prefix/suffix options to `ActiveRecord::Store.store_accessor`.
+ ([Pull Request](https://github.com/rails/rails/pull/32306))
+
+* Add `ActiveRecord::Base.create_or_find_by`/`!` to deal with the SELECT/INSERT race condition in
+ `ActiveRecord::Base.find_or_create_by`/`!` by leaning on unique constraints in the database.
+ ([Pull Request](https://github.com/rails/rails/pull/31989))
+
+* Add `Relation#pick` as short-hand for single-value plucks.
+ ([Pull Request](https://github.com/rails/rails/pull/31941))
+
Active Storage
--------------
diff --git a/guides/source/association_basics.md b/guides/source/association_basics.md
index 10538eff0f..a60ce7fb32 100644
--- a/guides/source/association_basics.md
+++ b/guides/source/association_basics.md
@@ -384,7 +384,7 @@ end
The corresponding migration might look like this:
```ruby
-class CreateSuppliers < ActiveRecord::Migration[5.0]
+class CreateSuppliers < ActiveRecord::Migration[5.2]
def change
create_table :suppliers do |t|
t.string :name
@@ -392,7 +392,7 @@ class CreateSuppliers < ActiveRecord::Migration[5.0]
end
create_table :accounts do |t|
- t.integer :supplier_id
+ t.bigint :supplier_id
t.string :account_number
t.timestamps
end
@@ -402,7 +402,7 @@ class CreateSuppliers < ActiveRecord::Migration[5.0]
end
```
-NOTE: Using `t.integer :supplier_id` makes the foreign key naming obvious and explicit. In current versions of Rails, you can abstract away this implementation detail by using `t.references :supplier` instead.
+NOTE: Using `t.bigint :supplier_id` makes the foreign key naming obvious and explicit. In current versions of Rails, you can abstract away this implementation detail by using `t.references :supplier` instead.
### Choosing Between `has_many :through` and `has_and_belongs_to_many`
@@ -466,11 +466,11 @@ Similarly, you can retrieve `@product.pictures`.
If you have an instance of the `Picture` model, you can get to its parent via `@picture.imageable`. To make this work, you need to declare both a foreign key column and a type column in the model that declares the polymorphic interface:
```ruby
-class CreatePictures < ActiveRecord::Migration[5.0]
+class CreatePictures < ActiveRecord::Migration[5.2]
def change
create_table :pictures do |t|
t.string :name
- t.integer :imageable_id
+ t.bigint :imageable_id
t.string :imageable_type
t.timestamps
end
@@ -619,11 +619,11 @@ end
These need to be backed up by a migration to create the `assemblies_parts` table. This table should be created without a primary key:
```ruby
-class CreateAssembliesPartsJoinTable < ActiveRecord::Migration[5.0]
+class CreateAssembliesPartsJoinTable < ActiveRecord::Migration[5.2]
def change
create_table :assemblies_parts, id: false do |t|
- t.integer :assembly_id
- t.integer :part_id
+ t.bigint :assembly_id
+ t.bigint :part_id
end
add_index :assemblies_parts, :assembly_id
diff --git a/guides/source/contributing_to_ruby_on_rails.md b/guides/source/contributing_to_ruby_on_rails.md
index 569f52652f..c33d523c0e 100644
--- a/guides/source/contributing_to_ruby_on_rails.md
+++ b/guides/source/contributing_to_ruby_on_rails.md
@@ -302,7 +302,7 @@ the recommended workflow with the [rails-dev-box](https://github.com/rails/rails
As a compromise, test what your code obviously affects, and if the change is
not in railties, run the whole test suite of the affected component. If all
tests are passing, that's enough to propose your contribution. We have
-[Travis CI](https://travis-ci.org/rails/rails) as a safety net for catching
+[Buildkite](https://buildkite.com/rails/rails) as a safety net for catching
unexpected breakages elsewhere.
#### Entire Rails:
@@ -418,7 +418,7 @@ To run a single test against all adapters, use:
$ bundle exec rake TEST=test/cases/associations/has_many_associations_test.rb
```
-You can invoke `test_jdbcmysql`, `test_jdbcsqlite3` or `test_jdbcpostgresql` also. See the file `activerecord/RUNNING_UNIT_TESTS.rdoc` for information on running more targeted database tests, or the file `ci/travis.rb` for the test suite run by the continuous integration server.
+You can invoke `test_jdbcmysql`, `test_jdbcsqlite3` or `test_jdbcpostgresql` also. See the file `activerecord/RUNNING_UNIT_TESTS.rdoc` for information on running more targeted database tests.
### Warnings
@@ -677,7 +677,7 @@ $ git apply ~/my_changes.patch
This works well for simple changes. However, if your changes are complicated or if the code in master has deviated significantly from your target branch, it might require more work on your part. The difficulty of a backport varies greatly from case to case, and sometimes it is simply not worth the effort.
-Once you have resolved all conflicts and made sure all the tests are passing, push your changes and open a separate pull request for your backport. It is also worth noting that older branches might have a different set of build targets than master. When possible, it is best to first test your backport locally against the Ruby versions listed in `.travis.yml` before submitting your pull request.
+Once you have resolved all conflicts and made sure all the tests are passing, push your changes and open a separate pull request for your backport. It is also worth noting that older branches might have a different set of build targets than master. When possible, it is best to first test your backport locally against the oldest Ruby version permitted by the target branch's `rails.gemspec` before submitting your pull request.
And then... think about your next contribution!
diff --git a/guides/source/debugging_rails_applications.md b/guides/source/debugging_rails_applications.md
index 77513c3a84..170c22905b 100644
--- a/guides/source/debugging_rails_applications.md
+++ b/guides/source/debugging_rails_applications.md
@@ -962,15 +962,8 @@ Plugins for Debugging
There are some Rails plugins to help you to find errors and debug your
application. Here is a list of useful plugins for debugging:
-* [Footnotes](https://github.com/josevalim/rails-footnotes) Every Rails page has
-footnotes that give request information and link back to your source via
-TextMate.
* [Query Trace](https://github.com/ruckus/active-record-query-trace/tree/master) Adds query
origin tracing to your logs.
-* [Query Reviewer](https://github.com/nesquena/query_reviewer) This Rails plugin
-not only runs "EXPLAIN" before each of your select queries in development, but
-provides a small DIV in the rendered output of each page with the summary of
-warnings for each query that it analyzed.
* [Exception Notifier](https://github.com/smartinez87/exception_notification/tree/master)
Provides a mailer object and a default set of templates for sending email
notifications when errors occur in a Rails application.
diff --git a/guides/source/engines.md b/guides/source/engines.md
index 0b17137270..84a49bec9b 100644
--- a/guides/source/engines.md
+++ b/guides/source/engines.md
@@ -217,8 +217,8 @@ important parts about namespacing, and is discussed later in the
#### `app` Directory
Inside the `app` directory are the standard `assets`, `controllers`, `helpers`,
-`mailers`, `models` and `views` directories that you should be familiar with
-from an application. The `helpers`, `mailers` and `models` directories are
+`jobs`, `mailers`, `models`, and `views` directories that you should be familiar with
+from an application. The `helpers`, `mailers`, and `models` directories are
empty, so they aren't described in this section. We'll look more into models in
a future section, when we're writing the engine.
@@ -347,14 +347,11 @@ create app/views/blorgh/articles/new.html.erb
create app/views/blorgh/articles/_form.html.erb
invoke test_unit
create test/controllers/blorgh/articles_controller_test.rb
+create test/system/blorgh/articles_test.rb
invoke helper
create app/helpers/blorgh/articles_helper.rb
-invoke test_unit
-create test/application_system_test_case.rb
-create test/system/articles_test.rb
+invoke test_unit
invoke assets
-invoke js
-create app/assets/javascripts/blorgh/articles.js
invoke css
create app/assets/stylesheets/blorgh/articles.css
invoke css
@@ -394,9 +391,8 @@ be isolated from those routes that are within the application. The
Next, the `scaffold_controller` generator is invoked, generating a controller
called `Blorgh::ArticlesController` (at
`app/controllers/blorgh/articles_controller.rb`) and its related views at
-`app/views/blorgh/articles`. This generator also generates a test for the
-controller (`test/controllers/blorgh/articles_controller_test.rb`) and a helper
-(`app/helpers/blorgh/articles_helper.rb`).
+`app/views/blorgh/articles`. This generator also generates tests for the
+controller (`test/controllers/blorgh/articles_controller_test.rb` and `test/system/blorgh/articles_test.rb`) and a helper (`app/helpers/blorgh/articles_helper.rb`).
Everything this generator has created is neatly namespaced. The controller's
class is defined within the `Blorgh` module:
@@ -425,10 +421,7 @@ end
This helps prevent conflicts with any other engine or application that may have
an article resource as well.
-Finally, the assets for this resource are generated in two files:
-`app/assets/javascripts/blorgh/articles.js` and
-`app/assets/stylesheets/blorgh/articles.css`. You'll see how to use these a little
-later.
+Finally, the assets for this resource are generated in one file: `app/assets/stylesheets/blorgh/articles.css`. You'll see how to use these a little later.
You can see what the engine has so far by running `rails db:migrate` at the root
of our engine to run the migration generated by the scaffold generator, and then
@@ -579,9 +572,8 @@ invoke test_unit
create test/controllers/blorgh/comments_controller_test.rb
invoke helper
create app/helpers/blorgh/comments_helper.rb
+invoke test_unit
invoke assets
-invoke js
-create app/assets/javascripts/blorgh/comments.js
invoke css
create app/assets/stylesheets/blorgh/comments.css
```
@@ -1137,10 +1129,11 @@ end
```
```ruby
-# Blorgh/app/models/article.rb
-
-class Article < ApplicationRecord
- has_many :comments
+# Blorgh/app/models/blorgh/article.rb
+module Blorgh
+ class Article < ApplicationRecord
+ has_many :comments
+ end
end
```
@@ -1158,12 +1151,13 @@ end
```
```ruby
-# Blorgh/app/models/article.rb
-
-class Article < ApplicationRecord
- has_many :comments
- def summary
- "#{title}"
+# Blorgh/app/models/blorgh/article.rb
+module Blorgh
+ class Article < ApplicationRecord
+ has_many :comments
+ def summary
+ "#{title}"
+ end
end
end
```
@@ -1195,10 +1189,11 @@ end
```
```ruby
-# Blorgh/app/models/article.rb
-
-class Article < ApplicationRecord
- include Blorgh::Concerns::Models::Article
+# Blorgh/app/models/blorgh/article.rb
+module Blorgh
+ class Article < ApplicationRecord
+ include Blorgh::Concerns::Models::Article
+ end
end
```
@@ -1401,7 +1396,7 @@ s.add_development_dependency "moo"
Both kinds of dependencies will be installed when `bundle install` is run inside
of the application. The development dependencies for the gem will only be used
-when the tests for the engine are running.
+when the development and tests for the engine are running.
Note that if you want to immediately require dependencies when the engine is
required, you should require them before the engine's initialization. For
@@ -1536,4 +1531,6 @@ These are the available configuration hooks. They do not hook into any particula
### Example
-`config.before_configuration { puts 'I am called before any initializers' }`
+```ruby
+config.before_configuration { puts 'I am called before any initializers' }
+```
diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md
index c5136b7ab0..ab400e5982 100644
--- a/railties/CHANGELOG.md
+++ b/railties/CHANGELOG.md
@@ -1,3 +1,7 @@
+* Only force `:async` ActiveJob adapter to `:inline` during seeding.
+
+ *BatedUrGonnaDie*
+
* The `connection` option of `rails dbconsole` command is deprecated in
favor of `database` option.
diff --git a/railties/Rakefile b/railties/Rakefile
index 4ae546b202..0f305ea332 100644
--- a/railties/Rakefile
+++ b/railties/Rakefile
@@ -11,6 +11,33 @@ task test: "test:isolated"
namespace :test do
task :isolated do
+ estimated_duration = {
+ "test/application/test_runner_test.rb" => 201,
+ "test/application/assets_test.rb" => 131,
+ "test/application/rake/migrations_test.rb" => 65,
+ "test/generators/scaffold_generator_test.rb" => 57,
+ "test/generators/plugin_test_runner_test.rb" => 57,
+ "test/application/test_test.rb" => 52,
+ "test/application/configuration_test.rb" => 49,
+ "test/generators/app_generator_test.rb" => 43,
+ "test/application/rake/dbs_test.rb" => 43,
+ "test/application/rake_test.rb" => 33,
+ "test/generators/plugin_generator_test.rb" => 30,
+ "test/railties/engine_test.rb" => 27,
+ "test/generators/scaffold_controller_generator_test.rb" => 23,
+ "test/railties/generators_test.rb" => 19,
+ "test/application/console_test.rb" => 16,
+ "test/engine/commands_test.rb" => 15,
+ "test/application/routing_test.rb" => 15,
+ "test/application/mailer_previews_test.rb" => 15,
+ "test/application/rake/multi_dbs_test.rb" => 13,
+ "test/application/asset_debugging_test.rb" => 12,
+ "test/application/bin_setup_test.rb" => 11,
+ "test/engine/test_test.rb" => 10,
+ "test/application/runner_test.rb" => 10,
+ }
+ estimated_duration.default = 1
+
dash_i = [
"test",
"lib",
@@ -39,13 +66,23 @@ namespace :test do
test_patterns = dirs.map { |dir| "test/#{dir}/*_test.rb" }
test_files = Dir[*test_patterns].select do |file|
!file.start_with?("test/fixtures/") && !file.start_with?("test/isolation/assets/")
- end.sort
+ end
if ENV["BUILDKITE_PARALLEL_JOB_COUNT"]
n = ENV["BUILDKITE_PARALLEL_JOB"].to_i
m = ENV["BUILDKITE_PARALLEL_JOB_COUNT"].to_i
- test_files = test_files.each_slice(m).map { |slice| slice[n] }.compact
+ buckets = Array.new(m) { [] }
+ allocations = Array.new(m) { 0 }
+ test_files.sort_by { |file| [-estimated_duration[file], file] }.each do |file|
+ idx = allocations.index(allocations.min)
+ buckets[idx] << file
+ allocations[idx] += estimated_duration[file]
+ end
+
+ puts "Running #{buckets[n].size} of #{test_files.size} test files, estimated duration #{allocations[n]}s"
+
+ test_files = buckets[n]
end
test_files.each do |file|
diff --git a/railties/lib/rails/application/finisher.rb b/railties/lib/rails/application/finisher.rb
index 8d2c13d2a8..1cf44a480c 100644
--- a/railties/lib/rails/application/finisher.rb
+++ b/railties/lib/rails/application/finisher.rb
@@ -24,7 +24,7 @@ module Rails
initializer :let_zeitwerk_take_over do
if config.autoloader == :zeitwerk
require "active_support/dependencies/zeitwerk_integration"
- ActiveSupport::Dependencies::ZeitwerkIntegration.take_over
+ ActiveSupport::Dependencies::ZeitwerkIntegration.take_over(enable_reloading: !config.cache_classes)
end
end
diff --git a/railties/lib/rails/engine.rb b/railties/lib/rails/engine.rb
index 778bbebe75..eb2f0e8fca 100644
--- a/railties/lib/rails/engine.rb
+++ b/railties/lib/rails/engine.rb
@@ -550,7 +550,13 @@ module Rails
# Blog::Engine.load_seed
def load_seed
seed_file = paths["db/seeds.rb"].existent.first
- with_inline_jobs { load(seed_file) } if seed_file
+ return unless seed_file
+
+ if config.active_job.queue_adapter == :async
+ with_inline_jobs { load(seed_file) }
+ else
+ load(seed_file)
+ end
end
# Add configured load paths to Ruby's load path, and remove duplicate entries.
diff --git a/railties/lib/rails/generators/rails/plugin/plugin_generator.rb b/railties/lib/rails/generators/rails/plugin/plugin_generator.rb
index 79a06648b5..895b3b2e92 100644
--- a/railties/lib/rails/generators/rails/plugin/plugin_generator.rb
+++ b/railties/lib/rails/generators/rails/plugin/plugin_generator.rb
@@ -144,17 +144,6 @@ task default: :test
end
end
- def javascripts
- return if options.skip_javascript?
-
- if mountable?
- template "rails/javascripts.js",
- "app/assets/javascripts/#{namespaced_name}/application.js"
- elsif full?
- empty_directory_with_keep_file "app/assets/javascripts/#{namespaced_name}"
- end
- end
-
def bin(force = false)
bin_file = engine? ? "bin/rails.tt" : "bin/test.tt"
template bin_file, force: force do |content|
@@ -236,10 +225,6 @@ task default: :test
build(:stylesheets) unless api?
end
- def create_javascript_files
- build(:javascripts) unless api?
- end
-
def create_bin_files
build(:bin)
end
diff --git a/railties/test/application/zeitwerk_integration_test.rb b/railties/test/application/zeitwerk_integration_test.rb
index dc6db429a9..40d06ee999 100644
--- a/railties/test/application/zeitwerk_integration_test.rb
+++ b/railties/test/application/zeitwerk_integration_test.rb
@@ -98,24 +98,35 @@ class ZeitwerkIntegrationTest < ActiveSupport::TestCase
assert_nil deps.safe_constantize("Admin")
end
- test "autoloaded_constants returns autoloaded constant paths" do
- app_file "app/models/admin/user.rb", "class Admin::User; end"
+ test "to_unload? says if a constant would be unloaded (main)" do
+ app_file "app/models/user.rb", "class User; end"
app_file "app/models/post.rb", "class Post; end"
boot
- assert Admin::User
- assert_equal ["Admin", "Admin::User"], deps.autoloaded_constants
+ assert Post
+ assert deps.to_unload?("Post")
+ assert_not deps.to_unload?("User")
end
- test "autoloaded? says if a constant has been autoloaded" do
+ test "to_unload? says if a constant would be unloaded (once)" do
+ add_to_config 'config.autoload_once_paths << "#{Rails.root}/extras"'
+ app_file "extras/foo.rb", "class Foo; end"
+ app_file "extras/bar.rb", "class Bar; end"
+ boot
+
+ assert Foo
+ assert_not deps.to_unload?("Foo")
+ assert_not deps.to_unload?("Bar")
+ end
+
+ test "to_unload? says if a constant would be unloaded (reloading disabled)" do
app_file "app/models/user.rb", "class User; end"
app_file "app/models/post.rb", "class Post; end"
- boot
+ boot("production")
assert Post
- assert deps.autoloaded?("Post")
- assert deps.autoloaded?(Post)
- assert_not deps.autoloaded?("User")
+ assert_not deps.to_unload?("Post")
+ assert_not deps.to_unload?("User")
end
test "eager loading loads the application code" do
@@ -131,6 +142,29 @@ class ZeitwerkIntegrationTest < ActiveSupport::TestCase
assert $zeitwerk_integration_test_post
end
+ test "reloading is enabled if config.cache_classes is false" do
+ boot
+
+ assert Rails.autoloaders.main.reloading_enabled?
+ assert_not Rails.autoloaders.once.reloading_enabled?
+ end
+
+ test "reloading is disabled if config.cache_classes is true" do
+ boot("production")
+
+ assert_not Rails.autoloaders.main.reloading_enabled?
+ assert_not Rails.autoloaders.once.reloading_enabled?
+ end
+
+ test "reloading raises if config.cache_classes is true" do
+ boot("production")
+
+ e = assert_raises(StandardError) do
+ deps.clear
+ end
+ assert_equal "reloading is disabled because config.cache_classes is true", e.message
+ end
+
test "eager loading loads code in engines" do
$test_blog_engine_eager_loaded = false
@@ -301,4 +335,46 @@ class ZeitwerkIntegrationTest < ActiveSupport::TestCase
assert_nil autoloader.logger
end
end
+
+ # This is here because to guarantee classic mode works as always, Zeitwerk
+ # integration does not touch anything in classic. The descendants tracker is a
+ # very small one-liner exception. We leave its main test suite untouched, and
+ # add some minimal safety net here.
+ #
+ # When time passes, things are going to be reorganized (famous last words).
+ test "descendants tracker" do
+ class ::ZeitwerkDTIntegrationTestRoot
+ extend ActiveSupport::DescendantsTracker
+ end
+ class ::ZeitwerkDTIntegrationTestChild < ::ZeitwerkDTIntegrationTestRoot; end
+ class ::ZeitwerkDTIntegrationTestGrandchild < ::ZeitwerkDTIntegrationTestChild; end
+
+ begin
+ app_file "app/models/user.rb", "class User < ZeitwerkDTIntegrationTestRoot; end"
+ app_file "app/models/post.rb", "class Post < ZeitwerkDTIntegrationTestRoot; end"
+ app_file "app/models/tutorial.rb", "class Tutorial < Post; end"
+ boot
+
+ assert User
+ assert Tutorial
+
+ direct_descendants = [ZeitwerkDTIntegrationTestChild, User, Post].to_set
+ assert_equal direct_descendants, ZeitwerkDTIntegrationTestRoot.direct_descendants.to_set
+
+ descendants = direct_descendants.merge([ZeitwerkDTIntegrationTestGrandchild, Tutorial])
+ assert_equal descendants, ZeitwerkDTIntegrationTestRoot.descendants.to_set
+
+ ActiveSupport::DescendantsTracker.clear
+
+ direct_descendants = [ZeitwerkDTIntegrationTestChild].to_set
+ assert_equal direct_descendants, ZeitwerkDTIntegrationTestRoot.direct_descendants.to_set
+
+ descendants = direct_descendants.merge([ZeitwerkDTIntegrationTestGrandchild])
+ assert_equal descendants, ZeitwerkDTIntegrationTestRoot.descendants.to_set
+ ensure
+ Object.send(:remove_const, :ZeitwerkDTIntegrationTestRoot)
+ Object.send(:remove_const, :ZeitwerkDTIntegrationTestChild)
+ Object.send(:remove_const, :ZeitwerkDTIntegrationTestGrandchild)
+ end
+ end
end
diff --git a/railties/test/generators/actions_test.rb b/railties/test/generators/actions_test.rb
index 44d4e92256..d913bb5438 100644
--- a/railties/test/generators/actions_test.rb
+++ b/railties/test/generators/actions_test.rb
@@ -200,7 +200,7 @@ class ActionsTest < Rails::Generators::TestCase
run_generator
action :environment do
- _ = "# This wont be added"# assignment to silence parse-time warning "unused literal ignored"
+ _ = "# This wont be added" # assignment to silence parse-time warning "unused literal ignored"
"# This will be added"
end
diff --git a/railties/test/railties/engine_test.rb b/railties/test/railties/engine_test.rb
index 69f6e34d58..fe5c62c07d 100644
--- a/railties/test/railties/engine_test.rb
+++ b/railties/test/railties/engine_test.rb
@@ -879,7 +879,7 @@ YAML
assert Bukkits::Engine.config.bukkits_seeds_loaded
end
- test "jobs are ran inline while loading seeds" do
+ test "jobs are ran inline while loading seeds with async adapter configured" do
app_file "db/seeds.rb", <<-RUBY
Rails.application.config.seed_queue_adapter = ActiveJob::Base.queue_adapter
RUBY
@@ -891,6 +891,19 @@ YAML
assert_instance_of ActiveJob::QueueAdapters::AsyncAdapter, ActiveJob::Base.queue_adapter
end
+ test "jobs are ran with original adapter while loading seeds with custom adapter configured" do
+ app_file "db/seeds.rb", <<-RUBY
+ Rails.application.config.seed_queue_adapter = ActiveJob::Base.queue_adapter
+ RUBY
+
+ boot_rails
+ Rails.application.config.active_job.queue_adapter = :delayed_job
+ Rails.application.load_seed
+
+ assert_instance_of ActiveJob::QueueAdapters::DelayedJobAdapter, Rails.application.config.seed_queue_adapter
+ assert_instance_of ActiveJob::QueueAdapters::DelayedJobAdapter, ActiveJob::Base.queue_adapter
+ end
+
test "skips nonexistent seed data" do
FileUtils.rm "#{app_path}/db/seeds.rb"
boot_rails