1
0
Fork 0
mirror of https://github.com/rails/rails.git synced 2022-11-09 12:12:34 -05:00

Translate Foreign Key violation to the specific exception for SQLite3 adapter

Raise `ActiveRecord::InvalidForeignKey` when a record cannot be inserted
or updated because it references a non-existent record for SQLite3
adapter.
This commit is contained in:
Ryuta Kamizono 2016-06-21 08:39:05 +09:00
parent f5d66cb3e5
commit 974f5fbbc2
2 changed files with 74 additions and 45 deletions

View file

@ -95,6 +95,8 @@ module ActiveRecord
@active = nil @active = nil
@statements = StatementPool.new(self.class.type_cast_config_to_integer(config[:statement_limit])) @statements = StatementPool.new(self.class.type_cast_config_to_integer(config[:statement_limit]))
configure_connection
end end
def supports_ddl_transactions? def supports_ddl_transactions?
@ -185,6 +187,19 @@ module ActiveRecord
true true
end end
# REFERENTIAL INTEGRITY ====================================
def disable_referential_integrity # :nodoc:
old = select_value("PRAGMA foreign_keys")
begin
execute("PRAGMA foreign_keys = OFF")
yield
ensure
execute("PRAGMA foreign_keys = #{old}")
end
end
#-- #--
# DATABASE STATEMENTS ====================================== # DATABASE STATEMENTS ======================================
#++ #++
@ -525,6 +540,8 @@ module ActiveRecord
RecordNotUnique.new(message) RecordNotUnique.new(message)
when /.* may not be NULL/, /NOT NULL constraint failed: .*/ when /.* may not be NULL/, /NOT NULL constraint failed: .*/
NotNullViolation.new(message) NotNullViolation.new(message)
when /FOREIGN KEY constraint failed/i
InvalidForeignKey.new(message)
else else
super super
end end
@ -574,6 +591,10 @@ module ActiveRecord
def create_table_definition(*args) def create_table_definition(*args)
SQLite3::TableDefinition.new(*args) SQLite3::TableDefinition.new(*args)
end end
def configure_connection
execute("PRAGMA foreign_keys = ON", "SCHEMA")
end
end end
end end
end end

View file

@ -185,34 +185,6 @@ module ActiveRecord
end end
unless current_adapter?(:SQLite3Adapter) unless current_adapter?(:SQLite3Adapter)
def test_foreign_key_violations_are_translated_to_specific_exception
error = assert_raises(ActiveRecord::InvalidForeignKey) do
# Oracle adapter uses prefetched primary key values from sequence and passes them to connection adapter insert method
if @connection.prefetch_primary_key?
id_value = @connection.next_sequence_value(@connection.default_sequence_name("fk_test_has_fk", "id"))
@connection.execute "INSERT INTO fk_test_has_fk (id, fk_id) VALUES (#{id_value},0)"
else
@connection.execute "INSERT INTO fk_test_has_fk (fk_id) VALUES (0)"
end
end
assert_not_nil error.cause
end
def test_foreign_key_violations_are_translated_to_specific_exception_with_validate_false
klass_has_fk = Class.new(ActiveRecord::Base) do
self.table_name = "fk_test_has_fk"
end
error = assert_raises(ActiveRecord::InvalidForeignKey) do
has_fk = klass_has_fk.new
has_fk.fk_id = 1231231231
has_fk.save(validate: false)
end
assert_not_nil error.cause
end
def test_value_limit_violations_are_translated_to_specific_exception def test_value_limit_violations_are_translated_to_specific_exception
error = assert_raises(ActiveRecord::ValueTooLong) do error = assert_raises(ActiveRecord::ValueTooLong) do
Event.create(title: "abcdefgh") Event.create(title: "abcdefgh")
@ -230,23 +202,6 @@ module ActiveRecord
end end
end end
def test_disable_referential_integrity
assert_nothing_raised do
@connection.disable_referential_integrity do
# Oracle adapter uses prefetched primary key values from sequence and passes them to connection adapter insert method
if @connection.prefetch_primary_key?
id_value = @connection.next_sequence_value(@connection.default_sequence_name("fk_test_has_fk", "id"))
@connection.execute "INSERT INTO fk_test_has_fk (id, fk_id) VALUES (#{id_value},0)"
else
@connection.execute "INSERT INTO fk_test_has_fk (fk_id) VALUES (0)"
end
# should delete created record as otherwise disable_referential_integrity will try to enable constraints after executed block
# and will fail (at least on Oracle)
@connection.execute "DELETE FROM fk_test_has_fk"
end
end
end
def test_select_all_always_return_activerecord_result def test_select_all_always_return_activerecord_result
result = @connection.select_all "SELECT * FROM posts" result = @connection.select_all "SELECT * FROM posts"
assert result.is_a?(ActiveRecord::Result) assert result.is_a?(ActiveRecord::Result)
@ -290,6 +245,59 @@ module ActiveRecord
end end
end end
class AdapterForeignKeyTest < ActiveRecord::TestCase
self.use_transactional_tests = false
def setup
@connection = ActiveRecord::Base.connection
end
def test_foreign_key_violations_are_translated_to_specific_exception_with_validate_false
klass_has_fk = Class.new(ActiveRecord::Base) do
self.table_name = "fk_test_has_fk"
end
error = assert_raises(ActiveRecord::InvalidForeignKey) do
has_fk = klass_has_fk.new
has_fk.fk_id = 1231231231
has_fk.save(validate: false)
end
assert_not_nil error.cause
end
def test_foreign_key_violations_are_translated_to_specific_exception
error = assert_raises(ActiveRecord::InvalidForeignKey) do
insert_into_fk_test_has_fk
end
assert_not_nil error.cause
end
def test_disable_referential_integrity
assert_nothing_raised do
@connection.disable_referential_integrity do
insert_into_fk_test_has_fk
# should delete created record as otherwise disable_referential_integrity will try to enable constraints
# after executed block and will fail (at least on Oracle)
@connection.execute "DELETE FROM fk_test_has_fk"
end
end
end
private
def insert_into_fk_test_has_fk
# Oracle adapter uses prefetched primary key values from sequence and passes them to connection adapter insert method
if @connection.prefetch_primary_key?
id_value = @connection.next_sequence_value(@connection.default_sequence_name("fk_test_has_fk", "id"))
@connection.execute "INSERT INTO fk_test_has_fk (id,fk_id) VALUES (#{id_value},0)"
else
@connection.execute "INSERT INTO fk_test_has_fk (fk_id) VALUES (0)"
end
end
end
class AdapterTestWithoutTransaction < ActiveRecord::TestCase class AdapterTestWithoutTransaction < ActiveRecord::TestCase
self.use_transactional_tests = false self.use_transactional_tests = false