Skip to content

Commit 992d689

Browse files
authored
Merge pull request rails#47659 from alpaca-tc/add_foreign_key_deferrable_must_be_false_or_symbol
Deprecate `deferrable: true` option of `add_foreign_key`
2 parents 5940955 + 896d359 commit 992d689

File tree

7 files changed

+80
-23
lines changed

7 files changed

+80
-23
lines changed

activerecord/CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,14 @@
1+
* Deprecate `deferrable: true` option of `add_foreign_key`.
2+
3+
`deferrable: true` is deprecated in favor of `deferrable: :immediate`, and
4+
will be removed in Rails 7.2.
5+
6+
Because `deferrable: true` and `deferrable: :deferred` are hard to understand.
7+
Both true and :deferred are truthy values.
8+
This behavior is the same as the deferrable option of the add_unique_key method, added in #46192.
9+
10+
*Hiroyuki Ishii*
11+
112
* `AbstractAdapter#execute` and `#exec_query` now clear the query cache
213

314
If you need to perform a read only SQL query without clearing the query

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

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,7 @@ def visit_AlterTable(o)
1818

1919
def visit_AddForeignKey(o)
2020
super.dup.tap do |sql|
21-
if o.deferrable
22-
sql << " DEFERRABLE"
23-
sql << " INITIALLY #{o.deferrable.to_s.upcase}" unless o.deferrable == true
24-
end
25-
21+
sql << " DEFERRABLE INITIALLY #{o.options[:deferrable].to_s.upcase}" if o.deferrable
2622
sql << " NOT VALID" unless o.validate?
2723
end
2824
end

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

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -518,6 +518,20 @@ def index_name(table_name, options) # :nodoc:
518518
super
519519
end
520520

521+
def add_foreign_key(from_table, to_table, **options)
522+
if options[:deferrable] == true
523+
ActiveRecord.deprecator.warn(<<~MSG)
524+
`deferrable: true` is deprecated in favor of `deferrable: :immediate`, and will be removed in Rails 7.2.
525+
MSG
526+
527+
options[:deferrable] = :immediate
528+
end
529+
530+
assert_valid_deferrable(options[:deferrable])
531+
532+
super
533+
end
534+
521535
def foreign_keys(table_name)
522536
scope = quoted_scope(table_name)
523537
fk_info = internal_exec_query(<<~SQL, "SCHEMA", allow_retry: true, materialize_transactions: false)
@@ -543,7 +557,7 @@ def foreign_keys(table_name)
543557

544558
options[:on_delete] = extract_foreign_key_action(row["on_delete"])
545559
options[:on_update] = extract_foreign_key_action(row["on_update"])
546-
options[:deferrable] = extract_foreign_key_deferrable(row["deferrable"], row["deferred"])
560+
options[:deferrable] = extract_constraint_deferrable(row["deferrable"], row["deferred"])
547561

548562
options[:validate] = row["valid"]
549563
to_table = Utils.unquote_identifier(row["to_table"])
@@ -947,20 +961,16 @@ def extract_foreign_key_action(specifier)
947961
end
948962
end
949963

950-
def extract_foreign_key_deferrable(deferrable, deferred)
951-
deferrable && (deferred ? :deferred : true)
964+
def assert_valid_deferrable(deferrable)
965+
return if !deferrable || %i(immediate deferred).include?(deferrable)
966+
967+
raise ArgumentError, "deferrable must be `:immediate` or `:deferred`, got: `#{deferrable.inspect}`"
952968
end
953969

954970
def extract_constraint_deferrable(deferrable, deferred)
955971
deferrable && (deferred ? :deferred : :immediate)
956972
end
957973

958-
def assert_valid_deferrable(deferrable) # :nodoc:
959-
return if !deferrable || %i(immediate deferred).include?(deferrable)
960-
961-
raise ArgumentError, "deferrable must be `:immediate` or `:deferred`, got: `#{deferrable.inspect}`"
962-
end
963-
964974
def reference_name_for_table(table_name)
965975
_schema, table_name = extract_schema_qualified_name(table_name.to_s)
966976
table_name.singularize

activerecord/lib/active_record/migration/compatibility.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,13 @@ def disable_extension(name, **options)
138138
super
139139
end
140140

141+
def add_foreign_key(from_table, to_table, **options)
142+
if connection.adapter_name == "PostgreSQL" && options[:deferrable] == true
143+
options[:deferrable] = :immediate
144+
end
145+
super
146+
end
147+
141148
private
142149
def compatible_table_definition(t)
143150
class << t

