Rename back unique keys to unique constraints

As we (I and @yahonda) talked about the naming in person, naming unique
constraints as unique keys is very confusing to me.
All documents and descriptions says it's unique constraints, but naming
unique keys leads to misunderstanding it's a short-hand of unique
indexes.
Just naming it unique constraints is not misleading.
This commit is contained in:
Ryuta Kamizono 2023-09-26 16:36:45 +09:00
parent c32813ddc6
commit b2790b6680
17 changed files with 332 additions and 332 deletions

@ -503,7 +503,7 @@
Because `deferrable: true` and `deferrable: :deferred` are hard to understand. Because `deferrable: true` and `deferrable: :deferred` are hard to understand.
Both true and :deferred are truthy values. Both true and :deferred are truthy values.
This behavior is the same as the deferrable option of the add_unique_key method, added in #46192. This behavior is the same as the deferrable option of the add_unique_constraint method, added in #46192.
*Hiroyuki Ishii* *Hiroyuki Ishii*
@ -720,8 +720,8 @@
* Add support for unique constraints (PostgreSQL-only). * Add support for unique constraints (PostgreSQL-only).
```ruby ```ruby
add_unique_key :sections, [:position], deferrable: :deferred, name: "unique_section_position" add_unique_constraint :sections, [:position], deferrable: :deferred, name: "unique_section_position"
remove_unique_key :sections, name: "unique_section_position" remove_unique_constraint :sections, name: "unique_section_position"
``` ```
See PostgreSQL's [Unique Constraints](https://www.postgresql.org/docs/current/ddl-constraints.html#DDL-CONSTRAINTS-UNIQUE-CONSTRAINTS) documentation for more on unique constraints. See PostgreSQL's [Unique Constraints](https://www.postgresql.org/docs/current/ddl-constraints.html#DDL-CONSTRAINTS-UNIQUE-CONSTRAINTS) documentation for more on unique constraints.
@ -746,11 +746,11 @@
Using the default behavior, the transaction would fail when executing the Using the default behavior, the transaction would fail when executing the
first `UPDATE` statement. first `UPDATE` statement.
By passing the `:deferrable` option to the `add_unique_key` statement in By passing the `:deferrable` option to the `add_unique_constraint` statement in
migrations, it's possible to defer this check. migrations, it's possible to defer this check.
```ruby ```ruby
add_unique_key :items, [:position], deferrable: :immediate add_unique_constraint :items, [:position], deferrable: :immediate
``` ```
Passing `deferrable: :immediate` does not change the behaviour of the previous example, Passing `deferrable: :immediate` does not change the behaviour of the previous example,
@ -761,14 +761,14 @@
check (after the statement), to a deferred check (after the transaction): check (after the statement), to a deferred check (after the transaction):
```ruby ```ruby
add_unique_key :items, [:position], deferrable: :deferred add_unique_constraint :items, [:position], deferrable: :deferred
``` ```
If you want to change an existing unique index to deferrable, you can use :using_index If you want to change an existing unique index to deferrable, you can use :using_index
to create deferrable unique constraints. to create deferrable unique constraints.
```ruby ```ruby
add_unique_key :items, deferrable: :deferred, using_index: "index_items_on_position" add_unique_constraint :items, deferrable: :deferred, using_index: "index_items_on_position"
``` ```
*Hiroyuki Ishii* *Hiroyuki Ishii*

@ -16,7 +16,7 @@ def accept(o)
delegate :quote_column_name, :quote_table_name, :quote_default_expression, :type_to_sql, delegate :quote_column_name, :quote_table_name, :quote_default_expression, :type_to_sql,
:options_include_default?, :supports_indexes_in_create?, :use_foreign_keys?, :options_include_default?, :supports_indexes_in_create?, :use_foreign_keys?,
:quoted_columns_for_index, :supports_partial_index?, :supports_check_constraints?, :quoted_columns_for_index, :supports_partial_index?, :supports_check_constraints?,
:supports_index_include?, :supports_exclusion_constraints?, :supports_unique_keys?, :supports_index_include?, :supports_exclusion_constraints?, :supports_unique_constraints?,
:supports_nulls_not_distinct?, :supports_nulls_not_distinct?,
to: :@conn, private: true to: :@conn, private: true
@ -65,8 +65,8 @@ def visit_TableDefinition(o)
statements.concat(o.exclusion_constraints.map { |exc| accept exc }) statements.concat(o.exclusion_constraints.map { |exc| accept exc })
end end
if supports_unique_keys? if supports_unique_constraints?
statements.concat(o.unique_keys.map { |exc| accept exc }) statements.concat(o.unique_constraints.map { |exc| accept exc })
end end
create_sql << "(#{statements.join(', ')})" if statements.present? create_sql << "(#{statements.join(', ')})" if statements.present?

@ -498,7 +498,7 @@ def supports_exclusion_constraints?
end end
# Does this adapter support creating unique constraints? # Does this adapter support creating unique constraints?
def supports_unique_keys? def supports_unique_constraints?
false false
end end

@ -12,8 +12,8 @@ def visit_AlterTable(o)
sql << o.constraint_validations.map { |fk| visit_ValidateConstraint fk }.join(" ") sql << o.constraint_validations.map { |fk| visit_ValidateConstraint fk }.join(" ")
sql << o.exclusion_constraint_adds.map { |con| visit_AddExclusionConstraint con }.join(" ") sql << o.exclusion_constraint_adds.map { |con| visit_AddExclusionConstraint con }.join(" ")
sql << o.exclusion_constraint_drops.map { |con| visit_DropExclusionConstraint con }.join(" ") sql << o.exclusion_constraint_drops.map { |con| visit_DropExclusionConstraint con }.join(" ")
sql << o.unique_key_adds.map { |con| visit_AddUniqueKey con }.join(" ") sql << o.unique_constraint_adds.map { |con| visit_AddUniqueConstraint con }.join(" ")
sql << o.unique_key_drops.map { |con| visit_DropUniqueKey con }.join(" ") sql << o.unique_constraint_drops.map { |con| visit_DropUniqueConstraint con }.join(" ")
end end
def visit_AddForeignKey(o) def visit_AddForeignKey(o)
@ -49,7 +49,7 @@ def visit_ExclusionConstraintDefinition(o)
sql.join(" ") sql.join(" ")
end end
def visit_UniqueKeyDefinition(o) def visit_UniqueConstraintDefinition(o)
column_name = Array(o.column).map { |column| quote_column_name(column) }.join(", ") column_name = Array(o.column).map { |column| quote_column_name(column) }.join(", ")
sql = ["CONSTRAINT"] sql = ["CONSTRAINT"]
@ -77,11 +77,11 @@ def visit_DropExclusionConstraint(name)
"DROP CONSTRAINT #{quote_column_name(name)}" "DROP CONSTRAINT #{quote_column_name(name)}"
end end
def visit_AddUniqueKey(o) def visit_AddUniqueConstraint(o)
"ADD #{accept(o)}" "ADD #{accept(o)}"
end end
def visit_DropUniqueKey(name) def visit_DropUniqueConstraint(name)
"DROP CONSTRAINT #{quote_column_name(name)}" "DROP CONSTRAINT #{quote_column_name(name)}"
end end

@ -211,7 +211,7 @@ def export_name_on_schema_dump?
end end
end end
UniqueKeyDefinition = Struct.new(:table_name, :column, :options) do UniqueConstraintDefinition = Struct.new(:table_name, :column, :options) do
def name def name
options[:name] options[:name]
end end
@ -239,12 +239,12 @@ def defined_for?(name: nil, column: nil, **options)
class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition
include ColumnMethods include ColumnMethods
attr_reader :exclusion_constraints, :unique_keys, :unlogged attr_reader :exclusion_constraints, :unique_constraints, :unlogged
def initialize(*, **) def initialize(*, **)
super super
@exclusion_constraints = [] @exclusion_constraints = []
@unique_keys = [] @unique_constraints = []
@unlogged = ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.create_unlogged_tables @unlogged = ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.create_unlogged_tables
end end
@ -252,8 +252,8 @@ def exclusion_constraint(expression, **options)
exclusion_constraints << new_exclusion_constraint_definition(expression, options) exclusion_constraints << new_exclusion_constraint_definition(expression, options)
end end
def unique_key(column_name, **options) def unique_constraint(column_name, **options)
unique_keys << new_unique_key_definition(column_name, options) unique_constraints << new_unique_constraint_definition(column_name, options)
end end
def new_exclusion_constraint_definition(expression, options) # :nodoc: def new_exclusion_constraint_definition(expression, options) # :nodoc:
@ -261,9 +261,9 @@ def new_exclusion_constraint_definition(expression, options) # :nodoc:
ExclusionConstraintDefinition.new(name, expression, options) ExclusionConstraintDefinition.new(name, expression, options)
end end
def new_unique_key_definition(column_name, options) # :nodoc: def new_unique_constraint_definition(column_name, options) # :nodoc:
options = @conn.unique_key_options(name, column_name, options) options = @conn.unique_constraint_options(name, column_name, options)
UniqueKeyDefinition.new(name, column_name, options) UniqueConstraintDefinition.new(name, column_name, options)
end end
def new_column_definition(name, type, **options) # :nodoc: def new_column_definition(name, type, **options) # :nodoc:
@ -317,34 +317,34 @@ def remove_exclusion_constraint(*args)
# Adds an unique constraint. # Adds an unique constraint.
# #
# t.unique_key(:position, name: 'unique_position', deferrable: :deferred) # t.unique_constraint(:position, name: 'unique_position', deferrable: :deferred)
# #
# See {connection.add_unique_key}[rdoc-ref:SchemaStatements#add_unique_key] # See {connection.add_unique_constraint}[rdoc-ref:SchemaStatements#add_unique_constraint]
def unique_key(*args) def unique_constraint(*args)
@base.add_unique_key(name, *args) @base.add_unique_constraint(name, *args)
end end
# Removes the given unique constraint from the table. # Removes the given unique constraint from the table.
# #
# t.remove_unique_key(name: "unique_position") # t.remove_unique_constraint(name: "unique_position")
# #
# See {connection.remove_unique_key}[rdoc-ref:SchemaStatements#remove_unique_key] # See {connection.remove_unique_constraint}[rdoc-ref:SchemaStatements#remove_unique_constraint]
def remove_unique_key(*args) def remove_unique_constraint(*args)
@base.remove_unique_key(name, *args) @base.remove_unique_constraint(name, *args)
end end
end end
# = Active Record PostgreSQL Adapter Alter \Table # = Active Record PostgreSQL Adapter Alter \Table
class AlterTable < ActiveRecord::ConnectionAdapters::AlterTable class AlterTable < ActiveRecord::ConnectionAdapters::AlterTable
attr_reader :constraint_validations, :exclusion_constraint_adds, :exclusion_constraint_drops, :unique_key_adds, :unique_key_drops attr_reader :constraint_validations, :exclusion_constraint_adds, :exclusion_constraint_drops, :unique_constraint_adds, :unique_constraint_drops
def initialize(td) def initialize(td)
super super
@constraint_validations = [] @constraint_validations = []
@exclusion_constraint_adds = [] @exclusion_constraint_adds = []
@exclusion_constraint_drops = [] @exclusion_constraint_drops = []
@unique_key_adds = [] @unique_constraint_adds = []
@unique_key_drops = [] @unique_constraint_drops = []
end end
def validate_constraint(name) def validate_constraint(name)
@ -359,12 +359,12 @@ def drop_exclusion_constraint(constraint_name)
@exclusion_constraint_drops << constraint_name @exclusion_constraint_drops << constraint_name
end end
def add_unique_key(column_name, options) def add_unique_constraint(column_name, options)
@unique_key_adds << @td.new_unique_key_definition(column_name, options) @unique_constraint_adds << @td.new_unique_constraint_definition(column_name, options)
end end
def drop_unique_key(unique_key_name) def drop_unique_constraint(unique_constraint_name)
@unique_key_drops << unique_key_name @unique_constraint_drops << unique_constraint_name
end end
end end
end end

@ -61,23 +61,23 @@ def exclusion_constraints_in_create(table, stream)
end end
end end
def unique_keys_in_create(table, stream) def unique_constraints_in_create(table, stream)
if (unique_keys = @connection.unique_keys(table)).any? if (unique_constraints = @connection.unique_constraints(table)).any?
add_unique_key_statements = unique_keys.map do |unique_key| add_unique_constraint_statements = unique_constraints.map do |unique_constraint|
parts = [ parts = [
"t.unique_key #{unique_key.column.inspect}" "t.unique_constraint #{unique_constraint.column.inspect}"
] ]
parts << "deferrable: #{unique_key.deferrable.inspect}" if unique_key.deferrable parts << "deferrable: #{unique_constraint.deferrable.inspect}" if unique_constraint.deferrable
if unique_key.export_name_on_schema_dump? if unique_constraint.export_name_on_schema_dump?
parts << "name: #{unique_key.name.inspect}" parts << "name: #{unique_constraint.name.inspect}"
end end
" #{parts.join(', ')}" " #{parts.join(', ')}"
end end
stream.puts add_unique_key_statements.sort.join("\n") stream.puts add_unique_constraint_statements.sort.join("\n")
end end
end end

@ -640,8 +640,8 @@ def exclusion_constraints(table_name)
end end
# Returns an array of unique constraints for the given table. # Returns an array of unique constraints for the given table.
# The unique constraints are represented as UniqueKeyDefinition objects. # The unique constraints are represented as UniqueConstraintDefinition objects.
def unique_keys(table_name) def unique_constraints(table_name)
scope = quoted_scope(table_name) scope = quoted_scope(table_name)
unique_info = internal_exec_query(<<~SQL, "SCHEMA", allow_retry: true, materialize_transactions: false) unique_info = internal_exec_query(<<~SQL, "SCHEMA", allow_retry: true, materialize_transactions: false)
@ -665,7 +665,7 @@ def unique_keys(table_name)
deferrable: deferrable deferrable: deferrable
} }
UniqueKeyDefinition.new(table_name, columns, options) UniqueConstraintDefinition.new(table_name, columns, options)
end end
end end
@ -717,7 +717,7 @@ def remove_exclusion_constraint(table_name, expression = nil, **options)
# Adds a new unique constraint to the table. # Adds a new unique constraint to the table.
# #
# add_unique_key :sections, [:position], deferrable: :deferred, name: "unique_position" # add_unique_constraint :sections, [:position], deferrable: :deferred, name: "unique_position"
# #
# generates: # generates:
# #
@ -725,7 +725,7 @@ def remove_exclusion_constraint(table_name, expression = nil, **options)
# #
# If you want to change an existing unique index to deferrable, you can use :using_index to create deferrable unique constraints. # If you want to change an existing unique index to deferrable, you can use :using_index to create deferrable unique constraints.
# #
# add_unique_key :sections, deferrable: :deferred, name: "unique_position", using_index: "index_sections_on_position" # add_unique_constraint :sections, deferrable: :deferred, name: "unique_position", using_index: "index_sections_on_position"
# #
# The +options+ hash can include the following keys: # The +options+ hash can include the following keys:
# [<tt>:name</tt>] # [<tt>:name</tt>]
@ -734,15 +734,15 @@ def remove_exclusion_constraint(table_name, expression = nil, **options)
# Specify whether or not the unique constraint should be deferrable. Valid values are +false+ or +:immediate+ or +:deferred+ to specify the default behavior. Defaults to +false+. # Specify whether or not the unique constraint should be deferrable. Valid values are +false+ or +:immediate+ or +:deferred+ to specify the default behavior. Defaults to +false+.
# [<tt>:using_index</tt>] # [<tt>:using_index</tt>]
# To specify an existing unique index name. Defaults to +nil+. # To specify an existing unique index name. Defaults to +nil+.
def add_unique_key(table_name, column_name = nil, **options) def add_unique_constraint(table_name, column_name = nil, **options)
options = unique_key_options(table_name, column_name, options) options = unique_constraint_options(table_name, column_name, options)
at = create_alter_table(table_name) at = create_alter_table(table_name)
at.add_unique_key(column_name, options) at.add_unique_constraint(column_name, options)
execute schema_creation.accept(at) execute schema_creation.accept(at)
end end
def unique_key_options(table_name, column_name, options) # :nodoc: def unique_constraint_options(table_name, column_name, options) # :nodoc:
assert_valid_deferrable(options[:deferrable]) assert_valid_deferrable(options[:deferrable])
if column_name && options[:using_index] if column_name && options[:using_index]
@ -750,22 +750,22 @@ def unique_key_options(table_name, column_name, options) # :nodoc:
end end
options = options.dup options = options.dup
options[:name] ||= unique_key_name(table_name, column: column_name, **options) options[:name] ||= unique_constraint_name(table_name, column: column_name, **options)
options options
end end
# Removes the given unique constraint from the table. # Removes the given unique constraint from the table.
# #
# remove_unique_key :sections, name: "unique_position" # remove_unique_constraint :sections, name: "unique_position"
# #
# The +column_name+ parameter will be ignored if present. It can be helpful # The +column_name+ parameter will be ignored if present. It can be helpful
# to provide this in a migration's +change+ method so it can be reverted. # to provide this in a migration's +change+ method so it can be reverted.
# In that case, +column_name+ will be used by #add_unique_key. # In that case, +column_name+ will be used by #add_unique_constraint.
def remove_unique_key(table_name, column_name = nil, **options) def remove_unique_constraint(table_name, column_name = nil, **options)
unique_name_to_delete = unique_key_for!(table_name, column: column_name, **options).name unique_name_to_delete = unique_constraint_for!(table_name, column: column_name, **options).name
at = create_alter_table(table_name) at = create_alter_table(table_name)
at.drop_unique_key(unique_name_to_delete) at.drop_unique_constraint(unique_name_to_delete)
execute schema_creation.accept(at) execute schema_creation.accept(at)
end end
@ -1038,7 +1038,7 @@ def exclusion_constraint_for!(table_name, expression: nil, **options)
raise(ArgumentError, "Table '#{table_name}' has no exclusion constraint for #{expression || options}") raise(ArgumentError, "Table '#{table_name}' has no exclusion constraint for #{expression || options}")
end end
def unique_key_name(table_name, **options) def unique_constraint_name(table_name, **options)
options.fetch(:name) do options.fetch(:name) do
column_or_index = Array(options[:column] || options[:using_index]).map(&:to_s) column_or_index = Array(options[:column] || options[:using_index]).map(&:to_s)
identifier = "#{table_name}_#{column_or_index * '_and_'}_unique" identifier = "#{table_name}_#{column_or_index * '_and_'}_unique"
@ -1048,13 +1048,13 @@ def unique_key_name(table_name, **options)
end end
end end
def unique_key_for(table_name, **options) def unique_constraint_for(table_name, **options)
name = unique_key_name(table_name, **options) unless options.key?(:column) name = unique_constraint_name(table_name, **options) unless options.key?(:column)
unique_keys(table_name).detect { |unique_key| unique_key.defined_for?(name: name, **options) } unique_constraints(table_name).detect { |unique_constraint| unique_constraint.defined_for?(name: name, **options) }
end end
def unique_key_for!(table_name, column: nil, **options) def unique_constraint_for!(table_name, column: nil, **options)
unique_key_for(table_name, column: column, **options) || unique_constraint_for(table_name, column: column, **options) ||
raise(ArgumentError, "Table '#{table_name}' has no unique constraint for #{column || options}") raise(ArgumentError, "Table '#{table_name}' has no unique constraint for #{column || options}")
end end

