Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Deprecate deferrable: true option of add_foreign_key #47659

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
11 changes: 11 additions & 0 deletions activerecord/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
* Deprecate `deferrable: true` option of `add_foreign_key`.

`deferrable: true` is deprecated in favor of `deferrable: :immediate`, and
will be removed in Rails 7.2.

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.

*Hiroyuki Ishii*

* `AbstractAdapter#execute` and `#exec_query` now clear the query cache

If you need to perform a read only SQL query without clearing the query
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,7 @@ def visit_AlterTable(o)

def visit_AddForeignKey(o)
super.dup.tap do |sql|
if o.deferrable
sql << " DEFERRABLE"
sql << " INITIALLY #{o.deferrable.to_s.upcase}" unless o.deferrable == true
end

sql << " DEFERRABLE INITIALLY #{o.options[:deferrable].to_s.upcase}" if o.deferrable
sql << " NOT VALID" unless o.validate?
end
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -518,6 +518,20 @@ def index_name(table_name, options) # :nodoc:
super
end

def add_foreign_key(from_table, to_table, **options)
if options[:deferrable] == true
ActiveRecord.deprecator.warn(<<~MSG)
`deferrable: true` is deprecated in favor of `deferrable: :immediate`, and will be removed in Rails 7.2.
MSG

options[:deferrable] = :immediate
end

assert_valid_deferrable(options[:deferrable])

super
end

def foreign_keys(table_name)
scope = quoted_scope(table_name)
fk_info = internal_exec_query(<<~SQL, "SCHEMA", allow_retry: true, materialize_transactions: false)
Expand All @@ -543,7 +557,7 @@ def foreign_keys(table_name)

options[:on_delete] = extract_foreign_key_action(row["on_delete"])
options[:on_update] = extract_foreign_key_action(row["on_update"])
options[:deferrable] = extract_foreign_key_deferrable(row["deferrable"], row["deferred"])
options[:deferrable] = extract_constraint_deferrable(row["deferrable"], row["deferred"])

options[:validate] = row["valid"]
to_table = Utils.unquote_identifier(row["to_table"])
Expand Down Expand Up @@ -947,20 +961,16 @@ def extract_foreign_key_action(specifier)
end
end

def extract_foreign_key_deferrable(deferrable, deferred)
deferrable && (deferred ? :deferred : true)
def assert_valid_deferrable(deferrable)
return if !deferrable || %i(immediate deferred).include?(deferrable)

raise ArgumentError, "deferrable must be `:immediate` or `:deferred`, got: `#{deferrable.inspect}`"
end

def extract_constraint_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
Expand Down
7 changes: 7 additions & 0 deletions activerecord/lib/active_record/migration/compatibility.rb
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,13 @@ def disable_extension(name, **options)
super
end

def add_foreign_key(from_table, to_table, **options)
yahonda marked this conversation as resolved.
Show resolved Hide resolved
if connection.adapter_name == "PostgreSQL" && options[:deferrable] == true
options[:deferrable] = :immediate
end
super
end

private
def compatible_table_definition(t)
class << t
Expand Down
21 changes: 21 additions & 0 deletions activerecord/test/cases/migration/compatibility_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -756,6 +756,27 @@ def up
ensure
disable_extension!(:hstore, connection)
end

def test_legacy_add_foreign_key_with_deferrable_true
yahonda marked this conversation as resolved.
Show resolved Hide resolved
migration = Class.new(ActiveRecord::Migration[7.0]) {
def migrate(x)
create_table :sub_testings do |t|
t.bigint :testing_id
end

add_foreign_key(:sub_testings, :testings, name: "deferrable_foreign_key", deferrable: true)
end
}.new

ActiveRecord::Migrator.new(:up, [migration], @schema_migration, @internal_metadata).migrate

foreign_keys = Testing.connection.foreign_keys("sub_testings")
assert_equal 1, foreign_keys.size
assert_equal :immediate, foreign_keys.first.deferrable
ensure
connection.drop_table(:sub_testings, if_exists: true)
ActiveRecord::Base.clear_cache!
end
end

if current_adapter?(:Mysql2Adapter, :TrilogyAdapter)
Expand Down
28 changes: 20 additions & 8 deletions activerecord/test/cases/migration/foreign_key_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -519,13 +519,13 @@ def test_add_invalid_foreign_key

if ActiveRecord::Base.connection.supports_deferrable_constraints?
def test_deferrable_foreign_key
@connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", deferrable: true
@connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", deferrable: :immediate

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

fk = foreign_keys.first
assert_equal true, fk.options[:deferrable]
assert_equal :immediate, fk.deferrable
end

def test_not_deferrable_foreign_key
Expand All @@ -535,7 +535,7 @@ def test_not_deferrable_foreign_key
assert_equal 1, foreign_keys.size

fk = foreign_keys.first
assert_equal false, fk.options[:deferrable]
assert_equal false, fk.deferrable
end

def test_deferrable_initially_deferred_foreign_key
Expand All @@ -545,7 +545,7 @@ def test_deferrable_initially_deferred_foreign_key
assert_equal 1, foreign_keys.size

fk = foreign_keys.first
assert_equal :deferred, fk.options[:deferrable]
assert_equal :deferred, fk.deferrable
end

def test_deferrable_initially_immediate_foreign_key
Expand All @@ -555,15 +555,15 @@ def test_deferrable_initially_immediate_foreign_key
assert_equal 1, foreign_keys.size

fk = foreign_keys.first
assert_equal true, fk.options[:deferrable]
assert_equal :immediate, fk.deferrable
end

def test_schema_dumping_with_defferable
@connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", deferrable: true
@connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", deferrable: :immediate

output = dump_table_schema "astronauts"

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

def test_schema_dumping_with_disabled_defferable
Expand All @@ -587,7 +587,19 @@ def test_schema_dumping_with_defferable_initially_immediate

output = dump_table_schema "astronauts"

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

def test_deferrable_true_foreign_key
assert_deprecated(ActiveRecord.deprecator) do
@connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", deferrable: true
end

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

fk = foreign_keys.first
assert_equal :immediate, fk.deferrable
end
end

Expand Down
2 changes: 1 addition & 1 deletion guides/source/active_record_postgresql.md
Original file line number Diff line number Diff line change
Expand Up @@ -618,7 +618,7 @@ ActiveRecord::Base.connection.transaction do
end
```

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:
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:

```ruby
ActiveRecord::Base.transaction do
Expand Down