activerecord/test/cases/migration/compatibility_test.rb

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -756,6 +756,27 @@ def up
756756
ensure
757757
disable_extension!(:hstore, connection)
758758
end
759+
760+
def test_legacy_add_foreign_key_with_deferrable_true
761+
migration = Class.new(ActiveRecord::Migration[7.0]) {
762+
def migrate(x)
763+
create_table :sub_testings do |t|
764+
t.bigint :testing_id
765+
end
766+
767+
add_foreign_key(:sub_testings, :testings, name: "deferrable_foreign_key", deferrable: true)
768+
end
769+
}.new
770+
771+
ActiveRecord::Migrator.new(:up, [migration], @schema_migration, @internal_metadata).migrate
772+
773+
foreign_keys = Testing.connection.foreign_keys("sub_testings")
774+
assert_equal 1, foreign_keys.size
775+
assert_equal :immediate, foreign_keys.first.deferrable
776+
ensure
777+
connection.drop_table(:sub_testings, if_exists: true)
778+
ActiveRecord::Base.clear_cache!
779+
end
759780
end
760781

761782
if current_adapter?(:Mysql2Adapter, :TrilogyAdapter)

activerecord/test/cases/migration/foreign_key_test.rb

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -519,13 +519,13 @@ def test_add_invalid_foreign_key
519519

520520
if ActiveRecord::Base.connection.supports_deferrable_constraints?
521521
def test_deferrable_foreign_key
522-
@connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", deferrable: true
522+
@connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", deferrable: :immediate
523523

524524
foreign_keys = @connection.foreign_keys("astronauts")
525525
assert_equal 1, foreign_keys.size
526526

527527
fk = foreign_keys.first
528-
assert_equal true, fk.options[:deferrable]
528+
assert_equal :immediate, fk.deferrable
529529
end
530530

531531
def test_not_deferrable_foreign_key
@@ -535,7 +535,7 @@ def test_not_deferrable_foreign_key
535535
assert_equal 1, foreign_keys.size
536536

537537
fk = foreign_keys.first
538-
assert_equal false, fk.options[:deferrable]
538+
assert_equal false, fk.deferrable
539539
end
540540

541541
def test_deferrable_initially_deferred_foreign_key
@@ -545,7 +545,7 @@ def test_deferrable_initially_deferred_foreign_key
545545
assert_equal 1, foreign_keys.size
546546

547547
fk = foreign_keys.first
548-
assert_equal :deferred, fk.options[:deferrable]
548+
assert_equal :deferred, fk.deferrable
549549
end
550550

551551
def test_deferrable_initially_immediate_foreign_key
@@ -555,15 +555,15 @@ def test_deferrable_initially_immediate_foreign_key
555555
assert_equal 1, foreign_keys.size
556556

557557
fk = foreign_keys.first
558-
assert_equal true, fk.options[:deferrable]
558+
assert_equal :immediate, fk.deferrable
559559
end
560560

561561
def test_schema_dumping_with_defferable
562-
@connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", deferrable: true
562+
@connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", deferrable: :immediate
563563

564564
output = dump_table_schema "astronauts"
565565

566-
assert_match %r{\s+add_foreign_key "astronauts", "rockets", deferrable: true$}, output
566+
assert_match %r{\s+add_foreign_key "astronauts", "rockets", deferrable: :immediate$}, output
567567
end
568568

569569
def test_schema_dumping_with_disabled_defferable
@@ -587,7 +587,19 @@ def test_schema_dumping_with_defferable_initially_immediate
587587

588588
output = dump_table_schema "astronauts"
589589

590-
assert_match %r{\s+add_foreign_key "astronauts", "rockets", deferrable: true$}, output
590+
assert_match %r{\s+add_foreign_key "astronauts", "rockets", deferrable: :immediate$}, output
591+
end
592+
593+
def test_deferrable_true_foreign_key
594+
assert_deprecated(ActiveRecord.deprecator) do
595+
@connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", deferrable: true
596+
end
597+
598+
foreign_keys = @connection.foreign_keys("astronauts")
599+
assert_equal 1, foreign_keys.size
600+
601+
fk = foreign_keys.first
602+
assert_equal :immediate, fk.deferrable
591603
end
592604
end
593605

guides/source/active_record_postgresql.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -618,7 +618,7 @@ ActiveRecord::Base.connection.transaction do
618618
end
619619
```
620620

621-
The `:deferrable` option can also be set to `true` or `:immediate`, which has the same effect. Both options let the foreign keys keep the default behavior of checking the constraint immediately, but allow manually deferring the checks using `SET CONSTRAINTS ALL DEFERRED` within a transaction. This will cause the foreign keys to be checked when the transaction is committed:
621+
When the `:deferrable` option is set to `:immediate`, let the foreign keys keep the default behavior of checking the constraint immediately, but allow manually deferring the checks using `SET CONSTRAINTS ALL DEFERRED` within a transaction. This will cause the foreign keys to be checked when the transaction is committed:
622622

623623
```ruby
624624
ActiveRecord::Base.transaction do

0 commit comments

Comments
 (0)