@ -226,7 +226,7 @@ def supports_exclusion_constraints?
true true
end end
def supports_unique_keys? def supports_unique_constraints?
true true
end end

@ -12,7 +12,7 @@ class Migration
# * add_foreign_key # * add_foreign_key
# * add_check_constraint # * add_check_constraint
# * add_exclusion_constraint # * add_exclusion_constraint
# * add_unique_key # * add_unique_constraint
# * add_index # * add_index
# * add_reference # * add_reference
# * add_timestamps # * add_timestamps
@ -33,7 +33,7 @@ class Migration
# * remove_foreign_key (must supply a second table) # * remove_foreign_key (must supply a second table)
# * remove_check_constraint # * remove_check_constraint
# * remove_exclusion_constraint # * remove_exclusion_constraint
# * remove_unique_key # * remove_unique_constraint
# * remove_index # * remove_index
# * remove_reference # * remove_reference
# * remove_timestamps # * remove_timestamps
@ -53,7 +53,7 @@ class CommandRecorder
:change_column_comment, :change_table_comment, :change_column_comment, :change_table_comment,
:add_check_constraint, :remove_check_constraint, :add_check_constraint, :remove_check_constraint,
:add_exclusion_constraint, :remove_exclusion_constraint, :add_exclusion_constraint, :remove_exclusion_constraint,
:add_unique_key, :remove_unique_key, :add_unique_constraint, :remove_unique_constraint,
:create_enum, :drop_enum, :rename_enum, :add_enum_value, :rename_enum_value, :create_enum, :drop_enum, :rename_enum, :add_enum_value, :rename_enum_value,
] ]
include JoinTable include JoinTable
@ -161,7 +161,7 @@ module StraightReversions # :nodoc:
add_foreign_key: :remove_foreign_key, add_foreign_key: :remove_foreign_key,
add_check_constraint: :remove_check_constraint, add_check_constraint: :remove_check_constraint,
add_exclusion_constraint: :remove_exclusion_constraint, add_exclusion_constraint: :remove_exclusion_constraint,
add_unique_key: :remove_unique_key, add_unique_constraint: :remove_unique_constraint,
enable_extension: :disable_extension, enable_extension: :disable_extension,
create_enum: :drop_enum create_enum: :drop_enum
}.each do |cmd, inv| }.each do |cmd, inv|
@ -329,17 +329,17 @@ def invert_remove_exclusion_constraint(args)
super super
end end
def invert_add_unique_key(args) def invert_add_unique_constraint(args)
options = args.dup.extract_options! options = args.dup.extract_options!
raise ActiveRecord::IrreversibleMigration, "add_unique_key is not reversible if given an using_index." if options[:using_index] raise ActiveRecord::IrreversibleMigration, "add_unique_constraint is not reversible if given an using_index." if options[:using_index]
super super
end end
def invert_remove_unique_key(args) def invert_remove_unique_constraint(args)
_table, columns = args.dup.tap(&:extract_options!) _table, columns = args.dup.tap(&:extract_options!)
raise ActiveRecord::IrreversibleMigration, "remove_unique_key is only reversible if given an column_name." if columns.blank? raise ActiveRecord::IrreversibleMigration, "remove_unique_constraint is only reversible if given an column_name." if columns.blank?
super super
end end

