diff options
Diffstat (limited to 'activerecord')
62 files changed, 563 insertions, 208 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 7305c2c738..639dbe7df1 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,34 @@ +* `preload` preserves readonly flag for associations. + + See #15853. + + *Yves Senn* + +* Assume numeric types have changed if they were assigned to a value that + would fail numericality validation, regardless of the old value. Previously + this would only occur if the old value was 0. + + Example: + + model = Model.create!(number: 5) + model.number = '5wibble' + model.number_changed? # => true + + Fixes #14731. + + *Sean Griffin* + +* `reload` no longer merges with the existing attributes. + The attribute hash is fully replaced. The record is put into the same state + as it would be with `Model.find(model.id)`. + + *Sean Griffin* + +* The object returned from `select_all` must respond to `column_types`. + If this is not the case a `NoMethodError` is raised. + + *Sean Griffin* + * `has_many :through` associations will no longer save the through record twice when added in an `after_create` callback defined before the associations. @@ -14,8 +45,7 @@ *Yves Senn* -* Deprecate `serialized_attributes` without replacement. You can access its - behavior by going through the column's type object. +* Deprecate `serialized_attributes` without replacement. *Sean Griffin* @@ -106,7 +136,8 @@ *Lauro Caetano*, *Carlos Antonio da Silva* -* Return a null column from `column_for_attribute` when no column exists. +* Deprecate returning `nil` from `column_for_attribute` when no column exists. + It will return a null object in Rails 5.0 *Sean Griffin* diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index ab85414277..17b00bbaea 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -27,12 +27,12 @@ require 'active_model' require 'arel' require 'active_record/version' +require 'active_record/attribute_set' module ActiveRecord extend ActiveSupport::Autoload autoload :Attribute - autoload :AttributeSet autoload :Base autoload :Callbacks autoload :Core diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb index fbb4551b22..ec5c189cd3 100644 --- a/activerecord/lib/active_record/associations/join_dependency.rb +++ b/activerecord/lib/active_record/associations/join_dependency.rb @@ -131,7 +131,6 @@ module ActiveRecord def instantiate(result_set, aliases) primary_key = aliases.column_alias(join_root, join_root.primary_key) - type_caster = result_set.column_type primary_key seen = Hash.new { |h,parent_klass| h[parent_klass] = Hash.new { |i,parent_id| @@ -144,8 +143,7 @@ module ActiveRecord column_aliases = aliases.column_aliases join_root result_set.each { |row_hash| - primary_id = type_caster.type_cast_from_database row_hash[primary_key] - parent = parents[primary_id] ||= join_root.instantiate(row_hash, column_aliases) + parent = parents[row_hash[primary_key]] ||= join_root.instantiate(row_hash, column_aliases) construct(parent, join_root, row_hash, result_set, seen, model_cache, aliases) } diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb index 33c8619359..c0639742be 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) @@ -151,6 +151,10 @@ module ActiveRecord end end + if preload_values[:readonly] || values[:readonly] + scope.readonly! + end + if options[:as] scope.where!(klass.table_name => { reflection.type => model.base_class.sti_name }) end diff --git a/activerecord/lib/active_record/attribute.rb b/activerecord/lib/active_record/attribute.rb index 8604ccb90d..13c8bc3676 100644 --- a/activerecord/lib/active_record/attribute.rb +++ b/activerecord/lib/active_record/attribute.rb @@ -8,6 +8,10 @@ module ActiveRecord def from_user(value, type) FromUser.new(value, type) end + + def uninitialized(type) + Uninitialized.new(type) + end end attr_reader :value_before_type_cast, :type @@ -37,10 +41,22 @@ module ActiveRecord type.changed_in_place?(old_value, value) end + def with_value_from_user(value) + self.class.from_user(value, type) + end + + def with_value_from_database(value) + self.class.from_database(value, type) + end + def type_cast raise NotImplementedError end + def initialized? + true + end + protected def initialize_dup(other) @@ -49,27 +65,44 @@ module ActiveRecord end end - class FromDatabase < Attribute + class FromDatabase < Attribute # :nodoc: def type_cast(value) type.type_cast_from_database(value) end end - class FromUser < Attribute + class FromUser < Attribute # :nodoc: def type_cast(value) type.type_cast_from_user(value) end end - class Null - class << self - attr_reader :value, :value_before_type_cast, :value_for_database + class NullAttribute < Attribute # :nodoc: + def initialize + super(nil, Type::Value.new) + end - def changed_from?(*) - false - end - alias changed_in_place_from? changed_from? + def value + nil end end + + class Uninitialized < Attribute # :nodoc: + def initialize(type) + super(nil, type) + end + + def value + nil + end + alias value_for_database value + + def initialized? + false + end + end + private_constant :FromDatabase, :FromUser, :NullAttribute, :Uninitialized + + Null = NullAttribute.new # :nodoc: end end diff --git a/activerecord/lib/active_record/attribute_assignment.rb b/activerecord/lib/active_record/attribute_assignment.rb index 40e2918777..2887db3bf7 100644 --- a/activerecord/lib/active_record/attribute_assignment.rb +++ b/activerecord/lib/active_record/attribute_assignment.rb @@ -115,7 +115,7 @@ module ActiveRecord end class MultiparameterAttribute #:nodoc: - attr_reader :object, :name, :values, :column + attr_reader :object, :name, :values, :cast_type def initialize(object, name, values) @object = object @@ -126,22 +126,22 @@ module ActiveRecord def read_value return if values.values.compact.empty? - @column = object.column_for_attribute(name) - klass = column ? column.klass : nil + @cast_type = object.type_for_attribute(name) + klass = cast_type.klass if klass == Time read_time elsif klass == Date read_date else - read_other(klass) + read_other end end private def instantiate_time_object(set_values) - if object.class.send(:create_time_zone_conversion_attribute?, name, column) + 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) @@ -151,7 +151,7 @@ module ActiveRecord 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 column.type == :time + 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 @@ -181,7 +181,7 @@ module ActiveRecord end end - def read_other(klass) + def read_other max_position = extract_max_param positions = (1..max_position) validate_required_parameters!(positions) diff --git a/activerecord/lib/active_record/attribute_decorators.rb b/activerecord/lib/active_record/attribute_decorators.rb index 92627f8d5d..5745bbe0e3 100644 --- a/activerecord/lib/active_record/attribute_decorators.rb +++ b/activerecord/lib/active_record/attribute_decorators.rb @@ -7,7 +7,7 @@ module ActiveRecord self.attribute_type_decorations = TypeDecorator.new end - module ClassMethods + module ClassMethods # :nodoc: def decorate_attribute_type(column_name, decorator_name, &block) matcher = ->(name, _) { name == column_name.to_s } key = "_#{column_name}_#{decorator_name}" @@ -32,7 +32,7 @@ module ActiveRecord end end - class TypeDecorator + class TypeDecorator # :nodoc: delegate :clear, to: :@decorations def initialize(decorations = {}) diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index 268cec6160..51f6a009db 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -186,8 +186,7 @@ module ActiveRecord end # Returns the column object for the named attribute. - # Returns a +ActiveRecord::ConnectionAdapters::NullColumn+ if the - # named attribute does not exist. + # Returns nil if the named attribute does not exist. # # class Person < ActiveRecord::Base # end @@ -197,12 +196,17 @@ module ActiveRecord # # => #<ActiveRecord::ConnectionAdapters::SQLite3Column:0x007ff4ab083980 @name="name", @sql_type="varchar(255)", @null=true, ...> # # person.column_for_attribute(:nothing) - # # => #<ActiveRecord::ConnectionAdapters::NullColumn:0xXXX @name=nil, @sql_type=nil, @cast_type=#<Type::Value>, ...> + # # => nil def column_for_attribute(name) - name = name.to_s - columns_hash.fetch(name) do - ConnectionAdapters::NullColumn.new(name) + column = columns_hash[name.to_s] + if column.nil? + ActiveSupport::Deprecation.warn(<<-MESSAGE.strip_heredoc) + `column_for_attribute` will return a null object for non-existent columns + in Rails 5.0. If you would like to continue to receive `nil`, you should + instead call `model.class.columns_hash[name]` + MESSAGE end + column end end @@ -271,9 +275,7 @@ module ActiveRecord # person.attributes # # => {"id"=>3, "created_at"=>Sun, 21 Oct 2012 04:53:04, "updated_at"=>Sun, 21 Oct 2012 04:53:04, "name"=>"Francesco", "age"=>22} def attributes - attribute_names.each_with_object({}) { |name, attrs| - attrs[name] = read_attribute(name) - } + @attributes.to_hash end # Returns an <tt>#inspect</tt>-like string for the value of the 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 d057f0941a..fd61febd57 100644 --- a/activerecord/lib/active_record/attribute_methods/before_type_cast.rb +++ b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb @@ -43,9 +43,7 @@ module ActiveRecord # task.read_attribute_before_type_cast('completed_on') # => "2012-10-21" # task.read_attribute_before_type_cast(:completed_on) # => "2012-10-21" def read_attribute_before_type_cast(attr_name) - if attr = @attributes[attr_name.to_s] - attr.value_before_type_cast - end + @attributes[attr_name.to_s].value_before_type_cast end # Returns a hash of attributes before typecasting and deserialization. @@ -59,7 +57,7 @@ module ActiveRecord # task.attributes_before_type_cast # # => {"id"=>nil, "title"=>nil, "is_done"=>true, "completed_on"=>"2012-10-21", "created_at"=>nil, "updated_at"=>nil} def attributes_before_type_cast - @attributes.each_with_object({}) { |(k, v), h| h[k] = v.value_before_type_cast } + @attributes.values_before_type_cast end private diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index ca71834641..e1a86fd3aa 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -129,7 +129,7 @@ module ActiveRecord end def _field_changed?(attr, old_value) - attribute_named(attr).changed_from?(old_value) + @attributes[attr].changed_from?(old_value) end def changed_in_place @@ -140,7 +140,7 @@ module ActiveRecord def changed_in_place?(attr_name) old_value = original_raw_attribute(attr_name) - attribute_named(attr_name).changed_in_place_from?(old_value) + @attributes[attr_name].changed_in_place_from?(old_value) end def original_raw_attribute(attr_name) @@ -154,7 +154,7 @@ module ActiveRecord end def store_original_raw_attribute(attr_name) - original_raw_attributes[attr_name] = attribute_named(attr_name).value_for_database + original_raw_attributes[attr_name] = @attributes[attr_name].value_for_database end def store_original_raw_attributes diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index 525c46970a..10869dfc1e 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -81,17 +81,10 @@ module ActiveRecord # Returns the value of the attribute identified by <tt>attr_name</tt> after # it has been typecast (for example, "2004-12-12" in a date column is cast # to a date object, like Date.new(2004, 12, 12)). - def read_attribute(attr_name) + def read_attribute(attr_name, &block) name = attr_name.to_s - @attributes.fetch(name) { - if name == 'id' - return read_attribute(self.class.primary_key) - elsif block_given? && self.class.columns_hash.key?(name) - return yield(name) - else - return nil - end - }.value + name = self.class.primary_key if name == 'id' + @attributes.fetch_value(name, &block) end private @@ -99,10 +92,6 @@ module ActiveRecord def attribute(attribute_name) read_attribute(attribute_name) end - - def attribute_named(attribute_name) - @attributes.fetch(attribute_name, Attribute::Null) - end end end end 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 188d5d2aab..5f1dc8bc9f 100644 --- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb +++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb @@ -42,10 +42,10 @@ module ActiveRecord module ClassMethods private - def create_time_zone_conversion_attribute?(name, column) + 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 == column.type) + (:datetime == cast_type.type) end end end diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb index 246a2cd8ba..94fce2db60 100644 --- a/activerecord/lib/active_record/attribute_methods/write.rb +++ b/activerecord/lib/active_record/attribute_methods/write.rb @@ -69,16 +69,15 @@ module ActiveRecord def write_attribute_with_type_cast(attr_name, value, should_type_cast) attr_name = attr_name.to_s attr_name = self.class.primary_key if attr_name == 'id' && self.class.primary_key - type = type_for_attribute(attr_name) unless has_attribute?(attr_name) || self.class.columns_hash.key?(attr_name) raise ActiveModel::MissingAttributeError, "can't write unknown attribute `#{attr_name}'" end if should_type_cast - @attributes[attr_name] = Attribute.from_user(value, type) + @attributes.write_from_user(attr_name, value) else - @attributes[attr_name] = Attribute.from_database(value, type) + @attributes.write_from_database(attr_name, value) end value diff --git a/activerecord/lib/active_record/attribute_set.rb b/activerecord/lib/active_record/attribute_set.rb index 102ef17e16..65e15b16dd 100644 --- a/activerecord/lib/active_record/attribute_set.rb +++ b/activerecord/lib/active_record/attribute_set.rb @@ -1,13 +1,42 @@ +require 'active_record/attribute_set/builder' + module ActiveRecord class AttributeSet # :nodoc: - delegate :[], :[]=, :fetch, :include?, :keys, :each_with_object, to: :attributes + delegate :[], to: :attributes + delegate :keys, to: :initialized_attributes def initialize(attributes) @attributes = attributes end - def update(other) - attributes.update(other.attributes) + def values_before_type_cast + attributes.each_with_object({}) { |(k, v), h| h[k] = v.value_before_type_cast } + end + + def to_hash + initialized_attributes.each_with_object({}) { |(k, v), h| h[k] = v.value } + end + alias_method :to_h, :to_hash + + def include?(name) + attributes.include?(name) && self[name].initialized? + end + + def fetch_value(name) + attribute = self[name] + if attribute.initialized? || !block_given? + attribute.value + else + yield name + end + end + + def write_from_database(name, value) + attributes[name] = self[name].with_value_from_database(value) + end + + def write_from_user(name, value) + attributes[name] = self[name].with_value_from_user(value) end def freeze @@ -29,23 +58,14 @@ module ActiveRecord super end - class Builder - def initialize(types) - @types = types - end - - def build_from_database(values, additional_types = {}) - attributes = values.each_with_object({}) do |(name, value), hash| - type = additional_types.fetch(name, @types[name]) - hash[name] = Attribute.from_database(value, type) - end - AttributeSet.new(attributes) - end - end - protected attr_reader :attributes + private + + def initialized_attributes + attributes.select { |_, attr| attr.initialized? } + end end end diff --git a/activerecord/lib/active_record/attribute_set/builder.rb b/activerecord/lib/active_record/attribute_set/builder.rb new file mode 100644 index 0000000000..f9cf9c1809 --- /dev/null +++ b/activerecord/lib/active_record/attribute_set/builder.rb @@ -0,0 +1,33 @@ +module ActiveRecord + class AttributeSet # :nodoc: + class Builder # :nodoc: + attr_reader :types + + def initialize(types) + @types = types + end + + def build_from_database(values = {}, additional_types = {}) + attributes = build_attributes_from_values(values, additional_types) + add_uninitialized_attributes(attributes) + AttributeSet.new(attributes) + end + + private + + def build_attributes_from_values(values, additional_types) + attributes = Hash.new(Attribute::Null) + values.each_with_object(attributes) do |(name, value), hash| + type = additional_types.fetch(name, types[name]) + hash[name] = Attribute.from_database(value, type) + end + end + + def add_uninitialized_attributes(attributes) + types.except(*attributes.keys).each do |name, type| + attributes[name] = Attribute.uninitialized(type) + end + end + end + end +end diff --git a/activerecord/lib/active_record/attributes.rb b/activerecord/lib/active_record/attributes.rb index d0287140c8..492d8f3560 100644 --- a/activerecord/lib/active_record/attributes.rb +++ b/activerecord/lib/active_record/attributes.rb @@ -9,7 +9,7 @@ module ActiveRecord self.user_provided_columns = {} end - module ClassMethods + 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. @@ -27,7 +27,7 @@ module ActiveRecord # # ==== Examples # - # The type detected by Active Record can be overriden. + # The type detected by Active Record can be overridden. # # # db/schema.rb # create_table :store_listings, force: true do |t| 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 db80c0faee..cb75070e3a 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -364,7 +364,7 @@ module ActiveRecord conn.expire end - release conn, owner + release owner @available.add conn end @@ -377,7 +377,7 @@ module ActiveRecord @connections.delete conn @available.delete conn - release conn, conn.owner + release conn.owner @available.add checkout_new_connection if @available.any_waiting? end @@ -425,7 +425,7 @@ module ActiveRecord end end - def release(conn, owner) + def release(owner) thread_id = owner.object_id @reserved_connections.delete thread_id diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb index 8be4678ace..1f1e2c46f4 100644 --- a/activerecord/lib/active_record/connection_adapters/column.rb +++ b/activerecord/lib/active_record/connection_adapters/column.rb @@ -57,12 +57,6 @@ module ActiveRecord end end end - - class NullColumn < Column - def initialize(name) - super name, nil, Type::Value.new - end - end end # :startdoc: end diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index 252b82643d..ad07a46e51 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -263,7 +263,7 @@ module ActiveRecord end module Fields # :nodoc: - class DateTime < Type::DateTime + class DateTime < Type::DateTime # :nodoc: def cast_value(value) if Mysql::Time === value new_time( @@ -280,7 +280,7 @@ module ActiveRecord end end - class Time < Type::Time + class Time < Type::Time # :nodoc: def cast_value(value) if Mysql::Time === value new_time( 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 d322c56acc..cd5efe2bb8 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb @@ -2,7 +2,7 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class Array < Type::Value + class Array < Type::Value # :nodoc: include Type::Mutable # Loads pg_array_parser if available. String parsing can be 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 3073f8ff30..243ecd13cf 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb @@ -2,7 +2,7 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class Bit < Type::Value + class Bit < Type::Value # :nodoc: def type :bit end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit_varying.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit_varying.rb index 054af285bb..4c21097d48 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit_varying.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit_varying.rb @@ -2,7 +2,7 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class BitVarying < OID::Bit + class BitVarying < OID::Bit # :nodoc: def type :bit_varying 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 89b203d2b1..997613d7be 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb @@ -2,7 +2,7 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class Bytea < Type::Binary + class Bytea < Type::Binary # :nodoc: def type_cast_from_database(value) return if value.nil? 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 534961a414..a53b4ee8e2 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb @@ -2,7 +2,7 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class Cidr < Type::Value + class Cidr < Type::Value # :nodoc: def type :cidr end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb index 3c30ad5fec..1d8d264530 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb @@ -2,7 +2,7 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class Date < Type::Date + class Date < Type::Date # :nodoc: include Infinity 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 34e2276dd1..b9e7894e5c 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 @@ -2,7 +2,7 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class DateTime < Type::DateTime + class DateTime < Type::DateTime # :nodoc: include Infinity def cast_value(value) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/decimal.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/decimal.rb index ed4b8911d9..43d22c8daf 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/decimal.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/decimal.rb @@ -2,7 +2,7 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class Decimal < Type::Decimal + class Decimal < Type::Decimal # :nodoc: def infinity(options = {}) BigDecimal.new("Infinity") * (options[:negative] ? -1 : 1) end 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 5fed8b0f89..77d5038efd 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb @@ -2,7 +2,7 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class Enum < Type::Value + class Enum < Type::Value # :nodoc: def type :enum 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 index 77dd97e140..26c5d3d78f 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/float.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/float.rb @@ -2,7 +2,7 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class Float < Type::Float + class Float < Type::Float # :nodoc: include Infinity def cast_value(value) 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 0dd4b65333..57b20477df 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb @@ -2,7 +2,7 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class Hstore < Type::Value + class Hstore < Type::Value # :nodoc: include Type::Mutable def type diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/inet.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/inet.rb index 7ed8f5f031..96486fa65b 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/inet.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/inet.rb @@ -2,7 +2,7 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class Inet < Cidr + class Inet < Cidr # :nodoc: def type :inet end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/infinity.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/infinity.rb index d438ffa140..e47780399a 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/infinity.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/infinity.rb @@ -2,7 +2,7 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - module Infinity + module Infinity # :nodoc: def infinity(options = {}) options[:negative] ? -::Float::INFINITY : ::Float::INFINITY 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 index 388d3dd9ed..59abdc0009 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/integer.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/integer.rb @@ -2,7 +2,7 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class Integer < Type::Integer + class Integer < Type::Integer # :nodoc: include Infinity 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 d1347c7bb5..ab1165f301 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb @@ -2,7 +2,7 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class Json < Type::Value + class Json < Type::Value # :nodoc: include Type::Mutable def type 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 d25eb256c2..df890c2ed6 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb @@ -2,7 +2,7 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class Money < Type::Decimal + class Money < Type::Decimal # :nodoc: include Infinity class_attribute :precision 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 86277c5542..9b6494867f 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb @@ -2,7 +2,7 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class Point < Type::Value + class Point < Type::Value # :nodoc: include Type::Mutable def type 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 c289ba8980..991cdd0913 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb @@ -2,7 +2,7 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class Range < Type::Value + class Range < Type::Value # :nodoc: attr_reader :subtype, :type def initialize(subtype, type) 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 7b1ca16bc4..b2a42e9ebb 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 @@ -2,7 +2,7 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class SpecializedString < Type::String + class SpecializedString < Type::String # :nodoc: attr_reader :type def initialize(type) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/time.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/time.rb index ea1f599b0f..8f0246eddb 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/time.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/time.rb @@ -2,7 +2,7 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class Time < Type::Time + class Time < Type::Time # :nodoc: include Infinity 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 0ed5491887..89728b0fe2 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb @@ -2,7 +2,7 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class Uuid < Type::Value + class Uuid < Type::Value # :nodoc: def type :uuid 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 2f7d1be197..de4187b028 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/vector.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/vector.rb @@ -2,7 +2,7 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class Vector < Type::Value + class Vector < Type::Value # :nodoc: attr_reader :delim, :subtype # +delim+ corresponds to the `typdelim` column in the pg_types diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb index 3fea8f490d..4caed77952 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb @@ -186,7 +186,7 @@ module ActiveRecord end end - class AdapterProxyType < SimpleDelegator + class AdapterProxyType < SimpleDelegator # :nodoc: def initialize(column, adapter) @column = column @adapter = adapter diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 6d5151dfe4..be4ae47d09 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -344,14 +344,13 @@ module ActiveRecord if supports_extensions? res = exec_query "SELECT EXISTS(SELECT * FROM pg_available_extensions WHERE name = '#{name}' AND installed_version IS NOT NULL) as enabled", 'SCHEMA' - res.column_types['enabled'].type_cast_from_database res.rows.first.first + res.cast_values.first end end def extensions if supports_extensions? - res = exec_query "SELECT extname from pg_extension", "SCHEMA" - res.rows.map { |r| res.column_types['extname'].type_cast_from_database r.first } + exec_query("SELECT extname from pg_extension", "SCHEMA").cast_values else super end diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index 3c24dc6420..c434b0d40a 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -254,7 +254,6 @@ module ActiveRecord end @attributes = self.class.attributes_builder.build_from_database(defaults) - @column_types = self.class.column_types init_internals initialize_internals_callback @@ -280,7 +279,6 @@ module ActiveRecord # post.title # => 'hello world' def init_with(coder) @attributes = coder['attributes'] - @column_types = self.class.column_types init_internals @@ -324,7 +322,7 @@ module ActiveRecord def initialize_dup(other) # :nodoc: pk = self.class.primary_key @attributes = @attributes.dup - @attributes[pk] = Attribute.from_database(nil, type_for_attribute(pk)) + @attributes.write_from_database(pk, nil) run_callbacks(:initialize) unless _initialize_callbacks.empty? @@ -523,7 +521,9 @@ module ActiveRecord def init_internals pk = self.class.primary_key - @attributes[pk] ||= Attribute.from_database(nil, type_for_attribute(pk)) + unless @attributes.include?(pk) + @attributes.write_from_database(pk, nil) + end @aggregation_cache = {} @association_cache = {} diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index 0a764fb7ad..f4ec13a177 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -170,7 +170,7 @@ module ActiveRecord end end - class LockingType < SimpleDelegator + class LockingType < SimpleDelegator # :nodoc: def type_cast_from_database(value) # `nil` *should* be changed to 0 super.to_i diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index 6707f12489..ee634d7bb3 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -395,9 +395,7 @@ module ActiveRecord self.class.unscoped { self.class.find(id) } end - @attributes.update(fresh_object.instance_variable_get('@attributes')) - - @column_types = self.class.column_types + @attributes = fresh_object.instance_variable_get('@attributes') self end diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb index 39817703cd..a9ddd9141f 100644 --- a/activerecord/lib/active_record/querying.rb +++ b/activerecord/lib/active_record/querying.rb @@ -37,14 +37,7 @@ module ActiveRecord # Post.find_by_sql ["SELECT body FROM comments WHERE author = :user_id OR approved_by = :user_id", { :user_id => user_id }] def find_by_sql(sql, binds = []) result_set = connection.select_all(sanitize_sql(sql), "#{name} Load", binds) - column_types = {} - - if result_set.respond_to? :column_types - column_types = result_set.column_types.except(*columns_hash.keys) - else - ActiveSupport::Deprecation.warn "the object returned from `select_all` must respond to `column_types`" - end - + column_types = result_set.column_types.except(*columns_hash.keys) result_set.map { |record| instantiate(record, column_types) } end diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index c80ffbae92..b4ae204813 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -178,16 +178,7 @@ module ActiveRecord columns_hash.key?(cn) ? arel_table[cn] : cn } result = klass.connection.select_all(relation.arel, nil, bind_values) - columns = result.columns.map do |key| - klass.column_types.fetch(key) { - result.column_types.fetch(key) { result.identity_type } - } - end - - result = result.rows.map do |values| - columns.zip(values).map { |column, value| column.type_cast_from_database value } - end - columns.one? ? result.map!(&:first) : result + result.cast_values(klass.column_types) end end diff --git a/activerecord/lib/active_record/result.rb b/activerecord/lib/active_record/result.rb index 8c29c69a9b..8405fdaeb9 100644 --- a/activerecord/lib/active_record/result.rb +++ b/activerecord/lib/active_record/result.rb @@ -42,14 +42,6 @@ module ActiveRecord @column_types = column_types end - def identity_type # :nodoc: - IDENTITY_TYPE - end - - def column_type(name) - @column_types[name] || identity_type - end - def each if block_given? hash_rows.each { |row| yield row } @@ -82,6 +74,15 @@ module ActiveRecord hash_rows.last end + def cast_values(type_overrides = {}) # :nodoc: + types = columns.map { |name| column_type(name, type_overrides) } + result = rows.map do |values| + types.zip(values).map { |type, value| type.type_cast_from_database(value) } + end + + columns.one? ? result.map!(&:first) : result + end + def initialize_copy(other) @columns = columns.dup @rows = rows.dup @@ -91,6 +92,12 @@ module ActiveRecord private + def column_type(name, type_overrides = {}) + type_overrides.fetch(name) do + column_types.fetch(name, IDENTITY_TYPE) + end + end + def hash_rows @hash_rows ||= begin diff --git a/activerecord/lib/active_record/store.rb b/activerecord/lib/active_record/store.rb index 7014bc6d45..3c291f28e3 100644 --- a/activerecord/lib/active_record/store.rb +++ b/activerecord/lib/active_record/store.rb @@ -99,7 +99,7 @@ module ActiveRecord self.local_stored_attributes[store_attribute] |= keys end - def _store_accessors_module + def _store_accessors_module # :nodoc: @_store_accessors_module ||= begin mod = Module.new include mod @@ -129,10 +129,10 @@ module ActiveRecord private def store_accessor_for(store_attribute) - @column_types[store_attribute.to_s].accessor + type_for_attribute(store_attribute.to_s).accessor end - class HashAccessor + class HashAccessor # :nodoc: def self.read(object, attribute, key) prepare(object, attribute) object.public_send(attribute)[key] @@ -151,7 +151,7 @@ module ActiveRecord end end - class StringKeyedHashAccessor < HashAccessor + class StringKeyedHashAccessor < HashAccessor # :nodoc: def self.read(object, attribute, key) super object, attribute, key.to_s end @@ -161,7 +161,7 @@ module ActiveRecord end end - class IndifferentHashAccessor < ActiveRecord::Store::HashAccessor + class IndifferentHashAccessor < ActiveRecord::Store::HashAccessor # :nodoc: def self.prepare(object, store_attribute) attribute = object.send(store_attribute) unless attribute.is_a?(ActiveSupport::HashWithIndifferentAccess) diff --git a/activerecord/lib/active_record/type/binary.rb b/activerecord/lib/active_record/type/binary.rb index 3bf29b5026..7416c554c7 100644 --- a/activerecord/lib/active_record/type/binary.rb +++ b/activerecord/lib/active_record/type/binary.rb @@ -22,7 +22,7 @@ module ActiveRecord Data.new(super) end - class Data + class Data # :nodoc: def initialize(value) @value = value end diff --git a/activerecord/lib/active_record/type/mutable.rb b/activerecord/lib/active_record/type/mutable.rb index 64cf4b9b93..066617ea59 100644 --- a/activerecord/lib/active_record/type/mutable.rb +++ b/activerecord/lib/active_record/type/mutable.rb @@ -1,6 +1,6 @@ module ActiveRecord module Type - module Mutable + module Mutable # :nodoc: def type_cast_from_user(value) type_cast_from_database(type_cast_for_database(value)) end @@ -8,7 +8,7 @@ module ActiveRecord # +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) # :nodoc: + def changed_in_place?(raw_old_value, new_value) raw_old_value != type_cast_for_database(new_value) end end diff --git a/activerecord/lib/active_record/type/numeric.rb b/activerecord/lib/active_record/type/numeric.rb index a7bf0657b9..fa43266504 100644 --- a/activerecord/lib/active_record/type/numeric.rb +++ b/activerecord/lib/active_record/type/numeric.rb @@ -16,13 +16,13 @@ module ActiveRecord end def changed?(old_value, _new_value, new_value_before_type_cast) # :nodoc: - super || zero_to_non_number?(old_value, new_value_before_type_cast) + super || number_to_non_number?(old_value, new_value_before_type_cast) end private - def zero_to_non_number?(old_value, new_value_before_type_cast) - old_value == 0 && non_numeric_string?(new_value_before_type_cast) + 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) diff --git a/activerecord/lib/active_record/type/value.rb b/activerecord/lib/active_record/type/value.rb index 875fb98c4b..081da7547e 100644 --- a/activerecord/lib/active_record/type/value.rb +++ b/activerecord/lib/active_record/type/value.rb @@ -3,8 +3,8 @@ 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+. They are only + # used when dumping schema. def initialize(options = {}) options.assert_valid_keys(:precision, :scale, :limit) @precision = options[:precision] @@ -12,65 +12,85 @@ module ActiveRecord @limit = options[:limit] end - # The simplified type that this object represents. Subclasses - # must override this method. + # The simplified type that this object represents. Returns a symbol such + # as +:string+ or +:integer+ def type; 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) 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. + # + # 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) 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) value end - def type_cast_for_schema(value) + # Type cast a value for schema dumping. This method is private, as we are + # hoping to remove it entirely. + def type_cast_for_schema(value) # :nodoc: value.inspect end - def text? + # 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? + def number? # :nodoc: false end - def binary? + def binary? # :nodoc: false end def klass # :nodoc: end - # +old_value+ will always be type-cast. - # +new_value+ will come straight from the database - # or from assignment, so it could be anything. Types - # which cannot typecast arbitrary values should override - # this method. - def changed?(old_value, new_value, _new_value_before_type_cast) # :nodoc: + # 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. + def changed?(old_value, new_value, _new_value_before_type_cast) old_value != new_value end - def changed_in_place?(*) # :nodoc: + # Determines whether the mutable value has been modified since it was + # read. Returns +false+ by default. This method should not need to be + # overriden directly. Types which return a mutable value should include + # +Type::Mutable+, which will define this method. + def changed_in_place?(*) false end private - # Takes an input from the database, or from attribute setters, - # and casts it to a type appropriate for this object. This method - # should not be overriden by subclasses. Instead, override `cast_value`. - def type_cast(value) # :api: public + + def type_cast(value) cast_value(value) unless value.nil? end - # Responsible for casting values from external sources to the appropriate - # type. Called by `type_cast` for all values except `nil`. - def cast_value(value) # :api: public + # 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`. + def cast_value(value) # :doc: value end end diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index 910067666a..21912fdf0f 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -1261,4 +1261,33 @@ class EagerAssociationTest < ActiveRecord::TestCase Author.eager_load(:posts_with_signature).to_a end end + + test "preloading readonly association" do + # has-one + firm = Firm.where(id: "1").preload(:readonly_account).first! + assert firm.readonly_account.readonly? + + # has_and_belongs_to_many + project = Project.where(id: "2").preload(:readonly_developers).first! + assert project.readonly_developers.first.readonly? + + # has-many :through + david = Author.where(id: "1").preload(:readonly_comments).first! + assert david.readonly_comments.first.readonly? + end + + test "eager-loading readonly association" do + skip "eager_load does not yet preserve readonly associations" + # has-one + firm = Firm.where(id: "1").eager_load(:readonly_account).first! + assert firm.readonly_account.readonly? + + # has_and_belongs_to_many + project = Project.where(id: "2").eager_load(:readonly_developers).first! + assert project.readonly_developers.first.readonly? + + # has-many :through + david = Author.where(id: "1").eager_load(:readonly_comments).first! + assert david.readonly_comments.first.readonly? + end end diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb index f832ca3451..7566af920f 100644 --- a/activerecord/test/cases/attribute_methods_test.rb +++ b/activerecord/test/cases/attribute_methods_test.rb @@ -831,6 +831,37 @@ class AttributeMethodsTest < ActiveRecord::TestCase end end + def test_attribute_method? + assert @target.attribute_method?(:title) + assert @target.attribute_method?(:title=) + assert_not @target.attribute_method?(:wibble) + end + + def test_attribute_method_returns_false_if_table_does_not_exist + @target.table_name = 'wibble' + assert_not @target.attribute_method?(:title) + end + + def test_attribute_names_on_new_record + model = @target.new + + assert_equal @target.column_names, model.attribute_names + end + + def test_attribute_names_on_queried_record + model = @target.last! + + assert_equal @target.column_names, model.attribute_names + end + + def test_attribute_names_with_custom_select + model = @target.select('id').last! + + assert_equal ['id'], model.attribute_names + # Sanity check, make sure other columns exist + assert_not_equal ['id'], @target.column_names + end + private def new_topic_like_ar_class(&block) diff --git a/activerecord/test/cases/attribute_set_test.rb b/activerecord/test/cases/attribute_set_test.rb index 091f7e396a..35caa0ddab 100644 --- a/activerecord/test/cases/attribute_set_test.rb +++ b/activerecord/test/cases/attribute_set_test.rb @@ -18,6 +18,14 @@ module ActiveRecord assert_equal 4, attributes[:bar].value end + test "[] returns a null object" do + builder = AttributeSet::Builder.new(foo: Type::Float.new) + attributes = builder.build_from_database(foo: '3.3') + + assert_equal '3.3', attributes[:foo].value_before_type_cast + assert_equal nil, attributes[:bar].value_before_type_cast + end + test "duping creates a new hash and dups each attribute" do builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::String.new) attributes = builder.build_from_database(foo: 1, bar: 'foo') @@ -27,7 +35,7 @@ module ActiveRecord attributes[:bar].value duped = attributes.dup - duped[:foo] = Attribute.from_database(2, Type::Integer.new) + duped.write_from_database(:foo, 2) duped[:bar].value << 'bar' assert_equal 1, attributes[:foo].value @@ -45,5 +53,110 @@ module ActiveRecord assert clone.frozen? assert_not attributes.frozen? end + + test "to_hash returns a hash of the type cast values" do + builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new) + attributes = builder.build_from_database(foo: '1.1', bar: '2.2') + + assert_equal({ foo: 1, bar: 2.2 }, attributes.to_hash) + assert_equal({ foo: 1, bar: 2.2 }, attributes.to_h) + end + + test "values_before_type_cast" do + builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Integer.new) + attributes = builder.build_from_database(foo: '1.1', bar: '2.2') + + assert_equal({ foo: '1.1', bar: '2.2' }, attributes.values_before_type_cast) + end + + test "known columns are built with uninitialized attributes" do + attributes = attributes_with_uninitialized_key + assert attributes[:foo].initialized? + assert_not attributes[:bar].initialized? + end + + test "uninitialized attributes are not included in the attributes hash" do + attributes = attributes_with_uninitialized_key + assert_equal({ foo: 1 }, attributes.to_hash) + end + + test "uninitialized attributes are not included in keys" do + attributes = attributes_with_uninitialized_key + assert_equal [:foo], attributes.keys + end + + test "uninitialized attributes return false for include?" do + attributes = attributes_with_uninitialized_key + assert attributes.include?(:foo) + assert_not attributes.include?(:bar) + end + + test "unknown attributes return false for include?" do + attributes = attributes_with_uninitialized_key + assert_not attributes.include?(:wibble) + end + + test "fetch_value returns the value for the given initialized attribute" do + builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new) + attributes = builder.build_from_database(foo: '1.1', bar: '2.2') + + assert_equal 1, attributes.fetch_value(:foo) + assert_equal 2.2, attributes.fetch_value(:bar) + end + + test "fetch_value returns nil for unknown attributes" do + attributes = attributes_with_uninitialized_key + assert_nil attributes.fetch_value(:wibble) + end + + test "fetch_value uses the given block for uninitialized attributes" do + attributes = attributes_with_uninitialized_key + value = attributes.fetch_value(:bar) { |n| n.to_s + '!' } + assert_equal 'bar!', value + end + + test "fetch_value returns nil for uninitialized attributes if no block is given" do + attributes = attributes_with_uninitialized_key + assert_nil attributes.fetch_value(:bar) + end + + class MyType + def type_cast_from_user(value) + return if value.nil? + value + " from user" + end + + def type_cast_from_database(value) + return if value.nil? + value + " from database" + end + end + + test "write_from_database sets the attribute with database typecasting" do + builder = AttributeSet::Builder.new(foo: MyType.new) + attributes = builder.build_from_database + + assert_nil attributes.fetch_value(:foo) + + attributes.write_from_database(:foo, "value") + + assert_equal "value from database", attributes.fetch_value(:foo) + end + + test "write_from_user sets the attribute with user typecasting" do + builder = AttributeSet::Builder.new(foo: MyType.new) + attributes = builder.build_from_database + + assert_nil attributes.fetch_value(:foo) + + attributes.write_from_user(:foo, "value") + + assert_equal "value from user", attributes.fetch_value(:foo) + end + + def attributes_with_uninitialized_key + builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new) + builder.build_from_database(foo: '1.1') + end end end diff --git a/activerecord/test/cases/attribute_test.rb b/activerecord/test/cases/attribute_test.rb index 57dd2e9a5e..0adf9545b8 100644 --- a/activerecord/test/cases/attribute_test.rb +++ b/activerecord/test/cases/attribute_test.rb @@ -99,5 +99,31 @@ module ActiveRecord attribute = Attribute.from_database('a value', @type) attribute.dup end + + class MyType + def type_cast_from_user(value) + value + " from user" + end + + def type_cast_from_database(value) + value + " from database" + end + end + + test "with_value_from_user returns a new attribute with the value from the user" do + old = Attribute.from_database("old", MyType.new) + new = old.with_value_from_user("new") + + assert_equal "old from database", old.value + assert_equal "new from user", new.value + end + + test "with_value_from_database returns a new attribute with the value from the database" do + old = Attribute.from_user("old", MyType.new) + new = old.with_value_from_database("new") + + assert_equal "old from user", old.value + assert_equal "new from database", new.value + end end end diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb index 28341d0b42..1192ecd6b4 100644 --- a/activerecord/test/cases/persistence_test.rb +++ b/activerecord/test/cases/persistence_test.rb @@ -858,4 +858,11 @@ class PersistenceTest < ActiveRecord::TestCase post.body end end + + def test_reload_removes_custom_selects + post = Post.select('posts.*, 1 as wibble').last! + + assert_equal 1, post[:wibble] + assert_nil post.reload[:wibble] + end end diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb index c6e73f78cc..acbd065649 100644 --- a/activerecord/test/cases/reflection_test.rb +++ b/activerecord/test/cases/reflection_test.rb @@ -80,24 +80,10 @@ class ReflectionTest < ActiveRecord::TestCase assert_equal :integer, @first.column_for_attribute("id").type end - def test_non_existent_columns_return_null_object - column = @first.column_for_attribute("attribute_that_doesnt_exist") - assert_instance_of ActiveRecord::ConnectionAdapters::NullColumn, column - assert_equal "attribute_that_doesnt_exist", column.name - assert_equal nil, column.sql_type - assert_equal nil, column.type - assert_not column.number? - assert_not column.text? - assert_not column.binary? - end - - def test_non_existent_columns_are_identity_types - column = @first.column_for_attribute("attribute_that_doesnt_exist") - object = Object.new - - assert_equal object, column.type_cast_from_database(object) - assert_equal object, column.type_cast_from_user(object) - assert_equal object, column.type_cast_for_database(object) + def test_non_existent_columns_return_nil + assert_deprecated do + assert_nil @first.column_for_attribute("attribute_that_doesnt_exist") + end end def test_reflection_klass_for_nested_class_name diff --git a/activerecord/test/cases/result_test.rb b/activerecord/test/cases/result_test.rb index 2131b32a0c..d6decafad9 100644 --- a/activerecord/test/cases/result_test.rb +++ b/activerecord/test/cases/result_test.rb @@ -10,7 +10,7 @@ module ActiveRecord ]) end - def test_to_hash_returns_row_hashes + test "to_hash returns row_hashes" do assert_equal [ {'col_1' => 'row 1 col 1', 'col_2' => 'row 1 col 2'}, {'col_1' => 'row 2 col 1', 'col_2' => 'row 2 col 2'}, @@ -18,13 +18,13 @@ module ActiveRecord ], result.to_hash end - def test_each_with_block_returns_row_hashes + test "each with block returns row hashes" do result.each do |row| assert_equal ['col_1', 'col_2'], row.keys end end - def test_each_without_block_returns_an_enumerator + test "each without block returns an enumerator" do result.each.with_index do |row, index| assert_equal ['col_1', 'col_2'], row.keys assert_kind_of Integer, index @@ -32,9 +32,45 @@ module ActiveRecord end if Enumerator.method_defined? :size - def test_each_without_block_returns_a_sized_enumerator + test "each without block returns a sized enumerator" do assert_equal 3, result.each.size end end + + test "cast_values returns rows after type casting" do + values = [["1.1", "2.2"], ["3.3", "4.4"]] + columns = ["col1", "col2"] + types = { "col1" => Type::Integer.new, "col2" => Type::Float.new } + result = Result.new(columns, values, types) + + assert_equal [[1, 2.2], [3, 4.4]], result.cast_values + end + + test "cast_values uses identity type for unknown types" do + values = [["1.1", "2.2"], ["3.3", "4.4"]] + columns = ["col1", "col2"] + types = { "col1" => Type::Integer.new } + result = Result.new(columns, values, types) + + assert_equal [[1, "2.2"], [3, "4.4"]], result.cast_values + end + + test "cast_values returns single dimensional array if single column" do + values = [["1.1"], ["3.3"]] + columns = ["col1"] + types = { "col1" => Type::Integer.new } + result = Result.new(columns, values, types) + + assert_equal [1, 3], result.cast_values + end + + test "cast_values can receive types to use instead" do + values = [["1.1", "2.2"], ["3.3", "4.4"]] + columns = ["col1", "col2"] + types = { "col1" => Type::Integer.new, "col2" => Type::Float.new } + result = Result.new(columns, values, types) + + assert_equal [[1.1, 2.2], [3.3, 4.4]], result.cast_values("col1" => Type::Float.new) + end end end diff --git a/activerecord/test/cases/types_test.rb b/activerecord/test/cases/types_test.rb index 731f8cfba3..961aae88cb 100644 --- a/activerecord/test/cases/types_test.rb +++ b/activerecord/test/cases/types_test.rb @@ -79,11 +79,29 @@ module ActiveRecord assert_nil type.type_cast_from_user(1.0/0.0) end + def test_changing_integers + type = Type::Integer.new + + assert type.changed?(5, 5, '5wibble') + assert_not type.changed?(5, 5, '5') + assert_not type.changed?(5, 5, '5.0') + assert_not type.changed?(nil, nil, nil) + end + def test_type_cast_float type = Type::Float.new assert_equal 1.0, type.type_cast_from_user("1") end + def test_changing_float + type = Type::Float.new + + assert type.changed?(5.0, 5.0, '5wibble') + assert_not type.changed?(5.0, 5.0, '5') + assert_not type.changed?(5.0, 5.0, '5.0') + assert_not type.changed?(nil, nil, nil) + end + def test_type_cast_decimal type = Type::Decimal.new assert_equal BigDecimal.new("0"), type.type_cast_from_user(BigDecimal.new("0")) |