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.rb30
-rw-r--r--activerecord/lib/active_record/associations/preloader.rb5
-rw-r--r--activerecord/lib/active_record/associations/preloader/association.rb26
-rw-r--r--activerecord/lib/active_record/associations/preloader/has_many_through.rb10
-rw-r--r--activerecord/lib/active_record/associations/preloader/through_association.rb106
-rw-r--r--activerecord/lib/active_record/attribute.rb242
-rw-r--r--activerecord/lib/active_record/attribute/user_provided_default.rb32
-rw-r--r--activerecord/lib/active_record/attribute_methods.rb2
-rw-r--r--activerecord/lib/active_record/attribute_methods/dirty.rb96
-rw-r--r--activerecord/lib/active_record/attribute_mutation_tracker.rb111
-rw-r--r--activerecord/lib/active_record/attribute_set.rb113
-rw-r--r--activerecord/lib/active_record/attribute_set/builder.rb126
-rw-r--r--activerecord/lib/active_record/attribute_set/yaml_encoder.rb43
-rw-r--r--activerecord/lib/active_record/attributes.rb6
-rw-r--r--activerecord/lib/active_record/collection_cache_key.rb3
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/transaction.rb2
-rw-r--r--activerecord/lib/active_record/counter_cache.rb2
-rw-r--r--activerecord/lib/active_record/legacy_yaml_adapter.rb2
-rw-r--r--activerecord/lib/active_record/migration.rb2
-rw-r--r--activerecord/lib/active_record/model_schema.rb6
-rw-r--r--activerecord/lib/active_record/railties/databases.rake19
-rw-r--r--activerecord/lib/active_record/relation.rb10
-rw-r--r--activerecord/lib/active_record/relation/calculations.rb4
-rw-r--r--activerecord/lib/active_record/relation/finder_methods.rb2
-rw-r--r--activerecord/lib/active_record/relation/query_attribute.rb4
-rw-r--r--activerecord/lib/active_record/relation/query_methods.rb4
-rw-r--r--activerecord/lib/active_record/scoping/named.rb8
-rw-r--r--activerecord/lib/active_record/tasks/database_tasks.rb15
28 files changed, 127 insertions, 904 deletions
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb
index bc7e288500..661605d3e5 100644
--- a/activerecord/lib/active_record/associations.rb
+++ b/activerecord/lib/active_record/associations.rb
@@ -140,26 +140,6 @@ module ActiveRecord
class HasOneThroughCantAssociateThroughHasOneOrManyReflection < ThroughCantAssociateThroughHasOneOrManyReflection #:nodoc:
end
- class HasManyThroughCantAssociateNewRecords < ActiveRecordError #:nodoc:
- def initialize(owner = nil, reflection = nil)
- if owner && reflection
- super("Cannot associate new records through '#{owner.class.name}##{reflection.name}' on '#{reflection.source_reflection.class_name rescue nil}##{reflection.source_reflection.name rescue nil}'. Both records must have an id in order to create the has_many :through record associating them.")
- else
- super("Cannot associate new records.")
- end
- end
- end
-
- class HasManyThroughCantDissociateNewRecords < ActiveRecordError #:nodoc:
- def initialize(owner = nil, reflection = nil)
- if owner && reflection
- super("Cannot dissociate new records through '#{owner.class.name}##{reflection.name}' on '#{reflection.source_reflection.class_name rescue nil}##{reflection.source_reflection.name rescue nil}'. Both records must have an id in order to delete the has_many :through record associating them.")
- else
- super("Cannot dissociate new records.")
- end
- end
- end
-
class ThroughNestedAssociationsAreReadonly < ActiveRecordError #:nodoc:
def initialize(owner = nil, reflection = nil)
if owner && reflection
@@ -189,16 +169,6 @@ module ActiveRecord
end
end
- class ReadOnlyAssociation < ActiveRecordError #:nodoc:
- def initialize(reflection = nil)
- if reflection
- super("Cannot add to a has_many :through association. Try adding to #{reflection.through_reflection.name.inspect}.")
- else
- super("Read-only reflection error.")
- end
- end
- end
-
# This error is raised when trying to destroy a parent instance in N:1 or 1:1 associations
# (has_many, has_one) when there is at least 1 child associated instance.
# ex: if @project.tasks.size > 0, DeleteRestrictionError will be raised when trying to destroy @project
diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb
index e1754d4a19..5a93a89d0a 100644
--- a/activerecord/lib/active_record/associations/preloader.rb
+++ b/activerecord/lib/active_record/associations/preloader.rb
@@ -166,8 +166,6 @@ module ActiveRecord
end
class AlreadyLoaded # :nodoc:
- attr_reader :owners, :reflection
-
def initialize(klass, owners, reflection, preload_scope)
@owners = owners
@reflection = reflection
@@ -178,6 +176,9 @@ module ActiveRecord
def preloaded_records
owners.flat_map { |owner| owner.association(reflection.name).target }
end
+
+ protected
+ attr_reader :owners, :reflection
end
# Returns a class containing the logic needed to load preload the data
diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb
index 607d376a08..19c337dc39 100644
--- a/activerecord/lib/active_record/associations/preloader/association.rb
+++ b/activerecord/lib/active_record/associations/preloader/association.rb
@@ -4,7 +4,6 @@ module ActiveRecord
module Associations
class Preloader
class Association #:nodoc:
- attr_reader :owners, :reflection, :preload_scope, :model, :klass
attr_reader :preloaded_records
def initialize(klass, owners, reflection, preload_scope)
@@ -17,11 +16,20 @@ module ActiveRecord
end
def run(preloader)
- associated_records_by_owner(preloader).each do |owner, records|
- associate_records_to_owner(owner, records)
+ 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)
+ end
+
+ owners.each do |owner|
+ associate_records_to_owner(owner, records[convert_key(owner[owner_key_name])] || [])
end
end
+ protected
+ attr_reader :owners, :reflection, :preload_scope, :model, :klass
+
private
# The name of the key on the associated records
def association_key_name
@@ -33,18 +41,6 @@ module ActiveRecord
reflection.join_foreign_key
end
- def associated_records_by_owner(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)
- end
-
- owners.each_with_object({}) do |owner, result|
- result[owner] = records[convert_key(owner[owner_key_name])] || []
- end
- end
-
def associate_records_to_owner(owner, records)
raise NotImplementedError
end
diff --git a/activerecord/lib/active_record/associations/preloader/has_many_through.rb b/activerecord/lib/active_record/associations/preloader/has_many_through.rb
index 0639fdca44..3e17d07a33 100644
--- a/activerecord/lib/active_record/associations/preloader/has_many_through.rb
+++ b/activerecord/lib/active_record/associations/preloader/has_many_through.rb
@@ -5,16 +5,6 @@ module ActiveRecord
class Preloader
class HasManyThrough < CollectionAssociation #:nodoc:
include ThroughAssociation
-
- def associated_records_by_owner(preloader)
- records_by_owner = super
-
- if reflection_scope.distinct_value
- records_by_owner.each_value(&:uniq!)
- end
-
- records_by_owner
- end
end
end
end
diff --git a/activerecord/lib/active_record/associations/preloader/through_association.rb b/activerecord/lib/active_record/associations/preloader/through_association.rb
index fa32cc5553..762275fbad 100644
--- a/activerecord/lib/active_record/associations/preloader/through_association.rb
+++ b/activerecord/lib/active_record/associations/preloader/through_association.rb
@@ -4,77 +4,53 @@ module ActiveRecord
module Associations
class Preloader
module ThroughAssociation #:nodoc:
- def through_reflection
- reflection.through_reflection
- end
-
- def source_reflection
- reflection.source_reflection
- end
-
- def associated_records_by_owner(preloader)
- through_scope = through_scope()
-
- preloader.preload(owners,
- through_reflection.name,
- through_scope)
-
- through_records = owners.map do |owner|
- center = owner.association(through_reflection.name).target
- [owner, Array(center)]
- end
-
- reset_association(owners, through_reflection.name, through_scope)
-
- middle_records = through_records.flat_map(&:last)
-
- reflection_scope = reflection_scope() if reflection.scope
-
- preloaders = preloader.preload(middle_records,
- source_reflection.name,
- reflection_scope)
-
+ def run(preloader)
+ already_loaded = owners.first.association(through_reflection.name).loaded?
+ through_scope = through_scope()
+ reflection_scope = target_reflection_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, reflection_scope)
@preloaded_records = preloaders.flat_map(&:preloaded_records)
- middle_to_pl = preloaders.each_with_object({}) do |pl, h|
- pl.owners.each { |middle|
- h[middle] = pl
- }
- end
-
- through_records.each_with_object({}) do |(lhs, center), records_by_owner|
- pl_to_middle = center.group_by { |record| middle_to_pl[record] }
-
- records_by_owner[lhs] = pl_to_middle.flat_map do |pl, middles|
- rhs_records = middles.flat_map { |r|
- r.association(source_reflection.name).target
- }.compact
-
- # Respect the order on `reflection_scope` if it exists, else use the natural order.
- if reflection_scope && !reflection_scope.order_values.empty?
- @id_map ||= id_to_index_map @preloaded_records
- rhs_records.sort_by { |rhs| @id_map[rhs] }
- else
- rhs_records
+ owners.each do |owner|
+ through_records = Array(owner.association(through_reflection.name).target)
+ 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|
+ association = record.association(source_reflection.name)
+ target = association.target
+ association.reset if preload_scope
+ target
end
+ result.compact!
+ if reflection_scope
+ result.sort_by! { |rhs| preload_index[rhs] } if reflection_scope.order_values.any?
+ result.uniq! if reflection_scope.distinct_value
+ end
+ associate_records_to_owner(owner, result)
end
end
private
+ def through_reflection
+ reflection.through_reflection
+ end
- def id_to_index_map(ids)
- id_map = {}
- ids.each_with_index { |id, index| id_map[id] = index }
- id_map
+ def source_reflection
+ reflection.source_reflection
end
- def reset_association(owners, association_name, should_reset)
- # Don't cache the association - we would only be caching a subset
- if should_reset
- owners.each { |owner|
- owner.association(association_name).reset
- }
+ def preload_index
+ @preload_index ||= @preloaded_records.each_with_object({}).with_index do |(id, result), index|
+ result[id] = index
end
end
@@ -115,6 +91,16 @@ module ActiveRecord
scope unless scope.empty_scope?
end
+
+ def target_reflection_scope
+ if preload_scope
+ reflection_scope.merge(preload_scope)
+ elsif reflection.scope
+ reflection_scope
+ else
+ nil
+ end
+ end
end
end
end
diff --git a/activerecord/lib/active_record/attribute.rb b/activerecord/lib/active_record/attribute.rb
deleted file mode 100644
index fc474edc15..0000000000
--- a/activerecord/lib/active_record/attribute.rb
+++ /dev/null
@@ -1,242 +0,0 @@
-# frozen_string_literal: true
-
-module ActiveRecord
- class Attribute # :nodoc:
- class << self
- def from_database(name, value, type)
- FromDatabase.new(name, value, type)
- end
-
- def from_user(name, value, type, original_attribute = nil)
- FromUser.new(name, value, type, original_attribute)
- end
-
- def with_cast_value(name, value, type)
- WithCastValue.new(name, value, type)
- end
-
- def null(name)
- Null.new(name)
- end
-
- def uninitialized(name, type)
- Uninitialized.new(name, type)
- end
- end
-
- attr_reader :name, :value_before_type_cast, :type
-
- # This method should not be called directly.
- # Use #from_database or #from_user
- def initialize(name, value_before_type_cast, type, original_attribute = nil)
- @name = name
- @value_before_type_cast = value_before_type_cast
- @type = type
- @original_attribute = original_attribute
- end
-
- def value
- # `defined?` is cheaper than `||=` when we get back falsy values
- @value = type_cast(value_before_type_cast) unless defined?(@value)
- @value
- end
-
- def original_value
- if assigned?
- original_attribute.original_value
- else
- type_cast(value_before_type_cast)
- end
- end
-
- def value_for_database
- type.serialize(value)
- end
-
- def changed?
- changed_from_assignment? || changed_in_place?
- end
-
- def changed_in_place?
- has_been_read? && type.changed_in_place?(original_value_for_database, value)
- end
-
- def forgetting_assignment
- with_value_from_database(value_for_database)
- end
-
- def with_value_from_user(value)
- type.assert_valid_value(value)
- self.class.from_user(name, value, type, original_attribute || self)
- end
-
- def with_value_from_database(value)
- self.class.from_database(name, value, type)
- end
-
- def with_cast_value(value)
- self.class.with_cast_value(name, value, type)
- end
-
- def with_type(type)
- if changed_in_place?
- with_value_from_user(value).with_type(type)
- else
- self.class.new(name, value_before_type_cast, type, original_attribute)
- end
- end
-
- def type_cast(*)
- raise NotImplementedError
- end
-
- def initialized?
- true
- end
-
- def came_from_user?
- false
- end
-
- def has_been_read?
- defined?(@value)
- end
-
- def ==(other)
- self.class == other.class &&
- name == other.name &&
- value_before_type_cast == other.value_before_type_cast &&
- type == other.type
- end
- alias eql? ==
-
- def hash
- [self.class, name, value_before_type_cast, type].hash
- end
-
- def init_with(coder)
- @name = coder["name"]
- @value_before_type_cast = coder["value_before_type_cast"]
- @type = coder["type"]
- @original_attribute = coder["original_attribute"]
- @value = coder["value"] if coder.map.key?("value")
- end
-
- def encode_with(coder)
- coder["name"] = name
- coder["value_before_type_cast"] = value_before_type_cast unless value_before_type_cast.nil?
- coder["type"] = type if type
- coder["original_attribute"] = original_attribute if original_attribute
- coder["value"] = value if defined?(@value)
- end
-
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
- protected
-
- attr_reader :original_attribute
- alias_method :assigned?, :original_attribute
-
- def original_value_for_database
- if assigned?
- original_attribute.original_value_for_database
- else
- _original_value_for_database
- end
- end
-
- private
- def initialize_dup(other)
- if defined?(@value) && @value.duplicable?
- @value = @value.dup
- end
- end
-
- def changed_from_assignment?
- assigned? && type.changed?(original_value, value, value_before_type_cast)
- end
-
- def _original_value_for_database
- type.serialize(original_value)
- end
-
- class FromDatabase < Attribute # :nodoc:
- def type_cast(value)
- type.deserialize(value)
- end
-
- def _original_value_for_database
- value_before_type_cast
- end
- end
-
- class FromUser < Attribute # :nodoc:
- def type_cast(value)
- type.cast(value)
- end
-
- def came_from_user?
- !type.value_constructed_by_mass_assignment?(value_before_type_cast)
- end
- end
-
- class WithCastValue < Attribute # :nodoc:
- def type_cast(value)
- value
- end
-
- def changed_in_place?
- false
- end
- end
-
- class Null < Attribute # :nodoc:
- def initialize(name)
- super(name, nil, Type.default_value)
- end
-
- def type_cast(*)
- nil
- end
-
- def with_type(type)
- self.class.with_cast_value(name, nil, type)
- end
-
- def with_value_from_database(value)
- raise ActiveModel::MissingAttributeError, "can't write unknown attribute `#{name}`"
- end
- alias_method :with_value_from_user, :with_value_from_database
- end
-
- class Uninitialized < Attribute # :nodoc:
- UNINITIALIZED_ORIGINAL_VALUE = Object.new
-
- def initialize(name, type)
- super(name, nil, type)
- end
-
- def value
- if block_given?
- yield name
- end
- end
-
- def original_value
- UNINITIALIZED_ORIGINAL_VALUE
- end
-
- def value_for_database
- end
-
- def initialized?
- false
- end
-
- def with_type(type)
- self.class.new(name, type)
- end
- end
- private_constant :FromDatabase, :FromUser, :Null, :Uninitialized, :WithCastValue
- end
-end
diff --git a/activerecord/lib/active_record/attribute/user_provided_default.rb b/activerecord/lib/active_record/attribute/user_provided_default.rb
deleted file mode 100644
index f746960fac..0000000000
--- a/activerecord/lib/active_record/attribute/user_provided_default.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-# frozen_string_literal: true
-
-require "active_record/attribute"
-
-module ActiveRecord
- class Attribute # :nodoc:
- class UserProvidedDefault < FromUser # :nodoc:
- def initialize(name, value, type, database_default)
- @user_provided_value = value
- super(name, value, type, database_default)
- end
-
- def value_before_type_cast
- if user_provided_value.is_a?(Proc)
- @memoized_value_before_type_cast ||= user_provided_value.call
- else
- @user_provided_value
- end
- end
-
- def with_type(type)
- self.class.new(name, user_provided_value, type, original_attribute)
- end
-
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
- protected
-
- attr_reader :user_provided_value
- end
- end
-end
diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb
index 891e556dc4..23d2aef214 100644
--- a/activerecord/lib/active_record/attribute_methods.rb
+++ b/activerecord/lib/active_record/attribute_methods.rb
@@ -236,7 +236,7 @@ module ActiveRecord
return has_attribute?(name)
end
- return true
+ true
end
# Returns +true+ if the given attribute is in the attributes hash, otherwise +false+.
diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb
index 79110d04f4..3de6fe566d 100644
--- a/activerecord/lib/active_record/attribute_methods/dirty.rb
+++ b/activerecord/lib/active_record/attribute_methods/dirty.rb
@@ -1,7 +1,6 @@
# frozen_string_literal: true
require "active_support/core_ext/module/attribute_accessors"
-require "active_record/attribute_mutation_tracker"
module ActiveRecord
module AttributeMethods
@@ -33,65 +32,13 @@ 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
- @changed_attributes = ActiveSupport::HashWithIndifferentAccess.new
end
end
- def initialize_dup(other) # :nodoc:
- super
- @attributes = self.class._default_attributes.map do |attr|
- attr.with_value_from_user(@attributes.fetch_value(attr.name))
- end
- @mutations_from_database = nil
- end
-
- def changes_applied # :nodoc:
- @mutations_before_last_save = mutations_from_database
- @changed_attributes = ActiveSupport::HashWithIndifferentAccess.new
- forget_attribute_assignments
- @mutations_from_database = nil
- end
-
- def clear_changes_information # :nodoc:
- @mutations_before_last_save = nil
- @changed_attributes = ActiveSupport::HashWithIndifferentAccess.new
- forget_attribute_assignments
- @mutations_from_database = nil
- end
-
- def clear_attribute_changes(attr_names) # :nodoc:
- super
- attr_names.each do |attr_name|
- clear_attribute_change(attr_name)
- end
- end
-
- def changed_attributes # :nodoc:
- # This should only be set by methods which will call changed_attributes
- # multiple times when it is known that the computed value cannot change.
- if defined?(@cached_changed_attributes)
- @cached_changed_attributes
- else
- super.reverse_merge(mutations_from_database.changed_values).freeze
- end
- end
-
- def changes # :nodoc:
- cache_changed_attributes do
- super
- end
- end
-
- def previous_changes # :nodoc:
- mutations_before_last_save.changes
- end
-
- def attribute_changed_in_place?(attr_name) # :nodoc:
- mutations_from_database.changed_in_place?(attr_name)
- end
-
# Did this attribute change when we last saved? This method can be invoked
# as +saved_change_to_name?+ instead of <tt>saved_change_to_attribute?("name")</tt>.
# Behaves similarly to +attribute_changed?+. This method is useful in
@@ -182,26 +129,6 @@ module ActiveRecord
result
end
- def mutations_from_database
- unless defined?(@mutations_from_database)
- @mutations_from_database = nil
- end
- @mutations_from_database ||= AttributeMutationTracker.new(@attributes)
- end
-
- def changes_include?(attr_name)
- super || mutations_from_database.changed?(attr_name)
- end
-
- def clear_attribute_change(attr_name)
- mutations_from_database.forget_change(attr_name)
- end
-
- def attribute_will_change!(attr_name)
- super
- mutations_from_database.force_change(attr_name)
- end
-
def _update_record(*)
partial_writes? ? super(keys_for_partial_write) : super
end
@@ -213,25 +140,6 @@ module ActiveRecord
def keys_for_partial_write
changed_attribute_names_to_save & self.class.column_names
end
-
- def forget_attribute_assignments
- @attributes = @attributes.map(&:forgetting_assignment)
- end
-
- def mutations_before_last_save
- @mutations_before_last_save ||= NullMutationTracker.instance
- end
-
- def cache_changed_attributes
- @cached_changed_attributes = changed_attributes
- yield
- ensure
- clear_changed_attributes_cache
- end
-
- def clear_changed_attributes_cache
- remove_instance_variable(:@cached_changed_attributes) if defined?(@cached_changed_attributes)
- end
end
end
end
diff --git a/activerecord/lib/active_record/attribute_mutation_tracker.rb b/activerecord/lib/active_record/attribute_mutation_tracker.rb
deleted file mode 100644
index 94bf641a5d..0000000000
--- a/activerecord/lib/active_record/attribute_mutation_tracker.rb
+++ /dev/null
@@ -1,111 +0,0 @@
-# frozen_string_literal: true
-
-module ActiveRecord
- class AttributeMutationTracker # :nodoc:
- OPTION_NOT_GIVEN = Object.new
-
- def initialize(attributes)
- @attributes = attributes
- @forced_changes = Set.new
- end
-
- def changed_values
- attr_names.each_with_object({}.with_indifferent_access) do |attr_name, result|
- if changed?(attr_name)
- result[attr_name] = attributes[attr_name].original_value
- end
- end
- end
-
- def changes
- attr_names.each_with_object({}.with_indifferent_access) do |attr_name, result|
- change = change_to_attribute(attr_name)
- if change
- result[attr_name] = change
- end
- end
- end
-
- def change_to_attribute(attr_name)
- attr_name = attr_name.to_s
- if changed?(attr_name)
- [attributes[attr_name].original_value, attributes.fetch_value(attr_name)]
- end
- end
-
- def any_changes?
- attr_names.any? { |attr| changed?(attr) }
- end
-
- def changed?(attr_name, from: OPTION_NOT_GIVEN, to: OPTION_NOT_GIVEN)
- attr_name = attr_name.to_s
- forced_changes.include?(attr_name) ||
- attributes[attr_name].changed? &&
- (OPTION_NOT_GIVEN == from || attributes[attr_name].original_value == from) &&
- (OPTION_NOT_GIVEN == to || attributes[attr_name].value == to)
- end
-
- def changed_in_place?(attr_name)
- attributes[attr_name.to_s].changed_in_place?
- end
-
- def forget_change(attr_name)
- attr_name = attr_name.to_s
- attributes[attr_name] = attributes[attr_name].forgetting_assignment
- forced_changes.delete(attr_name)
- end
-
- def original_value(attr_name)
- attributes[attr_name.to_s].original_value
- end
-
- def force_change(attr_name)
- forced_changes << attr_name.to_s
- end
-
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
- protected
-
- attr_reader :attributes, :forced_changes
-
- private
-
- def attr_names
- attributes.keys
- end
- end
-
- class NullMutationTracker # :nodoc:
- include Singleton
-
- def changed_values(*)
- {}
- end
-
- def changes(*)
- {}
- end
-
- def change_to_attribute(attr_name)
- end
-
- def any_changes?(*)
- false
- end
-
- def changed?(*)
- false
- end
-
- def changed_in_place?(*)
- false
- end
-
- def forget_change(*)
- end
-
- def original_value(*)
- end
- end
-end
diff --git a/activerecord/lib/active_record/attribute_set.rb b/activerecord/lib/active_record/attribute_set.rb
deleted file mode 100644
index 9785666e77..0000000000
--- a/activerecord/lib/active_record/attribute_set.rb
+++ /dev/null
@@ -1,113 +0,0 @@
-# frozen_string_literal: true
-
-require "active_record/attribute_set/builder"
-require "active_record/attribute_set/yaml_encoder"
-
-module ActiveRecord
- class AttributeSet # :nodoc:
- delegate :each_value, :fetch, to: :attributes
-
- def initialize(attributes)
- @attributes = attributes
- end
-
- def [](name)
- attributes[name] || Attribute.null(name)
- end
-
- def []=(name, value)
- attributes[name] = value
- end
-
- def values_before_type_cast
- attributes.transform_values(&:value_before_type_cast)
- end
-
- def to_hash
- initialized_attributes.transform_values(&:value)
- end
- alias_method :to_h, :to_hash
-
- def key?(name)
- attributes.key?(name) && self[name].initialized?
- end
-
- def keys
- attributes.each_key.select { |name| self[name].initialized? }
- end
-
- if defined?(JRUBY_VERSION)
- # This form is significantly faster on JRuby, and this is one of our biggest hotspots.
- # https://github.com/jruby/jruby/pull/2562
- def fetch_value(name, &block)
- self[name].value(&block)
- end
- else
- def fetch_value(name)
- self[name].value { |n| yield n if block_given? }
- end
- end
-
- def write_from_database(name, value)
- attributes[name] = self[name].with_value_from_database(value)
- end
-
- def write_from_user(name, value)
- attributes[name] = self[name].with_value_from_user(value)
- end
-
- def write_cast_value(name, value)
- attributes[name] = self[name].with_cast_value(value)
- end
-
- def freeze
- @attributes.freeze
- super
- end
-
- def deep_dup
- self.class.allocate.tap do |copy|
- copy.instance_variable_set(:@attributes, attributes.deep_dup)
- end
- end
-
- def initialize_dup(_)
- @attributes = attributes.dup
- super
- end
-
- def initialize_clone(_)
- @attributes = attributes.clone
- super
- end
-
- def reset(key)
- if key?(key)
- write_from_database(key, nil)
- end
- end
-
- def accessed
- attributes.select { |_, attr| attr.has_been_read? }.keys
- end
-
- def map(&block)
- new_attributes = attributes.transform_values(&block)
- AttributeSet.new(new_attributes)
- end
-
- def ==(other)
- attributes == other.attributes
- end
-
- protected
-
- attr_reader :attributes
-
- private
-
- def initialized_attributes
- attributes.select { |_, attr| attr.initialized? }
- end
- end
-end
diff --git a/activerecord/lib/active_record/attribute_set/builder.rb b/activerecord/lib/active_record/attribute_set/builder.rb
deleted file mode 100644
index 349cc7e403..0000000000
--- a/activerecord/lib/active_record/attribute_set/builder.rb
+++ /dev/null
@@ -1,126 +0,0 @@
-# frozen_string_literal: true
-
-require "active_record/attribute"
-
-module ActiveRecord
- class AttributeSet # :nodoc:
- class Builder # :nodoc:
- attr_reader :types, :always_initialized, :default
-
- def initialize(types, always_initialized = nil, &default)
- @types = types
- @always_initialized = always_initialized
- @default = default
- end
-
- def build_from_database(values = {}, additional_types = {})
- if always_initialized && !values.key?(always_initialized)
- values[always_initialized] = nil
- end
-
- attributes = LazyAttributeHash.new(types, values, additional_types, &default)
- AttributeSet.new(attributes)
- end
- end
- end
-
- class LazyAttributeHash # :nodoc:
- delegate :transform_values, :each_key, :each_value, :fetch, to: :materialize
-
- def initialize(types, values, additional_types, &default)
- @types = types
- @values = values
- @additional_types = additional_types
- @materialized = false
- @delegate_hash = {}
- @default = default || proc {}
- end
-
- def key?(key)
- delegate_hash.key?(key) || values.key?(key) || types.key?(key)
- end
-
- def [](key)
- delegate_hash[key] || assign_default_value(key)
- end
-
- def []=(key, value)
- if frozen?
- raise RuntimeError, "Can't modify frozen hash"
- end
- delegate_hash[key] = value
- end
-
- def deep_dup
- dup.tap do |copy|
- copy.instance_variable_set(:@delegate_hash, delegate_hash.transform_values(&:dup))
- end
- end
-
- def initialize_dup(_)
- @delegate_hash = Hash[delegate_hash]
- super
- end
-
- def select
- keys = types.keys | values.keys | delegate_hash.keys
- keys.each_with_object({}) do |key, hash|
- attribute = self[key]
- if yield(key, attribute)
- hash[key] = attribute
- end
- end
- end
-
- def ==(other)
- if other.is_a?(LazyAttributeHash)
- materialize == other.materialize
- else
- materialize == other
- end
- end
-
- def marshal_dump
- materialize
- end
-
- def marshal_load(delegate_hash)
- @delegate_hash = delegate_hash
- @types = {}
- @values = {}
- @additional_types = {}
- @materialized = true
- end
-
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
- protected
-
- attr_reader :types, :values, :additional_types, :delegate_hash, :default
-
- def materialize
- unless @materialized
- values.each_key { |key| self[key] }
- types.each_key { |key| self[key] }
- unless frozen?
- @materialized = true
- end
- end
- delegate_hash
- end
-
- private
-
- def assign_default_value(name)
- type = additional_types.fetch(name, types[name])
- value_present = true
- value = values.fetch(name) { value_present = false }
-
- if value_present
- delegate_hash[name] = Attribute.from_database(name, value, type)
- elsif types.key?(name)
- delegate_hash[name] = default.call(name) || Attribute.uninitialized(name, type)
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/attribute_set/yaml_encoder.rb b/activerecord/lib/active_record/attribute_set/yaml_encoder.rb
deleted file mode 100644
index 9254ce16ab..0000000000
--- a/activerecord/lib/active_record/attribute_set/yaml_encoder.rb
+++ /dev/null
@@ -1,43 +0,0 @@
-# frozen_string_literal: true
-
-module ActiveRecord
- class AttributeSet
- # Attempts to do more intelligent YAML dumping of an
- # ActiveRecord::AttributeSet to reduce the size of the resulting string
- class YAMLEncoder # :nodoc:
- def initialize(default_types)
- @default_types = default_types
- end
-
- def encode(attribute_set, coder)
- coder["concise_attributes"] = attribute_set.each_value.map do |attr|
- if attr.type.equal?(default_types[attr.name])
- attr.with_type(nil)
- else
- attr
- end
- end
- end
-
- def decode(coder)
- if coder["attributes"]
- coder["attributes"]
- else
- attributes_hash = Hash[coder["concise_attributes"].map do |attr|
- if attr.type.nil?
- attr = attr.with_type(default_types[attr.name])
- end
- [attr.name, attr]
- end]
- AttributeSet.new(attributes_hash)
- end
- end
-
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
- protected
-
- attr_reader :default_types
- end
- end
-end
diff --git a/activerecord/lib/active_record/attributes.rb b/activerecord/lib/active_record/attributes.rb
index 9224d58928..0b7c9398a8 100644
--- a/activerecord/lib/active_record/attributes.rb
+++ b/activerecord/lib/active_record/attributes.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require "active_record/attribute/user_provided_default"
+require "active_model/attribute/user_provided_default"
module ActiveRecord
# See ActiveRecord::Attributes::ClassMethods for documentation
@@ -250,14 +250,14 @@ module ActiveRecord
if value == NO_DEFAULT_PROVIDED
default_attribute = _default_attributes[name].with_type(type)
elsif from_user
- default_attribute = Attribute::UserProvidedDefault.new(
+ default_attribute = ActiveModel::Attribute::UserProvidedDefault.new(
name,
value,
type,
_default_attributes.fetch(name.to_s) { nil },
)
else
- default_attribute = Attribute.from_database(name, value, type)
+ default_attribute = ActiveModel::Attribute.from_database(name, value, type)
end
_default_attributes[name] = default_attribute
end
diff --git a/activerecord/lib/active_record/collection_cache_key.rb b/activerecord/lib/active_record/collection_cache_key.rb
index f3e6516414..88b398ad45 100644
--- a/activerecord/lib/active_record/collection_cache_key.rb
+++ b/activerecord/lib/active_record/collection_cache_key.rb
@@ -12,6 +12,9 @@ module ActiveRecord
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.to_s)
column = connection.column_name_from_arel_node(collection.arel_attribute(timestamp_column))
select_values = "COUNT(*) AS #{connection.quote_column_name("size")}, MAX(%s) AS timestamp"
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb
index 147e16e9fa..d9ac8db6a8 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb
@@ -240,7 +240,7 @@ module ActiveRecord
rollback_transaction if transaction
else
begin
- commit_transaction
+ commit_transaction if transaction
rescue Exception
rollback_transaction(transaction) unless transaction.state.completed?
raise
diff --git a/activerecord/lib/active_record/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb
index 5005d58f1c..ee4f818cbf 100644
--- a/activerecord/lib/active_record/counter_cache.rb
+++ b/activerecord/lib/active_record/counter_cache.rb
@@ -53,7 +53,7 @@ module ActiveRecord
unscoped.where(primary_key => object.id).update_all(updates)
end
- return true
+ true
end
# A generic "counter updater" implementation, intended primarily to be
diff --git a/activerecord/lib/active_record/legacy_yaml_adapter.rb b/activerecord/lib/active_record/legacy_yaml_adapter.rb
index 23644aab8f..ffa095dd94 100644
--- a/activerecord/lib/active_record/legacy_yaml_adapter.rb
+++ b/activerecord/lib/active_record/legacy_yaml_adapter.rb
@@ -8,7 +8,7 @@ module ActiveRecord
case coder["active_record_yaml_version"]
when 1, 2 then coder
else
- if coder["attributes"].is_a?(AttributeSet)
+ if coder["attributes"].is_a?(ActiveModel::AttributeSet)
Rails420.convert(klass, coder)
else
Rails41.convert(klass, coder)
diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb
index 1485687053..d12a979a7f 100644
--- a/activerecord/lib/active_record/migration.rb
+++ b/activerecord/lib/active_record/migration.rb
@@ -1224,7 +1224,7 @@ module ActiveRecord
# Return true if a valid version is not provided.
def invalid_target?
- !target && @target_version && @target_version > 0
+ @target_version && @target_version != 0 && !target
end
def execute_migration_in_transaction(migration, direction)
diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb
index bed9400f51..12ee4a4137 100644
--- a/activerecord/lib/active_record/model_schema.rb
+++ b/activerecord/lib/active_record/model_schema.rb
@@ -323,7 +323,7 @@ module ActiveRecord
end
def attributes_builder # :nodoc:
- @attributes_builder ||= AttributeSet::Builder.new(attribute_types, primary_key) do |name|
+ @attributes_builder ||= ActiveModel::AttributeSet::Builder.new(attribute_types, primary_key) do |name|
unless columns_hash.key?(name)
_default_attributes[name].dup
end
@@ -346,7 +346,7 @@ module ActiveRecord
end
def yaml_encoder # :nodoc:
- @yaml_encoder ||= AttributeSet::YAMLEncoder.new(attribute_types)
+ @yaml_encoder ||= ActiveModel::AttributeSet::YAMLEncoder.new(attribute_types)
end
# Returns the type of the attribute with the given name, after applying
@@ -376,7 +376,7 @@ module ActiveRecord
end
def _default_attributes # :nodoc:
- @default_attributes ||= AttributeSet.new({})
+ @default_attributes ||= ActiveModel::AttributeSet.new({})
end
# Returns an array of column names as strings.
diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake
index 723272b4b2..3bca2982e0 100644
--- a/activerecord/lib/active_record/railties/databases.rake
+++ b/activerecord/lib/active_record/railties/databases.rake
@@ -97,16 +97,27 @@ db_namespace = namespace :db do
task up: [:environment, :load_config] do
raise "VERSION is required" if !ENV["VERSION"] || ENV["VERSION"].empty?
- version = ENV["VERSION"] ? ENV["VERSION"].to_i : nil
- ActiveRecord::Migrator.run(:up, ActiveRecord::Tasks::DatabaseTasks.migrations_paths, version)
+ ActiveRecord::Tasks::DatabaseTasks.check_target_version
+
+ ActiveRecord::Migrator.run(
+ :up,
+ ActiveRecord::Tasks::DatabaseTasks.migrations_paths,
+ ActiveRecord::Tasks::DatabaseTasks.target_version
+ )
db_namespace["_dump"].invoke
end
# desc 'Runs the "down" for a given migration VERSION.'
task down: [:environment, :load_config] do
raise "VERSION is required - To go down one migration, use db:rollback" if !ENV["VERSION"] || ENV["VERSION"].empty?
- version = ENV["VERSION"] ? ENV["VERSION"].to_i : nil
- ActiveRecord::Migrator.run(:down, ActiveRecord::Tasks::DatabaseTasks.migrations_paths, version)
+
+ ActiveRecord::Tasks::DatabaseTasks.check_target_version
+
+ ActiveRecord::Migrator.run(
+ :down,
+ ActiveRecord::Tasks::DatabaseTasks.migrations_paths,
+ ActiveRecord::Tasks::DatabaseTasks.target_version
+ )
db_namespace["_dump"].invoke
end
diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb
index 997cfe4b5e..7615fb6ee9 100644
--- a/activerecord/lib/active_record/relation.rb
+++ b/activerecord/lib/active_record/relation.rb
@@ -359,6 +359,11 @@ module ActiveRecord
def update_all(updates)
raise ArgumentError, "Empty list of attributes to change" if updates.blank?
+ if eager_loading?
+ relation = apply_join_dependency
+ return relation.update_all(updates)
+ end
+
stmt = Arel::UpdateManager.new
stmt.set Arel.sql(@klass.send(:sanitize_sql_for_assignment, updates))
@@ -423,6 +428,11 @@ module ActiveRecord
raise ActiveRecordError.new("delete_all doesn't support #{invalid_methods.join(', ')}")
end
+ if eager_loading?
+ relation = apply_join_dependency
+ return relation.delete_all
+ end
+
stmt = Arel::DeleteManager.new
stmt.from(table)
diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb
index 116bddce85..11256ab3d9 100644
--- a/activerecord/lib/active_record/relation/calculations.rb
+++ b/activerecord/lib/active_record/relation/calculations.rb
@@ -130,7 +130,7 @@ module ActiveRecord
# end
def calculate(operation, column_name)
if has_include?(column_name)
- relation = apply_join_dependency(construct_join_dependency)
+ relation = apply_join_dependency
relation.distinct! if operation.to_s.downcase == "count"
relation.calculate(operation, column_name)
@@ -180,7 +180,7 @@ module ActiveRecord
end
if has_include?(column_names.first)
- relation = apply_join_dependency(construct_join_dependency)
+ relation = apply_join_dependency
relation.pluck(*column_names)
else
relation = spawn
diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb
index 707245bab2..18566b5662 100644
--- a/activerecord/lib/active_record/relation/finder_methods.rb
+++ b/activerecord/lib/active_record/relation/finder_methods.rb
@@ -394,7 +394,7 @@ module ActiveRecord
)
end
- def apply_join_dependency(join_dependency)
+ def apply_join_dependency(join_dependency = construct_join_dependency)
relation = except(:includes, :eager_load, :preload).joins!(join_dependency)
if using_limitable_reflections?(join_dependency.reflections)
diff --git a/activerecord/lib/active_record/relation/query_attribute.rb b/activerecord/lib/active_record/relation/query_attribute.rb
index fad08e2613..3532f28858 100644
--- a/activerecord/lib/active_record/relation/query_attribute.rb
+++ b/activerecord/lib/active_record/relation/query_attribute.rb
@@ -1,10 +1,10 @@
# frozen_string_literal: true
-require "active_record/attribute"
+require "active_model/attribute"
module ActiveRecord
class Relation
- class QueryAttribute < Attribute # :nodoc:
+ class QueryAttribute < ActiveModel::Attribute # :nodoc:
def type_cast(value)
value
end
diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb
index 897ff5c8af..9c5be4ad9b 100644
--- a/activerecord/lib/active_record/relation/query_methods.rb
+++ b/activerecord/lib/active_record/relation/query_methods.rb
@@ -930,7 +930,7 @@ module ActiveRecord
arel.where(where_clause.ast) unless where_clause.empty?
arel.having(having_clause.ast) unless having_clause.empty?
if limit_value
- limit_attribute = Attribute.with_cast_value(
+ limit_attribute = ActiveModel::Attribute.with_cast_value(
"LIMIT".freeze,
connection.sanitize_limit(limit_value),
Type.default_value,
@@ -938,7 +938,7 @@ module ActiveRecord
arel.take(Arel::Nodes::BindParam.new(limit_attribute))
end
if offset_value
- offset_attribute = Attribute.with_cast_value(
+ offset_attribute = ActiveModel::Attribute.with_cast_value(
"OFFSET".freeze,
offset_value.to_i,
Type.default_value,
diff --git a/activerecord/lib/active_record/scoping/named.rb b/activerecord/lib/active_record/scoping/named.rb
index 6fa096c1fe..310af72c41 100644
--- a/activerecord/lib/active_record/scoping/named.rb
+++ b/activerecord/lib/active_record/scoping/named.rb
@@ -24,8 +24,14 @@ module ActiveRecord
# You can define a scope that applies to all finders using
# {default_scope}[rdoc-ref:Scoping::Default::ClassMethods#default_scope].
def all
+ current_scope = self.current_scope
+
if current_scope
- current_scope.clone
+ if self == current_scope.klass
+ current_scope.clone
+ else
+ relation.merge!(current_scope)
+ end
else
default_scoped
end
diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb
index ff388ff1f6..4657e51e6d 100644
--- a/activerecord/lib/active_record/tasks/database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/database_tasks.rb
@@ -164,13 +164,12 @@ module ActiveRecord
end
def migrate
- raise "Empty VERSION provided" if ENV["VERSION"] && ENV["VERSION"].empty?
+ check_target_version
verbose = ENV["VERBOSE"] ? ENV["VERBOSE"] != "false" : true
- version = ENV["VERSION"] ? ENV["VERSION"].to_i : nil
scope = ENV["SCOPE"]
verbose_was, Migration.verbose = Migration.verbose, verbose
- Migrator.migrate(migrations_paths, version) do |migration|
+ Migrator.migrate(migrations_paths, target_version) do |migration|
scope.blank? || scope == migration.scope
end
ActiveRecord::Base.clear_cache!
@@ -178,6 +177,16 @@ module ActiveRecord
Migration.verbose = verbose_was
end
+ def check_target_version
+ if target_version && !(Migration::MigrationFilenameRegexp.match?(ENV["VERSION"]) || /\A\d+\z/.match?(ENV["VERSION"]))
+ raise "Invalid format of target version: `VERSION=#{ENV['VERSION']}`"
+ end
+ end
+
+ def target_version
+ ENV["VERSION"].to_i if ENV["VERSION"] && !ENV["VERSION"].empty?
+ end
+
def charset_current(environment = env)
charset ActiveRecord::Base.configurations[environment]
end