@ -193,7 +193,7 @@ def table(table, stream)
indexes_in_create(table, tbl) indexes_in_create(table, tbl)
check_constraints_in_create(table, tbl) if @connection.supports_check_constraints? check_constraints_in_create(table, tbl) if @connection.supports_check_constraints?
exclusion_constraints_in_create(table, tbl) if @connection.supports_exclusion_constraints? exclusion_constraints_in_create(table, tbl) if @connection.supports_exclusion_constraints?
unique_keys_in_create(table, tbl) if @connection.supports_unique_keys? unique_constraints_in_create(table, tbl) if @connection.supports_unique_constraints?
tbl.puts " end" tbl.puts " end"
tbl.puts tbl.puts
@ -229,10 +229,10 @@ def indexes_in_create(table, stream)
indexes = indexes.reject { |index| exclusion_constraint_names.include?(index.name) } indexes = indexes.reject { |index| exclusion_constraint_names.include?(index.name) }
end end
if @connection.supports_unique_keys? && (unique_keys = @connection.unique_keys(table)).any? if @connection.supports_unique_constraints? && (unique_constraints = @connection.unique_constraints(table)).any?
unique_key_names = unique_keys.collect(&:name) unique_constraint_names = unique_constraints.collect(&:name)
indexes = indexes.reject { |index| unique_key_names.include?(index.name) } indexes = indexes.reject { |index| unique_constraint_names.include?(index.name) }
end end
index_statements = indexes.map do |index| index_statements = indexes.map do |index|

