Skip to content

Commit

Permalink
Refactoring attributes/types [#3348 state:resolved]
Browse files Browse the repository at this point in the history
Signed-off-by: Joshua Peek <josh@joshpeek.com>
  • Loading branch information
Eric Chapweske authored and josh committed Oct 17, 2009
1 parent e13d232 commit f936a1f
Show file tree
Hide file tree
Showing 25 changed files with 760 additions and 148 deletions.
16 changes: 16 additions & 0 deletions activerecord/lib/active_record.rb
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ def self.load_all!
autoload :AssociationPreload, 'active_record/association_preload' autoload :AssociationPreload, 'active_record/association_preload'
autoload :Associations, 'active_record/associations' autoload :Associations, 'active_record/associations'
autoload :AttributeMethods, 'active_record/attribute_methods' autoload :AttributeMethods, 'active_record/attribute_methods'
autoload :Attributes, 'active_record/attributes'
autoload :AutosaveAssociation, 'active_record/autosave_association' autoload :AutosaveAssociation, 'active_record/autosave_association'
autoload :Relation, 'active_record/relation' autoload :Relation, 'active_record/relation'
autoload :Base, 'active_record/base' autoload :Base, 'active_record/base'
Expand All @@ -74,6 +75,7 @@ def self.load_all!
autoload :TestCase, 'active_record/test_case' autoload :TestCase, 'active_record/test_case'
autoload :Timestamp, 'active_record/timestamp' autoload :Timestamp, 'active_record/timestamp'
autoload :Transactions, 'active_record/transactions' autoload :Transactions, 'active_record/transactions'
autoload :Types, 'active_record/types'
autoload :Validator, 'active_record/validator' autoload :Validator, 'active_record/validator'
autoload :Validations, 'active_record/validations' autoload :Validations, 'active_record/validations'


Expand All @@ -87,6 +89,20 @@ module AttributeMethods
autoload :Write, 'active_record/attribute_methods/write' autoload :Write, 'active_record/attribute_methods/write'
end end


module Attributes
autoload :Aliasing, 'active_record/attributes/aliasing'
autoload :Store, 'active_record/attributes/store'
autoload :Typecasting, 'active_record/attributes/typecasting'
end

module Type
autoload :Number, 'active_record/types/number'
autoload :Object, 'active_record/types/object'
autoload :Serialize, 'active_record/types/serialize'
autoload :TimeWithZone, 'active_record/types/time_with_zone'
autoload :Unknown, 'active_record/types/unknown'
end

module Locking module Locking
autoload :Optimistic, 'active_record/locking/optimistic' autoload :Optimistic, 'active_record/locking/optimistic'
autoload :Pessimistic, 'active_record/locking/pessimistic' autoload :Pessimistic, 'active_record/locking/pessimistic'
Expand Down
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -8,25 +8,18 @@ module BeforeTypeCast
end end


def read_attribute_before_type_cast(attr_name) def read_attribute_before_type_cast(attr_name)
@attributes[attr_name] _attributes.without_typecast[attr_name]
end end


# Returns a hash of attributes before typecasting and deserialization. # Returns a hash of attributes before typecasting and deserialization.
def attributes_before_type_cast def attributes_before_type_cast
self.attribute_names.inject({}) do |attrs, name| _attributes.without_typecast
attrs[name] = read_attribute_before_type_cast(name)
attrs
end
end end


private private
# Handle *_before_type_cast for method_missing. # Handle *_before_type_cast for method_missing.
def attribute_before_type_cast(attribute_name) def attribute_before_type_cast(attribute_name)
if attribute_name == 'id' read_attribute_before_type_cast(attribute_name)
read_attribute_before_type_cast(self.class.primary_key)
else
read_attribute_before_type_cast(attribute_name)
end
end end
end end
end end
Expand Down
20 changes: 3 additions & 17 deletions activerecord/lib/active_record/attribute_methods/query.rb
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -8,23 +8,7 @@ module Query
end end


def query_attribute(attr_name) def query_attribute(attr_name)
unless value = read_attribute(attr_name) _attributes.has?(attr_name)
false
else
column = self.class.columns_hash[attr_name]
if column.nil?
if Numeric === value || value !~ /[^0-9]/
!value.to_i.zero?
else
return false if ActiveRecord::ConnectionAdapters::Column::FALSE_VALUES.include?(value)
!value.blank?
end
elsif column.number?
!value.zero?
else
!value.blank?
end
end
end end


private private
Expand All @@ -35,3 +19,5 @@ def attribute?(attribute_name)
end end
end end
end end


49 changes: 4 additions & 45 deletions activerecord/lib/active_record/attribute_methods/read.rb
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -37,30 +37,20 @@ def cache_attribute?(attr_name)


protected protected
def define_method_attribute(attr_name) def define_method_attribute(attr_name)
if self.serialized_attributes[attr_name] define_read_method(attr_name.to_sym, attr_name, columns_hash[attr_name])
define_read_method_for_serialized_attribute(attr_name)
else
define_read_method(attr_name.to_sym, attr_name, columns_hash[attr_name])
end


if attr_name == primary_key && attr_name != "id" if attr_name == primary_key && attr_name != "id"
define_read_method(:id, attr_name, columns_hash[attr_name]) define_read_method(:id, attr_name, columns_hash[attr_name])
end end
end end


private private
# Define read method for serialized attribute.
def define_read_method_for_serialized_attribute(attr_name)
generated_attribute_methods.module_eval("def #{attr_name}; unserialize_attribute('#{attr_name}'); end", __FILE__, __LINE__)
end


# Define an attribute reader method. Cope with nil column. # Define an attribute reader method. Cope with nil column.
def define_read_method(symbol, attr_name, column) def define_read_method(symbol, attr_name, column)
cast_code = column.type_cast_code('v') if column access_code = "_attributes['#{attr_name}']"
access_code = cast_code ? "(v=@attributes['#{attr_name}']) && #{cast_code}" : "@attributes['#{attr_name}']"

unless attr_name.to_s == self.primary_key.to_s unless attr_name.to_s == self.primary_key.to_s
access_code = access_code.insert(0, "missing_attribute('#{attr_name}', caller) unless @attributes.has_key?('#{attr_name}'); ") access_code = access_code.insert(0, "missing_attribute('#{attr_name}', caller) unless _attributes.key?('#{attr_name}'); ")
end end


if cache_attribute?(attr_name) if cache_attribute?(attr_name)
Expand All @@ -73,38 +63,7 @@ def define_read_method(symbol, attr_name, column)
# Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example, # Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example,
# "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)). # "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)).
def read_attribute(attr_name) def read_attribute(attr_name)
attr_name = attr_name.to_s _attributes[attr_name]
attr_name = self.class.primary_key if attr_name == 'id'
if !(value = @attributes[attr_name]).nil?
if column = column_for_attribute(attr_name)
if unserializable_attribute?(attr_name, column)
unserialize_attribute(attr_name)
else
column.type_cast(value)
end
else
value
end
else
nil
end
end

# Returns true if the attribute is of a text column and marked for serialization.
def unserializable_attribute?(attr_name, column)
column.text? && self.class.serialized_attributes[attr_name]
end

# Returns the unserialized object of the attribute.
def unserialize_attribute(attr_name)
unserialized_object = object_from_yaml(@attributes[attr_name])

if unserialized_object.is_a?(self.class.serialized_attributes[attr_name]) || unserialized_object.nil?
@attributes.frozen? ? unserialized_object : @attributes[attr_name] = unserialized_object
else
raise SerializationTypeMismatch,
"#{attr_name} was supposed to be a #{self.class.serialized_attributes[attr_name]}, but was a #{unserialized_object.class.to_s}"
end
end end


private private
Expand Down
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -12,48 +12,20 @@ module TimeZoneConversion
end end


module ClassMethods module ClassMethods

def cache_attribute?(attr_name)
time_zone_aware?(attr_name) || super
end

protected protected
# Defined for all +datetime+ and +timestamp+ attributes when +time_zone_aware_attributes+ are enabled.
# This enhanced read method automatically converts the UTC time stored in the database to the time zone stored in Time.zone.
def define_method_attribute(attr_name)
if create_time_zone_conversion_attribute?(attr_name, columns_hash[attr_name])
method_body = <<-EOV
def #{attr_name}(reload = false)
cached = @attributes_cache['#{attr_name}']
return cached if cached && !reload
time = read_attribute('#{attr_name}')
@attributes_cache['#{attr_name}'] = time.acts_like?(:time) ? time.in_time_zone : time
end
EOV
generated_attribute_methods.module_eval(method_body, __FILE__, __LINE__)
else
super
end
end


# Defined for all +datetime+ and +timestamp+ attributes when +time_zone_aware_attributes+ are enabled. def time_zone_aware?(attr_name)
# This enhanced write method will automatically convert the time passed to it to the zone stored in Time.zone. column = columns_hash[attr_name]
def define_method_attribute=(attr_name) time_zone_aware_attributes &&
if create_time_zone_conversion_attribute?(attr_name, columns_hash[attr_name]) !skip_time_zone_conversion_for_attributes.include?(attr_name.to_sym) &&
method_body = <<-EOV [:datetime, :timestamp].include?(column.type)
def #{attr_name}=(time)
unless time.acts_like?(:time)
time = time.is_a?(String) ? Time.zone.parse(time) : time.to_time rescue time
end
time = time.in_time_zone rescue nil if time
write_attribute(:#{attr_name}, time)
end
EOV
generated_attribute_methods.module_eval(method_body, __FILE__, __LINE__)
else
super
end
end end


private
def create_time_zone_conversion_attribute?(name, column)
time_zone_aware_attributes && !skip_time_zone_conversion_for_attributes.include?(name.to_sym) && [:datetime, :timestamp].include?(column.type)
end
end end
end end
end end
Expand Down
9 changes: 2 additions & 7 deletions activerecord/lib/active_record/attribute_methods/write.rb
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -17,14 +17,9 @@ def define_method_attribute=(attr_name)
# Updates the attribute identified by <tt>attr_name</tt> with the specified +value+. Empty strings for fixnum and float # Updates the attribute identified by <tt>attr_name</tt> with the specified +value+. Empty strings for fixnum and float
# columns are turned into +nil+. # columns are turned into +nil+.
def write_attribute(attr_name, value) def write_attribute(attr_name, value)
attr_name = attr_name.to_s attr_name = _attributes.unalias(attr_name)
attr_name = self.class.primary_key if attr_name == 'id'
@attributes_cache.delete(attr_name) @attributes_cache.delete(attr_name)
if (column = column_for_attribute(attr_name)) && column.number? _attributes[attr_name] = value
@attributes[attr_name] = convert_number_column_value(value)
else
@attributes[attr_name] = value
end
end end


private private
Expand Down
37 changes: 37 additions & 0 deletions activerecord/lib/active_record/attributes.rb
Original file line number Original file line Diff line number Diff line change
@@ -0,0 +1,37 @@
module ActiveRecord
module Attributes

# Returns true if the given attribute is in the attributes hash
def has_attribute?(attr_name)
_attributes.key?(attr_name)
end

# Returns an array of names for the attributes available on this object sorted alphabetically.
def attribute_names
_attributes.keys.sort!
end

# Returns a hash of all the attributes with their names as keys and the values of the attributes as values.
def attributes
attributes = _attributes.dup
attributes.typecast! unless _attributes.frozen?
attributes.to_h
end

protected

# Not to be confused with the public #attributes method, which returns a typecasted Hash.
def _attributes
@attributes
end

def initialize_attribute_store(merge_attributes = nil)
@attributes = ActiveRecord::Attributes::Store.new
@attributes.merge!(merge_attributes) if merge_attributes
@attributes.types.merge!(self.class.attribute_types)
@attributes.aliases.merge!('id' => self.class.primary_key) unless 'id' == self.class.primary_key
@attributes
end

end
end
42 changes: 42 additions & 0 deletions activerecord/lib/active_record/attributes/aliasing.rb
Original file line number Original file line Diff line number Diff line change
@@ -0,0 +1,42 @@
module ActiveRecord
module Attributes
module Aliasing
# Allows access to keys using aliased names.
#
# Example:
# class Attributes < Hash
# include Aliasing
# end
#
# attributes = Attributes.new
# attributes.aliases['id'] = 'fancy_primary_key'
# attributes['fancy_primary_key'] = 2020
#
# attributes['id']
# => 2020
#
# Additionally, symbols are always aliases of strings:
# attributes[:fancy_primary_key]
# => 2020
#
def [](key)
super(unalias(key))
end

def []=(key, value)
super(unalias(key), value)
end

def aliases
@aliases ||= {}
end

def unalias(key)
key = key.to_s
aliases[key] || key
end

end
end
end

15 changes: 15 additions & 0 deletions activerecord/lib/active_record/attributes/store.rb
Original file line number Original file line Diff line number Diff line change
@@ -0,0 +1,15 @@
module ActiveRecord
module Attributes
class Store < Hash
include ActiveRecord::Attributes::Typecasting
include ActiveRecord::Attributes::Aliasing

# Attributes not mapped to a column are handled using Type::Unknown,
# which enables boolean typecasting for unmapped keys.
def types
@types ||= Hash.new(Type::Unknown.new)
end

end
end
end
Loading

2 comments on commit f936a1f

@rubys
Copy link
Contributor

@rubys rubys commented on f936a1f Oct 17, 2009

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change causes decimal objects to be treated as strings, leading to errors in existing applications such as "comparison of String with Float failed" in validation routines such as a the following:

def price_must_be_at_least_a_cent
  errors.add(:price, 'should be at least 0.01') if price.nil? || price < 0.00
end

Full scenario and captured output here:

http://intertwingly.net/projects/AWDwR3/checkdepot.html#section-6.4

@rubys
Copy link
Contributor

@rubys rubys commented on f936a1f Oct 17, 2009

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.