mirror of https://github.com/rails/rails
Adding PG enum rename, add value, and rename value migration helpers
This commit is contained in:
parent
e10e35dd32
commit
ce6047f84f
|
@ -1,3 +1,25 @@
|
|||
* Added PostgreSQL migration commands for enum rename, add value, and rename value.
|
||||
|
||||
`rename_enum` and `rename_enum_value` are reversible. Due to Postgres
|
||||
limitation, `add_enum_value` is not reversible since you cannot delete enum
|
||||
values. As an alternative you should drop and recreate the enum entirely.
|
||||
|
||||
```ruby
|
||||
rename_enum :article_status, to: :article_state
|
||||
```
|
||||
|
||||
```ruby
|
||||
add_enum_value :article_state, "archived" # will be at the end of existing values
|
||||
add_enum_value :article_state, "in review", before: "published"
|
||||
add_enum_value :article_state, "approved", after: "in review"
|
||||
```
|
||||
|
||||
```ruby
|
||||
rename_enum_value :article_state, from: "archived", to: "deleted"
|
||||
```
|
||||
|
||||
*Ray Faddis*
|
||||
|
||||
* Allow composite primary key to be derived from schema
|
||||
|
||||
Booting an application with a schema that contains composite primary keys
|
||||
|
|
|
@ -605,6 +605,18 @@ module ActiveRecord
|
|||
def drop_enum(*) # :nodoc:
|
||||
end
|
||||
|
||||
# This is meant to be implemented by the adapters that support custom enum types
|
||||
def rename_enum(*) # :nodoc:
|
||||
end
|
||||
|
||||
# This is meant to be implemented by the adapters that support custom enum types
|
||||
def add_enum_value(*) # :nodoc:
|
||||
end
|
||||
|
||||
# This is meant to be implemented by the adapters that support custom enum types
|
||||
def rename_enum_value(*) # :nodoc:
|
||||
end
|
||||
|
||||
def advisory_locks_enabled? # :nodoc:
|
||||
supports_advisory_locks? && @advisory_locks_enabled
|
||||
end
|
||||
|
|
|
@ -550,6 +550,43 @@ module ActiveRecord
|
|||
internal_exec_query(query)
|
||||
end
|
||||
|
||||
# Rename an existing enum type to something else.
|
||||
def rename_enum(name, options = {})
|
||||
to = options.fetch(:to) { raise ArgumentError, ":to is required" }
|
||||
|
||||
exec_query("ALTER TYPE #{quote_table_name(name)} RENAME TO #{to}").tap { reload_type_map }
|
||||
end
|
||||
|
||||
# Add enum value to an existing enum type.
|
||||
def add_enum_value(type_name, value, options = {})
|
||||
before, after = options.values_at(:before, :after)
|
||||
sql = +"ALTER TYPE #{quote_table_name(type_name)} ADD VALUE '#{value}'"
|
||||
|
||||
if before && after
|
||||
raise ArgumentError, "Cannot have both :before and :after at the same time"
|
||||
elsif before
|
||||
sql << " BEFORE '#{before}'"
|
||||
elsif after
|
||||
sql << " AFTER '#{after}'"
|
||||
end
|
||||
|
||||
execute(sql).tap { reload_type_map }
|
||||
end
|
||||
|
||||
# Rename enum value on an existing enum type.
|
||||
def rename_enum_value(type_name, options = {})
|
||||
unless database_version >= 10_00_00 # >= 10.0
|
||||
raise ArgumentError, "Renaming enum values is only supported in PostgreSQL 10 or later"
|
||||
end
|
||||
|
||||
from = options.fetch(:from) { raise ArgumentError, ":from is required" }
|
||||
to = options.fetch(:to) { raise ArgumentError, ":to is required" }
|
||||
|
||||
execute("ALTER TYPE #{quote_table_name(type_name)} RENAME VALUE '#{from}' TO '#{to}'").tap {
|
||||
reload_type_map
|
||||
}
|
||||
end
|
||||
|
||||
# Returns the configured supported identifier length supported by PostgreSQL
|
||||
def max_identifier_length
|
||||
@max_identifier_length ||= query_value("SHOW max_identifier_length", "SCHEMA").to_i
|
||||
|
|
|
@ -38,6 +38,8 @@ module ActiveRecord
|
|||
# * remove_reference
|
||||
# * remove_timestamps
|
||||
# * rename_column
|
||||
# * rename_enum (must supply a +:to+ option)
|
||||
# * rename_enum_value (must supply a +:from+ and +:to+ option)
|
||||
# * rename_index
|
||||
# * rename_table
|
||||
class CommandRecorder
|
||||
|
@ -52,7 +54,7 @@ module ActiveRecord
|
|||
:add_check_constraint, :remove_check_constraint,
|
||||
:add_exclusion_constraint, :remove_exclusion_constraint,
|
||||
:add_unique_key, :remove_unique_key,
|
||||
:create_enum, :drop_enum,
|
||||
:create_enum, :drop_enum, :rename_enum, :add_enum_value, :rename_enum_value,
|
||||
]
|
||||
include JoinTable
|
||||
|
||||
|
@ -340,6 +342,26 @@ module ActiveRecord
|
|||
super
|
||||
end
|
||||
|
||||
def invert_rename_enum(args)
|
||||
name, options = args
|
||||
|
||||
unless options.is_a?(Hash) && options.has_key?(:to)
|
||||
raise ActiveRecord::IrreversibleMigration, "rename_enum is only reversible if given a :to option."
|
||||
end
|
||||
|
||||
[:rename_enum, [options[:to], to: name]]
|
||||
end
|
||||
|
||||
def invert_rename_enum_value(args)
|
||||
type_name, options = args
|
||||
|
||||
unless options.is_a?(Hash) && options.has_key?(:from) && options.has_key?(:to)
|
||||
raise ActiveRecord::IrreversibleMigration, "rename_enum_value is only reversible if given a :from and :to option."
|
||||
end
|
||||
|
||||
[:rename_enum_value, [type_name, from: options[:to], to: options[:from]]]
|
||||
end
|
||||
|
||||
def respond_to_missing?(method, _)
|
||||
super || delegate.respond_to?(method)
|
||||
end
|
||||
|
|
|
@ -111,6 +111,34 @@ class PostgresqlEnumTest < ActiveRecord::PostgreSQLTestCase
|
|||
assert_includes output, 't.enum "good_mood", default: "happy", null: false, enum_type: "mood"'
|
||||
end
|
||||
|
||||
def test_schema_dump_renamed_enum
|
||||
@connection.rename_enum :mood, to: :feeling
|
||||
|
||||
output = dump_table_schema("postgresql_enums")
|
||||
|
||||
assert_includes output, 'create_enum "feeling", ["sad", "ok", "happy"]'
|
||||
|
||||
assert_includes output, 't.enum "current_mood", enum_type: "feeling"'
|
||||
end
|
||||
|
||||
def test_schema_dump_added_enum_value
|
||||
@connection.add_enum_value :mood, :angry, before: :ok
|
||||
@connection.add_enum_value :mood, :nervous, after: :ok
|
||||
@connection.add_enum_value :mood, :glad
|
||||
|
||||
output = dump_table_schema("postgresql_enums")
|
||||
|
||||
assert_includes output, 'create_enum "mood", ["sad", "angry", "ok", "nervous", "happy", "glad"]'
|
||||
end
|
||||
|
||||
def test_schema_dump_renamed_enum_value
|
||||
@connection.rename_enum_value :mood, from: :ok, to: :okay
|
||||
|
||||
output = dump_table_schema("postgresql_enums")
|
||||
|
||||
assert_includes output, 'create_enum "mood", ["sad", "okay", "happy"]'
|
||||
end
|
||||
|
||||
def test_schema_load
|
||||
original, $stdout = $stdout, StringIO.new
|
||||
|
||||
|
|
|
@ -512,6 +512,40 @@ module ActiveRecord
|
|||
@recorder.inverse_of :drop_enum, [:color, if_exists: true]
|
||||
end
|
||||
end
|
||||
|
||||
def test_invert_rename_enum
|
||||
enum = @recorder.inverse_of :rename_enum, [:dog_breed, to: :breed]
|
||||
assert_equal [:rename_enum, [:breed, to: :dog_breed]], enum
|
||||
end
|
||||
|
||||
def test_invert_rename_enum_without_to
|
||||
assert_raises(ActiveRecord::IrreversibleMigration) do
|
||||
@recorder.inverse_of :rename_enum, [:breed]
|
||||
end
|
||||
end
|
||||
|
||||
def test_invert_add_enum_value
|
||||
assert_raises(ActiveRecord::IrreversibleMigration) do
|
||||
@recorder.inverse_of :add_enum_value, [:dog_breed, :beagle]
|
||||
end
|
||||
end
|
||||
|
||||
def test_invert_rename_enum_value
|
||||
enum_value = @recorder.inverse_of :rename_enum_value, [:dog_breed, from: :retriever, to: :beagle]
|
||||
assert_equal [:rename_enum_value, [:dog_breed, from: :beagle, to: :retriever]], enum_value
|
||||
end
|
||||
|
||||
def test_invert_rename_enum_value_without_from
|
||||
assert_raises(ActiveRecord::IrreversibleMigration) do
|
||||
@recorder.inverse_of :rename_enum_value, [:dog_breed, to: :retriever]
|
||||
end
|
||||
end
|
||||
|
||||
def test_invert_rename_enum_value_without_to
|
||||
assert_raises(ActiveRecord::IrreversibleMigration) do
|
||||
@recorder.inverse_of :rename_enum_value, [:dog_breed, from: :beagle]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -314,25 +314,39 @@ irb> article.status = "deleted"
|
|||
ArgumentError: 'deleted' is not a valid status
|
||||
```
|
||||
|
||||
To add a new value (before or after an existing one) or to rename a value you should use [ALTER TYPE](https://www.postgresql.org/docs/current/static/sql-altertype.html):
|
||||
To rename the enum you can use `rename_enum` along with updating any model
|
||||
usage:
|
||||
|
||||
```ruby
|
||||
# db/migrate/20150720144913_add_new_state_to_articles.rb
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
execute <<-SQL
|
||||
ALTER TYPE article_status ADD VALUE IF NOT EXISTS 'deleted' AFTER 'archived';
|
||||
ALTER TYPE article_status RENAME VALUE 'archived' TO 'hidden';
|
||||
SQL
|
||||
# db/migrate/20150718144917_rename_article_status.rb
|
||||
def change
|
||||
rename_enum :article_status, to: :article_state
|
||||
end
|
||||
```
|
||||
|
||||
NOTE: `ALTER TYPE ... ADD VALUE` cannot be executed inside of a transaction block so here we are using `disable_ddl_transaction!`
|
||||
To add a new value you can use `add_enum_value`:
|
||||
|
||||
WARNING. Enum values [can't be dropped or reordered](https://www.postgresql.org/docs/current/datatype-enum.html). Adding a value is not easily reversed.
|
||||
```ruby
|
||||
# db/migrate/20150720144913_add_new_state_to_articles.rb
|
||||
def up
|
||||
add_enum_value :article_state, "archived", # will be at the end after published
|
||||
add_enum_value :article_state, "in review", before: "published"
|
||||
add_enum_value :article_state, "approved", after: "in review"
|
||||
end
|
||||
```
|
||||
|
||||
Hint: to show all the values of the all enums you have, you should call this query in `bin/rails db` or `psql` console:
|
||||
NOTE: Enum values can't be dropped or renamed which also means add_enum_value is irreversible. You can read why [here](https://www.postgresql.org/message-id/29F36C7C98AB09499B1A209D48EAA615B7653DBC8A@mail2a.alliedtesting.com).
|
||||
|
||||
To rename a value you can use `rename_enum_value`:
|
||||
|
||||
```ruby
|
||||
# db/migrate/20150722144915_rename_article_state.rb
|
||||
def change
|
||||
rename_enum_value :article_state, from: "archived", to: "deleted"
|
||||
end
|
||||
```
|
||||
|
||||
Hint: to show all the values of the all enums you have, you can call this query in `bin/rails db` or `psql` console:
|
||||
|
||||
```sql
|
||||
SELECT n.nspname AS enum_schema,
|
||||
|
|
Loading…
Reference in New Issue