@ -177,17 +177,17 @@ def test_remove_exclusion_constraint_removes_exclusion_constraint
end end
end end
def test_unique_key_creates_unique_key def test_unique_constraint_creates_unique_constraint
with_change_table do |t| with_change_table do |t|
expect :add_unique_key, nil, [:delete_me, :foo, deferrable: :deferred, name: "unique_key"] expect :add_unique_constraint, nil, [:delete_me, :foo, deferrable: :deferred, name: "unique_constraint"]
t.unique_key :foo, deferrable: :deferred, name: "unique_key" t.unique_constraint :foo, deferrable: :deferred, name: "unique_constraint"
end end
end end
def test_remove_unique_key_removes_unique_key def test_remove_unique_constraint_removes_unique_constraint
with_change_table do |t| with_change_table do |t|
expect :remove_unique_key, nil, [:delete_me, name: "unique_key"] expect :remove_unique_constraint, nil, [:delete_me, name: "unique_constraint"]
t.remove_unique_key name: "unique_key" t.remove_unique_constraint name: "unique_constraint"
end end
end end
end end

@ -486,25 +486,25 @@ def test_invert_remove_check_constraint_if_exists
assert_equal [:add_check_constraint, [:dogs, "speed > 0", name: "speed_check", if_not_exists: true], nil], enable assert_equal [:add_check_constraint, [:dogs, "speed > 0", name: "speed_check", if_not_exists: true], nil], enable
end end
def test_invert_add_unique_key_constraint_with_using_index def test_invert_add_unique_constraint_constraint_with_using_index
assert_raises(ActiveRecord::IrreversibleMigration) do assert_raises(ActiveRecord::IrreversibleMigration) do
@recorder.inverse_of :add_unique_key, [:dogs, using_index: "unique_index"] @recorder.inverse_of :add_unique_constraint, [:dogs, using_index: "unique_index"]
end end
end end
def test_invert_remove_unique_key_constraint def test_invert_remove_unique_constraint_constraint
enable = @recorder.inverse_of :remove_unique_key, [:dogs, ["speed"], deferrable: :deferred, name: "uniq_speed"] enable = @recorder.inverse_of :remove_unique_constraint, [:dogs, ["speed"], deferrable: :deferred, name: "uniq_speed"]
assert_equal [:add_unique_key, [:dogs, ["speed"], deferrable: :deferred, name: "uniq_speed"], nil], enable assert_equal [:add_unique_constraint, [:dogs, ["speed"], deferrable: :deferred, name: "uniq_speed"], nil], enable
end end
def test_invert_remove_unique_key_constraint_without_options def test_invert_remove_unique_constraint_constraint_without_options
enable = @recorder.inverse_of :remove_unique_key, [:dogs, ["speed"]] enable = @recorder.inverse_of :remove_unique_constraint, [:dogs, ["speed"]]
assert_equal [:add_unique_key, [:dogs, ["speed"]], nil], enable assert_equal [:add_unique_constraint, [:dogs, ["speed"]], nil], enable
end end
def test_invert_remove_unique_key_constraint_without_columns def test_invert_remove_unique_constraint_constraint_without_columns
assert_raises(ActiveRecord::IrreversibleMigration) do assert_raises(ActiveRecord::IrreversibleMigration) do
@recorder.inverse_of :remove_unique_key, [:dogs, name: "uniq_speed"] @recorder.inverse_of :remove_unique_constraint, [:dogs, name: "uniq_speed"]
end end
end end

