aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record/type
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord/lib/active_record/type')
-rw-r--r--activerecord/lib/active_record/type/big_integer.rb13
-rw-r--r--activerecord/lib/active_record/type/binary.rb14
-rw-r--r--activerecord/lib/active_record/type/boolean.rb13
-rw-r--r--activerecord/lib/active_record/type/date_time.rb10
-rw-r--r--activerecord/lib/active_record/type/decimal.rb27
-rw-r--r--activerecord/lib/active_record/type/decimal_without_scale.rb4
-rw-r--r--activerecord/lib/active_record/type/decorator.rb14
-rw-r--r--activerecord/lib/active_record/type/hash_lookup_type_map.rb12
-rw-r--r--activerecord/lib/active_record/type/integer.rb34
-rw-r--r--activerecord/lib/active_record/type/mutable.rb4
-rw-r--r--activerecord/lib/active_record/type/numeric.rb18
-rw-r--r--activerecord/lib/active_record/type/serialized.rb19
-rw-r--r--activerecord/lib/active_record/type/string.rb23
-rw-r--r--activerecord/lib/active_record/type/type_map.rb30
-rw-r--r--activerecord/lib/active_record/type/unsigned_integer.rb15
-rw-r--r--activerecord/lib/active_record/type/value.rb71
16 files changed, 253 insertions, 68 deletions
diff --git a/activerecord/lib/active_record/type/big_integer.rb b/activerecord/lib/active_record/type/big_integer.rb
new file mode 100644
index 0000000000..0c72d8914f
--- /dev/null
+++ b/activerecord/lib/active_record/type/big_integer.rb
@@ -0,0 +1,13 @@
+require 'active_record/type/integer'
+
+module ActiveRecord
+ module Type
+ class BigInteger < Integer # :nodoc:
+ private
+
+ def max_value
+ ::Float::INFINITY
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/binary.rb b/activerecord/lib/active_record/type/binary.rb
index 3bf29b5026..005a48ef0d 100644
--- a/activerecord/lib/active_record/type/binary.rb
+++ b/activerecord/lib/active_record/type/binary.rb
@@ -22,18 +22,28 @@ module ActiveRecord
Data.new(super)
end
- class Data
+ def changed_in_place?(raw_old_value, value)
+ old_value = type_cast_from_database(raw_old_value)
+ old_value != value
+ end
+
+ class Data # :nodoc:
def initialize(value)
- @value = value
+ @value = value.to_s
end
def to_s
@value
end
+ alias_method :to_str, :to_s
def hex
@value.unpack('H*')[0]
end
+
+ def ==(other)
+ other == to_s || super
+ end
end
end
end
diff --git a/activerecord/lib/active_record/type/boolean.rb b/activerecord/lib/active_record/type/boolean.rb
index 06dd17ed28..978d16d524 100644
--- a/activerecord/lib/active_record/type/boolean.rb
+++ b/activerecord/lib/active_record/type/boolean.rb
@@ -10,8 +10,19 @@ module ActiveRecord
def cast_value(value)
if value == ''
nil
+ elsif ConnectionAdapters::Column::TRUE_VALUES.include?(value)
+ true
else
- ConnectionAdapters::Column::TRUE_VALUES.include?(value)
+ if !ConnectionAdapters::Column::FALSE_VALUES.include?(value)
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ You attempted to assign a value which is not explicitly `true` or `false`
+ to a boolean column. Currently this value casts to `false`. This will
+ change to match Ruby's semantics, and will cast to `true` in Rails 5.
+ If you would like to maintain the current behavior, you should
+ explicitly handle the values you would like cast to `false`.
+ MSG
+ end
+ false
end
end
end
diff --git a/activerecord/lib/active_record/type/date_time.rb b/activerecord/lib/active_record/type/date_time.rb
index 560d63c101..5f19608a33 100644
--- a/activerecord/lib/active_record/type/date_time.rb
+++ b/activerecord/lib/active_record/type/date_time.rb
@@ -7,6 +7,16 @@ module ActiveRecord
:datetime
end
+ def type_cast_for_database(value)
+ zone_conversion_method = ActiveRecord::Base.default_timezone == :utc ? :getutc : :getlocal
+
+ if value.acts_like?(:time)
+ value.send(zone_conversion_method)
+ else
+ super
+ end
+ end
+
private
def cast_value(string)
diff --git a/activerecord/lib/active_record/type/decimal.rb b/activerecord/lib/active_record/type/decimal.rb
index a9db51c6ba..7b2bee2c42 100644
--- a/activerecord/lib/active_record/type/decimal.rb
+++ b/activerecord/lib/active_record/type/decimal.rb
@@ -14,10 +14,33 @@ module ActiveRecord
private
def cast_value(value)
- if value.respond_to?(:to_d)
+ case value
+ when ::Float
+ convert_float_to_big_decimal(value)
+ when ::Numeric, ::String
+ BigDecimal(value, precision.to_i)
+ else
+ if value.respond_to?(:to_d)
+ value.to_d
+ else
+ cast_value(value.to_s)
+ end
+ end
+ end
+
+ def convert_float_to_big_decimal(value)
+ if precision
+ BigDecimal(value, float_precision)
+ else
value.to_d
+ end
+ end
+
+ def float_precision
+ if precision.to_i > ::Float::DIG + 1
+ ::Float::DIG + 1
else
- value.to_s.to_d
+ precision.to_i
end
end
end
diff --git a/activerecord/lib/active_record/type/decimal_without_scale.rb b/activerecord/lib/active_record/type/decimal_without_scale.rb
index cabdcecdd7..ff5559e300 100644
--- a/activerecord/lib/active_record/type/decimal_without_scale.rb
+++ b/activerecord/lib/active_record/type/decimal_without_scale.rb
@@ -1,8 +1,8 @@
-require 'active_record/type/integer'
+require 'active_record/type/big_integer'
module ActiveRecord
module Type
- class DecimalWithoutScale < Integer # :nodoc:
+ class DecimalWithoutScale < BigInteger # :nodoc:
def type
:decimal
end
diff --git a/activerecord/lib/active_record/type/decorator.rb b/activerecord/lib/active_record/type/decorator.rb
new file mode 100644
index 0000000000..9fce38ea44
--- /dev/null
+++ b/activerecord/lib/active_record/type/decorator.rb
@@ -0,0 +1,14 @@
+module ActiveRecord
+ module Type
+ module Decorator # :nodoc:
+ def init_with(coder)
+ @subtype = coder['subtype']
+ __setobj__(@subtype)
+ end
+
+ def encode_with(coder)
+ coder['subtype'] = __getobj__
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/hash_lookup_type_map.rb b/activerecord/lib/active_record/type/hash_lookup_type_map.rb
index bf92680268..82d9327fc0 100644
--- a/activerecord/lib/active_record/type/hash_lookup_type_map.rb
+++ b/activerecord/lib/active_record/type/hash_lookup_type_map.rb
@@ -3,16 +3,14 @@ module ActiveRecord
class HashLookupTypeMap < TypeMap # :nodoc:
delegate :key?, to: :@mapping
- def lookup(type, *args)
- @mapping.fetch(type, proc { default_value }).call(type, *args)
+ def alias_type(type, alias_type)
+ register_type(type) { |_, *args| lookup(alias_type, *args) }
end
- def fetch(type, *args, &block)
- @mapping.fetch(type, block).call(type, *args)
- end
+ private
- def alias_type(type, alias_type)
- register_type(type) { |_, *args| lookup(alias_type, *args) }
+ def perform_fetch(type, *args, &block)
+ @mapping.fetch(type, block).call(type, *args)
end
end
end
diff --git a/activerecord/lib/active_record/type/integer.rb b/activerecord/lib/active_record/type/integer.rb
index 08477d1303..fc260a081a 100644
--- a/activerecord/lib/active_record/type/integer.rb
+++ b/activerecord/lib/active_record/type/integer.rb
@@ -3,21 +3,53 @@ module ActiveRecord
class Integer < Value # :nodoc:
include Numeric
+ def initialize(*)
+ super
+ @range = min_value...max_value
+ end
+
def type
:integer
end
alias type_cast_for_database type_cast
+ def type_cast_from_database(value)
+ return if value.nil?
+ value.to_i
+ end
+
+ protected
+
+ attr_reader :range
+
private
def cast_value(value)
case value
when true then 1
when false then 0
- else value.to_i rescue nil
+ else
+ result = value.to_i rescue nil
+ ensure_in_range(result) if result
+ result
end
end
+
+ def ensure_in_range(value)
+ unless range.cover?(value)
+ raise RangeError, "#{value} is out of range for #{self.class} with limit #{limit || 4}"
+ end
+ end
+
+ def max_value
+ limit = self.limit || 4
+ 1 << (limit * 8 - 1) # 8 bits per byte with one bit for sign
+ end
+
+ def min_value
+ -max_value
+ end
end
end
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 137c9e4c99..674f996f38 100644
--- a/activerecord/lib/active_record/type/numeric.rb
+++ b/activerecord/lib/active_record/type/numeric.rb
@@ -16,26 +16,20 @@ module ActiveRecord
end
def changed?(old_value, _new_value, new_value_before_type_cast) # :nodoc:
- # 0 => 'wibble' should mark as changed so numericality validations run
- if nil_or_zero?(old_value) && non_numeric_string?(new_value_before_type_cast)
- # nil => '' should not mark as changed
- old_value != new_value_before_type_cast.presence
- else
- super
- end
+ super || number_to_non_number?(old_value, new_value_before_type_cast)
end
private
+ def number_to_non_number?(old_value, new_value_before_type_cast)
+ old_value != nil && non_numeric_string?(new_value_before_type_cast)
+ end
+
def non_numeric_string?(value)
# 'wibble'.to_i will give zero, we want to make sure
# that we aren't marking int zero to string zero as
# changed.
- value !~ /\A\d+\.?\d*\z/
- end
-
- def nil_or_zero?(value)
- value.nil? || value == 0
+ value.to_s !~ /\A-?\d+\.?\d*\z/
end
end
end
diff --git a/activerecord/lib/active_record/type/serialized.rb b/activerecord/lib/active_record/type/serialized.rb
index 42bbed7103..3cac03464e 100644
--- a/activerecord/lib/active_record/type/serialized.rb
+++ b/activerecord/lib/active_record/type/serialized.rb
@@ -1,7 +1,8 @@
module ActiveRecord
module Type
- class Serialized < SimpleDelegator # :nodoc:
+ class Serialized < DelegateClass(Type::Value) # :nodoc:
include Mutable
+ include Decorator
attr_reader :subtype, :coder
@@ -12,7 +13,7 @@ module ActiveRecord
end
def type_cast_from_database(value)
- if is_default_value?(value)
+ if default_value?(value)
value
else
coder.load(super)
@@ -21,29 +22,33 @@ module ActiveRecord
def type_cast_for_database(value)
return if value.nil?
- unless is_default_value?(value)
+ unless default_value?(value)
super coder.dump(value)
end
end
+ def changed_in_place?(raw_old_value, value)
+ return false if value.nil?
+ subtype.changed_in_place?(raw_old_value, type_cast_for_database(value))
+ end
+
def accessor
ActiveRecord::Store::IndifferentHashAccessor
end
def init_with(coder)
- @subtype = coder['subtype']
@coder = coder['coder']
- __setobj__(@subtype)
+ super
end
def encode_with(coder)
- coder['subtype'] = @subtype
coder['coder'] = @coder
+ super
end
private
- def is_default_value?(value)
+ def default_value?(value)
value == coder.load(nil)
end
end
diff --git a/activerecord/lib/active_record/type/string.rb b/activerecord/lib/active_record/type/string.rb
index 3b1554bd5a..cf95e25be0 100644
--- a/activerecord/lib/active_record/type/string.rb
+++ b/activerecord/lib/active_record/type/string.rb
@@ -5,6 +5,22 @@ module ActiveRecord
:string
end
+ def changed_in_place?(raw_old_value, new_value)
+ if new_value.is_a?(::String)
+ raw_old_value != new_value
+ end
+ end
+
+ def type_cast_for_database(value)
+ case value
+ when ::Numeric, ActiveSupport::Duration then value.to_s
+ when ::String then ::String.new(value)
+ when true then "t"
+ when false then "f"
+ else super
+ end
+ end
+
def text?
true
end
@@ -13,9 +29,10 @@ module ActiveRecord
def cast_value(value)
case value
- when true then "1"
- when false then "0"
- else value.to_s
+ when true then "t"
+ when false then "f"
+ # String.new is slightly faster than dup
+ else ::String.new(value.to_s)
end
end
end
diff --git a/activerecord/lib/active_record/type/type_map.rb b/activerecord/lib/active_record/type/type_map.rb
index 88c5f9c497..09f5ba6b74 100644
--- a/activerecord/lib/active_record/type/type_map.rb
+++ b/activerecord/lib/active_record/type/type_map.rb
@@ -1,24 +1,28 @@
+require 'thread_safe'
+
module ActiveRecord
module Type
class TypeMap # :nodoc:
def initialize
@mapping = {}
+ @cache = ThreadSafe::Cache.new do |h, key|
+ h.fetch_or_store(key, ThreadSafe::Cache.new)
+ end
end
def lookup(lookup_key, *args)
- matching_pair = @mapping.reverse_each.detect do |key, _|
- key === lookup_key
- end
+ fetch(lookup_key, *args) { default_value }
+ end
- if matching_pair
- matching_pair.last.call(lookup_key, *args)
- else
- default_value
+ def fetch(lookup_key, *args, &block)
+ @cache[lookup_key].fetch_or_store(args) do
+ perform_fetch(lookup_key, *args, &block)
end
end
def register_type(key, value = nil, &block)
raise ::ArgumentError unless value || block
+ @cache.clear
if block
@mapping[key] = block
@@ -40,6 +44,18 @@ module ActiveRecord
private
+ def perform_fetch(lookup_key, *args)
+ matching_pair = @mapping.reverse_each.detect do |key, _|
+ key === lookup_key
+ end
+
+ if matching_pair
+ matching_pair.last.call(lookup_key, *args)
+ else
+ yield lookup_key, *args
+ end
+ end
+
def default_value
@default_value ||= Value.new
end
diff --git a/activerecord/lib/active_record/type/unsigned_integer.rb b/activerecord/lib/active_record/type/unsigned_integer.rb
new file mode 100644
index 0000000000..ed3e527483
--- /dev/null
+++ b/activerecord/lib/active_record/type/unsigned_integer.rb
@@ -0,0 +1,15 @@
+module ActiveRecord
+ module Type
+ class UnsignedInteger < Integer # :nodoc:
+ private
+
+ def max_value
+ super * 2
+ end
+
+ def min_value
+ 0
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/value.rb b/activerecord/lib/active_record/type/value.rb
index 875fb98c4b..60ae47db3d 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,92 @@ 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 be overridden
+ # directly. Types which return a mutable value should include
+ # +Type::Mutable+, which will define this method.
+ def changed_in_place?(*)
false
end
+ def ==(other)
+ self.class == other.class &&
+ precision == other.precision &&
+ scale == other.scale &&
+ limit == other.limit
+ 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