aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord/lib/active_record')
-rw-r--r--activerecord/lib/active_record/associations/association.rb2
-rw-r--r--activerecord/lib/active_record/associations/builder/association.rb32
-rw-r--r--activerecord/lib/active_record/associations/builder/belongs_to.rb7
-rw-r--r--activerecord/lib/active_record/associations/builder/collection_association.rb16
-rw-r--r--activerecord/lib/active_record/associations/builder/has_many.rb2
-rw-r--r--activerecord/lib/active_record/associations/builder/has_one.rb36
-rw-r--r--activerecord/lib/active_record/associations/builder/singular_association.rb2
-rw-r--r--activerecord/lib/active_record/associations/collection_proxy.rb2
-rw-r--r--activerecord/lib/active_record/associations/has_many_through_association.rb15
-rw-r--r--activerecord/lib/active_record/associations/join_dependency.rb19
-rw-r--r--activerecord/lib/active_record/associations/join_dependency/join_association.rb13
-rw-r--r--activerecord/lib/active_record/associations/preloader.rb17
-rw-r--r--activerecord/lib/active_record/associations/preloader/association.rb62
-rw-r--r--activerecord/lib/active_record/associations/preloader/through_association.rb76
-rw-r--r--activerecord/lib/active_record/attribute_methods.rb10
-rw-r--r--activerecord/lib/active_record/attribute_methods/before_type_cast.rb5
-rw-r--r--activerecord/lib/active_record/attribute_methods/dirty.rb61
-rw-r--r--activerecord/lib/active_record/attribute_methods/primary_key.rb22
-rw-r--r--activerecord/lib/active_record/attribute_methods/query.rb2
-rw-r--r--activerecord/lib/active_record/attribute_methods/read.rb12
-rw-r--r--activerecord/lib/active_record/attribute_methods/write.rb18
-rw-r--r--activerecord/lib/active_record/attributes.rb13
-rw-r--r--activerecord/lib/active_record/autosave_association.rb41
-rw-r--r--activerecord/lib/active_record/base.rb1
-rw-r--r--activerecord/lib/active_record/collection_cache_key.rb53
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb33
-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.rb9
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb3
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb6
-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.rb66
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/transaction.rb16
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_adapter.rb19
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb75
-rw-r--r--activerecord/lib/active_record/connection_adapters/column.rb30
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb20
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb8
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb15
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/type_metadata.rb16
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb8
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/column.rb47
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb8
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb68
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb50
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb20
-rw-r--r--activerecord/lib/active_record/connection_adapters/schema_cache.rb35
-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.rb86
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb118
-rw-r--r--activerecord/lib/active_record/connection_handling.rb27
-rw-r--r--activerecord/lib/active_record/core.rb30
-rw-r--r--activerecord/lib/active_record/database_configurations.rb26
-rw-r--r--activerecord/lib/active_record/database_configurations/hash_config.rb22
-rw-r--r--activerecord/lib/active_record/database_configurations/url_config.rb24
-rw-r--r--activerecord/lib/active_record/dynamic_matchers.rb6
-rw-r--r--activerecord/lib/active_record/gem_version.rb4
-rw-r--r--activerecord/lib/active_record/insert_all.rb81
-rw-r--r--activerecord/lib/active_record/integration.rb14
-rw-r--r--activerecord/lib/active_record/internal_metadata.rb6
-rw-r--r--activerecord/lib/active_record/locking/optimistic.rb7
-rw-r--r--activerecord/lib/active_record/log_subscriber.rb2
-rw-r--r--activerecord/lib/active_record/middleware/database_selector.rb6
-rw-r--r--activerecord/lib/active_record/migration.rb41
-rw-r--r--activerecord/lib/active_record/migration/command_recorder.rb25
-rw-r--r--activerecord/lib/active_record/migration/compatibility.rb15
-rw-r--r--activerecord/lib/active_record/persistence.rb270
-rw-r--r--activerecord/lib/active_record/querying.rb6
-rw-r--r--activerecord/lib/active_record/railties/databases.rake64
-rw-r--r--activerecord/lib/active_record/reflection.rb6
-rw-r--r--activerecord/lib/active_record/relation.rb122
-rw-r--r--activerecord/lib/active_record/relation/calculations.rb67
-rw-r--r--activerecord/lib/active_record/relation/delegation.rb5
-rw-r--r--activerecord/lib/active_record/relation/finder_methods.rb12
-rw-r--r--activerecord/lib/active_record/relation/merger.rb27
-rw-r--r--activerecord/lib/active_record/relation/query_methods.rb163
-rw-r--r--activerecord/lib/active_record/relation/where_clause.rb14
-rw-r--r--activerecord/lib/active_record/schema_dumper.rb5
-rw-r--r--activerecord/lib/active_record/schema_migration.rb2
-rw-r--r--activerecord/lib/active_record/scoping/named.rb2
-rw-r--r--activerecord/lib/active_record/store.rb48
-rw-r--r--activerecord/lib/active_record/table_metadata.rb22
-rw-r--r--activerecord/lib/active_record/tasks/database_tasks.rb20
-rw-r--r--activerecord/lib/active_record/touch_later.rb13
-rw-r--r--activerecord/lib/active_record/transactions.rb104
-rw-r--r--activerecord/lib/active_record/type_caster/connection.rb26
87 files changed, 1551 insertions, 1112 deletions
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/associations/builder/association.rb b/activerecord/lib/active_record/associations/builder/association.rb
index 7c69cd65ee..0c61094d6c 100644
--- a/activerecord/lib/active_record/associations/builder/association.rb
+++ b/activerecord/lib/active_record/associations/builder/association.rb
@@ -27,40 +27,32 @@ module ActiveRecord::Associations::Builder # :nodoc:
"Please choose a different association name."
end
- extension = define_extensions model, name, &block
- reflection = create_reflection model, name, scope, options, extension
+ reflection = create_reflection(model, name, scope, options, &block)
define_accessors model, reflection
define_callbacks model, reflection
define_validations model, reflection
reflection
end
- def self.create_reflection(model, name, scope, options, extension = nil)
+ def self.create_reflection(model, name, scope, options, &block)
raise ArgumentError, "association names must be a Symbol" unless name.kind_of?(Symbol)
validate_options(options)
- scope = build_scope(scope, extension)
+ extension = define_extensions(model, name, &block)
+ options[:extend] = [*options[:extend], extension] if extension
+
+ scope = build_scope(scope)
ActiveRecord::Reflection.create(macro, name, scope, options, model)
end
- def self.build_scope(scope, extension)
- new_scope = scope
-
+ def self.build_scope(scope)
if scope && scope.arity == 0
- new_scope = proc { instance_exec(&scope) }
- end
-
- if extension
- new_scope = wrap_scope new_scope, extension
+ proc { instance_exec(&scope) }
+ else
+ scope
end
-
- new_scope
- end
-
- def self.wrap_scope(scope, extension)
- scope
end
def self.macro
@@ -136,5 +128,9 @@ module ActiveRecord::Associations::Builder # :nodoc:
name = reflection.name
model.before_destroy lambda { |o| o.association(name).handle_dependency }
end
+
+ private_class_method :build_scope, :macro, :valid_options, :validate_options, :define_extensions,
+ :define_callbacks, :define_accessors, :define_readers, :define_writers, :define_validations,
+ :valid_dependent_options, :check_dependent_options, :add_destroy_callbacks
end
end
diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb
index fc00f1e900..321ccba918 100644
--- a/activerecord/lib/active_record/associations/builder/belongs_to.rb
+++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb
@@ -74,11 +74,11 @@ module ActiveRecord::Associations::Builder # :nodoc:
def self.add_touch_callbacks(model, reflection)
foreign_key = reflection.foreign_key
- n = reflection.name
+ name = reflection.name
touch = reflection.options[:touch]
callback = lambda { |changes_method| lambda { |record|
- BelongsTo.touch_record(record, record.send(changes_method), foreign_key, n, touch, belongs_to_touch_method)
+ BelongsTo.touch_record(record, record.send(changes_method), foreign_key, name, touch, belongs_to_touch_method)
}}
if reflection.counter_cache_column
@@ -123,5 +123,8 @@ module ActiveRecord::Associations::Builder # :nodoc:
model.validates_presence_of reflection.name, message: :required
end
end
+
+ private_class_method :macro, :valid_options, :valid_dependent_options, :define_callbacks, :define_validations,
+ :add_counter_cache_callbacks, :add_touch_callbacks, :add_default_callbacks, :add_destroy_callbacks
end
end
diff --git a/activerecord/lib/active_record/associations/builder/collection_association.rb b/activerecord/lib/active_record/associations/builder/collection_association.rb
index 5848cd9112..e78d25441b 100644
--- a/activerecord/lib/active_record/associations/builder/collection_association.rb
+++ b/activerecord/lib/active_record/associations/builder/collection_association.rb
@@ -22,9 +22,9 @@ module ActiveRecord::Associations::Builder # :nodoc:
def self.define_extensions(model, name, &block)
if block_given?
- extension_module_name = "#{model.name.demodulize}#{name.to_s.camelize}AssociationExtension"
+ extension_module_name = "#{name.to_s.camelize}AssociationExtension"
extension = Module.new(&block)
- model.module_parent.const_set(extension_module_name, extension)
+ model.const_set(extension_module_name, extension)
end
end
@@ -67,16 +67,6 @@ module ActiveRecord::Associations::Builder # :nodoc:
CODE
end
- def self.wrap_scope(scope, mod)
- if scope
- if scope.arity > 0
- proc { |owner| instance_exec(owner, &scope).extending(mod) }
- else
- proc { instance_exec(&scope).extending(mod) }
- end
- else
- proc { extending(mod) }
- end
- end
+ private_class_method :valid_options, :define_callback, :define_extensions, :define_readers, :define_writers
end
end
diff --git a/activerecord/lib/active_record/associations/builder/has_many.rb b/activerecord/lib/active_record/associations/builder/has_many.rb
index 5b9617bc6d..556e2988f5 100644
--- a/activerecord/lib/active_record/associations/builder/has_many.rb
+++ b/activerecord/lib/active_record/associations/builder/has_many.rb
@@ -13,5 +13,7 @@ module ActiveRecord::Associations::Builder # :nodoc:
def self.valid_dependent_options
[:destroy, :delete_all, :nullify, :restrict_with_error, :restrict_with_exception]
end
+
+ private_class_method :macro, :valid_options, :valid_dependent_options
end
end
diff --git a/activerecord/lib/active_record/associations/builder/has_one.rb b/activerecord/lib/active_record/associations/builder/has_one.rb
index bfb37d6eee..27ebe8cb71 100644
--- a/activerecord/lib/active_record/associations/builder/has_one.rb
+++ b/activerecord/lib/active_record/associations/builder/has_one.rb
@@ -7,7 +7,7 @@ module ActiveRecord::Associations::Builder # :nodoc:
end
def self.valid_options(options)
- valid = super + [:as]
+ valid = super + [:as, :touch]
valid += [:through, :source, :source_type] if options[:through]
valid
end
@@ -16,6 +16,11 @@ module ActiveRecord::Associations::Builder # :nodoc:
[:destroy, :delete, :nullify, :restrict_with_error, :restrict_with_exception]
end
+ def self.define_callbacks(model, reflection)
+ super
+ add_touch_callbacks(model, reflection) if reflection.options[:touch]
+ end
+
def self.add_destroy_callbacks(model, reflection)
super unless reflection.options[:through]
end
@@ -26,5 +31,34 @@ module ActiveRecord::Associations::Builder # :nodoc:
model.validates_presence_of reflection.name, message: :required
end
end
+
+ def self.touch_record(o, name, touch)
+ record = o.send name
+
+ return unless record && record.persisted?
+
+ if touch != true
+ record.touch(touch)
+ else
+ record.touch
+ end
+ end
+
+ def self.add_touch_callbacks(model, reflection)
+ name = reflection.name
+ touch = reflection.options[:touch]
+
+ callback = lambda { |record|
+ HasOne.touch_record(record, name, touch)
+ }
+
+ model.after_create callback, if: :saved_changes?
+ model.after_update callback, if: :saved_changes?
+ model.after_destroy callback
+ model.after_touch callback
+ end
+
+ private_class_method :macro, :valid_options, :valid_dependent_options, :add_destroy_callbacks,
+ :define_callbacks, :define_validations, :add_touch_callbacks
end
end
diff --git a/activerecord/lib/active_record/associations/builder/singular_association.rb b/activerecord/lib/active_record/associations/builder/singular_association.rb
index 0a02ef4cc1..0e22563b41 100644
--- a/activerecord/lib/active_record/associations/builder/singular_association.rb
+++ b/activerecord/lib/active_record/associations/builder/singular_association.rb
@@ -38,5 +38,7 @@ module ActiveRecord::Associations::Builder # :nodoc:
end
CODE
end
+
+ private_class_method :valid_options, :define_accessors, :define_constructors
end
end
diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb
index 105813cb2f..797d647900 100644
--- a/activerecord/lib/active_record/associations/collection_proxy.rb
+++ b/activerecord/lib/active_record/associations/collection_proxy.rb
@@ -1002,7 +1002,7 @@ module ActiveRecord
end
# Adds one or more +records+ to the collection by setting their foreign keys
- # to the association's primary key. Since +<<+ flattens its argument list and
+ # to the association's primary key. Since <tt><<</tt> flattens its argument list and
# inserts each record, +push+ and +concat+ behave identically. Returns +self+
# so several appends may be chained together.
#
diff --git a/activerecord/lib/active_record/associations/has_many_through_association.rb b/activerecord/lib/active_record/associations/has_many_through_association.rb
index 84a9797aa5..0d384950fe 100644
--- a/activerecord/lib/active_record/associations/has_many_through_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_through_association.rb
@@ -57,21 +57,14 @@ module ActiveRecord
@through_records[record.object_id] ||= begin
ensure_mutable
- through_record = through_association.build(*options_for_through_record)
- through_record.send("#{source_reflection.name}=", record)
+ attributes = through_scope_attributes
+ attributes[source_reflection.name] = record
+ attributes[source_reflection.foreign_type] = options[:source_type] if options[:source_type]
- if options[:source_type]
- through_record.send("#{source_reflection.foreign_type}=", options[:source_type])
- end
-
- through_record
+ through_association.build(attributes)
end
end
- def options_for_through_record
- [through_scope_attributes]
- end
-
def through_scope_attributes
scope.where_values_hash(through_association.reflection.name.to_s).
except!(through_association.reflection.foreign_key,
diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb
index b76005b587..f35a40fb2f 100644
--- a/activerecord/lib/active_record/associations/join_dependency.rb
+++ b/activerecord/lib/active_record/associations/join_dependency.rb
@@ -64,16 +64,17 @@ module ActiveRecord
end
end
- def initialize(base, table, associations)
+ def initialize(base, table, associations, join_type)
tree = self.class.make_tree associations
@join_root = JoinBase.new(base, table, build(tree, base))
+ @join_type = join_type
end
def reflections
join_root.drop(1).map!(&:reflection)
end
- def join_constraints(joins_to_add, join_type, alias_tracker)
+ def join_constraints(joins_to_add, alias_tracker)
@alias_tracker = alias_tracker
construct_tables!(join_root)
@@ -82,9 +83,9 @@ module ActiveRecord
joins.concat joins_to_add.flat_map { |oj|
construct_tables!(oj.join_root)
if join_root.match? oj.join_root
- walk join_root, oj.join_root
+ walk(join_root, oj.join_root, oj.join_type)
else
- make_join_constraints(oj.join_root, join_type)
+ make_join_constraints(oj.join_root, oj.join_type)
end
}
end
@@ -125,7 +126,7 @@ module ActiveRecord
end
protected
- attr_reader :join_root
+ attr_reader :join_root, :join_type
private
attr_reader :alias_tracker
@@ -151,7 +152,7 @@ module ActiveRecord
end
end
- def make_constraints(parent, child, join_type = Arel::Nodes::OuterJoin)
+ def make_constraints(parent, child, join_type)
foreign_table = parent.table
foreign_klass = parent.base_klass
joins = child.join_constraints(foreign_table, foreign_klass, join_type, alias_tracker)
@@ -173,13 +174,13 @@ module ActiveRecord
join ? "#{name}_join" : name
end
- def walk(left, right)
+ def walk(left, right, join_type)
intersection, missing = right.children.map { |node1|
[left.children.find { |node2| node1.match? node2 }, node1]
}.partition(&:first)
- joins = intersection.flat_map { |l, r| r.table = l.table; walk(l, r) }
- joins.concat missing.flat_map { |_, n| make_constraints(left, n) }
+ joins = intersection.flat_map { |l, r| r.table = l.table; walk(l, r, join_type) }
+ joins.concat missing.flat_map { |_, n| make_constraints(left, n, join_type) }
end
def find_reflection(klass, name)
diff --git a/activerecord/lib/active_record/associations/join_dependency/join_association.rb b/activerecord/lib/active_record/associations/join_dependency/join_association.rb
index ca0305abbb..6a7e92dc28 100644
--- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb
+++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb
@@ -44,8 +44,7 @@ module ActiveRecord
unless others.empty?
joins.concat arel.join_sources
- right = joins.last.right
- right.expr.children.concat(others)
+ append_constraints(joins.last, others)
end
# The current table in this iteration becomes the foreign table in the next
@@ -65,6 +64,16 @@ module ActiveRecord
@readonly = reflection.scope && reflection.scope_for(base_klass.unscoped).readonly_value
end
+
+ private
+ def append_constraints(join, constraints)
+ if join.is_a?(Arel::Nodes::StringJoin)
+ join_string = table.create_and(constraints.unshift(join.left))
+ join.left = Arel.sql(base_klass.connection.visitor.compile(join_string))
+ else
+ join.right.expr.children.concat(constraints)
+ end
+ end
end
end
end
diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb
index 8997579527..6b57e5093a 100644
--- a/activerecord/lib/active_record/associations/preloader.rb
+++ b/activerecord/lib/active_record/associations/preloader.rb
@@ -143,16 +143,13 @@ module ActiveRecord
def preloaders_for_reflection(reflection, records, scope)
records.group_by { |record| record.association(reflection.name).klass }.map do |rhs_klass, rs|
- loader = preloader_for(reflection, rs).new(rhs_klass, rs, reflection, scope)
- loader.run self
- loader
+ preloader_for(reflection, rs).new(rhs_klass, rs, reflection, scope).run
end
end
def grouped_records(association, records, polymorphic_parent)
h = {}
records.each do |record|
- next unless record
reflection = record.class._reflect_on_association(association)
next if polymorphic_parent && !reflection || !record.association(association).klass
(h[reflection] ||= []) << record
@@ -166,10 +163,18 @@ module ActiveRecord
@reflection = reflection
end
- def run(preloader); end
+ def run
+ self
+ end
def preloaded_records
- owners.flat_map { |owner| owner.association(reflection.name).target }
+ @preloaded_records ||= records_by_owner.flat_map(&:last)
+ end
+
+ def records_by_owner
+ @records_by_owner ||= owners.each_with_object({}) do |owner, result|
+ result[owner] = Array(owner.association(reflection.name).target)
+ end
end
private
diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb
index 7048ff43b8..342d9e7a5a 100644
--- a/activerecord/lib/active_record/associations/preloader/association.rb
+++ b/activerecord/lib/active_record/associations/preloader/association.rb
@@ -4,29 +4,41 @@ module ActiveRecord
module Associations
class Preloader
class Association #:nodoc:
- attr_reader :preloaded_records
-
def initialize(klass, owners, reflection, preload_scope)
@klass = klass
@owners = owners
@reflection = reflection
@preload_scope = preload_scope
@model = owners.first && owners.first.class
- @preloaded_records = []
end
- def run(preloader)
- records = load_records do |record|
- owner = owners_by_key[convert_key(record[association_key_name])]
- association = owner.association(reflection.name)
- association.set_inverse_instance(record)
+ def run
+ if !preload_scope || preload_scope.empty_scope?
+ owners.each do |owner|
+ associate_records_to_owner(owner, records_by_owner[owner] || [])
+ end
+ else
+ # Custom preload scope is used and
+ # the association can not be marked as loaded
+ # Loading into a Hash instead
+ records_by_owner
end
+ self
+ end
- owners.each do |owner|
- associate_records_to_owner(owner, records[convert_key(owner[owner_key_name])] || [])
+ def records_by_owner
+ @records_by_owner ||= preloaded_records.each_with_object({}) do |record, result|
+ owners_by_key[convert_key(record[association_key_name])].each do |owner|
+ (result[owner] ||= []) << record
+ end
end
end
+ def preloaded_records
+ return @preloaded_records if defined?(@preloaded_records)
+ @preloaded_records = owner_keys.empty? ? [] : records_for(owner_keys)
+ end
+
private
attr_reader :owners, :reflection, :preload_scope, :model, :klass
@@ -54,13 +66,10 @@ module ActiveRecord
end
def owners_by_key
- unless defined?(@owners_by_key)
- @owners_by_key = owners.each_with_object({}) do |owner, h|
- key = convert_key(owner[owner_key_name])
- h[key] = owner if key
- end
+ @owners_by_key ||= owners.each_with_object({}) do |owner, result|
+ key = convert_key(owner[owner_key_name])
+ (result[key] ||= []) << owner if key
end
- @owners_by_key
end
def key_conversion_required?
@@ -87,23 +96,16 @@ module ActiveRecord
@model.type_for_attribute(owner_key_name).type
end
- def load_records(&block)
- return {} if owner_keys.empty?
- # Some databases impose a limit on the number of ids in a list (in Oracle it's 1000)
- # Make several smaller queries if necessary or make one query if the adapter supports it
- slices = owner_keys.each_slice(klass.connection.in_clause_length || owner_keys.size)
- @preloaded_records = slices.flat_map do |slice|
- records_for(slice, &block)
- end
- @preloaded_records.group_by do |record|
- convert_key(record[association_key_name])
+ def records_for(ids)
+ scope.where(association_key_name => ids).load do |record|
+ # Processing only the first owner
+ # because the record is modified but not an owner
+ owner = owners_by_key[convert_key(record[association_key_name])].first
+ association = owner.association(reflection.name)
+ association.set_inverse_instance(record)
end
end
- def records_for(ids, &block)
- scope.where(association_key_name => ids).load(&block)
- end
-
def scope
@scope ||= build_scope
end
diff --git a/activerecord/lib/active_record/associations/preloader/through_association.rb b/activerecord/lib/active_record/associations/preloader/through_association.rb
index 32653956b2..bec1c4c94a 100644
--- a/activerecord/lib/active_record/associations/preloader/through_association.rb
+++ b/activerecord/lib/active_record/associations/preloader/through_association.rb
@@ -4,45 +4,57 @@ module ActiveRecord
module Associations
class Preloader
class ThroughAssociation < Association # :nodoc:
- def run(preloader)
- already_loaded = owners.first.association(through_reflection.name).loaded?
- through_scope = through_scope()
- through_preloaders = preloader.preload(owners, through_reflection.name, through_scope)
- middle_records = through_preloaders.flat_map(&:preloaded_records)
- preloaders = preloader.preload(middle_records, source_reflection.name, scope)
- @preloaded_records = preloaders.flat_map(&:preloaded_records)
-
- owners.each do |owner|
- through_records = Array(owner.association(through_reflection.name).target)
-
- if already_loaded
+ PRELOADER = ActiveRecord::Associations::Preloader.new
+
+ def initialize(*)
+ super
+ @already_loaded = owners.first.association(through_reflection.name).loaded?
+ end
+
+ def preloaded_records
+ @preloaded_records ||= source_preloaders.flat_map(&:preloaded_records)
+ end
+
+ def records_by_owner
+ return @records_by_owner if defined?(@records_by_owner)
+ source_records_by_owner = source_preloaders.map(&:records_by_owner).reduce(:merge)
+ through_records_by_owner = through_preloaders.map(&:records_by_owner).reduce(:merge)
+
+ @records_by_owner = owners.each_with_object({}) do |owner, result|
+ through_records = through_records_by_owner[owner] || []
+
+ if @already_loaded
if source_type = reflection.options[:source_type]
through_records = through_records.select do |record|
record[reflection.foreign_type] == source_type
end
end
- else
- owner.association(through_reflection.name).reset if through_scope
end
- result = through_records.flat_map do |record|
- record.association(source_reflection.name).target
+ records = through_records.flat_map do |record|
+ source_records_by_owner[record]
end
- result.compact!
- result.sort_by! { |rhs| preload_index[rhs] } if scope.order_values.any?
- result.uniq! if scope.distinct_value
- associate_records_to_owner(owner, result)
- end
-
- unless scope.empty_scope?
- middle_records.each do |owner|
- owner.association(source_reflection.name).reset if owner
- end
+ records.compact!
+ records.sort_by! { |rhs| preload_index[rhs] } if scope.order_values.any?
+ records.uniq! if scope.distinct_value
+ result[owner] = records
end
end
private
+ def source_preloaders
+ @source_preloaders ||= PRELOADER.preload(middle_records, source_reflection.name, scope)
+ end
+
+ def middle_records
+ through_preloaders.flat_map(&:preloaded_records)
+ end
+
+ def through_preloaders
+ @through_preloaders ||= PRELOADER.preload(owners, through_reflection.name, through_scope)
+ end
+
def through_reflection
reflection.through_reflection
end
@@ -52,8 +64,8 @@ module ActiveRecord
end
def preload_index
- @preload_index ||= @preloaded_records.each_with_object({}).with_index do |(id, result), index|
- result[id] = index
+ @preload_index ||= preloaded_records.each_with_object({}).with_index do |(record, result), index|
+ result[record] = index
end
end
@@ -61,11 +73,15 @@ module ActiveRecord
scope = through_reflection.klass.unscoped
options = reflection.options
+ values = reflection_scope.values
+ if annotations = values[:annotate]
+ scope.annotate!(*annotations)
+ end
+
if options[:source_type]
scope.where! reflection.foreign_type => options[:source_type]
elsif !reflection_scope.where_clause.empty?
scope.where_clause = reflection_scope.where_clause
- values = reflection_scope.values
if includes = values[:includes]
scope.includes!(source_reflection.name => includes)
@@ -92,7 +108,7 @@ module ActiveRecord
end
end
- scope unless scope.empty_scope?
+ scope
end
end
end
diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb
index af7e46e649..220043c061 100644
--- a/activerecord/lib/active_record/attribute_methods.rb
+++ b/activerecord/lib/active_record/attribute_methods.rb
@@ -24,7 +24,7 @@ module ActiveRecord
RESTRICTED_CLASS_METHODS = %w(private public protected allocate new name parent superclass)
- class GeneratedAttributeMethodsBuilder < Module #:nodoc:
+ class GeneratedAttributeMethods < Module #:nodoc:
include Mutex_m
end
@@ -35,7 +35,7 @@ module ActiveRecord
end
def initialize_generated_modules # :nodoc:
- @generated_attribute_methods = const_set(:GeneratedAttributeMethods, GeneratedAttributeMethodsBuilder.new)
+ @generated_attribute_methods = const_set(:GeneratedAttributeMethods, GeneratedAttributeMethods.new)
private_constant :GeneratedAttributeMethods
@attribute_methods_generated = false
include @generated_attribute_methods
@@ -89,7 +89,7 @@ module ActiveRecord
# If ThisClass < ... < SomeSuperClass < ... < Base and SomeSuperClass
# defines its own attribute method, then we don't want to overwrite that.
defined = method_defined_within?(method_name, superclass, Base) &&
- ! superclass.instance_method(method_name).owner.is_a?(GeneratedAttributeMethodsBuilder)
+ ! superclass.instance_method(method_name).owner.is_a?(GeneratedAttributeMethods)
defined || super
end
end
@@ -197,7 +197,7 @@ module ActiveRecord
"Dangerous query method (method whose arguments are used as raw " \
"SQL) called with non-attribute argument(s): " \
"#{unexpected.map(&:inspect).join(", ")}. Non-attribute " \
- "arguments will be disallowed in Rails 6.0. This method should " \
+ "arguments will be disallowed in Rails 6.1. This method should " \
"not be called with user-provided values, such as request " \
"parameters or model attributes. Known-safe values can be passed " \
"by wrapping them in Arel.sql()."
@@ -465,7 +465,7 @@ module ActiveRecord
end
def pk_attribute?(name)
- name == self.class.primary_key
+ name == @primary_key
end
end
end
diff --git a/activerecord/lib/active_record/attribute_methods/before_type_cast.rb b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb
index 5941f51a1a..3d917ec9b1 100644
--- a/activerecord/lib/active_record/attribute_methods/before_type_cast.rb
+++ b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb
@@ -46,6 +46,7 @@ module ActiveRecord
# task.read_attribute_before_type_cast('completed_on') # => "2012-10-21"
# task.read_attribute_before_type_cast(:completed_on) # => "2012-10-21"
def read_attribute_before_type_cast(attr_name)
+ sync_with_transaction_state if @transaction_state&.finalized?
@attributes[attr_name.to_s].value_before_type_cast
end
@@ -60,17 +61,19 @@ module ActiveRecord
# task.attributes_before_type_cast
# # => {"id"=>nil, "title"=>nil, "is_done"=>true, "completed_on"=>"2012-10-21", "created_at"=>nil, "updated_at"=>nil}
def attributes_before_type_cast
+ sync_with_transaction_state if @transaction_state&.finalized?
@attributes.values_before_type_cast
end
private
- # Handle *_before_type_cast for method_missing.
+ # Dispatch target for <tt>*_before_type_cast</tt> attribute methods.
def attribute_before_type_cast(attribute_name)
read_attribute_before_type_cast(attribute_name)
end
def attribute_came_from_user?(attribute_name)
+ sync_with_transaction_state if @transaction_state&.finalized?
@attributes[attribute_name].came_from_user?
end
end
diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb
index 45e4b8adfa..45341765c1 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?
@@ -158,16 +156,51 @@ module ActiveRecord
end
private
+ def mutations_from_database
+ sync_with_transaction_state if @transaction_state&.finalized?
+ super
+ end
+
+ def mutations_before_last_save
+ sync_with_transaction_state if @transaction_state&.finalized?
+ super
+ end
+
def write_attribute_without_type_cast(attr_name, value)
- name = attr_name.to_s
- if self.class.attribute_alias?(name)
- name = self.class.attribute_alias(name)
- end
- result = super(name, value)
- clear_attribute_change(name)
+ result = super
+ clear_attribute_change(attr_name)
result
end
+ def _touch_row(attribute_names, time)
+ @_touch_attr_names = Set.new(attribute_names)
+
+ affected_rows = super
+
+ if @_skip_dirty_tracking ||= false
+ clear_attribute_changes(@_touch_attr_names)
+ return affected_rows
+ end
+
+ changes = {}
+ @attributes.keys.each do |attr_name|
+ next if @_touch_attr_names.include?(attr_name)
+
+ if attribute_changed?(attr_name)
+ changes[attr_name] = _read_attribute(attr_name)
+ _write_attribute(attr_name, attribute_was(attr_name))
+ clear_attribute_change(attr_name)
+ end
+ end
+
+ changes_applied
+ changes.each { |attr_name, value| _write_attribute(attr_name, value) }
+
+ affected_rows
+ ensure
+ @_touch_attr_names, @_skip_dirty_tracking = nil, nil
+ end
+
def _update_record(attribute_names = attribute_names_for_partial_writes)
affected_rows = super
changes_applied
diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb
index 6af5346fa7..b4f5e6e75a 100644
--- a/activerecord/lib/active_record/attribute_methods/primary_key.rb
+++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb
@@ -16,40 +16,32 @@ module ActiveRecord
# Returns the primary key column's value.
def id
- sync_with_transaction_state
- primary_key = self.class.primary_key
- _read_attribute(primary_key) if primary_key
+ _read_attribute(@primary_key)
end
# Sets the primary key column's value.
def id=(value)
- sync_with_transaction_state
- primary_key = self.class.primary_key
- _write_attribute(primary_key, value) if primary_key
+ _write_attribute(@primary_key, value)
end
# Queries the primary key column's value.
def id?
- sync_with_transaction_state
- query_attribute(self.class.primary_key)
+ query_attribute(@primary_key)
end
# Returns the primary key column's value before type cast.
def id_before_type_cast
- sync_with_transaction_state
- read_attribute_before_type_cast(self.class.primary_key)
+ read_attribute_before_type_cast(@primary_key)
end
# Returns the primary key column's previous value.
def id_was
- sync_with_transaction_state
- attribute_was(self.class.primary_key)
+ attribute_was(@primary_key)
end
# Returns the primary key column's value from the database.
def id_in_database
- sync_with_transaction_state
- attribute_in_database(self.class.primary_key)
+ attribute_in_database(@primary_key)
end
private
@@ -122,7 +114,7 @@ module ActiveRecord
#
# Project.primary_key # => "foo_id"
def primary_key=(value)
- @primary_key = value && value.to_s
+ @primary_key = value && -value.to_s
@quoted_primary_key = nil
@attributes_builder = nil
end
diff --git a/activerecord/lib/active_record/attribute_methods/query.rb b/activerecord/lib/active_record/attribute_methods/query.rb
index 6811f54b10..0cf67644af 100644
--- a/activerecord/lib/active_record/attribute_methods/query.rb
+++ b/activerecord/lib/active_record/attribute_methods/query.rb
@@ -32,7 +32,7 @@ module ActiveRecord
end
private
- # Handle *? for method_missing.
+ # Dispatch target for <tt>*?</tt> attribute methods.
def attribute?(attribute_name)
query_attribute(attribute_name)
end
diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb
index ffac5313ad..0562327a9a 100644
--- a/activerecord/lib/active_record/attribute_methods/read.rb
+++ b/activerecord/lib/active_record/attribute_methods/read.rb
@@ -9,14 +9,11 @@ module ActiveRecord
private
def define_method_attribute(name)
- sync_with_transaction_state = "sync_with_transaction_state" if name == primary_key
-
ActiveModel::AttributeMethods::AttrNames.define_attribute_accessor_method(
generated_attribute_methods, name
) do |temp_method_name, attr_name_expr|
generated_attribute_methods.module_eval <<-RUBY, __FILE__, __LINE__ + 1
def #{temp_method_name}
- #{sync_with_transaction_state}
name = #{attr_name_expr}
_read_attribute(name) { |n| missing_attribute(n, caller) }
end
@@ -30,19 +27,16 @@ module ActiveRecord
# to a date object, like Date.new(2004, 12, 12)).
def read_attribute(attr_name, &block)
name = attr_name.to_s
- if self.class.attribute_alias?(name)
- name = self.class.attribute_alias(name)
- end
+ name = self.class.attribute_aliases[name] || name
- primary_key = self.class.primary_key
- name = primary_key if name == "id" && primary_key
- sync_with_transaction_state if name == primary_key
+ name = @primary_key if name == "id" && @primary_key
_read_attribute(name, &block)
end
# This method exists to avoid the expensive primary_key check internally, without
# breaking compatibility with the read_attribute API
def _read_attribute(attr_name, &block) # :nodoc
+ sync_with_transaction_state if @transaction_state&.finalized?
@attributes.fetch_value(attr_name.to_s, &block)
end
diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb
index 455e67e19b..1c63b553d0 100644
--- a/activerecord/lib/active_record/attribute_methods/write.rb
+++ b/activerecord/lib/active_record/attribute_methods/write.rb
@@ -13,15 +13,12 @@ module ActiveRecord
private
def define_method_attribute=(name)
- sync_with_transaction_state = "sync_with_transaction_state" if name == primary_key
-
ActiveModel::AttributeMethods::AttrNames.define_attribute_accessor_method(
generated_attribute_methods, name, writer: true,
) do |temp_method_name, attr_name_expr|
generated_attribute_methods.module_eval <<-RUBY, __FILE__, __LINE__ + 1
def #{temp_method_name}(value)
name = #{attr_name_expr}
- #{sync_with_transaction_state}
_write_attribute(name, value)
end
RUBY
@@ -34,31 +31,28 @@ module ActiveRecord
# turned into +nil+.
def write_attribute(attr_name, value)
name = attr_name.to_s
- if self.class.attribute_alias?(name)
- name = self.class.attribute_alias(name)
- end
+ name = self.class.attribute_aliases[name] || name
- primary_key = self.class.primary_key
- name = primary_key if name == "id" && primary_key
- sync_with_transaction_state if name == primary_key
+ name = @primary_key if name == "id" && @primary_key
_write_attribute(name, value)
end
# This method exists to avoid the expensive primary_key check internally, without
# breaking compatibility with the write_attribute API
def _write_attribute(attr_name, value) # :nodoc:
+ sync_with_transaction_state if @transaction_state&.finalized?
@attributes.write_from_user(attr_name.to_s, value)
value
end
private
def write_attribute_without_type_cast(attr_name, value)
- name = attr_name.to_s
- @attributes.write_cast_value(name, value)
+ sync_with_transaction_state if @transaction_state&.finalized?
+ @attributes.write_cast_value(attr_name.to_s, value)
value
end
- # Handle *= for method_missing.
+ # Dispatch target for <tt>*=</tt> attribute methods.
def attribute=(attribute_name, value)
_write_attribute(attribute_name, value)
end
diff --git a/activerecord/lib/active_record/attributes.rb b/activerecord/lib/active_record/attributes.rb
index 35150889d9..7cf421c184 100644
--- a/activerecord/lib/active_record/attributes.rb
+++ b/activerecord/lib/active_record/attributes.rb
@@ -41,6 +41,9 @@ module ActiveRecord
# +range+ (PostgreSQL only) specifies that the type should be a range (see the
# examples below).
#
+ # When using a symbol for +cast_type+, extra options are forwarded to the
+ # constructor of the type object.
+ #
# ==== Examples
#
# The type detected by Active Record can be overridden.
@@ -112,6 +115,16 @@ module ActiveRecord
# my_float_range: 1.0..3.5
# }
#
+ # Passing options to the type constructor
+ #
+ # # app/models/my_model.rb
+ # class MyModel < ActiveRecord::Base
+ # attribute :small_int, :integer, limit: 2
+ # end
+ #
+ # MyModel.create(small_int: 65537)
+ # # => Error: 65537 is out of range for the limit of two bytes
+ #
# ==== Creating Custom Types
#
# Users may also define their own custom types, as long as they respond
diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb
index d77d76cb1e..8d89e7d84a 100644
--- a/activerecord/lib/active_record/autosave_association.rb
+++ b/activerecord/lib/active_record/autosave_association.rb
@@ -330,21 +330,16 @@ module ActiveRecord
if reflection.options[:autosave]
indexed_attribute = !index.nil? && (reflection.options[:index_errors] || ActiveRecord::Base.index_nested_attribute_errors)
- record.errors.each do |attribute, message|
+ record.errors.group_by_attribute.each { |attribute, errors|
attribute = normalize_reflection_attribute(indexed_attribute, reflection, index, attribute)
- errors[attribute] << message
- errors[attribute].uniq!
- end
-
- record.errors.details.each_key do |attribute|
- reflection_attribute =
- normalize_reflection_attribute(indexed_attribute, reflection, index, attribute).to_sym
- record.errors.details[attribute].each do |error|
- errors.details[reflection_attribute] << error
- errors.details[reflection_attribute].uniq!
- end
- end
+ errors.each { |error|
+ self.errors.import(
+ error,
+ attribute: attribute
+ )
+ }
+ }
else
errors.add(reflection.name)
end
@@ -382,10 +377,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 +396,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?
@@ -457,10 +456,16 @@ module ActiveRecord
# If the record is new or it has changed, returns true.
def record_changed?(reflection, record, key)
record.new_record? ||
- (record.has_attribute?(reflection.foreign_key) && record[reflection.foreign_key] != key) ||
+ association_foreign_key_changed?(reflection, record, key) ||
record.will_save_change_to_attribute?(reflection.foreign_key)
end
+ def association_foreign_key_changed?(reflection, record, key)
+ return false if reflection.through_reflection?
+
+ record.has_attribute?(reflection.foreign_key) && record[reflection.foreign_key] != key
+ end
+
# Saves the associated record if it's new or <tt>:autosave</tt> is enabled.
#
# In addition, it will destroy the association if it was marked for destruction.
@@ -490,9 +495,7 @@ module ActiveRecord
end
def _ensure_no_duplicate_errors
- errors.messages.each_key do |attribute|
- errors[attribute].uniq!
- end
+ errors.uniq!
end
end
end
diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb
index db097cb930..2af6d09b53 100644
--- a/activerecord/lib/active_record/base.rb
+++ b/activerecord/lib/active_record/base.rb
@@ -288,7 +288,6 @@ module ActiveRecord #:nodoc:
extend Explain
extend Enum
extend Delegation::DelegateCache
- extend CollectionCacheKey
extend Aggregations::ClassMethods
include Core
diff --git a/activerecord/lib/active_record/collection_cache_key.rb b/activerecord/lib/active_record/collection_cache_key.rb
deleted file mode 100644
index 4b6db8a96c..0000000000
--- a/activerecord/lib/active_record/collection_cache_key.rb
+++ /dev/null
@@ -1,53 +0,0 @@
-# frozen_string_literal: true
-
-module ActiveRecord
- module CollectionCacheKey
- def collection_cache_key(collection = all, timestamp_column = :updated_at) # :nodoc:
- query_signature = ActiveSupport::Digest.hexdigest(collection.to_sql)
- key = "#{collection.model_name.cache_key}/query-#{query_signature}"
-
- if collection.loaded? || collection.distinct_value
- size = collection.records.size
- if size > 0
- timestamp = collection.max_by(&timestamp_column)._read_attribute(timestamp_column)
- end
- else
- if collection.eager_loading?
- collection = collection.send(:apply_join_dependency)
- end
- column_type = type_for_attribute(timestamp_column)
- column = connection.visitor.compile(collection.arel_attribute(timestamp_column))
- select_values = "COUNT(*) AS #{connection.quote_column_name("size")}, MAX(%s) AS timestamp"
-
- if collection.has_limit_or_offset?
- query = collection.select("#{column} AS collection_cache_key_timestamp")
- subquery_alias = "subquery_for_cache_key"
- subquery_column = "#{subquery_alias}.collection_cache_key_timestamp"
- subquery = query.arel.as(subquery_alias)
- arel = Arel::SelectManager.new(subquery).project(select_values % subquery_column)
- else
- query = collection.unscope(:order)
- query.select_values = [select_values % column]
- arel = query.arel
- end
-
- result = connection.select_one(arel, nil)
-
- if result.blank?
- size = 0
- timestamp = nil
- else
- size = result["size"]
- timestamp = column_type.deserialize(result["timestamp"])
- end
-
- end
-
- if timestamp
- "#{key}-#{size}-#{timestamp.utc.to_s(cache_timestamp_format)}"
- else
- "#{key}-#{size}"
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
index 0ded1a5318..4ff3cb0071 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
@@ -294,15 +294,33 @@ module ActiveRecord
@frequency = frequency
end
+ @@mutex = Mutex.new
+ @@pools = {}
+
+ def self.register_pool(pool, frequency) # :nodoc:
+ @@mutex.synchronize do
+ if @@pools.key?(frequency)
+ @@pools[frequency] << pool
+ else
+ @@pools[frequency] = [pool]
+ Thread.new(frequency) do |t|
+ loop do
+ sleep t
+ @@mutex.synchronize do
+ @@pools[frequency].each do |p|
+ p.reap
+ p.flush
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+
def run
return unless frequency && frequency > 0
- Thread.new(frequency, pool) { |t, p|
- loop do
- sleep t
- p.reap
- p.flush
- end
- }
+ self.class.register_pool(pool, frequency)
end
end
@@ -810,6 +828,7 @@ module ActiveRecord
def new_connection
Base.send(spec.adapter_method, spec.config).tap do |conn|
conn.schema_cache = schema_cache.dup if schema_cache
+ conn.check_version
end
end
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..044272ea51 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
@@ -205,8 +205,6 @@ module ActiveRecord
# In order to get around this problem, #transaction will emulate the effect
# of nested transactions, by using savepoints:
# https://dev.mysql.com/doc/refman/5.7/en/savepoint.html
- # Savepoints are supported by MySQL and PostgreSQL. SQLite3 version >= '3.6.8'
- # supports savepoints.
#
# It is safe to call this method if a database transaction is already open,
# i.e. if #transaction is called within another #transaction block. In case
@@ -427,8 +425,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 +485,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/query_cache.rb b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
index 93b1c4e632..a7753e3e9c 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
@@ -7,7 +7,8 @@ module ActiveRecord
module QueryCache
class << self
def included(base) #:nodoc:
- dirties_query_cache base, :insert, :update, :delete, :rollback_to_savepoint, :rollback_db_transaction
+ dirties_query_cache base, :insert, :update, :delete, :truncate, :truncate_tables,
+ :rollback_to_savepoint, :rollback_db_transaction
base.set_callback :checkout, :after, :configure_query_cache!
base.set_callback :checkin, :after, :disable_query_cache!
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb b/activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb
index 52a796b926..d6dbef3fc8 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb
@@ -8,15 +8,15 @@ module ActiveRecord
end
def create_savepoint(name = current_savepoint_name)
- execute("SAVEPOINT #{name}")
+ execute("SAVEPOINT #{name}", "TRANSACTION")
end
def exec_rollback_to_savepoint(name = current_savepoint_name)
- execute("ROLLBACK TO SAVEPOINT #{name}")
+ execute("ROLLBACK TO SAVEPOINT #{name}", "TRANSACTION")
end
def release_savepoint(name = current_savepoint_name)
- execute("RELEASE SAVEPOINT #{name}")
+ execute("RELEASE SAVEPOINT #{name}", "TRANSACTION")
end
end
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 6981ea6ecd..f97842b3f5 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
@@ -735,7 +735,7 @@ module ActiveRecord
#
# CREATE UNIQUE INDEX index_accounts_on_branch_id_and_party_id ON accounts(branch_id, party_id) WHERE active
#
- # Note: Partial indexes are only supported for PostgreSQL and SQLite 3.8.0+.
+ # Note: Partial indexes are only supported for PostgreSQL and SQLite.
#
# ====== Creating an index with a specific method
#
@@ -770,6 +770,17 @@ module ActiveRecord
# CREATE FULLTEXT INDEX index_developers_on_name ON developers (name) -- MySQL
#
# Note: only supported by MySQL.
+ #
+ # ====== Creating an index with a specific algorithm
+ #
+ # add_index(:developers, :name, algorithm: :concurrently)
+ # # CREATE INDEX CONCURRENTLY developers_on_name on developers (name)
+ #
+ # Note: only supported by PostgreSQL.
+ #
+ # Concurrently adding an index is not supported in a transaction.
+ #
+ # For more information see the {"Transactional Migrations" section}[rdoc-ref:Migration].
def add_index(table_name, column_name, options = {})
index_name, index_type, index_columns, index_options = add_index_options(table_name, column_name, options)
execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} (#{index_columns})#{index_options}"
@@ -793,6 +804,15 @@ module ActiveRecord
#
# remove_index :accounts, name: :by_branch_party
#
+ # Removes the index named +by_branch_party+ in the +accounts+ table +concurrently+.
+ #
+ # remove_index :accounts, name: :by_branch_party, algorithm: :concurrently
+ #
+ # Note: only supported by PostgreSQL.
+ #
+ # Concurrently removing an index is not supported in a transaction.
+ #
+ # For more information see the {"Transactional Migrations" section}[rdoc-ref:Migration].
def remove_index(table_name, options = {})
index_name = index_name_for_remove(table_name, options)
execute "DROP INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)}"
@@ -966,7 +986,7 @@ module ActiveRecord
# [<tt>:on_update</tt>]
# Action that happens <tt>ON UPDATE</tt>. Valid values are +:nullify+, +:cascade+ and +:restrict+
# [<tt>:validate</tt>]
- # (Postgres only) Specify whether or not the constraint should be validated. Defaults to +true+.
+ # (PostgreSQL only) Specify whether or not the constraint should be validated. Defaults to +true+.
def add_foreign_key(from_table, to_table, options = {})
return unless supports_foreign_keys?
@@ -1097,7 +1117,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})"
@@ -1185,12 +1205,22 @@ module ActiveRecord
end
# Changes the comment for a table or removes it if +nil+.
- def change_table_comment(table_name, comment)
+ #
+ # Passing a hash containing +:from+ and +:to+ will make this change
+ # reversible in migration:
+ #
+ # change_table_comment(:posts, from: "old_comment", to: "new_comment")
+ def change_table_comment(table_name, comment_or_changes)
raise NotImplementedError, "#{self.class} does not support changing table comments"
end
# Changes the comment for a column or removes it if +nil+.
- def change_column_comment(table_name, column_name, comment)
+ #
+ # Passing a hash containing +:from+ and +:to+ will make this change
+ # reversible in migration:
+ #
+ # change_column_comment(:posts, :state, from: "old_comment", to: "new_comment")
+ def change_column_comment(table_name, column_name, comment_or_changes)
raise NotImplementedError, "#{self.class} does not support changing column comments"
end
@@ -1374,11 +1404,37 @@ module ActiveRecord
default_or_changes
end
end
+ alias :extract_new_comment_value :extract_new_default_value
def can_remove_index_by_name?(options)
options.is_a?(Hash) && options.key?(:name) && options.except(:name, :algorithm).empty?
end
+ def bulk_change_table(table_name, operations)
+ sql_fragments = []
+ non_combinable_operations = []
+
+ operations.each do |command, args|
+ table, arguments = args.shift, args
+ method = :"#{command}_for_alter"
+
+ if respond_to?(method, true)
+ sqls, procs = Array(send(method, table, *arguments)).partition { |v| v.is_a?(String) }
+ sql_fragments << sqls
+ non_combinable_operations.concat(procs)
+ else
+ execute "ALTER TABLE #{quote_table_name(table_name)} #{sql_fragments.join(", ")}" unless sql_fragments.empty?
+ non_combinable_operations.each(&:call)
+ sql_fragments = []
+ non_combinable_operations = []
+ send(command, table, *arguments)
+ end
+ end
+
+ execute "ALTER TABLE #{quote_table_name(table_name)} #{sql_fragments.join(", ")}" unless sql_fragments.empty?
+ non_combinable_operations.each(&:call)
+ end
+
def add_column_for_alter(table_name, column_name, type, options = {})
td = create_table_definition(table_name)
cd = td.new_column_definition(column_name, type, options)
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb
index c9e84e48cc..7b6321d83b 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb
@@ -98,9 +98,13 @@ module ActiveRecord
end
def rollback_records
- ite = records.uniq
+ ite = records.uniq(&:object_id)
+ already_run_callbacks = {}
while record = ite.shift
- record.rolledback!(force_restore_state: full_rollback?)
+ trigger_callbacks = record.trigger_transactional_callbacks?
+ should_run_callbacks = !already_run_callbacks[record] && trigger_callbacks
+ already_run_callbacks[record] ||= trigger_callbacks
+ record.rolledback!(force_restore_state: full_rollback?, should_run_callbacks: should_run_callbacks)
end
ensure
ite.each do |i|
@@ -113,10 +117,14 @@ module ActiveRecord
end
def commit_records
- ite = records.uniq
+ ite = records.uniq(&:object_id)
+ already_run_callbacks = {}
while record = ite.shift
if @run_commit_callbacks
- record.committed!
+ trigger_callbacks = record.trigger_transactional_callbacks?
+ should_run_callbacks = !already_run_callbacks[record] && trigger_callbacks
+ already_run_callbacks[record] ||= trigger_callbacks
+ record.committed!(should_run_callbacks: should_run_callbacks)
else
# if not running callbacks, only adds the record to the parent transaction
connection.add_transaction_record(record)
diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
index 7aad306d50..bf0bb84c93 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
@@ -133,8 +133,6 @@ module ActiveRecord
@advisory_locks_enabled = self.class.type_cast_config_to_boolean(
config.fetch(:advisory_locks, true)
)
-
- check_version
end
def replica?
@@ -172,8 +170,11 @@ module ActiveRecord
class Version
include Comparable
- def initialize(version_string)
+ attr_reader :full_version_string
+
+ def initialize(version_string, full_version_string = nil)
@version = version_string.split(".").map(&:to_i)
+ @full_version_string = full_version_string
end
def <=>(version_string)
@@ -575,9 +576,17 @@ module ActiveRecord
"INSERT #{insert.into} #{insert.values_list}"
end
+ def get_database_version # :nodoc:
+ end
+
+ def database_version # :nodoc:
+ schema_cache.database_version
+ end
+
+ def check_version # :nodoc:
+ end
+
private
- def check_version
- end
def type_map
@type_map ||= Type::TypeMap.new.tap do |mapping|
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 8ca2cfa9ed..ef33d712ad 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
@@ -55,24 +55,26 @@ module ActiveRecord
super(connection, logger, config)
end
- def version #:nodoc:
- @version ||= Version.new(version_string)
+ def get_database_version #:nodoc:
+ full_version_string = get_full_version
+ version_string = version_string(full_version_string)
+ Version.new(version_string, full_version_string)
end
def mariadb? # :nodoc:
/mariadb/i.match?(full_version)
end
- def supports_bulk_alter? #:nodoc:
+ def supports_bulk_alter?
true
end
def supports_index_sort_order?
- !mariadb? && version >= "8.0.1"
+ !mariadb? && database_version >= "8.0.1"
end
def supports_expression_index?
- !mariadb? && version >= "8.0.13"
+ !mariadb? && database_version >= "8.0.13"
end
def supports_transaction_isolation?
@@ -96,16 +98,16 @@ module ActiveRecord
end
def supports_datetime_with_precision?
- mariadb? || version >= "5.6.4"
+ mariadb? || database_version >= "5.6.4"
end
def supports_virtual_columns?
- mariadb? || version >= "5.7.5"
+ mariadb? || database_version >= "5.7.5"
end
# See https://dev.mysql.com/doc/refman/8.0/en/optimizer-hints.html for more details.
def supports_optimizer_hints?
- !mariadb? && version >= "5.7.7"
+ !mariadb? && database_version >= "5.7.7"
end
def supports_advisory_locks?
@@ -174,15 +176,6 @@ module ActiveRecord
# DATABASE STATEMENTS ======================================
#++
- def explain(arel, binds = [])
- sql = "EXPLAIN #{to_sql(arel, binds)}"
- start = Concurrent.monotonic_time
- result = exec_query(sql, "EXPLAIN", binds)
- elapsed = Concurrent.monotonic_time - start
-
- MySQL::ExplainPrettyPrinter.new.pp(result, elapsed)
- end
-
# Executes the SQL statement in the context of this connection.
def execute(sql, name = nil)
materialize_transactions
@@ -202,7 +195,7 @@ module ActiveRecord
end
def begin_db_transaction
- execute "BEGIN"
+ execute("BEGIN", "TRANSACTION")
end
def begin_isolated_db_transaction(isolation)
@@ -211,11 +204,11 @@ module ActiveRecord
end
def commit_db_transaction #:nodoc:
- execute "COMMIT"
+ execute("COMMIT", "TRANSACTION")
end
def exec_rollback_db_transaction #:nodoc:
- execute "ROLLBACK"
+ execute("ROLLBACK", "TRANSACTION")
end
def empty_insert_statement_value(primary_key = nil)
@@ -285,22 +278,8 @@ module ActiveRecord
SQL
end
- def bulk_change_table(table_name, operations) #:nodoc:
- sqls = operations.flat_map do |command, args|
- table, arguments = args.shift, args
- method = :"#{command}_for_alter"
-
- if respond_to?(method, true)
- send(method, table, *arguments)
- else
- raise "Unknown method called : #{method}(#{arguments.inspect})"
- end
- end.join(", ")
-
- execute("ALTER TABLE #{quote_table_name(table_name)} #{sqls}")
- end
-
- def change_table_comment(table_name, comment) #:nodoc:
+ def change_table_comment(table_name, comment_or_changes) # :nodoc:
+ comment = extract_new_comment_value(comment_or_changes)
comment = "" if comment.nil?
execute("ALTER TABLE #{quote_table_name(table_name)} COMMENT #{quote(comment)}")
end
@@ -356,7 +335,8 @@ module ActiveRecord
change_column table_name, column_name, nil, null: null
end
- def change_column_comment(table_name, column_name, comment) #:nodoc:
+ def change_column_comment(table_name, column_name, comment_or_changes) # :nodoc:
+ comment = extract_new_comment_value(comment_or_changes)
change_column table_name, column_name, nil, comment: comment
end
@@ -516,8 +496,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(",")
@@ -526,12 +506,13 @@ module ActiveRecord
sql
end
- private
- def check_version
- if version < "5.5.8"
- raise "Your version of MySQL (#{version_string}) is too old. Active Record supports MySQL >= 5.5.8."
- end
+ def check_version # :nodoc:
+ if database_version < "5.5.8"
+ raise "Your version of MySQL (#{database_version}) is too old. Active Record supports MySQL >= 5.5.8."
end
+ end
+
+ private
def initialize_type_map(m = type_map)
super
@@ -702,7 +683,7 @@ module ActiveRecord
end
def supports_rename_index?
- mariadb? ? false : version >= "5.7.6"
+ mariadb? ? false : database_version >= "5.7.6"
end
def configure_connection
@@ -800,8 +781,8 @@ module ActiveRecord
MismatchedForeignKey.new(options)
end
- def version_string
- full_version.match(/^(?:5\.5\.5-)?(\d+\.\d+\.\d+)/)[1]
+ def version_string(full_version_string)
+ full_version_string.match(/^(?:5\.5\.5-)?(\d+\.\d+\.\d+)/)[1]
end
class MysqlString < Type::String # :nodoc:
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 1199c0ad1b..bbcdc96cdc 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb
@@ -26,6 +26,15 @@ module ActiveRecord
!READ_QUERY.match?(sql)
end
+ def explain(arel, binds = [])
+ sql = "EXPLAIN #{to_sql(arel, binds)}"
+ start = Concurrent.monotonic_time
+ result = exec_query(sql, "EXPLAIN", binds)
+ elapsed = Concurrent.monotonic_time - start
+
+ MySQL::ExplainPrettyPrinter.new.pp(result, elapsed)
+ end
+
# Executes the SQL statement in the context of this connection.
def execute(sql, name = nil)
if preventing_writes? && write_query?(sql)
@@ -61,7 +70,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
@@ -145,15 +156,12 @@ module ActiveRecord
elsif previous_packet.nil?
true
else
- (current_packet.bytesize + previous_packet.bytesize) > max_allowed_packet
+ (current_packet.bytesize + previous_packet.bytesize + 2) > max_allowed_packet
end
end
def max_allowed_packet
- @max_allowed_packet ||= begin
- bytes_margin = 2
- show_variable("max_allowed_packet") - bytes_margin
- end
+ @max_allowed_packet ||= show_variable("max_allowed_packet")
end
def exec_stmt_and_free(sql, name, binds, cache_stmt: false)
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 57518b02fa..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"]
@@ -64,14 +64,14 @@ module ActiveRecord
end
def extract_expression_for_virtual_column(column)
- if @connection.mariadb? && @connection.version < "10.2.5"
- create_table_info = @connection.send(:create_table_info, column.table_name)
+ if @connection.mariadb? && @connection.database_version < "10.2.5"
+ 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 4018f0815c..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,14 +121,18 @@ 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"]
def row_format_dynamic_by_default?
if mariadb?
- version >= "10.2.2"
+ database_version >= "10.2.2"
else
- version >= "5.7.9"
+ database_version >= "5.7.9"
end
end
@@ -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..9167593064 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql/type_metadata.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql/type_metadata.rb
@@ -10,25 +10,21 @@ module ActiveRecord
def initialize(type_metadata, extra: "")
super(type_metadata)
- @type_metadata = type_metadata
@extra = extra
end
def ==(other)
- other.is_a?(MySQL::TypeMetadata) &&
- attributes_for_hash == other.attributes_for_hash
+ other.is_a?(TypeMetadata) &&
+ __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/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
index 9bdaa00336..5b0335c22b 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
@@ -43,7 +43,7 @@ module ActiveRecord
end
def supports_json?
- !mariadb? && version >= "5.7.8"
+ !mariadb? && database_version >= "5.7.8"
end
def supports_comments?
@@ -126,7 +126,11 @@ module ActiveRecord
end
def full_version
- @full_version ||= @connection.server_info[:version]
+ schema_cache.database_version.full_version_string
+ end
+
+ def get_full_version
+ @connection.server_info[:version]
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..ec25bb1e19 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb
@@ -2,42 +2,29 @@
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
+ module PostgreSQL
+ class Column < ConnectionAdapters::Column # :nodoc:
+ delegate :oid, :fmod, to: :sql_type_metadata
- 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
+ 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
+ def serial?
+ @serial
+ end
- if over_length > 0
- table_name = table_name[0, table_name.length - over_length]
- end
+ def array
+ sql_type_metadata.sql_type.end_with?("[]")
+ end
+ alias :array? :array
- "#{table_name}_#{column_name}_#{suffix}"
+ def sql_type
+ super.sub(/\[\]\z/, "")
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..45ec79ca78 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)
@@ -145,7 +145,7 @@ module ActiveRecord
# Begins a transaction.
def begin_db_transaction
- execute "BEGIN"
+ execute("BEGIN", "TRANSACTION")
end
def begin_isolated_db_transaction(isolation)
@@ -155,12 +155,12 @@ module ActiveRecord
# Commits a transaction.
def commit_db_transaction
- execute "COMMIT"
+ execute("COMMIT", "TRANSACTION")
end
# Aborts a transaction.
def exec_rollback_db_transaction
- execute "ROLLBACK"
+ execute("ROLLBACK", "TRANSACTION")
end
private
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 a38c1325c0..40c5e51d92 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
@@ -287,7 +287,7 @@ module ActiveRecord
quoted_sequence = quote_table_name(sequence)
max_pk = query_value("SELECT MAX(#{quote_column_name pk}) FROM #{quote_table_name(table)}", "SCHEMA")
if max_pk.nil?
- if postgresql_version >= 100000
+ if database_version >= 100000
minvalue = query_value("SELECT seqmin FROM pg_sequence WHERE seqrelid = #{quote(quoted_sequence)}::regclass", "SCHEMA")
else
minvalue = query_value("SELECT min_value FROM #{quoted_sequence}", "SCHEMA")
@@ -368,31 +368,6 @@ module ActiveRecord
SQL
end
- def bulk_change_table(table_name, operations)
- sql_fragments = []
- non_combinable_operations = []
-
- operations.each do |command, args|
- table, arguments = args.shift, args
- method = :"#{command}_for_alter"
-
- if respond_to?(method, true)
- sqls, procs = Array(send(method, table, *arguments)).partition { |v| v.is_a?(String) }
- sql_fragments << sqls
- non_combinable_operations.concat(procs)
- else
- execute "ALTER TABLE #{quote_table_name(table_name)} #{sql_fragments.join(", ")}" unless sql_fragments.empty?
- non_combinable_operations.each(&:call)
- sql_fragments = []
- non_combinable_operations = []
- send(command, table, *arguments)
- end
- end
-
- execute "ALTER TABLE #{quote_table_name(table_name)} #{sql_fragments.join(", ")}" unless sql_fragments.empty?
- non_combinable_operations.each(&:call)
- end
-
# Renames a table.
# Also renames a table's primary key sequence if the sequence name exists and
# matches the Active Record default.
@@ -443,14 +418,16 @@ module ActiveRecord
end
# Adds comment for given table column or drops it if +comment+ is a +nil+
- def change_column_comment(table_name, column_name, comment) # :nodoc:
+ def change_column_comment(table_name, column_name, comment_or_changes) # :nodoc:
clear_cache!
+ comment = extract_new_comment_value(comment_or_changes)
execute "COMMENT ON COLUMN #{quote_table_name(table_name)}.#{quote_column_name(column_name)} IS #{quote(comment)}"
end
# Adds comment for given table or drops it if +comment+ is a +nil+
- def change_table_comment(table_name, comment) # :nodoc:
+ def change_table_comment(table_name, comment_or_changes) # :nodoc:
clear_cache!
+ comment = extract_new_comment_value(comment_or_changes)
execute "COMMENT ON TABLE #{quote_table_name(table_name)} IS #{quote(comment)}"
end
@@ -548,21 +525,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 +627,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
@@ -672,7 +652,23 @@ module ActiveRecord
precision: cast_type.precision,
scale: cast_type.scale,
)
- PostgreSQLTypeMetadata.new(simple_type, oid: oid, fmod: fmod)
+ PostgreSQL::TypeMetadata.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)
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..8bdec623af 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb
@@ -3,38 +3,34 @@
module ActiveRecord
# :stopdoc:
module ConnectionAdapters
- class PostgreSQLTypeMetadata < DelegateClass(SqlTypeMetadata)
- undef to_yaml if method_defined?(:to_yaml)
+ module PostgreSQL
+ class TypeMetadata < DelegateClass(SqlTypeMetadata)
+ undef to_yaml if method_defined?(:to_yaml)
- attr_reader :oid, :fmod, :array
+ attr_reader :oid, :fmod
- def initialize(type_metadata, oid: nil, fmod: nil)
- super(type_metadata)
- @type_metadata = type_metadata
- @oid = oid
- @fmod = fmod
- @array = /\[\]$/.match?(type_metadata.sql_type)
- end
-
- def sql_type
- super.gsub(/\[\]$/, "")
- end
-
- def ==(other)
- other.is_a?(PostgreSQLTypeMetadata) &&
- attributes_for_hash == other.attributes_for_hash
- end
- alias eql? ==
-
- def hash
- attributes_for_hash.hash
- end
+ def initialize(type_metadata, oid: nil, fmod: nil)
+ super(type_metadata)
+ @oid = oid
+ @fmod = fmod
+ end
- protected
+ def ==(other)
+ other.is_a?(TypeMetadata) &&
+ __getobj__ == other.__getobj__ &&
+ oid == other.oid &&
+ fmod == other.fmod
+ end
+ alias eql? ==
- def attributes_for_hash
- [self.class, @type_metadata, oid, fmod]
+ def hash
+ TypeMetadata.hash ^
+ __getobj__.hash ^
+ oid.hash ^
+ fmod.hash
end
+ end
end
+ PostgreSQLTypeMetadata = PostgreSQL::TypeMetadata
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 29f764e8f4..91318a0af1 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
@@ -201,7 +201,7 @@ module ActiveRecord
end
def supports_insert_on_conflict?
- postgresql_version >= 90500
+ database_version >= 90500
end
alias supports_insert_on_duplicate_skip? supports_insert_on_conflict?
alias supports_insert_on_duplicate_update? supports_insert_on_conflict?
@@ -344,7 +344,7 @@ module ActiveRecord
end
def supports_pgcrypto_uuid?
- postgresql_version >= 90400
+ database_version >= 90400
end
def supports_optimizer_hints?
@@ -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 postgresql_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
@@ -446,12 +445,13 @@ module ActiveRecord
sql
end
- private
- def check_version
- if postgresql_version < 90300
- raise "Your version of PostgreSQL (#{postgresql_version}) is too old. Active Record supports PostgreSQL >= 9.3."
- end
+ def check_version # :nodoc:
+ if database_version < 90300
+ raise "Your version of PostgreSQL (#{database_version}) is too old. Active Record supports PostgreSQL >= 9.3."
end
+ end
+
+ private
# See https://www.postgresql.org/docs/current/static/errcodes-appendix.html
VALUE_LIMIT_VIOLATION = "22001"
diff --git a/activerecord/lib/active_record/connection_adapters/schema_cache.rb b/activerecord/lib/active_record/connection_adapters/schema_cache.rb
index 07453b4403..dbfe1e4a34 100644
--- a/activerecord/lib/active_record/connection_adapters/schema_cache.rb
+++ b/activerecord/lib/active_record/connection_adapters/schema_cache.rb
@@ -26,21 +26,23 @@ module ActiveRecord
end
def encode_with(coder)
- coder["columns"] = @columns
- coder["columns_hash"] = @columns_hash
- coder["primary_keys"] = @primary_keys
- coder["data_sources"] = @data_sources
- coder["indexes"] = @indexes
- coder["version"] = connection.migration_context.current_version
+ coder["columns"] = @columns
+ coder["columns_hash"] = @columns_hash
+ coder["primary_keys"] = @primary_keys
+ coder["data_sources"] = @data_sources
+ coder["indexes"] = @indexes
+ coder["version"] = connection.migration_context.current_version
+ coder["database_version"] = database_version
end
def init_with(coder)
- @columns = coder["columns"]
- @columns_hash = coder["columns_hash"]
- @primary_keys = coder["primary_keys"]
- @data_sources = coder["data_sources"]
- @indexes = coder["indexes"] || {}
- @version = coder["version"]
+ @columns = coder["columns"]
+ @columns_hash = coder["columns_hash"]
+ @primary_keys = coder["primary_keys"]
+ @data_sources = coder["data_sources"]
+ @indexes = coder["indexes"] || {}
+ @version = coder["version"]
+ @database_version = coder["database_version"]
end
def primary_keys(table_name)
@@ -91,6 +93,10 @@ module ActiveRecord
@indexes[table_name] ||= connection.indexes(table_name)
end
+ def database_version # :nodoc:
+ @database_version ||= connection.get_database_version
+ end
+
# Clears out internal caches
def clear!
@columns.clear
@@ -99,6 +105,7 @@ module ActiveRecord
@data_sources.clear
@indexes.clear
@version = nil
+ @database_version = nil
end
def size
@@ -117,11 +124,11 @@ module ActiveRecord
def marshal_dump
# if we get current version during initialization, it happens stack over flow.
@version = connection.migration_context.current_version
- [@version, @columns, @columns_hash, @primary_keys, @data_sources, @indexes]
+ [@version, @columns, @columns_hash, @primary_keys, @data_sources, @indexes, database_version]
end
def marshal_load(array)
- @version, @columns, @columns_hash, @primary_keys, @data_sources, @indexes = array
+ @version, @columns, @columns_hash, @primary_keys, @data_sources, @indexes, @database_version = array
@indexes = @indexes || {}
end
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 84dcae49b9..85053acf91 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,86 @@ 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 explain(arel, binds = [])
+ sql = "EXPLAIN QUERY PLAN #{to_sql(arel, binds)}"
+ SQLite3::ExplainPrettyPrinter.new.pp(exec_query(sql, "EXPLAIN", []))
+ 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", "TRANSACTION") { @connection.transaction }
+ end
+
+ def commit_db_transaction #:nodoc:
+ log("commit transaction", "TRANSACTION") { @connection.commit }
+ end
+
+ def exec_rollback_db_transaction #:nodoc:
+ log("rollback transaction", "TRANSACTION") { @connection.rollback }
+ end
+
private
def execute_batch(sql, name = nil)
if preventing_writes? && write_query?(sql)
@@ -14,11 +94,15 @@ module ActiveRecord
log(sql, name) do
ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
- @connection.execute_batch(sql)
+ @connection.execute_batch2(sql)
end
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 ff23a525b9..7f3f32162e 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
@@ -10,7 +10,7 @@ require "active_record/connection_adapters/sqlite3/schema_definitions"
require "active_record/connection_adapters/sqlite3/schema_dumper"
require "active_record/connection_adapters/sqlite3/schema_statements"
-gem "sqlite3", "~> 1.3", ">= 1.3.6"
+gem "sqlite3", "~> 1.4"
require "sqlite3"
module ActiveRecord
@@ -48,8 +48,8 @@ module ActiveRecord
end
module ConnectionAdapters #:nodoc:
- # The SQLite3 adapter works SQLite 3.6.16 or newer
- # with the sqlite3-ruby drivers (available as gem from https://rubygems.org/gems/sqlite3).
+ # The SQLite3 adapter works with the sqlite3-ruby drivers
+ # (available as gem from https://rubygems.org/gems/sqlite3).
#
# Options:
#
@@ -111,7 +111,7 @@ module ActiveRecord
end
def supports_expression_index?
- sqlite_version >= "3.9.0"
+ database_version >= "3.9.0"
end
def requires_reloading?
@@ -135,7 +135,7 @@ module ActiveRecord
end
def supports_insert_on_conflict?
- sqlite_version >= "3.24.0"
+ database_version >= "3.24.0"
end
alias supports_insert_on_duplicate_skip? supports_insert_on_conflict?
alias supports_insert_on_duplicate_update? supports_insert_on_conflict?
@@ -201,94 +201,6 @@ module ActiveRecord
end
end
- #--
- # 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:
@@ -397,6 +309,16 @@ module ActiveRecord
sql
end
+ def get_database_version # :nodoc:
+ SQLite3Adapter::Version.new(query_value("SELECT sqlite_version(*)"))
+ end
+
+ def check_version # :nodoc:
+ if database_version < "3.8.0"
+ raise "Your version of SQLite (#{database_version}) is too old. Active Record supports SQLite >= 3.8."
+ end
+ end
+
private
# See https://www.sqlite.org/limits.html,
# the default value is 999 when not configured.
@@ -404,12 +326,6 @@ module ActiveRecord
999
end
- def check_version
- if sqlite_version < "3.8.0"
- raise "Your version of SQLite (#{sqlite_version}) is too old. Active Record supports SQLite >= 3.8."
- end
- end
-
def initialize_type_map(m = type_map)
super
register_class_with_limit m, %r(int)i, SQLite3Integer
@@ -527,10 +443,6 @@ module ActiveRecord
SELECT #{quoted_from_columns} FROM #{quote_table_name(from)}")
end
- def sqlite_version
- @sqlite_version ||= SQLite3Adapter::Version.new(query_value("SELECT sqlite_version(*)"))
- end
-
def translate_exception(exception, message:, sql:, binds:)
case exception.message
# SQLite 3.8.2 returns a newly formatted error message:
diff --git a/activerecord/lib/active_record/connection_handling.rb b/activerecord/lib/active_record/connection_handling.rb
index 53069cd899..040ebdb960 100644
--- a/activerecord/lib/active_record/connection_handling.rb
+++ b/activerecord/lib/active_record/connection_handling.rb
@@ -85,14 +85,14 @@ module ActiveRecord
# based on the requested role:
#
# ActiveRecord::Base.connected_to(role: :writing) do
- # Dog.create! # creates dog using dog connection
+ # Dog.create! # creates dog using dog writing connection
# end
#
# ActiveRecord::Base.connected_to(role: :reading) do
# Dog.create! # throws exception because we're on a replica
# end
#
- # ActiveRecord::Base.connected_to(role: :unknown_ode) do
+ # ActiveRecord::Base.connected_to(role: :unknown_role) do
# # raises exception due to non-existent role
# end
#
@@ -100,11 +100,20 @@ module ActiveRecord
# you can use +connected_to+ with a +database+ argument. The +database+ argument
# expects a symbol that corresponds to the database key in your config.
#
- # This will connect to a new database for the queries inside the block.
- #
# ActiveRecord::Base.connected_to(database: :animals_slow_replica) do
# Dog.run_a_long_query # runs a long query while connected to the +animals_slow_replica+
# end
+ #
+ # This will connect to a new database for the queries inside the block. By
+ # default the `:writing` role will be used since all connections must be assigned
+ # a role. If you would like to use a different role you can pass a hash to database:
+ #
+ # ActiveRecord::Base.connected_to(database: { readonly_slow: :animals_slow_replica }) do
+ # # runs a long query while connected to the +animals_slow_replica+ using the readonly_slow role.
+ # Dog.run_a_long_query
+ # end
+ #
+ # When using the database key a new connection will be established every time.
def connected_to(database: nil, role: nil, &blk)
if database && role
raise ArgumentError, "connected_to can only accept a `database` or a `role` argument, but not both arguments."
@@ -112,17 +121,14 @@ module ActiveRecord
if database.is_a?(Hash)
role, database = database.first
role = role.to_sym
- else
- role = database.to_sym
end
config_hash = resolve_config_for_connection(database)
handler = lookup_connection_handler(role)
- with_handler(role) do
- handler.establish_connection(config_hash)
- yield
- end
+ handler.establish_connection(config_hash)
+
+ with_handler(role, &blk)
elsif role
with_handler(role.to_sym, &blk)
else
@@ -154,6 +160,7 @@ module ActiveRecord
end
def lookup_connection_handler(handler_key) # :nodoc:
+ handler_key ||= ActiveRecord::Base.writing_role
connection_handlers[handler_key] ||= ActiveRecord::ConnectionAdapters::ConnectionHandler.new
end
diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb
index eb4b48bc37..dfd33d3dd7 100644
--- a/activerecord/lib/active_record/core.rb
+++ b/activerecord/lib/active_record/core.rb
@@ -101,7 +101,6 @@ module ActiveRecord
# environment where dumping schema is rarely needed.
mattr_accessor :dump_schema_after_migration, instance_writer: false, default: true
- mattr_accessor :database_selector, instance_writer: false
##
# :singleton-method:
# Specifies which database schemas to dump when calling db:structure:dump.
@@ -175,8 +174,7 @@ module ActiveRecord
record = statement.execute([id], connection)&.first
unless record
- raise RecordNotFound.new("Couldn't find #{name} with '#{primary_key}'=#{id}",
- name, primary_key, id)
+ raise RecordNotFound.new("Couldn't find #{name} with '#{key}'=#{id}", name, key, id)
end
record
end
@@ -270,7 +268,8 @@ module ActiveRecord
end
def arel_attribute(name, table = arel_table) # :nodoc:
- name = attribute_alias(name) if attribute_alias?(name)
+ name = name.to_s
+ name = attribute_aliases[name] || name
table[name]
end
@@ -318,7 +317,7 @@ module ActiveRecord
# # Instantiates a single new object
# User.new(first_name: 'Jamie')
def initialize(attributes = nil)
- self.class.define_attribute_methods
+ @new_record = true
@attributes = self.class._default_attributes.deep_dup
init_internals
@@ -355,12 +354,10 @@ module ActiveRecord
# +attributes+ should be an attributes object, and unlike the
# `initialize` method, no assignment calls are made per attribute.
def init_with_attributes(attributes, new_record = false) # :nodoc:
- init_internals
-
@new_record = new_record
@attributes = attributes
- self.class.define_attribute_methods
+ init_internals
yield self if block_given?
@@ -399,13 +396,13 @@ module ActiveRecord
##
def initialize_dup(other) # :nodoc:
@attributes = @attributes.deep_dup
- @attributes.reset(self.class.primary_key)
+ @attributes.reset(@primary_key)
_run_initialize_callbacks
@new_record = true
@destroyed = false
- @_start_transaction_state = {}
+ @_start_transaction_state = nil
@transaction_state = nil
super
@@ -466,6 +463,7 @@ module ActiveRecord
# Returns +true+ if the attributes hash has been frozen.
def frozen?
+ sync_with_transaction_state if @transaction_state&.finalized?
@attributes.frozen?
end
@@ -570,22 +568,18 @@ module ActiveRecord
end
def init_internals
+ @primary_key = self.class.primary_key
@readonly = false
@destroyed = false
@marked_for_destruction = false
@destroyed_by_association = nil
- @new_record = true
- @_start_transaction_state = {}
+ @_start_transaction_state = nil
@transaction_state = nil
- end
- def initialize_internals_callback
+ self.class.define_attribute_methods
end
- def thaw
- if frozen?
- @attributes = @attributes.dup
- end
+ def initialize_internals_callback
end
def custom_inspect_method_defined?
diff --git a/activerecord/lib/active_record/database_configurations.rb b/activerecord/lib/active_record/database_configurations.rb
index 4656045fe5..44b5cfc738 100644
--- a/activerecord/lib/active_record/database_configurations.rb
+++ b/activerecord/lib/active_record/database_configurations.rb
@@ -7,7 +7,7 @@ require "active_record/database_configurations/url_config"
module ActiveRecord
# ActiveRecord::DatabaseConfigurations returns an array of DatabaseConfig
# objects (either a HashConfig or UrlConfig) that are constructed from the
- # application's database configuration hash or url string.
+ # application's database configuration hash or URL string.
class DatabaseConfigurations
attr_reader :configurations
delegate :any?, to: :configurations
@@ -17,22 +17,22 @@ module ActiveRecord
end
# Collects the configs for the environment and optionally the specification
- # name passed in. To include replica configurations pass `include_replicas: true`.
+ # name passed in. To include replica configurations pass <tt>include_replicas: true</tt>.
#
# If a spec name is provided a single DatabaseConfig object will be
# returned, otherwise an array of DatabaseConfig objects will be
# returned that corresponds with the environment and type requested.
#
- # Options:
+ # ==== Options
#
- # <tt>env_name:</tt> The environment name. Defaults to +nil+ which will collect
- # configs for all environments.
- # <tt>spec_name:</tt> The specification name (i.e. primary, animals, etc.). Defaults
- # to +nil+.
- # <tt>include_replicas:</tt> Determines whether to include replicas in
- # the returned list. Most of the time we're only iterating over the write
- # connection (i.e. migrations don't need to run for the write and read connection).
- # Defaults to +false+.
+ # * <tt>env_name:</tt> The environment name. Defaults to +nil+ which will collect
+ # configs for all environments.
+ # * <tt>spec_name:</tt> The specification name (i.e. primary, animals, etc.). Defaults
+ # to +nil+.
+ # * <tt>include_replicas:</tt> Determines whether to include replicas in
+ # the returned list. Most of the time we're only iterating over the write
+ # connection (i.e. migrations don't need to run for the write and read connection).
+ # Defaults to +false+.
def configs_for(env_name: nil, spec_name: nil, include_replicas: false)
configs = env_with_configs(env_name)
@@ -53,7 +53,7 @@ module ActiveRecord
# Returns the config hash that corresponds with the environment
#
- # If the application has multiple databases `default_hash` will
+ # If the application has multiple databases +default_hash+ will
# return the first config hash for the environment.
#
# { database: "my_db", adapter: "mysql2" }
@@ -65,7 +65,7 @@ module ActiveRecord
# Returns a single DatabaseConfig object based on the requested environment.
#
- # If the application has multiple databases `find_db_config` will return
+ # If the application has multiple databases +find_db_config+ will return
# the first DatabaseConfig for the environment.
def find_db_config(env)
configurations.find do |db_config|
diff --git a/activerecord/lib/active_record/database_configurations/hash_config.rb b/activerecord/lib/active_record/database_configurations/hash_config.rb
index 574cb6bf15..e31ff09391 100644
--- a/activerecord/lib/active_record/database_configurations/hash_config.rb
+++ b/activerecord/lib/active_record/database_configurations/hash_config.rb
@@ -14,16 +14,16 @@ module ActiveRecord
# #<ActiveRecord::DatabaseConfigurations::HashConfig:0x00007fd1acbded10
# @env_name="development", @spec_name="primary", @config={"database"=>"db_name"}>
#
- # Options are:
+ # ==== Options
#
- # <tt>:env_name</tt> - The Rails environment, i.e. "development"
- # <tt>:spec_name</tt> - The specification name. In a standard two-tier
- # database configuration this will default to "primary". In a multiple
- # database three-tier database configuration this corresponds to the name
- # used in the second tier, for example "primary_readonly".
- # <tt>:config</tt> - The config hash. This is the hash that contains the
- # database adapter, name, and other important information for database
- # connections.
+ # * <tt>:env_name</tt> - The Rails environment, i.e. "development".
+ # * <tt>:spec_name</tt> - The specification name. In a standard two-tier
+ # database configuration this will default to "primary". In a multiple
+ # database three-tier database configuration this corresponds to the name
+ # used in the second tier, for example "primary_readonly".
+ # * <tt>:config</tt> - The config hash. This is the hash that contains the
+ # database adapter, name, and other important information for database
+ # connections.
class HashConfig < DatabaseConfig
attr_reader :config
@@ -33,14 +33,14 @@ module ActiveRecord
end
# Determines whether a database configuration is for a replica / readonly
- # connection. If the `replica` key is present in the config, `replica?` will
+ # connection. If the +replica+ key is present in the config, +replica?+ will
# return +true+.
def replica?
config["replica"]
end
# The migrations paths for a database configuration. If the
- # `migrations_paths` key is present in the config, `migrations_paths`
+ # +migrations_paths+ key is present in the config, +migrations_paths+
# will return its value.
def migrations_paths
config["migrations_paths"]
diff --git a/activerecord/lib/active_record/database_configurations/url_config.rb b/activerecord/lib/active_record/database_configurations/url_config.rb
index 8e8aa69478..e2d30ae416 100644
--- a/activerecord/lib/active_record/database_configurations/url_config.rb
+++ b/activerecord/lib/active_record/database_configurations/url_config.rb
@@ -17,17 +17,17 @@ module ActiveRecord
# @config={"adapter"=>"postgresql", "database"=>"foo", "host"=>"localhost"},
# @url="postgres://localhost/foo">
#
- # Options are:
+ # ==== Options
#
- # <tt>:env_name</tt> - The Rails environment, ie "development"
- # <tt>:spec_name</tt> - The specification name. In a standard two-tier
- # database configuration this will default to "primary". In a multiple
- # database three-tier database configuration this corresponds to the name
- # used in the second tier, for example "primary_readonly".
- # <tt>:url</tt> - The database URL.
- # <tt>:config</tt> - The config hash. This is the hash that contains the
- # database adapter, name, and other important information for database
- # connections.
+ # * <tt>:env_name</tt> - The Rails environment, ie "development".
+ # * <tt>:spec_name</tt> - The specification name. In a standard two-tier
+ # database configuration this will default to "primary". In a multiple
+ # database three-tier database configuration this corresponds to the name
+ # used in the second tier, for example "primary_readonly".
+ # * <tt>:url</tt> - The database URL.
+ # * <tt>:config</tt> - The config hash. This is the hash that contains the
+ # database adapter, name, and other important information for database
+ # connections.
class UrlConfig < DatabaseConfig
attr_reader :url, :config
@@ -42,14 +42,14 @@ module ActiveRecord
end
# Determines whether a database configuration is for a replica / readonly
- # connection. If the `replica` key is present in the config, `replica?` will
+ # connection. If the +replica+ key is present in the config, +replica?+ will
# return +true+.
def replica?
config["replica"]
end
# The migrations paths for a database configuration. If the
- # `migrations_paths` key is present in the config, `migrations_paths`
+ # +migrations_paths+ key is present in the config, +migrations_paths+
# will return its value.
def migrations_paths
config["migrations_paths"]
diff --git a/activerecord/lib/active_record/dynamic_matchers.rb b/activerecord/lib/active_record/dynamic_matchers.rb
index 3bb8c6f4e3..398a029068 100644
--- a/activerecord/lib/active_record/dynamic_matchers.rb
+++ b/activerecord/lib/active_record/dynamic_matchers.rb
@@ -49,11 +49,11 @@ module ActiveRecord
attr_reader :model, :name, :attribute_names
- def initialize(model, name)
+ def initialize(model, method_name)
@model = model
- @name = name.to_s
+ @name = method_name.to_s
@attribute_names = @name.match(self.class.pattern)[1].split("_and_")
- @attribute_names.map! { |n| @model.attribute_aliases[n] || n }
+ @attribute_names.map! { |name| @model.attribute_aliases[name] || name }
end
def valid?
diff --git a/activerecord/lib/active_record/gem_version.rb b/activerecord/lib/active_record/gem_version.rb
index f77bc2e3c1..7f92174f87 100644
--- a/activerecord/lib/active_record/gem_version.rb
+++ b/activerecord/lib/active_record/gem_version.rb
@@ -8,9 +8,9 @@ module ActiveRecord
module VERSION
MAJOR = 6
- MINOR = 0
+ MINOR = 1
TINY = 0
- PRE = "beta3"
+ PRE = "alpha"
STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
end
diff --git a/activerecord/lib/active_record/insert_all.rb b/activerecord/lib/active_record/insert_all.rb
index 98c98d61cd..f6577dcbc4 100644
--- a/activerecord/lib/active_record/insert_all.rb
+++ b/activerecord/lib/active_record/insert_all.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module ActiveRecord
- class InsertAll
+ class InsertAll # :nodoc:
attr_reader :model, :connection, :inserts, :keys
attr_reader :on_duplicate, :returning, :unique_by
@@ -14,19 +14,28 @@ module ActiveRecord
@returning = (connection.supports_insert_returning? ? primary_keys : false) if @returning.nil?
@returning = false if @returning == []
+ @unique_by = find_unique_index_for(unique_by) if unique_by
@on_duplicate = :skip if @on_duplicate == :update && updatable_columns.empty?
ensure_valid_options_for_connection!
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
keys - readonly_columns - unique_by_columns
end
+ def primary_keys
+ Array(model.primary_key)
+ end
+
+
def skip_duplicates?
on_duplicate == :skip
end
@@ -47,20 +56,31 @@ module ActiveRecord
end
private
+ def find_unique_index_for(unique_by)
+ match = Array(unique_by).map(&:to_s)
+
+ if index = unique_indexes.find { |i| match.include?(i.name) || i.columns == match }
+ index
+ else
+ raise ArgumentError, "No unique index found for #{unique_by}"
+ end
+ end
+
+ def unique_indexes
+ connection.schema_cache.indexes(model.table_name).select(&:unique)
+ end
+
+
def ensure_valid_options_for_connection!
if returning && !connection.supports_insert_returning?
raise ArgumentError, "#{connection.class} does not support :returning"
end
- unless %i{ raise skip update }.member?(on_duplicate)
- raise NotImplementedError, "#{on_duplicate.inspect} is an unknown value for :on_duplicate. Valid values are :raise, :skip, and :update"
- end
-
- if on_duplicate == :skip && !connection.supports_insert_on_duplicate_skip?
+ if skip_duplicates? && !connection.supports_insert_on_duplicate_skip?
raise ArgumentError, "#{connection.class} does not support skipping duplicates"
end
- if on_duplicate == :update && !connection.supports_insert_on_duplicate_update?
+ if update_duplicates? && !connection.supports_insert_on_duplicate_update?
raise ArgumentError, "#{connection.class} does not support upsert"
end
@@ -69,21 +89,20 @@ module ActiveRecord
end
end
+
def to_sql
connection.build_insert_sql(ActiveRecord::InsertAll::Builder.new(self))
end
+
def readonly_columns
primary_keys + model.readonly_attributes.to_a
end
def unique_by_columns
- unique_by ? unique_by.fetch(:columns).map(&:to_s) : []
+ Array(unique_by&.columns)
end
- def primary_keys
- Array.wrap(model.primary_key)
- end
def verify_attributes(attributes)
if keys != attributes.keys.to_set
@@ -95,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
@@ -106,25 +125,27 @@ 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
end
def returning
- quote_columns(insert_all.returning).join(",") if insert_all.returning
+ format_columns(insert_all.returning) if insert_all.returning
end
def conflict_target
- return unless conflict_columns
- sql = +"(#{quote_columns(conflict_columns).join(',')})"
- sql << " WHERE #{where}" if where
- sql
+ if index = insert_all.unique_by
+ sql = +"(#{format_columns(index.columns)})"
+ sql << " WHERE #{index.where}" if index.where
+ sql
+ elsif update_duplicates?
+ "(#{format_columns(insert_all.primary_keys)})"
+ end
end
def updatable_columns
@@ -135,7 +156,7 @@ module ActiveRecord
attr_reader :connection, :insert_all
def columns_list
- quote_columns(insert_all.keys).join(",")
+ format_columns(insert_all.keys)
end
def extract_types_from_columns_on(table_name, keys:)
@@ -147,20 +168,12 @@ module ActiveRecord
keys.map { |key| [ key, connection.lookup_cast_type_from_column(columns[key]) ] }.to_h
end
- def quote_columns(columns)
- columns.map(&connection.method(:quote_column_name))
- end
-
- def conflict_columns
- @conflict_columns ||= begin
- conflict_columns = insert_all.unique_by.fetch(:columns) if insert_all.unique_by
- conflict_columns ||= Array.wrap(model.primary_key) if update_duplicates?
- conflict_columns
- end
+ def format_columns(columns)
+ quote_columns(columns).join(",")
end
- def where
- insert_all.unique_by && insert_all.unique_by[:where]
+ def quote_columns(columns)
+ columns.map(&connection.method(:quote_column_name))
end
end
end
diff --git a/activerecord/lib/active_record/integration.rb b/activerecord/lib/active_record/integration.rb
index fa6f0d36ec..573a823dbc 100644
--- a/activerecord/lib/active_record/integration.rb
+++ b/activerecord/lib/active_record/integration.rb
@@ -22,6 +22,14 @@ module ActiveRecord
#
# This is +true+, by default on Rails 5.2 and above.
class_attribute :cache_versioning, instance_writer: false, default: false
+
+ ##
+ # :singleton-method:
+ # Indicates whether to use a stable #cache_key method that is accompanied
+ # by a changing version in the #cache_version method on collections.
+ #
+ # This is +false+, by default until Rails 6.1.
+ class_attribute :collection_cache_versioning, instance_writer: false, default: false
end
# Returns a +String+, which Action Pack uses for constructing a URL to this
@@ -152,6 +160,10 @@ module ActiveRecord
end
end
end
+
+ def collection_cache_key(collection = all, timestamp_column = :updated_at) # :nodoc:
+ collection.send(:compute_cache_key, timestamp_column)
+ end
end
private
@@ -180,7 +192,7 @@ module ActiveRecord
# raw_timestamp_to_cache_version(timestamp)
# # => "20181015200215266505"
#
- # Postgres truncates trailing zeros,
+ # PostgreSQL truncates trailing zeros,
# https://github.com/postgres/postgres/commit/3e1beda2cde3495f41290e1ece5d544525810214
# to account for this we pad the output with zeros
def raw_timestamp_to_cache_version(timestamp)
diff --git a/activerecord/lib/active_record/internal_metadata.rb b/activerecord/lib/active_record/internal_metadata.rb
index 88b0c828ae..e6166581f1 100644
--- a/activerecord/lib/active_record/internal_metadata.rb
+++ b/activerecord/lib/active_record/internal_metadata.rb
@@ -17,7 +17,7 @@ module ActiveRecord
end
def table_name
- "#{table_name_prefix}#{ActiveRecord::Base.internal_metadata_table_name}#{table_name_suffix}"
+ "#{table_name_prefix}#{internal_metadata_table_name}#{table_name_suffix}"
end
def []=(key, value)
@@ -44,6 +44,10 @@ module ActiveRecord
end
end
end
+
+ def drop_table
+ connection.drop_table table_name, if_exists: true
+ end
end
end
end
diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb
index 4a3a31fc95..6711ee9bf4 100644
--- a/activerecord/lib/active_record/locking/optimistic.rb
+++ b/activerecord/lib/active_record/locking/optimistic.rb
@@ -71,9 +71,8 @@ module ActiveRecord
end
def _touch_row(attribute_names, time)
+ @_touch_attr_names << self.class.locking_column if locking_enabled?
super
- ensure
- clear_attribute_change(self.class.locking_column) if locking_enabled?
end
def _update_row(attribute_names, attempted_action = "update")
@@ -88,7 +87,7 @@ module ActiveRecord
affected_rows = self.class._update_record(
attributes_with_values(attribute_names),
- self.class.primary_key => id_in_database,
+ @primary_key => id_in_database,
locking_column => previous_lock_value
)
@@ -111,7 +110,7 @@ module ActiveRecord
locking_column = self.class.locking_column
affected_rows = self.class._delete_record(
- self.class.primary_key => id_in_database,
+ @primary_key => id_in_database,
locking_column => read_attribute_before_type_cast(locking_column)
)
diff --git a/activerecord/lib/active_record/log_subscriber.rb b/activerecord/lib/active_record/log_subscriber.rb
index 6b84431343..6248c2f578 100644
--- a/activerecord/lib/active_record/log_subscriber.rb
+++ b/activerecord/lib/active_record/log_subscriber.rb
@@ -110,7 +110,7 @@ module ActiveRecord
end
def extract_query_source_location(locations)
- backtrace_cleaner.clean(locations).first
+ backtrace_cleaner.clean(locations.lazy).first
end
end
end
diff --git a/activerecord/lib/active_record/middleware/database_selector.rb b/activerecord/lib/active_record/middleware/database_selector.rb
index b5b5df074c..93a1a39c3e 100644
--- a/activerecord/lib/active_record/middleware/database_selector.rb
+++ b/activerecord/lib/active_record/middleware/database_selector.rb
@@ -35,10 +35,10 @@ module ActiveRecord
# config.active_record.database_resolver = MyResolver
# config.active_record.database_resolver_context = MyResolver::MySession
class DatabaseSelector
- def initialize(app, resolver_klass = Resolver, context_klass = Resolver::Session, options = {})
+ def initialize(app, resolver_klass = nil, context_klass = nil, options = {})
@app = app
- @resolver_klass = resolver_klass
- @context_klass = context_klass
+ @resolver_klass = resolver_klass || Resolver
+ @context_klass = context_klass || Resolver::Session
@options = options
end
diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb
index 997b7f763a..f20edbeb93 100644
--- a/activerecord/lib/active_record/migration.rb
+++ b/activerecord/lib/active_record/migration.rb
@@ -4,9 +4,10 @@ require "benchmark"
require "set"
require "zlib"
require "active_support/core_ext/module/attribute_accessors"
+require "active_support/actionable_error"
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 +88,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 +98,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 +118,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 +128,13 @@ module ActiveRecord
end
end
- class PendingMigrationError < MigrationError#:nodoc:
+ class PendingMigrationError < MigrationError #:nodoc:
+ include ActiveSupport::ActionableError
+
+ action "Run pending migrations" do
+ ActiveRecord::Tasks::DatabaseTasks.migrate
+ end
+
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 +527,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 +548,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 +575,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 +602,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 +625,7 @@ module ActiveRecord
end
end
- def disable_ddl_transaction # :nodoc:
+ def disable_ddl_transaction #:nodoc:
self.class.disable_ddl_transaction
end
@@ -693,7 +700,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 +1013,7 @@ module ActiveRecord
end
end
- class MigrationContext # :nodoc:
+ class MigrationContext #:nodoc:
attr_reader :migrations_paths
def initialize(migrations_paths)
@@ -1165,7 +1172,7 @@ module ActiveRecord
end
end
- class Migrator # :nodoc:
+ class Migrator #:nodoc:
class << self
attr_accessor :migrations_paths
diff --git a/activerecord/lib/active_record/migration/command_recorder.rb b/activerecord/lib/active_record/migration/command_recorder.rb
index 8e7f596076..efed4b0e26 100644
--- a/activerecord/lib/active_record/migration/command_recorder.rb
+++ b/activerecord/lib/active_record/migration/command_recorder.rb
@@ -14,6 +14,8 @@ module ActiveRecord
# * change_column
# * change_column_default (must supply a :from and :to option)
# * change_column_null
+ # * change_column_comment (must supply a :from and :to option)
+ # * change_table_comment (must supply a :from and :to option)
# * create_join_table
# * create_table
# * disable_extension
@@ -35,7 +37,8 @@ module ActiveRecord
:change_column_default, :add_reference, :remove_reference, :transaction,
:drop_join_table, :drop_table, :execute_block, :enable_extension, :disable_extension,
:change_column, :execute, :remove_columns, :change_column_null,
- :add_foreign_key, :remove_foreign_key
+ :add_foreign_key, :remove_foreign_key,
+ :change_column_comment, :change_table_comment
]
include JoinTable
@@ -244,6 +247,26 @@ module ActiveRecord
[:add_foreign_key, reversed_args]
end
+ def invert_change_column_comment(args)
+ table, column, options = *args
+
+ unless options && options.is_a?(Hash) && options.has_key?(:from) && options.has_key?(:to)
+ raise ActiveRecord::IrreversibleMigration, "change_column_comment is only reversible if given a :from and :to option."
+ end
+
+ [:change_column_comment, [table, column, from: options[:to], to: options[:from]]]
+ end
+
+ def invert_change_table_comment(args)
+ table, options = *args
+
+ unless options && options.is_a?(Hash) && options.has_key?(:from) && options.has_key?(:to)
+ raise ActiveRecord::IrreversibleMigration, "change_table_comment is only reversible if given a :from and :to option."
+ end
+
+ [:change_table_comment, [table, from: options[:to], to: options[:from]]]
+ end
+
def respond_to_missing?(method, _)
super || delegate.respond_to?(method)
end
diff --git a/activerecord/lib/active_record/migration/compatibility.rb b/activerecord/lib/active_record/migration/compatibility.rb
index abc939826b..ef78a9161e 100644
--- a/activerecord/lib/active_record/migration/compatibility.rb
+++ b/activerecord/lib/active_record/migration/compatibility.rb
@@ -13,7 +13,10 @@ module ActiveRecord
const_get(name)
end
- V6_0 = Current
+ V6_1 = Current
+
+ class V6_0 < V6_1
+ end
class V5_2 < V6_0
module TableDefinition
@@ -27,6 +30,16 @@ module ActiveRecord
def invert_transaction(args, &block)
[:transaction, args, block]
end
+
+ def invert_change_column_comment(args)
+ table_name, column_name, comment = args
+ [:change_column_comment, [table_name, column_name, from: comment, to: comment]]
+ end
+
+ def invert_change_table_comment(args)
+ table_name, comment = args
+ [:change_table_comment, [table_name, from: comment, to: comment]]
+ end
end
def create_table(table_name, **options)
diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb
index 0c31f0f57e..a58c5fd48a 100644
--- a/activerecord/lib/active_record/persistence.rb
+++ b/activerecord/lib/active_record/persistence.rb
@@ -57,203 +57,188 @@ module ActiveRecord
end
end
- # Inserts a single record into the database. This method constructs a single SQL INSERT
- # statement and sends it straight to the database. It does not instantiate the involved
- # models and it does not trigger Active Record callbacks or validations. However, values
- # passed to #insert will still go through Active Record's normal type casting and
- # serialization.
+ # Inserts 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.
#
# See <tt>ActiveRecord::Persistence#insert_all</tt> for documentation.
def insert(attributes, returning: nil, unique_by: nil)
insert_all([ attributes ], returning: returning, unique_by: unique_by)
end
- # Inserts multiple records into the database. This method constructs a single SQL INSERT
- # statement and sends it straight to the database. It does not instantiate the involved
- # models and it does not trigger Active Record callbacks or validations. However, values
- # passed to #insert_all will still go through Active Record's normal type casting and
- # serialization.
+ # Inserts multiple records 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.
#
- # The +attributes+ parameter is an Array of Hashes. These Hashes describe the
- # attributes on the objects that are to be created. All of the Hashes must have
- # same keys.
+ # The +attributes+ parameter is an Array of Hashes. Every Hash determines
+ # the attributes for a single row and must have the same keys.
#
- # Records that would violate a unique constraint on the table are skipped.
+ # Rows are considered to be unique by every unique index on the table. Any
+ # duplicate rows are skipped.
+ # Override with <tt>:unique_by</tt> (see below).
#
- # Returns an <tt>ActiveRecord::Result</tt>. The contents of the result depend on the
- # value of <tt>:returning</tt> (see below).
+ # Returns an <tt>ActiveRecord::Result</tt> with its contents based on
+ # <tt>:returning</tt> (see below).
#
# ==== Options
#
# [:returning]
- # (Postgres-only) An array of attributes that should be returned for all successfully
- # inserted records. For databases that support <tt>INSERT ... RETURNING</tt>, this will default
- # to returning the primary keys of the successfully inserted records. Pass
- # <tt>returning: %w[ id name ]</tt> to return the id and name of every successfully inserted
- # record or pass <tt>returning: false</tt> to omit the clause.
+ # (PostgreSQL only) An array of attributes to return for all successfully
+ # inserted records, which by default is the primary key.
+ # Pass <tt>returning: %w[ id name ]</tt> for both id and name
+ # or <tt>returning: false</tt> to omit the underlying <tt>RETURNING</tt> SQL
+ # clause entirely.
#
# [:unique_by]
- # (Postgres and SQLite only) In a table with more than one unique constraint or index,
- # new records may be considered duplicates according to different criteria. By default,
- # new rows will be skipped if they violate _any_ unique constraint or index. By defining
- # <tt>:unique_by</tt>, you can skip rows that would create duplicates according to the given
- # constraint but raise <tt>ActiveRecord::RecordNotUnique</tt> if rows violate other constraints.
+ # (PostgreSQL and SQLite only) By default rows are considered to be unique
+ # by every unique index on the table. Any duplicate rows are skipped.
#
- # (For example, maybe you assume a client will try to import the same ISBNs more than
- # once and want to silently ignore the duplicate records, but you don't except any of
- # your code to attempt to create two rows with the same primary key and would appreciate
- # an exception report in that scenario.)
+ # To skip rows according to just one unique index pass <tt>:unique_by</tt>.
#
- # Indexes can be identified by an array of columns:
+ # Consider a Book model where no duplicate ISBNs make sense, but if any
+ # row has an existing id, or is not unique by another unique index,
+ # <tt>ActiveRecord::RecordNotUnique</tt> is raised.
#
- # unique_by: { columns: %w[ isbn ] }
+ # Unique indexes can be identified by columns or name:
#
- # Partial indexes can be identified by an array of columns and a <tt>:where</tt> condition:
+ # unique_by: :isbn
+ # unique_by: %i[ author_id name ]
+ # unique_by: :index_books_on_isbn
#
- # unique_by: { columns: %w[ isbn ], where: "published_on IS NOT NULL" }
+ # Because it relies on the index information from the database
+ # <tt>:unique_by</tt> is recommended to be paired with
+ # Active Record's schema_cache.
#
# ==== Example
#
- # # Insert multiple records and skip duplicates
- # # ('Eloquent Ruby' will be skipped because its id is duplicate)
+ # # Insert records and skip inserting any duplicates.
+ # # Here "Eloquent Ruby" is skipped because its id is not unique.
+ #
# Book.insert_all([
- # { id: 1, title: 'Rework', author: 'David' },
- # { id: 1, title: 'Eloquent Ruby', author: 'Russ' }
+ # { id: 1, title: "Rework", author: "David" },
+ # { id: 1, title: "Eloquent Ruby", author: "Russ" }
# ])
- #
def insert_all(attributes, returning: nil, unique_by: nil)
InsertAll.new(self, attributes, on_duplicate: :skip, returning: returning, unique_by: unique_by).execute
end
- # Inserts a single record into the database. This method constructs a single SQL INSERT
- # statement and sends it straight to the database. It does not instantiate the involved
- # models and it does not trigger Active Record callbacks or validations. However, values
- # passed to #insert! will still go through Active Record's normal type casting and
- # serialization.
+ # Inserts 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.
#
- # See <tt>ActiveRecord::Persistence#insert_all!</tt> for documentation.
+ # See <tt>ActiveRecord::Persistence#insert_all!</tt> for more.
def insert!(attributes, returning: nil)
insert_all!([ attributes ], returning: returning)
end
- # Inserts multiple records into the database. This method constructs a single SQL INSERT
- # statement and sends it straight to the database. It does not instantiate the involved
- # models and it does not trigger Active Record callbacks or validations. However, values
- # passed to #insert_all! will still go through Active Record's normal type casting and
- # serialization.
+ # Inserts multiple records 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.
#
- # The +attributes+ parameter is an Array of Hashes. These Hashes describe the
- # attributes on the objects that are to be created. All of the Hashes must have
- # same keys.
+ # The +attributes+ parameter is an Array of Hashes. Every Hash determines
+ # the attributes for a single row and must have the same keys.
#
- # #insert_all! will raise <tt>ActiveRecord::RecordNotUnique</tt> if any of the records being
- # inserts would violate a unique constraint on the table. In that case, no records
- # would be inserted.
+ # Raises <tt>ActiveRecord::RecordNotUnique</tt> if any rows violate a
+ # unique index on the table. In that case, no rows are inserted.
#
- # To skip duplicate records, see <tt>ActiveRecord::Persistence#insert_all</tt>.
+ # To skip duplicate rows, see <tt>ActiveRecord::Persistence#insert_all</tt>.
# To replace them, see <tt>ActiveRecord::Persistence#upsert_all</tt>.
#
- # Returns an <tt>ActiveRecord::Result</tt>. The contents of the result depend on the
- # value of <tt>:returning</tt> (see below).
+ # Returns an <tt>ActiveRecord::Result</tt> with its contents based on
+ # <tt>:returning</tt> (see below).
#
# ==== Options
#
# [:returning]
- # (Postgres-only) An array of attributes that should be returned for all successfully
- # inserted records. For databases that support <tt>INSERT ... RETURNING</tt>, this will default
- # to returning the primary keys of the successfully inserted records. Pass
- # <tt>returning: %w[ id name ]</tt> to return the id and name of every successfully inserted
- # record or pass <tt>returning: false</tt> to omit the clause.
+ # (PostgreSQL only) An array of attributes to return for all successfully
+ # inserted records, which by default is the primary key.
+ # Pass <tt>returning: %w[ id name ]</tt> for both id and name
+ # or <tt>returning: false</tt> to omit the underlying <tt>RETURNING</tt> SQL
+ # clause entirely.
#
# ==== Examples
#
# # Insert multiple records
# Book.insert_all!([
- # { title: 'Rework', author: 'David' },
- # { title: 'Eloquent Ruby', author: 'Russ' }
+ # { title: "Rework", author: "David" },
+ # { title: "Eloquent Ruby", author: "Russ" }
# ])
#
- # # Raises ActiveRecord::RecordNotUnique because 'Eloquent Ruby'
- # # does not have a unique ID
+ # # Raises ActiveRecord::RecordNotUnique because "Eloquent Ruby"
+ # # does not have a unique id.
# Book.insert_all!([
- # { id: 1, title: 'Rework', author: 'David' },
- # { id: 1, title: 'Eloquent Ruby', author: 'Russ' }
+ # { id: 1, title: "Rework", author: "David" },
+ # { id: 1, title: "Eloquent Ruby", author: "Russ" }
# ])
- #
def insert_all!(attributes, returning: nil)
InsertAll.new(self, attributes, on_duplicate: :raise, returning: returning).execute
end
- # Upserts (updates or inserts) a single record into the database. This method constructs
- # a single SQL INSERT statement and sends it straight to the database. It does not
- # instantiate the involved models and it does not trigger Active Record callbacks or
- # validations. However, values passed to #upsert will still go through Active Record's
- # normal type casting and serialization.
+ # 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.
#
# See <tt>ActiveRecord::Persistence#upsert_all</tt> for documentation.
def upsert(attributes, returning: nil, unique_by: nil)
upsert_all([ attributes ], returning: returning, unique_by: unique_by)
end
- # Upserts (updates or inserts) multiple records into the database. This method constructs
- # a single SQL INSERT statement and sends it straight to the database. It does not
- # instantiate the involved models and it does not trigger Active Record callbacks or
- # validations. However, values passed to #upsert_all will still go through Active Record's
- # normal type casting and serialization.
+ # Updates or inserts (upserts) multiple records 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.
#
- # The +attributes+ parameter is an Array of Hashes. These Hashes describe the
- # attributes on the objects that are to be created. All of the Hashes must have
- # same keys.
+ # The +attributes+ parameter is an Array of Hashes. Every Hash determines
+ # the attributes for a single row and must have the same keys.
#
- # Returns an <tt>ActiveRecord::Result</tt>. The contents of the result depend on the
- # value of <tt>:returning</tt> (see below).
+ # Returns an <tt>ActiveRecord::Result</tt> with its contents based on
+ # <tt>:returning</tt> (see below).
#
# ==== Options
#
# [:returning]
- # (Postgres-only) An array of attributes that should be returned for all successfully
- # inserted records. For databases that support <tt>INSERT ... RETURNING</tt>, this will default
- # to returning the primary keys of the successfully inserted records. Pass
- # <tt>returning: %w[ id name ]</tt> to return the id and name of every successfully inserted
- # record or pass <tt>returning: false</tt> to omit the clause.
+ # (PostgreSQL only) An array of attributes to return for all successfully
+ # inserted records, which by default is the primary key.
+ # Pass <tt>returning: %w[ id name ]</tt> for both id and name
+ # or <tt>returning: false</tt> to omit the underlying <tt>RETURNING</tt> SQL
+ # clause entirely.
#
# [:unique_by]
- # (Postgres and SQLite only) In a table with more than one unique constraint or index,
- # new records may be considered duplicates according to different criteria. For MySQL,
- # an upsert will take place if a new record violates _any_ unique constraint. For
- # Postgres and SQLite, new rows will replace existing rows when the new row has the
- # same primary key as the existing row. In case of SQLite, an upsert operation causes
- # an insert to behave as an update or a no-op if the insert would violate
- # a uniqueness constraint. By defining <tt>:unique_by</tt>, you can supply
- # a different unique constraint for matching new records to existing ones than the
- # primary key.
+ # (PostgreSQL and SQLite only) By default rows are considered to be unique
+ # by every unique index on the table. Any duplicate rows are skipped.
#
- # (For example, if you have a unique index on the ISBN column and use that as
- # the <tt>:unique_by</tt>, a new record with the same ISBN as an existing record
- # will replace the existing record but a new record with the same primary key
- # as an existing record will raise <tt>ActiveRecord::RecordNotUnique</tt>.)
+ # To skip rows according to just one unique index pass <tt>:unique_by</tt>.
#
- # Indexes can be identified by an array of columns:
+ # Consider a Book model where no duplicate ISBNs make sense, but if any
+ # row has an existing id, or is not unique by another unique index,
+ # <tt>ActiveRecord::RecordNotUnique</tt> is raised.
#
- # unique_by: { columns: %w[ isbn ] }
+ # Unique indexes can be identified by columns or name:
#
- # Partial indexes can be identified by an array of columns and a <tt>:where</tt> condition:
+ # unique_by: :isbn
+ # unique_by: %i[ author_id name ]
+ # unique_by: :index_books_on_isbn
#
- # unique_by: { columns: %w[ isbn ], where: "published_on IS NOT NULL" }
+ # Because it relies on the index information from the database
+ # <tt>:unique_by</tt> is recommended to be paired with
+ # Active Record's schema_cache.
#
# ==== Examples
#
- # # Given a unique index on <tt>books.isbn</tt> and the following record:
- # Book.create!(title: 'Rework', author: 'David', isbn: '1')
+ # # Inserts multiple records, performing an upsert when records have duplicate ISBNs.
+ # # Here "Eloquent Ruby" overwrites "Rework" because its ISBN is duplicate.
#
- # # Insert multiple records, allowing new records with the same ISBN
- # # as an existing record to overwrite the existing record.
- # # ('Eloquent Ruby' will overwrite 'Rework' because its ISBN is duplicate)
# Book.upsert_all([
- # { title: 'Eloquent Ruby', author: 'Russ', isbn: '1' },
- # { title: 'Clean Code', author: 'Robert', isbn: '2' }
- # ], unique_by: { columns: %w[ isbn ] })
+ # { title: "Rework", author: "David", isbn: "1" },
+ # { title: "Eloquent Ruby", author: "Russ", isbn: "1" }
+ # ], unique_by: :isbn)
#
+ # Book.find_by(isbn: "1").title # => "Eloquent Ruby"
def upsert_all(attributes, returning: nil, unique_by: nil)
InsertAll.new(self, attributes, on_duplicate: :update, returning: returning, unique_by: unique_by).execute
end
@@ -368,6 +353,7 @@ module ActiveRecord
end
def _insert_record(values) # :nodoc:
+ primary_key = self.primary_key
primary_key_value = nil
if primary_key && Hash === values
@@ -438,20 +424,20 @@ module ActiveRecord
# Returns true if this object hasn't been saved yet -- that is, a record
# for the object doesn't exist in the database yet; otherwise, returns false.
def new_record?
- sync_with_transaction_state
+ sync_with_transaction_state if @transaction_state&.finalized?
@new_record
end
# Returns true if this object has been destroyed, otherwise returns false.
def destroyed?
- sync_with_transaction_state
+ sync_with_transaction_state if @transaction_state&.finalized?
@destroyed
end
# Returns true if the record is persisted, i.e. it's not a new record and it was
# not destroyed, otherwise returns false.
def persisted?
- sync_with_transaction_state
+ sync_with_transaction_state if @transaction_state&.finalized?
!(@new_record || @destroyed)
end
@@ -545,7 +531,6 @@ module ActiveRecord
def destroy
_raise_readonly_record_error if readonly?
destroy_associations
- self.class.connection.add_transaction_record(self)
@_trigger_destroy_callback = if persisted?
destroy_row > 0
else
@@ -583,7 +568,6 @@ module ActiveRecord
became.send(:initialize)
became.instance_variable_set("@attributes", @attributes)
became.instance_variable_set("@mutations_from_database", @mutations_from_database ||= nil)
- became.instance_variable_set("@changed_attributes", attributes_changed_by_setter)
became.instance_variable_set("@new_record", new_record?)
became.instance_variable_set("@destroyed", destroyed?)
became.errors.copy!(errors)
@@ -680,8 +664,13 @@ module ActiveRecord
raise ActiveRecordError, "cannot update a new record" if new_record?
raise ActiveRecordError, "cannot update a destroyed record" if destroyed?
+ attributes = attributes.transform_keys do |key|
+ name = key.to_s
+ self.class.attribute_aliases[name] || name
+ end
+
attributes.each_key do |key|
- verify_readonly_attribute(key.to_s)
+ verify_readonly_attribute(key)
end
id_in_database = self.id_in_database
@@ -691,7 +680,7 @@ module ActiveRecord
affected_rows = self.class._update_record(
attributes,
- self.class.primary_key => id_in_database
+ @primary_key => id_in_database
)
affected_rows == 1
@@ -860,15 +849,12 @@ module ActiveRecord
# ball.touch(:updated_at) # => raises ActiveRecordError
#
def touch(*names, time: nil)
- unless persisted?
- raise ActiveRecordError, <<-MSG.squish
- cannot touch on a new or destroyed record object. Consider using
- persisted?, new_record?, or destroyed? before touching
- MSG
- end
+ _raise_record_not_touched_error unless persisted?
attribute_names = timestamp_attributes_for_update_in_model
- attribute_names |= names.map(&:to_s)
+ attribute_names |= names.map!(&:to_s).map! { |name|
+ self.class.attribute_aliases[name] || name
+ }
unless attribute_names.empty?
affected_rows = _touch_row(attribute_names, time)
@@ -889,15 +875,14 @@ module ActiveRecord
end
def _delete_row
- self.class._delete_record(self.class.primary_key => id_in_database)
+ self.class._delete_record(@primary_key => id_in_database)
end
def _touch_row(attribute_names, time)
time ||= current_time_from_proper_timezone
attribute_names.each do |attr_name|
- write_attribute(attr_name, time)
- clear_attribute_change(attr_name)
+ _write_attribute(attr_name, time)
end
_update_row(attribute_names, "touch")
@@ -906,7 +891,7 @@ module ActiveRecord
def _update_row(attribute_names, attempted_action = "update")
self.class._update_record(
attributes_with_values(attribute_names),
- self.class.primary_key => id_in_database
+ @primary_key => id_in_database
)
end
@@ -944,7 +929,7 @@ module ActiveRecord
attributes_with_values(attribute_names)
)
- self.id ||= new_id if self.class.primary_key
+ self.id ||= new_id if @primary_key
@new_record = false
@@ -964,14 +949,21 @@ module ActiveRecord
@_association_destroy_exception = nil
end
+ def _raise_readonly_record_error
+ raise ReadOnlyRecord, "#{self.class} is marked as readonly"
+ end
+
+ def _raise_record_not_touched_error
+ raise ActiveRecordError, <<~MSG.squish
+ Cannot touch on a new or destroyed record object. Consider using
+ persisted?, new_record?, or destroyed? before touching.
+ MSG
+ end
+
# The name of the method used to touch a +belongs_to+ association when the
# +:touch+ option is used.
def belongs_to_touch_method
:touch
end
-
- def _raise_readonly_record_error
- raise ReadOnlyRecord, "#{self.class} is marked as readonly"
- end
end
end
diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb
index 86021f0a80..08cfc3fe5f 100644
--- a/activerecord/lib/active_record/querying.rb
+++ b/activerecord/lib/active_record/querying.rb
@@ -10,12 +10,12 @@ module ActiveRecord
:first_or_create, :first_or_create!, :first_or_initialize,
:find_or_create_by, :find_or_create_by!, :find_or_initialize_by,
:create_or_find_by, :create_or_find_by!,
- :destroy_all, :delete_all, :update_all, :destroy_by, :delete_by,
+ :destroy_all, :delete_all, :update_all, :touch_all, :destroy_by, :delete_by,
:find_each, :find_in_batches, :in_batches,
:select, :reselect, :order, :reorder, :group, :limit, :offset, :joins, :left_joins, :left_outer_joins,
- :where, :rewhere, :preload, :eager_load, :includes, :from, :lock, :readonly, :extending, :or,
+ :where, :rewhere, :preload, :extract_associated, :eager_load, :includes, :from, :lock, :readonly, :extending, :or,
:having, :create_with, :distinct, :references, :none, :unscope, :optimizer_hints, :merge, :except, :only,
- :count, :average, :minimum, :maximum, :sum, :calculate,
+ :count, :average, :minimum, :maximum, :sum, :calculate, :annotate,
:pluck, :pick, :ids
].freeze # :nodoc:
delegate(*QUERYING_METHODS, to: :all)
diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake
index f021a8f6c4..e0bc5180c0 100644
--- a/activerecord/lib/active_record/railties/databases.rake
+++ b/activerecord/lib/active_record/railties/databases.rake
@@ -78,7 +78,7 @@ db_namespace = namespace :db do
desc "Migrate the database (options: VERSION=x, VERBOSE=false, SCOPE=blog)."
task migrate: :load_config do
- ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).each do |db_config|
+ ActiveRecord::Base.configurations.configs_for(env_name: ActiveRecord::Tasks::DatabaseTasks.env).each do |db_config|
ActiveRecord::Base.establish_connection(db_config.config)
ActiveRecord::Tasks::DatabaseTasks.migrate
end
@@ -128,6 +128,8 @@ db_namespace = namespace :db do
# desc 'Runs the "up" for a given migration VERSION.'
task up: :load_config do
+ ActiveRecord::Tasks::DatabaseTasks.raise_for_multi_db(command: "db:migrate:up")
+
raise "VERSION is required" if !ENV["VERSION"] || ENV["VERSION"].empty?
ActiveRecord::Tasks::DatabaseTasks.check_target_version
@@ -139,8 +141,29 @@ db_namespace = namespace :db do
db_namespace["_dump"].invoke
end
+ namespace :up do
+ ActiveRecord::Tasks::DatabaseTasks.for_each do |spec_name|
+ task spec_name => :load_config do
+ raise "VERSION is required" if !ENV["VERSION"] || ENV["VERSION"].empty?
+
+ db_config = ActiveRecord::Base.configurations.configs_for(env_name: Rails.env, spec_name: spec_name)
+
+ ActiveRecord::Base.establish_connection(db_config.config)
+ ActiveRecord::Tasks::DatabaseTasks.check_target_version
+ ActiveRecord::Base.connection.migration_context.run(
+ :up,
+ ActiveRecord::Tasks::DatabaseTasks.target_version
+ )
+
+ db_namespace["_dump"].invoke
+ end
+ end
+ end
+
# desc 'Runs the "down" for a given migration VERSION.'
task down: :load_config do
+ ActiveRecord::Tasks::DatabaseTasks.raise_for_multi_db(command: "db:migrate:down")
+
raise "VERSION is required - To go down one migration, use db:rollback" if !ENV["VERSION"] || ENV["VERSION"].empty?
ActiveRecord::Tasks::DatabaseTasks.check_target_version
@@ -152,9 +175,28 @@ db_namespace = namespace :db do
db_namespace["_dump"].invoke
end
+ namespace :down do
+ ActiveRecord::Tasks::DatabaseTasks.for_each do |spec_name|
+ task spec_name => :load_config do
+ raise "VERSION is required" if !ENV["VERSION"] || ENV["VERSION"].empty?
+
+ db_config = ActiveRecord::Base.configurations.configs_for(env_name: Rails.env, spec_name: spec_name)
+
+ ActiveRecord::Base.establish_connection(db_config.config)
+ ActiveRecord::Tasks::DatabaseTasks.check_target_version
+ ActiveRecord::Base.connection.migration_context.run(
+ :down,
+ ActiveRecord::Tasks::DatabaseTasks.target_version
+ )
+
+ db_namespace["_dump"].invoke
+ end
+ end
+ end
+
desc "Display status of migrations"
task status: :load_config do
- ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).each do |db_config|
+ ActiveRecord::Base.configurations.configs_for(env_name: ActiveRecord::Tasks::DatabaseTasks.env).each do |db_config|
ActiveRecord::Base.establish_connection(db_config.config)
ActiveRecord::Tasks::DatabaseTasks.migrate_status
end
@@ -222,6 +264,16 @@ db_namespace = namespace :db do
desc "Creates the database, loads the schema, and initializes with the seed data (use db:reset to also drop the database first)"
task setup: ["db:schema:load_if_ruby", "db:structure:load_if_sql", :seed]
+ desc "Runs setup if database does not exist, or runs migrations if it does"
+ task prepare: :load_config do
+ ActiveRecord::Base.configurations.configs_for(env_name: ActiveRecord::Tasks::DatabaseTasks.env).each do |db_config|
+ ActiveRecord::Base.establish_connection(db_config.config)
+ db_namespace["migrate"].invoke
+ rescue ActiveRecord::NoDatabaseError
+ db_namespace["setup"].invoke
+ end
+ end
+
desc "Loads the seed data from db/seeds.rb"
task seed: :load_config do
db_namespace["abort_if_pending_migrations"].invoke
@@ -285,7 +337,7 @@ db_namespace = namespace :db do
desc "Creates a db/schema.rb file that is portable against any DB supported by Active Record"
task dump: :load_config do
require "active_record/schema_dumper"
- ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).each do |db_config|
+ ActiveRecord::Base.configurations.configs_for(env_name: ActiveRecord::Tasks::DatabaseTasks.env).each do |db_config|
filename = ActiveRecord::Tasks::DatabaseTasks.dump_filename(db_config.spec_name, :ruby)
File.open(filename, "w:utf-8") do |file|
ActiveRecord::Base.establish_connection(db_config.config)
@@ -308,7 +360,7 @@ db_namespace = namespace :db do
namespace :cache do
desc "Creates a db/schema_cache.yml file."
task dump: :load_config do
- ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).each do |db_config|
+ ActiveRecord::Base.configurations.configs_for(env_name: ActiveRecord::Tasks::DatabaseTasks.env).each do |db_config|
ActiveRecord::Base.establish_connection(db_config.config)
filename = ActiveRecord::Tasks::DatabaseTasks.cache_dump_filename(db_config.spec_name)
ActiveRecord::Tasks::DatabaseTasks.dump_schema_cache(
@@ -320,7 +372,7 @@ db_namespace = namespace :db do
desc "Clears a db/schema_cache.yml file."
task clear: :load_config do
- ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).each do |db_config|
+ ActiveRecord::Base.configurations.configs_for(env_name: ActiveRecord::Tasks::DatabaseTasks.env).each do |db_config|
filename = ActiveRecord::Tasks::DatabaseTasks.cache_dump_filename(db_config.spec_name)
rm_f filename, verbose: false
end
@@ -331,7 +383,7 @@ db_namespace = namespace :db do
namespace :structure do
desc "Dumps the database structure to db/structure.sql. Specify another file with SCHEMA=db/my_structure.sql"
task dump: :load_config do
- ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).each do |db_config|
+ ActiveRecord::Base.configurations.configs_for(env_name: ActiveRecord::Tasks::DatabaseTasks.env).each do |db_config|
ActiveRecord::Base.establish_connection(db_config.config)
filename = ActiveRecord::Tasks::DatabaseTasks.dump_filename(db_config.spec_name, :sql)
ActiveRecord::Tasks::DatabaseTasks.structure_dump(db_config.config, filename)
diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb
index 3452cf971b..eefda2b8f4 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
@@ -477,7 +477,7 @@ module ActiveRecord
def check_preloadable!
return unless scope
- if scope.arity > 0
+ unless scope.arity == 0
raise ArgumentError, <<-MSG.squish
The association scope '#{name}' is instance dependent (the scope
block takes an argument). Preloading instance dependent scopes is
diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb
index 6d954a2b2e..ea8f44752b 100644
--- a/activerecord/lib/active_record/relation.rb
+++ b/activerecord/lib/active_record/relation.rb
@@ -5,7 +5,7 @@ module ActiveRecord
class Relation
MULTI_VALUE_METHODS = [:includes, :eager_load, :preload, :select, :group,
:order, :joins, :left_outer_joins, :references,
- :extending, :unscope, :optimizer_hints]
+ :extending, :unscope, :optimizer_hints, :annotate]
SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :reordering,
:reverse_order, :distinct, :create_with, :skip_query_cache]
@@ -291,31 +291,99 @@ module ActiveRecord
limit_value ? records.many? : size > 1
end
- # Returns a cache key that can be used to identify the records fetched by
- # this query. The cache key is built with a fingerprint of the sql query,
- # the number of records matched by the query and a timestamp of the last
- # updated record. When a new record comes to match the query, or any of
- # the existing records is updated or deleted, the cache key changes.
+ # Returns a stable cache key that can be used to identify this query.
+ # The cache key is built with a fingerprint of the SQL query.
#
- # Product.where("name like ?", "%Cosmic Encounter%").cache_key
- # # => "products/query-1850ab3d302391b85b8693e941286659-1-20150714212553907087000"
+ # Product.where("name like ?", "%Cosmic Encounter%").cache_key
+ # # => "products/query-1850ab3d302391b85b8693e941286659"
#
- # If the collection is loaded, the method will iterate through the records
- # to generate the timestamp, otherwise it will trigger one SQL query like:
+ # If ActiveRecord::Base.collection_cache_versioning is turned off, as it was
+ # in Rails 6.0 and earlier, the cache key will also include a version.
#
- # SELECT COUNT(*), MAX("products"."updated_at") FROM "products" WHERE (name like '%Cosmic Encounter%')
+ # ActiveRecord::Base.collection_cache_versioning = false
+ # Product.where("name like ?", "%Cosmic Encounter%").cache_key
+ # # => "products/query-1850ab3d302391b85b8693e941286659-1-20150714212553907087000"
#
# You can also pass a custom timestamp column to fetch the timestamp of the
# last updated record.
#
# Product.where("name like ?", "%Game%").cache_key(:last_reviewed_at)
- #
- # You can customize the strategy to generate the key on a per model basis
- # overriding ActiveRecord::Base#collection_cache_key.
def cache_key(timestamp_column = :updated_at)
@cache_keys ||= {}
- @cache_keys[timestamp_column] ||= @klass.collection_cache_key(self, timestamp_column)
+ @cache_keys[timestamp_column] ||= klass.collection_cache_key(self, timestamp_column)
+ end
+
+ def compute_cache_key(timestamp_column = :updated_at) # :nodoc:
+ query_signature = ActiveSupport::Digest.hexdigest(to_sql)
+ key = "#{klass.model_name.cache_key}/query-#{query_signature}"
+
+ if cache_version(timestamp_column)
+ key
+ else
+ "#{key}-#{compute_cache_version(timestamp_column)}"
+ end
+ end
+ private :compute_cache_key
+
+ # Returns a cache version that can be used together with the cache key to form
+ # a recyclable caching scheme. The cache version is built with the number of records
+ # matching the query, and the timestamp of the last updated record. When a new record
+ # comes to match the query, or any of the existing records is updated or deleted,
+ # the cache version changes.
+ #
+ # If the collection is loaded, the method will iterate through the records
+ # to generate the timestamp, otherwise it will trigger one SQL query like:
+ #
+ # SELECT COUNT(*), MAX("products"."updated_at") FROM "products" WHERE (name like '%Cosmic Encounter%')
+ def cache_version(timestamp_column = :updated_at)
+ if collection_cache_versioning
+ @cache_versions ||= {}
+ @cache_versions[timestamp_column] ||= compute_cache_version(timestamp_column)
+ end
+ end
+
+ def compute_cache_version(timestamp_column) # :nodoc:
+ if loaded? || distinct_value
+ size = records.size
+ if size > 0
+ timestamp = max_by(&timestamp_column)._read_attribute(timestamp_column)
+ end
+ else
+ collection = eager_loading? ? apply_join_dependency : self
+
+ column = connection.visitor.compile(arel_attribute(timestamp_column))
+ select_values = "COUNT(*) AS #{connection.quote_column_name("size")}, MAX(%s) AS timestamp"
+
+ if collection.has_limit_or_offset?
+ query = collection.select("#{column} AS collection_cache_key_timestamp")
+ subquery_alias = "subquery_for_cache_key"
+ subquery_column = "#{subquery_alias}.collection_cache_key_timestamp"
+ arel = query.build_subquery(subquery_alias, select_values % subquery_column)
+ else
+ query = collection.unscope(:order)
+ query.select_values = [select_values % column]
+ arel = query.arel
+ end
+
+ result = connection.select_one(arel, nil)
+
+ if result
+ column_type = klass.type_for_attribute(timestamp_column)
+ timestamp = column_type.deserialize(result["timestamp"])
+ size = result["size"]
+ else
+ timestamp = nil
+ size = 0
+ end
+ end
+
+ if timestamp
+ "#{size}-#{timestamp.utc.to_s(cache_timestamp_format)}"
+ else
+ "#{size}"
+ end
end
+ private :compute_cache_version
# Scope all queries to the current scope.
#
@@ -400,7 +468,19 @@ module ActiveRecord
end
end
- def update_counters(counters) # :nodoc:
+ # Updates the counters of the records in the current relation.
+ #
+ # === Parameters
+ #
+ # * +counter+ - A Hash containing the names of the fields to update as keys and the amount to update as values.
+ # * <tt>:touch</tt> option - Touch the timestamp columns when updating.
+ # * If attributes names are passed, they are updated along with update_at/on attributes.
+ #
+ # === Examples
+ #
+ # # For Posts by a given author increment the comment_count by 1.
+ # Post.where(author_id: author.id).update_counters(comment_count: 1)
+ def update_counters(counters)
touch = counters.delete(:touch)
updates = {}
@@ -485,8 +565,8 @@ module ActiveRecord
# # => ActiveRecord::ActiveRecordError: delete_all doesn't support distinct
def delete_all
invalid_methods = INVALID_METHODS_FOR_DELETE_ALL.select do |method|
- value = get_value(method)
- SINGLE_VALUE_METHODS.include?(method) ? value : value.any?
+ value = @values[method]
+ method == :distinct ? value : value&.any?
end
if invalid_methods.any?
raise ActiveRecordError.new("delete_all doesn't support #{invalid_methods.join(', ')}")
@@ -676,6 +756,10 @@ module ActiveRecord
@loaded = true
end
+ def null_relation? # :nodoc:
+ is_a?(NullRelation)
+ end
+
private
def already_in_scope?
@delegate_to_klass && begin
@@ -725,7 +809,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 4f9ddf302e..0be9ba7d7b 100644
--- a/activerecord/lib/active_record/relation/calculations.rb
+++ b/activerecord/lib/active_record/relation/calculations.rb
@@ -129,11 +129,12 @@ module ActiveRecord
relation = apply_join_dependency
if operation.to_s.downcase == "count"
- relation.distinct!
- # PostgreSQL: ORDER BY expressions must appear in SELECT list when using DISTINCT
- if (column_name == :all || column_name.nil?) && select_values.empty?
- relation.order_values = []
+ unless distinct_value || distinct_select?(column_name || select_for_count)
+ relation.distinct!
+ relation.select_values = [ klass.primary_key || table[Arel.star] ]
end
+ # PostgreSQL: ORDER BY expressions must appear in SELECT list when using DISTINCT
+ relation.order_values = []
end
relation.calculate(operation, column_name)
@@ -259,10 +260,8 @@ module ActiveRecord
def aggregate_column(column_name)
return column_name if Arel::Expressions === column_name
- if @klass.has_attribute?(column_name) || @klass.attribute_alias?(column_name)
- @klass.arel_attribute(column_name)
- else
- Arel.sql(column_name == :all ? "*" : column_name.to_s)
+ arel_column(column_name.to_s) do |name|
+ Arel.sql(column_name == :all ? "*" : name)
end
end
@@ -307,25 +306,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(
@@ -344,7 +340,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) }
@@ -370,25 +366,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)
@@ -416,16 +410,17 @@ module ActiveRecord
def build_count_subquery(relation, column_name, distinct)
if column_name == :all
+ column_alias = Arel.star
relation.select_values = [ Arel.sql(FinderMethods::ONE_AS_ONE) ] unless distinct
else
column_alias = Arel.sql("count_column")
relation.select_values = [ aggregate_column(column_name).as(column_alias) ]
end
- subquery = relation.arel.as(Arel.sql("subquery_for_count"))
- select_value = operation_over_aggregate_column(column_alias || Arel.star, "count", false)
+ subquery_alias = Arel.sql("subquery_for_count")
+ select_value = operation_over_aggregate_column(column_alias, "count", false)
- Arel::SelectManager.new(subquery).project(select_value)
+ relation.build_subquery(subquery_alias, select_value)
end
end
end
diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb
index 7a53a9d1c7..d59331053e 100644
--- a/activerecord/lib/active_record/relation/delegation.rb
+++ b/activerecord/lib/active_record/relation/delegation.rb
@@ -45,7 +45,10 @@ module ActiveRecord
private
def generated_relation_methods
- @generated_relation_methods ||= GeneratedRelationMethods.new
+ @generated_relation_methods ||= GeneratedRelationMethods.new.tap do |mod|
+ const_set(:GeneratedRelationMethods, mod)
+ private_constant :GeneratedRelationMethods
+ end
end
end
diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb
index e2efd4aa0d..9c7ac80447 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
@@ -370,14 +370,10 @@ module ActiveRecord
relation
end
- def construct_join_dependency(associations)
- ActiveRecord::Associations::JoinDependency.new(
- klass, table, associations
- )
- end
-
def apply_join_dependency(eager_loading: group_values.empty?)
- join_dependency = construct_join_dependency(eager_load_values + includes_values)
+ join_dependency = construct_join_dependency(
+ eager_load_values + includes_values, Arel::Nodes::OuterJoin
+ )
relation = except(:includes, :eager_load, :preload).joins!(join_dependency)
if eager_loading && !using_limitable_reflections?(join_dependency.reflections)
diff --git a/activerecord/lib/active_record/relation/merger.rb b/activerecord/lib/active_record/relation/merger.rb
index 4de7465128..84fe424ef0 100644
--- a/activerecord/lib/active_record/relation/merger.rb
+++ b/activerecord/lib/active_record/relation/merger.rb
@@ -117,16 +117,16 @@ module ActiveRecord
if other.klass == relation.klass
relation.joins!(*other.joins_values)
else
- joins_dependency = other.joins_values.map do |join|
+ associations, others = other.joins_values.partition do |join|
case join
- when Hash, Symbol, Array
- other.send(:construct_join_dependency, join)
- else
- join
+ when Hash, Symbol, Array; true
end
end
- relation.joins!(*joins_dependency)
+ join_dependency = other.construct_join_dependency(
+ associations, Arel::Nodes::InnerJoin
+ )
+ relation.joins!(join_dependency, *others)
end
end
@@ -136,16 +136,11 @@ module ActiveRecord
if other.klass == relation.klass
relation.left_outer_joins!(*other.left_outer_joins_values)
else
- joins_dependency = other.left_outer_joins_values.map do |join|
- case join
- when Hash, Symbol, Array
- other.send(:construct_join_dependency, join)
- else
- join
- end
- end
-
- relation.left_outer_joins!(*joins_dependency)
+ associations = other.left_outer_joins_values
+ join_dependency = other.construct_join_dependency(
+ associations, Arel::Nodes::OuterJoin
+ )
+ relation.joins!(join_dependency)
end
end
diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb
index 05ea0850d3..50ff733dc7 100644
--- a/activerecord/lib/active_record/relation/query_methods.rb
+++ b/activerecord/lib/active_record/relation/query_methods.rb
@@ -41,18 +41,31 @@ module ActiveRecord
#
# User.where.not(name: %w(Ko1 Nobu))
# # SELECT * FROM users WHERE name NOT IN ('Ko1', 'Nobu')
- #
- # User.where.not(name: "Jon", role: "admin")
- # # SELECT * FROM users WHERE name != 'Jon' AND role != 'admin'
def not(opts, *rest)
opts = sanitize_forbidden_attributes(opts)
where_clause = @scope.send(:where_clause_factory).build(opts, rest)
@scope.references!(PredicateBuilder.references(opts)) if Hash === opts
- @scope.where_clause += where_clause.invert
+
+ if not_behaves_as_nor?(opts)
+ ActiveSupport::Deprecation.warn(<<~MSG.squish)
+ NOT conditions will no longer behave as NOR in Rails 6.1.
+ To continue using NOR conditions, NOT each conditions manually
+ (`#{ opts.keys.map { |key| ".where.not(#{key.inspect} => ...)" }.join }`).
+ MSG
+ @scope.where_clause += where_clause.invert(:nor)
+ else
+ @scope.where_clause += where_clause.invert
+ end
+
@scope
end
+
+ private
+ def not_behaves_as_nor?(opts)
+ opts.is_a?(Hash) && opts.size > 1
+ end
end
FROZEN_EMPTY_ARRAY = [].freeze
@@ -67,11 +80,13 @@ module ActiveRecord
end
class_eval <<-CODE, __FILE__, __LINE__ + 1
def #{method_name} # def includes_values
- get_value(#{name.inspect}) # get_value(:includes)
+ default = DEFAULT_VALUES[:#{name}] # default = DEFAULT_VALUES[:includes]
+ @values.fetch(:#{name}, default) # @values.fetch(:includes, default)
end # end
def #{method_name}=(value) # def includes_values=(value)
- set_value(#{name.inspect}, value) # set_value(:includes, value)
+ assert_mutability! # assert_mutability!
+ @values[:#{name}] = value # @values[:includes] = value
end # end
CODE
end
@@ -100,7 +115,7 @@ module ActiveRecord
#
# === conditions
#
- # If you want to add conditions to your included models you'll have
+ # If you want to add string conditions to your included models, you'll have
# to explicitly reference them. For example:
#
# User.includes(:posts).where('posts.name = ?', 'example')
@@ -111,6 +126,12 @@ module ActiveRecord
#
# Note that #includes works with association names while #references needs
# the actual table name.
+ #
+ # If you pass the conditions via hash, you don't need to call #references
+ # explicitly, as #where references the tables for you. For example, this
+ # will work correctly:
+ #
+ # User.includes(:posts).where(posts: { name: 'example' })
def includes(*args)
check_if_method_has_arguments!(:includes, args)
spawn.includes!(*args)
@@ -154,6 +175,19 @@ module ActiveRecord
self
end
+ # Extracts a named +association+ from the relation. The named association is first preloaded,
+ # then the individual association records are collected from the relation. Like so:
+ #
+ # account.memberships.extract_associated(:user)
+ # # => Returns collection of User records
+ #
+ # This is short-hand for:
+ #
+ # account.memberships.preload(:user).collect(&:user)
+ def extract_associated(association)
+ preload(association).collect(&association)
+ end
+
# Use to indicate that the given +table_names+ are referenced by an SQL string,
# and should therefore be JOINed in any query rather than loaded separately.
# This method only works in conjunction with #includes.
@@ -233,9 +267,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
@@ -349,7 +380,7 @@ module ActiveRecord
end
VALID_UNSCOPING_VALUES = Set.new([:where, :select, :group, :order, :lock,
- :limit, :offset, :joins, :left_outer_joins,
+ :limit, :offset, :joins, :left_outer_joins, :annotate,
:includes, :from, :readonly, :having, :optimizer_hints])
# Removes an unwanted relation that is already defined on a chain of relations.
@@ -401,7 +432,8 @@ module ActiveRecord
if !VALID_UNSCOPING_VALUES.include?(scope)
raise ArgumentError, "Called unscope() with invalid unscoping argument ':#{scope}'. Valid arguments are :#{VALID_UNSCOPING_VALUES.to_a.join(", :")}."
end
- set_value(scope, DEFAULT_VALUES[scope])
+ assert_mutability!
+ @values[scope] = DEFAULT_VALUES[scope]
when Hash
scope.each do |key, target_value|
if key != :where
@@ -948,23 +980,47 @@ module ActiveRecord
self
end
+ # Adds an SQL comment to queries generated from this relation. For example:
+ #
+ # User.annotate("selecting user names").select(:name)
+ # # SELECT "users"."name" FROM "users" /* selecting user names */
+ #
+ # User.annotate("selecting", "user", "names").select(:name)
+ # # SELECT "users"."name" FROM "users" /* selecting */ /* user */ /* names */
+ #
+ # The SQL block comment delimiters, "/*" and "*/", will be added automatically.
+ def annotate(*args)
+ check_if_method_has_arguments!(:annotate, args)
+ spawn.annotate!(*args)
+ end
+
+ # Like #annotate, but modifies relation in place.
+ def annotate!(*args) # :nodoc:
+ self.annotate_values += args
+ self
+ end
+
# Returns the Arel object associated with the relation.
def arel(aliases = nil) # :nodoc:
@arel ||= build_arel(aliases)
end
- private
- # Returns a relation value with a given name
- def get_value(name)
- @values.fetch(name, DEFAULT_VALUES[name])
- end
+ def construct_join_dependency(associations, join_type) # :nodoc:
+ ActiveRecord::Associations::JoinDependency.new(
+ klass, table, associations, join_type
+ )
+ end
+
+ protected
+ def build_subquery(subquery_alias, select_value) # :nodoc:
+ subquery = except(:optimizer_hints).arel.as(subquery_alias)
- # Sets the relation value with the given name
- def set_value(name, value)
- assert_mutability!
- @values[name] = value
+ Arel::SelectManager.new(subquery).project(select_value).tap do |arel|
+ arel.optimizer_hints(*optimizer_hints_values) unless optimizer_hints_values.empty?
+ end
end
+ private
def assert_mutability!
raise ImmutableRelation if @loaded
raise ImmutableRelation if defined?(@arel) && @arel
@@ -973,8 +1029,11 @@ module ActiveRecord
def build_arel(aliases)
arel = Arel::SelectManager.new(table)
- aliases = build_joins(arel, joins_values.flatten, aliases) unless joins_values.empty?
- build_left_outer_joins(arel, left_outer_joins_values.flatten, aliases) unless left_outer_joins_values.empty?
+ if !joins_values.empty?
+ build_joins(arel, joins_values.flatten, aliases)
+ elsif !left_outer_joins_values.empty?
+ build_left_outer_joins(arel, left_outer_joins_values.flatten, aliases)
+ end
arel.where(where_clause.ast) unless where_clause.empty?
arel.having(having_clause.ast) unless having_clause.empty?
@@ -1004,6 +1063,7 @@ module ActiveRecord
arel.distinct(distinct_value)
arel.from(build_from) unless from_clause.empty?
arel.lock(lock_value) if lock_value
+ arel.comment(*annotate_values) unless annotate_values.empty?
arel
end
@@ -1023,22 +1083,28 @@ module ActiveRecord
end
end
- def build_left_outer_joins(manager, outer_joins, aliases)
- buckets = outer_joins.group_by do |join|
- case join
+ def valid_association_list(associations)
+ associations.each do |association|
+ case association
when Hash, Symbol, Array
- :association_join
- when ActiveRecord::Associations::JoinDependency
- :stashed_join
+ # valid
else
raise ArgumentError, "only Hash, Symbol and Array are allowed"
end
end
+ end
+ def build_left_outer_joins(manager, outer_joins, aliases)
+ buckets = { association_join: valid_association_list(outer_joins) }
build_join_query(manager, buckets, Arel::Nodes::OuterJoin, aliases)
end
def build_joins(manager, joins, aliases)
+ unless left_outer_joins_values.empty?
+ left_joins = valid_association_list(left_outer_joins_values.flatten)
+ joins.unshift construct_join_dependency(left_joins, Arel::Nodes::OuterJoin)
+ end
+
buckets = joins.group_by do |join|
case join
when String
@@ -1062,27 +1128,21 @@ module ActiveRecord
association_joins = buckets[:association_join]
stashed_joins = buckets[:stashed_join]
- join_nodes = buckets[:join_node].uniq
- string_joins = buckets[:string_join].map(&:strip).uniq
-
- join_list = join_nodes + convert_join_strings_to_ast(string_joins)
- alias_tracker = alias_tracker(join_list, aliases)
-
- join_dependency = construct_join_dependency(association_joins)
+ join_nodes = buckets[:join_node].tap(&:uniq!)
+ string_joins = buckets[:string_join].delete_if(&:blank?).map!(&:strip).tap(&:uniq!)
- joins = join_dependency.join_constraints(stashed_joins, join_type, alias_tracker)
- joins.each { |join| manager.from(join) }
+ string_joins.map! { |join| table.create_string_join(Arel.sql(join)) }
- manager.join_sources.concat(join_list)
+ join_sources = manager.join_sources
+ join_sources.concat(join_nodes) unless join_nodes.empty?
- alias_tracker.aliases
- end
+ unless association_joins.empty? && stashed_joins.empty?
+ alias_tracker = alias_tracker(join_nodes + string_joins, aliases)
+ join_dependency = construct_join_dependency(association_joins, join_type)
+ join_sources.concat(join_dependency.join_constraints(stashed_joins, alias_tracker))
+ end
- def convert_join_strings_to_ast(joins)
- joins
- .flatten
- .reject(&:blank?)
- .map { |join| table.create_string_join(Arel.sql(join)) }
+ join_sources.concat(string_joins) unless string_joins.empty?
end
def build_select(arel)
@@ -1100,9 +1160,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
@@ -1112,13 +1172,13 @@ module ActiveRecord
end
def arel_column(field)
- field = klass.attribute_alias(field) if klass.attribute_alias?(field)
+ field = klass.attribute_aliases[field] || field
from = from_clause.name || from_clause.value
if klass.columns_hash.key?(field) && (!from || table_name_matches?(from))
arel_attribute(field)
else
- yield
+ yield field
end
end
@@ -1255,7 +1315,8 @@ module ActiveRecord
def structurally_incompatible_values_for_or(other)
values = other.values
STRUCTURAL_OR_METHODS.reject do |method|
- get_value(method) == values.fetch(method, DEFAULT_VALUES[method])
+ default = DEFAULT_VALUES[method]
+ @values.fetch(method, default) == values.fetch(method, default)
end
end
diff --git a/activerecord/lib/active_record/relation/where_clause.rb b/activerecord/lib/active_record/relation/where_clause.rb
index 47728aac30..b91b135867 100644
--- a/activerecord/lib/active_record/relation/where_clause.rb
+++ b/activerecord/lib/active_record/relation/where_clause.rb
@@ -70,7 +70,15 @@ module ActiveRecord
predicates == other.predicates
end
- def invert
+ def invert(as = :nand)
+ if predicates.size == 1
+ inverted_predicates = [ invert_predicate(predicates.first) ]
+ elsif as == :nor
+ inverted_predicates = predicates.map { |node| invert_predicate(node) }
+ else
+ inverted_predicates = [ Arel::Nodes::Not.new(ast) ]
+ end
+
WhereClause.new(inverted_predicates)
end
@@ -115,10 +123,6 @@ module ActiveRecord
node.respond_to?(:operator) && node.operator == :==
end
- def inverted_predicates
- predicates.map { |node| invert_predicate(node) }
- end
-
def invert_predicate(node)
case node
when NilClass
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/schema_migration.rb b/activerecord/lib/active_record/schema_migration.rb
index 1fca1a18f6..74547de862 100644
--- a/activerecord/lib/active_record/schema_migration.rb
+++ b/activerecord/lib/active_record/schema_migration.rb
@@ -19,7 +19,7 @@ module ActiveRecord
end
def table_name
- "#{table_name_prefix}#{ActiveRecord::Base.schema_migrations_table_name}#{table_name_suffix}"
+ "#{table_name_prefix}#{schema_migrations_table_name}#{table_name_suffix}"
end
def table_exists?
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/active_record/store.rb b/activerecord/lib/active_record/store.rb
index 3537e2d008..6fecb06897 100644
--- a/activerecord/lib/active_record/store.rb
+++ b/activerecord/lib/active_record/store.rb
@@ -11,6 +11,12 @@ module ActiveRecord
# of the model. This is very helpful for easily exposing store keys to a form or elsewhere that's
# already built around just accessing attributes on the model.
#
+ # Every accessor comes with dirty tracking methods (+key_changed?+, +key_was+ and +key_change+) and
+ # methods to access the changes made during the last save (+saved_change_to_key?+, +saved_change_to_key+ and
+ # +key_before_last_save+).
+ #
+ # NOTE: There is no +key_will_change!+ method for accessors, use +store_will_change!+ instead.
+ #
# Make sure that you declare the database column used for the serialized store as a text, so there's
# plenty of room.
#
@@ -49,6 +55,12 @@ module ActiveRecord
# u.settings[:country] # => 'Denmark'
# u.settings['country'] # => 'Denmark'
#
+ # # Dirty tracking
+ # u.color = 'green'
+ # u.color_changed? # => true
+ # u.color_was # => 'black'
+ # u.color_change # => ['black', 'red']
+ #
# # Add additional accessors to an existing store through store_accessor
# class SuperUser < User
# store_accessor :settings, :privileges, :servants
@@ -127,6 +139,42 @@ module ActiveRecord
define_method(accessor_key) do
read_store_attribute(store_attribute, key)
end
+
+ define_method("#{accessor_key}_changed?") do
+ return false unless attribute_changed?(store_attribute)
+ prev_store, new_store = changes[store_attribute]
+ prev_store&.dig(key) != new_store&.dig(key)
+ end
+
+ define_method("#{accessor_key}_change") do
+ return unless attribute_changed?(store_attribute)
+ prev_store, new_store = changes[store_attribute]
+ [prev_store&.dig(key), new_store&.dig(key)]
+ end
+
+ define_method("#{accessor_key}_was") do
+ return unless attribute_changed?(store_attribute)
+ prev_store, _new_store = changes[store_attribute]
+ prev_store&.dig(key)
+ end
+
+ define_method("saved_change_to_#{accessor_key}?") do
+ return false unless saved_change_to_attribute?(store_attribute)
+ prev_store, new_store = saved_change_to_attribute(store_attribute)
+ prev_store&.dig(key) != new_store&.dig(key)
+ end
+
+ define_method("saved_change_to_#{accessor_key}") do
+ return unless saved_change_to_attribute?(store_attribute)
+ prev_store, new_store = saved_change_to_attribute(store_attribute)
+ [prev_store&.dig(key), new_store&.dig(key)]
+ end
+
+ define_method("#{accessor_key}_before_last_save") do
+ return unless saved_change_to_attribute?(store_attribute)
+ prev_store, _new_store = saved_change_to_attribute(store_attribute)
+ prev_store&.dig(key)
+ end
end
end
diff --git a/activerecord/lib/active_record/table_metadata.rb b/activerecord/lib/active_record/table_metadata.rb
index b67479fb6a..9a1176db6a 100644
--- a/activerecord/lib/active_record/table_metadata.rb
+++ b/activerecord/lib/active_record/table_metadata.rb
@@ -4,17 +4,18 @@ module ActiveRecord
class TableMetadata # :nodoc:
delegate :foreign_type, :foreign_key, :join_primary_key, :join_foreign_key, to: :association, prefix: true
- def initialize(klass, arel_table, association = nil)
+ def initialize(klass, arel_table, association = nil, types = klass)
@klass = klass
+ @types = types
@arel_table = arel_table
@association = association
end
def resolve_column_aliases(hash)
new_hash = hash.dup
- hash.each do |key, _|
- if (key.is_a?(Symbol)) && klass.attribute_alias?(key)
- new_hash[klass.attribute_alias(key)] = new_hash.delete(key)
+ hash.each_key do |key|
+ if key.is_a?(Symbol) && new_key = klass.attribute_aliases[key.to_s]
+ new_hash[new_key] = new_hash.delete(key)
end
end
new_hash
@@ -29,11 +30,7 @@ module ActiveRecord
end
def type(column_name)
- if klass
- klass.type_for_attribute(column_name)
- else
- Type.default_value
- end
+ types.type_for_attribute(column_name)
end
def has_column?(column_name)
@@ -52,13 +49,12 @@ module ActiveRecord
elsif association && !association.polymorphic?
association_klass = association.klass
arel_table = association_klass.arel_table.alias(table_name)
+ TableMetadata.new(association_klass, arel_table, association)
else
type_caster = TypeCaster::Connection.new(klass, table_name)
- association_klass = nil
arel_table = Arel::Table.new(table_name, type_caster: type_caster)
+ TableMetadata.new(nil, arel_table, association, type_caster)
end
-
- TableMetadata.new(association_klass, arel_table, association)
end
def polymorphic_association?
@@ -74,6 +70,6 @@ module ActiveRecord
end
private
- attr_reader :klass, :arel_table, :association
+ attr_reader :klass, :types, :arel_table, :association
end
end
diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb
index 155d2b0b98..c79ed8db60 100644
--- a/activerecord/lib/active_record/tasks/database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/database_tasks.rb
@@ -142,6 +142,8 @@ module ActiveRecord
end
def for_each
+ return {} unless defined?(Rails)
+
databases = Rails.application.config.load_database_yaml
database_configs = ActiveRecord::DatabaseConfigurations.new(databases).configs_for(env_name: Rails.env)
@@ -153,6 +155,20 @@ module ActiveRecord
end
end
+ def raise_for_multi_db(environment = env, command:)
+ db_configs = ActiveRecord::Base.configurations.configs_for(env_name: environment)
+
+ if db_configs.count > 1
+ dbs_list = []
+
+ db_configs.each do |db|
+ dbs_list << "#{command}:#{db.spec_name}"
+ end
+
+ raise "You're using a multiple database application. To use `#{command}` you must run the namespaced task with a VERSION. Available tasks are #{dbs_list.to_sentence}."
+ end
+ end
+
def create_current(environment = env)
each_current_configuration(environment) { |configuration|
create configuration
@@ -186,8 +202,8 @@ module ActiveRecord
ActiveRecord::Base.connected_to(database: { truncation: configuration }) do
table_names = ActiveRecord::Base.connection.tables
table_names -= [
- ActiveRecord::Base.schema_migrations_table_name,
- ActiveRecord::Base.internal_metadata_table_name
+ SchemaMigration.table_name,
+ InternalMetadata.table_name
]
ActiveRecord::Base.connection.truncate_tables(*table_names)
diff --git a/activerecord/lib/active_record/touch_later.rb b/activerecord/lib/active_record/touch_later.rb
index f70b7c50a2..bc63c8d987 100644
--- a/activerecord/lib/active_record/touch_later.rb
+++ b/activerecord/lib/active_record/touch_later.rb
@@ -2,7 +2,7 @@
module ActiveRecord
# = Active Record Touch Later
- module TouchLater
+ module TouchLater # :nodoc:
extend ActiveSupport::Concern
included do
@@ -10,19 +10,15 @@ module ActiveRecord
end
def touch_later(*names) # :nodoc:
- unless persisted?
- raise ActiveRecordError, <<-MSG.squish
- cannot touch on a new or destroyed record object. Consider using
- persisted?, new_record?, or destroyed? before touching
- MSG
- end
+ _raise_record_not_touched_error unless persisted?
@_defer_touch_attrs ||= timestamp_attributes_for_update_in_model
@_defer_touch_attrs |= names
@_touch_time = current_time_from_proper_timezone
surreptitiously_touch @_defer_touch_attrs
- self.class.connection.add_transaction_record self
+ add_to_transaction
+ @_new_record_before_last_commit ||= false
# touch the parents as we are not calling the after_save callbacks
self.class.reflect_on_all_associations(:belongs_to).each do |r|
@@ -48,6 +44,7 @@ module ActiveRecord
def touch_deferred_attributes
if has_defer_touch_attrs? && persisted?
+ @_skip_dirty_tracking = true
touch(*@_defer_touch_attrs, time: @_touch_time)
@_defer_touch_attrs, @_touch_time = nil, nil
end
diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb
index fe3842b905..cbb970ac98 100644
--- a/activerecord/lib/active_record/transactions.rb
+++ b/activerecord/lib/active_record/transactions.rb
@@ -164,12 +164,12 @@ module ActiveRecord
# end
# end
#
- # only "Kotori" is created. This works on MySQL and PostgreSQL. SQLite3 version >= '3.6.8' also supports it.
+ # only "Kotori" is created.
#
# Most databases don't support true nested transactions. At the time of
# writing, the only database that we're aware of that supports true nested
# transactions, is MS-SQL. Because of this, Active Record emulates nested
- # transactions by using savepoints on MySQL and PostgreSQL. See
+ # transactions by using savepoints. See
# https://dev.mysql.com/doc/refman/5.7/en/savepoint.html
# for more information about savepoints.
#
@@ -234,6 +234,12 @@ module ActiveRecord
set_callback(:commit, :after, *args, &block)
end
+ # Shortcut for <tt>after_commit :hook, on: [ :create, :update ]</tt>.
+ def after_save_commit(*args, &block)
+ set_options_for_callbacks!(args, on: [ :create, :update ])
+ set_callback(:commit, :after, *args, &block)
+ end
+
# Shortcut for <tt>after_commit :hook, on: :create</tt>.
def after_create_commit(*args, &block)
set_options_for_callbacks!(args, on: :create)
@@ -327,7 +333,7 @@ module ActiveRecord
# Ensure that it is not called if the object was never persisted (failed create),
# but call it after the commit of a destroyed object.
def committed!(should_run_callbacks: true) #:nodoc:
- if should_run_callbacks && (destroyed? || persisted?)
+ if should_run_callbacks
@_committed_already_called = true
_run_commit_without_transaction_enrollment_callbacks
_run_commit_callbacks
@@ -349,18 +355,6 @@ module ActiveRecord
clear_transaction_record_state
end
- # Add the record to the current transaction so that the #after_rollback and #after_commit callbacks
- # can be called.
- def add_to_transaction
- if has_transactional_callbacks?
- self.class.connection.add_transaction_record(self)
- else
- sync_with_transaction_state
- set_transaction_state(self.class.connection.transaction_state)
- end
- remember_transaction_record_state
- end
-
# Executes +method+ within a transaction and captures its return value as a
# status flag. If the status is true the transaction is committed, otherwise
# a ROLLBACK is issued. In any case the status flag is returned.
@@ -370,29 +364,40 @@ module ActiveRecord
def with_transaction_returning_status
status = nil
self.class.transaction do
- add_to_transaction
+ if has_transactional_callbacks?
+ add_to_transaction
+ else
+ sync_with_transaction_state if @transaction_state&.finalized?
+ @transaction_state = self.class.connection.transaction_state
+ end
+ remember_transaction_record_state
+
status = yield
raise ActiveRecord::Rollback unless status
end
status
end
+ def trigger_transactional_callbacks? # :nodoc:
+ (@_new_record_before_last_commit || _trigger_update_callback) && persisted? ||
+ _trigger_destroy_callback && destroyed?
+ end
+
private
attr_reader :_committed_already_called, :_trigger_update_callback, :_trigger_destroy_callback
# Save the new record state and id of a record so it can be restored later if a transaction fails.
def remember_transaction_record_state
- @_start_transaction_state.reverse_merge!(
+ @_start_transaction_state ||= {
id: id,
new_record: @new_record,
destroyed: @destroyed,
+ attributes: @attributes,
frozen?: frozen?,
- )
- @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) + 1
- remember_new_record_before_last_commit
- end
+ level: 0
+ }
+ @_start_transaction_state[:level] += 1
- def remember_new_record_before_last_commit
if _committed_already_called
@_new_record_before_last_commit = false
else
@@ -402,27 +407,32 @@ module ActiveRecord
# Clear the new record state and id of a record.
def clear_transaction_record_state
- @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1
+ return unless @_start_transaction_state
+ @_start_transaction_state[:level] -= 1
force_clear_transaction_record_state if @_start_transaction_state[:level] < 1
end
# Force to clear the transaction record state.
def force_clear_transaction_record_state
- @_start_transaction_state.clear
+ @_start_transaction_state = nil
+ @transaction_state = nil
end
# Restore the new record state and id of a record that was previously saved by a call to save_record_state.
- def restore_transaction_record_state(force = false)
- unless @_start_transaction_state.empty?
- transaction_level = (@_start_transaction_state[:level] || 0) - 1
- if transaction_level < 1 || force
- restore_state = @_start_transaction_state
- thaw
+ def restore_transaction_record_state(force_restore_state = false)
+ if restore_state = @_start_transaction_state
+ if force_restore_state || restore_state[:level] <= 1
@new_record = restore_state[:new_record]
@destroyed = restore_state[:destroyed]
- pk = self.class.primary_key
- if pk && _read_attribute(pk) != restore_state[:id]
- _write_attribute(pk, restore_state[:id])
+ @attributes = restore_state[:attributes].map do |attr|
+ value = @attributes.fetch_value(attr.name)
+ attr = attr.with_value_from_user(value) if attr.value != value
+ attr
+ end
+ @mutations_from_database = nil
+ @mutations_before_last_save = nil
+ if @attributes.fetch_value(@primary_key) != restore_state[:id]
+ @attributes.write_from_user(@primary_key, restore_state[:id])
end
freeze if restore_state[:frozen?]
end
@@ -443,8 +453,10 @@ module ActiveRecord
end
end
- def set_transaction_state(state)
- @transaction_state = state
+ # Add the record to the current transaction so that the #after_rollback and #after_commit
+ # callbacks can be called.
+ def add_to_transaction
+ self.class.connection.add_transaction_record(self)
end
def has_transactional_callbacks?
@@ -464,19 +476,17 @@ module ActiveRecord
# This method checks to see if the ActiveRecord object's state reflects
# the TransactionState, and rolls back or commits the Active Record object
# as appropriate.
- #
- # Since Active Record objects can be inside multiple transactions, this
- # method recursively goes through the parent of the TransactionState and
- # checks if the Active Record object reflects the state of the object.
def sync_with_transaction_state
- update_attributes_from_transaction_state(@transaction_state)
- end
-
- def update_attributes_from_transaction_state(transaction_state)
- if transaction_state && transaction_state.finalized?
- restore_transaction_record_state(transaction_state.fully_rolledback?) if transaction_state.rolledback?
- force_clear_transaction_record_state if transaction_state.fully_committed?
- clear_transaction_record_state if transaction_state.fully_completed?
+ if transaction_state = @transaction_state
+ if transaction_state.fully_committed?
+ force_clear_transaction_record_state
+ elsif transaction_state.committed?
+ clear_transaction_record_state
+ elsif transaction_state.rolledback?
+ force_restore_state = transaction_state.fully_rolledback?
+ restore_transaction_record_state(force_restore_state)
+ clear_transaction_record_state
+ end
end
end
end
diff --git a/activerecord/lib/active_record/type_caster/connection.rb b/activerecord/lib/active_record/type_caster/connection.rb
index 7cf8181d8e..f43559f4cb 100644
--- a/activerecord/lib/active_record/type_caster/connection.rb
+++ b/activerecord/lib/active_record/type_caster/connection.rb
@@ -8,21 +8,27 @@ module ActiveRecord
@table_name = table_name
end
- def type_cast_for_database(attribute_name, value)
+ def type_cast_for_database(attr_name, value)
return value if value.is_a?(Arel::Nodes::BindParam)
- column = column_for(attribute_name)
- connection.type_cast_from_column(column, value)
+ type = type_for_attribute(attr_name)
+ type.serialize(value)
end
- private
- attr_reader :table_name
- delegate :connection, to: :@klass
+ def type_for_attribute(attr_name)
+ schema_cache = connection.schema_cache
- def column_for(attribute_name)
- if connection.schema_cache.data_source_exists?(table_name)
- connection.schema_cache.columns_hash(table_name)[attribute_name.to_s]
- end
+ if schema_cache.data_source_exists?(table_name)
+ column = schema_cache.columns_hash(table_name)[attr_name.to_s]
+ type = connection.lookup_cast_type_from_column(column) if column
end
+
+ type || Type.default_value
+ end
+
+ delegate :connection, to: :@klass, private: true
+
+ private
+ attr_reader :table_name
end
end
end