@ -0,0 +1,220 @@
# frozen_string_literal: true
require "cases/helper"
require "support/schema_dumping_helper"
if ActiveRecord::Base.connection.supports_unique_constraints?
module ActiveRecord
class Migration
class UniqueConstraintTest < ActiveRecord::TestCase
include SchemaDumpingHelper
class Section < ActiveRecord::Base
end
setup do
@connection = ActiveRecord::Base.connection
@connection.create_table "sections", force: true do |t|
t.integer "position", null: false
end
end
teardown do
@connection.drop_table "sections", if_exists: true
end
def test_unique_constraints
unique_constraints = @connection.unique_constraints("test_unique_constraints")
expected_constraints = [
{
name: "test_unique_constraints_position_deferrable_false",
deferrable: false,
column: ["position_1"]
}, {
name: "test_unique_constraints_position_deferrable_immediate",
deferrable: :immediate,
column: ["position_2"]
}, {
name: "test_unique_constraints_position_deferrable_deferred",
deferrable: :deferred,
column: ["position_3"]
}
]
assert_equal expected_constraints.size, unique_constraints.size
expected_constraints.each do |expected_constraint|
constraint = unique_constraints.find { |constraint| constraint.name == expected_constraint[:name] }
assert_equal "test_unique_constraints", constraint.table_name
assert_equal expected_constraint[:name], constraint.name
assert_equal expected_constraint[:column], constraint.column
assert_equal expected_constraint[:deferrable], constraint.deferrable
end
end
def test_unique_constraints_scoped_to_schemas
@connection.add_unique_constraint :sections, [:position]
assert_no_changes -> { @connection.unique_constraints("sections").size } do
@connection.create_schema "test_schema"
@connection.create_table "test_schema.sections" do |t|
t.integer :position
end
@connection.add_unique_constraint "test_schema.sections", [:position]
end
ensure
@connection.drop_schema "test_schema"
end
def test_add_unique_constraint_without_deferrable
@connection.add_unique_constraint :sections, [:position]
unique_constraints = @connection.unique_constraints("sections")
assert_equal 1, unique_constraints.size
constraint = unique_constraints.first
assert_equal "sections", constraint.table_name
assert_equal "uniq_rails_1e07660b77", constraint.name
assert_equal false, constraint.deferrable
end
def test_add_unique_constraint_with_deferrable_false
@connection.add_unique_constraint :sections, [:position], deferrable: false
unique_constraints = @connection.unique_constraints("sections")
assert_equal 1, unique_constraints.size
constraint = unique_constraints.first
assert_equal "sections", constraint.table_name
assert_equal "uniq_rails_1e07660b77", constraint.name
assert_equal false, constraint.deferrable
end
def test_add_unique_constraint_with_deferrable_immediate
@connection.add_unique_constraint :sections, [:position], deferrable: :immediate
unique_constraints = @connection.unique_constraints("sections")
assert_equal 1, unique_constraints.size
constraint = unique_constraints.first
assert_equal "sections", constraint.table_name
assert_equal "uniq_rails_1e07660b77", constraint.name
assert_equal :immediate, constraint.deferrable
end
def test_add_unique_constraint_with_deferrable_deferred
@connection.add_unique_constraint :sections, [:position], deferrable: :deferred
unique_constraints = @connection.unique_constraints("sections")
assert_equal 1, unique_constraints.size
constraint = unique_constraints.first
assert_equal "sections", constraint.table_name
assert_equal "uniq_rails_1e07660b77", constraint.name
assert_equal :deferred, constraint.deferrable
end
def test_add_unique_constraint_with_deferrable_invalid
error = assert_raises(ArgumentError) do
@connection.add_unique_constraint :sections, [:position], deferrable: true
end
assert_equal "deferrable must be `:immediate` or `:deferred`, got: `true`", error.message
end
def test_added_deferrable_initially_immediate_unique_constraint
@connection.add_unique_constraint :sections, [:position], deferrable: :immediate, name: "unique_section_position"
section = Section.create!(position: 1)
assert_raises(ActiveRecord::StatementInvalid) do
Section.transaction(requires_new: true) do
Section.create!(position: 1)
section.update!(position: 2)
end
end
assert_nothing_raised do
Section.transaction(requires_new: true) do
Section.connection.exec_query("SET CONSTRAINTS unique_section_position DEFERRED")
Section.create!(position: 1)
section.update!(position: 2)
# NOTE: Clear `SET CONSTRAINTS` statement at the end of transaction.
raise ActiveRecord::Rollback
end
end
end
def test_add_unique_constraint_with_name_and_using_index
@connection.add_index :sections, [:position], name: "unique_index", unique: true
@connection.add_unique_constraint :sections, name: "unique_constraint", deferrable: :immediate, using_index: "unique_index"
unique_constraints = @connection.unique_constraints("sections")
assert_equal 1, unique_constraints.size
constraint = unique_constraints.first
assert_equal "sections", constraint.table_name
assert_equal "unique_constraint", constraint.name
assert_equal ["position"], constraint.column
assert_equal :immediate, constraint.deferrable
end
def test_add_unique_constraint_with_only_using_index
@connection.add_index :sections, [:position], name: "unique_index", unique: true
@connection.add_unique_constraint :sections, using_index: "unique_index"
unique_constraints = @connection.unique_constraints("sections")
assert_equal 1, unique_constraints.size
constraint = unique_constraints.first
assert_equal "sections", constraint.table_name
assert_equal "uniq_rails_79b901ffb4", constraint.name
assert_equal ["position"], constraint.column
assert_equal false, constraint.deferrable
end
def test_add_unique_constraint_with_columns_and_using_index
@connection.add_index :sections, [:position], name: "unique_index", unique: true
assert_raises(ArgumentError) do
@connection.add_unique_constraint :sections, [:position], using_index: "unique_index"
end
end
def test_remove_unique_constraint
@connection.add_unique_constraint :sections, [:position], name: :unique_section_position
assert_equal 1, @connection.unique_constraints("sections").size
@connection.remove_unique_constraint :sections, name: :unique_section_position
assert_empty @connection.unique_constraints("sections")
end
def test_remove_unique_constraint_by_column
@connection.add_unique_constraint :sections, [:position]
assert_equal 1, @connection.unique_constraints("sections").size
@connection.remove_unique_constraint :sections, [:position]
assert_empty @connection.unique_constraints("sections")
end
def test_remove_non_existing_unique_constraint
assert_raises(ArgumentError, match: /Table 'sections' has no unique constraint/) do
@connection.remove_unique_constraint :sections, name: "nonexistent"
end
end
def test_renamed_unique_constraint
@connection.add_unique_constraint :sections, [:position]
@connection.rename_column :sections, :position, :new_position
unique_constraints = @connection.unique_constraints("sections")
assert_equal 1, unique_constraints.size
constraint = unique_constraints.first
assert_equal "sections", constraint.table_name
assert_equal ["new_position"], constraint.column
end
end
end
end
end

