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.
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*
@ -720,8 +720,8 @@
* Add support for unique constraints (PostgreSQL-only).
```ruby
add_unique_key :sections, [:position], deferrable: :deferred, name: "unique_section_position"
remove_unique_key :sections, name: "unique_section_position"
add_unique_constraint :sections, [:position], deferrable: :deferred, 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.
@ -746,11 +746,11 @@
Using the default behavior, the transaction would fail when executing the
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.
```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,
@ -761,14 +761,14 @@
check (after the statement), to a deferred check (after the transaction):
```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
to create deferrable unique constraints.
```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*

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

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

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

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

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

@ -640,8 +640,8 @@ def exclusion_constraints(table_name)
end
# Returns an array of unique constraints for the given table.
# The unique constraints are represented as UniqueKeyDefinition objects.
def unique_keys(table_name)
# The unique constraints are represented as UniqueConstraintDefinition objects.
def unique_constraints(table_name)
scope = quoted_scope(table_name)
unique_info = internal_exec_query(<<~SQL, "SCHEMA", allow_retry: true, materialize_transactions: false)
@ -665,7 +665,7 @@ def unique_keys(table_name)
deferrable: deferrable
}
UniqueKeyDefinition.new(table_name, columns, options)
UniqueConstraintDefinition.new(table_name, columns, options)
end
end
@ -717,7 +717,7 @@ def remove_exclusion_constraint(table_name, expression = nil, **options)
# 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:
#
@ -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.
#
# 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:
# [<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+.
# [<tt>:using_index</tt>]
# To specify an existing unique index name. Defaults to +nil+.
def add_unique_key(table_name, column_name = nil, **options)
options = unique_key_options(table_name, column_name, options)
def add_unique_constraint(table_name, column_name = nil, **options)
options = unique_constraint_options(table_name, column_name, options)
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)
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])
if column_name && options[:using_index]
@ -750,22 +750,22 @@ def unique_key_options(table_name, column_name, options) # :nodoc:
end
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
end
# 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
# 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.
def remove_unique_key(table_name, column_name = nil, **options)
unique_name_to_delete = unique_key_for!(table_name, column: column_name, **options).name
# In that case, +column_name+ will be used by #add_unique_constraint.
def remove_unique_constraint(table_name, column_name = nil, **options)
unique_name_to_delete = unique_constraint_for!(table_name, column: column_name, **options).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)
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}")
end
def unique_key_name(table_name, **options)
def unique_constraint_name(table_name, **options)
options.fetch(:name) do
column_or_index = Array(options[:column] || options[:using_index]).map(&:to_s)
identifier = "#{table_name}_#{column_or_index * '_and_'}_unique"
@ -1048,13 +1048,13 @@ def unique_key_name(table_name, **options)
end
end
def unique_key_for(table_name, **options)
name = unique_key_name(table_name, **options) unless options.key?(:column)
unique_keys(table_name).detect { |unique_key| unique_key.defined_for?(name: name, **options) }
def unique_constraint_for(table_name, **options)
name = unique_constraint_name(table_name, **options) unless options.key?(:column)
unique_constraints(table_name).detect { |unique_constraint| unique_constraint.defined_for?(name: name, **options) }
end
def unique_key_for!(table_name, column: nil, **options)
unique_key_for(table_name, column: column, **options) ||
def unique_constraint_for!(table_name, column: nil, **options)
unique_constraint_for(table_name, column: column, **options) ||
raise(ArgumentError, "Table '#{table_name}' has no unique constraint for #{column || options}")
end

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

@ -12,7 +12,7 @@ class Migration
# * add_foreign_key
# * add_check_constraint
# * add_exclusion_constraint
# * add_unique_key
# * add_unique_constraint
# * add_index
# * add_reference
# * add_timestamps
@ -33,7 +33,7 @@ class Migration
# * remove_foreign_key (must supply a second table)
# * remove_check_constraint
# * remove_exclusion_constraint
# * remove_unique_key
# * remove_unique_constraint
# * remove_index
# * remove_reference
# * remove_timestamps
@ -53,7 +53,7 @@ class CommandRecorder
:change_column_comment, :change_table_comment,
:add_check_constraint, :remove_check_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,
]
include JoinTable
@ -161,7 +161,7 @@ module StraightReversions # :nodoc:
add_foreign_key: :remove_foreign_key,
add_check_constraint: :remove_check_constraint,
add_exclusion_constraint: :remove_exclusion_constraint,
add_unique_key: :remove_unique_key,
add_unique_constraint: :remove_unique_constraint,
enable_extension: :disable_extension,
create_enum: :drop_enum
}.each do |cmd, inv|
@ -329,17 +329,17 @@ def invert_remove_exclusion_constraint(args)
super
end
def invert_add_unique_key(args)
def invert_add_unique_constraint(args)
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
end
def invert_remove_unique_key(args)
def invert_remove_unique_constraint(args)
_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
end

