activerecord-postgres_enum
                                
                                 activerecord-postgres_enum copied to clipboard
                                
                                    activerecord-postgres_enum copied to clipboard
                            
                            
                            
                        Support for ActiveRecord::Enum
When trying to use ActiveRecord's Enum stuff with an enum column I end up with a type mismatch when creating records. ActiveRecord defaults to converting the enum values to integers which does not map correctly to PostgreSQL's enum types. The workaround is to specify each value as the string version of the enum.
Model
class AddOn < ActiveRecord::Base
  enum category: %i[thermostat humidifier cleaner purifier]
end
AddOn.create(category: "thermostat")
Error
   (0.1ms)  BEGIN
  AddOn Create (0.4ms)  INSERT INTO "add_ons" ("created_at", "updated_at", "category") VALUES ($1, $2, $3) RETURNING "id"  [["created_at", "2019-03-05 16:35:17.980836"], ["updated_at", "2019-03-05 16:35:17.980836"], ["category", 0]]
   (0.0ms)  ROLLBACK
Traceback (most recent call last):
        1: from (irb):1
ActiveRecord::StatementInvalid (PG::InvalidTextRepresentation: ERROR:  invalid input value for enum add_on_categories: "0")
: INSERT INTO "add_ons" ("created_at", "updated_at", "category") VALUES ($1, $2, $3) RETURNING "id"
Workaround
class AddOn < ActiveRecord::Base
  enum category: {
    thermostat: "thermostat",
    humidifier: "humidifier",
    cleaner: "cleaner",
    purifier: "purifier",
  }
end
Yeah, this is expected behavior. It looks like about this should be written into Readme.
I think Rails' enum is working as expected and we shouldn't try to change it, however maybe there is an opportunity for this gem to add a pg_enum that accepts an array of strings or symbols, so we don't have do repetitive things like like the suggested workaround.
Or, we could probably even query the enum on class load, so a model would only have:
class AddOn < ActiveRecord::Base
  pg_enum :category
end
The code for the AR::Enum is really short, I "forked" it for our application to change a few things:
- It takes an array and uses the string value of the symbol as the DB value
- The !method does only change the enum value and does not save the objct
Could be used as a base here: https://gist.github.com/ioki-klaus/9cc363153cc6ecef649c274aff00a7ad
I don't know if something like this should be integrated into the project, but here's how I'm making working with the enums a little easier. In my case, I really did need to get the list of candidate values so I could create a GraphQL enum with graphql-ruby.
A base class for working with all PostgreSQL enums:
class PgEnum
  def enum_name
    raise NotImplementedError
  end
  def self.values
    ActiveRecord::Base.connection.enums[:integration_account_state]
  end
  def self.as_activerecord_enum
    values.each_with_object({}) { |enum_value, hash| hash[enum_value] = enum_value }
  end
end
What a specific enum looks like:
class IntegrationAccountState < PgEnum
  def self.enum_name
    :integration_account_state
  end
end
How a model can use this enum helper:
class Sample < ActiveRecord::Base
  enum account_state: IntegrationAccountState.as_activerecord_enum, _prefix: :account_state
  validates :account_state, :inclusion => { in: IntegrationAccountState.values }
end
I've only just begun to use this library, but those little helpers helped improve the aesthetics of the code. I haven't used it long enough to have encountered any pitfalls yet.
…maybe there is an opportunity for this gem to add a
pg_enum…
I added these methods to ApplicationRecord in my project based on suggestions from @jeremy-ebler-vineti and code above from @nirvdrum:
class ApplicationRecord < ActiveRecord::Base
  def self.pg_enum_values(attr_name)
    metadata = column_for_attribute(attr_name).sql_type_metadata
    unless metadata.type == :enum
      raise ArgumentError, "Expected `:enum` type for column `#{attr_name}`, but was #{metadata.type}"
    end
    connection.enums[metadata.sql_type.to_sym]
  end
  def self.pg_enum(attr_name, options = {})
    enum options.merge attr_name => pg_enum_values(attr_name).to_h { |v| [v, v] }
  end
end
So now my models look like this:
class Listing < ApplicationRecord
  pg_enum :status
  pg_enum :condition, _suffix: true
  validates :condition, :inclusion => { in: pg_enum_values(:condition) }
end
In general, I am very against of having code, that calls database on class load. IMO this might create desync problems with code/db versions of enum. So have come to another idea of doing this w/o interacting with database - via parsing of schema.rb.
class ApplicationRecord
  def self.pg_enumerize(field, as:, **attrs)
    schema = Rails.root.join("db/schema.rb").read
    matched = schema.match(/create_enum :#{as}, \[([^\]]*)\]/m)
    values = matched[1].tr(" \n\"", "").split(",").map(&:to_sym)
    enumerize field, in: values, **attrs
  end
end
class SomeModel < ApplicationRecord
  pg_enumerize :field, as: :enum_type, ...
end
P.S. This is actually for enumerize gem - we prefer it more vs rail's enum because of richer functionality, but code is similar.