Giter Club home page Giter Club logo

activerecord-pg_enum's Introduction

ActiveRecord::PGEnum Build Status

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.

activerecord-pg_enum's People

Contributors

agrobbin avatar alassek avatar andrewspeed avatar dudleyf avatar pedantic-git avatar tylerrick avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

activerecord-pg_enum's Issues

enum helper not working with change_table

I assumed that create_table and change_table use the same class but it appears not.

NoMethodError: undefined method `enum' for #<ActiveRecord::ConnectionAdapters::PostgreSQL::Table:0x00007fe990c40bb8>

Consider removing enum types as part of `db:drop` / `db:reset`

Since Postgres defines types in the postgres tables, running db:reset can get you into a situation where you cannot re-run your migrations that define your Postgres enums. As a simple reproduction:

  1. Create a migration where you use create_enum
  2. Run the migration with db:migrate
  3. Reset your database with db:reset

The migration will fail because the type already exists.

This all makes sense, but I wonder if there's a way we could make it so db:drop also drops any enums defined by this gem?

Schema dumper assumes that enum values will not contain spaces

Hi ๐Ÿ‘‹

I've been using activerecord-pg_enum for a few weeks now, it's a fantastic gem, thanks for writing it.

Today I was adding a migration to create an enum which has values containing spaces, but the dumper doesn't appear to handle this correctly.

I've been taking a look at the codebase and would be happy to try and make this change, assuming it's deemed valid.

Steps to reproduce

Rails version: 5.2.2.1
Ruby version: 2.6.3
activerecord-pg_enum version: 1.0.2

Migration

# frozen_string_literal: true
โ€‹
class AddStatusEnum < ActiveRecord::Migration[5.2]
  def up
    create_enum 'status', ['sick', 'on vacation', 'working from home']
  end
โ€‹
  def down
    drop_enum 'status'
  end
end

Schema

Expected Schema

ActiveRecord::Schema.define(version: 2019_09_27_145140) do
  # ...

  # These are custom enum types that must be created before they can be used in the schema definition
  create_enum "status", ['sick', 'on vacation', 'working from home']
end

Actual Schema

ActiveRecord::Schema.define(version: 2019_09_27_145140) do
  # ...

  # These are custom enum types that must be created before they can be used in the schema definition
  create_enum "status", %w[sick on vacation working from home]
end

This โ˜๏ธwill lead to the enum being created with values ["sick", "on", "vacation", "working", "from", "home"]

Cannot add new enum value

Hi, thanks for your work!

I try to add a new enum value on an existing enum like the documentation

class AddNewValue < ActiveRecord::Migration[5.1]
  def change
    add_enum_value :my_test_enum, 'FOO', before: 'BAR'
  end
end

But when i run migrations, i got this error

Caused by:
ActiveRecord::StatementInvalid: PG::ActiveSqlTransaction: ERROR:  ALTER TYPE ... ADD cannot run inside a transaction block
: ALTER TYPE my_test_enum ADD VALUE 'FOO' BEFORE 'BAR'

I use this gem on a rails 5.1 application

Support Rails enum options

๐Ÿ‘‹ I ran into another use case which activerecord-pg_enum doesn't seem to support yet, and wanted to start a discussion about whether we would want to support it, and if so how it would be implemented.

As with #3, I'm happy to attempt implementing this change, if it's decided that we want to support it.

Example

When using activerecord-pg_enum, we can't specify any of the options supported by the ActiveRecord::Enum class, e.g. prefix and suffix.

This means that two enums with the same keys will collide when it comes to the autogenerated methods provided by this class, for example:

Rails enum

class Conversation < ActiveRecord::Base
  enum status: [:active, :archived], _suffix: true
  enum comments_status: [:active, :inactive], _prefix: :comments
end
conversation.active_status!
conversation.archived_status? # => false

conversation.comments_inactive!
conversation.comments_active? # => false

Proposed activerecord-pg_enum implementation

One option to implement this behaviour could be to accept any options to the include and pass them through to the ActiveRecord::Enum in the module definition

class Conversation < ActiveRecord::Base
  include PGEnum(status: [:active, :archived], _suffix: true)
  include PGEnum(status: [:active, :archived], _prefix: :comments)
end
conversation.active_status!
conversation.archived_status? # => false

conversation.comments_inactive!
conversation.comments_active? # => false

Application does not boot when also using active_record_extended

Hi ๐Ÿ‘‹ !

I tried using activerecord-pg_enum on a project which already uses active_record_extended. Since active_record_extended has been in use without issues I am reporting the issue here.

Traceback (most recent call last):
	7: from pg_enum.rb:6:in `<main>'
	6: from /Users/dominik/.asdf/installs/ruby/2.6.1/lib/ruby/2.6.0/bundler/inline.rb:70:in `gemfile'
	5: from /Users/dominik/.asdf/installs/ruby/2.6.1/lib/ruby/2.6.0/bundler/runtime.rb:65:in `require'
	4: from /Users/dominik/.asdf/installs/ruby/2.6.1/lib/ruby/2.6.0/bundler/runtime.rb:65:in `each'
	3: from /Users/dominik/.asdf/installs/ruby/2.6.1/lib/ruby/2.6.0/bundler/runtime.rb:76:in `block in require'
	2: from /Users/dominik/.asdf/installs/ruby/2.6.1/lib/ruby/2.6.0/bundler/runtime.rb:76:in `each'
	1: from /Users/dominik/.asdf/installs/ruby/2.6.1/lib/ruby/2.6.0/bundler/runtime.rb:81:in `block (2 levels) in require'
