activerecord-postgres_enum
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.