Last Updated: October 12, 2018
·
11.4K
· glebm

Postgres Enum Type in Rails 4

Rails has no support for Postgres Enum types yet, but if you need them today, here is a quick way to do it.

So you've created the enum type in the db:

CREATE TYPE mood AS ENUM ('sad', 'ok', 'happy');

We should let ActiveRecord know. First alias the type to text (do this to avoid "Invalid OID" type warnings, really this will be a custom type once Rails supports these out of the box):

ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::OID.alias_type 'my_enum_type', 'text'

You'll also want to patch the column parser to return the correct column type:

class ActiveRecord::ConnectionAdapters::Column
  private
  def simplified_type_with_my_enum_type(field_type)
    if field_type == 'my_enum_type'
      field_type.to_sym
    else
      simplified_type_without_my_enum_type(field_type)
    end
  end
  alias_method_chain :simplified_type, :my_enum_type
end

When querying, you will also want to convert the string to your type, for example (if you want to, there is a way to make rails convert it automatically, see OID::Point from this pull request):

scope :my_enum_value_is, ->(value){ 
  where('my_enum_value = ?::my_enum_type', value) 
}

This is an extended version of an answer to this StackOverflow question, and includes stuff from the comments

4 Responses
Add your response

Worth noting that if you're going to use ENUMs you'll need a migration for them:

class CreateEnumerations < ActiveRecord::Migration
  ENUM_TYPES = {
    :common_sex => [
      'Male', 'Female',
    ],
  }

  def self.up
    ENUM_TYPES.each do |type, values|
      values = "'" + values.join("', '") + "'"
      sql = "CREATE TYPE #{type} AS ENUM(#{values})"
      execute sql
    end
  end

  def self.down
    ENUM_TYPES.each_key do |type|
      sql = "DROP TYPE #{type}"
      execute sql
    end
  end
end

Also, a nicer core_ext/initializer for patching ActiveRecord::ConnectionAdapters::Column and adding type aliases could be put in place:

module AppName
  module ActiveRecord
    module ConnectionAdapters
      module Column
        ENUM_TYPES = [
          :common_sex,
          :demographic_projections_race,
          :demographic_projections_education,
        ]

        def self.included(base)
          ENUM_TYPES.each do |type|
            ::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::OID.alias_type type.to_s, 'text'
          end

          base.class_eval do
            def simplified_type_with_custom_enum_types(type)
              sym = type.to_sym

              if sym.in?(ENUM_TYPES)
                sym
              else
                simplified_type_without_custom_enum_types(type)
              end
            end

            alias_method_chain :simplified_type, :custom_enum_types
          end
        end
      end
    end
  end
end

unless ::ActiveRecord::ConnectionAdapters::Column.include?(::AppName::ActiveRecord::ConnectionAdapters::Column)
  ::ActiveRecord::ConnectionAdapters::Column.send(:include, ::AppName::ActiveRecord::ConnectionAdapters::Column)
end

This way everything sums up to just 2 files (or more, if you need to add more ENUMs as the project goes)
And last, but not least - typecasting the value is optional:

User.where(:sex => 'Male')
over 1 year ago ·

Any advices on creating migration file? I've did it this way:

 create_table :messages do |t|
   #...
   t.column :direction, :message_direction
   #...
end

And everything seems fine, but I got such comment in my schema file:

# Could not dump table "messages" because of following StandardError
#   Unknown type 'message_direction' for column 'direction'

Any ideas to fix it? Did I write correct migration file?

over 1 year ago ·

I'm using this patch:

# PostgreSQL enums support (adapted from https://coderwall.com/p/azi3ka)
# Should be fixed in Rails >= 4.2 (https://github.com/rails/rails/pull/13244)
module ActiveRecord
  module ConnectionAdapters
    class PostgreSQLAdapter
      module OID
        class Enum < Type
          def type_cast(value)
            value.to_s
          end
        end
      end

      def enum_types
        @enum_types ||= begin
          result = execute 'SELECT DISTINCT t.oid, t.typname FROM pg_type t JOIN pg_enum e ON t.oid = e.enumtypid', 'SCHEMA'
          Hash[ result.map { |v| [ v['oid'], v['typname'] ] } ]
        end
      end

      private

      def initialize_type_map_with_enum_types_support
        initialize_type_map_without_enum_types_support

        # populate enum types
        enum_types.reject { |_, name| OID.registered_type? name }.each do |oid, name|
          OID::TYPE_MAP[oid.to_i] = OID::Enum.new
        end
      end
      alias_method_chain :initialize_type_map, :enum_types_support
    end

    class PostgreSQLColumn
      private

      def simplified_type_with_enum_types(field_type)
        case field_type
        when *Base.connection.enum_types.values
          field_type.to_sym
        else
          simplified_type_without_enum_types(field_type)
        end
      end
      alias_method_chain :simplified_type, :enum_types
    end
  end
end
over 1 year ago ·

In ActiveRecord 2.4.1 there is native support for postgres enums. Maybe a little bit limited but works.

over 1 year ago ·