Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for ActiveRecord::Enum #9

Open
gaffneyc opened this issue Mar 5, 2019 · 6 comments
Open

Support for ActiveRecord::Enum #9

gaffneyc opened this issue Mar 5, 2019 · 6 comments
Labels
docs Docs are incorrect or incomplete

Comments

@gaffneyc
Copy link

gaffneyc commented Mar 5, 2019

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
@bibendi
Copy link
Owner

bibendi commented Mar 6, 2019

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

@bibendi bibendi added the docs Docs are incorrect or incomplete label Mar 6, 2019
@jeremy-ebler-vineti
Copy link
Contributor

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

@ioki-klaus
Copy link

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

@nirvdrum
Copy link
Contributor

nirvdrum commented Aug 19, 2020

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.

@bkeepers
Copy link

bkeepers commented Sep 2, 2020

…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

@dzirtusss
Copy link

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
docs Docs are incorrect or incomplete
Projects
None yet
Development

No branches or pull requests

7 participants