diff options
Diffstat (limited to 'activerecord/lib')
136 files changed, 2299 insertions, 1478 deletions
diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb index 1040e6e3bb..39077aea7e 100644 --- a/activerecord/lib/active_record/aggregations.rb +++ b/activerecord/lib/active_record/aggregations.rb @@ -245,7 +245,8 @@ module ActiveRecord define_method("#{name}=") do |part| klass = class_name.constantize if part.is_a?(Hash) - part = klass.new(*part.values) + raise ArgumentError unless part.size == part.keys.max + part = klass.new(*part.sort.map(&:last)) end unless part.is_a?(klass) || converter.nil? || part.nil? diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 14af55f327..a146d78a5a 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1000,6 +1000,8 @@ module ActiveRecord # callbacks declared either before or after the <tt>:dependent</tt> option # can affect what it does. # + # Note that <tt>:dependent</tt> option is ignored for +has_one+ <tt>:through</tt> associations. + # # === Delete or destroy? # # +has_many+ and +has_and_belongs_to_many+ associations have the methods <tt>destroy</tt>, @@ -1012,7 +1014,7 @@ module ActiveRecord # record(s) being removed so that callbacks are run. However <tt>delete</tt> and <tt>delete_all</tt> will either # do the deletion according to the strategy specified by the <tt>:dependent</tt> option, or # if no <tt>:dependent</tt> option is given, then it will follow the default strategy. - # The default strategy is <tt>:nullify</tt> (set the foreign keys to <tt>nil</tt>), except for + # The default strategy is to do nothing (leave the foreign keys with the parent ids set), except for # +has_many+ <tt>:through</tt>, where the default strategy is <tt>delete_all</tt> (delete # the join records, without running their callbacks). # @@ -1245,6 +1247,10 @@ module ActiveRecord # that is the inverse of this <tt>has_many</tt> association. Does not work in combination # with <tt>:through</tt> or <tt>:as</tt> options. # See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail. + # [:extend] + # Specifies a module or array of modules that will be extended into the association object returned. + # Useful for defining methods on associations, especially when they should be shared between multiple + # association objects. # # Option examples: # has_many :comments, -> { order "posted_on" } @@ -1326,6 +1332,8 @@ module ActiveRecord # * <tt>:nullify</tt> causes the foreign key to be set to +NULL+. Callbacks are not executed. # * <tt>:restrict_with_exception</tt> causes an exception to be raised if there is an associated record # * <tt>:restrict_with_error</tt> causes an error to be added to the owner if there is an associated object + # + # Note that <tt>:dependent</tt> option is ignored when using <tt>:through</tt> option. # [:foreign_key] # Specify the foreign key used for the association. By default this is guessed to be the name # of this class in lower-case and "_id" suffixed. So a Person class that makes a +has_one+ association @@ -1378,7 +1386,7 @@ module ActiveRecord # has_one :last_comment, -> { order 'posted_on' }, class_name: "Comment" # has_one :project_manager, -> { where role: 'project_manager' }, class_name: "Person" # has_one :attachment, as: :attachable - # has_one :boss, readonly: :true + # has_one :boss, -> { readonly } # has_one :club, through: :membership # has_one :primary_address, -> { where primary: true }, through: :addressables, source: :addressable # has_one :credit_card, required: true @@ -1430,7 +1438,7 @@ module ActiveRecord # when you access the associated object. # # Scope examples: - # belongs_to :user, -> { where(id: 2) } + # belongs_to :firm, -> { where(id: 2) } # belongs_to :user, -> { joins(:friends) } # belongs_to :level, ->(level) { where("game_level > ?", level.current) } # @@ -1506,11 +1514,11 @@ module ActiveRecord # belongs_to :valid_coupon, ->(o) { where "discounts > ?", o.payments_count }, # class_name: "Coupon", foreign_key: "coupon_id" # belongs_to :attachable, polymorphic: true - # belongs_to :project, readonly: true + # belongs_to :project, -> { readonly } # belongs_to :post, counter_cache: true - # belongs_to :company, touch: true + # belongs_to :comment, touch: true # belongs_to :company, touch: :employees_last_updated_at - # belongs_to :company, required: true + # belongs_to :user, required: true def belongs_to(name, scope = nil, options = {}) reflection = Builder::BelongsTo.build(self, name, scope, options) Reflection.add_reflection self, name, reflection diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb index d06b7b3508..2416167834 100644 --- a/activerecord/lib/active_record/associations/association_scope.rb +++ b/activerecord/lib/active_record/associations/association_scope.rb @@ -2,42 +2,30 @@ module ActiveRecord module Associations class AssociationScope #:nodoc: def self.scope(association, connection) - INSTANCE.scope association, connection - end - - class BindSubstitution - def initialize(block) - @block = block - end - - def bind_value(scope, column, value, connection) - substitute = connection.substitute_at(column) - scope.bind_values += [[column, @block.call(value)]] - substitute - end + INSTANCE.scope(association, connection) end def self.create(&block) - block = block ? block : lambda { |val| val } - new BindSubstitution.new(block) + block ||= lambda { |val| val } + new(block) end - def initialize(bind_substitution) - @bind_substitution = bind_substitution + def initialize(value_transformation) + @value_transformation = value_transformation end INSTANCE = create def scope(association, connection) - klass = association.klass - reflection = association.reflection - scope = klass.unscoped - owner = association.owner + klass = association.klass + reflection = association.reflection + scope = klass.unscoped + owner = association.owner alias_tracker = AliasTracker.create connection, association.klass.table_name, klass.type_caster chain_head, chain_tail = get_chain(reflection, association, alias_tracker) scope.extending! Array(reflection.options[:extend]) - add_constraints(scope, owner, klass, reflection, connection, chain_head, chain_tail) + add_constraints(scope, owner, klass, reflection, chain_head, chain_tail) end def join_type @@ -61,43 +49,36 @@ module ActiveRecord binds end + protected + + attr_reader :value_transformation + private def join(table, constraint) table.create_join(table, table.create_on(constraint), join_type) end - def column_for(table_name, column_name, connection) - columns = connection.schema_cache.columns_hash(table_name) - columns[column_name] - end - - def bind_value(scope, column, value, connection) - @bind_substitution.bind_value scope, column, value, connection - end - - def bind(scope, table_name, column_name, value, connection) - column = column_for table_name, column_name, connection - bind_value scope, column, value, connection - end - - def last_chain_scope(scope, table, reflection, owner, connection, association_klass) + def last_chain_scope(scope, table, reflection, owner, association_klass) join_keys = reflection.join_keys(association_klass) key = join_keys.key foreign_key = join_keys.foreign_key - bind_val = bind scope, table.table_name, key.to_s, owner[foreign_key], connection - scope = scope.where(table[key].eq(bind_val)) + value = transform_value(owner[foreign_key]) + scope = scope.where(table.name => { key => value }) if reflection.type - value = owner.class.base_class.name - bind_val = bind scope, table.table_name, reflection.type, value, connection - scope = scope.where(table[reflection.type].eq(bind_val)) - else - scope + polymorphic_type = transform_value(owner.class.base_class.name) + scope = scope.where(table.name => { reflection.type => polymorphic_type }) end + + scope + end + + def transform_value(value) + value_transformation.call(value) end - def next_chain_scope(scope, table, reflection, connection, association_klass, foreign_table, next_reflection) + def next_chain_scope(scope, table, reflection, association_klass, foreign_table, next_reflection) join_keys = reflection.join_keys(association_klass) key = join_keys.key foreign_key = join_keys.foreign_key @@ -105,9 +86,8 @@ module ActiveRecord constraint = table[key].eq(foreign_table[foreign_key]) if reflection.type - value = next_reflection.klass.base_class.name - bind_val = bind scope, table.table_name, reflection.type, value, connection - scope = scope.where(table[reflection.type].eq(bind_val)) + value = transform_value(next_reflection.klass.base_class.name) + scope = scope.where(table.name => { reflection.type => value }) end scope = scope.joins(join(foreign_table, constraint)) @@ -138,10 +118,10 @@ module ActiveRecord [runtime_reflection, previous_reflection] end - def add_constraints(scope, owner, association_klass, refl, connection, chain_head, chain_tail) + def add_constraints(scope, owner, association_klass, refl, chain_head, chain_tail) owner_reflection = chain_tail table = owner_reflection.alias_name - scope = last_chain_scope(scope, table, owner_reflection, owner, connection, association_klass) + scope = last_chain_scope(scope, table, owner_reflection, owner, association_klass) reflection = chain_head loop do @@ -151,7 +131,7 @@ module ActiveRecord unless reflection == chain_tail next_reflection = reflection.next foreign_table = next_reflection.alias_name - scope = next_chain_scope(scope, table, reflection, connection, association_klass, foreign_table, next_reflection) + scope = next_chain_scope(scope, table, reflection, association_klass, foreign_table, next_reflection) end # Exclude the scope of the association itself, because that @@ -160,15 +140,14 @@ module ActiveRecord item = eval_scope(reflection.klass, scope_chain_item, owner) if scope_chain_item == refl.scope - scope.merge! item.except(:where, :includes, :bind) + scope.merge! item.except(:where, :includes) end reflection.all_includes do scope.includes! item.includes_values end - scope.where_values += item.where_values - scope.bind_values += item.bind_values + scope.where_clause += item.where_clause scope.order_values |= item.order_values end diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb index c63b42e2a0..265a65c4c1 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -68,6 +68,9 @@ module ActiveRecord def increment_counter(counter_cache_name) if foreign_key_present? klass.increment_counter(counter_cache_name, target_id) + if target && !stale_target? + target.increment(counter_cache_name) + end end end diff --git a/activerecord/lib/active_record/associations/builder/singular_association.rb b/activerecord/lib/active_record/associations/builder/singular_association.rb index 1369212837..f6274c027e 100644 --- a/activerecord/lib/active_record/associations/builder/singular_association.rb +++ b/activerecord/lib/active_record/associations/builder/singular_association.rb @@ -31,7 +31,7 @@ module ActiveRecord::Associations::Builder def self.define_validations(model, reflection) super if reflection.options[:required] - model.validates_presence_of reflection.name + model.validates_presence_of reflection.name, message: :required end end end diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index f2c96e9a2a..eea847cfa3 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -63,7 +63,7 @@ module ActiveRecord def ids_writer(ids) pk_type = reflection.primary_key_type ids = Array(ids).reject(&:blank?) - ids.map! { |i| pk_type.type_cast_from_user(i) } + ids.map! { |i| pk_type.cast(i) } replace(klass.find(ids).index_by(&:id).values_at(*ids)) end @@ -151,6 +151,7 @@ module ActiveRecord # be chained. Since << flattens its argument list and inserts each record, # +push+ and +concat+ behave identically. def concat(*records) + records = records.flatten if owner.new_record? load_target concat_records(records) @@ -318,7 +319,8 @@ module ActiveRecord end # Returns true if the collections is not empty. - # Equivalent to +!collection.empty?+. + # If block given, loads all records and checks for one or more matches. + # Otherwise, equivalent to +!collection.empty?+. def any? if block_given? load_target.any? { |*block_args| yield(*block_args) } @@ -328,7 +330,8 @@ module ActiveRecord end # Returns true if the collection has more than 1 record. - # Equivalent to +collection.size > 1+. + # If block given, loads all records and checks for two or more matches. + # Otherwise, equivalent to +collection.size > 1+. def many? if block_given? load_target.many? { |*block_args| yield(*block_args) } @@ -549,7 +552,7 @@ module ActiveRecord def concat_records(records, should_raise = false) result = true - records.flatten.each do |record| + records.each do |record| raise_on_type_mismatch!(record) add_to_target(record) do |rec| result &&= insert_record(rec, true, should_raise) unless owner.new_record? diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index 2a782c06d0..ca27c9fdde 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -85,7 +85,11 @@ module ActiveRecord end def cached_counter_attribute_name(reflection = reflection()) - options[:counter_cache] || "#{reflection.name}_count" + if reflection.options[:counter_cache] + reflection.options[:counter_cache].to_s + else + "#{reflection.name}_count" + end end def update_counter(difference, reflection = reflection()) @@ -101,7 +105,7 @@ module ActiveRecord end def update_counter_in_memory(difference, reflection = reflection()) - if has_cached_counter?(reflection) + if counter_must_be_updated_by_has_many?(reflection) counter = cached_counter_attribute_name(reflection) owner[counter] += difference owner.send(:clear_attribute_changes, counter) # eww @@ -118,18 +122,28 @@ module ActiveRecord # it will be decremented twice. # # Hence this method. - def inverse_updates_counter_cache?(reflection = reflection()) + def inverse_which_updates_counter_cache(reflection = reflection()) counter_name = cached_counter_attribute_name(reflection) - inverse_updates_counter_named?(counter_name, reflection) + inverse_which_updates_counter_named(counter_name, reflection) end + alias inverse_updates_counter_cache? inverse_which_updates_counter_cache - def inverse_updates_counter_named?(counter_name, reflection = reflection()) - reflection.klass._reflections.values.any? { |inverse_reflection| + def inverse_which_updates_counter_named(counter_name, reflection) + reflection.klass._reflections.values.find { |inverse_reflection| inverse_reflection.belongs_to? && inverse_reflection.counter_cache_column == counter_name } end + def inverse_updates_counter_in_memory?(reflection) + inverse = inverse_which_updates_counter_cache(reflection) + inverse && inverse == reflection.inverse_of + end + + def counter_must_be_updated_by_has_many?(reflection) + !inverse_updates_counter_in_memory?(reflection) && has_cached_counter?(reflection) + end + def delete_count(method, scope) if method == :delete_all scope.delete_all 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 f1e784d771..4897ec44e9 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -143,7 +143,7 @@ module ActiveRecord stmt.from scope.klass.arel_table stmt.wheres = arel.constraints - count = scope.klass.connection.delete(stmt, 'SQL', scope.bind_values) + count = scope.klass.connection.delete(stmt, 'SQL', scope.bound_attributes) end when :nullify count = scope.update_all(source_reflection.foreign_key => nil) @@ -160,9 +160,9 @@ module ActiveRecord if through_reflection.collection? && update_through_counter?(method) update_counter(-count, through_reflection) + else + update_counter(-count) end - - update_counter(-count) end def through_records_for(record) diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb index 4b75370171..fcf06323e6 100644 --- a/activerecord/lib/active_record/associations/join_dependency.rb +++ b/activerecord/lib/active_record/associations/join_dependency.rb @@ -232,6 +232,7 @@ module ActiveRecord end def construct(ar_parent, parent, row, rs, seen, model_cache, aliases) + return if ar_parent.nil? primary_id = ar_parent.id parent.children.each do |node| @@ -248,7 +249,11 @@ module ActiveRecord key = aliases.column_alias(node, node.primary_key) id = row[key] - next if id.nil? + if id.nil? + nil_association = ar_parent.association(node.reflection.name) + nil_association.loaded! + next + end model = seen[parent.base_klass][primary_id][node.base_klass][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 c1ef86a95b..a6ad09a38a 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb @@ -25,7 +25,7 @@ module ActiveRecord def join_constraints(foreign_table, foreign_klass, node, join_type, tables, scope_chain, chain) joins = [] - bind_values = [] + binds = [] tables = tables.reverse scope_chain_index = 0 @@ -66,7 +66,7 @@ module ActiveRecord end if rel && !rel.arel.constraints.empty? - bind_values.concat rel.bind_values + binds += rel.bound_attributes constraint = constraint.and rel.arel.constraints end @@ -75,7 +75,7 @@ module ActiveRecord column = klass.columns_hash[reflection.type.to_s] substitute = klass.connection.substitute_at(column) - bind_values.push [column, value] + binds << Relation::QueryAttribute.new(column.name, value, klass.type_for_attribute(column.name)) constraint = constraint.and table[reflection.type].eq substitute end @@ -85,7 +85,7 @@ module ActiveRecord foreign_table, foreign_klass = table, klass end - JoinInformation.new joins, bind_values + JoinInformation.new joins, binds end # Builds equality condition. diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb index 4358f3b581..97f4bd3811 100644 --- a/activerecord/lib/active_record/associations/preloader.rb +++ b/activerecord/lib/active_record/associations/preloader.rb @@ -89,7 +89,7 @@ module ActiveRecord # { author: :avatar } # [ :books, { author: :avatar } ] - NULL_RELATION = Struct.new(:values, :bind_values).new({}, []) + NULL_RELATION = Struct.new(:values, :where_clause, :joins_values).new({}, Relation::WhereClause.empty, []) def preload(records, associations, preload_scope = nil) records = Array.wrap(records).compact.uniq diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb index afcaa5d55a..1dc8bff193 100644 --- a/activerecord/lib/active_record/associations/preloader/association.rb +++ b/activerecord/lib/active_record/associations/preloader/association.rb @@ -104,11 +104,11 @@ module ActiveRecord end def association_key_type - @klass.column_for_attribute(association_key_name).type + @klass.type_for_attribute(association_key_name.to_s).type end def owner_key_type - @model.column_for_attribute(owner_key_name).type + @model.type_for_attribute(owner_key_name.to_s).type end def load_slices(slices) @@ -131,18 +131,19 @@ module ActiveRecord def build_scope scope = klass.unscoped - values = reflection_scope.values - reflection_binds = reflection_scope.bind_values + values = reflection_scope.values preload_values = preload_scope.values - preload_binds = preload_scope.bind_values - scope.where_values = Array(values[:where]) + Array(preload_values[:where]) + scope.where_clause = reflection_scope.where_clause + preload_scope.where_clause scope.references_values = Array(values[:references]) + Array(preload_values[:references]) - scope.bind_values = (reflection_binds + preload_binds) - scope._select! preload_values[:select] || values[:select] || table[Arel.star] + scope._select! preload_values[:select] || values[:select] || table[Arel.star] scope.includes! preload_values[:includes] || values[:includes] - scope.joins! preload_values[:joins] || values[:joins] + if preload_scope.joins_values.any? + scope.joins!(preload_scope.joins_values) + else + scope.joins!(reflection_scope.joins_values) + end scope.order! preload_values[:order] || values[:order] if preload_values[:readonly] || values[:readonly] diff --git a/activerecord/lib/active_record/associations/preloader/through_association.rb b/activerecord/lib/active_record/associations/preloader/through_association.rb index 12bf3ef138..56aa23b173 100644 --- a/activerecord/lib/active_record/associations/preloader/through_association.rb +++ b/activerecord/lib/active_record/associations/preloader/through_association.rb @@ -78,10 +78,9 @@ module ActiveRecord if options[:source_type] scope.where! reflection.foreign_type => options[:source_type] else - unless reflection_scope.where_values.empty? + unless reflection_scope.where_clause.empty? scope.includes_values = Array(reflection_scope.values[:includes] || options[:source]) - scope.where_values = reflection_scope.values[:where] - scope.bind_values = reflection_scope.bind_values + scope.where_clause = reflection_scope.where_clause end scope.references! reflection_scope.values[:references] diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb index 09828dbd9b..3ce9cffdbc 100644 --- a/activerecord/lib/active_record/associations/through_association.rb +++ b/activerecord/lib/active_record/associations/through_association.rb @@ -18,7 +18,7 @@ module ActiveRecord reflection_scope = reflection.scope if reflection_scope && reflection_scope.arity.zero? - relation.merge!(reflection_scope) + relation = relation.merge(reflection_scope) end scope.merge!( diff --git a/activerecord/lib/active_record/attribute.rb b/activerecord/lib/active_record/attribute.rb index 88536eaac0..73dd3fa041 100644 --- a/activerecord/lib/active_record/attribute.rb +++ b/activerecord/lib/active_record/attribute.rb @@ -43,7 +43,7 @@ module ActiveRecord end def value_for_database - type.type_cast_for_database(value) + type.serialize(value) end def changed_from?(old_value) @@ -51,7 +51,7 @@ module ActiveRecord end def changed_in_place_from?(old_value) - type.changed_in_place?(old_value, value) + has_been_read? && type.changed_in_place?(old_value, value) end def with_value_from_user(value) @@ -66,6 +66,10 @@ module ActiveRecord self.class.with_cast_value(name, value, type) end + def with_type(type) + self.class.new(name, value_before_type_cast, type) + end + def type_cast(*) raise NotImplementedError end @@ -74,12 +78,25 @@ module ActiveRecord 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 protected @@ -91,13 +108,17 @@ module ActiveRecord class FromDatabase < Attribute # :nodoc: def type_cast(value) - type.type_cast_from_database(value) + type.deserialize(value) end end class FromUser < Attribute # :nodoc: def type_cast(value) - type.type_cast_from_user(value) + type.cast(value) + end + + def came_from_user? + true end end @@ -120,6 +141,10 @@ module ActiveRecord 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 @@ -144,6 +169,6 @@ module ActiveRecord false end end - private_constant :FromDatabase, :FromUser, :Null, :Uninitialized + private_constant :FromDatabase, :FromUser, :Null, :Uninitialized, :WithCastValue end end diff --git a/activerecord/lib/active_record/attribute_assignment.rb b/activerecord/lib/active_record/attribute_assignment.rb index bf64830417..483d4200bd 100644 --- a/activerecord/lib/active_record/attribute_assignment.rb +++ b/activerecord/lib/active_record/attribute_assignment.rb @@ -3,63 +3,32 @@ require 'active_model/forbidden_attributes_protection' module ActiveRecord module AttributeAssignment extend ActiveSupport::Concern - include ActiveModel::ForbiddenAttributesProtection + include ActiveModel::AttributeAssignment - # Allows you to set all the attributes by passing in a hash of attributes with - # keys matching the attribute names (which again matches the column names). - # - # If the passed hash responds to <tt>permitted?</tt> method and the return value - # of this method is +false+ an <tt>ActiveModel::ForbiddenAttributesError</tt> - # exception is raised. - # - # cat = Cat.new(name: "Gorby", status: "yawning") - # cat.attributes # => { "name" => "Gorby", "status" => "yawning", "created_at" => nil, "updated_at" => nil} - # cat.assign_attributes(status: "sleeping") - # cat.attributes # => { "name" => "Gorby", "status" => "sleeping", "created_at" => nil, "updated_at" => nil } - # - # New attributes will be persisted in the database when the object is saved. - # - # Aliased to <tt>attributes=</tt>. - def assign_attributes(new_attributes) - if !new_attributes.respond_to?(:stringify_keys) - raise ArgumentError, "When assigning attributes, you must pass a hash as an argument." - end - return if new_attributes.blank? + # Alias for `assign_attributes`. See +ActiveModel::AttributeAssignment+. + def attributes=(attributes) + assign_attributes(attributes) + end - attributes = new_attributes.stringify_keys - multi_parameter_attributes = [] - nested_parameter_attributes = [] + private - attributes = sanitize_for_mass_assignment(attributes) + def _assign_attributes(attributes) # :nodoc: + multi_parameter_attributes = {} + nested_parameter_attributes = {} attributes.each do |k, v| if k.include?("(") - multi_parameter_attributes << [ k, v ] + multi_parameter_attributes[k] = attributes.delete(k) elsif v.is_a?(Hash) - nested_parameter_attributes << [ k, v ] - else - _assign_attribute(k, v) + nested_parameter_attributes[k] = attributes.delete(k) end end + super(attributes) assign_nested_parameter_attributes(nested_parameter_attributes) unless nested_parameter_attributes.empty? assign_multiparameter_attributes(multi_parameter_attributes) unless multi_parameter_attributes.empty? end - alias attributes= assign_attributes - - private - - def _assign_attribute(k, v) - public_send("#{k}=", v) - rescue NoMethodError - if respond_to?("#{k}=") - raise - else - raise UnknownAttributeError.new(self, k) - end - end - # Assign any deferred nested attributes after the base attributes have been set. def assign_nested_parameter_attributes(pairs) pairs.each { |k, v| _assign_attribute(k, v) } @@ -81,7 +50,12 @@ module ActiveRecord errors = [] callstack.each do |name, values_with_empty_parameters| begin - send("#{name}=", MultiparameterAttribute.new(self, name, values_with_empty_parameters).read_value) + if values_with_empty_parameters.each_value.all?(&:nil?) + values = nil + else + values = values_with_empty_parameters + end + send("#{name}=", values) rescue => ex errors << AttributeAssignmentError.new("error on assignment #{values_with_empty_parameters.values.inspect} to #{name} (#{ex.message})", ex, name) end @@ -113,100 +87,5 @@ module ActiveRecord def find_parameter_position(multiparameter_name) multiparameter_name.scan(/\(([0-9]*).*\)/).first.first.to_i end - - class MultiparameterAttribute #:nodoc: - attr_reader :object, :name, :values, :cast_type - - def initialize(object, name, values) - @object = object - @name = name - @values = values - end - - def read_value - return if values.values.compact.empty? - - @cast_type = object.type_for_attribute(name) - klass = cast_type.klass - - if klass == Time - read_time - elsif klass == Date - read_date - else - read_other - end - end - - private - - def instantiate_time_object(set_values) - if object.class.send(:create_time_zone_conversion_attribute?, name, cast_type) - Time.zone.local(*set_values) - else - Time.send(object.class.default_timezone, *set_values) - end - end - - def read_time - # If column is a :time (and not :date or :datetime) there is no need to validate if - # there are year/month/day fields - if cast_type.type == :time - # if the column is a time set the values to their defaults as January 1, 1970, but only if they're nil - { 1 => 1970, 2 => 1, 3 => 1 }.each do |key,value| - values[key] ||= value - end - else - # else column is a timestamp, so if Date bits were not provided, error - validate_required_parameters!([1,2,3]) - - # If Date bits were provided but blank, then return nil - return if blank_date_parameter? - end - - max_position = extract_max_param(6) - set_values = values.values_at(*(1..max_position)) - # If Time bits are not there, then default to 0 - (3..5).each { |i| set_values[i] = set_values[i].presence || 0 } - instantiate_time_object(set_values) - end - - def read_date - return if blank_date_parameter? - set_values = values.values_at(1,2,3) - begin - Date.new(*set_values) - rescue ArgumentError # if Date.new raises an exception on an invalid date - instantiate_time_object(set_values).to_date # we instantiate Time object and convert it back to a date thus using Time's logic in handling invalid dates - end - end - - def read_other - max_position = extract_max_param - positions = (1..max_position) - validate_required_parameters!(positions) - - values.slice(*positions) - end - - # Checks whether some blank date parameter exists. Note that this is different - # than the validate_required_parameters! method, since it just checks for blank - # positions instead of missing ones, and does not raise in case one blank position - # exists. The caller is responsible to handle the case of this returning true. - def blank_date_parameter? - (1..3).any? { |position| values[position].blank? } - end - - # If some position is not provided, it errors out a missing parameter exception. - def validate_required_parameters!(positions) - if missing_parameter = positions.detect { |position| !values.key?(position) } - raise ArgumentError.new("Missing Parameter - #{name}(#{missing_parameter})") - end - end - - def extract_max_param(upper_cap = 100) - [values.keys.max, upper_cap].min - end - end end end diff --git a/activerecord/lib/active_record/attribute_decorators.rb b/activerecord/lib/active_record/attribute_decorators.rb index 5b96623b6e..7d0ae32411 100644 --- a/activerecord/lib/active_record/attribute_decorators.rb +++ b/activerecord/lib/active_record/attribute_decorators.rb @@ -15,7 +15,7 @@ module ActiveRecord end def decorate_matching_attribute_types(matcher, decorator_name, &block) - clear_caches_calculated_from_columns + reload_schema_from_cache decorator_name = decorator_name.to_s # Create new hashes so we don't modify parent classes @@ -24,10 +24,11 @@ module ActiveRecord private - def add_user_provided_columns(*) - super.map do |column| - decorated_type = attribute_type_decorations.apply(column.name, column.cast_type) - column.with_type(decorated_type) + def load_schema! + super + attribute_types.each do |name, type| + decorated_type = attribute_type_decorations.apply(name, type) + define_attribute(name, decorated_type) end end end diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index 8f165fb1dc..9d58a19304 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -83,7 +83,7 @@ module ActiveRecord generated_attribute_methods.synchronize do return false if @attribute_methods_generated superclass.define_attribute_methods unless self == base_class - super(column_names) + super(attribute_names) @attribute_methods_generated = true end true @@ -185,7 +185,7 @@ module ActiveRecord # # => ["id", "created_at", "updated_at", "name", "age"] def attribute_names @attribute_names ||= if !abstract_class? && table_exists? - column_names + attribute_types.keys else [] end @@ -369,6 +369,39 @@ module ActiveRecord write_attribute(attr_name, value) end + # Returns the name of all database fields which have been read from this + # model. This can be useful in development mode to determine which fields + # need to be selected. For performance critical pages, selecting only the + # required fields can be an easy performance win (assuming you aren't using + # all of the fields on the model). + # + # For example: + # + # class PostsController < ActionController::Base + # after_action :print_accessed_fields, only: :index + # + # def index + # @posts = Post.all + # end + # + # private + # + # def print_accessed_fields + # p @posts.first.accessed_fields + # end + # end + # + # Which allows you to quickly change your code to: + # + # class PostsController < ActionController::Base + # def index + # @posts = Post.select(:id, :title, :author_id, :updated_at) + # end + # end + def accessed_fields + @attributes.accessed + end + protected def clone_attribute_value(reader_method, attribute_name) # :nodoc: diff --git a/activerecord/lib/active_record/attribute_methods/before_type_cast.rb b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb index fd61febd57..56c1898551 100644 --- a/activerecord/lib/active_record/attribute_methods/before_type_cast.rb +++ b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb @@ -28,6 +28,7 @@ module ActiveRecord included do attribute_method_suffix "_before_type_cast" + attribute_method_suffix "_came_from_user?" end # Returns the value of the attribute identified by +attr_name+ before @@ -66,6 +67,10 @@ module ActiveRecord def attribute_before_type_cast(attribute_name) read_attribute_before_type_cast(attribute_name) end + + def attribute_came_from_user?(attribute_name) + @attributes[attribute_name].came_from_user? + end end end end diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index d5702accaf..7ba907f786 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -108,7 +108,7 @@ module ActiveRecord end def save_changed_attribute(attr, old_value) - if attribute_changed?(attr) + if attribute_changed_by_setter?(attr) clear_attribute_changes(attr) unless _field_changed?(attr, old_value) else set_attribute_was(attr, old_value) if _field_changed?(attr, old_value) @@ -131,10 +131,8 @@ module ActiveRecord partial_writes? ? super(keys_for_partial_write) : super end - # Serialized attributes should always be written in case they've been - # changed in place. def keys_for_partial_write - changed + changed & self.class.column_names end def _field_changed?(attr, old_value) @@ -165,7 +163,7 @@ module ActiveRecord end def store_original_raw_attribute(attr_name) - original_raw_attributes[attr_name] = @attributes[attr_name].value_for_database + original_raw_attributes[attr_name] = @attributes[attr_name].value_for_database rescue nil end def store_original_raw_attributes diff --git a/activerecord/lib/active_record/attribute_methods/query.rb b/activerecord/lib/active_record/attribute_methods/query.rb index dc689f399a..553122a5fc 100644 --- a/activerecord/lib/active_record/attribute_methods/query.rb +++ b/activerecord/lib/active_record/attribute_methods/query.rb @@ -22,7 +22,7 @@ module ActiveRecord return false if ActiveRecord::ConnectionAdapters::Column::FALSE_VALUES.include?(value) !value.blank? end - elsif column.number? + elsif value.respond_to?(:zero?) !value.zero? else !value.blank? diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index 24e30b6608..0d989c2eca 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -69,9 +69,18 @@ module ActiveRecord # This method exists to avoid the expensive primary_key check internally, without # breaking compatibility with the read_attribute API - def _read_attribute(attr_name) # :nodoc: - @attributes.fetch_value(attr_name.to_s) { |n| yield n if block_given? } + 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 _read_attribute(attr_name, &block) # :nodoc + @attributes.fetch_value(attr_name.to_s, &block) + end + else + def _read_attribute(attr_name) # :nodoc: + @attributes.fetch_value(attr_name.to_s) { |n| yield n if block_given? } + end end + alias :attribute :_read_attribute private :attribute diff --git a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb index 777f7ab4d7..f9beb43e4b 100644 --- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb +++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb @@ -2,24 +2,26 @@ module ActiveRecord module AttributeMethods module TimeZoneConversion class TimeZoneConverter < DelegateClass(Type::Value) # :nodoc: - include Type::Decorator - - def type_cast_from_database(value) + def deserialize(value) convert_time_to_time_zone(super) end - def type_cast_from_user(value) + def cast(value) if value.is_a?(Array) - value.map { |v| type_cast_from_user(v) } + value.map { |v| cast(v) } + elsif value.is_a?(Hash) + set_time_zone_without_conversion(super) elsif value.respond_to?(:in_time_zone) begin - value.in_time_zone || super + user_input_in_time_zone(value) || super rescue ArgumentError nil end end end + private + def convert_time_to_time_zone(value) if value.is_a?(Array) value.map { |v| convert_time_to_time_zone(v) } @@ -29,6 +31,10 @@ module ActiveRecord value end end + + def set_time_zone_without_conversion(value) + ::Time.zone.local_to_utc(value).in_time_zone + end end extend ActiveSupport::Concern @@ -39,6 +45,9 @@ module ActiveRecord class_attribute :skip_time_zone_conversion_for_attributes, instance_writer: false self.skip_time_zone_conversion_for_attributes = [] + + class_attribute :time_zone_aware_types, instance_writer: false + self.time_zone_aware_types = [:datetime, :not_explicitly_configured] end module ClassMethods @@ -59,9 +68,31 @@ module ActiveRecord end def create_time_zone_conversion_attribute?(name, cast_type) - time_zone_aware_attributes && - !self.skip_time_zone_conversion_for_attributes.include?(name.to_sym) && - (:datetime == cast_type.type) + enabled_for_column = time_zone_aware_attributes && + !self.skip_time_zone_conversion_for_attributes.include?(name.to_sym) + result = enabled_for_column && + time_zone_aware_types.include?(cast_type.type) + + if enabled_for_column && + !result && + cast_type.type == :time && + time_zone_aware_types.include?(:not_explicitly_configured) + ActiveSupport::Deprecation.warn(<<-MESSAGE) + Time columns will become time zone aware in Rails 5.1. This + still causes `String`s to be parsed as if they were in `Time.zone`, + and `Time`s to be converted to `Time.zone`. + + To keep the old behavior, you must add the following to your initializer: + + config.active_record.time_zone_aware_types = [:datetime] + + To silence this deprecation warning, add the following: + + config.active_record.time_zone_aware_types << :time + MESSAGE + end + + result end end end diff --git a/activerecord/lib/active_record/attribute_set.rb b/activerecord/lib/active_record/attribute_set.rb index 66fcaf6945..013a7d0e01 100644 --- a/activerecord/lib/active_record/attribute_set.rb +++ b/activerecord/lib/active_record/attribute_set.rb @@ -10,6 +10,10 @@ module ActiveRecord 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 @@ -24,11 +28,19 @@ module ActiveRecord end def keys - attributes.initialized_keys + attributes.each_key.select { |name| self[name].initialized? } end - def fetch_value(name) - self[name].value { |n| yield n if block_given? } + 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) @@ -49,7 +61,7 @@ module ActiveRecord end def initialize_dup(_) - @attributes = attributes.dup + @attributes = attributes.deep_dup super end @@ -64,6 +76,10 @@ module ActiveRecord end end + def accessed + attributes.select { |_, attr| attr.has_been_read? }.keys + end + protected attr_reader :attributes diff --git a/activerecord/lib/active_record/attribute_set/builder.rb b/activerecord/lib/active_record/attribute_set/builder.rb index 3a76f5262d..e85777c335 100644 --- a/activerecord/lib/active_record/attribute_set/builder.rb +++ b/activerecord/lib/active_record/attribute_set/builder.rb @@ -20,7 +20,7 @@ module ActiveRecord end class LazyAttributeHash # :nodoc: - delegate :select, :transform_values, to: :materialize + delegate :transform_values, :each_key, to: :materialize def initialize(types, values, additional_types) @types = types @@ -45,15 +45,21 @@ module ActiveRecord delegate_hash[key] = value end - def initialized_keys - delegate_hash.keys | values.keys - end - def initialize_dup(_) @delegate_hash = delegate_hash.transform_values(&:dup) 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 + protected attr_reader :types, :values, :additional_types, :delegate_hash diff --git a/activerecord/lib/active_record/attributes.rb b/activerecord/lib/active_record/attributes.rb index aafb990bc1..c8979a60d7 100644 --- a/activerecord/lib/active_record/attributes.rb +++ b/activerecord/lib/active_record/attributes.rb @@ -1,31 +1,42 @@ module ActiveRecord - module Attributes # :nodoc: + # See ActiveRecord::Attributes::ClassMethods for documentation + module Attributes extend ActiveSupport::Concern + # :nodoc: Type = ActiveRecord::Type included do - class_attribute :user_provided_columns, instance_accessor: false # :internal: - class_attribute :user_provided_defaults, instance_accessor: false # :internal: - self.user_provided_columns = {} - self.user_provided_defaults = {} + class_attribute :attributes_to_define_after_schema_loads, instance_accessor: false # :internal: + self.attributes_to_define_after_schema_loads = {} end - module ClassMethods # :nodoc: - # Defines or overrides a attribute on this model. This allows customization of - # Active Record's type casting behavior, as well as adding support for user defined - # types. + module ClassMethods + # Defines an attribute with a type on this model. It will override the + # type of existing attributes if needed. This allows control over how + # values are converted to and from SQL when assigned to a model. It also + # changes the behavior of values passed to + # ActiveRecord::QueryMethods#where. This will let you use + # your domain objects across much of Active Record, without having to + # rely on implementation details or monkey patching. + # + # +name+ The name of the methods to define attribute methods for, and the + # column which this will persist to. + # + # +cast_type+ A symbol such as +:string+ or +:integer+, or a type object + # to be used for this attribute. See the examples below for more + # information about providing custom type objects. # - # +name+ The name of the methods to define attribute methods for, and the column which - # this will persist to. + # ==== Options + # The following options are accepted: # - # +cast_type+ A type object that contains information about how to type cast the value. - # See the examples section for more information. + # +default+ The default value to use when no value is provided. If this option + # is not passed, the previous default value (if any) will be used. + # Otherwise, the default will be +nil+. # - # ==== Options - # The options hash accepts the following options: + # +array+ (PG only) specifies that the type should be an array (see the examples below). # - # +default+ is the default value that the column should use on a new record. + # +range+ (PG only) specifies that the type should be a range (see the examples below). # # ==== Examples # @@ -46,94 +57,188 @@ module ActiveRecord # store_listing.price_in_cents # => BigDecimal.new(10.1) # # class StoreListing < ActiveRecord::Base - # attribute :price_in_cents, Type::Integer.new + # attribute :price_in_cents, :integer # end # # # after # store_listing.price_in_cents # => 10 # - # Users may also define their own custom types, as long as they respond to the methods - # defined on the value type. The +type_cast+ method on your type object will be called - # with values both from the database, and from your controllers. See - # +ActiveRecord::Attributes::Type::Value+ for the expected API. It is recommended that your - # type objects inherit from an existing type, or the base value type. + # A default can also be provided. + # + # create_table :store_listings, force: true do |t| + # t.string :my_string, default: "original default" + # end + # + # StoreListing.new.my_string # => "original default" + # + # class StoreListing < ActiveRecord::Base + # attribute :my_string, :string, default: "new default" + # end + # + # StoreListing.new.my_string # => "new default" + # + # Attributes do not need to be backed by a database column. + # + # class MyModel < ActiveRecord::Base + # attribute :my_string, :string + # attribute :my_int_array, :integer, array: true + # attribute :my_float_range, :float, range: true + # end + # + # model = MyModel.new( + # my_string: "string", + # my_int_array: ["1", "2", "3"], + # my_float_range: "[1,3.5]", + # ) + # model.attributes + # # => + # { + # my_string: "string", + # my_int_array: [1, 2, 3], + # my_float_range: 1.0..3.5 + # } + # + # ==== Creating Custom Types + # + # Users may also define their own custom types, as long as they respond + # to the methods defined on the value type. The method + # +deserialize+ or +cast+ will be called on + # your type object, with raw input from the database or from your + # controllers. See ActiveRecord::Type::Value for the expected API. It is + # recommended that your type objects inherit from an existing type, or + # from ActiveRecord::Type::Value # # class MoneyType < ActiveRecord::Type::Integer - # def type_cast(value) + # def cast(value) # if value.include?('$') # price_in_dollars = value.gsub(/\$/, '').to_f - # price_in_dollars * 100 + # super(price_in_dollars * 100) # else - # value.to_i + # super # end # end # end # + # # config/initializers/types.rb + # ActiveRecord::Type.register(:money, MoneyType) + # + # # /app/models/store_listing.rb # class StoreListing < ActiveRecord::Base - # attribute :price_in_cents, MoneyType.new + # attribute :price_in_cents, :money # end # # store_listing = StoreListing.new(price_in_cents: '$10.00') # store_listing.price_in_cents # => 1000 - def attribute(name, cast_type, options = {}) + # + # For more details on creating custom types, see the documentation for + # ActiveRecord::Type::Value. For more details on registering your types + # to be referenced by a symbol, see ActiveRecord::Type.register. You can + # also pass a type object directly, in place of a symbol. + # + # ==== Querying + # + # When ActiveRecord::QueryMethods#where is called, it will + # use the type defined by the model class to convert the value to SQL, + # calling +serialize+ on your type object. For example: + # + # class Money < Struct.new(:amount, :currency) + # end + # + # class MoneyType < Type::Value + # def initialize(currency_converter) + # @currency_converter = currency_converter + # end + # + # # value will be the result of +deserialize+ or + # # +cast+. Assumed to be in instance of +Money+ in + # # this case. + # def serialize(value) + # value_in_bitcoins = @currency_converter.convert_to_bitcoins(value) + # value_in_bitcoins.amount + # end + # end + # + # ActiveRecord::Type.register(:money, MoneyType) + # + # class Product < ActiveRecord::Base + # currency_converter = ConversionRatesFromTheInternet.new + # attribute :price_in_bitcoins, :money, currency_converter + # end + # + # Product.where(price_in_bitcoins: Money.new(5, "USD")) + # # => SELECT * FROM products WHERE price_in_bitcoins = 0.02230 + # + # Product.where(price_in_bitcoins: Money.new(5, "GBP")) + # # => SELECT * FROM products WHERE price_in_bitcoins = 0.03412 + # + # ==== Dirty Tracking + # + # The type of an attribute is given the opportunity to change how dirty + # tracking is performed. The methods +changed?+ and +changed_in_place?+ + # will be called from ActiveModel::Dirty. See the documentation for those + # methods in ActiveRecord::Type::Value for more details. + def attribute(name, cast_type, **options) name = name.to_s - clear_caches_calculated_from_columns - # Assign a new hash to ensure that subclasses do not share a hash - self.user_provided_columns = user_provided_columns.merge(name => cast_type) - - if options.key?(:default) - self.user_provided_defaults = user_provided_defaults.merge(name => options[:default]) - end - end + reload_schema_from_cache - # Returns an array of column objects for the table associated with this class. - def columns - @columns ||= add_user_provided_columns(connection.schema_cache.columns(table_name)) + self.attributes_to_define_after_schema_loads = + attributes_to_define_after_schema_loads.merge( + name => [cast_type, options] + ) end - # Returns a hash of column objects for the table associated with this class. - def columns_hash - @columns_hash ||= Hash[columns.map { |c| [c.name, c] }] + # This is the low level API which sits beneath +attribute+. It only + # accepts type objects, and will do its work immediately instead of + # waiting for the schema to load. Automatic schema detection and + # ClassMethods#attribute both call this under the hood. While this method + # is provided so it can be used by plugin authors, application code + # should probably use ClassMethods#attribute. + # + # +name+ The name of the attribute being defined. Expected to be a +String+. + # + # +cast_type+ The type object to use for this attribute. + # + # +default+ The default value to use when no value is provided. If this option + # is not passed, the previous default value (if any) will be used. + # Otherwise, the default will be +nil+. + # + # +user_provided_default+ Whether the default value should be cast using + # +cast+ or +deserialize+. + def define_attribute( + name, + cast_type, + default: NO_DEFAULT_PROVIDED, + user_provided_default: true + ) + attribute_types[name] = cast_type + define_default_attribute(name, default, cast_type, from_user: user_provided_default) end - def reset_column_information # :nodoc: + def load_schema! # :nodoc: super - clear_caches_calculated_from_columns - end - - private - - def add_user_provided_columns(schema_columns) - existing_columns = schema_columns.map do |column| - new_type = user_provided_columns[column.name] - if new_type - column.with_type(new_type) - else - column + attributes_to_define_after_schema_loads.each do |name, (type, options)| + if type.is_a?(Symbol) + type = ActiveRecord::Type.lookup(type, **options.except(:default)) end - end - existing_column_names = existing_columns.map(&:name) - new_columns = user_provided_columns.except(*existing_column_names).map do |(name, type)| - connection.new_column(name, nil, type) + define_attribute(name, type, **options.slice(:default)) end - - existing_columns + new_columns end - def clear_caches_calculated_from_columns - @arel_table = nil - @attributes_builder = nil - @column_names = nil - @column_types = nil - @columns = nil - @columns_hash = nil - @content_columns = nil - @default_attributes = nil - end + private - def raw_default_values - super.merge(user_provided_defaults) + NO_DEFAULT_PROVIDED = Object.new # :nodoc: + private_constant :NO_DEFAULT_PROVIDED + + def define_default_attribute(name, value, type, from_user:) + if value == NO_DEFAULT_PROVIDED + default_attribute = _default_attributes[name].with_type(type) + elsif from_user + default_attribute = Attribute.from_user(name, value, type) + else + default_attribute = Attribute.from_database(name, value, type) + end + _default_attributes[name] = default_attribute end end end diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index fa6c5e9e8c..0792d19c3e 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -177,10 +177,8 @@ module ActiveRecord # before actually defining them. def add_autosave_association_callbacks(reflection) save_method = :"autosave_associated_records_for_#{reflection.name}" - validation_method = :"validate_associated_records_for_#{reflection.name}" - collection = reflection.collection? - if collection + if reflection.collection? before_save :before_save_collection_association define_non_cyclic_method(save_method) { save_collection_association(reflection) } @@ -204,8 +202,18 @@ module ActiveRecord before_save save_method end + define_autosave_validation_callbacks(reflection) + end + + def define_autosave_validation_callbacks(reflection) + validation_method = :"validate_associated_records_for_#{reflection.name}" if reflection.validate? && !method_defined?(validation_method) - method = (collection ? :validate_collection_association : :validate_single_association) + if reflection.collection? + method = :validate_collection_association + else + method = :validate_single_association + end + define_non_cyclic_method(validation_method) do send(method, reflection) # TODO: remove the following line as soon as the return value of @@ -278,11 +286,18 @@ module ActiveRecord # go through nested autosave associations that are loaded in memory (without loading # any new ones), and return true if is changed for autosave def nested_records_changed_for_autosave? - self.class._reflections.values.any? do |reflection| - if reflection.options[:autosave] - association = association_instance_get(reflection.name) - association && Array.wrap(association.target).any?(&:changed_for_autosave?) + @_nested_records_changed_for_autosave_already_called ||= false + return false if @_nested_records_changed_for_autosave_already_called + begin + @_nested_records_changed_for_autosave_already_called = true + self.class._reflections.values.any? do |reflection| + if reflection.options[:autosave] + association = association_instance_get(reflection.name) + association && Array.wrap(association.target).any?(&:changed_for_autosave?) + end end + ensure + @_nested_records_changed_for_autosave_already_called = false end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb index 1371317e3c..d99dc9a5db 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -453,6 +453,10 @@ module ActiveRecord c.verify! end c + rescue + remove c + c.disconnect! + raise end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb index 59cdd8e98c..42ad285340 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -201,16 +201,14 @@ module ActiveRecord # isolation level. However, support is disabled for MySQL versions below 5, # because they are affected by a bug[http://bugs.mysql.com/bug.php?id=39170] # which means the isolation level gets persisted outside the transaction. - def transaction(options = {}) - options.assert_valid_keys :requires_new, :joinable, :isolation - - if !options[:requires_new] && current_transaction.joinable? - if options[:isolation] + def transaction(requires_new: nil, isolation: nil, joinable: true) + if !requires_new && current_transaction.joinable? + if isolation raise ActiveRecord::TransactionIsolationError, "cannot set isolation when joining a transaction" end yield else - transaction_manager.within_new_transaction(options) { yield } + transaction_manager.within_new_transaction(isolation: isolation, joinable: joinable) { yield } end rescue ActiveRecord::Rollback # rollbacks are silently swallowed @@ -234,6 +232,10 @@ module ActiveRecord current_transaction.add_record(record) end + def transaction_state + current_transaction.state + end + # Begins the transaction (and turns off auto-committing). def begin_db_transaction() end @@ -285,10 +287,17 @@ module ActiveRecord def insert_fixture(fixture, table_name) columns = schema_cache.columns_hash(table_name) - key_list = [] - value_list = fixture.map do |name, value| - key_list << quote_column_name(name) - quote(value, columns[name]) + binds = fixture.map do |name, value| + type = lookup_cast_type_from_column(columns[name]) + Relation::QueryAttribute.new(name, value, type) + end + key_list = fixture.keys.map { |name| quote_column_name(name) } + value_list = prepare_binds_for_database(binds).map do |value| + begin + quote(value) + rescue TypeError + quote(YAML.dump(value)) + end end execute "INSERT INTO #{quote_table_name(table_name)} (#{key_list.join(', ')}) VALUES (#{value_list.join(', ')})", 'Fixture Insert' @@ -375,7 +384,7 @@ module ActiveRecord def binds_from_relation(relation, binds) if relation.is_a?(Relation) && binds.empty? - relation, binds = relation.arel, relation.bind_values + relation, binds = relation.arel, relation.bound_attributes end [relation, binds] end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index 143d7d9574..d2840b9498 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -10,7 +10,13 @@ module ActiveRecord return value.quoted_id if value.respond_to?(:quoted_id) if column - value = column.cast_type.type_cast_for_database(value) + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Passing a column to `quote` has been deprecated. It is only used + for type casting, which should be handled elsewhere. See + https://github.com/rails/arel/commit/6160bfbda1d1781c3b08a33ec4955f170e95be11 + for more information. + MSG + value = type_cast_from_column(column, value) end _quote(value) @@ -25,7 +31,7 @@ module ActiveRecord end if column - value = column.cast_type.type_cast_for_database(value) + value = type_cast_from_column(column, value) end _type_cast(value) @@ -34,6 +40,40 @@ module ActiveRecord raise TypeError, "can't cast #{value.class}#{to_type}" end + # If you are having to call this function, you are likely doing something + # wrong. The column does not have sufficient type information if the user + # provided a custom type on the class level either explicitly (via + # `attribute`) or implicitly (via `serialize`, + # `time_zone_aware_attributes`). In almost all cases, the sql type should + # only be used to change quoting behavior, when the primitive to + # represent the type doesn't sufficiently reflect the differences + # (varchar vs binary) for example. The type used to get this primitive + # should have been provided before reaching the connection adapter. + def type_cast_from_column(column, value) # :nodoc: + if column + type = lookup_cast_type_from_column(column) + type.serialize(value) + else + value + end + end + + # See docs for +type_cast_from_column+ + def lookup_cast_type_from_column(column) # :nodoc: + lookup_cast_type(column.sql_type) + end + + def fetch_type_metadata(sql_type) + cast_type = lookup_cast_type(sql_type) + SqlTypeMetadata.new( + sql_type: sql_type, + type: cast_type.type, + limit: cast_type.limit, + precision: cast_type.precision, + scale: cast_type.scale, + ) + end + # Quotes a string, escaping any ' (single quote) and \ (backslash) # characters. def quote_string(s) @@ -62,6 +102,11 @@ module ActiveRecord quote_table_name("#{table}.#{attr}") end + def quote_default_expression(value, column) #:nodoc: + value = lookup_cast_type(column.sql_type).serialize(value) + quote(value) + end + def quoted_true "'t'" end @@ -87,7 +132,16 @@ module ActiveRecord end end - value.to_s(:db) + result = value.to_s(:db) + if value.respond_to?(:usec) && value.usec > 0 + "#{result}.#{sprintf("%06d", value.usec)}" + else + result + end + end + + def prepare_binds_for_database(binds) # :nodoc: + binds.map(&:value_for_database) end private @@ -109,8 +163,7 @@ module ActiveRecord when Date, Time then "'#{quoted_date(value)}'" when Symbol then "'#{quote_string(value.to_s)}'" when Class then "'#{value}'" - else - "'#{quote_string(YAML.dump(value))}'" + else raise TypeError, "can't quote #{value.class.name}" end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb index db20b60d60..bc8fa9b6cf 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb @@ -28,7 +28,7 @@ module ActiveRecord end def visit_ColumnDefinition(o) - o.sql_type = type_to_sql(o.type, o.limit, o.precision, o.scale) + o.sql_type ||= type_to_sql(o.type, o.limit, o.precision, o.scale) column_sql = "#{quote_column_name(o.name)} #{o.sql_type}" add_column_options!(column_sql, column_options(o)) unless o.type == :primary_key column_sql @@ -98,8 +98,7 @@ module ActiveRecord end def quote_default_expression(value, column) - value = type_for_column(column).type_cast_for_database(value) - @conn.quote(value) + @conn.quote_default_expression(value, column) end def options_include_default?(options) @@ -118,10 +117,6 @@ module ActiveRecord MSG end end - - def type_for_column(column) - @conn.lookup_cast_type(column.sql_type) - end end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb index 7eaa89c9a7..a2777fcd0a 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -15,7 +15,7 @@ module ActiveRecord # are typically created by methods in TableDefinition, and added to the # +columns+ attribute of said TableDefinition object, in order to be used # for generating a number of table creation or table changing SQL statements. - class ColumnDefinition < Struct.new(:name, :type, :limit, :precision, :scale, :default, :null, :first, :after, :auto_increment, :primary_key, :sql_type, :cast_type) #:nodoc: + class ColumnDefinition < Struct.new(:name, :type, :limit, :precision, :scale, :default, :null, :first, :after, :auto_increment, :primary_key, :sql_type) #:nodoc: def primary_key? primary_key || type.to_sym == :primary_key @@ -50,6 +50,14 @@ module ActiveRecord options[:primary_key] != default_primary_key end + def defined_for?(options_or_to_table = {}) + if options_or_to_table.is_a?(Hash) + options_or_to_table.all? {|key, value| options[key].to_s == value.to_s } + else + to_table == options_or_to_table.to_s + end + end + private def default_primary_key "id" @@ -651,6 +659,10 @@ module ActiveRecord @base.add_foreign_key(name, *args) end + def foreign_key_exists?(*args) # :nodoc: + @base.foreign_key_exists?(name, *args) + end + private def native @base.native_database_types diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb index 42ea599a74..af7ef7cbaa 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb @@ -46,9 +46,10 @@ module ActiveRecord private def schema_default(column) - default = column.type_cast_from_database(column.default) + type = lookup_cast_type_from_column(column) + default = type.deserialize(column.default) unless default.nil? - column.type_cast_for_schema(default) + type.type_cast_for_schema(default) end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb index 0f44c332ae..503b09d1fc 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -1,4 +1,6 @@ require 'active_record/migration/join_table' +require 'active_support/core_ext/string/access' +require 'digest' module ActiveRecord module ConnectionAdapters # :nodoc: @@ -380,7 +382,7 @@ module ActiveRecord # it can be helpful to provide these in a migration's +change+ method so it can be reverted. # In that case, +options+ and the block will be used by create_table. def drop_table(table_name, options = {}) - execute "DROP TABLE #{quote_table_name(table_name)}" + execute "DROP TABLE#{' IF EXISTS' if options[:if_exists]} #{quote_table_name(table_name)}" end # Adds a new column to the named table. @@ -660,7 +662,13 @@ module ActiveRecord # # remove_reference(:products, :supplier, polymorphic: true) # + # ====== Remove the reference with a foreign key + # + # remove_reference(:products, :user, index: true, foreign_key: true) + # def remove_reference(table_name, ref_name, options = {}) + remove_foreign_key table_name, ref_name.to_s.pluralize if options[:foreign_key] + remove_column(table_name, "#{ref_name}_id") remove_column(table_name, "#{ref_name}_type") if options[:polymorphic] end @@ -749,21 +757,7 @@ module ActiveRecord def remove_foreign_key(from_table, options_or_to_table = {}) return unless supports_foreign_keys? - if options_or_to_table.is_a?(Hash) - options = options_or_to_table - else - options = { column: foreign_key_column_for(options_or_to_table) } - end - - fk_name_to_delete = options.fetch(:name) do - fk_to_delete = foreign_keys(from_table).detect {|fk| fk.column == options[:column].to_s } - - if fk_to_delete - fk_to_delete.name - else - raise ArgumentError, "Table '#{from_table}' has no foreign key on column '#{options[:column]}'" - end - end + fk_name_to_delete = foreign_key_for!(from_table, options_or_to_table).name at = create_alter_table from_table at.drop_foreign_key fk_name_to_delete @@ -771,6 +765,31 @@ module ActiveRecord execute schema_creation.accept(at) end + # Checks to see if a foreign key exists on a table for a given foreign key definition. + # + # # Check a foreign key exists + # foreign_key_exists?(:accounts, :branches) + # + # # Check a foreign key on specified column exists + # foreign_key_exists?(:accounts, column: :owner_id) + # + # # Check a foreign key with a custom name exists + # foreign_key_exists?(:accounts, name: "special_fk_name") + # + def foreign_key_exists?(from_table, options_or_to_table = {}) + foreign_key_for(from_table, options_or_to_table).present? + end + + def foreign_key_for(from_table, options_or_to_table = {}) # :nodoc: + return unless supports_foreign_keys? + foreign_keys(from_table).detect {|fk| fk.defined_for? options_or_to_table } + end + + def foreign_key_for!(from_table, options_or_to_table = {}) # :nodoc: + foreign_key_for(from_table, options_or_to_table) or \ + raise ArgumentError, "Table '#{from_table}' has no foreign key for #{options_or_to_table}" + end + def foreign_key_column_for(table_name) # :nodoc: "#{table_name.to_s.singularize}_id" end @@ -990,8 +1009,10 @@ module ActiveRecord end def foreign_key_name(table_name, options) # :nodoc: + identifier = "#{table_name}_#{options.fetch(:column)}_fk" + hashed_identifier = Digest::SHA256.hexdigest(identifier).first(10) options.fetch(:name) do - "fk_rails_#{SecureRandom.hex(5)}" + "fk_rails_#{hashed_identifier}" end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb index f6ef3b0675..11440e30d4 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb @@ -55,11 +55,7 @@ module ActiveRecord end def add_record(record) - if record.has_transactional_callbacks? - records << record - else - record.set_transaction_state(@state) - end + records << record end def rollback @@ -69,11 +65,11 @@ module ActiveRecord def rollback_records ite = records.uniq while record = ite.shift - record.rolledback! full_rollback? + record.rolledback!(force_restore_state: full_rollback?) end ensure ite.each do |i| - i.rolledback!(full_rollback?, false) + i.rolledback!(force_restore_state: full_rollback?, should_run_callbacks: false) end end @@ -88,7 +84,7 @@ module ActiveRecord end ensure ite.each do |i| - i.committed!(false) + i.committed!(should_run_callbacks: false) end end @@ -111,14 +107,11 @@ module ActiveRecord def rollback connection.rollback_to_savepoint(savepoint_name) super - rollback_records end def commit connection.release_savepoint(savepoint_name) super - parent = connection.transaction_manager.current_transaction - records.each { |r| parent.add_record(r) } end def full_rollback?; false; end @@ -138,13 +131,11 @@ module ActiveRecord def rollback connection.rollback_db_transaction super - rollback_records end def commit connection.commit_db_transaction super - commit_records end end @@ -161,16 +152,28 @@ module ActiveRecord else SavepointTransaction.new(@connection, "active_record_#{@stack.size}", options) end + @stack.push(transaction) transaction end def commit_transaction - @stack.pop.commit + inner_transaction = @stack.pop + inner_transaction.commit + + if current_transaction.joinable? + inner_transaction.records.each do |r| + r.add_to_transaction + end + else + inner_transaction.commit_records + end end - def rollback_transaction - @stack.pop.rollback + def rollback_transaction(transaction = nil) + transaction ||= @stack.pop + transaction.rollback + transaction.rollback_records end def within_new_transaction(options = {}) @@ -182,12 +185,12 @@ module ActiveRecord ensure unless error if Thread.current.status == 'aborting' - rollback_transaction + rollback_transaction if transaction else begin commit_transaction rescue Exception - transaction.rollback unless transaction.state.completed? + rollback_transaction(transaction) unless transaction.state.completed? raise end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index c3a8bf5c74..ee11c0efa4 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -4,6 +4,7 @@ require 'bigdecimal/util' require 'active_record/type' require 'active_support/core_ext/benchmark' require 'active_record/connection_adapters/schema_cache' +require 'active_record/connection_adapters/sql_type_metadata' require 'active_record/connection_adapters/abstract/schema_dumper' require 'active_record/connection_adapters/abstract/schema_creation' require 'monitor' @@ -113,7 +114,8 @@ module ActiveRecord class BindCollector < Arel::Collectors::Bind def compile(bvs, conn) - super(bvs.map { |bv| conn.quote(*bv.reverse) }) + casted_binds = conn.prepare_binds_for_database(bvs) + super(casted_binds.map { |value| conn.quote(value) }) end end @@ -261,18 +263,6 @@ module ActiveRecord {} end - # QUOTING ================================================== - - # Quote date/time values for use in SQL input. Includes microseconds - # if the value is a Time responding to usec. - def quoted_date(value) #:nodoc: - if value.acts_like?(:time) && value.respond_to?(:usec) - "#{super}.#{sprintf("%06d", value.usec)}" - else - super - end - end - # Returns a bind substitution value given a bind +column+ # NOTE: The column param is currently being used by the sqlserver-adapter def substitute_at(column, _unused = 0) @@ -365,8 +355,17 @@ module ActiveRecord end def case_insensitive_comparison(table, attribute, column, value) - table[attribute].lower.eq(table.lower(value)) + if can_perform_case_insensitive_comparison_for?(column) + table[attribute].lower.eq(table.lower(value)) + else + case_sensitive_comparison(table, attribute, column, value) + end + end + + def can_perform_case_insensitive_comparison_for?(column) + true end + private :can_perform_case_insensitive_comparison_for? def current_savepoint_name current_transaction.savepoint_name @@ -383,8 +382,8 @@ module ActiveRecord end end - def new_column(name, default, cast_type, sql_type = nil, null = true) - Column.new(name, default, cast_type, sql_type, null) + def new_column(name, default, sql_type_metadata = nil, null = true) + Column.new(name, default, sql_type_metadata, null) end def lookup_cast_type(sql_type) # :nodoc: @@ -392,21 +391,21 @@ module ActiveRecord end def column_name_for_operation(operation, node) # :nodoc: - node.to_sql + visitor.accept(node, collector).value end protected def initialize_type_map(m) # :nodoc: - register_class_with_limit m, %r(boolean)i, Type::Boolean - register_class_with_limit m, %r(char)i, Type::String - register_class_with_limit m, %r(binary)i, Type::Binary - register_class_with_limit m, %r(text)i, Type::Text - register_class_with_limit m, %r(date)i, Type::Date - register_class_with_limit m, %r(time)i, Type::Time - register_class_with_limit m, %r(datetime)i, Type::DateTime - register_class_with_limit m, %r(float)i, Type::Float - register_class_with_limit m, %r(int)i, Type::Integer + register_class_with_limit m, %r(boolean)i, Type::Boolean + register_class_with_limit m, %r(char)i, Type::String + register_class_with_limit m, %r(binary)i, Type::Binary + register_class_with_limit m, %r(text)i, Type::Text + register_class_with_precision m, %r(date)i, Type::Date + register_class_with_precision m, %r(time)i, Type::Time + register_class_with_precision m, %r(datetime)i, Type::DateTime + register_class_with_limit m, %r(float)i, Type::Float + register_class_with_limit m, %r(int)i, Type::Integer m.alias_type %r(blob)i, 'binary' m.alias_type %r(clob)i, 'text' @@ -440,6 +439,13 @@ module ActiveRecord end end + def register_class_with_precision(mapping, key, klass) # :nodoc: + mapping.register_type(key) do |*args| + precision = extract_precision(args.last) + klass.new(precision: precision) + end + end + def extract_scale(sql_type) # :nodoc: case sql_type when /\((\d+)\)/ then 0 @@ -452,7 +458,12 @@ module ActiveRecord end def extract_limit(sql_type) # :nodoc: - $1.to_i if sql_type =~ /\((.*)\)/ + case sql_type + when /^bigint/i + 8 + when /\((.*)\)/ + $1.to_i + end end def translate_exception_class(e, sql) diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb index e9a3c26c32..0a1c66be1e 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -63,7 +63,7 @@ module ActiveRecord def column_spec_for_primary_key(column) spec = {} - if column.extra == 'auto_increment' + if column.auto_increment? return unless column.limit == 8 spec[:id] = ':bigint' else @@ -73,14 +73,17 @@ module ActiveRecord spec end + def prepare_column_options(column) + spec = super + spec.delete(:precision) if column.type == :datetime && column.precision == 0 + spec + end + class Column < ConnectionAdapters::Column # :nodoc: - attr_reader :collation, :strict, :extra + delegate :strict, :collation, :extra, to: :sql_type_metadata, allow_nil: true - def initialize(name, default, cast_type, sql_type = nil, null = true, collation = nil, strict = false, extra = "") - @strict = strict - @collation = collation - @extra = extra - super(name, default, cast_type, sql_type, null) + def initialize(*) + super assert_valid_default(default) extract_default end @@ -88,7 +91,7 @@ module ActiveRecord def extract_default if blob_or_text_column? @default = null || strict ? nil : '' - elsif missing_default_forged_as_empty_string?(@default) + elsif missing_default_forged_as_empty_string?(default) @default = nil end end @@ -106,11 +109,8 @@ module ActiveRecord collation && !collation.match(/_ci$/) end - def ==(other) - super && - collation == other.collation && - strict == other.strict && - extra == other.extra + def auto_increment? + extra == 'auto_increment' end private @@ -131,9 +131,33 @@ module ActiveRecord raise ArgumentError, "#{type} columns cannot have a default value: #{default.inspect}" end end + end + + class MysqlTypeMetadata < DelegateClass(SqlTypeMetadata) # :nodoc: + attr_reader :collation, :extra, :strict + + def initialize(type_metadata, collation: "", extra: "", strict: false) + super(type_metadata) + @type_metadata = type_metadata + @collation = collation + @extra = extra + @strict = strict + end + + def ==(other) + other.is_a?(MysqlTypeMetadata) && + attributes_for_hash == other.attributes_for_hash + end + alias eql? == + + def hash + attributes_for_hash.hash + end + + protected def attributes_for_hash - super + [collation, strict, extra] + [self.class, @type_metadata, collation, extra, strict] end end @@ -243,8 +267,8 @@ module ActiveRecord raise NotImplementedError end - def new_column(field, default, cast_type, sql_type = nil, null = true, collation = "", extra = "") # :nodoc: - Column.new(field, default, cast_type, sql_type, null, collation, strict_mode?, extra) + def new_column(field, default, sql_type_metadata = nil, null = true) # :nodoc: + Column.new(field, default, sql_type_metadata, null) end # Must return the MySQL error number from the exception, if the exception has an @@ -467,8 +491,8 @@ module ActiveRecord each_hash(result).map do |field| field_name = set_field_encoding(field[:Field]) sql_type = field[:Type] - cast_type = lookup_cast_type(sql_type) - new_column(field_name, field[:Default], cast_type, sql_type, field[:Null] == "YES", field[:Collation], field[:Extra]) + type_metadata = fetch_type_metadata(sql_type, field[:Collation], field[:Extra]) + new_column(field_name, field[:Default], type_metadata, field[:Null] == "YES") end end end @@ -502,7 +526,7 @@ module ActiveRecord end def drop_table(table_name, options = {}) - execute "DROP#{' TEMPORARY' if options[:temporary]} TABLE #{quote_table_name(table_name)}#{' CASCADE' if options[:force] == :cascade}" + execute "DROP#{' TEMPORARY' if options[:temporary]} TABLE#{' IF EXISTS' if options[:if_exists]} #{quote_table_name(table_name)}#{' CASCADE' if options[:force] == :cascade}" end def rename_index(table_name, old_name, new_name) @@ -694,11 +718,6 @@ module ActiveRecord m.alias_type %r(year)i, 'integer' m.alias_type %r(bit)i, 'binary' - m.register_type(%r(datetime)i) do |sql_type| - precision = extract_precision(sql_type) - MysqlDateTime.new(precision: precision) - end - m.register_type(%r(enum)i) do |sql_type| limit = sql_type[/^enum\((.+)\)/i, 1] .split(',').map{|enum| enum.strip.length - 2}.max @@ -716,6 +735,18 @@ module ActiveRecord end end + def extract_precision(sql_type) + if /datetime/ === sql_type + super || 0 + else + super + end + end + + def fetch_type_metadata(sql_type, collation = "", extra = "") + MysqlTypeMetadata.new(super(sql_type), collation: collation, extra: extra, strict: strict_mode?) + end + # MySQL is too stupid to create a temporary table for use subquery, so we have # to give it some prompting in the form of a subsubquery. Ugh! def subquery_for(key, select) @@ -790,7 +821,7 @@ module ActiveRecord options = { default: column.default, null: column.null, - auto_increment: column.extra == "auto_increment" + auto_increment: column.auto_increment? } current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'", 'SCHEMA')["Type"] @@ -894,24 +925,8 @@ module ActiveRecord TableDefinition.new(native_database_types, name, temporary, options, as) end - class MysqlDateTime < Type::DateTime # :nodoc: - def type_cast_for_database(value) - if value.acts_like?(:time) && value.respond_to?(:usec) - result = super.to_s(:db) - case precision - when 1..6 - "#{result}.#{sprintf("%0#{precision}d", value.usec / 10 ** (6 - precision))}" - else - result - end - else - super - end - end - end - class MysqlString < Type::String # :nodoc: - def type_cast_for_database(value) + def serialize(value) case value when true then "1" when false then "0" @@ -929,6 +944,9 @@ module ActiveRecord end end end + + ActiveRecord::Type.register(:string, MysqlString, adapter: :mysql) + ActiveRecord::Type.register(:string, MysqlString, adapter: :mysql2) end end end diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb index e74de60a83..fa5ed07b8a 100644 --- a/activerecord/lib/active_record/connection_adapters/column.rb +++ b/activerecord/lib/active_record/connection_adapters/column.rb @@ -12,29 +12,21 @@ module ActiveRecord ISO_DATETIME = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(\.\d+)?\z/ end - attr_reader :name, :cast_type, :null, :sql_type, :default, :default_function + attr_reader :name, :null, :sql_type_metadata, :default, :default_function - delegate :type, :precision, :scale, :limit, :klass, :accessor, - :text?, :number?, :binary?, :changed?, - :type_cast_from_user, :type_cast_from_database, :type_cast_for_database, - :type_cast_for_schema, - to: :cast_type + delegate :precision, :scale, :limit, :type, :sql_type, to: :sql_type_metadata, allow_nil: true # Instantiates a new column in the table. # # +name+ is the column's name, such as <tt>supplier_id</tt> in <tt>supplier_id int(11)</tt>. # +default+ is the type-casted default value, such as +new+ in <tt>sales_stage varchar(20) default 'new'</tt>. - # +cast_type+ is the object used for type casting and type information. - # +sql_type+ is used to extract the column's length, if necessary. For example +60+ in - # <tt>company_name varchar(60)</tt>. - # It will be mapped to one of the standard Rails SQL types in the <tt>type</tt> attribute. + # +sql_type_metadata+ is various information about the type of the column # +null+ determines if this column allows +NULL+ values. - def initialize(name, default, cast_type, sql_type = nil, null = true, default_function = nil) - @name = name - @cast_type = cast_type - @sql_type = sql_type - @null = null - @default = default + def initialize(name, default, sql_type_metadata = nil, null = true, default_function = nil) + @name = name + @sql_type_metadata = sql_type_metadata + @null = null + @default = default @default_function = default_function end @@ -50,19 +42,9 @@ module ActiveRecord Base.human_attribute_name(@name) end - def with_type(type) - dup.tap do |clone| - clone.instance_variable_set('@cast_type', type) - end - end - def ==(other) - other.name == name && - other.default == default && - other.cast_type == cast_type && - other.sql_type == sql_type && - other.null == null && - other.default_function == default_function + other.is_a?(Column) && + attributes_for_hash == other.attributes_for_hash end alias :eql? :== @@ -70,16 +52,16 @@ module ActiveRecord attributes_for_hash.hash end - private + protected def attributes_for_hash - [self.class, name, default, cast_type, sql_type, null, default_function] + [self.class, name, default, sql_type_metadata, null, default_function] end end class NullColumn < Column def initialize(name) - super name, nil, Type::Value.new + super(name, nil) end end end diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index 75f244b3f3..fac6f81540 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -39,7 +39,7 @@ module ActiveRecord MAX_INDEX_LENGTH_FOR_UTF8MB4 = 191 def initialize_schema_migrations_table - if @config[:encoding] == 'utf8mb4' + if charset == 'utf8mb4' ActiveRecord::SchemaMigration.create_table(MAX_INDEX_LENGTH_FOR_UTF8MB4) else ActiveRecord::SchemaMigration.create_table diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index 23d8389abb..16f50fc594 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -395,11 +395,9 @@ module ActiveRecord def exec_stmt(sql, name, binds) cache = {} - type_casted_binds = binds.map { |col, val| - [col, type_cast(val, col)] - } + type_casted_binds = binds.map { |attr| type_cast(attr.value_for_database) } - log(sql, name, type_casted_binds) do + log(sql, name, binds) do if binds.empty? stmt = @connection.prepare(sql) else @@ -410,7 +408,7 @@ module ActiveRecord end begin - stmt.execute(*type_casted_binds.map { |_, val| val }) + stmt.execute(*type_casted_binds) rescue Mysql::Error => e # Older versions of MySQL leave the prepared statement in a bad # place when an error occurs. To support older MySQL versions, we diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb index acb1278499..0eb4fb468c 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb @@ -2,19 +2,9 @@ module ActiveRecord module ConnectionAdapters # PostgreSQL-specific extensions to column definitions in a table. class PostgreSQLColumn < Column #:nodoc: - attr_reader :array + delegate :array, :oid, :fmod, to: :sql_type_metadata alias :array? :array - def initialize(name, default, cast_type, sql_type = nil, null = true, default_function = nil) - if sql_type =~ /\[\]$/ - @array = true - sql_type = sql_type[0..sql_type.length - 3] - else - @array = false - end - super - end - def serial? default_function && default_function =~ /\Anextval\(.*\)\z/ end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb index d28a2b4fa0..92349e2f9b 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb @@ -1,25 +1,19 @@ -require 'active_record/connection_adapters/postgresql/oid/infinity' - require 'active_record/connection_adapters/postgresql/oid/array' require 'active_record/connection_adapters/postgresql/oid/bit' require 'active_record/connection_adapters/postgresql/oid/bit_varying' require 'active_record/connection_adapters/postgresql/oid/bytea' require 'active_record/connection_adapters/postgresql/oid/cidr' -require 'active_record/connection_adapters/postgresql/oid/date' require 'active_record/connection_adapters/postgresql/oid/date_time' require 'active_record/connection_adapters/postgresql/oid/decimal' require 'active_record/connection_adapters/postgresql/oid/enum' -require 'active_record/connection_adapters/postgresql/oid/float' require 'active_record/connection_adapters/postgresql/oid/hstore' require 'active_record/connection_adapters/postgresql/oid/inet' -require 'active_record/connection_adapters/postgresql/oid/integer' require 'active_record/connection_adapters/postgresql/oid/json' require 'active_record/connection_adapters/postgresql/oid/jsonb' require 'active_record/connection_adapters/postgresql/oid/money' require 'active_record/connection_adapters/postgresql/oid/point' require 'active_record/connection_adapters/postgresql/oid/range' require 'active_record/connection_adapters/postgresql/oid/specialized_string' -require 'active_record/connection_adapters/postgresql/oid/time' require 'active_record/connection_adapters/postgresql/oid/uuid' require 'active_record/connection_adapters/postgresql/oid/vector' require 'active_record/connection_adapters/postgresql/oid/xml' 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 c203e6c604..fb4e0de2a8 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb @@ -3,7 +3,7 @@ module ActiveRecord module PostgreSQL module OID # :nodoc: class Array < Type::Value # :nodoc: - include Type::Mutable + include Type::Helpers::Mutable # Loads pg_array_parser if available. String parsing can be # performed quicker by a native extension, which will not create @@ -18,29 +18,29 @@ module ActiveRecord end attr_reader :subtype, :delimiter - delegate :type, to: :subtype + delegate :type, :user_input_in_time_zone, to: :subtype def initialize(subtype, delimiter = ',') @subtype = subtype @delimiter = delimiter end - def type_cast_from_database(value) + def deserialize(value) if value.is_a?(::String) - type_cast_array(parse_pg_array(value), :type_cast_from_database) + type_cast_array(parse_pg_array(value), :deserialize) else super end end - def type_cast_from_user(value) + def cast(value) if value.is_a?(::String) value = parse_pg_array(value) end - type_cast_array(value, :type_cast_from_user) + type_cast_array(value, :cast) end - def type_cast_for_database(value) + def serialize(value) if value.is_a?(::Array) cast_value_for_database(value) else @@ -48,6 +48,12 @@ module ActiveRecord end end + def ==(other) + other.is_a?(Array) && + subtype == other.subtype && + delimiter == other.delimiter + end + private def type_cast_array(value, method) @@ -63,7 +69,7 @@ module ActiveRecord casted_values = value.map { |item| cast_value_for_database(item) } "{#{casted_values.join(delimiter)}}" else - quote_and_escape(subtype.type_cast_for_database(value)) + quote_and_escape(subtype.serialize(value)) end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb index 1dbb40ca1d..ea0fa2517f 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb @@ -7,7 +7,7 @@ module ActiveRecord :bit end - def type_cast(value) + def cast(value) if ::String === value case value when /^0x/i @@ -20,7 +20,7 @@ module ActiveRecord end end - def type_cast_for_database(value) + def serialize(value) Data.new(super) if value end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb index 6bd1b8ecae..8f9d6e7f9b 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb @@ -3,7 +3,7 @@ module ActiveRecord module PostgreSQL module OID # :nodoc: class Bytea < Type::Binary # :nodoc: - def type_cast_from_database(value) + def deserialize(value) return if value.nil? return value.to_s if value.is_a?(Type::Binary::Data) PGconn.unescape_bytea(super) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb index 222f10fa8f..eeccb09bdf 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb @@ -18,7 +18,7 @@ module ActiveRecord end end - def type_cast_for_database(value) + def serialize(value) if IPAddr === value "#{value}/#{value.instance_variable_get(:@mask_addr).to_s(2).count('1')}" else diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb deleted file mode 100644 index 1d8d264530..0000000000 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb +++ /dev/null @@ -1,11 +0,0 @@ -module ActiveRecord - module ConnectionAdapters - module PostgreSQL - module OID # :nodoc: - class Date < Type::Date # :nodoc: - include Infinity - end - end - end - end -end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb index b9e7894e5c..2c04c46131 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb @@ -3,8 +3,6 @@ module ActiveRecord module PostgreSQL module OID # :nodoc: class DateTime < Type::DateTime # :nodoc: - include Infinity - def cast_value(value) if value.is_a?(::String) case value diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb index 77d5038efd..b3b610a5f6 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb @@ -7,7 +7,7 @@ module ActiveRecord :enum end - def type_cast(value) + def cast(value) value.to_s end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/float.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/float.rb deleted file mode 100644 index 78ef94b912..0000000000 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/float.rb +++ /dev/null @@ -1,21 +0,0 @@ -module ActiveRecord - module ConnectionAdapters - module PostgreSQL - module OID # :nodoc: - class Float < Type::Float # :nodoc: - include Infinity - - def cast_value(value) - case value - when ::Float then value - when 'Infinity' then ::Float::INFINITY - when '-Infinity' then -::Float::INFINITY - when 'NaN' then ::Float::NAN - else value.to_f - end - end - end - end - end - end -end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb index be4525c94f..9270fc9f21 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb @@ -3,13 +3,13 @@ module ActiveRecord module PostgreSQL module OID # :nodoc: class Hstore < Type::Value # :nodoc: - include Type::Mutable + include Type::Helpers::Mutable def type :hstore end - def type_cast_from_database(value) + def deserialize(value) if value.is_a?(::String) ::Hash[value.scan(HstorePair).map { |k, v| v = v.upcase == 'NULL' ? nil : v.gsub(/\A"(.*)"\Z/m,'\1').gsub(/\\(.)/, '\1') @@ -21,7 +21,7 @@ module ActiveRecord end end - def type_cast_for_database(value) + def serialize(value) if value.is_a?(::Hash) value.map { |k, v| "#{escape_hstore(k)}=>#{escape_hstore(v)}" }.join(', ') else diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/infinity.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/infinity.rb deleted file mode 100644 index e47780399a..0000000000 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/infinity.rb +++ /dev/null @@ -1,13 +0,0 @@ -module ActiveRecord - module ConnectionAdapters - module PostgreSQL - module OID # :nodoc: - module Infinity # :nodoc: - def infinity(options = {}) - options[:negative] ? -::Float::INFINITY : ::Float::INFINITY - end - end - end - end - end -end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/integer.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/integer.rb deleted file mode 100644 index 59abdc0009..0000000000 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/integer.rb +++ /dev/null @@ -1,11 +0,0 @@ -module ActiveRecord - module ConnectionAdapters - module PostgreSQL - module OID # :nodoc: - class Integer < Type::Integer # :nodoc: - include Infinity - end - end - end - end -end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb index e12ddd9901..8e1256baad 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb @@ -3,25 +3,25 @@ module ActiveRecord module PostgreSQL module OID # :nodoc: class Json < Type::Value # :nodoc: - include Type::Mutable + include Type::Helpers::Mutable def type :json end - def type_cast_from_database(value) + def deserialize(value) if value.is_a?(::String) - ::ActiveSupport::JSON.decode(value) + ::ActiveSupport::JSON.decode(value) rescue nil else - super + value end end - def type_cast_for_database(value) + def serialize(value) if value.is_a?(::Array) || value.is_a?(::Hash) ::ActiveSupport::JSON.encode(value) else - super + value end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb index 380c50fc14..afc9383f91 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb @@ -13,7 +13,7 @@ module ActiveRecord # the comparison here. Therefore, we need to parse and re-dump the # raw value here to ensure the insignificant whitespaces are # consistent with our encoder's output. - raw_old_value = type_cast_for_database(type_cast_from_database(raw_old_value)) + raw_old_value = serialize(deserialize(raw_old_value)) super(raw_old_value, new_value) end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb index df890c2ed6..2163674019 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb @@ -3,8 +3,6 @@ module ActiveRecord module PostgreSQL module OID # :nodoc: class Money < Type::Decimal # :nodoc: - include Infinity - class_attribute :precision def type diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb index bac8b01d6b..bf565bcf47 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb @@ -3,19 +3,19 @@ module ActiveRecord module PostgreSQL module OID # :nodoc: class Point < Type::Value # :nodoc: - include Type::Mutable + include Type::Helpers::Mutable def type :point end - def type_cast(value) + def cast(value) case value when ::String if value[0] == '(' && value[-1] == ')' value = value[1...-1] end - type_cast(value.split(',')) + cast(value.split(',')) when ::Array value.map { |v| Float(v) } else @@ -23,7 +23,7 @@ module ActiveRecord end end - def type_cast_for_database(value) + def serialize(value) if value.is_a?(::Array) "(#{number_for_point(value[0])},#{number_for_point(value[1])})" else 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 3adfb8b9d8..fc201f8fb9 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb @@ -7,7 +7,7 @@ module ActiveRecord class Range < Type::Value # :nodoc: attr_reader :subtype, :type - def initialize(subtype, type) + def initialize(subtype, type = :range) @subtype = subtype @type = type end @@ -30,7 +30,7 @@ module ActiveRecord ::Range.new(from, to, extracted[:exclude_end]) end - def type_cast_for_database(value) + def serialize(value) if value.is_a?(::Range) from = type_cast_single_for_database(value.begin) to = type_cast_single_for_database(value.end) @@ -40,26 +40,42 @@ module ActiveRecord end end + def ==(other) + other.is_a?(Range) && + other.subtype == subtype && + other.type == type + end + private def type_cast_single(value) - infinity?(value) ? value : @subtype.type_cast_from_database(value) + infinity?(value) ? value : @subtype.deserialize(value) end def type_cast_single_for_database(value) - infinity?(value) ? '' : @subtype.type_cast_for_database(value) + infinity?(value) ? '' : @subtype.serialize(value) end def extract_bounds(value) from, to = value[1..-2].split(',') { - from: (value[1] == ',' || from == '-infinity') ? @subtype.infinity(negative: true) : from, - to: (value[-2] == ',' || to == 'infinity') ? @subtype.infinity : to, + from: (value[1] == ',' || from == '-infinity') ? infinity(negative: true) : from, + to: (value[-2] == ',' || to == 'infinity') ? infinity : to, exclude_start: (value[0] == '('), exclude_end: (value[-1] == ')') } end + def infinity(negative: false) + if subtype.respond_to?(:infinity) + subtype.infinity(negative: negative) + elsif negative + -::Float::INFINITY + else + ::Float::INFINITY + end + end + def infinity?(value) value.respond_to?(:infinite?) && value.infinite? end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb index b2a42e9ebb..2d2fede4e8 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb @@ -8,10 +8,6 @@ module ActiveRecord def initialize(type) @type = type end - - def text? - false - end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/time.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/time.rb deleted file mode 100644 index 8f0246eddb..0000000000 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/time.rb +++ /dev/null @@ -1,11 +0,0 @@ -module ActiveRecord - module ConnectionAdapters - module PostgreSQL - module OID # :nodoc: - class Time < Type::Time # :nodoc: - include Infinity - end - end - end - end -end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb index 97b4fd3d08..5e839228e9 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb @@ -5,13 +5,13 @@ module ActiveRecord class Uuid < Type::Value # :nodoc: ACCEPTABLE_UUID = %r{\A\{?([a-fA-F0-9]{4}-?){8}\}?\z}x - alias_method :type_cast_for_database, :type_cast_from_database + alias_method :serialize, :deserialize def type :uuid end - def type_cast(value) + def cast(value) value.to_s[ACCEPTABLE_UUID, 0] end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/vector.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/vector.rb index de4187b028..b26e876b54 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/vector.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/vector.rb @@ -16,7 +16,7 @@ module ActiveRecord # FIXME: this should probably split on +delim+ and use +subtype+ # to cast the values. Unfortunately, the current Rails behavior # is to just return the string. - def type_cast(value) + def cast(value) value end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/xml.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/xml.rb index 334af7c598..d40d837cee 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/xml.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/xml.rb @@ -7,7 +7,7 @@ module ActiveRecord :xml end - def type_cast_for_database(value) + def serialize(value) return unless value Data.new(super) end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb index 9de9e2c7dc..b7755c4593 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb @@ -52,15 +52,21 @@ module ActiveRecord end # Does not quote function default values for UUID columns - def quote_default_value(value, column) #:nodoc: + def quote_default_expression(value, column) #:nodoc: if column.type == :uuid && value =~ /\(\)/ value - else - value = column.cast_type.type_cast_for_database(value) + elsif column.respond_to?(:array?) + value = type_cast_from_column(column, value) quote(value) + else + super end end + def lookup_cast_type_from_column(column) # :nodoc: + type_map.lookup(column.oid, column.fmod, column.sql_type) + end + private def _quote(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 a9522e152f..b9078d4c86 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb @@ -76,15 +76,15 @@ module ActiveRecord column(name, :point, options) end - def bit(name, options) + def bit(name, options = {}) column(name, :bit, options) end - def bit_varying(name, options) + def bit_varying(name, options = {}) column(name, :bit_varying, options) end - def money(name, options) + def money(name, options = {}) column(name, :money, options) end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb index a90adcf4aa..81fde18f64 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -4,34 +4,11 @@ module ActiveRecord class SchemaCreation < AbstractAdapter::SchemaCreation private - def column_options(o) - column_options = super - column_options[:array] = o.array - column_options - end - - def add_column_options!(sql, options) - if options[:array] - sql << '[]' - end + def visit_ColumnDefinition(o) + o.sql_type = type_to_sql(o.type, o.limit, o.precision, o.scale) + o.sql_type << '[]' if o.array super end - - def quote_default_expression(value, column) - if column.type == :uuid && value =~ /\(\)/ - value - else - super - end - end - - def type_for_column(column) - if column.array - @conn.lookup_cast_type("#{column.sql_type}[]") - else - super - end - end end module SchemaStatements @@ -112,7 +89,7 @@ module ActiveRecord end def drop_table(table_name, options = {}) - execute "DROP TABLE #{quote_table_name(table_name)}#{' CASCADE' if options[:force] == :cascade}" + execute "DROP TABLE#{' IF EXISTS' if options[:if_exists]} #{quote_table_name(table_name)}#{' CASCADE' if options[:force] == :cascade}" end # Returns true if schema exists. @@ -183,15 +160,17 @@ module ActiveRecord def columns(table_name) # Limit, precision, and scale are all handled by the superclass. column_definitions(table_name).map do |column_name, type, default, notnull, oid, fmod| - oid = get_oid_type(oid.to_i, fmod.to_i, column_name, type) - default_value = extract_value_from_default(oid, default) + oid = oid.to_i + fmod = fmod.to_i + type_metadata = fetch_type_metadata(column_name, type, oid, fmod) + default_value = extract_value_from_default(default) default_function = extract_default_function(default_value, default) - new_column(column_name, default_value, oid, type, notnull == 'f', default_function) + new_column(column_name, default_value, type_metadata, notnull == 'f', default_function) end end - def new_column(name, default, cast_type, sql_type = nil, null = true, default_function = nil) # :nodoc: - PostgreSQLColumn.new(name, default, cast_type, sql_type, null, default_function) + def new_column(name, default, sql_type_metadata = nil, null = true, default_function = nil) # :nodoc: + PostgreSQLColumn.new(name, default, sql_type_metadata, null, default_function) end # Returns the current database name. @@ -453,7 +432,7 @@ module ActiveRecord # cast the default to the columns type, which leaves us with a default like "default NULL::character varying". execute alter_column_query % "DROP DEFAULT" else - execute alter_column_query % "SET DEFAULT #{quote_default_value(default, column)}" + execute alter_column_query % "SET DEFAULT #{quote_default_expression(default, column)}" end end @@ -461,7 +440,7 @@ module ActiveRecord clear_cache! unless null || default.nil? column = column_for(table_name, column_name) - execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote_default_value(default, column)} WHERE #{quote_column_name(column_name)} IS NULL") if column + execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote_default_expression(default, column)} WHERE #{quote_column_name(column_name)} IS NULL") if column end execute("ALTER TABLE #{quote_table_name(table_name)} ALTER #{quote_column_name(column_name)} #{null ? 'DROP' : 'SET'} NOT NULL") end @@ -580,6 +559,18 @@ module ActiveRecord [super, *order_columns].join(', ') end + + def fetch_type_metadata(column_name, sql_type, oid, fmod) + cast_type = get_oid_type(oid, fmod, column_name, sql_type) + simple_type = SqlTypeMetadata.new( + sql_type: sql_type, + type: cast_type.type, + limit: cast_type.limit, + precision: cast_type.precision, + scale: cast_type.scale, + ) + PostgreSQLTypeMetadata.new(simple_type, oid: oid, fmod: fmod) + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb b/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb new file mode 100644 index 0000000000..58715978f7 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb @@ -0,0 +1,35 @@ +module ActiveRecord + module ConnectionAdapters + class PostgreSQLTypeMetadata < DelegateClass(SqlTypeMetadata) + attr_reader :oid, :fmod, :array + + def initialize(type_metadata, oid: nil, fmod: nil) + super(type_metadata) + @type_metadata = type_metadata + @oid = oid + @fmod = fmod + @array = /\[\]$/ === type_metadata.sql_type + end + + def sql_type + super.gsub(/\[\]$/, "") + end + + def ==(other) + other.is_a?(PostgreSQLTypeMetadata) && + attributes_for_hash == other.attributes_for_hash + end + alias eql? == + + def hash + attributes_for_hash.hash + end + + protected + + def attributes_for_hash + [self.class, @type_metadata, oid, fmod] + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 5b070cae4f..5a887ea529 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -1,14 +1,14 @@ -require 'active_record/connection_adapters/abstract_adapter' -require 'active_record/connection_adapters/statement_pool' - -require 'active_record/connection_adapters/postgresql/utils' -require 'active_record/connection_adapters/postgresql/column' -require 'active_record/connection_adapters/postgresql/oid' -require 'active_record/connection_adapters/postgresql/quoting' -require 'active_record/connection_adapters/postgresql/referential_integrity' -require 'active_record/connection_adapters/postgresql/schema_definitions' -require 'active_record/connection_adapters/postgresql/schema_statements' -require 'active_record/connection_adapters/postgresql/database_statements' +require "active_record/connection_adapters/abstract_adapter" +require "active_record/connection_adapters/postgresql/column" +require "active_record/connection_adapters/postgresql/database_statements" +require "active_record/connection_adapters/postgresql/oid" +require "active_record/connection_adapters/postgresql/quoting" +require "active_record/connection_adapters/postgresql/referential_integrity" +require "active_record/connection_adapters/postgresql/schema_definitions" +require "active_record/connection_adapters/postgresql/schema_statements" +require "active_record/connection_adapters/postgresql/type_metadata" +require "active_record/connection_adapters/postgresql/utils" +require "active_record/connection_adapters/statement_pool" require 'arel/visitors/bind_visitor' @@ -64,7 +64,7 @@ module ActiveRecord # <tt>SET client_min_messages TO <min_messages></tt> call on the connection. # * <tt>:variables</tt> - An optional hash of additional parameters that # will be used in <tt>SET SESSION key = val</tt> calls on the connection. - # * <tt>:insert_returning</tt> - An optional boolean to control the use or <tt>RETURNING</tt> for <tt>INSERT</tt> statements + # * <tt>:insert_returning</tt> - An optional boolean to control the use of <tt>RETURNING</tt> for <tt>INSERT</tt> statements # defaults to true. # # Any further options are used as connection parameters to libpq. See @@ -255,6 +255,8 @@ module ActiveRecord @table_alias_length = nil connect + add_pg_decoders + @statements = StatementPool.new @connection, self.class.type_cast_config_to_integer(config.fetch(:statement_limit) { 1000 }) @@ -459,11 +461,11 @@ module ActiveRecord end def initialize_type_map(m) # :nodoc: - register_class_with_limit m, 'int2', OID::Integer - register_class_with_limit m, 'int4', OID::Integer - register_class_with_limit m, 'int8', OID::Integer + register_class_with_limit m, 'int2', Type::Integer + register_class_with_limit m, 'int4', Type::Integer + register_class_with_limit m, 'int8', Type::Integer m.alias_type 'oid', 'int2' - m.register_type 'float4', OID::Float.new + m.register_type 'float4', Type::Float.new m.alias_type 'float8', 'float4' m.register_type 'text', Type::Text.new register_class_with_limit m, 'varchar', Type::String @@ -474,8 +476,8 @@ module ActiveRecord register_class_with_limit m, 'bit', OID::Bit register_class_with_limit m, 'varbit', OID::BitVarying m.alias_type 'timestamptz', 'timestamp' - m.register_type 'date', OID::Date.new - m.register_type 'time', OID::Time.new + m.register_type 'date', Type::Date.new + m.register_type 'time', Type::Time.new m.register_type 'money', OID::Money.new m.register_type 'bytea', OID::Bytea.new @@ -541,7 +543,7 @@ module ActiveRecord end # Extracts the value from a PostgreSQL column default definition. - def extract_value_from_default(oid, default) # :nodoc: + def extract_value_from_default(default) # :nodoc: case default # Quoted types when /\A[\(B]?'(.*)'::/m @@ -609,12 +611,10 @@ module ActiveRecord def exec_cache(sql, name, binds) stmt_key = prepare_statement(sql) - type_casted_binds = binds.map { |col, val| - [col, type_cast(val, col)] - } + type_casted_binds = binds.map { |attr| type_cast(attr.value_for_database) } - log(sql, name, type_casted_binds, stmt_key) do - @connection.exec_prepared(stmt_key, type_casted_binds.map { |_, val| val }) + log(sql, name, binds, stmt_key) do + @connection.exec_prepared(stmt_key, type_casted_binds) end rescue ActiveRecord::StatementInvalid => e pgerror = e.original_exception @@ -764,6 +764,73 @@ module ActiveRecord def create_table_definition(name, temporary = false, options = nil, as = nil) # :nodoc: PostgreSQL::TableDefinition.new native_database_types, name, temporary, options, as end + + def can_perform_case_insensitive_comparison_for?(column) + @case_insensitive_cache ||= {} + @case_insensitive_cache[column.sql_type] ||= begin + sql = <<-end_sql + SELECT exists( + SELECT * FROM pg_proc + INNER JOIN pg_cast + ON casttarget::text::oidvector = proargtypes + WHERE proname = 'lower' + AND castsource = '#{column.sql_type}'::regtype::oid + ) + end_sql + execute_and_clear(sql, "SCHEMA", []) do |result| + result.getvalue(0, 0) == 't' + end + end + end + + def add_pg_decoders + coders_by_name = { + 'int2' => PG::TextDecoder::Integer, + 'int4' => PG::TextDecoder::Integer, + 'int8' => PG::TextDecoder::Integer, + 'oid' => PG::TextDecoder::Integer, + 'float4' => PG::TextDecoder::Float, + 'float8' => PG::TextDecoder::Float, + 'bool' => PG::TextDecoder::Boolean, + } + query = <<-SQL + SELECT t.oid, t.typname, t.typelem, t.typdelim, t.typinput, t.typtype, t.typbasetype + FROM pg_type as t + SQL + coders = execute_and_clear(query, "SCHEMA", []) do |result| + result + .map { |row| construct_coder(row, coders_by_name['typname']) } + .compact + end + + map = PG::TypeMapByOid.new + coders.each { |coder| map.add_coder(coder) } + @connection.type_map_for_results = map + end + + def construct_coder(row, coder_class) + return unless coder_class + coder_class.new(oid: row['oid'], name: row['typname']) + end + + ActiveRecord::Type.add_modifier({ array: true }, OID::Array, adapter: :postgresql) + ActiveRecord::Type.add_modifier({ range: true }, OID::Range, adapter: :postgresql) + ActiveRecord::Type.register(:bit, OID::Bit, adapter: :postgresql) + ActiveRecord::Type.register(:bit_varying, OID::BitVarying, adapter: :postgresql) + ActiveRecord::Type.register(:binary, OID::Bytea, adapter: :postgresql) + ActiveRecord::Type.register(:cidr, OID::Cidr, adapter: :postgresql) + ActiveRecord::Type.register(:date_time, OID::DateTime, adapter: :postgresql) + ActiveRecord::Type.register(:decimal, OID::Decimal, adapter: :postgresql) + ActiveRecord::Type.register(:enum, OID::Enum, adapter: :postgresql) + ActiveRecord::Type.register(:hstore, OID::Hstore, adapter: :postgresql) + ActiveRecord::Type.register(:inet, OID::Inet, adapter: :postgresql) + ActiveRecord::Type.register(:json, OID::Json, adapter: :postgresql) + ActiveRecord::Type.register(:jsonb, OID::Jsonb, adapter: :postgresql) + ActiveRecord::Type.register(:money, OID::Money, adapter: :postgresql) + ActiveRecord::Type.register(:point, OID::Point, adapter: :postgresql) + ActiveRecord::Type.register(:uuid, OID::Uuid, adapter: :postgresql) + ActiveRecord::Type.register(:vector, OID::Vector, adapter: :postgresql) + ActiveRecord::Type.register(:xml, OID::Xml, adapter: :postgresql) end end end diff --git a/activerecord/lib/active_record/connection_adapters/sql_type_metadata.rb b/activerecord/lib/active_record/connection_adapters/sql_type_metadata.rb new file mode 100644 index 0000000000..ccb7e154ee --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/sql_type_metadata.rb @@ -0,0 +1,32 @@ +module ActiveRecord + # :stopdoc: + module ConnectionAdapters + class SqlTypeMetadata + attr_reader :sql_type, :type, :limit, :precision, :scale + + def initialize(sql_type: nil, type: nil, limit: nil, precision: nil, scale: nil) + @sql_type = sql_type + @type = type + @limit = limit + @precision = precision + @scale = scale + end + + def ==(other) + other.is_a?(SqlTypeMetadata) && + attributes_for_hash == other.attributes_for_hash + end + alias eql? == + + def hash + attributes_for_hash.hash + end + + protected + + def attributes_for_hash + [self.class, sql_type, type, limit, precision, scale] + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 03dfd29a0a..400b586c95 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -41,25 +41,6 @@ module ActiveRecord end module ConnectionAdapters #:nodoc: - class SQLite3Binary < Type::Binary # :nodoc: - def cast_value(value) - if value.encoding != Encoding::ASCII_8BIT - value = value.force_encoding(Encoding::ASCII_8BIT) - end - value - end - end - - class SQLite3String < Type::String # :nodoc: - def type_cast_for_database(value) - if value.is_a?(::String) && value.encoding == Encoding::ASCII_8BIT - value.encode(Encoding::UTF_8) - else - super - end - end - end - # The SQLite3 adapter works SQLite 3.6.16 or newer # with the sqlite3-ruby drivers (available as gem from https://rubygems.org/gems/sqlite3). # @@ -239,6 +220,12 @@ module ActiveRecord case value when BigDecimal value.to_f + when String + if value.encoding == Encoding::ASCII_8BIT + super(value.encode(Encoding::UTF_8)) + else + super + end else super end @@ -280,11 +267,9 @@ module ActiveRecord end def exec_query(sql, name = nil, binds = []) - type_casted_binds = binds.map { |col, val| - [col, type_cast(val, col)] - } + type_casted_binds = binds.map { |attr| type_cast(attr.value_for_database) } - log(sql, name, type_casted_binds) do + log(sql, name, binds) do # Don't cache statements if they are not prepared if without_prepared_statement?(binds) stmt = @connection.prepare(sql) @@ -302,7 +287,7 @@ module ActiveRecord stmt = cache[:stmt] cols = cache[:cols] ||= stmt.columns stmt.reset! - stmt.bind_params type_casted_binds.map { |_, val| val } + stmt.bind_params type_casted_binds end ActiveRecord::Result.new(cols, stmt.to_a) @@ -387,8 +372,8 @@ module ActiveRecord end sql_type = field['type'] - cast_type = lookup_cast_type(sql_type) - new_column(field['name'], field['dflt_value'], cast_type, sql_type, field['notnull'].to_i == 0) + type_metadata = fetch_type_metadata(sql_type) + new_column(field['name'], field['dflt_value'], type_metadata, field['notnull'].to_i == 0) end end @@ -495,12 +480,6 @@ module ActiveRecord protected - def initialize_type_map(m) - super - m.register_type(/binary/i, SQLite3Binary.new) - register_class_with_limit m, %r(char)i, SQLite3String - end - def table_structure(table_name) structure = exec_query("PRAGMA table_info(#{quote_table_name(table_name)})", 'SCHEMA').to_hash raise(ActiveRecord::StatementInvalid, "Could not find table '#{table_name}'") if structure.empty? diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index 5a5139256d..44d587206d 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -107,20 +107,18 @@ module ActiveRecord super end - def initialize_find_by_cache + def initialize_find_by_cache # :nodoc: self.find_by_statement_cache = {}.extend(Mutex_m) end - def inherited(child_class) + def inherited(child_class) # :nodoc: child_class.initialize_find_by_cache super end - def find(*ids) + def find(*ids) # :nodoc: # We don't have cache keys for this stuff yet return super unless ids.length == 1 - # Allow symbols to super to maintain compatibility for deprecated finders until Rails 5 - return super if ids.first.kind_of?(Symbol) return super if block_given? || primary_key.nil? || default_scopes.any? || @@ -152,7 +150,7 @@ module ActiveRecord raise RecordNotFound, "Couldn't find #{name} with an out of range value for '#{primary_key}'" end - def find_by(*args) + def find_by(*args) # :nodoc: return super if current_scope || !(Hash === args.first) || reflect_on_all_aggregations.any? return super if default_scopes.any? @@ -185,11 +183,11 @@ module ActiveRecord end end - def find_by!(*args) + def find_by!(*args) # :nodoc: find_by(*args) or raise RecordNotFound.new("Couldn't find #{name}") end - def initialize_generated_modules + def initialize_generated_modules # :nodoc: generated_association_methods end @@ -210,7 +208,7 @@ module ActiveRecord elsif !connected? "#{super} (call '#{super}.connection' to establish a connection)" elsif table_exists? - attr_list = columns.map { |c| "#{c.name}: #{c.type}" } * ', ' + attr_list = attribute_types.map { |name, type| "#{name}: #{type.type}" } * ', ' "#{super}(#{attr_list})" else "#{super}(Table doesn't exist)" @@ -274,16 +272,14 @@ module ActiveRecord # ==== Example: # # Instantiates a single new object # User.new(first_name: 'Jamie') - def initialize(attributes = nil, options = {}) + def initialize(attributes = nil) @attributes = self.class._default_attributes.dup self.class.define_attribute_methods init_internals initialize_internals_callback - # +options+ argument is only needed to make protected_attributes gem easier to hook. - # Remove it when we drop support to this gem. - init_attributes(attributes, options) if attributes + assign_attributes(attributes) if attributes yield self if block_given? _run_initialize_callbacks @@ -458,6 +454,7 @@ module ActiveRecord # Takes a PP and prettily prints this record to it, allowing you to get a nice result from `pp record` # when pp is required. def pretty_print(pp) + return super if custom_inspect_method_defined? pp.object_address_group(self) do if defined?(@attributes) && @attributes column_names = self.class.column_names.select { |name| has_attribute?(name) || new_record? } @@ -483,16 +480,16 @@ module ActiveRecord Hash[methods.map! { |method| [method, public_send(method)] }].with_indifferent_access end + private + def set_transaction_state(state) # :nodoc: @transaction_state = state end def has_transactional_callbacks? # :nodoc: - !_rollback_callbacks.empty? || !_commit_callbacks.empty? || !_create_callbacks.empty? + !_rollback_callbacks.empty? || !_commit_callbacks.empty? end - private - # Updates the attributes on this particular ActiveRecord object so that # if it is associated with a transaction, then the state of the AR object # will be updated to reflect the current state of the transaction @@ -515,6 +512,8 @@ module ActiveRecord end def update_attributes_from_transaction_state(transaction_state, depth) + @reflects_state = [false] if depth == 0 + if transaction_state && transaction_state.finalized? && !has_transactional_callbacks? unless @reflects_state[depth] restore_transaction_record_state if transaction_state.rolledback? @@ -551,22 +550,19 @@ module ActiveRecord @txn = nil @_start_transaction_state = {} @transaction_state = nil - @reflects_state = [false] end def initialize_internals_callback end - # This method is needed to make protected_attributes gem easier to hook. - # Remove it when we drop support to this gem. - def init_attributes(attributes, options) - assign_attributes(attributes) - end - def thaw if frozen? @attributes = @attributes.dup end end + + def custom_inspect_method_defined? + self.class.instance_method(:inspect).owner != ActiveRecord::Base.instance_method(:inspect).owner + end end end diff --git a/activerecord/lib/active_record/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb index 7d8e0a2063..82596b63df 100644 --- a/activerecord/lib/active_record/counter_cache.rb +++ b/activerecord/lib/active_record/counter_cache.rb @@ -156,7 +156,7 @@ module ActiveRecord def each_counter_cached_associations _reflections.each do |name, reflection| - yield association(name) if reflection.belongs_to? && reflection.counter_cache_column + yield association(name.to_sym) if reflection.belongs_to? && reflection.counter_cache_column end end diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb index f053372cfb..ea88983917 100644 --- a/activerecord/lib/active_record/enum.rb +++ b/activerecord/lib/active_record/enum.rb @@ -32,6 +32,12 @@ module ActiveRecord # Conversation.active # Conversation.archived # + # Of course, you can also query them directly if the scopes doesn't fit your + # needs: + # + # Conversation.where(status: [:active, :archived]) + # Conversation.where.not(status: :active) + # # You can set the default value from the database declaration, like: # # create_table :conversations do |t| @@ -59,15 +65,17 @@ module ActiveRecord # # In rare circumstances you might need to access the mapping directly. # The mappings are exposed through a class method with the pluralized attribute - # name: + # name, which return the mapping in a +HashWithIndifferentAccess+: # - # Conversation.statuses # => { "active" => 0, "archived" => 1 } + # Conversation.statuses[:active] # => 0 + # Conversation.statuses["archived"] # => 1 # - # Use that class method when you need to know the ordinal value of an enum: + # Use that class method when you need to know the ordinal value of an enum. + # For example, you can use that when manually building SQL strings: # # Conversation.where("status <> ?", Conversation.statuses[:archived]) # - # Where conditions on an enum attribute must use the ordinal value of an enum. + module Enum def self.extended(base) # :nodoc: base.class_attribute(:defined_enums) @@ -79,6 +87,38 @@ module ActiveRecord super end + class EnumType < Type::Value + def initialize(name, mapping) + @name = name + @mapping = mapping + end + + def cast(value) + return if value.blank? + + if mapping.has_key?(value) + value.to_s + elsif mapping.has_value?(value) + mapping.key(value) + else + raise ArgumentError, "'#{value}' is not a valid #{name}" + end + end + + def deserialize(value) + return if value.nil? + mapping.key(value.to_i) + end + + def serialize(value) + mapping.fetch(value, value) + end + + protected + + attr_reader :name, :mapping + end + def enum(definitions) klass = self definitions.each do |name, values| @@ -90,37 +130,19 @@ module ActiveRecord detect_enum_conflict!(name, name.to_s.pluralize, true) klass.singleton_class.send(:define_method, name.to_s.pluralize) { enum_values } - _enum_methods_module.module_eval do - # def status=(value) self[:status] = statuses[value] end - klass.send(:detect_enum_conflict!, name, "#{name}=") - define_method("#{name}=") { |value| - if enum_values.has_key?(value) || value.blank? - self[name] = enum_values[value] - elsif enum_values.has_value?(value) - # Assigning a value directly is not a end-user feature, hence it's not documented. - # This is used internally to make building objects from the generated scopes work - # as expected, i.e. +Conversation.archived.build.archived?+ should be true. - self[name] = value - else - raise ArgumentError, "'#{value}' is not a valid #{name}" - end - } - - # def status() statuses.key self[:status] end - klass.send(:detect_enum_conflict!, name, name) - define_method(name) { enum_values.key self[name] } + detect_enum_conflict!(name, name) + detect_enum_conflict!(name, "#{name}=") - # def status_before_type_cast() statuses.key self[:status] end - klass.send(:detect_enum_conflict!, name, "#{name}_before_type_cast") - define_method("#{name}_before_type_cast") { enum_values.key self[name] } + attribute name, EnumType.new(name, enum_values) + _enum_methods_module.module_eval do pairs = values.respond_to?(:each_pair) ? values.each_pair : values.each_with_index pairs.each do |value, i| enum_values[value] = i # def active?() status == 0 end klass.send(:detect_enum_conflict!, name, "#{value}?") - define_method("#{value}?") { self[name] == i } + define_method("#{value}?") { self[name] == value.to_s } # def active!() update! status: :active end klass.send(:detect_enum_conflict!, name, "#{value}!") @@ -128,7 +150,7 @@ module ActiveRecord # scope :active, -> { where status: 0 } klass.send(:detect_enum_conflict!, name, value, true) - klass.scope value, -> { klass.where name => i } + klass.scope value, -> { klass.where name => value } end end defined_enums[name.to_s] = enum_values @@ -138,25 +160,7 @@ module ActiveRecord private def _enum_methods_module @_enum_methods_module ||= begin - mod = Module.new do - private - def save_changed_attribute(attr_name, old) - if (mapping = self.class.defined_enums[attr_name.to_s]) - value = _read_attribute(attr_name) - if attribute_changed?(attr_name) - if mapping[old] == value - clear_attribute_changes([attr_name]) - end - else - if old != value - set_attribute_was(attr_name, mapping.key(old)) - end - end - else - super - end - end - end + mod = Module.new include mod mod end diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb index fc28ab585f..d710d96a9a 100644 --- a/activerecord/lib/active_record/errors.rb +++ b/activerecord/lib/active_record/errors.rb @@ -178,18 +178,10 @@ module ActiveRecord class DangerousAttributeError < ActiveRecordError end - # Raised when unknown attributes are supplied via mass assignment. - class UnknownAttributeError < NoMethodError - - attr_reader :record, :attribute - - def initialize(record, attribute) - @record = record - @attribute = attribute.to_s - super("unknown attribute '#{attribute}' for #{@record.class}.") - end - - end + UnknownAttributeError = ActiveSupport::Deprecation::DeprecatedConstantProxy.new( # :nodoc: + 'ActiveRecord::UnknownAttributeError', + 'ActiveModel::AttributeAssignment::UnknownAttributeError' + ) # Raised when an error occurred while doing a mass assignment to an attribute through the # +attributes=+ method. The exception has an +attribute+ property that is the name of the diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 10e9be20b5..739be524da 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -1,6 +1,7 @@ require 'erb' require 'yaml' require 'zlib' +require 'set' require 'active_support/dependencies' require 'active_support/core_ext/digest/uuid' require 'active_record/fixture_set/file' @@ -131,7 +132,7 @@ module ActiveRecord # Digest::SHA2.hexdigest(File.read(Rails.root.join('test/fixtures', path))) # end # end - # ActiveRecord::FixtureSet.context_class.send :include, FixtureFileHelpers + # ActiveRecord::FixtureSet.context_class.include FixtureFileHelpers # # - use the helper method in a fixture # photo: @@ -521,12 +522,16 @@ module ActiveRecord update_all_loaded_fixtures fixtures_map connection.transaction(:requires_new => true) do + deleted_tables = Set.new fixture_sets.each do |fs| conn = fs.model_class.respond_to?(:connection) ? fs.model_class.connection : connection table_rows = fs.table_rows table_rows.each_key do |table| - conn.delete "DELETE FROM #{conn.quote_table_name(table)}", 'Fixture Delete' + unless deleted_tables.include? table + conn.delete "DELETE FROM #{conn.quote_table_name(table)}", 'Fixture Delete' + end + deleted_tables << table end table_rows.each do |fixture_set_name, rows| @@ -661,7 +666,7 @@ module ActiveRecord row[association.foreign_type] = $1 end - fk_type = association.active_record.columns_hash[fk_name].type + fk_type = association.active_record.type_for_attribute(fk_name).type row[fk_name] = ActiveRecord::FixtureSet.identify(value, fk_type) end when :has_many @@ -691,7 +696,7 @@ module ActiveRecord end def primary_key_type - @association.klass.column_types[@association.klass.primary_key].type + @association.klass.type_for_attribute(@association.klass.primary_key).type end end @@ -711,7 +716,7 @@ module ActiveRecord end def primary_key_type - @primary_key_type ||= model_class && model_class.column_types[model_class.primary_key].type + @primary_key_type ||= model_class && model_class.type_for_attribute(model_class.primary_key).type end def add_join_records(rows, row, association) diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb index fd1e22349b..24098f72dc 100644 --- a/activerecord/lib/active_record/inheritance.rb +++ b/activerecord/lib/active_record/inheritance.rb @@ -192,7 +192,7 @@ module ActiveRecord # If this is a StrongParameters hash, and access to inheritance_column is not permitted, # this will ignore the inheritance column and return nil def subclass_from_attributes?(attrs) - columns_hash.include?(inheritance_column) && attrs.is_a?(Hash) + attribute_names.include?(inheritance_column) && attrs.is_a?(Hash) end def subclass_from_attributes(attrs) diff --git a/activerecord/lib/active_record/locale/en.yml b/activerecord/lib/active_record/locale/en.yml index b1fbd38622..8a3c27e6da 100644 --- a/activerecord/lib/active_record/locale/en.yml +++ b/activerecord/lib/active_record/locale/en.yml @@ -7,6 +7,7 @@ en: # Default error messages errors: messages: + required: "must exist" taken: "has already been taken" # Active Record models configuration diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index 9f053453bd..a58d8355aa 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -66,6 +66,15 @@ module ActiveRecord send(lock_col + '=', previous_lock_value + 1) end + def _create_record(attribute_names = self.attribute_names, *) # :nodoc: + if locking_enabled? + # We always want to persist the locking version, even if we don't detect + # a change from the default, since the database might have no default + attribute_names |= [self.class.locking_column] + end + super + end + def _update_record(attribute_names = self.attribute_names) #:nodoc: return super unless locking_enabled? return 0 if attribute_names.empty? @@ -116,12 +125,8 @@ module ActiveRecord relation = super if locking_enabled? - column_name = self.class.locking_column - column = self.class.columns_hash[column_name] - substitute = self.class.connection.substitute_at(column) - - relation = relation.where(self.class.arel_table[column_name].eq(substitute)) - relation.bind_values << [column, self[column_name].to_i] + locking_column = self.class.locking_column + relation = relation.where(locking_column => _read_attribute(locking_column)) end relation @@ -139,7 +144,7 @@ module ActiveRecord # Set the column to use for optimistic locking. Defaults to +lock_version+. def locking_column=(value) - clear_caches_calculated_from_columns + reload_schema_from_cache @locking_column = value.to_s end @@ -179,17 +184,12 @@ module ActiveRecord end end - class LockingType < SimpleDelegator # :nodoc: - def type_cast_from_database(value) + class LockingType < DelegateClass(Type::Value) # :nodoc: + def deserialize(value) # `nil` *should* be changed to 0 super.to_i end - def changed?(old_value, *) - # Ensure we save if the default was `nil` - super || old_value == 0 - end - def init_with(coder) __setobj__(coder['subtype']) end diff --git a/activerecord/lib/active_record/log_subscriber.rb b/activerecord/lib/active_record/log_subscriber.rb index eb64d197f0..6b26d7be78 100644 --- a/activerecord/lib/active_record/log_subscriber.rb +++ b/activerecord/lib/active_record/log_subscriber.rb @@ -20,18 +20,14 @@ module ActiveRecord @odd = false end - def render_bind(column, value) - if column - if column.binary? - # This specifically deals with the PG adapter that casts bytea columns into a Hash. - value = value[:value] if value.is_a?(Hash) - value = value ? "<#{value.bytesize} bytes of binary data>" : "<NULL binary data>" - end - - [column.name, value] + def render_bind(attribute) + value = if attribute.type.binary? && attribute.value + "<#{attribute.value.bytesize} bytes of binary data>" else - [nil, value] + attribute.value_for_database end + + [attribute.name, value] end def sql(event) @@ -47,9 +43,7 @@ module ActiveRecord binds = nil unless (payload[:binds] || []).empty? - binds = " " + payload[:binds].map { |col,v| - render_bind(col, v) - }.inspect + binds = " " + payload[:binds].map { |attr| render_bind(attr) }.inspect end if odd? diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index 641512d323..75adcccce6 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -111,17 +111,6 @@ module ActiveRecord # class Mouse < ActiveRecord::Base # self.table_name = "mice" # end - # - # Alternatively, you can override the table_name method to define your - # own computation. (Possibly using <tt>super</tt> to manipulate the default - # table name.) Example: - # - # class Post < ActiveRecord::Base - # def self.table_name - # "special_" + super - # end - # end - # Post.table_name # => "special_posts" def table_name reset_table_name unless defined?(@table_name) @table_name @@ -132,9 +121,6 @@ module ActiveRecord # class Project < ActiveRecord::Base # self.table_name = "project" # end - # - # You can also just define your own <tt>self.table_name</tt> method; see - # the documentation for ActiveRecord::Base#table_name. def table_name=(value) value = value && value.to_s @@ -231,28 +217,37 @@ module ActiveRecord end def attributes_builder # :nodoc: - @attributes_builder ||= AttributeSet::Builder.new(column_types, primary_key) + @attributes_builder ||= AttributeSet::Builder.new(attribute_types, primary_key) end - def column_types # :nodoc: - @column_types ||= columns_hash.transform_values(&:cast_type).tap do |h| - h.default = Type::Value.new - end + def columns_hash # :nodoc: + load_schema + @columns_hash + end + + def columns + load_schema + @columns ||= columns_hash.values + end + + def attribute_types # :nodoc: + load_schema + @attribute_types ||= Hash.new(Type::Value.new) end def type_for_attribute(attr_name) # :nodoc: - column_types[attr_name] + attribute_types[attr_name] end # Returns a hash where the keys are column names and the values are # default values when instantiating the AR object for this table. def column_defaults + load_schema _default_attributes.to_hash end def _default_attributes # :nodoc: - @default_attributes ||= attributes_builder.build_from_database( - raw_default_values) + @default_attributes ||= AttributeSet.new({}) end # Returns an array of column names as strings. @@ -295,19 +290,53 @@ module ActiveRecord def reset_column_information connection.clear_cache! undefine_attribute_methods - connection.schema_cache.clear_table_cache!(table_name) if table_exists? + connection.schema_cache.clear_table_cache!(table_name) - @arel_engine = nil - @arel_table = nil - @column_names = nil - @column_types = nil - @content_columns = nil - @default_attributes = nil - @inheritance_column = nil unless defined?(@explicit_inheritance_column) && @explicit_inheritance_column + reload_schema_from_cache end private + def schema_loaded? + defined?(@columns_hash) && @columns_hash + end + + def load_schema + unless schema_loaded? + load_schema! + end + end + + def load_schema! + @columns_hash = connection.schema_cache.columns_hash(table_name) + @columns_hash.each do |name, column| + define_attribute( + name, + connection.lookup_cast_type_from_column(column), + default: column.default, + user_provided_default: false + ) + end + end + + def reload_schema_from_cache + @arel_engine = nil + @arel_table = nil + @column_names = nil + @attribute_types = nil + @content_columns = nil + @default_attributes = nil + @inheritance_column = nil unless defined?(@explicit_inheritance_column) && @explicit_inheritance_column + @attributes_builder = nil + @column_names = nil + @attribute_types = nil + @columns = nil + @columns_hash = nil + @content_columns = nil + @default_attributes = nil + @attribute_names = nil + end + # Guesses the table name, but does not decorate it with prefix and suffix information. def undecorated_table_name(class_name = base_class.name) table_name = class_name.to_s.demodulize.underscore @@ -331,10 +360,6 @@ module ActiveRecord base.table_name end end - - def raw_default_values - columns_hash.transform_values(&:default) - end end end end diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index 846e1162a9..084ef397a8 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -312,7 +312,7 @@ module ActiveRecord attr_names.each do |association_name| if reflection = _reflect_on_association(association_name) reflection.autosave = true - add_autosave_association_callbacks(reflection) + define_autosave_validation_callbacks(reflection) nested_attributes_options = self.nested_attributes_options.dup nested_attributes_options[association_name.to_sym] = options @@ -521,7 +521,7 @@ module ActiveRecord # Determines if a hash contains a truthy _destroy key. def has_destroy_flag?(hash) - Type::Boolean.new.type_cast_from_user(hash['_destroy']) + Type::Boolean.new.cast(hash['_destroy']) end # Determines if a new record should be rejected by checking diff --git a/activerecord/lib/active_record/null_relation.rb b/activerecord/lib/active_record/null_relation.rb index b406da14dc..fa6eaa7e64 100644 --- a/activerecord/lib/active_record/null_relation.rb +++ b/activerecord/lib/active_record/null_relation.rb @@ -30,10 +30,18 @@ module ActiveRecord true end + def none? + true + end + def any? false end + def one? + false + end + def many? false end @@ -75,5 +83,9 @@ module ActiveRecord def exists?(_id = false) false end + + def or(other) + other.spawn + end end end diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index cf6673db2e..af7aef6e43 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -178,6 +178,7 @@ module ActiveRecord def destroy raise ReadOnlyRecord, "#{self.class} is marked as readonly" if readonly? destroy_associations + self.class.connection.add_transaction_record(self) destroy_row if persisted? @destroyed = true freeze @@ -245,8 +246,8 @@ module ActiveRecord def update_attribute(name, value) name = name.to_s verify_readonly_attribute(name) - send("#{name}=", value) - save(validate: false) + public_send("#{name}=", value) + save(validate: false) if changed? end # Updates the attributes of the model from the passed-in hash and saves the @@ -352,7 +353,7 @@ module ActiveRecord # method toggles directly the underlying value without calling any setter. # Returns +self+. def toggle(attribute) - self[attribute] = !send("#{attribute}?") + self[attribute] = !public_send("#{attribute}?") self end @@ -495,15 +496,7 @@ module ActiveRecord end def relation_for_destroy - pk = self.class.primary_key - column = self.class.columns_hash[pk] - substitute = self.class.connection.substitute_at(column) - - relation = self.class.unscoped.where( - self.class.arel_table[pk].eq(substitute)) - - relation.bind_values = [[column, id]] - relation + self.class.unscoped.where(self.class.primary_key => id) end def create_or_update(*args) diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb index 91c9a0db99..4e597590e9 100644 --- a/activerecord/lib/active_record/querying.rb +++ b/activerecord/lib/active_record/querying.rb @@ -7,7 +7,7 @@ module ActiveRecord delegate :find_by, :find_by!, to: :all delegate :destroy, :destroy_all, :delete, :delete_all, :update, :update_all, to: :all delegate :find_each, :find_in_batches, to: :all - delegate :select, :group, :order, :except, :reorder, :limit, :offset, :joins, + delegate :select, :group, :order, :except, :reorder, :limit, :offset, :joins, :or, :where, :rewhere, :preload, :eager_load, :includes, :from, :lock, :readonly, :having, :create_with, :uniq, :distinct, :references, :none, :unscope, to: :all delegate :count, :average, :minimum, :maximum, :sum, :calculate, to: :all diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index 04c2be045d..2591e7492d 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -269,9 +269,9 @@ db_namespace = namespace :db do end namespace :structure do - desc 'Dump the database structure to db/structure.sql. Specify another file with DB_STRUCTURE=db/my_structure.sql' + desc 'Dump the database structure to db/structure.sql. Specify another file with SCHEMA=db/my_structure.sql' task :dump => [:environment, :load_config] do - filename = ENV['DB_STRUCTURE'] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "structure.sql") + filename = ENV['SCHEMA'] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "structure.sql") current_config = ActiveRecord::Tasks::DatabaseTasks.current_config ActiveRecord::Tasks::DatabaseTasks.structure_dump(current_config, filename) @@ -286,8 +286,8 @@ db_namespace = namespace :db do end desc "Recreate the databases from the structure.sql file" - task :load => [:environment, :load_config] do - ActiveRecord::Tasks::DatabaseTasks.load_schema_current(:sql, ENV['DB_STRUCTURE']) + task :load => [:load_config] do + ActiveRecord::Tasks::DatabaseTasks.load_schema_current(:sql, ENV['SCHEMA']) end task :load_if_sql => ['db:create', :environment] do diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index ab3debc03b..6bbec7c0c0 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -1,18 +1,19 @@ # -*- coding: utf-8 -*- -require 'arel/collectors/bind' +require "arel/collectors/bind" module ActiveRecord # = Active Record Relation class Relation MULTI_VALUE_METHODS = [:includes, :eager_load, :preload, :select, :group, - :order, :joins, :where, :having, :bind, :references, + :order, :joins, :references, :extending, :unscope] - SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :from, :reordering, + SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :reordering, :reverse_order, :distinct, :create_with, :uniq] + CLAUSE_METHODS = [:where, :having, :from] INVALID_METHODS_FOR_DELETE_ALL = [:limit, :distinct, :offset, :group, :having] - VALUE_METHODS = MULTI_VALUE_METHODS + SINGLE_VALUE_METHODS + VALUE_METHODS = MULTI_VALUE_METHODS + SINGLE_VALUE_METHODS + CLAUSE_METHODS include FinderMethods, Calculations, SpawnMethods, QueryMethods, Batches, Explain, Delegation @@ -33,7 +34,6 @@ module ActiveRecord # This method is a hot spot, so for now, use Hash[] to dup the hash. # https://bugs.ruby-lang.org/issues/7166 @values = Hash[@values] - @values[:bind] = @values[:bind].dup if @values.key? :bind reset end @@ -81,7 +81,7 @@ module ActiveRecord end relation = scope.where(@klass.primary_key => (id_was || id)) - bvs = binds + relation.bind_values + bvs = binds + relation.bound_attributes um = relation .arel .compile_update(substitutes, @klass.primary_key) @@ -95,11 +95,11 @@ module ActiveRecord def substitute_values(values) # :nodoc: binds = values.map do |arel_attr, value| - [@klass.columns_hash[arel_attr.name], value] + QueryAttribute.new(arel_attr.name, value, klass.type_for_attribute(arel_attr.name)) end - substitutes = values.each_with_index.map do |(arel_attr, _), i| - [arel_attr, @klass.connection.substitute_at(binds[i][0])] + substitutes = values.map do |(arel_attr, _)| + [arel_attr, connection.substitute_at(klass.columns_hash[arel_attr.name])] end [substitutes, binds] @@ -271,6 +271,15 @@ module ActiveRecord end end + # Returns true if there are no records. + def none? + if block_given? + to_a.none? { |*block_args| yield(*block_args) } + else + empty? + end + end + # Returns true if there are any records. def any? if block_given? @@ -280,6 +289,15 @@ module ActiveRecord end end + # Returns true if there is exactly one record. + def one? + if block_given? + to_a.one? { |*block_args| yield(*block_args) } + else + limit_value ? to_a.one? : size == 1 + end + end + # Returns true if there is more than one record. def many? if block_given? @@ -342,8 +360,7 @@ module ActiveRecord stmt.wheres = arel.constraints end - bvs = arel.bind_values + bind_values - @klass.connection.update stmt, 'SQL', bvs + @klass.connection.update stmt, 'SQL', bound_attributes end # Updates an object (or multiple objects) and saves it to the database, if validations pass. @@ -467,8 +484,10 @@ module ActiveRecord invalid_methods = INVALID_METHODS_FOR_DELETE_ALL.select { |method| if MULTI_VALUE_METHODS.include?(method) send("#{method}_values").any? - else + elsif SINGLE_VALUE_METHODS.include?(method) send("#{method}_value") + elsif CLAUSE_METHODS.include?(method) + send("#{method}_clause").any? end } if invalid_methods.any? @@ -487,7 +506,7 @@ module ActiveRecord stmt.wheres = arel.constraints end - affected = @klass.connection.delete(stmt, 'SQL', bind_values) + affected = @klass.connection.delete(stmt, 'SQL', bound_attributes) reset affected @@ -557,10 +576,10 @@ module ActiveRecord find_with_associations { |rel| relation = rel } end - arel = relation.arel - binds = (arel.bind_values + relation.bind_values).dup - binds.map! { |bv| connection.quote(*bv.reverse) } - collect = visitor.accept(arel.ast, Arel::Collectors::Bind.new) + binds = relation.bound_attributes + binds = connection.prepare_binds_for_database(binds) + binds.map! { |value| connection.quote(value) } + collect = visitor.accept(relation.arel.ast, Arel::Collectors::Bind.new) collect.substitute_binds(binds).join end end @@ -570,22 +589,7 @@ module ActiveRecord # User.where(name: 'Oscar').where_values_hash # # => {name: "Oscar"} def where_values_hash(relation_table_name = table_name) - equalities = where_values.grep(Arel::Nodes::Equality).find_all { |node| - node.left.relation.name == relation_table_name - } - - binds = Hash[bind_values.find_all(&:first).map { |column, v| [column.name, v] }] - - Hash[equalities.map { |where| - name = where.left.name - [name, binds.fetch(name.to_s) { - case where.right - when Array then where.right.map(&:val) - when Arel::Nodes::Casted, Arel::Nodes::Quoted - where.right.val - end - }] - }] + where_clause.to_h(relation_table_name) end def scope_for_create @@ -648,7 +652,7 @@ module ActiveRecord private def exec_queries - @records = eager_loading? ? find_with_associations : @klass.find_by_sql(arel, arel.bind_values + bind_values) + @records = eager_loading? ? find_with_associations : @klass.find_by_sql(arel, bound_attributes) preload = preload_values preload += includes_values unless eager_loading? diff --git a/activerecord/lib/active_record/relation/batches.rb b/activerecord/lib/active_record/relation/batches.rb index 4f0502ae75..e07580a563 100644 --- a/activerecord/lib/active_record/relation/batches.rb +++ b/activerecord/lib/active_record/relation/batches.rb @@ -27,14 +27,15 @@ module ActiveRecord # # ==== Options # * <tt>:batch_size</tt> - Specifies the size of the batch. Default to 1000. - # * <tt>:start</tt> - Specifies the primary key value to start from. + # * <tt>:begin_at</tt> - Specifies the primary key value to start from, inclusive of the value. + # * <tt>:end_at</tt> - Specifies the primary key value to end at, inclusive of the value. # This is especially useful if you want multiple workers dealing with # the same processing queue. You can make worker 1 handle all the records # between id 0 and 10,000 and worker 2 handle from 10,000 and beyond - # (by setting the +:start+ option on that worker). + # (by setting the +:begin_at+ and +:end_at+ option on each worker). # # # Let's process for a batch of 2000 records, skipping the first 2000 rows - # Person.find_each(start: 2000, batch_size: 2000) do |person| + # Person.find_each(begin_at: 2000, batch_size: 2000) do |person| # person.party_all_night! # end # @@ -45,19 +46,27 @@ module ActiveRecord # # NOTE: You can't set the limit either, that's used to control # the batch sizes. - def find_each(options = {}) + def find_each(begin_at: nil, end_at: nil, batch_size: 1000, start: nil) + if start + begin_at = start + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Passing `start` value to find_each is deprecated, and will be removed in Rails 5.1. + Please pass `begin_at` instead. + MSG + end if block_given? - find_in_batches(options) do |records| + find_in_batches(begin_at: begin_at, end_at: end_at, batch_size: batch_size) do |records| records.each { |record| yield record } end else - enum_for :find_each, options do - options[:start] ? where(table[primary_key].gteq(options[:start])).size : size + enum_for(:find_each, begin_at: begin_at, end_at: end_at, batch_size: batch_size) do + relation = self + apply_limits(relation, begin_at, end_at).size end end end - # Yields each batch of records that was found by the find +options+ as + # Yields each batch of records that was found by the find options as # an array. # # Person.where("age > 21").find_in_batches do |group| @@ -77,14 +86,15 @@ module ActiveRecord # # ==== Options # * <tt>:batch_size</tt> - Specifies the size of the batch. Default to 1000. - # * <tt>:start</tt> - Specifies the primary key value to start from. + # * <tt>:begin_at</tt> - Specifies the primary key value to start from, inclusive of the value. + # * <tt>:end_at</tt> - Specifies the primary key value to end at, inclusive of the value. # This is especially useful if you want multiple workers dealing with # the same processing queue. You can make worker 1 handle all the records # between id 0 and 10,000 and worker 2 handle from 10,000 and beyond - # (by setting the +:start+ option on that worker). + # (by setting the +:begin_at+ and +:end_at+ option on each worker). # # # Let's process the next 2000 records - # Person.find_in_batches(start: 2000, batch_size: 2000) do |group| + # Person.find_in_batches(begin_at: 2000, batch_size: 2000) do |group| # group.each { |person| person.party_all_night! } # end # @@ -95,16 +105,19 @@ module ActiveRecord # # NOTE: You can't set the limit either, that's used to control # the batch sizes. - def find_in_batches(options = {}) - options.assert_valid_keys(:start, :batch_size) + def find_in_batches(begin_at: nil, end_at: nil, batch_size: 1000, start: nil) + if start + begin_at = start + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Passing `start` value to find_in_batches is deprecated, and will be removed in Rails 5.1. + Please pass `begin_at` instead. + MSG + end relation = self - start = options[:start] - batch_size = options[:batch_size] || 1000 - unless block_given? - return to_enum(:find_in_batches, options) do - total = start ? where(table[primary_key].gteq(start)).size : size + return to_enum(:find_in_batches, begin_at: begin_at, end_at: end_at, batch_size: batch_size) do + total = apply_limits(relation, begin_at, end_at).size (total - 1).div(batch_size) + 1 end end @@ -114,7 +127,8 @@ module ActiveRecord end relation = relation.reorder(batch_order).limit(batch_size) - records = start ? relation.where(table[primary_key].gteq(start)).to_a : relation.to_a + relation = apply_limits(relation, begin_at, end_at) + records = relation.to_a while records.any? records_size = records.size @@ -131,6 +145,12 @@ module ActiveRecord private + def apply_limits(relation, begin_at, end_at) + relation = relation.where(table[primary_key].gteq(begin_at)) if begin_at + relation = relation.where(table[primary_key].lteq(end_at)) if end_at + relation + end + def batch_order "#{quoted_table_name}.#{quoted_primary_key} ASC" end diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index 1d4cb1a83b..4a4de86d48 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -166,8 +166,8 @@ module ActiveRecord relation.select_values = column_names.map { |cn| columns_hash.key?(cn) ? arel_table[cn] : cn } - result = klass.connection.select_all(relation.arel, nil, relation.arel.bind_values + bind_values) - result.cast_values(klass.column_types) + result = klass.connection.select_all(relation.arel, nil, bound_attributes) + result.cast_values(klass.attribute_types) end end @@ -227,14 +227,11 @@ module ActiveRecord column_alias = column_name - bind_values = nil - if operation == "count" && (relation.limit_value || relation.offset_value) # Shortcut when limit is zero. return 0 if relation.limit_value == 0 query_builder = build_count_subquery(relation, column_name, distinct) - bind_values = query_builder.bind_values + relation.bind_values else column = aggregate_column(column_name) @@ -245,10 +242,9 @@ module ActiveRecord relation.select_values = [select_value] query_builder = relation.arel - bind_values = query_builder.bind_values + relation.bind_values end - result = @klass.connection.select_all(query_builder, nil, bind_values) + result = @klass.connection.select_all(query_builder, nil, bound_attributes) row = result.first value = row && row.values.first column = result.column_types.fetch(column_alias) do @@ -290,7 +286,7 @@ module ActiveRecord operation, distinct).as(aggregate_alias) ] - select_values += select_values unless having_values.empty? + select_values += select_values unless having_clause.empty? select_values.concat group_fields.zip(group_aliases).map { |field,aliaz| if field.respond_to?(:as) @@ -304,11 +300,11 @@ module ActiveRecord relation.group_values = group relation.select_values = select_values - calculated_data = @klass.connection.select_all(relation, nil, relation.arel.bind_values + bind_values) + calculated_data = @klass.connection.select_all(relation, nil, relation.bound_attributes) if association key_ids = calculated_data.collect { |row| row[group_aliases.first] } - key_records = association.klass.base_class.find(key_ids) + key_records = association.klass.base_class.where(association.klass.base_class.primary_key => key_ids) key_records = Hash[key_records.map { |r| [r.id, r] }] end @@ -357,9 +353,9 @@ module ActiveRecord def type_cast_calculated_value(value, type, operation = nil) case operation when 'count' then value.to_i - when 'sum' then type.type_cast_from_database(value || 0) + when 'sum' then type.deserialize(value || 0) when 'average' then value.respond_to?(:to_d) ? value.to_d : value - else type.type_cast_from_database(value) + else type.deserialize(value) end end @@ -378,11 +374,9 @@ module ActiveRecord aliased_column = aggregate_column(column_name == :all ? 1 : column_name).as(column_alias) relation.select_values = [aliased_column] - arel = relation.arel - subquery = arel.as(subquery_alias) + subquery = relation.arel.as(subquery_alias) sm = Arel::SelectManager.new relation.engine - sm.bind_values = arel.bind_values select_value = operation_over_aggregate_column(column_alias, 'count', distinct) sm.project(select_value).from(subquery) end diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 088bc203b7..fb47d915ff 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -307,11 +307,11 @@ module ActiveRecord relation = relation.where(conditions) else unless conditions == :none - relation = where(primary_key => conditions) + relation = relation.where(primary_key => conditions) end end - connection.select_value(relation, "#{name} Exists", relation.arel.bind_values + relation.bind_values) ? true : false + connection.select_value(relation, "#{name} Exists", relation.bound_attributes) ? true : false end # This method is called whenever no records are found with either a single @@ -365,7 +365,7 @@ module ActiveRecord [] else arel = relation.arel - rows = connection.select_all(arel, 'SQL', arel.bind_values + relation.bind_values) + rows = connection.select_all(arel, 'SQL', relation.bound_attributes) join_dependency.instantiate(rows, aliases) end end @@ -410,7 +410,7 @@ module ActiveRecord relation = relation.except(:select).select(values).distinct! arel = relation.arel - id_rows = @klass.connection.select_all(arel, 'SQL', arel.bind_values + relation.bind_values) + id_rows = @klass.connection.select_all(arel, 'SQL', relation.bound_attributes) id_rows.map {|row| row[primary_key]} end diff --git a/activerecord/lib/active_record/relation/from_clause.rb b/activerecord/lib/active_record/relation/from_clause.rb new file mode 100644 index 0000000000..a93952fa30 --- /dev/null +++ b/activerecord/lib/active_record/relation/from_clause.rb @@ -0,0 +1,32 @@ +module ActiveRecord + class Relation + class FromClause + attr_reader :value, :name + + def initialize(value, name) + @value = value + @name = name + end + + def binds + if value.is_a?(Relation) + value.bound_attributes + else + [] + end + end + + def merge(other) + self + end + + def empty? + value.nil? + end + + def self.empty + new(nil, nil) + end + end + end +end diff --git a/activerecord/lib/active_record/relation/merger.rb b/activerecord/lib/active_record/relation/merger.rb index afb0b208c3..65b607ff1c 100644 --- a/activerecord/lib/active_record/relation/merger.rb +++ b/activerecord/lib/active_record/relation/merger.rb @@ -49,9 +49,9 @@ module ActiveRecord @other = other end - NORMAL_VALUES = Relation::SINGLE_VALUE_METHODS + - Relation::MULTI_VALUE_METHODS - - [:joins, :where, :order, :bind, :reverse_order, :lock, :create_with, :reordering, :from] # :nodoc: + NORMAL_VALUES = Relation::VALUE_METHODS - + Relation::CLAUSE_METHODS - + [:joins, :order, :reverse_order, :lock, :create_with, :reordering] # :nodoc: def normal_values NORMAL_VALUES @@ -75,6 +75,7 @@ module ActiveRecord merge_multi_values merge_single_values + merge_clauses merge_joins relation @@ -107,20 +108,6 @@ module ActiveRecord end def merge_multi_values - lhs_wheres = relation.where_values - rhs_wheres = other.where_values - - lhs_binds = relation.bind_values - rhs_binds = other.bind_values - - removed, kept = partition_overwrites(lhs_wheres, rhs_wheres) - - where_values = kept + rhs_wheres - bind_values = filter_binds(lhs_binds, removed) + rhs_binds - - relation.where_values = where_values - relation.bind_values = bind_values - if other.reordering_value # override any order specified in the original relation relation.reorder! other.order_values @@ -133,36 +120,18 @@ module ActiveRecord end def merge_single_values - relation.from_value = other.from_value unless relation.from_value - relation.lock_value = other.lock_value unless relation.lock_value + relation.lock_value ||= other.lock_value unless other.create_with_value.blank? relation.create_with_value = (relation.create_with_value || {}).merge(other.create_with_value) end end - def filter_binds(lhs_binds, removed_wheres) - return lhs_binds if removed_wheres.empty? - - set = Set.new removed_wheres.map { |x| x.left.name.to_s } - lhs_binds.dup.delete_if { |col,_| set.include? col.name } - end - - # Remove equalities from the existing relation with a LHS which is - # present in the relation being merged in. - # returns [things_to_remove, things_to_keep] - def partition_overwrites(lhs_wheres, rhs_wheres) - if lhs_wheres.empty? || rhs_wheres.empty? - return [[], lhs_wheres] - end - - nodes = rhs_wheres.find_all do |w| - w.respond_to?(:operator) && w.operator == :== - end - seen = Set.new(nodes) { |node| node.left } - - lhs_wheres.partition do |w| - w.respond_to?(:operator) && w.operator == :== && seen.include?(w.left) + def merge_clauses + CLAUSE_METHODS.each do |name| + clause = relation.send("#{name}_clause") + other_clause = other.send("#{name}_clause") + relation.send("#{name}_clause=", clause.merge(other_clause)) end end end diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index 567efce8ae..43e9afe853 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -28,6 +28,11 @@ module ActiveRecord expand_from_hash(attributes) end + def create_binds(attributes) + attributes = convert_dot_notation_to_hash(attributes.stringify_keys) + create_binds_for_hash(attributes) + end + def expand(column, value) # Find the foreign key when using queries such as: # Post.where(author: author) @@ -80,16 +85,43 @@ module ActiveRecord attributes.flat_map do |key, value| if value.is_a?(Hash) - builder = self.class.new(table.associated_table(key)) - builder.expand_from_hash(value) + associated_predicate_builder(key).expand_from_hash(value) else expand(key, value) end end end + + def create_binds_for_hash(attributes) + result = attributes.dup + binds = [] + + attributes.each do |column_name, value| + case value + when Hash + attrs, bvs = associated_predicate_builder(column_name).create_binds_for_hash(value) + result[column_name] = attrs + binds += bvs + when Relation + binds += value.bound_attributes + else + if can_be_bound?(column_name, value) + result[column_name] = Arel::Nodes::BindParam.new + binds << Relation::QueryAttribute.new(column_name.to_s, value, table.type(column_name)) + end + end + end + + [result, binds] + end + private + def associated_predicate_builder(association_name) + self.class.new(table.associated_table(association_name)) + end + def convert_dot_notation_to_hash(attributes) dot_notation = attributes.keys.select { |s| s.include?(".") } @@ -107,5 +139,11 @@ module ActiveRecord def handler_for(object) @handlers.detect { |klass, _| klass === object }.last end + + def can_be_bound?(column_name, value) + !value.nil? && + handler_for(value).is_a?(BasicObjectHandler) && + !table.associated_with?(column_name) + end end end diff --git a/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb index aabcf20c1d..159889d3b8 100644 --- a/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb +++ b/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb @@ -31,7 +31,14 @@ module ActiveRecord end def ids - value + case value + when Relation + value.select(primary_key) + when Array + value.map { |v| convert_to_id(v) } + else + convert_to_id(value) + end end def base_class @@ -42,6 +49,10 @@ module ActiveRecord private + def primary_key + associated_table.association_primary_key(base_class) + end + def polymorphic_base_class_from_value case value when Relation @@ -53,6 +64,15 @@ module ActiveRecord value.class.base_class end end + + def convert_to_id(value) + case value + when Base + value._read_attribute(primary_key) + else + value + end + end end end end diff --git a/activerecord/lib/active_record/relation/query_attribute.rb b/activerecord/lib/active_record/relation/query_attribute.rb new file mode 100644 index 0000000000..e69319b4de --- /dev/null +++ b/activerecord/lib/active_record/relation/query_attribute.rb @@ -0,0 +1,19 @@ +require 'active_record/attribute' + +module ActiveRecord + class Relation + class QueryAttribute < Attribute + def type_cast(value) + value + end + + def value_for_database + @value_for_database ||= super + end + + def with_cast_value(value) + QueryAttribute.new(name, value, type) + end + end + end +end diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index f054e17017..7514401072 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -1,6 +1,9 @@ -require 'active_support/core_ext/array/wrap' -require 'active_support/core_ext/string/filters' +require "active_record/relation/from_clause" +require "active_record/relation/query_attribute" +require "active_record/relation/where_clause" +require "active_record/relation/where_clause_factory" require 'active_model/forbidden_attributes_protection' +require 'active_support/core_ext/string/filters' module ActiveRecord module QueryMethods @@ -39,23 +42,10 @@ module ActiveRecord # User.where.not(name: "Jon", role: "admin") # # SELECT * FROM users WHERE name != 'Jon' AND role != 'admin' def not(opts, *rest) - where_value = @scope.send(:build_where, opts, rest).map do |rel| - case rel - when NilClass - raise ArgumentError, 'Invalid argument for .where.not(), got nil.' - when Arel::Nodes::In - Arel::Nodes::NotIn.new(rel.left, rel.right) - when Arel::Nodes::Equality - Arel::Nodes::NotEqual.new(rel.left, rel.right) - when String - Arel::Nodes::Not.new(Arel::Nodes::SqlLiteral.new(rel)) - else - Arel::Nodes::Not.new(rel) - end - end + where_clause = @scope.send(:where_clause_factory).build(opts, rest) @scope.references!(PredicateBuilder.references(opts)) if Hash === opts - @scope.where_values += where_value + @scope.where_clause += where_clause.invert @scope end end @@ -90,6 +80,23 @@ module ActiveRecord CODE end + Relation::CLAUSE_METHODS.each do |name| + class_eval <<-CODE, __FILE__, __LINE__ + 1 + def #{name}_clause # def where_clause + @values[:#{name}] || new_#{name}_clause # @values[:where] || new_where_clause + end # end + # + def #{name}_clause=(value) # def where_clause=(value) + assert_mutability! # assert_mutability! + @values[:#{name}] = value # @values[:where] = value + end # end + CODE + end + + def bound_attributes + from_clause.binds + arel.bind_values + where_clause.binds + having_clause.binds + end + def create_with_value # :nodoc: @values[:create_with] || {} end @@ -392,9 +399,8 @@ module ActiveRecord raise ArgumentError, "Hash arguments in .unscope(*args) must have :where as the key." end - Array(target_value).each do |val| - where_unscoping(val) - end + target_values = Array(target_value).map(&:to_s) + self.where_clause = where_clause.except(*target_values) end else raise ArgumentError, "Unrecognized scoping: #{args.inspect}. Use .unscope(where: :attribute_name) or .unscope(:order), for example." @@ -425,15 +431,6 @@ module ActiveRecord self end - def bind(value) # :nodoc: - spawn.bind!(value) - end - - def bind!(value) # :nodoc: - self.bind_values += [value] - self - end - # Returns a new relation, which is the result of filtering the current relation # according to the conditions in the arguments. # @@ -569,7 +566,7 @@ module ActiveRecord references!(PredicateBuilder.references(opts)) end - self.where_values += build_where(opts, rest) + self.where_clause += where_clause_factory.build(opts, rest) self end @@ -585,6 +582,37 @@ module ActiveRecord unscope(where: conditions.keys).where(conditions) end + # Returns a new relation, which is the logical union of this relation and the one passed as an + # argument. + # + # The two relations must be structurally compatible: they must be scoping the same model, and + # they must differ only by +where+ (if no +group+ has been defined) or +having+ (if a +group+ is + # present). Neither relation may have a +limit+, +offset+, or +uniq+ set. + # + # Post.where("id = 1").or(Post.where("id = 2")) + # # SELECT `posts`.* FROM `posts` WHERE (('id = 1' OR 'id = 2')) + # + def or(other) + spawn.or!(other) + end + + def or!(other) # :nodoc: + unless structurally_compatible_for_or?(other) + raise ArgumentError, 'Relation passed to #or must be structurally compatible' + end + + self.where_clause = self.where_clause.or(other.where_clause) + self.having_clause = self.having_clause.or(other.having_clause) + + self + end + + private def structurally_compatible_for_or?(other) # :nodoc: + Relation::SINGLE_VALUE_METHODS.all? { |m| send("#{m}_value") == other.send("#{m}_value") } && + (Relation::MULTI_VALUE_METHODS - [:extending]).all? { |m| send("#{m}_values") == other.send("#{m}_values") } && + (Relation::CLAUSE_METHODS - [:having, :where]).all? { |m| send("#{m}_clause") != other.send("#{m}_clause") } + end + # Allows to specify a HAVING clause. Note that you can't use HAVING # without also specifying a GROUP clause. # @@ -596,7 +624,7 @@ module ActiveRecord def having!(opts, *rest) # :nodoc: references!(PredicateBuilder.references(opts)) if Hash === opts - self.having_values += build_where(opts, rest) + self.having_clause += having_clause_factory.build(opts, rest) self end @@ -744,10 +772,7 @@ module ActiveRecord end def from!(value, subquery_name = nil) # :nodoc: - self.from_value = [value, subquery_name] - if value.is_a? Relation - self.bind_values = value.arel.bind_values + value.bind_values + bind_values - end + self.from_clause = Relation::FromClause.new(value, subquery_name) self end @@ -858,21 +883,18 @@ module ActiveRecord build_joins(arel, joins_values.flatten) unless joins_values.empty? - collapse_wheres(arel, (where_values - [''])) #TODO: Add uniq with real value comparison / ignore uniqs that have binds - - arel.having(*having_values.uniq.reject(&:blank?)) unless having_values.empty? - + arel.where(where_clause.ast) unless where_clause.empty? + arel.having(having_clause.ast) unless having_clause.empty? arel.take(connection.sanitize_limit(limit_value)) if limit_value arel.skip(offset_value.to_i) if offset_value - - arel.group(*group_values.uniq.reject(&:blank?)) unless group_values.empty? + arel.group(*arel_columns(group_values.uniq.reject(&:blank?))) unless group_values.empty? build_order(arel) - build_select(arel, select_values.uniq) + build_select(arel) arel.distinct(distinct_value) - arel.from(build_from) if from_value + arel.from(build_from) unless from_clause.empty? arel.lock(lock_value) if lock_value arel @@ -883,114 +905,24 @@ module ActiveRecord raise ArgumentError, "Called unscope() with invalid unscoping argument ':#{scope}'. Valid arguments are :#{VALID_UNSCOPING_VALUES.to_a.join(", :")}." end - single_val_method = Relation::SINGLE_VALUE_METHODS.include?(scope) - unscope_code = "#{scope}_value#{'s' unless single_val_method}=" + clause_method = Relation::CLAUSE_METHODS.include?(scope) + multi_val_method = Relation::MULTI_VALUE_METHODS.include?(scope) + if clause_method + unscope_code = "#{scope}_clause=" + else + unscope_code = "#{scope}_value#{'s' if multi_val_method}=" + end case scope when :order result = [] - when :where - self.bind_values = [] else - result = [] unless single_val_method + result = [] if multi_val_method end self.send(unscope_code, result) end - def where_unscoping(target_value) - target_value = target_value.to_s - - where_values.reject! do |rel| - case rel - when Arel::Nodes::Between, Arel::Nodes::In, Arel::Nodes::NotIn, Arel::Nodes::Equality, Arel::Nodes::NotEqual, Arel::Nodes::LessThanOrEqual, Arel::Nodes::GreaterThanOrEqual - subrelation = (rel.left.kind_of?(Arel::Attributes::Attribute) ? rel.left : rel.right) - subrelation.name.to_s == target_value - end - end - - bind_values.reject! { |col,_| col.name == target_value } - end - - def custom_join_ast(table, joins) - joins = joins.reject(&:blank?) - - return [] if joins.empty? - - joins.map! do |join| - case join - when Array - join = Arel.sql(join.join(' ')) if array_of_strings?(join) - when String - join = Arel.sql(join) - end - table.create_string_join(join) - end - end - - def collapse_wheres(arel, wheres) - predicates = wheres.map do |where| - next where if ::Arel::Nodes::Equality === where - where = Arel.sql(where) if String === where - Arel::Nodes::Grouping.new(where) - end - - arel.where(Arel::Nodes::And.new(predicates)) if predicates.present? - end - - def build_where(opts, other = []) - case opts - when String, Array - [@klass.send(:sanitize_sql, other.empty? ? opts : ([opts] + other))] - when Hash - opts = predicate_builder.resolve_column_aliases(opts) - - tmp_opts, bind_values = create_binds(opts) - self.bind_values += bind_values - - attributes = @klass.send(:expand_hash_conditions_for_aggregates, tmp_opts) - add_relations_to_bind_values(attributes) - - predicate_builder.build_from_hash(attributes) - else - [opts] - end - end - - def create_binds(opts) - bindable, non_binds = opts.partition do |column, value| - case value - when String, Integer, ActiveRecord::StatementCache::Substitute - @klass.columns_hash.include? column.to_s - else - false - end - end - - association_binds, non_binds = non_binds.partition do |column, value| - value.is_a?(Hash) && association_for_table(column) - end - - new_opts = {} - binds = [] - - bindable.each do |(column,value)| - binds.push [@klass.columns_hash[column.to_s], value] - new_opts[column] = connection.substitute_at(column) - end - - association_binds.each do |(column, value)| - association_relation = association_for_table(column).klass.send(:relation) - association_new_opts, association_bind = association_relation.send(:create_binds, value) - new_opts[column] = association_new_opts - binds += association_bind - end - - non_binds.each { |column,value| new_opts[column] = value } - - [new_opts, binds] - end - def association_for_table(table_name) table_name = table_name.to_s @klass._reflect_on_association(table_name) || @@ -998,7 +930,8 @@ module ActiveRecord end def build_from - opts, name = from_value + opts = from_clause.value + name = from_clause.name case opts when Relation name ||= 'subquery' @@ -1023,13 +956,14 @@ module ActiveRecord raise 'unknown class: %s' % join.class.name end end + 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_association_joins = buckets[:stashed_join] + join_nodes = buckets[:join_node].uniq + string_joins = buckets[:string_join].map(&:strip).uniq - join_list = join_nodes + custom_join_ast(manager, string_joins) + join_list = join_nodes + convert_join_strings_to_ast(manager, string_joins) join_dependency = ActiveRecord::Associations::JoinDependency.new( @klass, @@ -1049,22 +983,31 @@ module ActiveRecord manager end - def build_select(arel, selects) - if !selects.empty? - expanded_select = selects.map do |field| - if (Symbol === field || String === field) && columns_hash.key?(field.to_s) - arel_table[field] - else - field - end - end + def convert_join_strings_to_ast(table, joins) + joins + .flatten + .reject(&:blank?) + .map { |join| table.create_string_join(Arel.sql(join)) } + end - arel.project(*expanded_select) + def build_select(arel) + if select_values.any? + arel.project(*arel_columns(select_values.uniq)) else arel.project(@klass.arel_table[Arel.star]) end end + def arel_columns(columns) + columns.map do |field| + if (Symbol === field || String === field) && columns_hash.key?(field.to_s) + arel_table[field] + else + field + end + end + end + def reverse_sql_order(order_query) order_query = ["#{quoted_table_name}.#{quoted_primary_key} ASC"] if order_query.empty? @@ -1083,10 +1026,6 @@ module ActiveRecord end end - def array_of_strings?(o) - o.is_a?(Array) && o.all? { |obj| obj.is_a?(String) } - end - def build_order(arel) orders = order_values.uniq orders.reject!(&:blank?) @@ -1154,18 +1093,18 @@ module ActiveRecord end end - # This function is recursive just for better readablity. - # #where argument doesn't support more than one level nested hash in real world. - def add_relations_to_bind_values(attributes) - if attributes.is_a?(Hash) - attributes.each_value do |value| - if value.is_a?(ActiveRecord::Relation) - self.bind_values += value.bind_values - else - add_relations_to_bind_values(value) - end - end - end + def new_where_clause + Relation::WhereClause.empty + end + alias new_having_clause new_where_clause + + def where_clause_factory + @where_clause_factory ||= Relation::WhereClauseFactory.new(klass, predicate_builder) + end + alias having_clause_factory where_clause_factory + + def new_from_clause + Relation::FromClause.empty end end end diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb index 01bddea6c9..70da37fa84 100644 --- a/activerecord/lib/active_record/relation/spawn_methods.rb +++ b/activerecord/lib/active_record/relation/spawn_methods.rb @@ -32,7 +32,7 @@ module ActiveRecord elsif other spawn.merge!(other) else - self + raise ArgumentError, "invalid argument: #{other.inspect}." end end @@ -58,9 +58,6 @@ module ActiveRecord # Post.order('id asc').only(:where) # discards the order condition # Post.order('id asc').only(:where, :order) # uses the specified order def only(*onlies) - if onlies.any? { |o| o == :where } - onlies << :bind - end relation_with values.slice(*onlies) end diff --git a/activerecord/lib/active_record/relation/where_clause.rb b/activerecord/lib/active_record/relation/where_clause.rb new file mode 100644 index 0000000000..f9b9e640ec --- /dev/null +++ b/activerecord/lib/active_record/relation/where_clause.rb @@ -0,0 +1,173 @@ +module ActiveRecord + class Relation + class WhereClause # :nodoc: + attr_reader :binds + + delegate :any?, :empty?, to: :predicates + + def initialize(predicates, binds) + @predicates = predicates + @binds = binds + end + + def +(other) + WhereClause.new( + predicates + other.predicates, + binds + other.binds, + ) + end + + def merge(other) + WhereClause.new( + predicates_unreferenced_by(other) + other.predicates, + non_conflicting_binds(other) + other.binds, + ) + end + + def except(*columns) + WhereClause.new( + predicates_except(columns), + binds_except(columns), + ) + end + + def or(other) + if empty? + self + elsif other.empty? + other + else + WhereClause.new( + [ast.or(other.ast)], + binds + other.binds + ) + end + end + + def to_h(table_name = nil) + equalities = predicates.grep(Arel::Nodes::Equality) + if table_name + equalities = equalities.select do |node| + node.left.relation.name == table_name + end + end + + binds = self.binds.map { |attr| [attr.name, attr.value] }.to_h + + equalities.map { |node| + name = node.left.name + [name, binds.fetch(name.to_s) { + case node.right + when Array then node.right.map(&:val) + when Arel::Nodes::Casted, Arel::Nodes::Quoted + node.right.val + end + }] + }.to_h + end + + def ast + Arel::Nodes::And.new(predicates_with_wrapped_sql_literals) + end + + def ==(other) + other.is_a?(WhereClause) && + predicates == other.predicates && + binds == other.binds + end + + def invert + WhereClause.new(inverted_predicates, binds) + end + + def self.empty + new([], []) + end + + protected + + attr_reader :predicates + + def referenced_columns + @referenced_columns ||= begin + equality_nodes = predicates.select { |n| equality_node?(n) } + Set.new(equality_nodes, &:left) + end + end + + private + + def predicates_unreferenced_by(other) + predicates.reject do |n| + equality_node?(n) && other.referenced_columns.include?(n.left) + end + end + + def equality_node?(node) + node.respond_to?(:operator) && node.operator == :== + end + + def non_conflicting_binds(other) + conflicts = referenced_columns & other.referenced_columns + conflicts.map! { |node| node.name.to_s } + binds.reject { |attr| conflicts.include?(attr.name) } + end + + def inverted_predicates + predicates.map { |node| invert_predicate(node) } + end + + def invert_predicate(node) + case node + when NilClass + raise ArgumentError, 'Invalid argument for .where.not(), got nil.' + when Arel::Nodes::In + Arel::Nodes::NotIn.new(node.left, node.right) + when Arel::Nodes::Equality + Arel::Nodes::NotEqual.new(node.left, node.right) + when String + Arel::Nodes::Not.new(Arel::Nodes::SqlLiteral.new(node)) + else + Arel::Nodes::Not.new(node) + end + end + + def predicates_except(columns) + predicates.reject do |node| + case node + when Arel::Nodes::Between, Arel::Nodes::In, Arel::Nodes::NotIn, Arel::Nodes::Equality, Arel::Nodes::NotEqual, Arel::Nodes::LessThanOrEqual, Arel::Nodes::GreaterThanOrEqual + subrelation = (node.left.kind_of?(Arel::Attributes::Attribute) ? node.left : node.right) + columns.include?(subrelation.name.to_s) + end + end + end + + def binds_except(columns) + binds.reject do |attr| + columns.include?(attr.name) + end + end + + def predicates_with_wrapped_sql_literals + non_empty_predicates.map do |node| + if Arel::Nodes::Equality === node + node + else + wrap_sql_literal(node) + end + end + end + + def non_empty_predicates + predicates - [''] + end + + def wrap_sql_literal(node) + if ::String === node + node = Arel.sql(node) + end + Arel::Nodes::Grouping.new(node) + end + end + end +end diff --git a/activerecord/lib/active_record/relation/where_clause_factory.rb b/activerecord/lib/active_record/relation/where_clause_factory.rb new file mode 100644 index 0000000000..0430922be3 --- /dev/null +++ b/activerecord/lib/active_record/relation/where_clause_factory.rb @@ -0,0 +1,34 @@ +module ActiveRecord + class Relation + class WhereClauseFactory + def initialize(klass, predicate_builder) + @klass = klass + @predicate_builder = predicate_builder + end + + def build(opts, other) + binds = [] + + case opts + when String, Array + parts = [klass.send(:sanitize_sql, other.empty? ? opts : ([opts] + other))] + when Hash + attributes = predicate_builder.resolve_column_aliases(opts) + attributes = klass.send(:expand_hash_conditions_for_aggregates, attributes) + + attributes, binds = predicate_builder.create_binds(attributes) + + parts = predicate_builder.build_from_hash(attributes) + else + parts = [opts] + end + + WhereClause.new(parts, binds) + end + + protected + + attr_reader :klass, :predicate_builder + end + end +end diff --git a/activerecord/lib/active_record/result.rb b/activerecord/lib/active_record/result.rb index 3a3e65ef32..500c478e65 100644 --- a/activerecord/lib/active_record/result.rb +++ b/activerecord/lib/active_record/result.rb @@ -81,7 +81,7 @@ module ActiveRecord 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.type_cast_from_database(value) } + types.zip(values).map { |type, value| type.deserialize(value) } end columns.one? ? result.map!(&:first) : result diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb index 768a72a947..c7f55ebaa1 100644 --- a/activerecord/lib/active_record/sanitization.rb +++ b/activerecord/lib/active_record/sanitization.rb @@ -3,14 +3,11 @@ module ActiveRecord extend ActiveSupport::Concern module ClassMethods - def quote_value(value, column) #:nodoc: - connection.quote(value, column) - end - # Used to sanitize objects before they're used in an SQL SELECT statement. Delegates to <tt>connection.quote</tt>. def sanitize(object) #:nodoc: connection.quote(object) end + alias_method :quote_value, :sanitize protected @@ -78,7 +75,7 @@ module ActiveRecord def sanitize_sql_hash_for_assignment(attrs, table) c = connection attrs.map do |attr, value| - value = type_for_attribute(attr.to_s).type_cast_for_database(value) + value = type_for_attribute(attr.to_s).serialize(value) "#{c.quote_table_name_for_assignment(table, attr)} = #{c.quote(value)}" end.join(', ') end @@ -156,7 +153,7 @@ module ActiveRecord # TODO: Deprecate this def quoted_id - self.class.quote_value(id, column_for_attribute(self.class.primary_key)) + self.class.quote_value(@attributes[self.class.primary_key].value_for_database) end end end diff --git a/activerecord/lib/active_record/scoping.rb b/activerecord/lib/active_record/scoping.rb index 3e43591672..fca4f1c9d3 100644 --- a/activerecord/lib/active_record/scoping.rb +++ b/activerecord/lib/active_record/scoping.rb @@ -11,11 +11,11 @@ module ActiveRecord module ClassMethods def current_scope #:nodoc: - ScopeRegistry.value_for(:current_scope, base_class.to_s) + ScopeRegistry.value_for(:current_scope, self.to_s) end def current_scope=(scope) #:nodoc: - ScopeRegistry.set_value_for(:current_scope, base_class.to_s, scope) + ScopeRegistry.set_value_for(:current_scope, self.to_s, scope) end end diff --git a/activerecord/lib/active_record/scoping/named.rb b/activerecord/lib/active_record/scoping/named.rb index 35420e6551..43c7b1c574 100644 --- a/activerecord/lib/active_record/scoping/named.rb +++ b/activerecord/lib/active_record/scoping/named.rb @@ -30,7 +30,13 @@ module ActiveRecord end def default_scoped # :nodoc: - relation.merge(build_default_scope) + scope = build_default_scope + + if scope + relation.spawn.merge!(scope) + else + relation + end end # Collects attributes from scopes that should be applied when creating @@ -151,11 +157,20 @@ module ActiveRecord extension = Module.new(&block) if block - singleton_class.send(:define_method, name) do |*args| - scope = all.scoping { body.call(*args) } - scope = scope.extending(extension) if extension + if body.respond_to?(:to_proc) + singleton_class.send(:define_method, name) do |*args| + scope = all.scoping { instance_exec(*args, &body) } + scope = scope.extending(extension) if extension + + scope || all + end + else + singleton_class.send(:define_method, name) do |*args| + scope = all.scoping { body.call(*args) } + scope = scope.extending(extension) if extension - scope || all + scope || all + end end end end diff --git a/activerecord/lib/active_record/secure_token.rb b/activerecord/lib/active_record/secure_token.rb index 23d4292cbb..0990f815a7 100644 --- a/activerecord/lib/active_record/secure_token.rb +++ b/activerecord/lib/active_record/secure_token.rb @@ -5,7 +5,7 @@ module ActiveRecord module ClassMethods # Example using has_secure_token # - # # Schema: User(toke:string, auth_token:string) + # # Schema: User(token:string, auth_token:string) # class User < ActiveRecord::Base # has_secure_token # has_secure_token :auth_token @@ -13,37 +13,26 @@ module ActiveRecord # # user = User.new # user.save - # user.token # => "44539a6a59835a4ee9d7b112" - # user.auth_token # => "e2426a93718d1817a43abbaa" + # user.token # => "4kUgL2pdQMSCQtjE" + # user.auth_token # => "77TMHrHJFvFDwodq8w7Ev2m7" # user.regenerate_token # => true # user.regenerate_auth_token # => true # - # SecureRandom is used to generate the 24-character unique token, so collisions are highly unlikely. - # We'll check to see if the generated token has been used already using #exists?, and retry up to 10 - # times to find another unused token. After that a RuntimeError is raised if the problem persists. + # SecureRandom::base58 is used to generate the 24-character unique token, so collisions are highly unlikely. # # Note that it's still possible to generate a race condition in the database in the same way that # validates_presence_of can. You're encouraged to add a unique index in the database to deal with # this even more unlikely scenario. def has_secure_token(attribute = :token) - # Load securerandom only when has_secure_key is used. - require 'securerandom' - define_method("regenerate_#{attribute}") { update! attribute => self.class.generate_unique_secure_token(attribute) } - before_create { self.send("#{attribute}=", self.class.generate_unique_secure_token(attribute)) } + # Load securerandom only when has_secure_token is used. + require 'active_support/core_ext/securerandom' + define_method("regenerate_#{attribute}") { update! attribute => self.class.generate_unique_secure_token } + before_create { self.send("#{attribute}=", self.class.generate_unique_secure_token) unless self.send("#{attribute}?")} end - def generate_unique_secure_token(attribute) - 10.times do |i| - SecureRandom.hex(12).tap do |token| - if exists?(attribute => token) - raise "Couldn't generate a unique token in 10 attempts!" if i == 9 - else - return token - end - end - end + def generate_unique_secure_token + SecureRandom.base58(24) end end end end - diff --git a/activerecord/lib/active_record/serializers/xml_serializer.rb b/activerecord/lib/active_record/serializers/xml_serializer.rb index c2484d02ed..89b7e0be82 100644 --- a/activerecord/lib/active_record/serializers/xml_serializer.rb +++ b/activerecord/lib/active_record/serializers/xml_serializer.rb @@ -180,9 +180,9 @@ module ActiveRecord #:nodoc: class Attribute < ActiveModel::Serializers::Xml::Serializer::Attribute #:nodoc: def compute_type klass = @serializable.class - column = klass.columns_hash[name] || Type::Value.new + cast_type = klass.type_for_attribute(name) - type = ActiveSupport::XmlMini::TYPE_NAMES[value.class.name] || column.type + type = ActiveSupport::XmlMini::TYPE_NAMES[value.class.name] || cast_type.type { :text => :string, :time => :datetime }[type] || type diff --git a/activerecord/lib/active_record/statement_cache.rb b/activerecord/lib/active_record/statement_cache.rb index 192a19f05d..95986c820c 100644 --- a/activerecord/lib/active_record/statement_cache.rb +++ b/activerecord/lib/active_record/statement_cache.rb @@ -47,8 +47,8 @@ module ActiveRecord def sql_for(binds, connection) val = @values.dup - binds = binds.dup - @indexes.each { |i| val[i] = connection.quote(*binds.shift.reverse) } + binds = connection.prepare_binds_for_database(binds) + @indexes.each { |i| val[i] = connection.quote(binds.shift) } val.join end end @@ -67,21 +67,21 @@ module ActiveRecord end class BindMap # :nodoc: - def initialize(bind_values) + def initialize(bound_attributes) @indexes = [] - @bind_values = bind_values + @bound_attributes = bound_attributes - bind_values.each_with_index do |(_, value), i| - if Substitute === value + bound_attributes.each_with_index do |attr, i| + if Substitute === attr.value @indexes << i end end end def bind(values) - bvs = @bind_values.map(&:dup) - @indexes.each_with_index { |offset,i| bvs[offset][1] = values[i] } - bvs + bas = @bound_attributes.dup + @indexes.each_with_index { |offset,i| bas[offset] = bas[offset].with_cast_value(values[i]) } + bas end end @@ -89,7 +89,7 @@ module ActiveRecord def self.create(connection, block = Proc.new) relation = block.call Params.new - bind_map = BindMap.new relation.bind_values + bind_map = BindMap.new relation.bound_attributes query_builder = connection.cacheable_query relation.arel new query_builder, bind_map end diff --git a/activerecord/lib/active_record/table_metadata.rb b/activerecord/lib/active_record/table_metadata.rb index 11e33e8dfe..3dd6321a97 100644 --- a/activerecord/lib/active_record/table_metadata.rb +++ b/activerecord/lib/active_record/table_metadata.rb @@ -1,6 +1,7 @@ module ActiveRecord class TableMetadata # :nodoc: delegate :foreign_type, :foreign_key, to: :association, prefix: true + delegate :association_primary_key, to: :association def initialize(klass, arel_table, association = nil) @klass = klass @@ -22,6 +23,14 @@ module ActiveRecord arel_table[column_name] end + def type(column_name) + if klass + klass.type_for_attribute(column_name.to_s) + else + Type::Value.new + end + end + def associated_with?(association_name) klass && klass._reflect_on_association(association_name) end @@ -34,7 +43,7 @@ module ActiveRecord association_klass = association.klass arel_table = association_klass.arel_table else - type_caster = TypeCaster::Connection.new(klass.connection, table_name) + type_caster = TypeCaster::Connection.new(klass, table_name) association_klass = nil arel_table = Arel::Table.new(table_name, type_caster: type_caster) end diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index 9cef50029b..dd405c7796 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -300,7 +300,7 @@ module ActiveRecord # # Ensure that it is not called if the object was never persisted (failed create), # but call it after the commit of a destroyed object. - def committed!(should_run_callbacks = true) #:nodoc: + def committed!(should_run_callbacks: true) #:nodoc: _run_commit_callbacks if should_run_callbacks && destroyed? || persisted? ensure force_clear_transaction_record_state @@ -308,7 +308,7 @@ module ActiveRecord # Call the +after_rollback+ callbacks. The +force_restore_state+ argument indicates if the record # state should be rolled back to the beginning or just to the last savepoint. - def rolledback!(force_restore_state = false, should_run_callbacks = true) #:nodoc: + def rolledback!(force_restore_state: false, should_run_callbacks: true) #:nodoc: _run_rollback_callbacks if should_run_callbacks ensure restore_transaction_record_state(force_restore_state) @@ -318,9 +318,12 @@ module ActiveRecord # Add the record to the current transaction so that the +after_rollback+ and +after_commit+ callbacks # can be called. def add_to_transaction - if self.class.connection.add_transaction_record(self) - remember_transaction_record_state + if has_transactional_callbacks? + self.class.connection.add_transaction_record(self) + else + set_transaction_state(self.class.connection.transaction_state) end + remember_transaction_record_state end # Executes +method+ within a transaction and captures its return value as a @@ -378,7 +381,10 @@ module ActiveRecord thaw unless restore_state[:frozen?] @new_record = restore_state[:new_record] @destroyed = restore_state[:destroyed] - write_attribute(self.class.primary_key, restore_state[:id]) if self.class.primary_key + pk = self.class.primary_key + if pk && read_attribute(pk) != restore_state[:id] + write_attribute(pk, restore_state[:id]) + end end end end diff --git a/activerecord/lib/active_record/type.rb b/activerecord/lib/active_record/type.rb index 250e8d5b23..2c0cda69d0 100644 --- a/activerecord/lib/active_record/type.rb +++ b/activerecord/lib/active_record/type.rb @@ -1,7 +1,4 @@ -require 'active_record/type/decorator' -require 'active_record/type/mutable' -require 'active_record/type/numeric' -require 'active_record/type/time_value' +require 'active_record/type/helpers' require 'active_record/type/value' require 'active_record/type/big_integer' @@ -19,5 +16,51 @@ require 'active_record/type/text' require 'active_record/type/time' require 'active_record/type/unsigned_integer' +require 'active_record/type/adapter_specific_registry' require 'active_record/type/type_map' require 'active_record/type/hash_lookup_type_map' + +module ActiveRecord + module Type + @registry = AdapterSpecificRegistry.new + + class << self + attr_accessor :registry # :nodoc: + delegate :add_modifier, to: :registry + + # Add a new type to the registry, allowing it to be referenced as a + # symbol by ActiveRecord::Attributes::ClassMethods#attribute. If your + # type is only meant to be used with a specific database adapter, you can + # do so by passing +adapter: :postgresql+. If your type has the same + # name as a native type for the current adapter, an exception will be + # raised unless you specify an +:override+ option. +override: true+ will + # cause your type to be used instead of the native type. +override: + # false+ will cause the native type to be used over yours if one exists. + def register(type_name, klass = nil, **options, &block) + registry.register(type_name, klass, **options, &block) + end + + def lookup(*args, adapter: current_adapter_name, **kwargs) # :nodoc: + registry.lookup(*args, adapter: adapter, **kwargs) + end + + private + + def current_adapter_name + ActiveRecord::Base.connection.adapter_name.downcase.to_sym + end + end + + register(:big_integer, Type::BigInteger, override: false) + register(:binary, Type::Binary, override: false) + register(:boolean, Type::Boolean, override: false) + register(:date, Type::Date, override: false) + register(:date_time, Type::DateTime, override: false) + register(:decimal, Type::Decimal, override: false) + register(:float, Type::Float, override: false) + register(:integer, Type::Integer, override: false) + register(:string, Type::String, override: false) + register(:text, Type::Text, override: false) + register(:time, Type::Time, override: false) + end +end diff --git a/activerecord/lib/active_record/type/adapter_specific_registry.rb b/activerecord/lib/active_record/type/adapter_specific_registry.rb new file mode 100644 index 0000000000..5f71b3cb94 --- /dev/null +++ b/activerecord/lib/active_record/type/adapter_specific_registry.rb @@ -0,0 +1,142 @@ +module ActiveRecord + # :stopdoc: + module Type + class AdapterSpecificRegistry + def initialize + @registrations = [] + end + + def register(type_name, klass = nil, **options, &block) + block ||= proc { |_, *args| klass.new(*args) } + registrations << Registration.new(type_name, block, **options) + end + + def lookup(symbol, *args) + registration = registrations + .select { |r| r.matches?(symbol, *args) } + .max + + if registration + registration.call(self, symbol, *args) + else + raise ArgumentError, "Unknown type #{symbol.inspect}" + end + end + + def add_modifier(options, klass, **args) + registrations << DecorationRegistration.new(options, klass, **args) + end + + protected + + attr_reader :registrations + end + + class Registration + def initialize(name, block, adapter: nil, override: nil) + @name = name + @block = block + @adapter = adapter + @override = override + end + + def call(_registry, *args, adapter: nil, **kwargs) + if kwargs.any? # https://bugs.ruby-lang.org/issues/10856 + block.call(*args, **kwargs) + else + block.call(*args) + end + end + + def matches?(type_name, *args, **kwargs) + type_name == name && matches_adapter?(**kwargs) + end + + def <=>(other) + if conflicts_with?(other) + raise TypeConflictError.new("Type #{name} was registered for all + adapters, but shadows a native type with + the same name for #{other.adapter}".squish) + end + priority <=> other.priority + end + + protected + + attr_reader :name, :block, :adapter, :override + + def priority + result = 0 + if adapter + result |= 1 + end + if override + result |= 2 + end + result + end + + def priority_except_adapter + priority & 0b111111100 + end + + private + + def matches_adapter?(adapter: nil, **) + (self.adapter.nil? || adapter == self.adapter) + end + + def conflicts_with?(other) + same_priority_except_adapter?(other) && + has_adapter_conflict?(other) + end + + def same_priority_except_adapter?(other) + priority_except_adapter == other.priority_except_adapter + end + + def has_adapter_conflict?(other) + (override.nil? && other.adapter) || + (adapter && other.override.nil?) + end + end + + class DecorationRegistration < Registration + def initialize(options, klass, adapter: nil) + @options = options + @klass = klass + @adapter = adapter + end + + def call(registry, *args, **kwargs) + subtype = registry.lookup(*args, **kwargs.except(*options.keys)) + klass.new(subtype) + end + + def matches?(*args, **kwargs) + matches_adapter?(**kwargs) && matches_options?(**kwargs) + end + + def priority + super | 4 + end + + protected + + attr_reader :options, :klass + + private + + def matches_options?(**kwargs) + options.all? do |key, value| + kwargs[key] == value + end + end + end + end + + class TypeConflictError < StandardError + end + + # :startdoc: +end diff --git a/activerecord/lib/active_record/type/binary.rb b/activerecord/lib/active_record/type/binary.rb index 005a48ef0d..0baf8c63ad 100644 --- a/activerecord/lib/active_record/type/binary.rb +++ b/activerecord/lib/active_record/type/binary.rb @@ -9,7 +9,7 @@ module ActiveRecord true end - def type_cast(value) + def cast(value) if value.is_a?(Data) value.to_s else @@ -17,13 +17,13 @@ module ActiveRecord end end - def type_cast_for_database(value) + def serialize(value) return if value.nil? Data.new(super) end def changed_in_place?(raw_old_value, value) - old_value = type_cast_from_database(raw_old_value) + old_value = deserialize(raw_old_value) old_value != value end diff --git a/activerecord/lib/active_record/type/date.rb b/activerecord/lib/active_record/type/date.rb index d90a6069b7..3ceab59ebb 100644 --- a/activerecord/lib/active_record/type/date.rb +++ b/activerecord/lib/active_record/type/date.rb @@ -1,14 +1,12 @@ module ActiveRecord module Type class Date < Value # :nodoc: + include Helpers::AcceptsMultiparameterTime.new + def type :date end - def klass - ::Date - end - def type_cast_for_schema(value) "'#{value.to_s(:db)}'" end @@ -41,6 +39,11 @@ module ActiveRecord ::Date.new(year, mon, mday) rescue nil end end + + def value_from_multiparameter_assignment(*) + time = super + time && time.to_date + end end end end diff --git a/activerecord/lib/active_record/type/date_time.rb b/activerecord/lib/active_record/type/date_time.rb index 0a737815bc..05d2af3808 100644 --- a/activerecord/lib/active_record/type/date_time.rb +++ b/activerecord/lib/active_record/type/date_time.rb @@ -1,24 +1,31 @@ module ActiveRecord module Type class DateTime < Value # :nodoc: - include TimeValue + include Helpers::TimeValue + include Helpers::AcceptsMultiparameterTime.new( + defaults: { 4 => 0, 5 => 0 } + ) def type :datetime end - def type_cast_for_database(value) - zone_conversion_method = ActiveRecord::Base.default_timezone == :utc ? :getutc : :getlocal + def serialize(value) + if precision && value.respond_to?(:usec) + number_of_insignificant_digits = 6 - precision + round_power = 10 ** number_of_insignificant_digits + value = value.change(usec: value.usec / round_power * round_power) + end if value.acts_like?(:time) + zone_conversion_method = ActiveRecord::Base.default_timezone == :utc ? :getutc : :getlocal + if value.respond_to?(zone_conversion_method) - value.send(zone_conversion_method) - else - value + value = value.send(zone_conversion_method) end - else - super end + + value end private @@ -42,6 +49,14 @@ module ActiveRecord new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction, :offset)) end + + def value_from_multiparameter_assignment(values_hash) + missing_parameter = (1..3).detect { |key| !values_hash.key?(key) } + if missing_parameter + raise ArgumentError, missing_parameter + end + super + end end end end diff --git a/activerecord/lib/active_record/type/decimal.rb b/activerecord/lib/active_record/type/decimal.rb index 7b2bee2c42..867b5f75c7 100644 --- a/activerecord/lib/active_record/type/decimal.rb +++ b/activerecord/lib/active_record/type/decimal.rb @@ -1,7 +1,7 @@ module ActiveRecord module Type class Decimal < Value # :nodoc: - include Numeric + include Helpers::Numeric def type :decimal diff --git a/activerecord/lib/active_record/type/decorator.rb b/activerecord/lib/active_record/type/decorator.rb deleted file mode 100644 index 9fce38ea44..0000000000 --- a/activerecord/lib/active_record/type/decorator.rb +++ /dev/null @@ -1,14 +0,0 @@ -module ActiveRecord - module Type - module Decorator # :nodoc: - def init_with(coder) - @subtype = coder['subtype'] - __setobj__(@subtype) - end - - def encode_with(coder) - coder['subtype'] = __getobj__ - end - end - end -end diff --git a/activerecord/lib/active_record/type/float.rb b/activerecord/lib/active_record/type/float.rb index 42eb44b9a9..d88482b85d 100644 --- a/activerecord/lib/active_record/type/float.rb +++ b/activerecord/lib/active_record/type/float.rb @@ -1,18 +1,24 @@ module ActiveRecord module Type class Float < Value # :nodoc: - include Numeric + include Helpers::Numeric def type :float end - alias type_cast_for_database type_cast + alias serialize cast private def cast_value(value) - value.to_f + case value + when ::Float then value + when "Infinity" then ::Float::INFINITY + when "-Infinity" then -::Float::INFINITY + when "NaN" then ::Float::NAN + else value.to_f + end end end end diff --git a/activerecord/lib/active_record/type/helpers.rb b/activerecord/lib/active_record/type/helpers.rb new file mode 100644 index 0000000000..634d417d13 --- /dev/null +++ b/activerecord/lib/active_record/type/helpers.rb @@ -0,0 +1,4 @@ +require 'active_record/type/helpers/accepts_multiparameter_time' +require 'active_record/type/helpers/numeric' +require 'active_record/type/helpers/mutable' +require 'active_record/type/helpers/time_value' diff --git a/activerecord/lib/active_record/type/helpers/accepts_multiparameter_time.rb b/activerecord/lib/active_record/type/helpers/accepts_multiparameter_time.rb new file mode 100644 index 0000000000..be571fc1c7 --- /dev/null +++ b/activerecord/lib/active_record/type/helpers/accepts_multiparameter_time.rb @@ -0,0 +1,30 @@ +module ActiveRecord + module Type + module Helpers + class AcceptsMultiparameterTime < Module # :nodoc: + def initialize(defaults: {}) + define_method(:cast) do |value| + if value.is_a?(Hash) + value_from_multiparameter_assignment(value) + else + super(value) + end + end + + define_method(:value_from_multiparameter_assignment) do |values_hash| + defaults.each do |k, v| + values_hash[k] ||= v + end + return unless values_hash[1] && values_hash[2] && values_hash[3] + values = values_hash.sort.map(&:last) + ::Time.send( + ActiveRecord::Base.default_timezone, + *values + ) + end + private :value_from_multiparameter_assignment + end + end + end + end +end diff --git a/activerecord/lib/active_record/type/helpers/mutable.rb b/activerecord/lib/active_record/type/helpers/mutable.rb new file mode 100644 index 0000000000..88a9099277 --- /dev/null +++ b/activerecord/lib/active_record/type/helpers/mutable.rb @@ -0,0 +1,18 @@ +module ActiveRecord + module Type + module Helpers + module Mutable # :nodoc: + def cast(value) + deserialize(serialize(value)) + end + + # +raw_old_value+ will be the `_before_type_cast` version of the + # value (likely a string). +new_value+ will be the current, type + # cast value. + def changed_in_place?(raw_old_value, new_value) + raw_old_value != serialize(new_value) + end + end + end + end +end diff --git a/activerecord/lib/active_record/type/helpers/numeric.rb b/activerecord/lib/active_record/type/helpers/numeric.rb new file mode 100644 index 0000000000..a755a02a59 --- /dev/null +++ b/activerecord/lib/active_record/type/helpers/numeric.rb @@ -0,0 +1,34 @@ +module ActiveRecord + module Type + module Helpers + module Numeric # :nodoc: + def cast(value) + value = case value + when true then 1 + when false then 0 + when ::String then value.presence + else value + end + super(value) + end + + def changed?(old_value, _new_value, new_value_before_type_cast) # :nodoc: + super || number_to_non_number?(old_value, new_value_before_type_cast) + end + + private + + def number_to_non_number?(old_value, new_value_before_type_cast) + old_value != nil && non_numeric_string?(new_value_before_type_cast) + end + + def non_numeric_string?(value) + # 'wibble'.to_i will give zero, we want to make sure + # that we aren't marking int zero to string zero as + # changed. + value.to_s !~ /\A-?\d+\.?\d*\z/ + end + end + end + end +end diff --git a/activerecord/lib/active_record/type/helpers/time_value.rb b/activerecord/lib/active_record/type/helpers/time_value.rb new file mode 100644 index 0000000000..6e14c3a9b5 --- /dev/null +++ b/activerecord/lib/active_record/type/helpers/time_value.rb @@ -0,0 +1,40 @@ +module ActiveRecord + module Type + module Helpers + module TimeValue # :nodoc: + def type_cast_for_schema(value) + "'#{value.to_s(:db)}'" + end + + def user_input_in_time_zone(value) + value.in_time_zone + end + + private + + def new_time(year, mon, mday, hour, min, sec, microsec, offset = nil) + # Treat 0000-00-00 00:00:00 as nil. + return if year.nil? || (year == 0 && mon == 0 && mday == 0) + + if offset + time = ::Time.utc(year, mon, mday, hour, min, sec, microsec) rescue nil + return unless time + + time -= offset + Base.default_timezone == :utc ? time : time.getlocal + else + ::Time.public_send(Base.default_timezone, year, mon, mday, hour, min, sec, microsec) rescue nil + end + end + + # Doesn't handle time zones. + def fast_string_to_time(string) + if string =~ ConnectionAdapters::Column::Format::ISO_DATETIME + microsec = ($7.to_r * 1_000_000).to_i + new_time $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, microsec + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/type/integer.rb b/activerecord/lib/active_record/type/integer.rb index fc260a081a..2a1b04ac7f 100644 --- a/activerecord/lib/active_record/type/integer.rb +++ b/activerecord/lib/active_record/type/integer.rb @@ -1,7 +1,11 @@ module ActiveRecord module Type class Integer < Value # :nodoc: - include Numeric + include Helpers::Numeric + + # Column storage size in bytes. + # 4 bytes means a MySQL int or Postgres integer as opposed to smallint etc. + DEFAULT_LIMIT = 4 def initialize(*) super @@ -12,13 +16,19 @@ module ActiveRecord :integer end - alias type_cast_for_database type_cast - - def type_cast_from_database(value) + def deserialize(value) return if value.nil? value.to_i end + def serialize(value) + result = cast(value) + if result + ensure_in_range(result) + end + result + end + protected attr_reader :range @@ -30,20 +40,18 @@ module ActiveRecord when true then 1 when false then 0 else - result = value.to_i rescue nil - ensure_in_range(result) if result - result + value.to_i rescue nil end end def ensure_in_range(value) unless range.cover?(value) - raise RangeError, "#{value} is out of range for #{self.class} with limit #{limit || 4}" + raise RangeError, "#{value} is out of range for #{self.class} with limit #{limit || DEFAULT_LIMIT}" end end def max_value - limit = self.limit || 4 + limit = self.limit || DEFAULT_LIMIT 1 << (limit * 8 - 1) # 8 bits per byte with one bit for sign end diff --git a/activerecord/lib/active_record/type/mutable.rb b/activerecord/lib/active_record/type/mutable.rb deleted file mode 100644 index 066617ea59..0000000000 --- a/activerecord/lib/active_record/type/mutable.rb +++ /dev/null @@ -1,16 +0,0 @@ -module ActiveRecord - module Type - module Mutable # :nodoc: - def type_cast_from_user(value) - type_cast_from_database(type_cast_for_database(value)) - end - - # +raw_old_value+ will be the `_before_type_cast` version of the - # value (likely a string). +new_value+ will be the current, type - # cast value. - def changed_in_place?(raw_old_value, new_value) - raw_old_value != type_cast_for_database(new_value) - end - end - end -end diff --git a/activerecord/lib/active_record/type/numeric.rb b/activerecord/lib/active_record/type/numeric.rb deleted file mode 100644 index 674f996f38..0000000000 --- a/activerecord/lib/active_record/type/numeric.rb +++ /dev/null @@ -1,36 +0,0 @@ -module ActiveRecord - module Type - module Numeric # :nodoc: - def number? - true - end - - def type_cast(value) - value = case value - when true then 1 - when false then 0 - when ::String then value.presence - else value - end - super(value) - end - - def changed?(old_value, _new_value, new_value_before_type_cast) # :nodoc: - super || number_to_non_number?(old_value, new_value_before_type_cast) - end - - private - - def number_to_non_number?(old_value, new_value_before_type_cast) - old_value != nil && non_numeric_string?(new_value_before_type_cast) - end - - def non_numeric_string?(value) - # 'wibble'.to_i will give zero, we want to make sure - # that we aren't marking int zero to string zero as - # changed. - value.to_s !~ /\A-?\d+\.?\d*\z/ - end - end - end -end diff --git a/activerecord/lib/active_record/type/serialized.rb b/activerecord/lib/active_record/type/serialized.rb index 3cac03464e..732029c723 100644 --- a/activerecord/lib/active_record/type/serialized.rb +++ b/activerecord/lib/active_record/type/serialized.rb @@ -1,8 +1,7 @@ module ActiveRecord module Type class Serialized < DelegateClass(Type::Value) # :nodoc: - include Mutable - include Decorator + include Helpers::Mutable attr_reader :subtype, :coder @@ -12,7 +11,7 @@ module ActiveRecord super(subtype) end - def type_cast_from_database(value) + def deserialize(value) if default_value?(value) value else @@ -20,7 +19,7 @@ module ActiveRecord end end - def type_cast_for_database(value) + def serialize(value) return if value.nil? unless default_value?(value) super coder.dump(value) @@ -29,23 +28,13 @@ module ActiveRecord def changed_in_place?(raw_old_value, value) return false if value.nil? - subtype.changed_in_place?(raw_old_value, type_cast_for_database(value)) + subtype.changed_in_place?(raw_old_value, serialize(value)) end def accessor ActiveRecord::Store::IndifferentHashAccessor end - def init_with(coder) - @coder = coder['coder'] - super - end - - def encode_with(coder) - coder['coder'] = @coder - super - end - private def default_value?(value) diff --git a/activerecord/lib/active_record/type/string.rb b/activerecord/lib/active_record/type/string.rb index cf95e25be0..2662b7e874 100644 --- a/activerecord/lib/active_record/type/string.rb +++ b/activerecord/lib/active_record/type/string.rb @@ -11,7 +11,7 @@ module ActiveRecord end end - def type_cast_for_database(value) + def serialize(value) case value when ::Numeric, ActiveSupport::Duration then value.to_s when ::String then ::String.new(value) @@ -21,10 +21,6 @@ module ActiveRecord end end - def text? - true - end - private def cast_value(value) diff --git a/activerecord/lib/active_record/type/time.rb b/activerecord/lib/active_record/type/time.rb index 41f7d97f0c..19a10021bc 100644 --- a/activerecord/lib/active_record/type/time.rb +++ b/activerecord/lib/active_record/type/time.rb @@ -1,12 +1,28 @@ module ActiveRecord module Type class Time < Value # :nodoc: - include TimeValue + include Helpers::TimeValue + include Helpers::AcceptsMultiparameterTime.new( + defaults: { 1 => 1970, 2 => 1, 3 => 1, 4 => 0, 5 => 0 } + ) def type :time end + def user_input_in_time_zone(value) + return unless value.present? + + case value + when ::String + value = "2000-01-01 #{value}" + when ::Time + value = value.change(year: 2000, day: 1, month: 1) + end + + super(value) + end + private def cast_value(value) diff --git a/activerecord/lib/active_record/type/time_value.rb b/activerecord/lib/active_record/type/time_value.rb deleted file mode 100644 index d611d72dd4..0000000000 --- a/activerecord/lib/active_record/type/time_value.rb +++ /dev/null @@ -1,38 +0,0 @@ -module ActiveRecord - module Type - module TimeValue # :nodoc: - def klass - ::Time - end - - def type_cast_for_schema(value) - "'#{value.to_s(:db)}'" - end - - private - - def new_time(year, mon, mday, hour, min, sec, microsec, offset = nil) - # Treat 0000-00-00 00:00:00 as nil. - return if year.nil? || (year == 0 && mon == 0 && mday == 0) - - if offset - time = ::Time.utc(year, mon, mday, hour, min, sec, microsec) rescue nil - return unless time - - time -= offset - Base.default_timezone == :utc ? time : time.getlocal - else - ::Time.public_send(Base.default_timezone, year, mon, mday, hour, min, sec, microsec) rescue nil - end - end - - # Doesn't handle time zones. - def fast_string_to_time(string) - if string =~ ConnectionAdapters::Column::Format::ISO_DATETIME - microsec = ($7.to_r * 1_000_000).to_i - new_time $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, microsec - end - end - end - end -end diff --git a/activerecord/lib/active_record/type/value.rb b/activerecord/lib/active_record/type/value.rb index 60ae47db3d..fc3ef5e83b 100644 --- a/activerecord/lib/active_record/type/value.rb +++ b/activerecord/lib/active_record/type/value.rb @@ -1,44 +1,46 @@ module ActiveRecord module Type - class Value # :nodoc: + class Value attr_reader :precision, :scale, :limit - # Valid options are +precision+, +scale+, and +limit+. They are only - # used when dumping schema. - def initialize(options = {}) - options.assert_valid_keys(:precision, :scale, :limit) - @precision = options[:precision] - @scale = options[:scale] - @limit = options[:limit] + def initialize(precision: nil, limit: nil, scale: nil) + @precision = precision + @scale = scale + @limit = limit end - # The simplified type that this object represents. Returns a symbol such - # as +:string+ or +:integer+ - def type; end + def type # :nodoc: + end - # Type casts a string from the database into the appropriate ruby type. - # Classes which do not need separate type casting behavior for database - # and user provided values should override +cast_value+ instead. - def type_cast_from_database(value) - type_cast(value) + # Convert a value from database input to the appropriate ruby type. The + # return value of this method will be returned from + # ActiveRecord::AttributeMethods::Read#read_attribute. The default + # implementation just calls Value#cast. + # + # +value+ The raw input, as provided from the database. + def deserialize(value) + cast(value) end # Type casts a value from user input (e.g. from a setter). This value may - # be a string from the form builder, or an already type cast value - # provided manually to a setter. + # be a string from the form builder, or a ruby object passed to a setter. + # There is currently no way to differentiate between which source it came + # from. + # + # The return value of this method will be returned from + # ActiveRecord::AttributeMethods::Read#read_attribute. See also: + # Value#cast_value. # - # Classes which do not need separate type casting behavior for database - # and user provided values should override +type_cast+ or +cast_value+ - # instead. - def type_cast_from_user(value) - type_cast(value) + # +value+ The raw input, as provided to the attribute setter. + def cast(value) + cast_value(value) unless value.nil? end # Cast a value from the ruby type to a type that the database knows how # to understand. The returned value from this method should be a # +String+, +Numeric+, +Date+, +Time+, +Symbol+, +true+, +false+, or - # +nil+ - def type_cast_for_database(value) + # +nil+. + def serialize(value) value end @@ -50,21 +52,10 @@ module ActiveRecord # These predicates are not documented, as I need to look further into # their use, and see if they can be removed entirely. - def text? # :nodoc: - false - end - - def number? # :nodoc: - false - end - def binary? # :nodoc: false end - def klass # :nodoc: - end - # Determines whether a value has changed for dirty checking. +old_value+ # and +new_value+ will always be type-cast. Types should not need to # override this method. @@ -73,10 +64,23 @@ module ActiveRecord end # Determines whether the mutable value has been modified since it was - # read. Returns +false+ by default. This method should not be overridden - # directly. Types which return a mutable value should include - # +Type::Mutable+, which will define this method. - def changed_in_place?(*) + # read. Returns +false+ by default. If your type returns an object + # which could be mutated, you should override this method. You will need + # to either: + # + # - pass +new_value+ to Value#serialize and compare it to + # +raw_old_value+ + # + # or + # + # - pass +raw_old_value+ to Value#deserialize and compare it to + # +new_value+ + # + # +raw_old_value+ The original value, before being passed to + # +deserialize+. + # + # +new_value+ The current value, after type casting. + def changed_in_place?(raw_old_value, new_value) false end @@ -89,14 +93,9 @@ module ActiveRecord private - def type_cast(value) - cast_value(value) unless value.nil? - end - # Convenience method for types which do not need separate type casting - # behavior for user and database inputs. Called by - # +type_cast_from_database+ and +type_cast_from_user+ for all values - # except +nil+. + # behavior for user and database inputs. Called by Value#cast for + # values except +nil+. def cast_value(value) # :doc: value end diff --git a/activerecord/lib/active_record/type_caster/connection.rb b/activerecord/lib/active_record/type_caster/connection.rb index 9e4a130b40..3878270770 100644 --- a/activerecord/lib/active_record/type_caster/connection.rb +++ b/activerecord/lib/active_record/type_caster/connection.rb @@ -1,34 +1,29 @@ module ActiveRecord module TypeCaster class Connection - def initialize(connection, table_name) - @connection = connection + def initialize(klass, table_name) + @klass = klass @table_name = table_name end def type_cast_for_database(attribute_name, value) return value if value.is_a?(Arel::Nodes::BindParam) - type = type_for(attribute_name) - type.type_cast_for_database(value) + column = column_for(attribute_name) + connection.type_cast_from_column(column, value) end protected - attr_reader :connection, :table_name + attr_reader :table_name + delegate :connection, to: :@klass private - def type_for(attribute_name) + def column_for(attribute_name) if connection.schema_cache.table_exists?(table_name) - column_for(attribute_name).cast_type - else - Type::Value.new + connection.schema_cache.columns_hash(table_name)[attribute_name.to_s] end end - - def column_for(attribute_name) - connection.schema_cache.columns_hash(table_name)[attribute_name.to_s] - end end end end diff --git a/activerecord/lib/active_record/type_caster/map.rb b/activerecord/lib/active_record/type_caster/map.rb index 03c9e8ff83..4b1941351c 100644 --- a/activerecord/lib/active_record/type_caster/map.rb +++ b/activerecord/lib/active_record/type_caster/map.rb @@ -8,7 +8,7 @@ module ActiveRecord def type_cast_for_database(attr_name, value) return value if value.is_a?(Arel::Nodes::BindParam) type = types.type_for_attribute(attr_name.to_s) - type.type_cast_for_database(value) + type.serialize(value) end protected diff --git a/activerecord/lib/active_record/validations/length.rb b/activerecord/lib/active_record/validations/length.rb index ef5a6cbbe7..5991fbad8e 100644 --- a/activerecord/lib/active_record/validations/length.rb +++ b/activerecord/lib/active_record/validations/length.rb @@ -2,11 +2,23 @@ module ActiveRecord module Validations class LengthValidator < ActiveModel::Validations::LengthValidator # :nodoc: def validate_each(record, attribute, association_or_value) + return unless should_validate?(record) || associations_are_dirty?(record) if association_or_value.respond_to?(:loaded?) && association_or_value.loaded? association_or_value = association_or_value.target.reject(&:marked_for_destruction?) end super end + + def associations_are_dirty?(record) + attributes.any? do |attribute| + value = record.read_attribute_for_validation(attribute) + if value.respond_to?(:loaded?) && value.loaded? + value.target.any?(&:marked_for_destruction?) + else + false + end + end + end end module ClassMethods diff --git a/activerecord/lib/active_record/validations/presence.rb b/activerecord/lib/active_record/validations/presence.rb index 61b30749d9..75d5bd5a35 100644 --- a/activerecord/lib/active_record/validations/presence.rb +++ b/activerecord/lib/active_record/validations/presence.rb @@ -2,6 +2,7 @@ module ActiveRecord module Validations class PresenceValidator < ActiveModel::Validations::PresenceValidator # :nodoc: def validate(record) + return unless should_validate?(record) super attributes.each do |attribute| next unless record.class._reflect_on_association(attribute) diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index f52f91e89c..9be4b10a55 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -11,6 +11,7 @@ module ActiveRecord end def validate_each(record, attribute, value) + return unless should_validate?(record) finder_class = find_finder_class_for(record) table = finder_class.arel_table value = map_enum_attribute(finder_class, attribute, value) @@ -59,7 +60,8 @@ module ActiveRecord end column = klass.columns_hash[attribute_name] - value = klass.type_for_attribute(attribute_name).type_cast_for_database(value) + cast_type = klass.type_for_attribute(attribute_name) + value = cast_type.serialize(value) value = klass.connection.type_cast(value) if value.is_a?(String) && column.limit value = value.to_s[0, column.limit] @@ -67,7 +69,7 @@ module ActiveRecord value = Arel::Nodes::Quoted.new(value) - comparison = if !options[:case_sensitive] && value && column.text? + comparison = if !options[:case_sensitive] && !value.nil? # will use SQL LOWER function before comparison, unless it detects a case insensitive collation klass.connection.case_insensitive_comparison(table, attribute, column, value) else diff --git a/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb b/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb index fd94a2d038..5b3e57dcf6 100644 --- a/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb +++ b/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb @@ -4,6 +4,8 @@ class <%= migration_class_name %> < ActiveRecord::Migration <% attributes.each do |attribute| -%> <% if attribute.password_digest? -%> t.string :password_digest<%= attribute.inject_options %> +<% elsif attribute.token? -%> + t.string :<%= attribute.name %><%= attribute.inject_options %> <% else -%> t.<%= attribute.type %> :<%= attribute.name %><%= attribute.inject_options %> <% end -%> @@ -12,6 +14,9 @@ class <%= migration_class_name %> < ActiveRecord::Migration t.timestamps <% end -%> end +<% attributes.select(&:token?).each do |attribute| -%> + add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %>, unique: true +<% end -%> <% attributes_with_index.each do |attribute| -%> add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %> <% end -%> diff --git a/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb b/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb index ae9c74fd05..23a377db6a 100644 --- a/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb +++ b/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb @@ -4,6 +4,9 @@ class <%= migration_class_name %> < ActiveRecord::Migration <% attributes.each do |attribute| -%> <%- if attribute.reference? -%> add_reference :<%= table_name %>, :<%= attribute.name %><%= attribute.inject_options %> + <%- elsif attribute.token? -%> + add_column :<%= table_name %>, :<%= attribute.name %>, :string<%= attribute.inject_options %> + add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %>, unique: true <%- else -%> add_column :<%= table_name %>, :<%= attribute.name %>, :<%= attribute.type %><%= attribute.inject_options %> <%- if attribute.has_index? -%> diff --git a/activerecord/lib/rails/generators/active_record/model/templates/model.rb b/activerecord/lib/rails/generators/active_record/model/templates/model.rb index 539d969fce..55dc65c8ad 100644 --- a/activerecord/lib/rails/generators/active_record/model/templates/model.rb +++ b/activerecord/lib/rails/generators/active_record/model/templates/model.rb @@ -3,6 +3,9 @@ class <%= class_name %> < <%= parent_class_name.classify %> <% attributes.select(&:reference?).each do |attribute| -%> belongs_to :<%= attribute.name %><%= ', polymorphic: true' if attribute.polymorphic? %><%= ', required: true' if attribute.required? %> <% end -%> +<% attributes.select(&:token?).each do |attribute| -%> + has_secure_token<% if attribute.name != "token" %> :<%= attribute.name %><% end %> +<% end -%> <% if attributes.any?(&:password_digest?) -%> has_secure_password <% end -%> |