diff options
Diffstat (limited to 'activerecord/lib/active_record')
16 files changed, 184 insertions, 171 deletions
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/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 48cb819484..840d900bbc 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -400,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 diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb index c1faa021aa..0835d2cc9b 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, true) - } + 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,11 +171,6 @@ 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}" join ? "#{name}_join" : name @@ -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/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/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb index faaedf1d4a..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,7 +111,8 @@ 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 @@ -171,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/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 31fb8ce0e5..b9e7c52e88 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -373,9 +373,8 @@ 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 @@ -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/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 11626c8e31..ffef229be2 100644 --- a/activerecord/lib/active_record/result.rb +++ b/activerecord/lib/active_record/result.rb @@ -97,12 +97,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 + + type = column_type(columns.first, type_overrides) - columns.one? ? result.map!(&:first) : result + 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) 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) |