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

See PostgreSQL's [Unique Constraints](https://www.postgresql.org/docs/current/ddl-constraints.html#DDL-CONSTRAINTS-UNIQUE-CONSTRAINTS) documentation for more on unique constraints.

By default, unique constraints in PostgreSQL are checked after each statement.
This works for most use cases, but becomes a major limitation when replacing
records with unique column by using multiple statements.

An example of swapping unique columns between records.

```ruby
old_item = Item.create!(position: 1)
new_item = Item.create!(position: 2)

Item.transaction do
  old_item.update!(position: 2)
  new_item.update!(position: 1)
end
```

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
migrations, it's possible to defer this check.

```ruby
add_unique_key :items, [:position], deferrable: :immediate
```

Passing `deferrable: :immediate` does not change the behaviour of the previous example,
but allows manually deferring the check using `SET CONSTRAINTS ALL DEFERRED` within a transaction.
This will cause the unique constraints to be checked after the transaction.

It's also possible to adjust the default behavior from an immediate
check (after the statement), to a deferred check (after the transaction):

```ruby
add_unique_key :items, [:position], deferrable: :deferred
```

PostgreSQL allows users to create a unique constraints on top of the unique
index that cannot be deferred. In this case, even if users creates deferrable
unique constraint, the existing unique index does not allow users to violate uniqueness
within the transaction. If you want to change existing unique index to deferrable,
you need execute `remove_index` before creating deferrable unique constraints.

*Hiroyuki Ishii*
This commit is contained in:
alpaca-tc 2022-10-03 21:59:33 +09:00
parent 2eed4dc0af
commit d849ee04c6
15 changed files with 534 additions and 3 deletions