@ -1,220 +0,0 @@
# frozen_string_literal: true
require "cases/helper"
require "support/schema_dumping_helper"
if ActiveRecord::Base.connection.supports_unique_keys?
module ActiveRecord
class Migration
class UniqueKeyTest < ActiveRecord::TestCase
include SchemaDumpingHelper
class Section < ActiveRecord::Base
end
setup do
@connection = ActiveRecord::Base.connection
@connection.create_table "sections", force: true do |t|
t.integer "position", null: false
end
end
teardown do
@connection.drop_table "sections", if_exists: true
end
def test_unique_keys
unique_keys = @connection.unique_keys("test_unique_keys")
expected_constraints = [
{
name: "test_unique_keys_position_deferrable_false",
deferrable: false,
column: ["position_1"]
}, {
name: "test_unique_keys_position_deferrable_immediate",
deferrable: :immediate,
column: ["position_2"]
}, {
name: "test_unique_keys_position_deferrable_deferred",
deferrable: :deferred,
column: ["position_3"]
}
]
assert_equal expected_constraints.size, unique_keys.size
expected_constraints.each do |expected_constraint|
constraint = unique_keys.find { |constraint| constraint.name == expected_constraint[:name] }
assert_equal "test_unique_keys", constraint.table_name
assert_equal expected_constraint[:name], constraint.name
assert_equal expected_constraint[:column], constraint.column
assert_equal expected_constraint[:deferrable], constraint.deferrable
end
end
def test_unique_keys_scoped_to_schemas
@connection.add_unique_key :sections, [:position]
assert_no_changes -> { @connection.unique_keys("sections").size } do
@connection.create_schema "test_schema"
@connection.create_table "test_schema.sections" do |t|
t.integer :position
end
@connection.add_unique_key "test_schema.sections", [:position]
end
ensure
@connection.drop_schema "test_schema"
end
def test_add_unique_key_without_deferrable
@connection.add_unique_key :sections, [:position]
unique_keys = @connection.unique_keys("sections")
assert_equal 1, unique_keys.size
constraint = unique_keys.first
assert_equal "sections", constraint.table_name
assert_equal "uniq_rails_1e07660b77", constraint.name
assert_equal false, constraint.deferrable
end
def test_add_unique_key_with_deferrable_false
@connection.add_unique_key :sections, [:position], deferrable: false
unique_keys = @connection.unique_keys("sections")
assert_equal 1, unique_keys.size
constraint = unique_keys.first
assert_equal "sections", constraint.table_name
assert_equal "uniq_rails_1e07660b77", constraint.name
assert_equal false, constraint.deferrable
end
def test_add_unique_key_with_deferrable_immediate
@connection.add_unique_key :sections, [:position], deferrable: :immediate
unique_keys = @connection.unique_keys("sections")
assert_equal 1, unique_keys.size
constraint = unique_keys.first
assert_equal "sections", constraint.table_name
assert_equal "uniq_rails_1e07660b77", constraint.name
assert_equal :immediate, constraint.deferrable
end
def test_add_unique_key_with_deferrable_deferred
@connection.add_unique_key :sections, [:position], deferrable: :deferred
unique_keys = @connection.unique_keys("sections")
assert_equal 1, unique_keys.size
constraint = unique_keys.first
assert_equal "sections", constraint.table_name
assert_equal "uniq_rails_1e07660b77", constraint.name
assert_equal :deferred, constraint.deferrable
end
def test_add_unique_key_with_deferrable_invalid
error = assert_raises(ArgumentError) do
@connection.add_unique_key :sections, [:position], deferrable: true
end
assert_equal "deferrable must be `:immediate` or `:deferred`, got: `true`", error.message
end
def test_added_deferrable_initially_immediate_unique_key
@connection.add_unique_key :sections, [:position], deferrable: :immediate, name: "unique_section_position"
section = Section.create!(position: 1)
assert_raises(ActiveRecord::StatementInvalid) do
Section.transaction(requires_new: true) do
Section.create!(position: 1)
section.update!(position: 2)
end
end
assert_nothing_raised do
Section.transaction(requires_new: true) do
Section.connection.exec_query("SET CONSTRAINTS unique_section_position DEFERRED")
Section.create!(position: 1)
section.update!(position: 2)
# NOTE: Clear `SET CONSTRAINTS` statement at the end of transaction.
raise ActiveRecord::Rollback
end
end
end
def test_add_unique_key_with_name_and_using_index
@connection.add_index :sections, [:position], name: "unique_index", unique: true
@connection.add_unique_key :sections, name: "unique_constraint", deferrable: :immediate, using_index: "unique_index"
unique_keys = @connection.unique_keys("sections")
assert_equal 1, unique_keys.size
constraint = unique_keys.first
assert_equal "sections", constraint.table_name
assert_equal "unique_constraint", constraint.name
assert_equal ["position"], constraint.column
assert_equal :immediate, constraint.deferrable
end
def test_add_unique_key_with_only_using_index
@connection.add_index :sections, [:position], name: "unique_index", unique: true
@connection.add_unique_key :sections, using_index: "unique_index"
unique_keys = @connection.unique_keys("sections")
assert_equal 1, unique_keys.size
constraint = unique_keys.first
assert_equal "sections", constraint.table_name
assert_equal "uniq_rails_79b901ffb4", constraint.name
assert_equal ["position"], constraint.column
assert_equal false, constraint.deferrable
end
def test_add_unique_key_with_columns_and_using_index
@connection.add_index :sections, [:position], name: "unique_index", unique: true
assert_raises(ArgumentError) do
@connection.add_unique_key :sections, [:position], using_index: "unique_index"
end
end
def test_remove_unique_key
@connection.add_unique_key :sections, [:position], name: :unique_section_position
assert_equal 1, @connection.unique_keys("sections").size
@connection.remove_unique_key :sections, name: :unique_section_position
assert_empty @connection.unique_keys("sections")
end
def test_remove_unique_key_by_column
@connection.add_unique_key :sections, [:position]
assert_equal 1, @connection.unique_keys("sections").size
@connection.remove_unique_key :sections, [:position]
assert_empty @connection.unique_keys("sections")
end
def test_remove_non_existing_unique_key
assert_raises(ArgumentError, match: /Table 'sections' has no unique constraint/) do
@connection.remove_unique_key :sections, name: "nonexistent"
end
end
def test_renamed_unique_key
@connection.add_unique_key :sections, [:position]
@connection.rename_column :sections, :position, :new_position
unique_keys = @connection.unique_keys("sections")
assert_equal 1, unique_keys.size
constraint = unique_keys.first
assert_equal "sections", constraint.table_name
assert_equal ["new_position"], constraint.column
end
end
end
end
end

