Postgres enums with Rails
Tags: rails, activerecord, postgres, enums
Published: 2018-07-11 05:14:03

---
title: Postgres enums with Rails
published: true
description: Guide and explanation of usage Postgres Enums with Rails
tags: #rails #active_record #postgres #enums
---

## Introduction

Enum - is a user-defined data type representing a set of named values.  In general, the idea of enums aims to provide 2 main purposes:
- to make codebase more readable and human-friendly
- to add extra type safety by limiting possible assignable value to the variable of enum type

Enum is based on integer type data type, what is definitely good for performance. If we are going to deal with a lot of data.

## The "Old school" way

The most common way of using "enum" is the definite state of some object. In spite of having no enums in pure Ruby, we are able to emulate them with constants or symbols. Here is the sample of old-styled Rails 3 (legacy) data-model source code:

```ruby
    class Account < ActiveRecord::Base
      PAID_STATUS = 'paid'
      OVERDUE_STATUS = 'ovedue'
      BLOCKED_STATUS = 'blocked'
    
      STATUSES = [PAID_STATUS, OVERDUE_STATUS, BLOCKED_STATUS].freeze
      validates :status, presence: true, inclusion: {in: STATUSES}
    
      def paid?
        self.status == PAID_STATUS
      end
    end
    
    # everywhere we work with Account instance - it is much easier
    # to deal with helper predicate instead of comparing values "in place"
    if account.paid?
      # do something special for the user who pays
    end
```

It gives more readability to the codebase and makes it looks friendly for a new developer! But it is overloaded with boilerplates - constants definition and values checking. This checks spread all over the codebase and make maintenance more difficult.

## The "Rails way"

Staring from [Rails 4.1 Active Record provides easier way to use enums](https://apidock.com/rails/ActiveRecord/Enum):
```ruby
    class Conversation < ActiveRecord::Base
      enum status: [ :active, :archived ]
    end
```
and we've got some useful mutation methods, check value methods and scopes out of the box, g.e.:
```ruby
    conversation.active! # sets the active status for object
    conversation.active? # checks if the object have active status
    Conversation.action.find_each { |c| puts c.status } # provides scope for searching
```

We've got source code to be more readable and maintainable.
Validation comes out of the box, and it raises the `ArgumentError` if a value is not defined.

```ruby
    conversation.status = :unknown
    => ArgumentError: 'unknown' is not a valid channel
```

Default behavior supposes to use numeric as the underlying type of the column:
```ruby
    create_table :conversations do |t|
      t.integer :status
    end
```
And Active Record will assign values for your named constants:
```ruby
    [1] pry(main)> Conversation.statuses
    => {"active"=>0, "archived"=>1}
```
Everything is great until we will need to add value to your enumeration (and make it first):
```ruby
    class Conversation < ActiveRecord::Base
      enum status: [ :initial, :active, :archived ]
    end
```
This change will force Active Record to re-enumerate values starting from zero. And values will mix up and crash your application logic:
```ruby
    > Conversation.statuses
    => {"initial"=>0, "active"=>1, "archived"=>2}
```

## The "Rails way" with Postgres Enums

We have to points to do it:
 - define enum values explicitly with model definition
 - introduce Postgres enum type to declare underlying database column data type

Here is the model with `channel` attribute having enumerated values:
```ruby
    class UserNotification < ActiveRecord::Base
      # define rails enum and the underlying values to use for every enum-value
      enum channel: {
        email: 'email',
        webpush: 'webpush',
        telegram: 'telegram'
      }
    end
```
It means that we will have `email`, `webpush` and `telegram` - as values for column `channel`.

Write migration to create Postgres enum-type `user_notification_channel`:
```ruby
    class CreateUserNotifications < ActiveRecord::Migration
      def up
        execute <<-DDL
          CREATE TYPE user_notification_channel AS ENUM (
            'email', 'webpush', 'telegram'
          );
        DDL
        
        create_table :user_notifications do |t|
          ...
          t.column :channel, :user_notification_channel
        end
      end
      
      def down
        drop_column :channel, :user_notification_channel
        execute "DROP type user_notification_channel;"
      end
    end
```

This trick lets us have 'string' value in Ruby:
```ruby
    notification.channel
    => "email"
```

but [occupies only 4 bytes in a database row](https://www.postgresql.org/docs/10/static/datatype-enum.html#id-1.5.7.15.8).

Main benefits of this complex "Rails&Postgres" way:
- readable and user-friendly source code
- string representation of enum in Ruby with optimal data storage in the database
- more type safety on database level
- meaningful and clear database structure out of Rails-application context

## How to examples

### Migrating existing text column to Postgres Enum

The reasons for doing this are:
- reduce table size (fixed 4 bytes enum vs string length)
- increase type safety and database data consistency
- make database values more meaningful and friendly (out of models context)

```ruby
    class CreateUserNotifications < ActiveRecord::Migration
      def up
        execute <<-DDL
          ALTER TABLE user_notifications
          ALTER COLUMN channel TYPE user_notification_channel
          USING channel::user_notification_channel;
        DDL
      end
      
      def down
        # moves column back to have text data-type
        execute <<-DDL
          ALTER TABLE user_notifications
          ALTER COLUMN channel TYPE text
          USING channel::text;
        DDL
      end
    end
```

This works well without breaking any changes!

Add value to existing enum migration:
```ruby
    class ExpandValueOfUserNotificationChannels < ActiveRecord::Migration
      def up
        execute <<-DDL
          ALTER TYPE user_notification_channel ADD VALUE 'post_pigeon';
        DDL
      end
    end
```

### Drop value from existing enum

Postgres does not allow to drop value from existing enum, but here is some trick to drop value:
 - migrate the column to text
 - re-create the new enum type
 - migrate back to the new enum

```ruby
    class RemoveTelegramFromUserNotificationChannel < ActiveRecord::Migration
      def up
        execute <<-DDL
          ALTER TABLE user_notifications 
          ALTER COLUMN channel TYPE text
          USING channel::text;
    
          DROP TYPE user_notification_channel;
    
          CREATE TYPE user_notification_channel AS ENUM (
            'email', 'webpush'
          );
    
          ALTER TABLE user_notifications
          ALTER COLUMN channel TYPE user_notification_channel
          USING channel::user_notification_channel;
        end
    end
```

### Migrate from numeric Rails enum to Postgres Enum

Imagine we have Rails enums and the integer channel column in database:

```ruby
    class UserNotification < ActiveRecord::Base
      enum channel: [:email, :webpush, :telegram]
    end
```

Numeric values of each Rails enum value could be gotten by the following code:
```ruby
    [1] pry(main)> UserNotification.channels
    => {"email"=>0, "webpush"=>1, "telegram"=>3}
```

Just alter column by changing type and transform the value from numeric to the enum:

```ruby
    class MigrateNotificationsChannelToPostgresEnum < ActiveRecord::Migration
      def up
        execute <<-DDL
          ALTER TABLE user_notifications
          ALTER COLUMN channel TYPE user_notification_channel
          USING CASE channel
                  WHEN 0 THEN 'email'::user_notification_channel
                  WHEN 1 THEN 'webpush'::user_notification_channel
                  WHEN 2 THEN 'telegram'::user_notification_channel
                  ELSE RAISE 'UNKNOWN VALUE'
          END
        DDL
      end
    end
```

This migration will transform numeric values to enums. Profit! 🙂 

## Conclusion

Postgres enums are great and helpful!
They are very easy to integrate with Rails! 
They help to ensure data consistency for the definite set of values. The increase human-readability of the data out of  the Rails application context.
They also have better performance (comparing to text values).

Thanks for reading.