diff options
Diffstat (limited to 'activerecord/lib/active_record/type')
21 files changed, 242 insertions, 501 deletions
diff --git a/activerecord/lib/active_record/type/adapter_specific_registry.rb b/activerecord/lib/active_record/type/adapter_specific_registry.rb new file mode 100644 index 0000000000..d440eac619 --- /dev/null +++ b/activerecord/lib/active_record/type/adapter_specific_registry.rb @@ -0,0 +1,130 @@ +require 'active_model/type/registry' + +module ActiveRecord + # :stopdoc: + module Type + class AdapterSpecificRegistry < ActiveModel::Type::Registry + def add_modifier(options, klass, **args) + registrations << DecorationRegistration.new(options, klass, **args) + end + + private + + def registration_klass + Registration + end + + def find_registration(symbol, *args) + registrations + .select { |registration| registration.matches?(symbol, *args) } + .max + end + end + + class Registration + def initialize(name, block, adapter: nil, override: nil) + @name = name + @block = block + @adapter = adapter + @override = override + end + + def call(_registry, *args, adapter: nil, **kwargs) + if kwargs.any? # https://bugs.ruby-lang.org/issues/10856 + block.call(*args, **kwargs) + else + block.call(*args) + end + end + + def matches?(type_name, *args, **kwargs) + type_name == name && matches_adapter?(**kwargs) + end + + def <=>(other) + if conflicts_with?(other) + raise TypeConflictError.new("Type #{name} was registered for all + adapters, but shadows a native type with + the same name for #{other.adapter}".squish) + end + priority <=> other.priority + end + + protected + + attr_reader :name, :block, :adapter, :override + + def priority + result = 0 + if adapter + result |= 1 + end + if override + result |= 2 + end + result + end + + def priority_except_adapter + priority & 0b111111100 + end + + private + + def matches_adapter?(adapter: nil, **) + (self.adapter.nil? || adapter == self.adapter) + end + + def conflicts_with?(other) + same_priority_except_adapter?(other) && + has_adapter_conflict?(other) + end + + def same_priority_except_adapter?(other) + priority_except_adapter == other.priority_except_adapter + end + + def has_adapter_conflict?(other) + (override.nil? && other.adapter) || + (adapter && other.override.nil?) + end + end + + class DecorationRegistration < Registration + def initialize(options, klass, adapter: nil) + @options = options + @klass = klass + @adapter = adapter + end + + def call(registry, *args, **kwargs) + subtype = registry.lookup(*args, **kwargs.except(*options.keys)) + klass.new(subtype) + end + + def matches?(*args, **kwargs) + matches_adapter?(**kwargs) && matches_options?(**kwargs) + end + + def priority + super | 4 + end + + protected + + attr_reader :options, :klass + + private + + def matches_options?(**kwargs) + options.all? do |key, value| + kwargs[key] == value + end + end + end + end + + class TypeConflictError < StandardError + end + # :startdoc: +end diff --git a/activerecord/lib/active_record/type/binary.rb b/activerecord/lib/active_record/type/binary.rb deleted file mode 100644 index d29ff4e494..0000000000 --- a/activerecord/lib/active_record/type/binary.rb +++ /dev/null @@ -1,40 +0,0 @@ -module ActiveRecord - module Type - class Binary < Value # :nodoc: - def type - :binary - end - - def binary? - true - end - - def type_cast(value) - if value.is_a?(Data) - value.to_s - else - super - end - end - - def type_cast_for_database(value) - return if value.nil? - Data.new(super) - end - - class Data # :nodoc: - def initialize(value) - @value = value.to_s - end - - def to_s - @value - end - - def hex - @value.unpack('H*')[0] - end - end - end - end -end diff --git a/activerecord/lib/active_record/type/boolean.rb b/activerecord/lib/active_record/type/boolean.rb deleted file mode 100644 index 06dd17ed28..0000000000 --- a/activerecord/lib/active_record/type/boolean.rb +++ /dev/null @@ -1,19 +0,0 @@ -module ActiveRecord - module Type - class Boolean < Value # :nodoc: - def type - :boolean - end - - private - - def cast_value(value) - if value == '' - nil - else - ConnectionAdapters::Column::TRUE_VALUES.include?(value) - end - end - end - end -end diff --git a/activerecord/lib/active_record/type/date.rb b/activerecord/lib/active_record/type/date.rb index d90a6069b7..ccafed054e 100644 --- a/activerecord/lib/active_record/type/date.rb +++ b/activerecord/lib/active_record/type/date.rb @@ -1,46 +1,7 @@ module ActiveRecord module Type - class Date < Value # :nodoc: - def type - :date - end - - def klass - ::Date - end - - def type_cast_for_schema(value) - "'#{value.to_s(:db)}'" - end - - private - - def cast_value(value) - if value.is_a?(::String) - return if value.empty? - fast_string_to_date(value) || fallback_string_to_date(value) - elsif value.respond_to?(:to_date) - value.to_date - else - value - end - end - - def fast_string_to_date(string) - if string =~ ConnectionAdapters::Column::Format::ISO_DATE - new_date $1.to_i, $2.to_i, $3.to_i - end - end - - def fallback_string_to_date(string) - new_date(*::Date._parse(string, false).values_at(:year, :mon, :mday)) - end - - def new_date(year, mon, mday) - if year && year != 0 - ::Date.new(year, mon, mday) rescue nil - end - end + class Date < ActiveModel::Type::Date + include Internal::Timezone end end end diff --git a/activerecord/lib/active_record/type/date_time.rb b/activerecord/lib/active_record/type/date_time.rb index 5f19608a33..1fb9380ecd 100644 --- a/activerecord/lib/active_record/type/date_time.rb +++ b/activerecord/lib/active_record/type/date_time.rb @@ -1,43 +1,7 @@ module ActiveRecord module Type - class DateTime < Value # :nodoc: - include TimeValue - - def type - :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) - return string unless string.is_a?(::String) - return if string.empty? - - fast_string_to_time(string) || fallback_string_to_time(string) - end - - # '0.123456' -> 123456 - # '1.123456' -> 123456 - def microseconds(time) - time[:sec_fraction] ? (time[:sec_fraction] * 1_000_000).to_i : 0 - end - - def fallback_string_to_time(string) - time_hash = ::Date._parse(string) - time_hash[:sec_fraction] = microseconds(time_hash) - - new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction, :offset)) - end + class DateTime < ActiveModel::Type::DateTime + include Internal::Timezone end end end diff --git a/activerecord/lib/active_record/type/decimal.rb b/activerecord/lib/active_record/type/decimal.rb deleted file mode 100644 index ba5d244729..0000000000 --- a/activerecord/lib/active_record/type/decimal.rb +++ /dev/null @@ -1,27 +0,0 @@ -module ActiveRecord - module Type - class Decimal < Value # :nodoc: - include Numeric - - def type - :decimal - end - - def type_cast_for_schema(value) - value.to_s - end - - private - - def cast_value(value) - if value.is_a?(::Numeric) || value.is_a?(::String) - BigDecimal(value, precision.to_i) - elsif value.respond_to?(:to_d) - value.to_d - else - cast_value(value.to_s) - end - end - 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 deleted file mode 100644 index cabdcecdd7..0000000000 --- a/activerecord/lib/active_record/type/decimal_without_scale.rb +++ /dev/null @@ -1,11 +0,0 @@ -require 'active_record/type/integer' - -module ActiveRecord - module Type - class DecimalWithoutScale < Integer # :nodoc: - def type - :decimal - end - end - end -end diff --git a/activerecord/lib/active_record/type/float.rb b/activerecord/lib/active_record/type/float.rb deleted file mode 100644 index 42eb44b9a9..0000000000 --- a/activerecord/lib/active_record/type/float.rb +++ /dev/null @@ -1,19 +0,0 @@ -module ActiveRecord - module Type - class Float < Value # :nodoc: - include Numeric - - def type - :float - end - - alias type_cast_for_database type_cast - - private - - def cast_value(value) - value.to_f - 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..3b01e3f8ca 100644 --- a/activerecord/lib/active_record/type/hash_lookup_type_map.rb +++ b/activerecord/lib/active_record/type/hash_lookup_type_map.rb @@ -1,18 +1,22 @@ module ActiveRecord module Type class HashLookupTypeMap < TypeMap # :nodoc: - delegate :key?, to: :@mapping + def alias_type(type, alias_type) + register_type(type) { |_, *args| lookup(alias_type, *args) } + end - def lookup(type, *args) - @mapping.fetch(type, proc { default_value }).call(type, *args) + def key?(key) + @mapping.key?(key) end - def fetch(type, *args, &block) - @mapping.fetch(type, block).call(type, *args) + def keys + @mapping.keys end - def alias_type(type, alias_type) - register_type(type) { |_, *args| lookup(alias_type, *args) } + private + + 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 deleted file mode 100644 index 08477d1303..0000000000 --- a/activerecord/lib/active_record/type/integer.rb +++ /dev/null @@ -1,23 +0,0 @@ -module ActiveRecord - module Type - class Integer < Value # :nodoc: - include Numeric - - def type - :integer - end - - alias type_cast_for_database type_cast - - private - - def cast_value(value) - case value - when true then 1 - when false then 0 - else value.to_i rescue nil - end - end - end - end -end diff --git a/activerecord/lib/active_record/type/internal/abstract_json.rb b/activerecord/lib/active_record/type/internal/abstract_json.rb new file mode 100644 index 0000000000..097d1bd363 --- /dev/null +++ b/activerecord/lib/active_record/type/internal/abstract_json.rb @@ -0,0 +1,33 @@ +module ActiveRecord + module Type + module Internal # :nodoc: + class AbstractJson < ActiveModel::Type::Value # :nodoc: + include ActiveModel::Type::Helpers::Mutable + + def type + :json + end + + def deserialize(value) + if value.is_a?(::String) + ::ActiveSupport::JSON.decode(value) rescue nil + else + value + end + end + + def serialize(value) + if value.is_a?(::Array) || value.is_a?(::Hash) + ::ActiveSupport::JSON.encode(value) + else + value + end + end + + def accessor + ActiveRecord::Store::StringKeyedHashAccessor + end + end + end + end +end diff --git a/activerecord/lib/active_record/type/internal/timezone.rb b/activerecord/lib/active_record/type/internal/timezone.rb new file mode 100644 index 0000000000..947e06158a --- /dev/null +++ b/activerecord/lib/active_record/type/internal/timezone.rb @@ -0,0 +1,15 @@ +module ActiveRecord + module Type + module Internal + module Timezone + def is_utc? + ActiveRecord::Base.default_timezone == :utc + end + + def default_timezone + ActiveRecord::Base.default_timezone + end + end + end + end +end diff --git a/activerecord/lib/active_record/type/mutable.rb b/activerecord/lib/active_record/type/mutable.rb deleted file mode 100644 index 066617ea59..0000000000 --- a/activerecord/lib/active_record/type/mutable.rb +++ /dev/null @@ -1,16 +0,0 @@ -module ActiveRecord - module Type - module Mutable # :nodoc: - def type_cast_from_user(value) - type_cast_from_database(type_cast_for_database(value)) - end - - # +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) - raw_old_value != type_cast_for_database(new_value) - end - end - end -end diff --git a/activerecord/lib/active_record/type/numeric.rb b/activerecord/lib/active_record/type/numeric.rb deleted file mode 100644 index fa43266504..0000000000 --- a/activerecord/lib/active_record/type/numeric.rb +++ /dev/null @@ -1,36 +0,0 @@ -module ActiveRecord - module Type - module Numeric # :nodoc: - def number? - true - end - - def type_cast(value) - value = case value - when true then 1 - when false then 0 - when ::String then value.presence - else value - end - super(value) - end - - def changed?(old_value, _new_value, new_value_before_type_cast) # :nodoc: - 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.to_s !~ /\A\d+\.?\d*\z/ - end - end - end -end diff --git a/activerecord/lib/active_record/type/serialized.rb b/activerecord/lib/active_record/type/serialized.rb index 42bbed7103..4ff0740cfb 100644 --- a/activerecord/lib/active_record/type/serialized.rb +++ b/activerecord/lib/active_record/type/serialized.rb @@ -1,7 +1,7 @@ module ActiveRecord module Type - class Serialized < SimpleDelegator # :nodoc: - include Mutable + class Serialized < DelegateClass(ActiveModel::Type::Value) # :nodoc: + include ActiveModel::Type::Helpers::Mutable attr_reader :subtype, :coder @@ -11,39 +11,45 @@ module ActiveRecord super(subtype) end - def type_cast_from_database(value) - if is_default_value?(value) + def deserialize(value) + if default_value?(value) value else coder.load(super) end end - def type_cast_for_database(value) + def serialize(value) return if value.nil? - unless is_default_value?(value) + unless default_value?(value) super coder.dump(value) end end - def accessor - ActiveRecord::Store::IndifferentHashAccessor + def inspect + Kernel.instance_method(:inspect).bind(self).call end - def init_with(coder) - @subtype = coder['subtype'] - @coder = coder['coder'] - __setobj__(@subtype) + def changed_in_place?(raw_old_value, value) + return false if value.nil? + raw_new_value = serialize(value) + raw_old_value.nil? != raw_new_value.nil? || + subtype.changed_in_place?(raw_old_value, raw_new_value) end - def encode_with(coder) - coder['subtype'] = @subtype - coder['coder'] = @coder + def accessor + ActiveRecord::Store::IndifferentHashAccessor + end + + def assert_valid_value(value) + if coder.respond_to?(:assert_valid_value) + coder.assert_valid_value(value) + end 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 deleted file mode 100644 index 150defb106..0000000000 --- a/activerecord/lib/active_record/type/string.rb +++ /dev/null @@ -1,36 +0,0 @@ -module ActiveRecord - module Type - class String < Value # :nodoc: - def type - :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 "1" - when false then "0" - else super - end - end - - private - - def cast_value(value) - case value - when true then "1" - when false then "0" - # String.new is slightly faster than dup - else ::String.new(value.to_s) - end - end - end - end -end diff --git a/activerecord/lib/active_record/type/text.rb b/activerecord/lib/active_record/type/text.rb deleted file mode 100644 index 26f980f060..0000000000 --- a/activerecord/lib/active_record/type/text.rb +++ /dev/null @@ -1,11 +0,0 @@ -require 'active_record/type/string' - -module ActiveRecord - module Type - class Text < String # :nodoc: - def type - :text - end - end - end -end diff --git a/activerecord/lib/active_record/type/time.rb b/activerecord/lib/active_record/type/time.rb index 41f7d97f0c..70988d84ff 100644 --- a/activerecord/lib/active_record/type/time.rb +++ b/activerecord/lib/active_record/type/time.rb @@ -1,26 +1,8 @@ module ActiveRecord module Type - class Time < Value # :nodoc: - include TimeValue - - def type - :time - end - - private - - def cast_value(value) - return value unless value.is_a?(::String) - return if value.empty? - - dummy_time_value = "2000-01-01 #{value}" - - fast_string_to_time(dummy_time_value) || begin - time_hash = ::Date._parse(dummy_time_value) - return if time_hash[:hour].nil? - new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction)) - end - end + class Time < ActiveModel::Type::Time + include Internal::Timezone end end end + diff --git a/activerecord/lib/active_record/type/time_value.rb b/activerecord/lib/active_record/type/time_value.rb deleted file mode 100644 index d611d72dd4..0000000000 --- a/activerecord/lib/active_record/type/time_value.rb +++ /dev/null @@ -1,38 +0,0 @@ -module ActiveRecord - module Type - module TimeValue # :nodoc: - def klass - ::Time - end - - def type_cast_for_schema(value) - "'#{value.to_s(:db)}'" - end - - private - - def new_time(year, mon, mday, hour, min, sec, microsec, offset = nil) - # Treat 0000-00-00 00:00:00 as nil. - return if year.nil? || (year == 0 && mon == 0 && mday == 0) - - if offset - time = ::Time.utc(year, mon, mday, hour, min, sec, microsec) rescue nil - return unless time - - time -= offset - Base.default_timezone == :utc ? time : time.getlocal - else - ::Time.public_send(Base.default_timezone, year, mon, mday, hour, min, sec, microsec) rescue nil - end - end - - # Doesn't handle time zones. - def fast_string_to_time(string) - if string =~ ConnectionAdapters::Column::Format::ISO_DATETIME - microsec = ($7.to_r * 1_000_000).to_i - new_time $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, microsec - end - end - 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..81d7ed39bb 100644 --- a/activerecord/lib/active_record/type/type_map.rb +++ b/activerecord/lib/active_record/type/type_map.rb @@ -1,24 +1,28 @@ +require 'concurrent' + module ActiveRecord module Type class TypeMap # :nodoc: def initialize @mapping = {} + @cache = Concurrent::Map.new do |h, key| + h.fetch_or_store(key, Concurrent::Map.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,8 +44,20 @@ 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 + @default_value ||= ActiveModel::Type::Value.new end end end diff --git a/activerecord/lib/active_record/type/value.rb b/activerecord/lib/active_record/type/value.rb deleted file mode 100644 index e0a783fb45..0000000000 --- a/activerecord/lib/active_record/type/value.rb +++ /dev/null @@ -1,94 +0,0 @@ -module ActiveRecord - module Type - class Value # :nodoc: - attr_reader :precision, :scale, :limit - - # 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] - @scale = options[:scale] - @limit = options[:limit] - end - - # 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 - - # 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 - - # These predicates are not documented, as I need to look further into - # their use, and see if they can be removed entirely. - def number? # :nodoc: - false - end - - def binary? # :nodoc: - false - end - - def klass # :nodoc: - end - - # 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 - - # 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 - - def type_cast(value) - cast_value(value) unless value.nil? - end - - # 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 - end -end |