1

I'm currently looking to implement a custom type with ActiveRecord::Type::Value following the enum implementation, using Rails 5.0.0.beta1.1.

So far I have I got it working to cast everything from and to database correctly.

class BitFieldType < ActiveRecord::Type::Value
  def initialize(name, mapping, subtype)
    @name = name
    @mapping = mapping
    @subtype = subtype
  end

  def cast(value)
    [...]
  end

  def deserialize(value)
    [...]
  end

  def serialize(value)
    [...]
  end

  def assert_valid_value(value)
    [...]
  end

  protected

  attr_reader :name, :mapping, :subtype
end

class SomeModel < ActiveRecord::Base
  decorate_attribute_type(:my_attribute_name, :bit_field) do |subtype|
    BitFieldType.new(:my_attribute_name, [...], subtype)
  end
end

Only problem I have: I want to cast a array as a whole in where() calls, not each value.

Now, when I call for example:

SomeModel.where(my_attribute_name: [1, 2, 3])

... it passes each value (1, 2, 3) to my serialize method and then builds a query like

[...] where my_attribute_name IN (<serialized 1>, <serialized 2>, <serialized 3>)

I'd rather want to decide how that call turns out, so basically that the array [1, 2, 3] is passed to my serialize method, not each value of it. Then I could serialize it to an value which should simply be compared with =:

[...] where my_attribute_name = <serialized [1, 2, 3]>

Is this possible?


Edit: I more or less archived what I wanted by monkey patching ActiveRecord::ActiveRecord::ArrayHandler

diff --git a/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb
index 95dbd6a..33077c1 100644
--- a/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb
+++ b/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb
@@ -6,6 +6,9 @@ module ActiveRecord
       end

       def call(attribute, value)
+        if attribute.relation.send(:type_caster).type_cast_where_array?(attribute.name)
+          value = Array.wrap(attribute.type_cast_for_database(value))
+        end
         values = value.map { |x| x.is_a?(Base) ? x.id : x }
         nils, values = values.partition(&:nil?)

This for now uses the protected method :type_caster and a new added method .type_cast_where_array?.

This new method has to be added to ActiveRecord::TypeCaster::Map:

diff --git a/activerecord/lib/active_record/type_caster/map.rb b/activerecord/lib/active_record/type_caster/map.rb
index 3a367b3..b710081 100644
--- a/activerecord/lib/active_record/type_caster/map.rb
+++ b/activerecord/lib/active_record/type_caster/map.rb
@@ -11,6 +11,10 @@ module ActiveRecord
         type.serialize(value)
       end

+      def type_cast_where_array?(attr_name)
+        types.type_for_attribute(attr_name.to_s).type_cast_where_array?
+      end
+
       protected

       attr_reader :types

And to make that work with default types ActiveModel::Type::Value has to be patched, too:

diff --git a/activemodel/lib/active_model/type/value.rb b/activemodel/lib/active_model/type/value.rb
index 0d2d687..114f85d 100644
--- a/activemodel/lib/active_model/type/value.rb
+++ b/activemodel/lib/active_model/type/value.rb
@@ -50,6 +50,9 @@ module ActiveModel
         value.inspect
       end

+      def type_cast_where_array?
+      end
+
       # These predicates are not documented, as I need to look further into
       # their use, and see if they can be removed entirely.
       def binary? # :nodoc:

Now in my BitFieldType I can override that method to return true:

class BitFieldType < ActiveRecord::Type::Value
  def type_cast_where_array?
    true
  end
end

Last but not least ActiveRecord::TypeCaster::Connection has to be patched with the new method too to make all tests pass.

diff --git a/activerecord/lib/active_record/type_caster/connection.rb b/activerecord/lib/active_record/type_caster/connection.rb
index 7ed8dcc..f5623b4 100644
--- a/activerecord/lib/active_record/type_caster/connection.rb
+++ b/activerecord/lib/active_record/type_caster/connection.rb
@@ -12,6 +12,9 @@ module ActiveRecord
         connection.type_cast_from_column(column, value)
       end

+      def type_cast_where_array?(_attr_name)
+      end
+
       protected

       attr_reader :table_name

I know the method name might not be the best and using a protected method is bad, too, but that seems to be a way to type cast array where calls without too much code changes and all activerecord tests passing.

Any comments on this?

4

1 に答える 1