@ -241,19 +241,19 @@ def test_schema_dumps_exclusion_constraints
end end
end end
if ActiveRecord::Base.connection.supports_unique_keys? if ActiveRecord::Base.connection.supports_unique_constraints?
def test_schema_dumps_unique_keys def test_schema_dumps_unique_constraints
output = dump_table_schema("test_unique_keys") output = dump_table_schema("test_unique_constraints")
constraint_definitions = output.split(/\n/).grep(/t\.unique_key/) constraint_definitions = output.split(/\n/).grep(/t\.unique_constraint/)
assert_equal 3, constraint_definitions.size assert_equal 3, constraint_definitions.size
assert_match 't.unique_key ["position_1"], name: "test_unique_keys_position_deferrable_false"', output assert_match 't.unique_constraint ["position_1"], name: "test_unique_constraints_position_deferrable_false"', output
assert_match 't.unique_key ["position_2"], deferrable: :immediate, name: "test_unique_keys_position_deferrable_immediate"', output assert_match 't.unique_constraint ["position_2"], deferrable: :immediate, name: "test_unique_constraints_position_deferrable_immediate"', output
assert_match 't.unique_key ["position_3"], deferrable: :deferred, name: "test_unique_keys_position_deferrable_deferred"', output assert_match 't.unique_constraint ["position_3"], deferrable: :deferred, name: "test_unique_constraints_position_deferrable_deferred"', output
end end
def test_schema_does_not_dumps_unique_key_indexes def test_schema_does_not_dump_unique_constraints_as_indexes
output = dump_table_schema("test_unique_keys") output = dump_table_schema("test_unique_constraints")
unique_index_definitions = output.split(/\n/).grep(/t\.index.*unique: true/) unique_index_definitions = output.split(/\n/).grep(/t\.index.*unique: true/)
assert_equal 0, unique_index_definitions.size assert_equal 0, unique_index_definitions.size