@ -1,3 +1,58 @@
* 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"
```
See PostgreSQL's [Unique Constraints](https://www.postgresql.org/docs/current/ddl-constraints.html#DDL-CONSTRAINTS-UNIQUE-CONSTRAINTS) documentation for more on unique constraints.
By default, unique constraints in PostgreSQL are checked after each statement.
This works for most use cases, but becomes a major limitation when replacing
records with unique column by using multiple statements.
An example of swapping unique columns between records.
```ruby
# position is unique column
old_item = Item.create!(position: 1)
new_item = Item.create!(position: 2)
Item.transaction do
old_item.update!(position: 2)
new_item.update!(position: 1)
end
```
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
migrations, it's possible to defer this check.
```ruby
add_unique_key :items, [:position], deferrable: :immediate
```
Passing `deferrable: :immediate` does not change the behaviour of the previous example,
but allows manually deferring the check using `SET CONSTRAINTS ALL DEFERRED` within a transaction.
This will cause the unique constraints to be checked after the transaction.
It's also possible to adjust the default behavior from an immediate
check (after the statement), to a deferred check (after the transaction):
```ruby
add_unique_key :items, [:position], deferrable: :deferred
```
PostgreSQL allows users to create a unique constraints on top of the unique
index that cannot be deferred. In this case, even if users creates deferrable
unique constraint, the existing unique index does not allow users to violate uniqueness
within the transaction. If you want to change existing unique index to deferrable,
you need execute `remove_index` before creating deferrable unique constraints.
*Hiroyuki Ishii*
* Remove deprecated `Tasks::DatabaseTasks.schema_file_type`.
*Rafael Mendonça França*

@ -16,7 +16,8 @@ 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?, to: :@conn, private: true
:supports_index_include?, :supports_exclusion_constraints?, :supports_unique_keys?,
to: :@conn, private: true
private
def visit_AlterTable(o)
@ -63,6 +64,10 @@ 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 })
end
create_sql << "(#{statements.join(', ')})" if statements.present?
add_table_options!(create_sql, o)
create_sql << " AS #{to_sql(o.as)}" if o.as

@ -500,6 +500,11 @@ def supports_exclusion_constraints?
false
end
# Does this adapter support creating unique constraints?
def supports_unique_keys?
false
end
# Does this adapter support views?
def supports_views?
false

@ -12,6 +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(" ")
end
def visit_AddForeignKey(o)
@ -44,6 +46,21 @@ def visit_ExclusionConstraintDefinition(o)
sql.join(" ")
end
def visit_UniqueKeyDefinition(o)
column_name = Array(o.columns).map { |column| quote_column_name(column) }.join(", ")
sql = ["CONSTRAINT"]
sql << quote_column_name(o.name)
sql << "UNIQUE"
sql << "(#{column_name})"
if o.deferrable
sql << "DEFERRABLE INITIALLY #{o.deferrable.to_s.upcase}"
end
sql.join(" ")
end
def visit_AddExclusionConstraint(o)
"ADD #{accept(o)}"
end
@ -52,6 +69,14 @@ def visit_DropExclusionConstraint(name)
"DROP CONSTRAINT #{quote_column_name(name)}"
end
def visit_AddUniqueKey(o)
"ADD #{accept(o)}"
end
def visit_DropUniqueKey(name)
"DROP CONSTRAINT #{quote_column_name(name)}"
end
def visit_ChangeColumnDefinition(o)
column = o.column
column.sql_type = type_to_sql(column.type, **column.options)

@ -207,14 +207,29 @@ def export_name_on_schema_dump?
end
end
UniqueKeyDefinition = Struct.new(:table_name, :columns, :options) do
def name
options[:name]
end
def deferrable
options[:deferrable]
end
def export_name_on_schema_dump?
!ActiveRecord::SchemaDumper.unique_ignore_pattern.match?(name) if name
end
end
class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition
include ColumnMethods
attr_reader :exclusion_constraints, :unlogged
attr_reader :exclusion_constraints, :unique_keys, :unlogged
def initialize(*, **)
super
@exclusion_constraints = []
@unique_keys = []
@unlogged = ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.create_unlogged_tables
end
@ -222,11 +237,20 @@ 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)
end
def new_exclusion_constraint_definition(expression, options) # :nodoc:
options = @conn.exclusion_constraint_options(name, expression, options)
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)
end
def new_column_definition(name, type, **options) # :nodoc:
case type
when :virtual
@ -274,16 +298,36 @@ def exclusion_constraint(*args)
def remove_exclusion_constraint(*args)
@base.remove_exclusion_constraint(name, *args)
end
# Adds an unique constraint.
#
# t.unique_key(: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)
end
# Removes the given unique constraint from the table.
#
# t.remove_unique_key(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)
end
end
class AlterTable < ActiveRecord::ConnectionAdapters::AlterTable
attr_reader :constraint_validations, :exclusion_constraint_adds, :exclusion_constraint_drops
attr_reader :constraint_validations, :exclusion_constraint_adds, :exclusion_constraint_drops, :unique_key_adds, :unique_key_drops
def initialize(td)
super
@constraint_validations = []
@exclusion_constraint_adds = []
@exclusion_constraint_drops = []
@unique_key_adds = []
@unique_key_drops = []
end
def validate_constraint(name)
@ -297,6 +341,14 @@ def add_exclusion_constraint(expression, options)
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)
end
def drop_unique_key(unique_key_name)
@unique_key_drops << unique_key_name
end
end
end
end

@ -49,6 +49,26 @@ 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|
parts = [
"t.unique_key #{unique_key.columns.inspect}"
]
parts << "deferrable: #{unique_key.deferrable.inspect}" if unique_key.deferrable
if unique_key.export_name_on_schema_dump?
parts << "name: #{unique_key.name.inspect}"
end
" #{parts.join(', ')}"
end
stream.puts add_unique_key_statements.sort.join("\n")
end
end
def prepare_column_options(column)
spec = super
spec[:array] = "true" if column.array?

@ -614,6 +614,40 @@ def exclusion_constraints(table_name)
end
end
# Returns an array of unique constraints for the given table.
# The unique constraints are represented as UniqueKeyDefinition objects.
def unique_keys(table_name)
scope = quoted_scope(table_name)
unique_info = exec_query(<<~SQL, "SCHEMA", allow_retry: true, uses_transaction: false)
SELECT c.conname, c.conindid, c.condeferrable, c.condeferred
FROM pg_constraint c
JOIN pg_class t ON c.conrelid = t.oid
JOIN pg_namespace n ON n.oid = c.connamespace
WHERE c.contype = 'u'
AND t.relname = #{scope[:name]}
AND n.nspname = #{scope[:schema]}
SQL
unique_info.map do |row|
deferrable = extract_unique_key_deferrable(row["condeferrable"], row["condeferred"])
columns = query_values(<<~SQL, "SCHEMA")
SELECT a.attname
FROM pg_attribute a
WHERE a.attrelid = #{row['conindid']}
ORDER BY a.attnum
SQL
options = {
name: row["conname"],
deferrable: deferrable
}
UniqueKeyDefinition.new(table_name, columns, options)
end
end
# Adds a new exclusion constraint to the table. +expression+ is a String
# representation of a list of exclusion elements and operators.
#
@ -656,6 +690,57 @@ def remove_exclusion_constraint(table_name, expression = nil, **options)
execute schema_creation.accept(at)
end
# Adds a new unique constraint to the table.
#
# PostgreSQL allows users to create a unique constraints on top of the unique index
# that cannot be deferred. In this case, even if users creates deferrable unique constraint,
# the existing unique index does not allow users to violate uniqueness within the transaction.
# If you want to change existing unique index to deferrable, you need execute `remove_index`
# before creating deferrable unique constraints.
#
# add_unique_key :sections, [:position], deferrable: :deferred, name: "unique_position"
#
# generates:
#
# ALTER TABLE "sections" ADD CONSTRAINT unique_position UNIQUE (position) DEFERRABLE INITIALLY DEFERRED
#
# The +options+ hash can include the following keys:
# [<tt>:name</tt>]
# The constraint name. Defaults to <tt>uniq_rails_<identifier></tt>.
# [<tt>:deferrable</tt>]
# 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+.
def add_unique_key(table_name, column_name, **options)
options = unique_key_options(table_name, column_name, options)
at = create_alter_table(table_name)
at.add_unique_key(column_name, options)
execute schema_creation.accept(at)
end
def unique_key_options(table_name, column_name, options) # :nodoc:
assert_valid_deferrable(options[:deferrable])
options = options.dup
options[:name] ||= unique_key_name(table_name, column_name: column_name, **options)
options
end
# Removes the given unique constraint from the table.
#
# remove_unique_key :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_name: column_name, **options).name
at = create_alter_table(table_name)
at.drop_unique_key(unique_name_to_delete)
execute schema_creation.accept(at)
end
# Maps logical Rails types to PostgreSQL-specific data types.
def type_to_sql(type, limit: nil, precision: nil, scale: nil, array: nil, enum_type: nil, **) # :nodoc:
sql = \
@ -854,6 +939,16 @@ def extract_foreign_key_deferrable(deferrable, deferred)
deferrable && (deferred ? :deferred : true)
end
def extract_unique_key_deferrable(deferrable, deferred)
deferrable && (deferred ? :deferred : :immediate)
end
def assert_valid_deferrable(deferrable) # :nodoc:
return if !deferrable || %i(immediate deferred).include?(deferrable)
raise ArgumentError, "deferrable must be `:immediate` or `:deferred`, got: `#{deferrable.inspect}`"
end
def reference_name_for_table(table_name)
_schema, table_name = extract_schema_qualified_name(table_name.to_s)
table_name.singularize
@ -911,6 +1006,26 @@ 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)
options.fetch(:name) do
column_name = options.fetch(:column_name)
identifier = "#{table_name}_#{column_name}_unique"
hashed_identifier = Digest::SHA256.hexdigest(identifier).first(10)
"uniq_rails_#{hashed_identifier}"
end
end
def unique_key_for(table_name, **options)
unique_key_name = unique_key_name(table_name, **options)
unique_keys(table_name).detect { |unique_key| unique_key.name == unique_key_name }
end
def unique_key_for!(table_name, column_name: nil, **options)
unique_key_for(table_name, column_name: column_name, **options) ||
raise(ArgumentError, "Table '#{table_name}' has no unique constraint for #{column_name || options}")
end
def data_source_sql(name = nil, type: nil)
scope = quoted_scope(name, type: type)
scope[:type] ||= "'r','v','m','p','f'" # (r)elation/table, (v)iew, (m)aterialized view, (p)artitioned table, (f)oreign table