/Users/dominik/.asdf/installs/ruby/2.6.1/lib/ruby/2.6.0/bundler/runtime.rb:81:in `require': cannot load such file -- activerecord-pg_enum (LoadError)
	16: from pg_enum.rb:6:in `<main>'
	15: from /Users/dominik/.asdf/installs/ruby/2.6.1/lib/ruby/2.6.0/bundler/inline.rb:70:in `gemfile'
	14: from /Users/dominik/.asdf/installs/ruby/2.6.1/lib/ruby/2.6.0/bundler/runtime.rb:65:in `require'
	13: from /Users/dominik/.asdf/installs/ruby/2.6.1/lib/ruby/2.6.0/bundler/runtime.rb:65:in `each'
	12: from /Users/dominik/.asdf/installs/ruby/2.6.1/lib/ruby/2.6.0/bundler/runtime.rb:72:in `block in require'
	11: from /Users/dominik/.asdf/installs/ruby/2.6.1/lib/ruby/2.6.0/bundler/runtime.rb:95:in `rescue in block in require'
	10: from /Users/dominik/.asdf/installs/ruby/2.6.1/lib/ruby/2.6.0/bundler/runtime.rb:95:in `require'
	 9: from /Users/dominik/.asdf/installs/ruby/2.6.1/lib/ruby/gems/2.6.0/gems/activerecord-pg_enum-1.0.1/lib/activerecord/pg_enum.rb:1:in `<top (required)>'
	 8: from /Users/dominik/.asdf/installs/ruby/2.6.1/lib/ruby/gems/2.6.0/gems/activerecord-pg_enum-1.0.1/lib/active_record/pg_enum.rb:4:in `<top (required)>'
	 7: from /Users/dominik/.asdf/installs/ruby/2.6.1/lib/ruby/gems/2.6.0/gems/activesupport-5.2.3/lib/active_support/lazy_load_hooks.rb:42:in `on_load'
	 6: from /Users/dominik/.asdf/installs/ruby/2.6.1/lib/ruby/gems/2.6.0/gems/activesupport-5.2.3/lib/active_support/lazy_load_hooks.rb:42:in `each'
	 5: from /Users/dominik/.asdf/installs/ruby/2.6.1/lib/ruby/gems/2.6.0/gems/activesupport-5.2.3/lib/active_support/lazy_load_hooks.rb:43:in `block in on_load'
	 4: from /Users/dominik/.asdf/installs/ruby/2.6.1/lib/ruby/gems/2.6.0/gems/activesupport-5.2.3/lib/active_support/lazy_load_hooks.rb:67:in `execute_hook'
	 3: from /Users/dominik/.asdf/installs/ruby/2.6.1/lib/ruby/gems/2.6.0/gems/activesupport-5.2.3/lib/active_support/lazy_load_hooks.rb:62:in `with_execution_control'
	 2: from /Users/dominik/.asdf/installs/ruby/2.6.1/lib/ruby/gems/2.6.0/gems/activesupport-5.2.3/lib/active_support/lazy_load_hooks.rb:71:in `block in execute_hook'
	 1: from /Users/dominik/.asdf/installs/ruby/2.6.1/lib/ruby/gems/2.6.0/gems/activesupport-5.2.3/lib/active_support/lazy_load_hooks.rb:71:in `instance_eval'
/Users/dominik/.asdf/installs/ruby/2.6.1/lib/ruby/gems/2.6.0/gems/activerecord-pg_enum-1.0.1/lib/active_record/pg_enum.rb:5:in `block in <top (required)>': uninitialized constant ActiveRecord::PGEnum (NameError)

