diff options
Diffstat (limited to 'activerecord/lib')
43 files changed, 399 insertions, 270 deletions
diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb index 27a641f05b..3250e29b82 100644 --- a/activerecord/lib/active_record/aggregations.rb +++ b/activerecord/lib/active_record/aggregations.rb @@ -3,8 +3,6 @@ module ActiveRecord # See ActiveRecord::Aggregations::ClassMethods for documentation module Aggregations - extend ActiveSupport::Concern - def initialize_dup(*) # :nodoc: @aggregation_cache = {} super @@ -225,6 +223,10 @@ module ActiveRecord def composed_of(part_id, options = {}) options.assert_valid_keys(:class_name, :mapping, :allow_nil, :constructor, :converter) + unless self < Aggregations + include Aggregations + end + name = part_id.id2name class_name = options[:class_name] || name.camelize mapping = options[:mapping] || [ name, name ] diff --git a/activerecord/lib/active_record/associations/alias_tracker.rb b/activerecord/lib/active_record/associations/alias_tracker.rb index 4f3893588e..272eede824 100644 --- a/activerecord/lib/active_record/associations/alias_tracker.rb +++ b/activerecord/lib/active_record/associations/alias_tracker.rb @@ -33,7 +33,7 @@ module ActiveRecord elsif join.is_a?(Arel::Nodes::Join) join.left.name == name ? 1 : 0 elsif join.is_a?(Hash) - join.fetch(name, 0) + join[name] else raise ArgumentError, "joins list should be initialized by list of Arel::Nodes::Join" end diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index ca8c7794e0..44596f4424 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -19,7 +19,6 @@ module ActiveRecord # HasManyThroughAssociation + ThroughAssociation class Association #:nodoc: attr_reader :owner, :target, :reflection - attr_accessor :inversed delegate :options, to: :reflection @@ -67,7 +66,7 @@ module ActiveRecord # # Note that if the target has not been loaded, it is not considered stale. def stale_target? - !inversed && loaded? && @stale_state != stale_state + !@inversed && loaded? && @stale_state != stale_state end # Sets the target of this association to <tt>\target</tt>, and the \loaded flag to +true+. @@ -98,23 +97,24 @@ module ActiveRecord # Set the inverse association, if possible def set_inverse_instance(record) - if invertible_for?(record) - inverse = record.association(inverse_reflection_for(record).name) - inverse.target = owner - inverse.inversed = true + if inverse = inverse_association_for(record) + inverse.inversed_from(owner) end record end # Remove the inverse association, if possible def remove_inverse_instance(record) - if invertible_for?(record) - inverse = record.association(inverse_reflection_for(record).name) - inverse.target = nil - inverse.inversed = false + if inverse = inverse_association_for(record) + inverse.inversed_from(nil) end end + def inversed_from(record) + self.target = record + @inversed = !!record + end + # Returns the class of the target. belongs_to polymorphic overrides this to look at the # polymorphic_type field on the owner. def klass @@ -240,6 +240,12 @@ module ActiveRecord end end + def inverse_association_for(record) + if invertible_for?(record) + record.association(inverse_reflection_for(record).name) + end + end + # Can be redefined by subclasses, notably polymorphic belongs_to # The record parameter is necessary to support polymorphic inverses as we must check for # the association in the specific class of the record. @@ -269,6 +275,7 @@ module ActiveRecord def build_record(attributes) reflection.build_association(attributes) do |record| initialize_attributes(record, attributes) + yield(record) if block_given? end end diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb index c8716741b0..08f450278d 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -16,7 +16,7 @@ module ActiveRecord end end - def target=(record) + def inversed_from(record) replace_keys(record) super end @@ -42,6 +42,10 @@ module ActiveRecord update_counters(1) end + def target_changed? + owner.saved_change_to_attribute?(reflection.foreign_key) + end + private def replace(record) if record @@ -53,6 +57,8 @@ module ActiveRecord decrement_counters end + replace_keys(record) + self.target = record end @@ -77,19 +83,22 @@ module ActiveRecord def update_counters_on_replace(record) if require_counter_update? && different_target?(record) owner.instance_variable_set :@_after_replace_counter_called, true - record.increment!(reflection.counter_cache_column) + record.increment!(reflection.counter_cache_column, touch: reflection.options[:touch]) decrement_counters end end # Checks whether record is different to the current target, without loading it def different_target?(record) - record.id != owner._read_attribute(reflection.foreign_key) + record._read_attribute(primary_key(record)) != owner._read_attribute(reflection.foreign_key) end def replace_keys(record) - owner[reflection.foreign_key] = record ? - record._read_attribute(reflection.association_primary_key(record.class)) : nil + owner[reflection.foreign_key] = record ? record._read_attribute(primary_key(record)) : nil + end + + def primary_key(record) + reflection.association_primary_key(record.class) end def foreign_key_present? diff --git a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb index 75b4c4481a..3fd2fb5f67 100644 --- a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb @@ -9,8 +9,11 @@ module ActiveRecord type.presence && type.constantize end - private + def target_changed? + super || owner.saved_change_to_attribute?(reflection.foreign_type) + end + private def replace_keys(record) super owner[reflection.foreign_type] = record ? record.class.polymorphic_name : nil diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb index c161454c1a..4b6cb76081 100644 --- a/activerecord/lib/active_record/associations/builder/belongs_to.rb +++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb @@ -36,7 +36,7 @@ module ActiveRecord::Associations::Builder # :nodoc: if (@_after_replace_counter_called ||= false) @_after_replace_counter_called = false - elsif saved_change_to_attribute?(foreign_key) && !new_record? + elsif association(reflection.name).target_changed? if reflection.polymorphic? model = attribute_in_database(reflection.foreign_type).try(:constantize) model_was = attribute_before_last_save(reflection.foreign_type).try(:constantize) @@ -49,14 +49,22 @@ module ActiveRecord::Associations::Builder # :nodoc: foreign_key = attribute_in_database foreign_key if foreign_key && model.respond_to?(:increment_counter) + foreign_key = counter_cache_target(reflection, model, foreign_key) model.increment_counter(cache_column, foreign_key) end if foreign_key_was && model_was.respond_to?(:decrement_counter) + foreign_key_was = counter_cache_target(reflection, model_was, foreign_key_was) model_was.decrement_counter(cache_column, foreign_key_was) end end end + + private + def counter_cache_target(reflection, model, foreign_key) + primary_key = reflection.association_primary_key(model) + model.unscoped.where!(primary_key => foreign_key) + end end end @@ -84,7 +92,8 @@ module ActiveRecord::Associations::Builder # :nodoc: else klass = association.klass end - old_record = klass.find_by(klass.primary_key => old_foreign_id) + primary_key = reflection.association_primary_key(klass) + old_record = klass.find_by(primary_key => old_foreign_id) if old_record if touch != true diff --git a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb index 1981da11a2..e3070e0472 100644 --- a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb +++ b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb @@ -2,39 +2,6 @@ module ActiveRecord::Associations::Builder # :nodoc: class HasAndBelongsToMany # :nodoc: - class JoinTableResolver # :nodoc: - KnownTable = Struct.new :join_table - - class KnownClass # :nodoc: - def initialize(lhs_class, rhs_class_name) - @lhs_class = lhs_class - @rhs_class_name = rhs_class_name - @join_table = nil - end - - def join_table - @join_table ||= [@lhs_class.table_name, klass.table_name].sort.join("\0").gsub(/^(.*[._])(.+)\0\1(.+)/, '\1\2_\3').tr("\0", "_") - end - - private - - def klass - @lhs_class.send(:compute_type, @rhs_class_name) - end - end - - def self.build(lhs_class, name, options) - if options[:join_table] - KnownTable.new options[:join_table].to_s - else - class_name = options.fetch(:class_name) { - name.to_s.camelize.singularize - } - KnownClass.new lhs_class, class_name.to_s - end - end - end - attr_reader :lhs_model, :association_name, :options def initialize(association_name, lhs_model, options) @@ -44,8 +11,6 @@ module ActiveRecord::Associations::Builder # :nodoc: end def through_model - habtm = JoinTableResolver.build lhs_model, association_name, options - join_model = Class.new(ActiveRecord::Base) { class << self attr_accessor :left_model @@ -56,7 +21,9 @@ module ActiveRecord::Associations::Builder # :nodoc: end def self.table_name - table_name_resolver.join_table + # Table name needs to be resolved lazily + # because RHS class might not have been loaded + @table_name ||= table_name_resolver.call end def self.compute_type(class_name) @@ -86,7 +53,7 @@ module ActiveRecord::Associations::Builder # :nodoc: } join_model.name = "HABTM_#{association_name.to_s.camelize}" - join_model.table_name_resolver = habtm + join_model.table_name_resolver = -> { table_name } join_model.left_model = lhs_model join_model.add_left_association :left_side, anonymous_class: lhs_model @@ -117,6 +84,18 @@ module ActiveRecord::Associations::Builder # :nodoc: middle_options end + def table_name + if options[:join_table] + options[:join_table].to_s + else + class_name = options.fetch(:class_name) { + association_name.to_s.camelize.singularize + } + klass = lhs_model.send(:compute_type, class_name.to_s) + [lhs_model.table_name, klass.table_name].sort.join("\0").gsub(/^(.*[._])(.+)\0\1(.+)/, '\1\2_\3').tr("\0", "_") + end + end + def belongs_to_options(options) rhs_options = {} diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index d61d105544..840d900bbc 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -105,9 +105,7 @@ module ActiveRecord if attributes.is_a?(Array) attributes.collect { |attr| build(attr, &block) } else - add_to_target(build_record(attributes)) do |record| - yield(record) if block_given? - end + add_to_target(build_record(attributes, &block)) end end @@ -360,15 +358,18 @@ module ActiveRecord if attributes.is_a?(Array) attributes.collect { |attr| _create_record(attr, raise, &block) } else + record = build_record(attributes, &block) transaction do - add_to_target(build_record(attributes)) do |record| - yield(record) if block_given? - insert_record(record, true, raise) { + result = nil + add_to_target(record) do + result = insert_record(record, true, raise) { @_was_loaded = loaded? @association_ids = nil } end + raise ActiveRecord::Rollback unless result end + record end end @@ -399,7 +400,7 @@ module ActiveRecord records.each { |record| callback(:before_remove, record) } delete_records(existing_records, method) if existing_records.any? - records.each { |record| target.delete(record) } + @target -= records records.each { |record| callback(:after_remove, record) } end @@ -446,7 +447,9 @@ module ActiveRecord end end - result && records + raise ActiveRecord::Rollback unless result + + records end def replace_on_target(record, index, skip_callbacks) 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 59929b8c4e..617956c768 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -90,7 +90,7 @@ module ActiveRecord def build_record(attributes) ensure_not_nested - record = super(attributes) + record = super inverse = source_reflection.inverse_of if inverse diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index d211884135..390bfd8b08 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -106,7 +106,7 @@ module ActiveRecord end end - def _create_record(attributes, raise_error = false) + def _create_record(attributes, raise_error = false, &block) unless owner.persisted? raise ActiveRecord::RecordNotSaved, "You cannot call create unless the parent is saved" end diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb index f88e383fe0..8cf1b5d25a 100644 --- a/activerecord/lib/active_record/associations/join_dependency.rb +++ b/activerecord/lib/active_record/associations/join_dependency.rb @@ -67,42 +67,31 @@ module ActiveRecord end end - def initialize(base, table, associations, alias_tracker) - @alias_tracker = alias_tracker + def initialize(base, table, associations) tree = self.class.make_tree associations @join_root = JoinBase.new(base, table, build(tree, base)) - @join_root.children.each { |child| construct_tables! @join_root, child } end def reflections join_root.drop(1).map!(&:reflection) end - def join_constraints(joins_to_add, join_type) - joins = join_root.children.flat_map { |child| - make_join_constraints(join_root, child, join_type) - } + def join_constraints(joins_to_add, join_type, alias_tracker) + @alias_tracker = alias_tracker + + construct_tables!(join_root) + joins = make_join_constraints(join_root, join_type) 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 else - oj.join_root.children.flat_map { |child| - make_join_constraints(oj.join_root, child, join_type) - } + make_join_constraints(oj.join_root, join_type) end } end - def aliases - @aliases ||= Aliases.new join_root.each_with_index.map { |join_part, i| - columns = join_part.column_names.each_with_index.map { |column_name, j| - Aliases::Column.new column_name, "t#{i}_r#{j}" - } - Aliases::Table.new(join_part, columns) - } - end - def instantiate(result_set, &block) primary_key = aliases.column_alias(join_root, join_root.primary_key) @@ -127,35 +116,49 @@ module ActiveRecord result_set.each { |row_hash| parent_key = primary_key ? row_hash[primary_key] : row_hash parent = parents[parent_key] ||= join_root.instantiate(row_hash, column_aliases, &block) - construct(parent, join_root, row_hash, result_set, seen, model_cache, aliases) + construct(parent, join_root, row_hash, seen, model_cache) } end parents.values end + def apply_column_aliases(relation) + relation._select!(-> { aliases.columns }) + end + protected - attr_reader :alias_tracker, :base_klass, :join_root + attr_reader :join_root private + attr_reader :alias_tracker - def make_constraints(parent, child, tables, join_type) - chain = child.reflection.chain - foreign_table = parent.table - foreign_klass = parent.base_klass - child.join_constraints(foreign_table, foreign_klass, join_type, tables, chain) + def aliases + @aliases ||= Aliases.new join_root.each_with_index.map { |join_part, i| + columns = join_part.column_names.each_with_index.map { |column_name, j| + Aliases::Column.new column_name, "t#{i}_r#{j}" + } + Aliases::Table.new(join_part, columns) + } end - def make_outer_joins(parent, child) - join_type = Arel::Nodes::OuterJoin - make_join_constraints(parent, child, join_type, true) + def construct_tables!(join_root) + join_root.each_children do |parent, child| + child.tables = table_aliases_for(parent, child) + end end - def make_join_constraints(parent, child, join_type, aliasing = false) - tables = aliasing ? table_aliases_for(parent, child) : child.tables - joins = make_constraints(parent, child, tables, join_type) + def make_join_constraints(join_root, join_type) + join_root.children.flat_map do |child| + make_constraints(join_root, child, join_type) + end + end - joins.concat child.children.flat_map { |c| make_join_constraints(child, c, join_type, aliasing) } + def make_constraints(parent, child, join_type = Arel::Nodes::OuterJoin) + foreign_table = parent.table + foreign_klass = parent.base_klass + joins = child.join_constraints(foreign_table, foreign_klass, join_type, alias_tracker) + joins.concat child.children.flat_map { |c| make_constraints(child, c, join_type) } end def table_aliases_for(parent, node) @@ -168,13 +171,8 @@ module ActiveRecord } end - def construct_tables!(parent, node) - node.tables = table_aliases_for(parent, node) - node.children.each { |child| construct_tables! node, child } - end - def table_alias_for(reflection, parent, join) - name = "#{reflection.plural_name}_#{parent.table_name}" + name = reflection.alias_candidate(parent.table_name) join ? "#{name}_join" : name end @@ -183,8 +181,8 @@ module ActiveRecord [left.children.find { |node2| node1.match? node2 }, node1] }.partition(&:first) - ojs = missing.flat_map { |_, n| make_outer_joins left, n } - intersection.flat_map { |l, r| walk l, r }.concat ojs + joins = intersection.flat_map { |l, r| r.table = l.table; walk(l, r) } + joins.concat missing.flat_map { |_, n| make_constraints(left, n) } end def find_reflection(klass, name) @@ -202,11 +200,11 @@ module ActiveRecord raise EagerLoadPolymorphicError.new(reflection) end - JoinAssociation.new(reflection, build(right, reflection.klass), alias_tracker) + JoinAssociation.new(reflection, build(right, reflection.klass)) end end - def construct(ar_parent, parent, row, rs, seen, model_cache, aliases) + def construct(ar_parent, parent, row, seen, model_cache) return if ar_parent.nil? parent.children.each do |node| @@ -215,7 +213,7 @@ module ActiveRecord other.loaded! elsif ar_parent.association_cached?(node.reflection.name) model = ar_parent.association(node.reflection.name).target - construct(model, node, row, rs, seen, model_cache, aliases) + construct(model, node, row, seen, model_cache) next end @@ -230,9 +228,9 @@ module ActiveRecord model = seen[ar_parent.object_id][node.base_klass][id] if model - construct(model, node, row, rs, seen, model_cache, aliases) + construct(model, node, row, seen, model_cache) else - model = construct_model(ar_parent, node, row, model_cache, id, aliases) + model = construct_model(ar_parent, node, row, model_cache, id) if node.reflection.scope && node.reflection.scope_for(node.base_klass.unscoped).readonly_value @@ -240,12 +238,12 @@ module ActiveRecord end seen[ar_parent.object_id][node.base_klass][id] = model - construct(model, node, row, rs, seen, model_cache, aliases) + construct(model, node, row, seen, model_cache) end end end - def construct_model(record, node, row, model_cache, id, aliases) + def construct_model(record, node, row, model_cache, id) other = record.association(node.reflection.name) model = model_cache[node][id] ||= 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 c36386ec7e..6e5e950e90 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb @@ -6,17 +6,14 @@ module ActiveRecord module Associations class JoinDependency # :nodoc: class JoinAssociation < JoinPart # :nodoc: - # The reflection of the association represented - attr_reader :reflection + attr_reader :reflection, :tables + attr_accessor :table - attr_accessor :tables - - def initialize(reflection, children, alias_tracker) + def initialize(reflection, children) super(reflection.klass, children) - @alias_tracker = alias_tracker - @reflection = reflection - @tables = nil + @reflection = reflection + @tables = nil end def match?(other) @@ -24,14 +21,13 @@ module ActiveRecord super && reflection == other.reflection end - def join_constraints(foreign_table, foreign_klass, join_type, tables, chain) - joins = [] - tables = tables.reverse + def join_constraints(foreign_table, foreign_klass, join_type, alias_tracker) + joins = [] # The chain starts with the target table, but we want to end with it here (makes # more sense in this context), so we reverse - chain.reverse_each do |reflection| - table = tables.shift + reflection.chain.reverse_each.with_index(1) do |reflection, i| + table = tables[-i] klass = reflection.klass constraint = reflection.build_join_constraint(table, foreign_table) @@ -54,12 +50,10 @@ module ActiveRecord joins end - def table - tables.first + def tables=(tables) + @tables = tables + @table = tables.first end - - private - attr_reader :alias_tracker end end end diff --git a/activerecord/lib/active_record/associations/join_dependency/join_part.rb b/activerecord/lib/active_record/associations/join_dependency/join_part.rb index 2181f308bf..3cabb21983 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_part.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_part.rb @@ -33,6 +33,13 @@ module ActiveRecord children.each { |child| child.each(&block) } end + def each_children(&block) + children.each do |child| + yield self, child + child.each_children(&block) + end + end + # An Arel::Table for the active_record def table raise NotImplementedError diff --git a/activerecord/lib/active_record/associations/singular_association.rb b/activerecord/lib/active_record/associations/singular_association.rb index ead89bfe6c..cfab16a745 100644 --- a/activerecord/lib/active_record/associations/singular_association.rb +++ b/activerecord/lib/active_record/associations/singular_association.rb @@ -17,9 +17,8 @@ module ActiveRecord replace(record) end - def build(attributes = {}) - record = build_record(attributes) - yield(record) if block_given? + def build(attributes = {}, &block) + record = build_record(attributes, &block) set_new_record(record) record end @@ -62,9 +61,8 @@ module ActiveRecord replace(record) end - def _create_record(attributes, raise_error = false) - record = build_record(attributes) - yield(record) if block_given? + def _create_record(attributes, raise_error = false, &block) + record = build_record(attributes, &block) saved = record.save set_new_record(record) raise RecordInvalid.new(record) if !saved && raise_error diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb index 5afb0bc068..15e6565e69 100644 --- a/activerecord/lib/active_record/associations/through_association.rb +++ b/activerecord/lib/active_record/associations/through_association.rb @@ -114,7 +114,7 @@ module ActiveRecord attributes[inverse.foreign_key] = target.id end - super(attributes) + super end end end diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index 83b5a5e698..e4b8b1a330 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -26,7 +26,7 @@ module ActiveRecord def self.set_name_cache(name, value) const_name = "ATTR_#{name}" unless const_defined? const_name - const_set const_name, value.dup.freeze + const_set const_name, -value end end } diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index 9575cc24c8..a405f05e0b 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -392,7 +392,7 @@ module ActiveRecord records -= records_to_destroy end - records.each do |record| + records.each_with_index do |record, index| next if record.destroyed? saved = true @@ -401,9 +401,11 @@ module ActiveRecord if autosave saved = association.insert_record(record, false) elsif !reflection.nested? - association_saved = association.insert_record(record) if reflection.validate? - saved = association_saved + valid = association_valid?(reflection, record, index) + saved = valid ? association.insert_record(record, false) : false + else + association.insert_record(record) end end elsif autosave diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 7ab9160265..5169f312f5 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -288,6 +288,7 @@ module ActiveRecord #:nodoc: extend Enum extend Delegation::DelegateCache extend CollectionCacheKey + extend Aggregations::ClassMethods include Core include DatabaseConfigurations @@ -314,7 +315,6 @@ module ActiveRecord #:nodoc: include ActiveModel::SecurePassword include AutosaveAssociation include NestedAttributes - include Aggregations include Transactions include TouchLater include NoTouching diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb index fd6819d08f..b6852bfc71 100644 --- a/activerecord/lib/active_record/callbacks.rb +++ b/activerecord/lib/active_record/callbacks.rb @@ -128,7 +128,7 @@ module ActiveRecord # end # end # - # So you specify the object you want messaged on a given callback. When that callback is triggered, the object has + # So you specify the object you want to be messaged on a given callback. When that callback is triggered, the object has # a method by the name of the callback messaged. You can make these callbacks more flexible by passing in other # initialization data such as the name of the attribute to work with: # @@ -318,6 +318,10 @@ module ActiveRecord _run_touch_callbacks { super } end + def increment!(*, touch: nil) # :nodoc: + touch ? _run_touch_callbacks { super } : super + end + private def create_or_update(*) 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 25622e34c8..8aeb934ec2 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb @@ -110,12 +110,7 @@ module ActiveRecord if @query_cache[sql].key?(binds) ActiveSupport::Notifications.instrument( "sql.active_record", - sql: sql, - binds: binds, - type_casted_binds: -> { type_casted_binds(binds) }, - name: name, - connection_id: object_id, - cached: true, + cache_notification_info(sql, name, binds) ) @query_cache[sql][binds] else @@ -125,6 +120,19 @@ module ActiveRecord end end + # Database adapters can override this method to + # provide custom cache information. + def cache_notification_info(sql, name, binds) + { + sql: sql, + binds: binds, + type_casted_binds: -> { type_casted_binds(binds) }, + name: name, + connection_id: object_id, + cached: true + } + end + # If arel is locked this is a SELECT ... FOR UPDATE or somesuch. Such # queries should not be cached. def locked?(arel) 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 5f090d16cd..582ac516c7 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -356,8 +356,12 @@ module ActiveRecord type = type.to_sym if type options = options.dup - if @columns_hash[name] && @columns_hash[name].primary_key? - raise ArgumentError, "you can't redefine the primary key column '#{name}'. To define a custom primary key, pass { id: false } to create_table." + if @columns_hash[name] + if @columns_hash[name].primary_key? + raise ArgumentError, "you can't redefine the primary key column '#{name}'. To define a custom primary key, pass { id: false } to create_table." + else + raise ArgumentError, "you can't define an already defined column '#{name}'." + end end index_options = options.delete(:index) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb index d6852082ac..26abeea7ed 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb @@ -66,6 +66,10 @@ module ActiveRecord deserialize(raw_old_value) != new_value end + def force_equality?(value) + value.is_a?(::Array) + end + private def type_cast_array(value, method) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb index 6edb7cfd3c..d85f9ab3ef 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb @@ -53,6 +53,10 @@ module ActiveRecord ::Range.new(new_begin, new_end, value.exclude_end?) end + def force_equality?(value) + value.is_a?(::Range) + end + private def type_cast_single(value) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb index 6047217fcd..206b855a18 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb @@ -13,10 +13,10 @@ module ActiveRecord # t.timestamps # end # - # By default, this will use the +gen_random_uuid()+ function from the + # By default, this will use the <tt>gen_random_uuid()</tt> function from the # +pgcrypto+ extension. As that extension is only available in # PostgreSQL 9.4+, for earlier versions an explicit default can be set - # to use +uuid_generate_v4()+ from the +uuid-ossp+ extension instead: + # to use <tt>uuid_generate_v4()</tt> from the +uuid-ossp+ extension instead: # # create_table :stuffs, id: false do |t| # t.primary_key :id, :uuid, default: "uuid_generate_v4()" diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 544374586c..844af952c1 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -15,6 +15,8 @@ require "sqlite3" module ActiveRecord module ConnectionHandling # :nodoc: def sqlite3_connection(config) + config = config.symbolize_keys + # Require database. unless config[:database] raise ArgumentError, "No database file specified. Missing argument: database" @@ -31,7 +33,7 @@ module ActiveRecord db = SQLite3::Database.new( config[:database].to_s, - results_as_hash: true + config.merge(results_as_hash: true) ) db.busy_timeout(ConnectionAdapters::SQLite3Adapter.type_cast_config_to_integer(config[:timeout])) if config[:timeout] diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index e1a0b2ecf8..df795df52e 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -139,11 +139,6 @@ module ActiveRecord end module ClassMethods # :nodoc: - def allocate - define_attribute_methods - super - end - def initialize_find_by_cache # :nodoc: @find_by_statement_cache = { true => Concurrent::Map.new, false => Concurrent::Map.new } end @@ -350,6 +345,28 @@ module ActiveRecord end ## + # Initializer used for instantiating objects that have been read from the + # database. +attributes+ should be an attributes object, and unlike the + # `initialize` method, no assignment calls are made per attribute. + # + # :nodoc: + def init_from_db(attributes) + init_internals + + @new_record = false + @attributes = attributes + + self.class.define_attribute_methods + + yield self if block_given? + + _run_find_callbacks + _run_initialize_callbacks + + self + end + + ## # :method: clone # Identical to Ruby's clone method. This is a "shallow" copy. Be warned that your attributes are not copied. # That means that modifying attributes of the clone will modify the original, since they will both point to the diff --git a/activerecord/lib/active_record/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb index ee4f818cbf..0d8748d7e6 100644 --- a/activerecord/lib/active_record/counter_cache.rb +++ b/activerecord/lib/active_record/counter_cache.rb @@ -47,8 +47,12 @@ module ActiveRecord reflection = child_class._reflections.values.find { |e| e.belongs_to? && e.foreign_key.to_s == foreign_key && e.options[:counter_cache].present? } counter_name = reflection.counter_cache_column - updates = { counter_name.to_sym => object.send(counter_association).count(:all) } - updates.merge!(touch_updates(touch)) if touch + updates = { counter_name => object.send(counter_association).count(:all) } + + if touch + names = touch if touch != true + updates.merge!(touch_attributes_with_time(*names)) + end unscoped.where(primary_key => object.id).update_all(updates) end @@ -68,8 +72,8 @@ module ActiveRecord # * +counters+ - A Hash containing the names of the fields # to update as keys and the amount to update the field by as values. # * <tt>:touch</tt> option - Touch timestamp columns when updating. - # Pass +true+ to touch +updated_at+ and/or +updated_on+. Pass a symbol to - # touch that column or an array of symbols to touch just those ones. + # If attribute names are passed, they are updated along with updated_at/on + # attributes. # # ==== Examples # @@ -107,11 +111,18 @@ module ActiveRecord end if touch - touch_updates = touch_updates(touch) + names = touch if touch != true + touch_updates = touch_attributes_with_time(*names) updates << sanitize_sql_for_assignment(touch_updates) unless touch_updates.empty? end - unscoped.where(primary_key => id).update_all updates.join(", ") + if id.is_a?(Relation) && self == id.klass + relation = id + else + relation = unscoped.where!(primary_key => id) + end + + relation.update_all updates.join(", ") end # Increment a numeric field by one, via a direct SQL update. @@ -165,13 +176,6 @@ module ActiveRecord def decrement_counter(counter_name, id, touch: nil) update_counters(id, counter_name => -1, touch: touch) end - - private - def touch_updates(touch) - touch = timestamp_attributes_for_update_in_model if touch == true - touch_time = current_time_from_proper_timezone - Array(touch).map { |column| [ column, touch_time ] }.to_h - end end private diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index 025201c20b..6ace973c29 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -831,10 +831,14 @@ module ActiveRecord write "== %s %s" % [text, "=" * length] end + # Takes a message argument and outputs it as is. + # A second boolean argument can be passed to specify whether to indent or not. def say(message, subitem = false) write "#{subitem ? " ->" : "--"} #{message}" end + # Outputs text along with how long it took to run its block. + # If the block returns an integer it assumes it is the number of rows affected. def say_with_time(message) say(message) result = nil @@ -844,6 +848,7 @@ module ActiveRecord result end + # Takes a block as an argument and suppresses any output generated by the block. def suppress_messages save, self.verbose = verbose, false yield diff --git a/activerecord/lib/active_record/no_touching.rb b/activerecord/lib/active_record/no_touching.rb index 754c891884..697076bdae 100644 --- a/activerecord/lib/active_record/no_touching.rb +++ b/activerecord/lib/active_record/no_touching.rb @@ -43,6 +43,13 @@ module ActiveRecord end end + # Returns +true+ if the class has +no_touching+ set, +false+ otherwise. + # + # Project.no_touching do + # Project.first.no_touching? # true + # Message.first.no_touching? # false + # end + # def no_touching? NoTouching.applied_to?(self.class) end diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index a0d5f1ee9f..155d67fd8f 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -67,8 +67,18 @@ module ActiveRecord # how this "single-table" inheritance mapping is implemented. def instantiate(attributes, column_types = {}, &block) klass = discriminate_class_for_record(attributes) + instantiate_instance_of(klass, attributes, column_types, &block) + end + + # Given a class, an attributes hash, +instantiate_instance_of+ returns a + # new instance of the class. Accepts only keys as strings. + # + # This is private, don't call it. :) + # + # :nodoc: + def instantiate_instance_of(klass, attributes, column_types = {}, &block) attributes = klass.attributes_builder.build_from_database(attributes, column_types) - klass.allocate.init_with("attributes" => attributes, "new_record" => false, &block) + klass.allocate.init_from_db(attributes, &block) end # Updates an object (or multiple objects) and saves it to the database, if validations pass. diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb index d33d36ac02..c84f3d0fbb 100644 --- a/activerecord/lib/active_record/querying.rb +++ b/activerecord/lib/active_record/querying.rb @@ -49,7 +49,12 @@ module ActiveRecord } message_bus.instrument("instantiation.active_record", payload) do - result_set.map { |record| instantiate(record, column_types, &block) } + if result_set.includes_column?(inheritance_column) + result_set.map { |record| instantiate(record, column_types, &block) } + else + # Instantiate a homogeneous set + result_set.map { |record| instantiate_instance_of(self, record, column_types, &block) } + end end end diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 22d195c9a4..6d2f75a3ae 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -13,33 +13,37 @@ module ActiveRecord class_attribute :aggregate_reflections, instance_writer: false, default: {} end - def self.create(macro, name, scope, options, ar) - klass = \ - case macro - when :composed_of - AggregateReflection - when :has_many - HasManyReflection - when :has_one - HasOneReflection - when :belongs_to - BelongsToReflection - else - raise "Unsupported Macro: #{macro}" - end + class << self + def create(macro, name, scope, options, ar) + reflection = reflection_class_for(macro).new(name, scope, options, ar) + options[:through] ? ThroughReflection.new(reflection) : reflection + end - reflection = klass.new(name, scope, options, ar) - options[:through] ? ThroughReflection.new(reflection) : reflection - end + def add_reflection(ar, name, reflection) + ar.clear_reflections_cache + name = name.to_s + ar._reflections = ar._reflections.except(name).merge!(name => reflection) + end - def self.add_reflection(ar, name, reflection) - ar.clear_reflections_cache - 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) + end - def self.add_aggregate_reflection(ar, name, reflection) - ar.aggregate_reflections = ar.aggregate_reflections.merge(name.to_s => reflection) + private + def reflection_class_for(macro) + case macro + when :composed_of + AggregateReflection + when :has_many + HasManyReflection + when :has_one + HasOneReflection + when :belongs_to + BelongsToReflection + else + raise "Unsupported Macro: #{macro}" + end + end end # \Reflection enables the ability to examine the associations and aggregations of diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index c055b97061..7ab9bb2d2d 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -393,10 +393,7 @@ module ActiveRecord # Person.where(name: 'David').touch_all # # => "UPDATE \"people\" SET \"updated_at\" = '2018-01-04 22:55:23.132670' WHERE \"people\".\"name\" = 'David'" def touch_all(*names, time: nil) - attributes = Array(names) + klass.timestamp_attributes_for_update_in_model - time ||= klass.current_time_from_proper_timezone - updates = {} - attributes.each { |column| updates[column] = time } + updates = touch_attributes_with_time(*names, time: time) if klass.locking_enabled? quoted_locking_column = connection.quote_column_name(klass.locking_column) @@ -505,17 +502,16 @@ module ActiveRecord # # => SELECT "users".* FROM "users" WHERE "users"."name" = 'Oscar' def to_sql @to_sql ||= begin - relation = self - - if eager_loading? - apply_join_dependency { |rel, _| relation = rel } - end - - conn = klass.connection - conn.unprepared_statement { - conn.to_sql(relation.arel) - } - end + if eager_loading? + apply_join_dependency do |relation, join_dependency| + relation = join_dependency.apply_column_aliases(relation) + relation.to_sql + end + else + conn = klass.connection + conn.unprepared_statement { conn.to_sql(arel) } + end + end end # Returns a hash of where conditions. @@ -625,6 +621,7 @@ module ActiveRecord if ActiveRecord::NullRelation === relation [] else + relation = join_dependency.apply_column_aliases(relation) rows = connection.select_all(relation.arel, "SQL") join_dependency.instantiate(rows, &block) end.freeze diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index f7613a187d..b9e7c52e88 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -373,13 +373,12 @@ module ActiveRecord def construct_join_dependency including = eager_load_values + includes_values - joins = joins_values.select { |join| join.is_a?(Arel::Nodes::Join) } ActiveRecord::Associations::JoinDependency.new( - klass, table, including, alias_tracker(joins) + klass, table, including ) end - def apply_join_dependency(eager_loading: true) + def apply_join_dependency(eager_loading: group_values.empty?) join_dependency = construct_join_dependency relation = except(:includes, :eager_load, :preload).joins!(join_dependency) @@ -392,7 +391,6 @@ module ActiveRecord end if block_given? - relation._select!(join_dependency.aliases.columns) yield relation, join_dependency else relation diff --git a/activerecord/lib/active_record/relation/merger.rb b/activerecord/lib/active_record/relation/merger.rb index 25510d4a57..9cbcf61b3e 100644 --- a/activerecord/lib/active_record/relation/merger.rb +++ b/activerecord/lib/active_record/relation/merger.rb @@ -117,13 +117,11 @@ module ActiveRecord if other.klass == relation.klass relation.joins!(*other.joins_values) else - alias_tracker = nil joins_dependency = other.joins_values.map do |join| case join when Hash, Symbol, Array - alias_tracker ||= other.alias_tracker ActiveRecord::Associations::JoinDependency.new( - other.klass, other.table, join, alias_tracker + other.klass, other.table, join ) else join @@ -140,13 +138,11 @@ module ActiveRecord if other.klass == relation.klass relation.left_outer_joins!(*other.left_outer_joins_values) else - alias_tracker = nil joins_dependency = other.left_outer_joins_values.map do |join| case join when Hash, Symbol, Array - alias_tracker ||= other.alias_tracker ActiveRecord::Associations::JoinDependency.new( - other.klass, other.table, join, alias_tracker + other.klass, other.table, join ) else join diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index 7a0edcbc33..f734cd0ad8 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -48,7 +48,12 @@ module ActiveRecord end def build(attribute, value) - handler_for(value).call(attribute, value) + if table.type(attribute.name).force_equality?(value) + bind = build_bind_attribute(attribute.name, value) + attribute.eq(bind) + else + handler_for(value).call(attribute, value) + end end def build_bind_attribute(column_name, value) @@ -95,10 +100,6 @@ module ActiveRecord end.reduce(&:and) end queries.reduce(&:or) - # FIXME: Deprecate this and provide a public API to force equality - elsif (value.is_a?(Range) || value.is_a?(Array)) && - table.type(key.to_s).respond_to?(:subtype) - BasicObjectHandler.new(self).call(table.arel_attribute(key), value) else build(table.arel_attribute(key), value) end diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index db9101a168..5b4ba85316 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -1018,19 +1018,19 @@ module ActiveRecord def build_join_query(manager, buckets, join_type, aliases) buckets.default = [] - association_joins = buckets[:association_join] - stashed_association_joins = buckets[:stashed_join] - join_nodes = buckets[:join_node].uniq - string_joins = buckets[:string_join].map(&:strip).uniq + 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 = ActiveRecord::Associations::JoinDependency.new( - klass, table, association_joins, alias_tracker + klass, table, association_joins ) - joins = join_dependency.join_constraints(stashed_association_joins, join_type) + joins = join_dependency.join_constraints(stashed_joins, join_type, alias_tracker) joins.each { |join| manager.from(join) } manager.join_sources.concat(join_list) @@ -1056,11 +1056,13 @@ module ActiveRecord end def arel_columns(columns) - columns.map do |field| + columns.flat_map do |field| if (Symbol === field || String === field) && (klass.has_attribute?(field) || klass.attribute_alias?(field)) && !from_clause.value arel_attribute(field) elsif Symbol === field connection.quote_table_name(field.to_s) + elsif Proc === field + field.call else field end diff --git a/activerecord/lib/active_record/result.rb b/activerecord/lib/active_record/result.rb index e54e8086dd..7f1c2fd7eb 100644 --- a/activerecord/lib/active_record/result.rb +++ b/activerecord/lib/active_record/result.rb @@ -43,6 +43,11 @@ module ActiveRecord @column_types = column_types end + # Returns true if this result set includes the column named +name+ + def includes_column?(name) + @columns.include? name + end + # Returns the number of elements in the rows array. def length @rows.length @@ -97,12 +102,21 @@ module ActiveRecord end def cast_values(type_overrides = {}) # :nodoc: - types = columns.map { |name| column_type(name, type_overrides) } - result = rows.map do |values| - types.zip(values).map { |type, value| type.deserialize(value) } - end + if columns.one? + # Separated to avoid allocating an array per row - columns.one? ? result.map!(&:first) : result + type = column_type(columns.first, type_overrides) + + rows.map do |(value)| + type.deserialize(value) + end + else + types = columns.map { |name| column_type(name, type_overrides) } + + rows.map do |values| + Array.new(values.size) { |i| types[i].deserialize(values[i]) } + end + end end def initialize_copy(other) @@ -125,7 +139,7 @@ module ActiveRecord begin # We freeze the strings to prevent them getting duped when # used as keys in ActiveRecord::Base's @attributes hash - columns = @columns.map { |c| c.dup.freeze } + columns = @columns.map(&:-@) @rows.map { |row| # In the past we used Hash[columns.zip(row)] # though elegant, the verbose way is much more efficient diff --git a/activerecord/lib/active_record/store.rb b/activerecord/lib/active_record/store.rb index 8d628359c3..3537e2d008 100644 --- a/activerecord/lib/active_record/store.rb +++ b/activerecord/lib/active_record/store.rb @@ -33,12 +33,16 @@ module ActiveRecord # store :settings, accessors: [ :color, :homepage ], coder: JSON # store :parent, accessors: [ :name ], coder: JSON, prefix: true # store :spouse, accessors: [ :name ], coder: JSON, prefix: :partner + # store :settings, accessors: [ :two_factor_auth ], suffix: true + # store :settings, accessors: [ :login_retry ], suffix: :config # end # # u = User.new(color: 'black', homepage: '37signals.com', parent_name: 'Mary', partner_name: 'Lily') # u.color # Accessor stored attribute # u.parent_name # Accessor stored attribute with prefix # u.partner_name # Accessor stored attribute with custom prefix + # u.two_factor_auth_settings # Accessor stored attribute with suffix + # u.login_retry_config # Accessor stored attribute with custom suffix # u.settings[:country] = 'Denmark' # Any attribute, even if not specified with an accessor # # # There is no difference between strings and symbols for accessing custom attributes @@ -49,11 +53,12 @@ module ActiveRecord # class SuperUser < User # store_accessor :settings, :privileges, :servants # store_accessor :parent, :birthday, prefix: true + # store_accessor :settings, :secret_question, suffix: :config # end # # The stored attribute names can be retrieved using {.stored_attributes}[rdoc-ref:rdoc-ref:ClassMethods#stored_attributes]. # - # User.stored_attributes[:settings] # [:color, :homepage] + # User.stored_attributes[:settings] # [:color, :homepage, :two_factor_auth, :login_retry] # # == Overwriting default accessors # @@ -86,10 +91,10 @@ module ActiveRecord module ClassMethods def store(store_attribute, options = {}) serialize store_attribute, IndifferentCoder.new(store_attribute, options[:coder]) - store_accessor(store_attribute, options[:accessors], prefix: options[:prefix]) if options.has_key? :accessors + store_accessor(store_attribute, options[:accessors], options.slice(:prefix, :suffix)) if options.has_key? :accessors end - def store_accessor(store_attribute, *keys, prefix: nil) + def store_accessor(store_attribute, *keys, prefix: nil, suffix: nil) keys = keys.flatten accessor_prefix = @@ -101,14 +106,25 @@ module ActiveRecord else "" end + accessor_suffix = + case suffix + when String, Symbol + "_#{suffix}" + when TrueClass + "_#{store_attribute}" + else + "" + end _store_accessors_module.module_eval do keys.each do |key| - define_method("#{accessor_prefix}#{key}=") do |value| + accessor_key = "#{accessor_prefix}#{key}#{accessor_suffix}" + + define_method("#{accessor_key}=") do |value| write_store_attribute(store_attribute, key, value) end - define_method("#{accessor_prefix}#{key}") do + define_method(accessor_key) do read_store_attribute(store_attribute, key) end end diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb index e47f06bf3a..d32f971ad1 100644 --- a/activerecord/lib/active_record/timestamp.rb +++ b/activerecord/lib/active_record/timestamp.rb @@ -53,12 +53,10 @@ module ActiveRecord end module ClassMethods # :nodoc: - def timestamp_attributes_for_update_in_model - timestamp_attributes_for_update.select { |c| column_names.include?(c) } - end - - def current_time_from_proper_timezone - default_timezone == :utc ? Time.now.utc : Time.now + def touch_attributes_with_time(*names, time: nil) + attribute_names = timestamp_attributes_for_update_in_model + attribute_names |= names.map(&:to_s) + attribute_names.index_with(time ||= current_time_from_proper_timezone) end private @@ -66,6 +64,10 @@ module ActiveRecord timestamp_attributes_for_create.select { |c| column_names.include?(c) } end + def timestamp_attributes_for_update_in_model + timestamp_attributes_for_update.select { |c| column_names.include?(c) } + end + def all_timestamp_attributes_in_model timestamp_attributes_for_create_in_model + timestamp_attributes_for_update_in_model end @@ -77,6 +79,10 @@ module ActiveRecord def timestamp_attributes_for_update ["updated_at", "updated_on"] end + + def current_time_from_proper_timezone + default_timezone == :utc ? Time.now.utc : Time.now + end end private @@ -116,7 +122,7 @@ module ActiveRecord end def timestamp_attributes_for_update_in_model - self.class.timestamp_attributes_for_update_in_model + self.class.send(:timestamp_attributes_for_update_in_model) end def all_timestamp_attributes_in_model @@ -124,7 +130,7 @@ module ActiveRecord end def current_time_from_proper_timezone - self.class.current_time_from_proper_timezone + self.class.send(:current_time_from_proper_timezone) end def max_updated_column_timestamp(timestamp_names = timestamp_attributes_for_update_in_model) diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index 82adb19f5b..c5d5fca672 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -328,10 +328,12 @@ module ActiveRecord # but call it after the commit of a destroyed object. def committed!(should_run_callbacks: true) #:nodoc: if should_run_callbacks && (destroyed? || persisted?) + @_committed_already_called = true _run_commit_without_transaction_enrollment_callbacks _run_commit_callbacks end ensure + @_committed_already_called = false force_clear_transaction_record_state end @@ -380,6 +382,7 @@ module ActiveRecord 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 @@ -390,6 +393,15 @@ module ActiveRecord frozen?: frozen?, ) @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) + 1 + remember_new_record_before_last_commit + end + + def remember_new_record_before_last_commit + if _committed_already_called + @_new_record_before_last_commit = false + else + @_new_record_before_last_commit = @_start_transaction_state[:new_record] + end end # Clear the new record state and id of a record. @@ -421,22 +433,16 @@ module ActiveRecord end end - # Determine if a record was created or destroyed in a transaction. State should be one of :new_record or :destroyed. - def transaction_record_state(state) - @_start_transaction_state[state] - end - # Determine if a transaction included an action for :create, :update, or :destroy. Used in filtering callbacks. def transaction_include_any_action?(actions) actions.any? do |action| case action when :create - transaction_record_state(:new_record) - when :destroy - defined?(@_trigger_destroy_callback) && @_trigger_destroy_callback + persisted? && @_new_record_before_last_commit when :update - !(transaction_record_state(:new_record) || destroyed?) && - (defined?(@_trigger_update_callback) && @_trigger_update_callback) + !(@_new_record_before_last_commit || destroyed?) && _trigger_update_callback + when :destroy + _trigger_destroy_callback end end end diff --git a/activerecord/lib/active_record/type/serialized.rb b/activerecord/lib/active_record/type/serialized.rb index e882784691..0a2f6cb9fb 100644 --- a/activerecord/lib/active_record/type/serialized.rb +++ b/activerecord/lib/active_record/type/serialized.rb @@ -51,6 +51,10 @@ module ActiveRecord end end + def force_equality?(value) + coder.respond_to?(:object_class) && value.is_a?(coder.object_class) + end + private def default_value?(value) diff --git a/activerecord/lib/arel/visitors/postgresql.rb b/activerecord/lib/arel/visitors/postgresql.rb index 108ee431ee..c5110fa89c 100644 --- a/activerecord/lib/arel/visitors/postgresql.rb +++ b/activerecord/lib/arel/visitors/postgresql.rb @@ -5,7 +5,7 @@ module Arel # :nodoc: all class PostgreSQL < Arel::Visitors::ToSql CUBE = "CUBE" ROLLUP = "ROLLUP" - GROUPING_SET = "GROUPING SET" + GROUPING_SETS = "GROUPING SETS" LATERAL = "LATERAL" private @@ -67,7 +67,7 @@ module Arel # :nodoc: all end def visit_Arel_Nodes_GroupingSet(o, collector) - collector << GROUPING_SET + collector << GROUPING_SETS grouping_array_or_grouping_element o, collector end |