@ -193,7 +193,7 @@ def table(table, stream)
indexes_in_create(table, tbl)
check_constraints_in_create(table, tbl) if @connection.supports_check_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
@ -229,10 +229,10 @@ def indexes_in_create(table, stream)
indexes = indexes.reject { |index| exclusion_constraint_names.include?(index.name) }
end
if @connection.supports_unique_keys? && (unique_keys = @connection.unique_keys(table)).any?
unique_key_names = unique_keys.collect(&:name)
if @connection.supports_unique_constraints? && (unique_constraints = @connection.unique_constraints(table)).any?
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
index_statements = indexes.map do |index|

@ -177,17 +177,17 @@ def test_remove_exclusion_constraint_removes_exclusion_constraint
end
end
def test_unique_key_creates_unique_key
def test_unique_constraint_creates_unique_constraint
with_change_table do |t|
expect :add_unique_key, nil, [:delete_me, :foo, deferrable: :deferred, name: "unique_key"]
t.unique_key :foo, deferrable: :deferred, name: "unique_key"
expect :add_unique_constraint, nil, [:delete_me, :foo, deferrable: :deferred, name: "unique_constraint"]
t.unique_constraint :foo, deferrable: :deferred, name: "unique_constraint"
end
end
def test_remove_unique_key_removes_unique_key
def test_remove_unique_constraint_removes_unique_constraint
with_change_table do |t|
expect :remove_unique_key, nil, [:delete_me, name: "unique_key"]
t.remove_unique_key name: "unique_key"
expect :remove_unique_constraint, nil, [:delete_me, name: "unique_constraint"]
t.remove_unique_constraint name: "unique_constraint"
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
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
@recorder.inverse_of :add_unique_key, [:dogs, using_index: "unique_index"]
@recorder.inverse_of :add_unique_constraint, [:dogs, using_index: "unique_index"]
end
end
def test_invert_remove_unique_key_constraint
enable = @recorder.inverse_of :remove_unique_key, [:dogs, ["speed"], deferrable: :deferred, name: "uniq_speed"]
assert_equal [:add_unique_key, [:dogs, ["speed"], deferrable: :deferred, name: "uniq_speed"], nil], enable
def test_invert_remove_unique_constraint_constraint
enable = @recorder.inverse_of :remove_unique_constraint, [:dogs, ["speed"], deferrable: :deferred, name: "uniq_speed"]
assert_equal [:add_unique_constraint, [:dogs, ["speed"], deferrable: :deferred, name: "uniq_speed"], nil], enable
end
def test_invert_remove_unique_key_constraint_without_options
enable = @recorder.inverse_of :remove_unique_key, [:dogs, ["speed"]]
assert_equal [:add_unique_key, [:dogs, ["speed"]], nil], enable
def test_invert_remove_unique_constraint_constraint_without_options
enable = @recorder.inverse_of :remove_unique_constraint, [:dogs, ["speed"]]
assert_equal [:add_unique_constraint, [:dogs, ["speed"]], nil], enable
end
def test_invert_remove_unique_key_constraint_without_columns
def test_invert_remove_unique_constraint_constraint_without_columns
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

@ -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
if ActiveRecord::Base.connection.supports_unique_keys?
def test_schema_dumps_unique_keys
output = dump_table_schema("test_unique_keys")
constraint_definitions = output.split(/\n/).grep(/t\.unique_key/)
if ActiveRecord::Base.connection.supports_unique_constraints?
def test_schema_dumps_unique_constraints
output = dump_table_schema("test_unique_constraints")
constraint_definitions = output.split(/\n/).grep(/t\.unique_constraint/)
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_key ["position_2"], deferrable: :immediate, name: "test_unique_keys_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_1"], name: "test_unique_constraints_position_deferrable_false"', output
assert_match 't.unique_constraint ["position_2"], deferrable: :immediate, name: "test_unique_constraints_position_deferrable_immediate"', output
assert_match 't.unique_constraint ["position_3"], deferrable: :deferred, name: "test_unique_constraints_position_deferrable_deferred"', output
end
def test_schema_does_not_dumps_unique_key_indexes
output = dump_table_schema("test_unique_keys")
def test_schema_does_not_dump_unique_constraints_as_indexes
output = dump_table_schema("test_unique_constraints")
unique_index_definitions = output.split(/\n/).grep(/t\.index.*unique: true/)
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
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_2
t.integer :position_3
t.unique_key :position_1, name: "test_unique_keys_position_deferrable_false"
t.unique_key :position_2, name: "test_unique_keys_position_deferrable_immediate", deferrable: :immediate
t.unique_key :position_3, name: "test_unique_keys_position_deferrable_deferred", deferrable: :deferred
t.unique_constraint :position_1, name: "test_unique_constraints_position_deferrable_false"
t.unique_constraint :position_2, name: "test_unique_constraints_position_deferrable_immediate", deferrable: :immediate
t.unique_constraint :position_3, name: "test_unique_constraints_position_deferrable_deferred", deferrable: :deferred
end
if supports_partitioned_indexes?

@ -654,14 +654,14 @@ Unique Constraint
# db/migrate/20230422225213_create_items.rb
create_table :items do |t|
t.integer :position, null: false
t.unique_key [:position], deferrable: :immediate
t.unique_constraint [:position], deferrable: :immediate
end
```
If you want to change an existing unique index to deferrable, you can use `:using_index` to create deferrable unique constraints.
```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.