diff options
Diffstat (limited to 'activerecord/lib/active_record/type')
-rw-r--r-- | activerecord/lib/active_record/type/adapter_specific_registry.rb | 136 | ||||
-rw-r--r-- | activerecord/lib/active_record/type/date.rb | 9 | ||||
-rw-r--r-- | activerecord/lib/active_record/type/date_time.rb | 9 | ||||
-rw-r--r-- | activerecord/lib/active_record/type/decimal_without_scale.rb | 15 | ||||
-rw-r--r-- | activerecord/lib/active_record/type/hash_lookup_type_map.rb | 25 | ||||
-rw-r--r-- | activerecord/lib/active_record/type/internal/timezone.rb | 17 | ||||
-rw-r--r-- | activerecord/lib/active_record/type/json.rb | 30 | ||||
-rw-r--r-- | activerecord/lib/active_record/type/serialized.rb | 67 | ||||
-rw-r--r-- | activerecord/lib/active_record/type/text.rb | 11 | ||||
-rw-r--r-- | activerecord/lib/active_record/type/time.rb | 21 | ||||
-rw-r--r-- | activerecord/lib/active_record/type/type_map.rb | 62 | ||||
-rw-r--r-- | activerecord/lib/active_record/type/unsigned_integer.rb | 17 |
12 files changed, 419 insertions, 0 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..e7468aa542 --- /dev/null +++ b/activerecord/lib/active_record/type/adapter_specific_registry.rb @@ -0,0 +1,136 @@ +# frozen_string_literal: true + +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 + + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. + 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 + + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. + 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/date.rb b/activerecord/lib/active_record/type/date.rb new file mode 100644 index 0000000000..8177074a20 --- /dev/null +++ b/activerecord/lib/active_record/type/date.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module ActiveRecord + module Type + 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 new file mode 100644 index 0000000000..4acde6b9f8 --- /dev/null +++ b/activerecord/lib/active_record/type/date_time.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module ActiveRecord + module Type + class DateTime < ActiveModel::Type::DateTime + include Internal::Timezone + 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 new file mode 100644 index 0000000000..a207940dc7 --- /dev/null +++ b/activerecord/lib/active_record/type/decimal_without_scale.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module ActiveRecord + module Type + class DecimalWithoutScale < ActiveModel::Type::BigInteger # :nodoc: + def type + :decimal + end + + def type_cast_for_schema(value) + value.to_s.inspect + 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 new file mode 100644 index 0000000000..db9853fbcc --- /dev/null +++ b/activerecord/lib/active_record/type/hash_lookup_type_map.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module ActiveRecord + module Type + class HashLookupTypeMap < TypeMap # :nodoc: + def alias_type(type, alias_type) + register_type(type) { |_, *args| lookup(alias_type, *args) } + end + + def key?(key) + @mapping.key?(key) + end + + def keys + @mapping.keys + end + + private + + def perform_fetch(type, *args, &block) + @mapping.fetch(type, block).call(type, *args) + 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..3059755752 --- /dev/null +++ b/activerecord/lib/active_record/type/internal/timezone.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +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/json.rb b/activerecord/lib/active_record/type/json.rb new file mode 100644 index 0000000000..3f9ff22796 --- /dev/null +++ b/activerecord/lib/active_record/type/json.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module ActiveRecord + module Type + class Json < ActiveModel::Type::Value + include ActiveModel::Type::Helpers::Mutable + + def type + :json + end + + def deserialize(value) + return value unless value.is_a?(::String) + ActiveSupport::JSON.decode(value) rescue nil + end + + def serialize(value) + ActiveSupport::JSON.encode(value) unless value.nil? + end + + def changed_in_place?(raw_old_value, new_value) + deserialize(raw_old_value) != new_value + end + + def accessor + ActiveRecord::Store::StringKeyedHashAccessor + end + end + end +end diff --git a/activerecord/lib/active_record/type/serialized.rb b/activerecord/lib/active_record/type/serialized.rb new file mode 100644 index 0000000000..e882784691 --- /dev/null +++ b/activerecord/lib/active_record/type/serialized.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +module ActiveRecord + module Type + class Serialized < DelegateClass(ActiveModel::Type::Value) # :nodoc: + undef to_yaml if method_defined?(:to_yaml) + + include ActiveModel::Type::Helpers::Mutable + + attr_reader :subtype, :coder + + def initialize(subtype, coder) + @subtype = subtype + @coder = coder + super(subtype) + end + + def deserialize(value) + if default_value?(value) + value + else + coder.load(super) + end + end + + def serialize(value) + return if value.nil? + unless default_value?(value) + super coder.dump(value) + end + end + + def inspect + Kernel.instance_method(:inspect).bind(self).call + end + + def changed_in_place?(raw_old_value, value) + return false if value.nil? + raw_new_value = encoded(value) + raw_old_value.nil? != raw_new_value.nil? || + subtype.changed_in_place?(raw_old_value, raw_new_value) + end + + def accessor + ActiveRecord::Store::IndifferentHashAccessor + end + + def assert_valid_value(value) + if coder.respond_to?(:assert_valid_value) + coder.assert_valid_value(value, action: "serialize") + end + end + + private + + def default_value?(value) + value == coder.load(nil) + end + + def encoded(value) + unless default_value?(value) + coder.dump(value) + end + end + end + end +end diff --git a/activerecord/lib/active_record/type/text.rb b/activerecord/lib/active_record/type/text.rb new file mode 100644 index 0000000000..6d19696671 --- /dev/null +++ b/activerecord/lib/active_record/type/text.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module ActiveRecord + module Type + class Text < ActiveModel::Type::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 new file mode 100644 index 0000000000..f4da1ecf2c --- /dev/null +++ b/activerecord/lib/active_record/type/time.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module ActiveRecord + module Type + class Time < ActiveModel::Type::Time + include Internal::Timezone + + class Value < DelegateClass(::Time) # :nodoc: + end + + def serialize(value) + case value = super + when ::Time + Value.new(value) + else + value + 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 new file mode 100644 index 0000000000..fc40b460f0 --- /dev/null +++ b/activerecord/lib/active_record/type/type_map.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +require "concurrent/map" + +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) + fetch(lookup_key, *args) { Type.default_value } + end + + 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 + else + @mapping[key] = proc { value } + end + end + + def alias_type(key, target_key) + register_type(key) do |sql_type, *args| + metadata = sql_type[/\(.*\)/, 0] + lookup("#{target_key}#{metadata}", *args) + end + end + + def clear + @mapping.clear + end + + 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 + end + end +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..4619528f81 --- /dev/null +++ b/activerecord/lib/active_record/type/unsigned_integer.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module ActiveRecord + module Type + class UnsignedInteger < ActiveModel::Type::Integer # :nodoc: + private + + def max_value + super * 2 + end + + def min_value + 0 + end + end + end +end |