Note: This was originally written before Rails added support for native enums. See the comments in version support.
The enum
feature in Rails has bad developer ergonomics. It uses integer types at the DB layer, which means trying to understand SQL output is a pain.
Using the easy form of the helper syntax is a minor footgun:
enum status: %w[new active archived]
It's not obvious that the above code is order-dependent, but if you decide to add a new enum anywhere but the end, you're in trouble.
If you choose the use varchar
fields instead, now you have to write annoying check constraints and lose the efficient storage.
enum status: { new: "new", active: "active", archived: "archived" }
Nobody has time to write that nonsense.
Enumerated Types: The Best of Both Worlds
Did you know you can define your own types in PostgreSQL? You can, and this type system also supports enumeration.
CREATE TYPE status_type AS ENUM ('new', 'active', 'archived');
Not only does this give you full type safety at the DB layer, the implementation is highly efficient. An enum value only takes up four bytes.
The best part is that PostgreSQL supports inserting new values at any point of the list without having to migrate your data.
ALTER TYPE status_type ADD VALUE 'pending' BEFORE 'active';
schema.rb Support
The principle motivation of this gem is to seamlessly integrate PG enums into your schema.rb
file. This means you can use them in your database columns without switching to structure.sql
.
ActiveRecord::Schema.define(version: 2019_06_19_214914) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
create_enum "status_type", %w[new pending active archived]
create_table "orders", id: :serial, force: :cascade do |t|
t.enum "status", as: "status_type", default: "new"
end
end
Version support
Every version of Rails with an enum
macro is supported. This means 4.1 through master. Yes, this was annoying and difficult.
The monkeypatches in this library are extremely narrow and contained; the dirty hacks I had to do to make 4.1 work, for instance, have no impact on 6.0.
Monkeypatching Rails internals is scary. So this library has a comprehensive test suite that runs against every known minor version.
Rails 7 added support for native enums, but they have so far neglected to support altering or dropping enums in the API, so this gem remains to fill in the gaps. I expect to slowly deprecate this over time.
Installation
Add this line to your application's Gemfile:
gem 'activerecord-pg_enum'
And then execute:
$ bundle
Usage
Migrations
Defining a new ENUM
class AddContactMethodType < ActiveRecord::Migration[5.2]
def up
create_enum "contact_method_type", %w[Email Phone]
end
def down
drop_enum "contact_method_type"
end
end
Adding a value to an existing ENUM (you must disable the wrapping transaction on PostgreSQL versions older than 12)
class AddSMSToContactMethodType < ActiveRecord::Migration[5.2]
disable_ddl_transaction!
def up
add_enum_value "contact_method_type", "SMS", before: "Phone"
end
def down
raise ActiveRecord::IrreversibleMigration
end
end
Adding an enum column to a table
class AddStatusToOrder < ActiveRecord::Migration[5.2]
def change
change_table :orders do |t|
t.enum :status, as: "status_type", default: "new"
end
end
end
Renaming an enum type
class RenameStatusType < ActiveRecord::Migration[6.0]
def change
rename_enum "status_type", to: "order_status_type"
end
end
ALTER TYPE status_type RENAME TO order_status_type;
PostgreSQL 10+ required:
Changing an enum label
class ChangeStatusHoldLabel < ActiveRecord::Migration[6.0]
def change
rename_enum_value "status_type", from: "on hold", to: "OnHold"
end
end
ALTER TYPE status_type RENAME VALUE 'on hold' TO 'OnHold';
Module Builder
class ContactInfo < ActiveRecord::Base
include PGEnum(contact_method: %w[Email SMS Phone])
end
The generated module calls the official enum
method converting array syntax into strings. The above example is equivalent to:
class ContactInfo < ActiveRecord::Base
enum contact_method: { Email: "Email", SMS: "SMS", Phone: "Phone" }
end
Additionally, enum
options are fully supported, for example
class User < ActiveRecord::Base
include PGEnum(status: %w[active inactive deleted], _prefix: 'user', _suffix: true)
end
is equivalent to
class User < ActiveRecord::Base
enum status: { active: 'active', inactive: 'inactive', deleted: 'deleted' }, _prefix: 'user', _suffix: true
end
There's no technical reason why you couldn't detect enum columns at startup time and automatically do this wireup, but I feel that the benefit of self-documenting outweighs the convenience.
Development
After checking out the repo, run bin/setup
to install dependencies. Then, run appraisal rake spec
to run the tests. You can also run bin/console
for an interactive prompt that will allow you to experiment.
Test a specific version with appraisal 6.0 rake spec
. This is usually necessary because different versions have different Ruby version support.
To install this gem onto your local machine, run bundle exec rake install
.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/alassek/activerecord-pg_enum.
License
The gem is available as open source under the terms of the MIT License.