Skip to content

Commit f5c1222

Browse files
committed
Adds support USING INDEX for unique constraints in PostgreSQL.
Adds `:using_index` option to use an existing index when defining a unique constraint. 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 :users, deferrable: :immediate, using_index: 'unique_index_name' ``` A unique constraint internally constructs a unique index. If an existing unique index has already been created, the unique constraint can be created much faster, since there is no need to create the unique index when generating the constraint.
1 parent 539144d commit f5c1222

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)