@ -224,6 +224,10 @@ def supports_exclusion_constraints?
true
end
def supports_unique_keys?
true
end
def supports_validate_constraints?
true
end

@ -10,6 +10,7 @@ class Migration
# * add_foreign_key
# * add_check_constraint
# * add_exclusion_constraint
# * add_unique_key
# * add_index
# * add_reference
# * add_timestamps
@ -30,6 +31,7 @@ class Migration
# * remove_foreign_key (must supply a second table)
# * remove_check_constraint
# * remove_exclusion_constraint
# * remove_unique_key
# * remove_index
# * remove_reference
# * remove_timestamps
@ -47,6 +49,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,
:create_enum, :drop_enum,
]
include JoinTable
@ -154,6 +157,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,
enable_extension: :disable_extension,
create_enum: :drop_enum
}.each do |cmd, inv|
@ -304,6 +308,13 @@ def invert_remove_exclusion_constraint(args)
super
end
def invert_remove_unique_key(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?
super
end
def invert_drop_enum(args)
_enum, values = args.dup.tap(&:extract_options!)
raise ActiveRecord::IrreversibleMigration, "drop_enum is only reversible if given a list of enum values." unless values

@ -34,6 +34,12 @@ class SchemaDumper # :nodoc:
# should not be dumped to db/schema.rb.
cattr_accessor :excl_ignore_pattern, default: /^excl_rails_[0-9a-f]{10}$/
##
# :singleton-method:
# Specify a custom regular expression matching unique constraints which name
# should not be dumped to db/schema.rb.
cattr_accessor :unique_ignore_pattern, default: /^uniq_rails_[0-9a-f]{10}$/
class << self
def dump(connection = ActiveRecord::Base.connection, stream = STDOUT, config = ActiveRecord::Base)
connection.create_schema_dumper(generate_options(config)).dump(stream)
@ -182,6 +188,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?
tbl.puts " end"
tbl.puts
@ -217,6 +224,12 @@ 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)
indexes = indexes.reject { |index| unique_key_names.include?(index.name) }
end
index_statements = indexes.map do |index|
" t.index #{index_parts(index).join(', ')}"
end

