diff options
Diffstat (limited to 'activerecord/lib/active_record')
32 files changed, 228 insertions, 180 deletions
diff --git a/activerecord/lib/active_record/attribute.rb b/activerecord/lib/active_record/attribute.rb index 88536eaac0..73bb059495 100644 --- a/activerecord/lib/active_record/attribute.rb +++ b/activerecord/lib/active_record/attribute.rb @@ -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) @@ -74,6 +74,14 @@ 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 && @@ -99,6 +107,10 @@ module ActiveRecord def type_cast(value) type.type_cast_from_user(value) end + + def came_from_user? + true + end end class WithCastValue < Attribute # :nodoc: diff --git a/activerecord/lib/active_record/attribute_assignment.rb b/activerecord/lib/active_record/attribute_assignment.rb index bf64830417..e368fdfff9 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 - - # 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? - - attributes = new_attributes.stringify_keys - multi_parameter_attributes = [] - nested_parameter_attributes = [] + include ActiveModel::AttributeAssignment - 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 + # Alias for `assign_attributes`. See +ActiveModel::AttributeAssignment+ + def attributes=(attributes) + assign_attributes(attributes) + end 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) } diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index 8f165fb1dc..b2db0ceae7 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -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 devleopment 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 ce7f575150..06d87ee01e 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) @@ -165,7 +165,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/time_zone_conversion.rb b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb index 777f7ab4d7..98671178cb 100644 --- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb +++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb @@ -13,7 +13,7 @@ module ActiveRecord value.map { |v| type_cast_from_user(v) } elsif value.respond_to?(:in_time_zone) begin - value.in_time_zone || super + user_input_in_time_zone(value) || super rescue ArgumentError nil end @@ -39,6 +39,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 +62,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 cause `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..fdce68ce45 100644 --- a/activerecord/lib/active_record/attribute_set.rb +++ b/activerecord/lib/active_record/attribute_set.rb @@ -64,6 +64,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/attributes.rb b/activerecord/lib/active_record/attributes.rb index b263a89d79..faf5d632ec 100644 --- a/activerecord/lib/active_record/attributes.rb +++ b/activerecord/lib/active_record/attributes.rb @@ -14,7 +14,7 @@ module ActiveRecord end module ClassMethods # :nodoc: - # Defines or overrides a attribute on this model. This allows customization of + # Defines or overrides an attribute on this model. This allows customization of # Active Record's type casting behavior, as well as adding support for user defined # types. # 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 6b5081b7a9..d99dc9a5db 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -454,7 +454,8 @@ module ActiveRecord end c rescue - disconnect! + 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 6e631ed9f7..787d07c4c2 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -289,7 +289,13 @@ module ActiveRecord [columns[name], value] end key_list = fixture.keys.map { |name| quote_column_name(name) } - value_list = prepare_binds_for_database(binds).map { |_, value| quote(value) } + 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' end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index 7c1a779577..c18caa2a2f 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -125,8 +125,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_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb index 0f44c332ae..f905669a24 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -380,7 +380,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. diff --git a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb index 7535e9147a..7f738e89c9 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb @@ -111,7 +111,6 @@ module ActiveRecord def rollback connection.rollback_to_savepoint(savepoint_name) super - rollback_records end def commit @@ -136,13 +135,11 @@ module ActiveRecord def rollback connection.rollback_db_transaction super - rollback_records end def commit connection.commit_db_transaction super - commit_records end end @@ -159,18 +156,28 @@ module ActiveRecord else SavepointTransaction.new(@connection, "active_record_#{@stack.size}", options) end + @stack.push(transaction) transaction end def commit_transaction - transaction = @stack.pop - transaction.commit - transaction.records.each { |r| current_transaction.add_record(r) } + inner_transaction = @stack.pop + inner_transaction.commit + + if current_transaction.joinable? + inner_transaction.records.each do |r| + current_transaction.add_record(r) + 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 = {}) @@ -187,7 +194,7 @@ module ActiveRecord 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_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb index e9a3c26c32..b61b717a61 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -502,7 +502,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) 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..e45a2f59d9 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb @@ -18,7 +18,7 @@ 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 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..7dadc09a44 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb @@ -11,7 +11,7 @@ module ActiveRecord def type_cast_from_database(value) if value.is_a?(::String) - ::ActiveSupport::JSON.decode(value) + ::ActiveSupport::JSON.decode(value) rescue nil else super 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..afd7a17c03 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -112,7 +112,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. diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 03dfd29a0a..52dce6291a 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -53,7 +53,7 @@ module ActiveRecord 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) + value.encode(Encoding::UTF_8, undef: :replace) else super end diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index a5eb18246f..4705f129f2 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -456,26 +456,23 @@ 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) - if self.class.instance_method(:inspect).owner != ActiveRecord::Base.instance_method(:inspect).owner - pp.text inspect - else - pp.object_address_group(self) do - if defined?(@attributes) && @attributes - column_names = self.class.column_names.select { |name| has_attribute?(name) || new_record? } - pp.seplist(column_names, proc { pp.text ',' }) do |column_name| - column_value = read_attribute(column_name) - pp.breakable ' ' - pp.group(1) do - pp.text column_name - pp.text ':' - pp.breakable - pp.pp column_value - end - end - else + 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? } + pp.seplist(column_names, proc { pp.text ',' }) do |column_name| + column_value = read_attribute(column_name) pp.breakable ' ' - pp.text 'not initialized' + pp.group(1) do + pp.text column_name + pp.text ':' + pp.breakable + pp.pp column_value + end end + else + pp.breakable ' ' + pp.text 'not initialized' end end end @@ -564,5 +561,9 @@ module ActiveRecord @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/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/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index aeb1a4ddc6..6f2b65c137 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -125,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 diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index cf6673db2e..22112fe8ff 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -245,8 +245,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 +352,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 +495,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/relation.rb b/activerecord/lib/active_record/relation.rb index dd78814c6a..9c4db8a05e 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -33,7 +33,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 diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 088bc203b7..c83abfba06 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -307,7 +307,7 @@ module ActiveRecord relation = relation.where(conditions) else unless conditions == :none - relation = where(primary_key => conditions) + relation = relation.where(primary_key => conditions) end end diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index 567efce8ae..2860a30f99 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -28,6 +28,27 @@ module ActiveRecord expand_from_hash(attributes) end + def create_binds(attributes) + result = attributes.dup + binds = [] + + attributes.each do |column_name, value| + case value + when String, Integer, ActiveRecord::StatementCache::Substitute + result[column_name] = Arel::Nodes::BindParam.new + binds.push([table.column(column_name), value]) + when Hash + attrs, bvs = associated_predicate_builder(column_name).create_binds(value) + result[column_name] = attrs + binds += bvs + when Relation + binds += value.arel.bind_values + value.bind_values + end + end + + [result, binds] + end + def expand(column, value) # Find the foreign key when using queries such as: # Post.where(author: author) @@ -80,8 +101,7 @@ 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 @@ -90,6 +110,10 @@ module ActiveRecord 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?(".") } diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index d6e6cb4d05..c34e4bfb9b 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -909,7 +909,7 @@ module ActiveRecord end end - bind_values.reject! { |col,_| col.name == target_value } + self.bind_values = bind_values.reject { |col,_| col.name == target_value } end def custom_join_ast(table, joins) @@ -945,11 +945,10 @@ module ActiveRecord when Hash opts = predicate_builder.resolve_column_aliases(opts) - tmp_opts, bind_values = create_binds(opts) + tmp_opts, bind_values = predicate_builder.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 @@ -957,40 +956,6 @@ module ActiveRecord 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) || @@ -1153,17 +1118,5 @@ module ActiveRecord raise ArgumentError, "The method .#{method_name}() must contain arguments." end end - - 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.arel.bind_values + value.bind_values - else - add_relations_to_bind_values(value) - end - end - end - end end end diff --git a/activerecord/lib/active_record/secure_token.rb b/activerecord/lib/active_record/secure_token.rb index be3c3bc847..07031b6371 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 diff --git a/activerecord/lib/active_record/table_metadata.rb b/activerecord/lib/active_record/table_metadata.rb index 11e33e8dfe..31a40adb67 100644 --- a/activerecord/lib/active_record/table_metadata.rb +++ b/activerecord/lib/active_record/table_metadata.rb @@ -22,6 +22,12 @@ module ActiveRecord arel_table[column_name] end + def column(column_name) + if klass + klass.columns_hash[column_name.to_s] + end + end + def associated_with?(association_name) klass && klass._reflect_on_association(association_name) end diff --git a/activerecord/lib/active_record/type/integer.rb b/activerecord/lib/active_record/type/integer.rb index fc260a081a..90ca9f88da 100644 --- a/activerecord/lib/active_record/type/integer.rb +++ b/activerecord/lib/active_record/type/integer.rb @@ -3,6 +3,10 @@ module ActiveRecord class Integer < Value # :nodoc: include 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 @range = min_value...max_value @@ -12,13 +16,19 @@ module ActiveRecord :integer end - alias type_cast_for_database type_cast - def type_cast_from_database(value) return if value.nil? value.to_i end + def type_cast_for_database(value) + result = type_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/time.rb b/activerecord/lib/active_record/type/time.rb index 41f7d97f0c..cab1c7bf1e 100644 --- a/activerecord/lib/active_record/type/time.rb +++ b/activerecord/lib/active_record/type/time.rb @@ -7,6 +7,19 @@ module ActiveRecord :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 index d611d72dd4..8d9ac25643 100644 --- a/activerecord/lib/active_record/type/time_value.rb +++ b/activerecord/lib/active_record/type/time_value.rb @@ -9,6 +9,10 @@ module ActiveRecord "'#{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) diff --git a/activerecord/lib/active_record/type/value.rb b/activerecord/lib/active_record/type/value.rb index 60ae47db3d..859b51ca90 100644 --- a/activerecord/lib/active_record/type/value.rb +++ b/activerecord/lib/active_record/type/value.rb @@ -3,8 +3,7 @@ module ActiveRecord class Value # :nodoc: attr_reader :precision, :scale, :limit - # Valid options are +precision+, +scale+, and +limit+. They are only - # used when dumping schema. + # Valid options are +precision+, +scale+, and +limit+. def initialize(options = {}) options.assert_valid_keys(:precision, :scale, :limit) @precision = options[:precision] |