提交 45881b0a 编写于 作者: Y Yasuo Honda

Disable foreign keys during `alter_table` for sqlite3 adapter

Unlike other databases, changing SQLite3 table definitions need to create a temporary table.
While changing table operations, the original table needs dropped which caused
`SQLite3::ConstraintException: FOREIGN KEY constraint failed` if the table is referenced by foreign keys.
This pull request disables foreign keys by `disable_referential_integrity`.

Also `disable_referential_integrity` method needs to execute `defer_foreign_keys = ON`
to defer re-enabling foreign keys until the transaction is committed.

https://www.sqlite.org/pragma.html#pragma_defer_foreign_keys

Fixes #31988

- This `defer_foreign_keys = ON` has been supported since SQLite 3.8.0
https://www.sqlite.org/releaselog/3_8_0.html and Rails 6 requires SQLite 3.8 #32923 now

- <Models>.reset_column_information added to address `ActiveModel::UnknownAttributeError`

```
Error:
ActiveRecord::Migration::ForeignKeyChangeColumnTest#test_change_column_of_parent_table:
ActiveModel::UnknownAttributeError: unknown attribute 'name' for ActiveRecord::Migration::ForeignKeyChangeColumnTest::Post.
```
上级 054893d5
......@@ -187,13 +187,16 @@ def supports_explain?
# REFERENTIAL INTEGRITY ====================================
def disable_referential_integrity # :nodoc:
old = query_value("PRAGMA foreign_keys")
old_foreign_keys = query_value("PRAGMA foreign_keys")
old_defer_foreign_keys = query_value("PRAGMA defer_foreign_keys")
begin
execute("PRAGMA defer_foreign_keys = ON")
execute("PRAGMA foreign_keys = OFF")
yield
ensure
execute("PRAGMA foreign_keys = #{old}")
execute("PRAGMA defer_foreign_keys = #{old_defer_foreign_keys}")
execute("PRAGMA foreign_keys = #{old_foreign_keys}")
end
end
......@@ -407,9 +410,11 @@ def alter_table(table_name, options = {})
caller = lambda { |definition| yield definition if block_given? }
transaction do
move_table(table_name, altered_table_name,
options.merge(temporary: true))
move_table(altered_table_name, table_name, &caller)
disable_referential_integrity do
move_table(table_name, altered_table_name,
options.merge(temporary: true))
move_table(altered_table_name, table_name, &caller)
end
end
end
......
......@@ -19,6 +19,52 @@ def test_foreign_keys
assert_equal "fk_name", fk.name unless current_adapter?(:SQLite3Adapter)
end
end
class ForeignKeyChangeColumnTest < ActiveRecord::TestCase
self.use_transactional_tests = false
class Rocket < ActiveRecord::Base
has_many :astronauts
end
class Astronaut < ActiveRecord::Base
belongs_to :rocket
end
setup do
@connection = ActiveRecord::Base.connection
@connection.create_table "rockets", force: true do |t|
t.string :name
end
@connection.create_table "astronauts", force: true do |t|
t.string :name
t.references :rocket, foreign_key: true
end
Rocket.reset_column_information
Astronaut.reset_column_information
end
teardown do
@connection.drop_table "astronauts", if_exists: true
@connection.drop_table "rockets", if_exists: true
Rocket.reset_column_information
Astronaut.reset_column_information
end
def test_change_column_of_parent_table
foreign_keys = ActiveRecord::Base.connection.foreign_keys("astronauts")
rocket = Rocket.create!(name: "myrocket")
rocket.astronauts << Astronaut.create!
@connection.change_column_null :rockets, :name, false
fk = foreign_keys.first
assert_equal "myrocket", Rocket.first.name
assert_equal "astronauts", fk.from_table
assert_equal "rockets", fk.to_table
end
end
end
end
end
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册