Digging into ActiveRecord and PostgreSQL Enums
I came across an interesting problem in one of my ActiveRecord models (paraphrased, this isn’t the exact model):
class Event < ActiveRecord::Base
attr_accessible :certainty
validates :certainty, :inclusion => {
:in => %w(less neutral more),
:message => "%{value}"
}
end
The problem is I would set certainty to one of the accepted values, let’s say 'less', and the form would wind up throwing an error. I overrode the default error message just to retrieve the value and it turns out the value for certainty is 0.
The reason this was happening is because certainty is defined as a PostgreSQL enumerated type:
CREATE TYPE certainty AS ENUM ('less', 'neutral', 'more');
and these are the type detection methods in ActiveRecord:
From lib/active_record/connection_adapters/postgresql_adapter.rb:
def simplified_type(field_type)
case field_type
# Numeric and monetary types
when /^(?:real|double precision)$/
:float
# Monetary types
when 'money'
:decimal
when 'hstore'
:hstore
# Network address types
when 'inet'
:inet
when 'cidr'
:cidr
when 'macaddr'
:macaddr
# Character types
when /^(?:character varying|bpchar)(?:\(\d+\))?$/
:string
# Binary data types
when 'bytea'
:binary
# Date/time types
when /^timestamp with(?:out)? time zone$/
:datetime
when 'interval'
:string
# Geometric types
when /^(?:point|line|lseg|box|"?path"?|polygon|circle)$/
:string
# Bit strings
when /^bit(?: varying)?(?:\(\d+\))?$/
:string
# XML type
when 'xml'
:xml
# tsvector type
when 'tsvector'
:tsvector
# Arrays
when /^\D+\[\]$/
:string
# Object identifier types
when 'oid'
:integer
# UUID type
when 'uuid'
:uuid
# Small and big integer types
when /^(?:small|big)int$/
:integer
# Pass through all types that are not specific to PostgreSQL.
else
super
end
end
and lib/active_record/connection_adapters/column.rb:
def simplified_type(field_type)
case field_type
when /int/i
:integer
when /float|double/i
:float
when /decimal|numeric|number/i
extract_scale(field_type) == 0 ? :integer : :decimal
when /datetime/i
:datetime
when /timestamp/i
:timestamp
when /time/i
:time
when /date/i
:date
when /clob/i, /text/i
:text
when /blob/i, /binary/i
:binary
when /char/i, /string/i
:string
when /boolean/i
:boolean
end
end
The field_type in the above method calls is 'certainty.' The simplified_type in PostgreSQLColumn doesn’t match on any of the cases and it gets passed to the parent. In the parent, Column, 'certainty' matches against 'when /int/i' and returns integer as a type. Once the integer is set as the type, ActiveRecord does its thing and converts the attribute to an integer prior to a save. The validation then triggers on the changed value.
I couldn’t figure out where the variable actually gets type checked and changed to the correct type value. I tried modifying the column type directly in the model but that didn’t prevent the variable from being converted. My solution was to simply ALTER TYPE certainty RENAME TO certain. Postgre cascaded the changes and everything ended up being okay.
I want to thank Joe for taking the lead and filing a bug report here: https://github.com/rails/rails/issues/7814.