diff options
Diffstat (limited to 'activerecord')
13 files changed, 169 insertions, 87 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 5bba673dee..3438c04245 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,7 @@ +* Raise `ActiveRecord::InvalidForeignKey` when a foreign key constraint fails on Sqlite3. + + *Ryuta Kamizono* + * Add the touch option to ActiveRecord#increment! and decrement! *Hiroaki Izu* diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb index 322684672f..807df2184a 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb @@ -15,9 +15,9 @@ 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?, :foreign_key_options, to: :@conn + :options_include_default?, :supports_indexes_in_create?, :supports_foreign_keys_in_create?, :foreign_key_options, to: :@conn private :quote_column_name, :quote_table_name, :quote_default_expression, :type_to_sql, - :options_include_default?, :supports_indexes_in_create?, :supports_foreign_keys?, :foreign_key_options + :options_include_default?, :supports_indexes_in_create?, :supports_foreign_keys_in_create?, :foreign_key_options private @@ -49,7 +49,7 @@ module ActiveRecord statements.concat(o.indexes.map { |column_name, options| index_in_create(o.name, column_name, options) }) end - if supports_foreign_keys? + if supports_foreign_keys_in_create? statements.concat(o.foreign_keys.map { |to_table, options| foreign_key_in_create(o.name, to_table, options) }) end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index cf2269bf12..348396de72 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -310,6 +310,12 @@ module ActiveRecord false end + # Does this adapter support creating foreign key constraints + # in the same statement as creating the table? + def supports_foreign_keys_in_create? + supports_foreign_keys? + end + # Does this adapter support views? def supports_views? false diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index ec44d020c2..ca6de37a6b 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -95,6 +95,8 @@ module ActiveRecord @active = nil @statements = StatementPool.new(self.class.type_cast_config_to_integer(config[:statement_limit])) + + configure_connection end def supports_ddl_transactions? @@ -128,6 +130,10 @@ module ActiveRecord true end + def supports_foreign_keys_in_create? + sqlite_version >= "3.6.19" + end + def supports_views? true end @@ -185,6 +191,19 @@ module ActiveRecord true 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 ====================================== #++ @@ -424,6 +443,19 @@ module ActiveRecord rename_column_indexes(table_name, column.name, new_column_name) end + def foreign_keys(table_name) + fk_info = select_all("PRAGMA foreign_key_list(#{quote(table_name)})", "SCHEMA") + fk_info.map do |row| + options = { + column: row["from"], + primary_key: row["to"], + on_delete: extract_foreign_key_action(row["on_delete"]), + on_update: extract_foreign_key_action(row["on_update"]) + } + ForeignKeyDefinition.new(table_name, row["table"], options) + end + end + private def table_structure(table_name) @@ -525,6 +557,8 @@ module ActiveRecord RecordNotUnique.new(message) when /.* may not be NULL/, /NOT NULL constraint failed: .*/ NotNullViolation.new(message) + when /FOREIGN KEY constraint failed/i + InvalidForeignKey.new(message) else super end @@ -574,6 +608,18 @@ module ActiveRecord def create_table_definition(*args) SQLite3::TableDefinition.new(*args) end + + def extract_foreign_key_action(specifier) + case specifier + when "CASCADE"; :cascade + when "SET NULL"; :nullify + when "RESTRICT"; :restrict + end + end + + def configure_connection + execute("PRAGMA foreign_keys = ON", "SCHEMA") + end end end end diff --git a/activerecord/test/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb index 11fcbfadb1..a2b7d53205 100644 --- a/activerecord/test/cases/adapter_test.rb +++ b/activerecord/test/cases/adapter_test.rb @@ -184,34 +184,6 @@ module ActiveRecord end 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 error = assert_raises(ActiveRecord::ValueTooLong) do Event.create(title: "abcdefgh") @@ -229,23 +201,6 @@ module ActiveRecord 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 result = @connection.select_all "SELECT * FROM posts" assert result.is_a?(ActiveRecord::Result) @@ -289,6 +244,59 @@ module ActiveRecord 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 self.use_transactional_tests = false diff --git a/activerecord/test/cases/date_time_precision_test.rb b/activerecord/test/cases/date_time_precision_test.rb index a1c3c5af9c..e4a2f9ee17 100644 --- a/activerecord/test/cases/date_time_precision_test.rb +++ b/activerecord/test/cases/date_time_precision_test.rb @@ -73,7 +73,7 @@ if subsecond_precision_supported? assert_match %r{t\.datetime\s+"updated_at",\s+precision: 6,\s+null: false$}, output end - if current_adapter?(:PostgreSQLAdapter) + if current_adapter?(:PostgreSQLAdapter, :SQLServerAdapter) def test_datetime_precision_with_zero_should_be_dumped @connection.create_table(:foos, force: true) do |t| t.timestamps precision: 0 diff --git a/activerecord/test/cases/migration/foreign_key_test.rb b/activerecord/test/cases/migration/foreign_key_test.rb index 9be6667aa1..96e775a58b 100644 --- a/activerecord/test/cases/migration/foreign_key_test.rb +++ b/activerecord/test/cases/migration/foreign_key_test.rb @@ -2,6 +2,26 @@ require "cases/helper" require "support/ddl_helper" require "support/schema_dumping_helper" +if ActiveRecord::Base.connection.supports_foreign_keys_in_create? + module ActiveRecord + class Migration + class ForeignKeyInCreateTest < ActiveRecord::TestCase + def test_foreign_keys + foreign_keys = ActiveRecord::Base.connection.foreign_keys("fk_test_has_fk") + assert_equal 1, foreign_keys.size + + fk = foreign_keys.first + assert_equal "fk_test_has_fk", fk.from_table + 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 unless current_adapter?(:SQLite3Adapter) + end + end + end + end +end + if ActiveRecord::Base.connection.supports_foreign_keys? module ActiveRecord class Migration @@ -29,10 +49,8 @@ if ActiveRecord::Base.connection.supports_foreign_keys? end teardown do - if defined?(@connection) - @connection.drop_table "astronauts", if_exists: true - @connection.drop_table "rockets", if_exists: true - end + @connection.drop_table "astronauts", if_exists: true + @connection.drop_table "rockets", if_exists: true end def test_foreign_keys @@ -305,9 +323,11 @@ else @connection.remove_foreign_key :clubs, :categories end - def test_foreign_keys_should_raise_not_implemented - assert_raises NotImplementedError do - @connection.foreign_keys("clubs") + unless current_adapter?(:SQLite3Adapter) + def test_foreign_keys_should_raise_not_implemented + assert_raises NotImplementedError do + @connection.foreign_keys("clubs") + end end end end diff --git a/activerecord/test/cases/migration/references_foreign_key_test.rb b/activerecord/test/cases/migration/references_foreign_key_test.rb index 4957ab8b3d..560adcbfed 100644 --- a/activerecord/test/cases/migration/references_foreign_key_test.rb +++ b/activerecord/test/cases/migration/references_foreign_key_test.rb @@ -1,9 +1,9 @@ require "cases/helper" -if ActiveRecord::Base.connection.supports_foreign_keys? +if ActiveRecord::Base.connection.supports_foreign_keys_in_create? module ActiveRecord class Migration - class ReferencesForeignKeyTest < ActiveRecord::TestCase + class ReferencesForeignKeyInCreateTest < ActiveRecord::TestCase setup do @connection = ActiveRecord::Base.connection @connection.create_table(:testing_parents, force: true) @@ -61,6 +61,24 @@ if ActiveRecord::Base.connection.supports_foreign_keys? assert_equal([["testings", "testing_parents", "parent_id"]], fks.map { |fk| [fk.from_table, fk.to_table, fk.column] }) end + end + end + end +end + +if ActiveRecord::Base.connection.supports_foreign_keys? + module ActiveRecord + class Migration + class ReferencesForeignKeyTest < ActiveRecord::TestCase + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table(:testing_parents, force: true) + end + + teardown do + @connection.drop_table "testings", if_exists: true + @connection.drop_table "testing_parents", if_exists: true + end test "foreign keys cannot be added to polymorphic relations when creating the table" do @connection.create_table :testings do |t| diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index bea78d2a95..34c5f356b8 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -343,7 +343,7 @@ class SchemaDumperTest < ActiveRecord::TestCase t.column :name, :string t.column :owner_id, :bigint t.index [:name] - t.foreign_key :dog_owners, column: "owner_id" if supports_foreign_keys? + t.foreign_key :dog_owners, column: "owner_id" end end def down diff --git a/activerecord/test/cases/time_precision_test.rb b/activerecord/test/cases/time_precision_test.rb index 03f6c234e8..09c585167e 100644 --- a/activerecord/test/cases/time_precision_test.rb +++ b/activerecord/test/cases/time_precision_test.rb @@ -68,7 +68,7 @@ if subsecond_precision_supported? assert_match %r{t\.time\s+"finish",\s+precision: 6$}, output end - if current_adapter?(:PostgreSQLAdapter) + if current_adapter?(:PostgreSQLAdapter, :SQLServerAdapter) def test_time_precision_with_zero_should_be_dumped @connection.create_table(:foos, force: true) do |t| t.time :start, precision: 0 diff --git a/activerecord/test/cases/view_test.rb b/activerecord/test/cases/view_test.rb index d055968a56..07288568e8 100644 --- a/activerecord/test/cases/view_test.rb +++ b/activerecord/test/cases/view_test.rb @@ -154,7 +154,7 @@ if ActiveRecord::Base.connection.supports_views? end # sqlite dose not support CREATE, INSERT, and DELETE for VIEW - if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter) + if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter, :SQLServerAdapter) class UpdateableViewTest < ActiveRecord::TestCase self.use_transactional_tests = false fixtures :books @@ -200,7 +200,7 @@ if ActiveRecord::Base.connection.supports_views? end end end - end # end of `if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter)` + end # end of `if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter, :SQLServerAdapter)` end # end of `if ActiveRecord::Base.connection.supports_views?` if ActiveRecord::Base.connection.respond_to?(:supports_materialized_views?) && diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index ba6f5de894..b38b9661b3 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -1005,16 +1005,14 @@ ActiveRecord::Schema.define do create_table :records, force: true do |t| end - if supports_foreign_keys? - # fk_test_has_fk should be before fk_test_has_pk - create_table :fk_test_has_fk, force: true do |t| - t.bigint :fk_id, null: false + disable_referential_integrity do + create_table :fk_test_has_pk, primary_key: "pk_id", force: :cascade do |t| end - create_table :fk_test_has_pk, force: true, primary_key: "pk_id" do |t| + create_table :fk_test_has_fk, force: true do |t| + t.references :fk, null: false + t.foreign_key :fk_test_has_pk, column: "fk_id", name: "fk_name", primary_key: "pk_id" end - - add_foreign_key :fk_test_has_fk, :fk_test_has_pk, column: "fk_id", name: "fk_name", primary_key: "pk_id" end create_table :overloaded_types, force: true do |t| diff --git a/activerecord/test/schema/sqlite_specific_schema.rb b/activerecord/test/schema/sqlite_specific_schema.rb deleted file mode 100644 index cc7c36fe2b..0000000000 --- a/activerecord/test/schema/sqlite_specific_schema.rb +++ /dev/null @@ -1,18 +0,0 @@ -ActiveRecord::Schema.define do - execute "DROP TABLE fk_test_has_fk" rescue nil - execute "DROP TABLE fk_test_has_pk" rescue nil - execute <<_SQL - CREATE TABLE 'fk_test_has_pk' ( - 'pk_id' INTEGER NOT NULL PRIMARY KEY - ); -_SQL - - execute <<_SQL - CREATE TABLE 'fk_test_has_fk' ( - 'id' INTEGER NOT NULL PRIMARY KEY, - 'fk_id' INTEGER NOT NULL, - - FOREIGN KEY ('fk_id') REFERENCES 'fk_test_has_pk'('pk_id') - ); -_SQL -end |