SQLite3: Implement add_foreign_key
and remove_foreign_key
I implemented Foreign key create in `create_table` for SQLite3 at #24743. This follows #24743 to implement `add_foreign_key` and `remove_foreign_key`. Unfortunately SQLite3 has one limitation that `PRAGMA foreign_key_list(table-name)` doesn't have constraint name. So we couldn't implement find/remove foreign key by name for now. Fixes #35207. Closes #31343.
This commit is contained in:
parent
d87afbf46f
commit
da5843436b
@ -42,4 +42,5 @@
|
||||
t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true
|
||||
end
|
||||
|
||||
add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
|
||||
end
|
||||
|
@ -55,4 +55,5 @@
|
||||
t.datetime "updated_at", precision: 6, null: false
|
||||
end
|
||||
|
||||
add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
|
||||
end
|
||||
|
@ -15,7 +15,7 @@ def accept(o)
|
||||
end
|
||||
|
||||
delegate :quote_column_name, :quote_table_name, :quote_default_expression, :type_to_sql,
|
||||
:options_include_default?, :supports_indexes_in_create?, :supports_foreign_keys_in_create?, :foreign_key_options,
|
||||
:options_include_default?, :supports_indexes_in_create?, :supports_foreign_keys?, :foreign_key_options,
|
||||
to: :@conn, private: true
|
||||
|
||||
private
|
||||
@ -50,7 +50,7 @@ def visit_TableDefinition(o)
|
||||
statements.concat(o.indexes.map { |column_name, options| index_in_create(o.name, column_name, options) })
|
||||
end
|
||||
|
||||
if supports_foreign_keys_in_create?
|
||||
if supports_foreign_keys?
|
||||
statements.concat(o.foreign_keys.map { |to_table, options| foreign_key_in_create(o.name, to_table, options) })
|
||||
end
|
||||
|
||||
|
@ -102,7 +102,7 @@ def validate?
|
||||
alias validated? validate?
|
||||
|
||||
def export_name_on_schema_dump?
|
||||
name !~ ActiveRecord::SchemaDumper.fk_ignore_pattern
|
||||
!ActiveRecord::SchemaDumper.fk_ignore_pattern.match?(name) if name
|
||||
end
|
||||
|
||||
def defined_for?(to_table_ord = nil, to_table: nil, **options)
|
||||
|
@ -335,6 +335,7 @@ def supports_validate_constraints?
|
||||
def supports_foreign_keys_in_create?
|
||||
supports_foreign_keys?
|
||||
end
|
||||
deprecate :supports_foreign_keys_in_create?
|
||||
|
||||
# Does this adapter support views?
|
||||
def supports_views?
|
||||
|
@ -52,6 +52,34 @@ def indexes(table_name)
|
||||
end.compact
|
||||
end
|
||||
|
||||
def add_foreign_key(from_table, to_table, **options)
|
||||
alter_table(from_table) do |definition|
|
||||
to_table = strip_table_name_prefix_and_suffix(to_table)
|
||||
definition.foreign_key(to_table, options)
|
||||
end
|
||||
end
|
||||
|
||||
def remove_foreign_key(from_table, to_table = nil, **options)
|
||||
to_table ||= options[:to_table]
|
||||
options = options.except(:name, :to_table)
|
||||
foreign_keys = foreign_keys(from_table)
|
||||
|
||||
fkey = foreign_keys.detect do |fk|
|
||||
table = to_table || begin
|
||||
table = options[:column].to_s.delete_suffix("_id")
|
||||
Base.pluralize_table_names ? table.pluralize : table
|
||||
end
|
||||
table = strip_table_name_prefix_and_suffix(table)
|
||||
fk_to_table = strip_table_name_prefix_and_suffix(fk.to_table)
|
||||
fk_to_table == table && options.all? { |k, v| fk.options[k].to_s == v.to_s }
|
||||
end || raise(ArgumentError, "Table '#{from_table}' has no foreign key for #{to_table}")
|
||||
|
||||
alter_table(from_table, foreign_keys) do |definition|
|
||||
fk_to_table = strip_table_name_prefix_and_suffix(fkey.to_table)
|
||||
definition.foreign_keys.delete([fk_to_table, fkey.options])
|
||||
end
|
||||
end
|
||||
|
||||
def create_schema_dumper(options)
|
||||
SQLite3::SchemaDumper.create(self, options)
|
||||
end
|
||||
|
@ -121,7 +121,7 @@ def requires_reloading?
|
||||
true
|
||||
end
|
||||
|
||||
def supports_foreign_keys_in_create?
|
||||
def supports_foreign_keys?
|
||||
true
|
||||
end
|
||||
|
||||
@ -424,9 +424,8 @@ def invalid_alter_table_type?(type, options)
|
||||
type.to_sym == :primary_key || options[:primary_key]
|
||||
end
|
||||
|
||||
def alter_table(table_name, options = {})
|
||||
def alter_table(table_name, foreign_keys = foreign_keys(table_name), **options)
|
||||
altered_table_name = "a#{table_name}"
|
||||
foreign_keys = foreign_keys(table_name)
|
||||
|
||||
caller = lambda do |definition|
|
||||
rename = options[:rename] || {}
|
||||
|
@ -348,6 +348,10 @@ def test_select_methods_passing_a_relation
|
||||
assert_equal "special_db_type", @connection.type_to_sql(:special_db_type)
|
||||
end
|
||||
|
||||
def test_supports_foreign_keys_in_create_is_deprecated
|
||||
assert_deprecated { @connection.supports_foreign_keys_in_create? }
|
||||
end
|
||||
|
||||
def test_supports_multi_insert_is_deprecated
|
||||
assert_deprecated { @connection.supports_multi_insert? }
|
||||
end
|
||||
|
@ -462,7 +462,11 @@ class ChangeSchemaWithDependentObjectsTest < ActiveRecord::TestCase
|
||||
end
|
||||
|
||||
def test_create_table_with_force_cascade_drops_dependent_objects
|
||||
skip "MySQL > 5.5 does not drop dependent objects with DROP TABLE CASCADE" if current_adapter?(:Mysql2Adapter)
|
||||
if current_adapter?(:Mysql2Adapter)
|
||||
skip "MySQL > 5.5 does not drop dependent objects with DROP TABLE CASCADE"
|
||||
elsif current_adapter?(:SQLite3Adapter)
|
||||
skip "SQLite3 does not support DROP TABLE CASCADE syntax"
|
||||
end
|
||||
# can't re-create table referenced by foreign key
|
||||
assert_raises(ActiveRecord::StatementInvalid) do
|
||||
@connection.create_table :trains, force: true
|
||||
|
@ -3,7 +3,7 @@
|
||||
require "cases/helper"
|
||||
require "support/schema_dumping_helper"
|
||||
|
||||
if ActiveRecord::Base.connection.supports_foreign_keys_in_create?
|
||||
if ActiveRecord::Base.connection.supports_foreign_keys?
|
||||
module ActiveRecord
|
||||
class Migration
|
||||
class ForeignKeyInCreateTest < ActiveRecord::TestCase
|
||||
@ -119,6 +119,15 @@ def test_remove_reference_column_of_child_table
|
||||
|
||||
assert_empty @connection.foreign_keys(Astronaut.table_name)
|
||||
end
|
||||
|
||||
def test_remove_foreign_key_by_column
|
||||
rocket = Rocket.create!(name: "myrocket")
|
||||
rocket.astronauts << Astronaut.create!
|
||||
|
||||
@connection.remove_foreign_key Astronaut.table_name, column: :rocket_id
|
||||
|
||||
assert_empty @connection.foreign_keys(Astronaut.table_name)
|
||||
end
|
||||
end
|
||||
|
||||
class ForeignKeyChangeColumnTest < ActiveRecord::TestCase
|
||||
@ -156,9 +165,7 @@ class ForeignKeyChangeColumnWithSuffixTest < ActiveRecord::TestCase
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if ActiveRecord::Base.connection.supports_foreign_keys?
|
||||
module ActiveRecord
|
||||
class Migration
|
||||
class ForeignKeyTest < ActiveRecord::TestCase
|
||||
@ -197,7 +204,7 @@ def test_foreign_keys
|
||||
assert_equal "fk_test_has_pk", fk.to_table
|
||||
assert_equal "fk_id", fk.column
|
||||
assert_equal "pk_id", fk.primary_key
|
||||
assert_equal "fk_name", fk.name
|
||||
assert_equal "fk_name", fk.name unless current_adapter?(:SQLite3Adapter)
|
||||
end
|
||||
|
||||
def test_add_foreign_key_inferes_column
|
||||
@ -211,7 +218,7 @@ def test_add_foreign_key_inferes_column
|
||||
assert_equal "rockets", fk.to_table
|
||||
assert_equal "rocket_id", fk.column
|
||||
assert_equal "id", fk.primary_key
|
||||
assert_equal("fk_rails_78146ddd2e", fk.name)
|
||||
assert_equal "fk_rails_78146ddd2e", fk.name unless current_adapter?(:SQLite3Adapter)
|
||||
end
|
||||
|
||||
def test_add_foreign_key_with_column
|
||||
@ -225,7 +232,7 @@ def test_add_foreign_key_with_column
|
||||
assert_equal "rockets", fk.to_table
|
||||
assert_equal "rocket_id", fk.column
|
||||
assert_equal "id", fk.primary_key
|
||||
assert_equal("fk_rails_78146ddd2e", fk.name)
|
||||
assert_equal "fk_rails_78146ddd2e", fk.name unless current_adapter?(:SQLite3Adapter)
|
||||
end
|
||||
|
||||
def test_add_foreign_key_with_non_standard_primary_key
|
||||
@ -244,7 +251,7 @@ def test_add_foreign_key_with_non_standard_primary_key
|
||||
assert_equal "space_shuttles", fk.to_table
|
||||
assert_equal "pk", fk.primary_key
|
||||
ensure
|
||||
@connection.remove_foreign_key :astronauts, name: "custom_pk"
|
||||
@connection.remove_foreign_key :astronauts, name: "custom_pk", to_table: "space_shuttles"
|
||||
@connection.drop_table :space_shuttles
|
||||
end
|
||||
|
||||
@ -318,6 +325,8 @@ def test_foreign_key_exists_by_column
|
||||
end
|
||||
|
||||
def test_foreign_key_exists_by_name
|
||||
skip if current_adapter?(:SQLite3Adapter)
|
||||
|
||||
@connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", name: "fancy_named_fk"
|
||||
|
||||
assert @connection.foreign_key_exists?(:astronauts, name: "fancy_named_fk")
|
||||
@ -349,6 +358,8 @@ def test_remove_foreign_key_by_symbol_column
|
||||
end
|
||||
|
||||
def test_remove_foreign_key_by_name
|
||||
skip if current_adapter?(:SQLite3Adapter)
|
||||
|
||||
@connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", name: "fancy_named_fk"
|
||||
|
||||
assert_equal 1, @connection.foreign_keys("astronauts").size
|
||||
@ -357,9 +368,10 @@ def test_remove_foreign_key_by_name
|
||||
end
|
||||
|
||||
def test_remove_foreign_non_existing_foreign_key_raises
|
||||
assert_raises ArgumentError do
|
||||
e = assert_raises ArgumentError do
|
||||
@connection.remove_foreign_key :astronauts, :rockets
|
||||
end
|
||||
assert_equal "Table 'astronauts' has no foreign key for rockets", e.message
|
||||
end
|
||||
|
||||
if ActiveRecord::Base.connection.supports_validate_constraints?
|
||||
@ -438,7 +450,11 @@ def test_schema_dumping
|
||||
|
||||
def test_schema_dumping_with_options
|
||||
output = dump_table_schema "fk_test_has_fk"
|
||||
assert_match %r{\s+add_foreign_key "fk_test_has_fk", "fk_test_has_pk", column: "fk_id", primary_key: "pk_id", name: "fk_name"$}, output
|
||||
if current_adapter?(:SQLite3Adapter)
|
||||
assert_match %r{\s+add_foreign_key "fk_test_has_fk", "fk_test_has_pk", column: "fk_id", primary_key: "pk_id"$}, output
|
||||
else
|
||||
assert_match %r{\s+add_foreign_key "fk_test_has_fk", "fk_test_has_pk", column: "fk_id", primary_key: "pk_id", name: "fk_name"$}, output
|
||||
end
|
||||
end
|
||||
|
||||
def test_schema_dumping_with_custom_fk_ignore_pattern
|
||||
@ -492,7 +508,7 @@ def test_foreign_key_constraint_is_not_cached_incorrectly
|
||||
end
|
||||
|
||||
class CreateSchoolsAndClassesMigration < ActiveRecord::Migration::Current
|
||||
def change
|
||||
def up
|
||||
create_table(:schools)
|
||||
|
||||
create_table(:classes) do |t|
|
||||
@ -500,6 +516,11 @@ def change
|
||||
end
|
||||
add_foreign_key :classes, :schools
|
||||
end
|
||||
|
||||
def down
|
||||
drop_table :classes, if_exists: true
|
||||
drop_table :schools, if_exists: true
|
||||
end
|
||||
end
|
||||
|
||||
def test_add_foreign_key_with_prefix
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
require "cases/helper"
|
||||
|
||||
if ActiveRecord::Base.connection.supports_foreign_keys_in_create?
|
||||
if ActiveRecord::Base.connection.supports_foreign_keys?
|
||||
module ActiveRecord
|
||||
class Migration
|
||||
class ReferencesForeignKeyInCreateTest < ActiveRecord::TestCase
|
||||
@ -65,9 +65,7 @@ class ReferencesForeignKeyInCreateTest < ActiveRecord::TestCase
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if ActiveRecord::Base.connection.supports_foreign_keys?
|
||||
module ActiveRecord
|
||||
class Migration
|
||||
class ReferencesForeignKeyTest < ActiveRecord::TestCase
|
||||
@ -172,13 +170,18 @@ class ReferencesForeignKeyTest < ActiveRecord::TestCase
|
||||
end
|
||||
|
||||
class CreateDogsMigration < ActiveRecord::Migration::Current
|
||||
def change
|
||||
def up
|
||||
create_table :dog_owners
|
||||
|
||||
create_table :dogs do |t|
|
||||
t.references :dog_owner, foreign_key: true
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
drop_table :dogs, if_exists: true
|
||||
drop_table :dog_owners, if_exists: true
|
||||
end
|
||||
end
|
||||
|
||||
def test_references_foreign_key_with_prefix
|
||||
|
Loading…
Reference in New Issue
Block a user