ActiveRecord 4.2's Type Casting

Last month Rails 4.2 was released and if you have been keeping up with my posts, I even covered how you can upgrade from 3.2 to 4.2 in one step! This speaks volumes to how easy it is to adopt to outward facing API changes within our beloved framework. But often times, version changes bring implementation changes that we never see. For example, all of Aaron Patterson's work for AdequateRecord Pro™ are performance optimizations that affect no outward API interface at all. Unless you read the source, many of these awesome changes go unnoticed and that's a damn shame because some of them can make our lives easier.

Today I want to share some of the new hotness I found while working on the latest ActiveRecord SQL Server Adapter. Specifically, how ActiveRecord type casts values. Up until Rails 4.2, all type casting was done in class methods like value_to_date implemented on the ActiveRecord::ConnectionAdapters::Column object. Sean Griffin does a great job explaining this convoluted process. Warning, it's kind of boring and a chore to read.

This process has been around for as long as I can remember. It made it really hard to write good abstract OO code that casts values going into and out of the database. All that has changed with the new ActiveRecord::Type namespace. All objects within this namespace are simple POROs with very obvious and well documented interfaces. The base class is ActiveRecord::Type::Value and below is a slightly trimmed down version of that object, sans comments. Take a quick read.

module ActiveRecord
  module Type
    class Value

      def type_cast_from_database(value)
        type_cast(value)
      end

      def type_cast_from_user(value)
        type_cast(value)
      end

      def type_cast_for_database(value)
        value
      end

      def type_cast_for_schema(value)
        value.inspect
      end

      def changed?(old_value, new_value, _new_value_before_type_cast)
        old_value != new_value
      end

      def changed_in_place?(*)
        false
      end


      private

      def type_cast(value)
        cast_value(value) unless value.nil?
      end

      def cast_value(value)
        value
      end

    end
  end
end

Do you see what I see? This is amazing. I see an object that finally handles all of the following.

Case in point, a lot of database connection gems still return raw strings for every value. Sub classes of Value can define their own type_cast_from_database implementation to deal with this. For example, here is the Integer object's default behavior. Super easy!

def type_cast_from_database(value)
  return if value.nil?
  value.to_i
end

One thing that Rails core team did to make this even better allows us to type check our Ruby values ahead of time during attribute assignment vs. when we save to the database. This is now done in the Integer class using the limit attribute parsed from the SQL type. Here are the salient points of that class.

module ActiveRecord
  module Type
    class Integer < Value

      def initialize(*)
        super
        @range = min_value...max_value
      end

      private

      def cast_value(value)
        case value
        when true then 1
        when false then 0
        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

Any type aliased to use the Integer value object will now type check that the value is within the accepted database range. As far as I can tell, only Integer objects in Rails core do this, but I plan on implementing these checks for Decimal and other values too. Here is how SQL Server's smallint(2) SQL type attribute behaves.

@obj.small_int_value = -32_768
@obj.small_int_value = -32_769  # => RangeError!

@obj.small_int_value = 32_767
@obj.small_int_value = 32_768   # => RangeError!

There is so much more that we can do with these objects. The PostgreSQL adapter already casts the JSON data type. I can even see SQL Server returning a Nokogiri object for an XML data type. The sky is the limit. The core Value object allows the SQL Server Adapter to implement guards for different connection modes. Our TinyTDS connection returns all DB values mapped to their proper Ruby primitive. To avoid wasting precious time, we bypass all Rails type casting in one single place now.

These objects are a great step forward and they should open up all sorts of possibilities for gems to extend our DB objects. Thanks so much to Sean Griffin and anyone else working on ActiveRecord to make it better, faster, and easier to use!

Resources

by Ken Collins
AWS Serverless Hero