Skip to content

Commit f7b56f9

Browse files
authored
Merge pull request rails#47971 from alpaca-tc/add_support_using_keyword_for_unique_constraint
Adds support `USING INDEX` for unique constraints in PostgreSQL.
2 parents d36fb65 + f5c1222 commit f7b56f9

File tree

7 files changed

+78
-15
lines changed

7 files changed

+78
-15
lines changed

activerecord/CHANGELOG.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -228,11 +228,12 @@
228228
add_unique_key :items, [:position], deferrable: :deferred
229229
```
230230

231-
PostgreSQL allows users to create a unique constraints on top of the unique
232-
index that cannot be deferred. In this case, even if users creates deferrable
233-
unique constraint, the existing unique index does not allow users to violate uniqueness
234-
within the transaction. If you want to change existing unique index to deferrable,
235-
you need execute `remove_index` before creating deferrable unique constraints.
231+
If you want to change an existing unique index to deferrable, you can use :using_index
232+
to create deferrable unique constraints.
233+
234+
```ruby
235+
add_unique_key :items, deferrable: :deferred, using_index: "index_items_on_position"
236+
```
236237

237238
*Hiroyuki Ishii*
238239

activerecord/lib/active_record/connection_adapters/postgresql/schema_creation.rb

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,12 @@ def visit_UniqueKeyDefinition(o)
5353
sql = ["CONSTRAINT"]
5454
sql << quote_column_name(o.name)
5555
sql << "UNIQUE"
56-
sql << "(#{column_name})"
56+
57+
if o.using_index
58+
sql << "USING INDEX #{quote_column_name(o.using_index)}"
59+
else
60+
sql << "(#{column_name})"
61+
end
5762

5863
if o.deferrable
5964
sql << "DEFERRABLE INITIALLY #{o.deferrable.to_s.upcase}"

activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,10 @@ def deferrable
220220
options[:deferrable]
221221
end
222222

223+
def using_index
224+
options[:using_index]
225+
end
226+
223227
def export_name_on_schema_dump?
224228
!ActiveRecord::SchemaDumper.unique_ignore_pattern.match?(name) if name
225229
end

activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -700,24 +700,24 @@ def remove_exclusion_constraint(table_name, expression = nil, **options)
700700

701701
# Adds a new unique constraint to the table.
702702
#
703-
# PostgreSQL allows users to create a unique constraints on top of the unique index
704-
# that cannot be deferred. In this case, even if users creates deferrable unique constraint,
705-
# the existing unique index does not allow users to violate uniqueness within the transaction.
706-
# If you want to change existing unique index to deferrable, you need execute `remove_index`
707-
# before creating deferrable unique constraints.
708-
#
709703
# add_unique_key :sections, [:position], deferrable: :deferred, name: "unique_position"
710704
#
711705
# generates:
712706
#
713707
# ALTER TABLE "sections" ADD CONSTRAINT unique_position UNIQUE (position) DEFERRABLE INITIALLY DEFERRED
714708
#
709+
# If you want to change an existing unique index to deferrable, you can use :using_index to create deferrable unique constraints.
710+
#
711+
# add_unique_key :sections, deferrable: :deferred, name: "unique_position", using_index: "index_sections_on_position"
712+
#
715713
# The +options+ hash can include the following keys:
716714
# [<tt>:name</tt>]
717715
# The constraint name. Defaults to <tt>uniq_rails_<identifier></tt>.
718716
# [<tt>:deferrable</tt>]
719717
# 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+.
720-
def add_unique_key(table_name, column_name, **options)
718+
# [<tt>:using_index</tt>]
719+
# To specify an existing unique index name. Defaults to +nil+.
720+
def add_unique_key(table_name, column_name = nil, **options)
721721
options = unique_key_options(table_name, column_name, options)
722722
at = create_alter_table(table_name)
723723
at.add_unique_key(column_name, options)
@@ -728,6 +728,10 @@ def add_unique_key(table_name, column_name, **options)
728728
def unique_key_options(table_name, column_name, options) # :nodoc:
729729
assert_valid_deferrable(options[:deferrable])
730730

731+
if column_name && options[:using_index]
732+
raise ArgumentError, "Cannot specify both column_name and :using_index options."
733+
end
734+
731735
options = options.dup
732736
options[:name] ||= unique_key_name(table_name, column_name: column_name, **options)
733737
options
@@ -1016,8 +1020,8 @@ def exclusion_constraint_for!(table_name, expression: nil, **options)
10161020

10171021
def unique_key_name(table_name, **options)
10181022
options.fetch(:name) do
1019-
column_name = options.fetch(:column_name)
1020-
identifier = "#{table_name}_#{column_name}_unique"
1023+
column_name_or_index_name = options.fetch(:column_name) || options[:using_index]
1024+
identifier = "#{table_name}_#{column_name_or_index_name}_unique"
10211025
hashed_identifier = Digest::SHA256.hexdigest(identifier).first(10)
10221026

10231027
"uniq_rails_#{hashed_identifier}"

activerecord/lib/active_record/migration/command_recorder.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,13 @@ def invert_remove_exclusion_constraint(args)
318318
super
319319
end
320320

321+
def invert_add_unique_key(args)
322+
options = args.dup.extract_options!
323+
324+
raise ActiveRecord::IrreversibleMigration, "add_unique_key is not reversible if given an using_index." if options[:using_index]
325+
super
326+
end
327+
321328
def invert_remove_unique_key(args)
322329
_table, columns = args.dup.tap(&:extract_options!)
323330

activerecord/test/cases/migration/command_recorder_test.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,12 @@ def test_invert_remove_check_constraint_without_expression
471471
end
472472
end
473473

474+
def test_invert_add_unique_key_constraint_with_using_index
475+
assert_raises(ActiveRecord::IrreversibleMigration) do
476+
@recorder.inverse_of :add_unique_key, [:dogs, using_index: "unique_index"]
477+
end
478+
end
479+
474480
def test_invert_remove_unique_key_constraint
475481
enable = @recorder.inverse_of :remove_unique_key, [:dogs, ["speed"], deferrable: :deferred, name: "uniq_speed"]
476482
assert_equal [:add_unique_key, [:dogs, ["speed"], deferrable: :deferred, name: "uniq_speed"], nil], enable

activerecord/test/cases/migration/unique_key_test.rb

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,42 @@ def test_added_deferrable_initially_immediate_unique_key
147147
end
148148
end
149149

150+
def test_add_unique_key_with_name_and_using_index
151+
@connection.add_index :sections, [:position], name: "unique_index", unique: true
152+
@connection.add_unique_key :sections, name: "unique_constraint", deferrable: :immediate, using_index: "unique_index"
153+
154+
unique_keys = @connection.unique_keys("sections")
155+
assert_equal 1, unique_keys.size
156+
157+
constraint = unique_keys.first
158+
assert_equal "sections", constraint.table_name
159+
assert_equal "unique_constraint", constraint.name
160+
assert_equal ["position"], constraint.columns
161+
assert_equal :immediate, constraint.deferrable
162+
end
163+
164+
def test_add_unique_key_with_only_using_index
165+
@connection.add_index :sections, [:position], name: "unique_index", unique: true
166+
@connection.add_unique_key :sections, using_index: "unique_index"
167+
168+
unique_keys = @connection.unique_keys("sections")
169+
assert_equal 1, unique_keys.size
170+
171+
constraint = unique_keys.first
172+
assert_equal "sections", constraint.table_name
173+
assert_equal "uniq_rails_79b901ffb4", constraint.name
174+
assert_equal ["position"], constraint.columns
175+
assert_equal false, constraint.deferrable
176+
end
177+
178+
def test_add_unique_key_with_columns_and_using_index
179+
@connection.add_index :sections, [:position], name: "unique_index", unique: true
180+
181+
assert_raises(ArgumentError) do
182+
@connection.add_unique_key :sections, [:position], using_index: "unique_index"
183+
end
184+
end
185+
150186
def test_remove_unique_key
151187
assert_equal 0, @connection.unique_keys("sections").size
152188

0 commit comments

Comments
 (0)