0

In my Rails 7 app using Postgres, I have defined a status enum for the User model:

class CreateUsers < ActiveRecord::Migration[7.0]
  def change
    create_enum :status, ["pending", "active", "archived"]

    create_table :users, force: true do |t|
      t.enum :status, enum_type: "status", default: "pending", null: false
      t.timestamps
    end
  end
end

app/models/user.rb

class User < ApplicationRecord
  enum status: {
    pending: 'Is Pending',
    active: 'Is active',
    archived: 'Is Archived',
    disabled: 'is Disabled',
    waiting: 'waiting'
  }

end

Notice that unlike most of the examples on the internet, my status enum values are distinct from the keys ('pending' maps to 'Is Pending', etc)

My view code looks like this:

<%= f.collection_select(:status,  enum_to_collection_select( User.defined_enums['status']), :key, :value, {selected: @user.status }, class: 'form-control') %>

The view displays correctly and submits the key back to the controller:

10:00:02 web.1  | Started POST "/users" for 127.0.0.1 at 2023-02-07 10:00:02 -0500
10:00:02 web.1  | Processing by UsersController#create as TURBO_STREAM
10:00:02 web.1  |   Parameters: {"authenticity_token"=>"[FILTERED]", "user"=>{"status"=>"active"}, "commit"=>"Save"}
10:00:02 web.1  |   TRANSACTION (0.1ms)  BEGIN
10:00:02 web.1  |   ↳ app/controllers/users_controller.rb:34:in `create'

But then when the controller processes it, it attempts to save using the value. This fails because I defined my Enums in postgres to be the keys not the values:

10:00:02 web.1  |   User Create (0.8ms)  INSERT INTO "users" ("status", "created_at", "updated_at") VALUES ($1, $2, $3) RETURNING "id"  [["status", "Is active"], ["created_at", "2023-02-07 15:00:02.951440"], ["updated_at", "2023-02-07 15:00:02.951440"]]
10:00:02 web.1  |   ↳ app/controllers/users_controller.rb:34:in `create'
10:00:02 web.1  |   TRANSACTION (0.1ms)  ROLLBACK
10:00:02 web.1  |   ↳ app/controllers/users_controller.rb:34:in `create'
10:00:02 web.1  | Completed 500 Internal Server Error in 5ms (ActiveRecord: 1.0ms | Allocations: 4529)
10:00:02 web.1  | 
10:00:02 web.1  | 
10:00:02 web.1  |   
10:00:02 web.1  | ActiveRecord::StatementInvalid (PG::InvalidTextRepresentation: ERROR:  invalid input value for enum status: "Is active"
10:00:02 web.1  | CONTEXT:  unnamed portal parameter $1 = '...'
10:00:02 web.1  | ):

Why is this? Shouldn't Postgres be using the keys for storage and not the values?

Isn't the point of the enum so that I can change the value displays on the frontend without changing the labels (the values) without having to modify the database?

Why does Rails translate the keys into values when saving the record?

example app here:

https://github.com/jasonfb/EnumTest1

Jason FB
  • 4,752
  • 3
  • 38
  • 69
  • Unfortunately that is not how [`ActiveRecord::Enum`](https://api.rubyonrails.org/v7.0/classes/ActiveRecord/Enum.html) works. For `Hash` implementation it is `attribute: database_value` – engineersmnky Feb 07 '23 at 15:10
  • 2
    You're using ActiveRecord::Enum incorrectly. The point is to provide a mapping between machine/programmer readible labels and an integer column in the database. Not between the machine/programmer readible labels and the user facing text. You can't really blame the hammer when you're banging on a screw. If you want to humanize the labels use the I18n module. – max Feb 07 '23 at 15:21
  • https://stackoverflow.com/questions/22827270/rails-how-to-use-i18n-with-rails-4-enums – max Feb 07 '23 at 15:24
  • @max - no, I'm talking about the Rals 7 implementation, which uses native enum support in Postgres, not integers. The keys are like integers. – Jason FB Feb 07 '23 at 15:32
  • @max - actually I think the solution in your link `self.human_enum_name(enum_name, enum_value)` is the closest hack to what I need, but I'm confused as to why Rails 7 would add native support for Enums and not prefer the keys instead of the labels. – Jason FB Feb 07 '23 at 15:42
  • Again - you're expecting the method to do something very different then what its designed for. Just because you're using an enum instead of an integer to back it you shouldn't expect it to cover a completely differrent use case. – max Feb 07 '23 at 15:46
  • And yes I know what the PG enum type is. I used it way back in Rails 4 or 5 and came to the conclusion that its not a good idea for what you use ActiveRecord::Enum for as adding states requires you to redefine the entire type. YMMV. The only thing new in Rails 7 is that there is a SQL generator in SchemaStatements to generate the SQL string used to define it. Neither the behavior nor use case for ActiveRecord::Enum has changed. – max Feb 07 '23 at 15:51
  • Right, it makes sense now. It just seems like there's actually 3 "names" for each enum: The Postgres name, the Rails code-name, and the label. Which seems like one name too many. But yes just strickly speaking under the hood it makes sense why it works this way. – Jason FB Feb 07 '23 at 16:10
  • 1
    You have to remember that its intended as a low level component to make handling enum states in a model more managable. Its not actually intented to solve the issue of generating selects or checkboxes to alter the state but rather its just the basic building block for making more complex functionality. – max Feb 07 '23 at 16:47

1 Answers1

0

So, here's a hack that makes it work the way I want it to, but it's not very elegant. It seems like this should be easier/more straight-forward

  enum status: {
    pending: "pending",
    active: "active",
    archived: "archived",
    disabled: "disabled",
    waiting: "waiting"
  }

  def self.status_labels
    {
      pending: 'Is Pending',
      active: 'Is active',
      archived: 'Is Archived',
      disabled: 'is Disabled',
      waiting: 'waiting'
    }
  end
    <span class='<%= "alert-danger" if user.errors.details.keys.include?(:status) %>'  style="display: inherit;"  >
      <%= f.collection_select(:status,  enum_to_collection_select( User.status_labels), :key, :value, {selected: @user.status }, class: 'form-control') %>
      <label class='small form-text text-muted'>Status</label>
    </span>

What's important here is that the enum ActiveRecord declaration using a Hash syntax maps the internal code name to the Postgres-database name. (the keys are the internal code name and the values are the Postgres-database name)... Neither one is intended for use as the 'label' on the front-end, which to me is disappointing because it seems like this is such a universally needed thing and an opportunity for Rail to make it "just work" but instead it requires extra steps to make it work.

Jason FB
  • 4,752
  • 3
  • 38
  • 69