@ -153,14 +153,14 @@
t.exclusion_constraint "daterange(transaction_from, transaction_to) WITH &&", using: :gist, where: "transaction_from IS NOT NULL AND transaction_to IS NOT NULL", name: "test_exclusion_constraints_transaction_overlap", deferrable: :deferred t.exclusion_constraint "daterange(transaction_from, transaction_to) WITH &&", using: :gist, where: "transaction_from IS NOT NULL AND transaction_to IS NOT NULL", name: "test_exclusion_constraints_transaction_overlap", deferrable: :deferred
end end
create_table :test_unique_keys, force: true do |t| create_table :test_unique_constraints, force: true do |t|
t.integer :position_1 t.integer :position_1
t.integer :position_2 t.integer :position_2
t.integer :position_3 t.integer :position_3
t.unique_key :position_1, name: "test_unique_keys_position_deferrable_false" t.unique_constraint :position_1, name: "test_unique_constraints_position_deferrable_false"
t.unique_key :position_2, name: "test_unique_keys_position_deferrable_immediate", deferrable: :immediate t.unique_constraint :position_2, name: "test_unique_constraints_position_deferrable_immediate", deferrable: :immediate
t.unique_key :position_3, name: "test_unique_keys_position_deferrable_deferred", deferrable: :deferred t.unique_constraint :position_3, name: "test_unique_constraints_position_deferrable_deferred", deferrable: :deferred
end end
if supports_partitioned_indexes? if supports_partitioned_indexes?

@ -654,14 +654,14 @@ Unique Constraint
# db/migrate/20230422225213_create_items.rb # db/migrate/20230422225213_create_items.rb
create_table :items do |t| create_table :items do |t|
t.integer :position, null: false t.integer :position, null: false
t.unique_key [:position], deferrable: :immediate t.unique_constraint [:position], deferrable: :immediate
end end
``` ```
If you want to change an existing unique index to deferrable, you can use `:using_index` to create deferrable unique constraints. If you want to change an existing unique index to deferrable, you can use `:using_index` to create deferrable unique constraints.
```ruby ```ruby
add_unique_key :items, deferrable: :deferred, using_index: "index_items_on_position" add_unique_constraint :items, deferrable: :deferred, using_index: "index_items_on_position"
``` ```
Like foreign keys, unique constraints can be deferred by setting `:deferrable` to either `:immediate` or `:deferred`. By default, `:deferrable` is `false` and the constraint is always checked immediately. Like foreign keys, unique constraints can be deferred by setting `:deferrable` to either `:immediate` or `:deferred`. By default, `:deferrable` is `false` and the constraint is always checked immediately.