Reproduction script:

# frozen_string_literal: true

require "bundler/inline"

gemfile(true) do
  source "https://rubygems.org"

  git_source(:github) { |repo| "https://github.com/#{repo}.git" }

  gem "rails", "~> 5.2"
  gem 'active_record_extended'
  gem 'activerecord-pg_enum'
  gem "pg"
end

require "active_record"
require "minitest/autorun"
require "logger"

Thoughts on new feature: force cascade for create_enum

When I run rails db:setup in my development environment and most the of the tables already exist, active record's create_table will drop the table for me by default because of the force: :cascade option in schema.rb. But when my schema.rb has instances of create_enum, they fail with the following error:

-- create_enum("boolean_operator_enum_type", ["any", "all"])
rails aborted!
ActiveRecord::StatementInvalid: PG::DuplicateObject: ERROR:  type "boolean_operator_enum_type" already exists
: CREATE TYPE boolean_operator_enum_type AS ENUM ('any', 'all')

What are your thoughts on adding a force: :cascade option to create_enum, so that the enum can be dropped and db:setup can be run easier? I'd be more than happy to create the PR, but wanted to check your thoughts on it first: good idea? bad idea?

Note: As a current solution, I temporarily change my schema.rb to this:

module ActiveRecord
  module PGEnum
    module SchemaStatements
      def drop_enum_if_exists_cascade(name, values_for_revert = nil)
        execute("DROP TYPE IF EXISTS #{name} CASCADE").tap {
          reload_type_map
        }
      end
    end
  end
end

ActiveRecord::Schema.define(version: 2020_12_22_214218) do
   drop_enum_if_exists_cascade "boolean_operator_enum_type"
   create_enum "boolean_operator_enum_type", ["any", "all"]
   # ... rails schema here
end

Segmentation fault for Ruby 3.0

Hi, seems like I found segfault for Ruby 3.0

When trying to load rails 6.1.1 environment:

/.rbenv/versions/3.0.0/lib/ruby/gems/3.0.0/gems/activerecord-pg_enum-1.2.2/lib/active_record/pg_enum/postgresql_adapter.rb:5: [BUG] Segmentation fault at 0x0000000300000004
ruby 3.0.0p0 (2020-12-25 revision 95aff21468) [x86_64-darwin20]

-- Crash Report log information --------------------------------------------
   See Crash Report log file under the one of following:
     * ~/Library/Logs/DiagnosticReports
     * /Library/Logs/DiagnosticReports
   for more details.
Don't forget to include the above Crash Report log file in bug reports.

Support Rails 6.1

There are currently some test failures on 6.1.0.rc1 related to the change to configuration objects.

couple notes on readme

Thanks for an awesome project! Couple notes on the readme:

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" }

You can actually continue to use integers in the db and do this:

enum status: { new: 0, active: 1, archived: 2 }`

Which solves the storage size and accidental reordering problems, but does not solve the ugly query problem.

An enum value only takes up four bytes.

You can actually back a rails enum with a 2-byte integer, with limit: 2 in the migration when creating the column.

I dream of a time when we can get the best of all worlds, where 1. postgres supports 1 or 2-byte columns, including for enums 2. rails supports postgres enums natively

Rails 7 support

[PGEnum] Current ActiveRecord version unsupported! Falling back to: 6.1

Rails 7 support

Hi, is there any plan to add support for Rails 7?

I tried using the gem within a Rails 7 app, but it seems they are not compatible.

Support for renaming, removing enum values

Thanks for this gem! It would be nice if this gem had support for renaming and removing enum values as well. It's rare for me to get things right the first time, so being able to easily change them later is pretty valuable.

It looks like on Postgres 10 and up renaming a value is relatively painless. Removing a value isn't directly supported, but it looks like there is a workaround that isn't too horrendous.

`create_enum` not getting dumped to schema.rb

https://github.com/alassek/activerecord-pg_enum implies that running a migration containing

class TestEnum < ActiveRecord::Migration[5.2]
  def change
    create_enum "status_type", %w[new pending active archived]
  end
end

will add this line to your schema.rb:

  create_enum "status_type", %w[new pending active archived]

but it didn't add that line to my schema.rb ๐Ÿ˜•

(It only added the t.enum ... line that depends on that type.)

Running bin/rails db:structure:dump confirms that it did get added to my database:

CREATE TYPE app_name_development.status_type AS ENUM (
    'new',
    'pending',
    'active',
    'archived'
);

... just not to my schema.rb, which was the main point of using this gem ๐Ÿ˜„

Using Rails 5.2

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.