@ -176,6 +176,20 @@ def test_remove_exclusion_constraint_removes_exclusion_constraint
t.remove_exclusion_constraint name: "date_overlap"
end
end
def test_unique_key_creates_unique_key
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"
end
end
def test_remove_unique_key_removes_unique_key
with_change_table do |t|
expect :remove_unique_key, nil, [:delete_me, name: "unique_key"]
t.remove_unique_key name: "unique_key"
end
end
end
def test_column_creates_column

@ -457,6 +457,22 @@ def test_invert_remove_check_constraint_without_expression
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
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
end
def test_invert_remove_unique_key_constraint_without_columns
assert_raises(ActiveRecord::IrreversibleMigration) do
@recorder.inverse_of :remove_unique_key, [:dogs, name: "uniq_speed"]
end
end
def test_invert_create_enum
drop = @recorder.inverse_of :create_enum, [:color, ["blue", "green"]]
assert_equal [:drop_enum, [:color, ["blue", "green"]], nil], drop

@ -0,0 +1,167 @@
# 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,
columns: ["position_1"]
}, {
name: "test_unique_keys_position_deferrable_immediate",
deferrable: :immediate,
columns: ["position_2"]
}, {
name: "test_unique_keys_position_deferrable_deferred",
deferrable: :deferred,
columns: ["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[:columns], constraint.columns
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_3d89d7e853", 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_3d89d7e853", 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_3d89d7e853", 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_3d89d7e853", 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_remove_unique_key
assert_equal 0, @connection.unique_keys("sections").size
@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_equal 0, @connection.unique_keys("sections").size
end
def test_remove_non_existing_unique_key
assert_raises(ArgumentError) do
@connection.remove_unique_key :sections, name: "nonexistent"
end
end
end
end
end
end

@ -227,6 +227,25 @@ 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/)
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
end
def test_schema_does_not_dumps_unique_key_indexes
output = dump_table_schema("test_unique_keys")
unique_index_definitions = output.split(/\n/).grep(/t\.index.*unique: true/)
assert_equal 0, unique_index_definitions.size
end
end
def test_schema_dump_should_honor_nonstandard_primary_keys
output = standard_dump
match = output.match(%r{create_table "movies"(.*)do})

@ -135,6 +135,16 @@
t.exclusion_constraint "daterange(start_date, end_date) WITH &&", using: :gist, where: "start_date IS NOT NULL AND end_date IS NOT NULL", name: "test_exclusion_constraints_date_overlap"
end
create_table :test_unique_keys, 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
end
if supports_partitioned_indexes?
create_table(:measurements, id: false, force: true, options: "PARTITION BY LIST (city_id)") do |t|
t.string :city_id, null: false