activerecord-postgres_enum icon indicating copy to clipboard operation
activerecord-postgres_enum copied to clipboard

Support for ActiveRecord::Enum

Open gaffneyc opened this issue 5 years ago • 6 comments

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

gaffneyc avatar Mar 05 '19 16:03 gaffneyc

Yeah, this is expected behavior. It looks like about this should be written into Readme.

bibendi avatar Mar 06 '19 07:03 bibendi

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

jeremy-ebler-vineti avatar Jun 13 '19 23:06 jeremy-ebler-vineti

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

ioki-klaus avatar Jul 21 '20 09:07 ioki-klaus

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.

nirvdrum avatar Aug 19 '20 22:08 nirvdrum

…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

bkeepers avatar Sep 02 '20 04:09 bkeepers

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.

dzirtusss avatar Sep 10 '20 21:09 dzirtusss