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:
Ryuta Kamizono 2019-02-10 22:49:08 +09:00
parent d87afbf46f
commit da5843436b
11 changed files with 83 additions and 21 deletions

View File

@ -42,4 +42,5 @@ ActiveRecord::Schema.define(version: 2018_02_12_164506) do
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

View File

@ -55,4 +55,5 @@ ActiveRecord::Schema.define(version: 2018_10_03_185713) do
t.datetime "updated_at", precision: 6, null: false
end
add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
end

View File

@ -15,7 +15,7 @@ module ActiveRecord
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 @@ module ActiveRecord
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

View File

@ -102,7 +102,7 @@ module ActiveRecord
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)

View File

@ -335,6 +335,7 @@ module ActiveRecord
def supports_foreign_keys_in_create?
supports_foreign_keys?
end
deprecate :supports_foreign_keys_in_create?
# Does this adapter support views?
def supports_views?

View File

@ -52,6 +52,34 @@ module ActiveRecord
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

View File

@ -121,7 +121,7 @@ module ActiveRecord
true
end
def supports_foreign_keys_in_create?
def supports_foreign_keys?
true
end
@ -424,9 +424,8 @@ module ActiveRecord
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] || {}

View File

@ -348,6 +348,10 @@ module ActiveRecord
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

View File

@ -462,7 +462,11 @@ module ActiveRecord
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

View File

@ -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 @@ if ActiveRecord::Base.connection.supports_foreign_keys_in_create?
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 @@ if ActiveRecord::Base.connection.supports_foreign_keys_in_create?
end
end
end
end
if ActiveRecord::Base.connection.supports_foreign_keys?
module ActiveRecord
class Migration
class ForeignKeyTest < ActiveRecord::TestCase
@ -197,7 +204,7 @@ if ActiveRecord::Base.connection.supports_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 @@ if ActiveRecord::Base.connection.supports_foreign_keys?
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 @@ if ActiveRecord::Base.connection.supports_foreign_keys?
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 @@ if ActiveRecord::Base.connection.supports_foreign_keys?
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 @@ if ActiveRecord::Base.connection.supports_foreign_keys?
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 @@ if ActiveRecord::Base.connection.supports_foreign_keys?
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 @@ if ActiveRecord::Base.connection.supports_foreign_keys?
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 @@ if ActiveRecord::Base.connection.supports_foreign_keys?
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 @@ if ActiveRecord::Base.connection.supports_foreign_keys?
end
class CreateSchoolsAndClassesMigration < ActiveRecord::Migration::Current
def change
def up
create_table(:schools)
create_table(:classes) do |t|
@ -500,6 +516,11 @@ if ActiveRecord::Base.connection.supports_foreign_keys?
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

View File

@ -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 @@ if ActiveRecord::Base.connection.supports_foreign_keys_in_create?
end
end
end
end
if ActiveRecord::Base.connection.supports_foreign_keys?
module ActiveRecord
class Migration
class ReferencesForeignKeyTest < ActiveRecord::TestCase
@ -172,13 +170,18 @@ if ActiveRecord::Base.connection.supports_foreign_keys?
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