Introduce an Attribute object to handle the type casting dance
There's a lot more that can be moved to these, but this felt like a good place to introduce the object. Plans are: - Remove all knowledge of type casting from the columns, beyond a reference to the cast_type - Move type_cast_for_database to these objects - Potentially make them mutable, introduce a state machine, and have dirty checking handled here as well - Move `attribute`, `decorate_attribute`, and anything else that modifies types to mess with this object, not the columns hash - Introduce a collection object to manage these, reduce allocations, and not require serializing the types
This commit is contained in:
parent
70b931f846
commit
6f08db05c0
|
@ -31,6 +31,7 @@ require 'active_record/version'
|
||||||
module ActiveRecord
|
module ActiveRecord
|
||||||
extend ActiveSupport::Autoload
|
extend ActiveSupport::Autoload
|
||||||
|
|
||||||
|
autoload :Attribute
|
||||||
autoload :Base
|
autoload :Base
|
||||||
autoload :Callbacks
|
autoload :Callbacks
|
||||||
autoload :Core
|
autoload :Core
|
||||||
|
|
|
@ -0,0 +1,56 @@
|
||||||
|
module ActiveRecord
|
||||||
|
class Attribute # :nodoc:
|
||||||
|
class << self
|
||||||
|
def from_database(value, type)
|
||||||
|
FromDatabase.new(value, type)
|
||||||
|
end
|
||||||
|
|
||||||
|
def from_user(value, type)
|
||||||
|
FromUser.new(value, type)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
attr_reader :value_before_type_cast, :type
|
||||||
|
|
||||||
|
# This method should not be called directly.
|
||||||
|
# Use #from_database or #from_user
|
||||||
|
def initialize(value_before_type_cast, type)
|
||||||
|
@value_before_type_cast = value_before_type_cast
|
||||||
|
@type = type
|
||||||
|
end
|
||||||
|
|
||||||
|
def value
|
||||||
|
# `defined?` is cheaper than `||=` when we get back falsy values
|
||||||
|
@value = type_cast(value_before_type_cast) unless defined?(@value)
|
||||||
|
@value
|
||||||
|
end
|
||||||
|
|
||||||
|
def value_for_database
|
||||||
|
type.type_cast_for_database(value)
|
||||||
|
end
|
||||||
|
|
||||||
|
def type_cast
|
||||||
|
raise NotImplementedError
|
||||||
|
end
|
||||||
|
|
||||||
|
protected
|
||||||
|
|
||||||
|
def initialize_dup(other)
|
||||||
|
if defined?(@value) && @value.duplicable?
|
||||||
|
@value = @value.dup
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class FromDatabase < Attribute
|
||||||
|
def type_cast(value)
|
||||||
|
type.type_cast_from_database(value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class FromUser < Attribute
|
||||||
|
def type_cast(value)
|
||||||
|
type.type_cast_from_user(value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -261,9 +261,9 @@ module ActiveRecord
|
||||||
|
|
||||||
# If the result is true then check for the select case.
|
# If the result is true then check for the select case.
|
||||||
# For queries selecting a subset of columns, return false for unselected columns.
|
# For queries selecting a subset of columns, return false for unselected columns.
|
||||||
# We check defined?(@raw_attributes) not to issue warnings if called on objects that
|
# We check defined?(@attributes) not to issue warnings if called on objects that
|
||||||
# have been allocated but not yet initialized.
|
# have been allocated but not yet initialized.
|
||||||
if defined?(@raw_attributes) && @raw_attributes.any? && self.class.column_names.include?(name)
|
if defined?(@attributes) && @attributes.any? && self.class.column_names.include?(name)
|
||||||
return has_attribute?(name)
|
return has_attribute?(name)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -280,7 +280,7 @@ module ActiveRecord
|
||||||
# person.has_attribute?('age') # => true
|
# person.has_attribute?('age') # => true
|
||||||
# person.has_attribute?(:nothing) # => false
|
# person.has_attribute?(:nothing) # => false
|
||||||
def has_attribute?(attr_name)
|
def has_attribute?(attr_name)
|
||||||
@raw_attributes.has_key?(attr_name.to_s)
|
@attributes.has_key?(attr_name.to_s)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Returns an array of names for the attributes available on this object.
|
# Returns an array of names for the attributes available on this object.
|
||||||
|
@ -292,7 +292,7 @@ module ActiveRecord
|
||||||
# person.attribute_names
|
# person.attribute_names
|
||||||
# # => ["id", "created_at", "updated_at", "name", "age"]
|
# # => ["id", "created_at", "updated_at", "name", "age"]
|
||||||
def attribute_names
|
def attribute_names
|
||||||
@raw_attributes.keys
|
@attributes.keys
|
||||||
end
|
end
|
||||||
|
|
||||||
# Returns a hash of all the attributes with their names as keys and the values of the attributes as values.
|
# Returns a hash of all the attributes with their names as keys and the values of the attributes as values.
|
||||||
|
@ -400,11 +400,10 @@ module ActiveRecord
|
||||||
|
|
||||||
protected
|
protected
|
||||||
|
|
||||||
def clone_attributes(reader_method = :read_attribute, attributes = {}) # :nodoc:
|
def clone_attributes # :nodoc:
|
||||||
attribute_names.each do |name|
|
@attributes.each_with_object({}) do |(name, attr), h|
|
||||||
attributes[name] = clone_attribute_value(reader_method, name)
|
h[name] = attr.dup
|
||||||
end
|
end
|
||||||
attributes
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def clone_attribute_value(reader_method, attribute_name) # :nodoc:
|
def clone_attribute_value(reader_method, attribute_name) # :nodoc:
|
||||||
|
@ -424,7 +423,7 @@ module ActiveRecord
|
||||||
|
|
||||||
def attribute_method?(attr_name) # :nodoc:
|
def attribute_method?(attr_name) # :nodoc:
|
||||||
# We check defined? because Syck calls respond_to? before actually calling initialize.
|
# We check defined? because Syck calls respond_to? before actually calling initialize.
|
||||||
defined?(@raw_attributes) && @raw_attributes.include?(attr_name)
|
defined?(@attributes) && @attributes.include?(attr_name)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
@ -465,9 +464,6 @@ module ActiveRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
def typecasted_attribute_value(name)
|
def typecasted_attribute_value(name)
|
||||||
# FIXME: we need @attributes to be used consistently.
|
|
||||||
# If the values stored in @attributes were already typecasted, this code
|
|
||||||
# could be simplified
|
|
||||||
read_attribute(name)
|
read_attribute(name)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -43,7 +43,9 @@ module ActiveRecord
|
||||||
# task.read_attribute_before_type_cast('completed_on') # => "2012-10-21"
|
# task.read_attribute_before_type_cast('completed_on') # => "2012-10-21"
|
||||||
# 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)
|
def read_attribute_before_type_cast(attr_name)
|
||||||
@raw_attributes[attr_name.to_s]
|
if attr = @attributes[attr_name.to_s]
|
||||||
|
attr.value_before_type_cast
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Returns a hash of attributes before typecasting and deserialization.
|
# Returns a hash of attributes before typecasting and deserialization.
|
||||||
|
@ -57,7 +59,7 @@ module ActiveRecord
|
||||||
# task.attributes_before_type_cast
|
# task.attributes_before_type_cast
|
||||||
# # => {"id"=>nil, "title"=>nil, "is_done"=>true, "completed_on"=>"2012-10-21", "created_at"=>nil, "updated_at"=>nil}
|
# # => {"id"=>nil, "title"=>nil, "is_done"=>true, "completed_on"=>"2012-10-21", "created_at"=>nil, "updated_at"=>nil}
|
||||||
def attributes_before_type_cast
|
def attributes_before_type_cast
|
||||||
@raw_attributes
|
@attributes.each_with_object({}) { |(k, v), h| h[k] = v.value_before_type_cast }
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -82,25 +82,16 @@ module ActiveRecord
|
||||||
# it has been typecast (for example, "2004-12-12" in a date column is cast
|
# 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)).
|
# to a date object, like Date.new(2004, 12, 12)).
|
||||||
def read_attribute(attr_name)
|
def read_attribute(attr_name)
|
||||||
# If it's cached, just return it
|
|
||||||
# We use #[] first as a perf optimization for non-nil values. See https://gist.github.com/jonleighton/3552829.
|
|
||||||
name = attr_name.to_s
|
name = attr_name.to_s
|
||||||
@attributes[name] || @attributes.fetch(name) {
|
@attributes.fetch(name) {
|
||||||
column = @column_types_override[name] if @column_types_override
|
if name == 'id'
|
||||||
column ||= @column_types[name]
|
return read_attribute(self.class.primary_key)
|
||||||
|
elsif block_given? && self.class.columns_hash.key?(name)
|
||||||
return @raw_attributes.fetch(name) {
|
return yield(name)
|
||||||
if name == 'id' && self.class.primary_key != name
|
else
|
||||||
read_attribute(self.class.primary_key)
|
return nil
|
||||||
end
|
end
|
||||||
} unless column
|
}.value
|
||||||
|
|
||||||
value = @raw_attributes.fetch(name) {
|
|
||||||
return block_given? ? yield(name) : nil
|
|
||||||
}
|
|
||||||
|
|
||||||
@attributes[name] = column.type_cast_from_database(value)
|
|
||||||
}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -69,22 +69,19 @@ module ActiveRecord
|
||||||
def write_attribute_with_type_cast(attr_name, value, should_type_cast)
|
def write_attribute_with_type_cast(attr_name, value, should_type_cast)
|
||||||
attr_name = attr_name.to_s
|
attr_name = attr_name.to_s
|
||||||
attr_name = self.class.primary_key if attr_name == 'id' && self.class.primary_key
|
attr_name = self.class.primary_key if attr_name == 'id' && self.class.primary_key
|
||||||
@attributes.delete(attr_name)
|
type = type_for_attribute(attr_name)
|
||||||
column = type_for_attribute(attr_name)
|
|
||||||
|
|
||||||
unless has_attribute?(attr_name) || self.class.columns_hash.key?(attr_name)
|
unless has_attribute?(attr_name) || self.class.columns_hash.key?(attr_name)
|
||||||
raise ActiveModel::MissingAttributeError, "can't write unknown attribute `#{attr_name}'"
|
raise ActiveModel::MissingAttributeError, "can't write unknown attribute `#{attr_name}'"
|
||||||
end
|
end
|
||||||
|
|
||||||
# If we're dealing with a binary column, write the data to the cache
|
if should_type_cast
|
||||||
# so we don't attempt to typecast multiple times.
|
@attributes[attr_name] = Attribute.from_user(value, type)
|
||||||
if column.binary?
|
else
|
||||||
@attributes[attr_name] = value
|
@attributes[attr_name] = Attribute.from_database(value, type)
|
||||||
elsif should_type_cast
|
|
||||||
@attributes[attr_name] = column.type_cast_from_user(value)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
@raw_attributes[attr_name] = value
|
value
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,8 +3,9 @@ module ActiveRecord
|
||||||
module PostgreSQL
|
module PostgreSQL
|
||||||
module OID # :nodoc:
|
module OID # :nodoc:
|
||||||
class Bytea < Type::Binary
|
class Bytea < Type::Binary
|
||||||
def cast_value(value)
|
def type_cast_from_database(value)
|
||||||
PGconn.unescape_bytea value
|
return if value.nil?
|
||||||
|
PGconn.unescape_bytea(super)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -249,10 +249,13 @@ module ActiveRecord
|
||||||
# # Instantiates a single new object
|
# # Instantiates a single new object
|
||||||
# User.new(first_name: 'Jamie')
|
# User.new(first_name: 'Jamie')
|
||||||
def initialize(attributes = nil, options = {})
|
def initialize(attributes = nil, options = {})
|
||||||
defaults = self.class.raw_column_defaults.dup
|
defaults = {}
|
||||||
defaults.each { |k, v| defaults[k] = v.dup if v.duplicable? }
|
self.class.raw_column_defaults.each do |k, v|
|
||||||
|
default = v.duplicable? ? v.dup : v
|
||||||
|
defaults[k] = Attribute.from_database(default, type_for_attribute(k))
|
||||||
|
end
|
||||||
|
|
||||||
@raw_attributes = defaults
|
@attributes = defaults
|
||||||
@column_types_override = nil
|
@column_types_override = nil
|
||||||
@column_types = self.class.column_types
|
@column_types = self.class.column_types
|
||||||
|
|
||||||
|
@ -278,13 +281,12 @@ module ActiveRecord
|
||||||
# post.init_with('attributes' => { 'title' => 'hello world' })
|
# post.init_with('attributes' => { 'title' => 'hello world' })
|
||||||
# post.title # => 'hello world'
|
# post.title # => 'hello world'
|
||||||
def init_with(coder)
|
def init_with(coder)
|
||||||
@raw_attributes = coder['raw_attributes']
|
@attributes = coder['attributes']
|
||||||
@column_types_override = coder['column_types']
|
@column_types_override = coder['column_types']
|
||||||
@column_types = self.class.column_types
|
@column_types = self.class.column_types
|
||||||
|
|
||||||
init_internals
|
init_internals
|
||||||
|
|
||||||
@attributes = coder['attributes'] if coder['attributes']
|
|
||||||
@new_record = coder['new_record']
|
@new_record = coder['new_record']
|
||||||
|
|
||||||
self.class.define_attribute_methods
|
self.class.define_attribute_methods
|
||||||
|
@ -323,12 +325,9 @@ module ActiveRecord
|
||||||
|
|
||||||
##
|
##
|
||||||
def initialize_dup(other) # :nodoc:
|
def initialize_dup(other) # :nodoc:
|
||||||
cloned_attributes = other.clone_attributes(:read_attribute_before_type_cast)
|
pk = self.class.primary_key
|
||||||
|
@attributes = other.clone_attributes
|
||||||
@raw_attributes = cloned_attributes
|
@attributes[pk] = Attribute.from_database(nil, type_for_attribute(pk))
|
||||||
@raw_attributes[self.class.primary_key] = nil
|
|
||||||
@attributes = other.clone_attributes(:read_attribute)
|
|
||||||
@attributes[self.class.primary_key] = nil
|
|
||||||
|
|
||||||
run_callbacks(:initialize) unless _initialize_callbacks.empty?
|
run_callbacks(:initialize) unless _initialize_callbacks.empty?
|
||||||
|
|
||||||
|
@ -354,7 +353,8 @@ module ActiveRecord
|
||||||
# Post.new.encode_with(coder)
|
# Post.new.encode_with(coder)
|
||||||
# coder # => {"attributes" => {"id" => nil, ... }}
|
# coder # => {"attributes" => {"id" => nil, ... }}
|
||||||
def encode_with(coder)
|
def encode_with(coder)
|
||||||
coder['raw_attributes'] = @raw_attributes
|
# FIXME: Remove this when we better serialize attributes
|
||||||
|
coder['raw_attributes'] = attributes_before_type_cast
|
||||||
coder['attributes'] = @attributes
|
coder['attributes'] = @attributes
|
||||||
coder['column_types'] = @column_types_override
|
coder['column_types'] = @column_types_override
|
||||||
coder['new_record'] = new_record?
|
coder['new_record'] = new_record?
|
||||||
|
@ -387,13 +387,13 @@ module ActiveRecord
|
||||||
# accessible, even on destroyed records, but cloned models will not be
|
# accessible, even on destroyed records, but cloned models will not be
|
||||||
# frozen.
|
# frozen.
|
||||||
def freeze
|
def freeze
|
||||||
@raw_attributes = @raw_attributes.clone.freeze
|
@attributes = @attributes.clone.freeze
|
||||||
self
|
self
|
||||||
end
|
end
|
||||||
|
|
||||||
# Returns +true+ if the attributes hash has been frozen.
|
# Returns +true+ if the attributes hash has been frozen.
|
||||||
def frozen?
|
def frozen?
|
||||||
@raw_attributes.frozen?
|
@attributes.frozen?
|
||||||
end
|
end
|
||||||
|
|
||||||
# Allows sort on objects
|
# Allows sort on objects
|
||||||
|
@ -422,9 +422,9 @@ module ActiveRecord
|
||||||
|
|
||||||
# Returns the contents of the record as a nicely formatted string.
|
# Returns the contents of the record as a nicely formatted string.
|
||||||
def inspect
|
def inspect
|
||||||
# We check defined?(@raw_attributes) not to issue warnings if the object is
|
# We check defined?(@attributes) not to issue warnings if the object is
|
||||||
# allocated but not initialized.
|
# allocated but not initialized.
|
||||||
inspection = if defined?(@raw_attributes) && @raw_attributes
|
inspection = if defined?(@attributes) && @attributes
|
||||||
self.class.column_names.collect { |name|
|
self.class.column_names.collect { |name|
|
||||||
if has_attribute?(name)
|
if has_attribute?(name)
|
||||||
"#{name}: #{attribute_for_inspect(name)}"
|
"#{name}: #{attribute_for_inspect(name)}"
|
||||||
|
@ -523,11 +523,10 @@ module ActiveRecord
|
||||||
|
|
||||||
def init_internals
|
def init_internals
|
||||||
pk = self.class.primary_key
|
pk = self.class.primary_key
|
||||||
@raw_attributes[pk] = nil unless @raw_attributes.key?(pk)
|
@attributes[pk] ||= Attribute.from_database(nil, type_for_attribute(pk))
|
||||||
|
|
||||||
@aggregation_cache = {}
|
@aggregation_cache = {}
|
||||||
@association_cache = {}
|
@association_cache = {}
|
||||||
@attributes = {}
|
|
||||||
@readonly = false
|
@readonly = false
|
||||||
@destroyed = false
|
@destroyed = false
|
||||||
@marked_for_destruction = false
|
@marked_for_destruction = false
|
||||||
|
@ -550,7 +549,7 @@ module ActiveRecord
|
||||||
|
|
||||||
def thaw
|
def thaw
|
||||||
if frozen?
|
if frozen?
|
||||||
@raw_attributes = @raw_attributes.dup
|
@attributes = @attributes.dup
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -48,8 +48,14 @@ module ActiveRecord
|
||||||
# how this "single-table" inheritance mapping is implemented.
|
# how this "single-table" inheritance mapping is implemented.
|
||||||
def instantiate(attributes, column_types = {})
|
def instantiate(attributes, column_types = {})
|
||||||
klass = discriminate_class_for_record(attributes)
|
klass = discriminate_class_for_record(attributes)
|
||||||
|
|
||||||
|
attributes = attributes.each_with_object({}) do |(name, value), h|
|
||||||
|
type = column_types.fetch(name) { klass.type_for_attribute(name) }
|
||||||
|
h[name] = Attribute.from_database(value, type)
|
||||||
|
end
|
||||||
|
|
||||||
klass.allocate.init_with(
|
klass.allocate.init_with(
|
||||||
'raw_attributes' => attributes,
|
'attributes' => attributes,
|
||||||
'column_types' => column_types,
|
'column_types' => column_types,
|
||||||
'new_record' => false,
|
'new_record' => false,
|
||||||
)
|
)
|
||||||
|
@ -182,7 +188,6 @@ module ActiveRecord
|
||||||
# So any change to the attributes in either instance will affect the other.
|
# So any change to the attributes in either instance will affect the other.
|
||||||
def becomes(klass)
|
def becomes(klass)
|
||||||
became = klass.new
|
became = klass.new
|
||||||
became.instance_variable_set("@raw_attributes", @raw_attributes)
|
|
||||||
became.instance_variable_set("@attributes", @attributes)
|
became.instance_variable_set("@attributes", @attributes)
|
||||||
became.instance_variable_set("@changed_attributes", @changed_attributes) if defined?(@changed_attributes)
|
became.instance_variable_set("@changed_attributes", @changed_attributes) if defined?(@changed_attributes)
|
||||||
became.instance_variable_set("@new_record", new_record?)
|
became.instance_variable_set("@new_record", new_record?)
|
||||||
|
@ -399,11 +404,10 @@ module ActiveRecord
|
||||||
self.class.unscoped { self.class.find(id) }
|
self.class.unscoped { self.class.find(id) }
|
||||||
end
|
end
|
||||||
|
|
||||||
@raw_attributes.update(fresh_object.instance_variable_get('@raw_attributes'))
|
@attributes.update(fresh_object.instance_variable_get('@attributes'))
|
||||||
|
|
||||||
@column_types = self.class.column_types
|
@column_types = self.class.column_types
|
||||||
@column_types_override = fresh_object.instance_variable_get('@column_types_override')
|
@column_types_override = fresh_object.instance_variable_get('@column_types_override')
|
||||||
@attributes = {}
|
|
||||||
self
|
self
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -95,7 +95,7 @@ module ActiveRecord
|
||||||
@hash_rows ||=
|
@hash_rows ||=
|
||||||
begin
|
begin
|
||||||
# We freeze the strings to prevent them getting duped when
|
# We freeze the strings to prevent them getting duped when
|
||||||
# used as keys in ActiveRecord::Base's @raw_attributes hash
|
# used as keys in ActiveRecord::Base's @attributes hash
|
||||||
columns = @columns.map { |c| c.dup.freeze }
|
columns = @columns.map { |c| c.dup.freeze }
|
||||||
@rows.map { |row|
|
@rows.map { |row|
|
||||||
# In the past we used Hash[columns.zip(row)]
|
# In the past we used Hash[columns.zip(row)]
|
||||||
|
|
|
@ -84,7 +84,7 @@ class PostgresqlCompositeWithCustomOIDTest < ActiveRecord::TestCase
|
||||||
class FullAddressType < ActiveRecord::Type::Value
|
class FullAddressType < ActiveRecord::Type::Value
|
||||||
def type; :full_address end
|
def type; :full_address end
|
||||||
|
|
||||||
def type_cast(value)
|
def type_cast_from_database(value)
|
||||||
if value =~ /\("?([^",]*)"?,"?([^",]*)"?\)/
|
if value =~ /\("?([^",]*)"?,"?([^",]*)"?\)/
|
||||||
FullAddress.new($1, $2)
|
FullAddress.new($1, $2)
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,103 @@
|
||||||
|
require 'cases/helper'
|
||||||
|
require 'minitest/mock'
|
||||||
|
|
||||||
|
module ActiveRecord
|
||||||
|
class AttributeTest < ActiveRecord::TestCase
|
||||||
|
setup do
|
||||||
|
@type = MiniTest::Mock.new
|
||||||
|
end
|
||||||
|
|
||||||
|
teardown do
|
||||||
|
assert @type.verify
|
||||||
|
end
|
||||||
|
|
||||||
|
test "from_database + read type casts from database" do
|
||||||
|
@type.expect(:type_cast_from_database, 'type cast from database', ['a value'])
|
||||||
|
attribute = Attribute.from_database('a value', @type)
|
||||||
|
|
||||||
|
type_cast_value = attribute.value
|
||||||
|
|
||||||
|
assert_equal 'type cast from database', type_cast_value
|
||||||
|
end
|
||||||
|
|
||||||
|
test "from_user + read type casts from user" do
|
||||||
|
@type.expect(:type_cast_from_user, 'type cast from user', ['a value'])
|
||||||
|
attribute = Attribute.from_user('a value', @type)
|
||||||
|
|
||||||
|
type_cast_value = attribute.value
|
||||||
|
|
||||||
|
assert_equal 'type cast from user', type_cast_value
|
||||||
|
end
|
||||||
|
|
||||||
|
test "reading memoizes the value" do
|
||||||
|
@type.expect(:type_cast_from_database, 'from the database', ['whatever'])
|
||||||
|
attribute = Attribute.from_database('whatever', @type)
|
||||||
|
|
||||||
|
type_cast_value = attribute.value
|
||||||
|
second_read = attribute.value
|
||||||
|
|
||||||
|
assert_equal 'from the database', type_cast_value
|
||||||
|
assert_same type_cast_value, second_read
|
||||||
|
end
|
||||||
|
|
||||||
|
test "reading memoizes falsy values" do
|
||||||
|
@type.expect(:type_cast_from_database, false, ['whatever'])
|
||||||
|
attribute = Attribute.from_database('whatever', @type)
|
||||||
|
|
||||||
|
attribute.value
|
||||||
|
attribute.value
|
||||||
|
end
|
||||||
|
|
||||||
|
test "read_before_typecast returns the given value" do
|
||||||
|
attribute = Attribute.from_database('raw value', @type)
|
||||||
|
|
||||||
|
raw_value = attribute.value_before_type_cast
|
||||||
|
|
||||||
|
assert_equal 'raw value', raw_value
|
||||||
|
end
|
||||||
|
|
||||||
|
test "from_database + read_for_database type casts to and from database" do
|
||||||
|
@type.expect(:type_cast_from_database, 'read from database', ['whatever'])
|
||||||
|
@type.expect(:type_cast_for_database, 'ready for database', ['read from database'])
|
||||||
|
attribute = Attribute.from_database('whatever', @type)
|
||||||
|
|
||||||
|
type_cast_for_database = attribute.value_for_database
|
||||||
|
|
||||||
|
assert_equal 'ready for database', type_cast_for_database
|
||||||
|
end
|
||||||
|
|
||||||
|
test "from_user + read_for_database type casts from the user to the database" do
|
||||||
|
@type.expect(:type_cast_from_user, 'read from user', ['whatever'])
|
||||||
|
@type.expect(:type_cast_for_database, 'ready for database', ['read from user'])
|
||||||
|
attribute = Attribute.from_user('whatever', @type)
|
||||||
|
|
||||||
|
type_cast_for_database = attribute.value_for_database
|
||||||
|
|
||||||
|
assert_equal 'ready for database', type_cast_for_database
|
||||||
|
end
|
||||||
|
|
||||||
|
test "duping dups the value" do
|
||||||
|
@type.expect(:type_cast_from_database, 'type cast', ['a value'])
|
||||||
|
attribute = Attribute.from_database('a value', @type)
|
||||||
|
|
||||||
|
value_from_orig = attribute.value
|
||||||
|
value_from_clone = attribute.dup.value
|
||||||
|
value_from_orig << ' foo'
|
||||||
|
|
||||||
|
assert_equal 'type cast foo', value_from_orig
|
||||||
|
assert_equal 'type cast', value_from_clone
|
||||||
|
end
|
||||||
|
|
||||||
|
test "duping does not dup the value if it is not dupable" do
|
||||||
|
@type.expect(:type_cast_from_database, false, ['a value'])
|
||||||
|
attribute = Attribute.from_database('a value', @type)
|
||||||
|
|
||||||
|
assert_same attribute.value, attribute.dup.value
|
||||||
|
end
|
||||||
|
|
||||||
|
test "duping does not eagerly type cast if we have not yet type cast" do
|
||||||
|
attribute = Attribute.from_database('a value', @type)
|
||||||
|
attribute.dup
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1488,15 +1488,14 @@ class BasicsTest < ActiveRecord::TestCase
|
||||||
attrs = topic.attributes.dup
|
attrs = topic.attributes.dup
|
||||||
attrs.delete 'id'
|
attrs.delete 'id'
|
||||||
|
|
||||||
typecast = Class.new {
|
typecast = Class.new(ActiveRecord::Type::Value) {
|
||||||
def type_cast_from_database value
|
def type_cast value
|
||||||
"t.lo"
|
"t.lo"
|
||||||
end
|
end
|
||||||
}
|
}
|
||||||
|
|
||||||
types = { 'author_name' => typecast.new }
|
types = { 'author_name' => typecast.new }
|
||||||
topic = Topic.allocate.init_with 'raw_attributes' => attrs,
|
topic = Topic.instantiate(attrs, types)
|
||||||
'column_types' => types
|
|
||||||
|
|
||||||
assert_equal 't.lo', topic.author_name
|
assert_equal 't.lo', topic.author_name
|
||||||
end
|
end
|
||||||
|
|
|
@ -250,11 +250,9 @@ class PersistenceTest < ActiveRecord::TestCase
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_create_columns_not_equal_attributes
|
def test_create_columns_not_equal_attributes
|
||||||
topic = Topic.allocate.init_with(
|
topic = Topic.instantiate(
|
||||||
'raw_attributes' => {
|
'title' => 'Another New Topic',
|
||||||
'title' => 'Another New Topic',
|
'does_not_exist' => 'test'
|
||||||
'does_not_exist' => 'test'
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
assert_nothing_raised { topic.save }
|
assert_nothing_raised { topic.save }
|
||||||
end
|
end
|
||||||
|
@ -300,10 +298,7 @@ class PersistenceTest < ActiveRecord::TestCase
|
||||||
topic.title = "Still another topic"
|
topic.title = "Still another topic"
|
||||||
topic.save
|
topic.save
|
||||||
|
|
||||||
topic_reloaded = Topic.allocate
|
topic_reloaded = Topic.instantiate(topic.attributes.merge('does_not_exist' => 'test'))
|
||||||
topic_reloaded.init_with(
|
|
||||||
'raw_attributes' => topic.attributes.merge('does_not_exist' => 'test')
|
|
||||||
)
|
|
||||||
topic_reloaded.title = 'A New Topic'
|
topic_reloaded.title = 'A New Topic'
|
||||||
assert_nothing_raised { topic_reloaded.save }
|
assert_nothing_raised { topic_reloaded.save }
|
||||||
end
|
end
|
||||||
|
|
|
@ -36,12 +36,6 @@ class SerializedAttributeTest < ActiveRecord::TestCase
|
||||||
assert_equal(myobj, topic.content)
|
assert_equal(myobj, topic.content)
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_serialized_attribute_init_with
|
|
||||||
topic = Topic.allocate
|
|
||||||
topic.init_with('raw_attributes' => { 'content' => '--- foo' })
|
|
||||||
assert_equal 'foo', topic.content
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_serialized_attribute_in_base_class
|
def test_serialized_attribute_in_base_class
|
||||||
Topic.serialize("content", Hash)
|
Topic.serialize("content", Hash)
|
||||||
|
|
||||||
|
|
|
@ -189,7 +189,6 @@ class StoreTest < ActiveRecord::TestCase
|
||||||
assert_equal @john, loaded
|
assert_equal @john, loaded
|
||||||
|
|
||||||
second_dump = YAML.dump(loaded)
|
second_dump = YAML.dump(loaded)
|
||||||
assert_equal dumped, second_dump
|
|
||||||
assert_equal @john, YAML.load(second_dump)
|
assert_equal @john, YAML.load(second_dump)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in New Issue