From e2ef25710682d884b2e6f5e99d47f18eb7083c68 Mon Sep 17 00:00:00 2001 From: Yves Senn Date: Fri, 6 Jun 2014 17:24:46 +0200 Subject: fk: `add_foreign_key` and `remove_foreign_key` for PostgreSQL adapter. --- .../connection_adapters/abstract_adapter.rb | 5 +++ .../postgresql/schema_statements.rb | 27 ++++++++++++ .../connection_adapters/postgresql_adapter.rb | 4 ++ .../test/cases/migration/foreign_key_test.rb | 49 ++++++++++++++++++++++ 4 files changed, 85 insertions(+) create mode 100644 activerecord/test/cases/migration/foreign_key_test.rb diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index cc494a7f40..294ed6d7bf 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -233,6 +233,11 @@ module ActiveRecord false end + # Does this adapter support creating foreign key constraints? + def supports_foreign_keys? + false + end + # This is meant to be implemented by the adapters that support extensions def disable_extension(name) end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb index b2aeb3a058..f09ce113d6 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -448,6 +448,33 @@ module ActiveRecord execute "ALTER INDEX #{quote_column_name(old_name)} RENAME TO #{quote_table_name(new_name)}" end + def add_foreign_key(from_table, to_table, options = {}) + foreign_key_column = options.fetch(:column) + referenced_column = "id" + foreign_key_name = foreign_key_name(from_table, options) + execute <<-SQL +ALTER TABLE #{quote_table_name(from_table)} +ADD CONSTRAINT #{foreign_key_name} + FOREIGN KEY (#{quote_column_name(foreign_key_column)}) + REFERENCES #{quote_table_name(to_table)} (#{quote_column_name(referenced_column)}) + SQL + end + + def remove_foreign_key(from_table, options = {}) + foreign_key_name = foreign_key_name(from_table, options) + execute <<-SQL +ALTER TABLE #{quote_table_name(from_table)} +DROP CONSTRAINT #{foreign_key_name} + SQL + end + + def foreign_key_name(table_name, options) + options.fetch(:name) do + column_name = options.fetch(:column) + "#{table_name}_#{column_name}_fk" + end + end + def index_name_length 63 end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index be4ae47d09..34262cf91d 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -159,6 +159,10 @@ module ActiveRecord true end + def supports_foreign_keys? + true + end + def index_algorithms { concurrently: 'CONCURRENTLY' } end diff --git a/activerecord/test/cases/migration/foreign_key_test.rb b/activerecord/test/cases/migration/foreign_key_test.rb new file mode 100644 index 0000000000..978d1a8cf1 --- /dev/null +++ b/activerecord/test/cases/migration/foreign_key_test.rb @@ -0,0 +1,49 @@ +require 'cases/helper' + +if ActiveRecord::Base.connection.supports_foreign_keys? +module ActiveRecord + class Migration + class ForeignKeyTest < ActiveRecord::TestCase + class Rocket < ActiveRecord::Base + end + + class Astronaut < ActiveRecord::Base + end + + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table "rockets" do |t| + t.string :name + end + + @connection.create_table "astronauts" do |t| + t.string :name + t.references :rocket + end + end + + def test_add_foreign_key + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id" + + assert_raises ActiveRecord::InvalidForeignKey do + Astronaut.create rocket_id: 33 + end + end + + def test_remove_foreign_key + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id" + @connection.remove_foreign_key :astronauts, column: "rocket_id" + + Astronaut.create rocket_id: 33 + end + + def test_remove_foreign_key_by_name + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", name: "fancy_named_fk" + @connection.remove_foreign_key :astronauts, name: "fancy_named_fk" + + Astronaut.create rocket_id: 33 + end + end + end +end +end -- cgit v1.2.3 From 09b3a2847ca51d0e5dcebcb636d8770b19c397a7 Mon Sep 17 00:00:00 2001 From: Yves Senn Date: Tue, 10 Jun 2014 10:59:18 +0200 Subject: fk: add `foreign_keys` for PostgreSQL adapter. --- .../postgresql/schema_definitions.rb | 14 +++++++++ .../postgresql/schema_statements.rb | 24 +++++++++++++++ .../test/cases/migration/foreign_key_test.rb | 34 +++++++++++++++++----- 3 files changed, 65 insertions(+), 7 deletions(-) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb index 0867e5ef54..724e3fa1ee 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb @@ -89,6 +89,20 @@ module ActiveRecord attr_accessor :array end + class ForeignKeyDefinition < Struct.new(:from_table, :to_table, :options) + def name + options[:name] + end + + def column + options[:column] + end + + def primary_key + options[:primary_key] + end + end + class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition include ColumnMethods diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb index f09ce113d6..b87fb85ae2 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -448,6 +448,30 @@ module ActiveRecord execute "ALTER INDEX #{quote_column_name(old_name)} RENAME TO #{quote_table_name(new_name)}" end + def foreign_keys(table_name) + fk_info = select_all <<-SQL +SELECT t2.relname AS to_table, a1.attname AS column, a2.attname AS primary_key, c.conname AS name, c.confdeltype AS dependency +FROM pg_constraint c +JOIN pg_class t1 ON c.conrelid = t1.oid +JOIN pg_class t2 ON c.confrelid = t2.oid +JOIN pg_attribute a1 ON a1.attnum = c.conkey[1] AND a1.attrelid = t1.oid +JOIN pg_attribute a2 ON a2.attnum = c.confkey[1] AND a2.attrelid = t2.oid +JOIN pg_namespace t3 ON c.connamespace = t3.oid +WHERE c.contype = 'f' + AND t1.relname = #{quote(table_name)} + AND t3.nspname = ANY (current_schemas(false)) +ORDER BY c.conname + SQL + + fk_info.map do |row| + options = { + column: row['column'], + name: row['name'], + primary_key: row['primary_key'] } + ForeignKeyDefinition.new(table_name, row["to_table"], options) + end + end + def add_foreign_key(from_table, to_table, options = {}) foreign_key_column = options.fetch(:column) referenced_column = "id" diff --git a/activerecord/test/cases/migration/foreign_key_test.rb b/activerecord/test/cases/migration/foreign_key_test.rb index 978d1a8cf1..9c804c12d1 100644 --- a/activerecord/test/cases/migration/foreign_key_test.rb +++ b/activerecord/test/cases/migration/foreign_key_test.rb @@ -22,26 +22,46 @@ module ActiveRecord end end + def test_foreign_keys + foreign_keys = @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 "id", fk.primary_key + assert_equal "fk_name", fk.name + end + def test_add_foreign_key @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id" - assert_raises ActiveRecord::InvalidForeignKey do - Astronaut.create rocket_id: 33 - end + foreign_keys = @connection.foreign_keys("astronauts") + assert_equal 1, foreign_keys.size + + fk = foreign_keys.first + assert_equal "astronauts", fk.from_table + assert_equal "rockets", fk.to_table + assert_equal "rocket_id", fk.column + assert_equal "id", fk.primary_key + assert_equal "astronauts_rocket_id_fk", fk.name end def test_remove_foreign_key @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id" - @connection.remove_foreign_key :astronauts, column: "rocket_id" - Astronaut.create rocket_id: 33 + assert_equal 1, @connection.foreign_keys("astronauts").size + @connection.remove_foreign_key :astronauts, column: "rocket_id" + assert_equal [], @connection.foreign_keys("astronauts") end def test_remove_foreign_key_by_name @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", name: "fancy_named_fk" - @connection.remove_foreign_key :astronauts, name: "fancy_named_fk" - Astronaut.create rocket_id: 33 + assert_equal 1, @connection.foreign_keys("astronauts").size + @connection.remove_foreign_key :astronauts, name: "fancy_named_fk" + assert_equal [], @connection.foreign_keys("astronauts") end end end -- cgit v1.2.3 From 74b2fe4c0febe051cb48c7c25a565333ddf22bce Mon Sep 17 00:00:00 2001 From: Yves Senn Date: Tue, 10 Jun 2014 11:10:54 +0200 Subject: fk: `foreign_keys`, `add_foreign_key` and `remove_foreign_key` for MySQL --- .../abstract/schema_definitions.rb | 14 +++++++ .../abstract/schema_statements.rb | 7 ++++ .../connection_adapters/abstract_mysql_adapter.rb | 44 ++++++++++++++++++++++ .../postgresql/schema_definitions.rb | 14 ------- .../postgresql/schema_statements.rb | 7 ---- .../test/cases/migration/foreign_key_test.rb | 5 +++ 6 files changed, 70 insertions(+), 21 deletions(-) diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb index a9b3e9cfb9..b132543332 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -25,6 +25,20 @@ module ActiveRecord class ChangeColumnDefinition < Struct.new(:column, :type, :options) #:nodoc: end + class ForeignKeyDefinition < Struct.new(:from_table, :to_table, :options) + def name + options[:name] + end + + def column + options[:column] + end + + def primary_key + options[:primary_key] + end + end + # Represents the schema of an SQL table in an abstract way. This class # provides methods for manipulating the schema representation. # diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb index 22823a8c58..55e3fb4477 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -625,6 +625,13 @@ module ActiveRecord end alias :add_belongs_to :add_reference + def foreign_key_name(table_name, options) + options.fetch(:name) do + column_name = options.fetch(:column) + "#{table_name}_#{column_name}_fk" + end + end + # Removes the reference(s). Also removes a +type+ column if one exists. # remove_reference, remove_references and remove_belongs_to are acceptable. # diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb index def04dbed2..431db591e6 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -192,6 +192,10 @@ module ActiveRecord true end + def supports_foreign_keys? + true + end + def native_database_types NATIVE_DATABASE_TYPES end @@ -501,6 +505,46 @@ module ActiveRecord execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} #{index_using} ON #{quote_table_name(table_name)} (#{index_columns})#{index_options} #{index_algorithm}" end + def foreign_keys(table_name) + fk_info = select_all %{ + SELECT fk.referenced_table_name as 'to_table' + ,fk.referenced_column_name as 'primary_key' + ,fk.column_name as 'column' + ,fk.constraint_name as 'name' + FROM information_schema.key_column_usage fk + WHERE fk.referenced_column_name is not null + AND fk.table_schema = '#{@config[:database]}' + AND fk.table_name = '#{table_name}' + } + + create_table_info = select_one("SHOW CREATE TABLE #{quote_table_name(table_name)}")["Create Table"] + + fk_info.map do |row| + options = {column: row['column'], name: row['name'], primary_key: row['primary_key']} + ForeignKeyDefinition.new(table_name, row['to_table'], options) + end + end + + def add_foreign_key(from_table, to_table, options = {}) + foreign_key_column = options.fetch(:column) + referenced_column = "id" + foreign_key_name = foreign_key_name(from_table, options) + execute <<-SQL +ALTER TABLE #{quote_table_name(from_table)} +ADD CONSTRAINT #{foreign_key_name} +FOREIGN KEY (#{quote_column_name(foreign_key_column)}) +REFERENCES #{quote_table_name(to_table)} (#{quote_column_name(referenced_column)}) + SQL + end + + def remove_foreign_key(from_table, options = {}) + foreign_key_name = foreign_key_name(from_table, options) + execute <<-SQL +ALTER TABLE #{quote_table_name(from_table)} +DROP FOREIGN KEY #{foreign_key_name} + SQL + end + # Maps logical Rails types to MySQL-specific data types. def type_to_sql(type, limit = nil, precision = nil, scale = nil) case type.to_s diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb index 724e3fa1ee..0867e5ef54 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb @@ -89,20 +89,6 @@ module ActiveRecord attr_accessor :array end - class ForeignKeyDefinition < Struct.new(:from_table, :to_table, :options) - def name - options[:name] - end - - def column - options[:column] - end - - def primary_key - options[:primary_key] - end - end - class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition include ColumnMethods diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb index b87fb85ae2..7a93d9cde7 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -492,13 +492,6 @@ DROP CONSTRAINT #{foreign_key_name} SQL end - def foreign_key_name(table_name, options) - options.fetch(:name) do - column_name = options.fetch(:column) - "#{table_name}_#{column_name}_fk" - end - end - def index_name_length 63 end diff --git a/activerecord/test/cases/migration/foreign_key_test.rb b/activerecord/test/cases/migration/foreign_key_test.rb index 9c804c12d1..655393445d 100644 --- a/activerecord/test/cases/migration/foreign_key_test.rb +++ b/activerecord/test/cases/migration/foreign_key_test.rb @@ -22,6 +22,11 @@ module ActiveRecord end end + teardown do + @connection.execute "DROP TABLE IF EXISTS astronauts" + @connection.execute "DROP TABLE IF EXISTS rockets" + end + def test_foreign_keys foreign_keys = @connection.foreign_keys("fk_test_has_fk") assert_equal 1, foreign_keys.size -- cgit v1.2.3 From 1c170fdea2be04691c7daa8266084766fe963fff Mon Sep 17 00:00:00 2001 From: Yves Senn Date: Tue, 10 Jun 2014 11:50:37 +0200 Subject: fk: generalize using `AlterTable` and `SchemaCreation`. --- .../abstract/schema_creation.rb | 14 ++++++++ .../abstract/schema_definitions.rb | 12 +++++++ .../abstract/schema_statements.rb | 37 ++++++++++++++++++---- .../connection_adapters/abstract_mysql_adapter.rb | 24 +++----------- .../postgresql/schema_statements.rb | 20 ------------ .../test/cases/migration/foreign_key_test.rb | 6 ++-- 6 files changed, 64 insertions(+), 49 deletions(-) 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 47fe501752..ad62eab4d2 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb @@ -18,11 +18,25 @@ module ActiveRecord add_column_options!(sql, column_options(o)) end + def visit_AddForeignKey(o) + <<-SQL +ADD CONSTRAINT #{quote_column_name(o.name)} +FOREIGN KEY (#{quote_column_name(o.column)}) + REFERENCES #{quote_table_name(o.to_table)} (#{quote_column_name(o.primary_key)}) + SQL + end + + def visit_DropForeignKey(name) + "DROP CONSTRAINT #{name}" + end + private def visit_AlterTable(o) sql = "ALTER TABLE #{quote_table_name(o.name)} " sql << o.adds.map { |col| visit_AddColumn col }.join(' ') + sql << o.foreign_key_adds.map { |fk| visit_AddForeignKey fk }.join(' ') + sql << o.foreign_key_drops.map { |fk| visit_DropForeignKey fk }.join(' ') end def visit_ColumnDefinition(o) diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb index b132543332..c18ebf1014 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -317,14 +317,26 @@ module ActiveRecord class AlterTable # :nodoc: attr_reader :adds + attr_reader :foreign_key_adds + attr_reader :foreign_key_drops def initialize(td) @td = td @adds = [] + @foreign_key_adds = [] + @foreign_key_drops = [] end def name; @td.name; end + def add_foreign_key(to_table, options) + @foreign_key_adds << ForeignKeyDefinition.new(name, to_table, options) + end + + def drop_foreign_key(name) + @foreign_key_drops << name + end + def add_column(name, type, options) name = name.to_s type = type.to_sym diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb index 55e3fb4477..da6b15dad0 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -625,13 +625,6 @@ module ActiveRecord end alias :add_belongs_to :add_reference - def foreign_key_name(table_name, options) - options.fetch(:name) do - column_name = options.fetch(:column) - "#{table_name}_#{column_name}_fk" - end - end - # Removes the reference(s). Also removes a +type+ column if one exists. # remove_reference, remove_references and remove_belongs_to are acceptable. # @@ -649,6 +642,36 @@ module ActiveRecord end alias :remove_belongs_to :remove_reference + def foreign_keys(table_name) + raise NotImplementedError, "foreign_keys is not implemented" + end + + def add_foreign_key(from_table, to_table, options = {}) + options = { + column: options.fetch(:column), + primary_key: "id", + name: foreign_key_name(from_table, options) + } + at = create_alter_table from_table + at.add_foreign_key to_table, options + + execute schema_creation.accept at + end + + def remove_foreign_key(from_table, options = {}) + at = create_alter_table from_table + at.drop_foreign_key foreign_key_name(from_table, options) + + execute schema_creation.accept at + end + + def foreign_key_name(table_name, options) # :nodoc: + options.fetch(:name) do + column_name = options.fetch(:column) + "#{table_name}_#{column_name}_fk" + end + end + def dump_schema_information #:nodoc: sm_table = ActiveRecord::Migrator.schema_migrations_table_name diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb index 431db591e6..6ba226765c 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -10,6 +10,10 @@ module ActiveRecord add_column_position!(super, column_options(o)) end + def visit_DropForeignKey(name) + "DROP FOREIGN KEY #{name}" + end + private def visit_TableDefinition(o) @@ -525,26 +529,6 @@ module ActiveRecord end end - def add_foreign_key(from_table, to_table, options = {}) - foreign_key_column = options.fetch(:column) - referenced_column = "id" - foreign_key_name = foreign_key_name(from_table, options) - execute <<-SQL -ALTER TABLE #{quote_table_name(from_table)} -ADD CONSTRAINT #{foreign_key_name} -FOREIGN KEY (#{quote_column_name(foreign_key_column)}) -REFERENCES #{quote_table_name(to_table)} (#{quote_column_name(referenced_column)}) - SQL - end - - def remove_foreign_key(from_table, options = {}) - foreign_key_name = foreign_key_name(from_table, options) - execute <<-SQL -ALTER TABLE #{quote_table_name(from_table)} -DROP FOREIGN KEY #{foreign_key_name} - SQL - end - # Maps logical Rails types to MySQL-specific data types. def type_to_sql(type, limit = nil, precision = nil, scale = nil) case type.to_s diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb index 7a93d9cde7..c061337e71 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -472,26 +472,6 @@ ORDER BY c.conname end end - def add_foreign_key(from_table, to_table, options = {}) - foreign_key_column = options.fetch(:column) - referenced_column = "id" - foreign_key_name = foreign_key_name(from_table, options) - execute <<-SQL -ALTER TABLE #{quote_table_name(from_table)} -ADD CONSTRAINT #{foreign_key_name} - FOREIGN KEY (#{quote_column_name(foreign_key_column)}) - REFERENCES #{quote_table_name(to_table)} (#{quote_column_name(referenced_column)}) - SQL - end - - def remove_foreign_key(from_table, options = {}) - foreign_key_name = foreign_key_name(from_table, options) - execute <<-SQL -ALTER TABLE #{quote_table_name(from_table)} -DROP CONSTRAINT #{foreign_key_name} - SQL - end - def index_name_length 63 end diff --git a/activerecord/test/cases/migration/foreign_key_test.rb b/activerecord/test/cases/migration/foreign_key_test.rb index 655393445d..6ad595668f 100644 --- a/activerecord/test/cases/migration/foreign_key_test.rb +++ b/activerecord/test/cases/migration/foreign_key_test.rb @@ -23,8 +23,10 @@ module ActiveRecord end teardown do - @connection.execute "DROP TABLE IF EXISTS astronauts" - @connection.execute "DROP TABLE IF EXISTS rockets" + if defined?(@connection) + @connection.execute "DROP TABLE IF EXISTS astronauts" + @connection.execute "DROP TABLE IF EXISTS rockets" + end end def test_foreign_keys -- cgit v1.2.3 From a48b675d54101b048228d1011ffa426c2b7fe94d Mon Sep 17 00:00:00 2001 From: Yves Senn Date: Tue, 10 Jun 2014 12:09:58 +0200 Subject: fk: `:primary_key` option for non-standard pk's. --- .../abstract/schema_statements.rb | 4 +++- .../test/cases/migration/foreign_key_test.rb | 20 ++++++++++++++++++++ activerecord/test/support/ddl_helper.rb | 4 ++-- 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb index da6b15dad0..db04ebc802 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -647,9 +647,11 @@ module ActiveRecord end def add_foreign_key(from_table, to_table, options = {}) + primary_key = options.fetch(:primary_key, "id") + options = { column: options.fetch(:column), - primary_key: "id", + primary_key: primary_key, name: foreign_key_name(from_table, options) } at = create_alter_table from_table diff --git a/activerecord/test/cases/migration/foreign_key_test.rb b/activerecord/test/cases/migration/foreign_key_test.rb index 6ad595668f..2b99ab6ecd 100644 --- a/activerecord/test/cases/migration/foreign_key_test.rb +++ b/activerecord/test/cases/migration/foreign_key_test.rb @@ -1,9 +1,12 @@ require 'cases/helper' +require 'support/ddl_helper' if ActiveRecord::Base.connection.supports_foreign_keys? module ActiveRecord class Migration class ForeignKeyTest < ActiveRecord::TestCase + include DdlHelper + class Rocket < ActiveRecord::Base end @@ -55,6 +58,23 @@ module ActiveRecord assert_equal "astronauts_rocket_id_fk", fk.name end + def test_add_foreign_key_with_non_standard_primary_key + with_example_table @connection, "space_shuttles", "pk integer PRIMARY KEY" do + @connection.add_foreign_key(:astronauts, :space_shuttles, + column: "rocket_id", primary_key: "pk", name: "custom_pk") + + foreign_keys = @connection.foreign_keys("astronauts") + assert_equal 1, foreign_keys.size + + fk = foreign_keys.first + assert_equal "astronauts", fk.from_table + assert_equal "space_shuttles", fk.to_table + assert_equal "pk", fk.primary_key + + @connection.remove_foreign_key :astronauts, name: "custom_pk" + end + end + def test_remove_foreign_key @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id" diff --git a/activerecord/test/support/ddl_helper.rb b/activerecord/test/support/ddl_helper.rb index 0107babaaf..43cb235e01 100644 --- a/activerecord/test/support/ddl_helper.rb +++ b/activerecord/test/support/ddl_helper.rb @@ -1,8 +1,8 @@ module DdlHelper def with_example_table(connection, table_name, definition = nil) - connection.exec_query("CREATE TABLE #{table_name}(#{definition})") + connection.execute("CREATE TABLE #{table_name}(#{definition})") yield ensure - connection.exec_query("DROP TABLE #{table_name}") + connection.execute("DROP TABLE #{table_name}") end end -- cgit v1.2.3 From 69c711f38cac85e9c8bdbe286591bf88ef720bfa Mon Sep 17 00:00:00 2001 From: Yves Senn Date: Tue, 10 Jun 2014 13:28:38 +0200 Subject: fk: dump foreign keys to schema.rb respect `table_name_prefix` and `table_name_suffix`. --- activerecord/lib/active_record/migration.rb | 4 +++- activerecord/lib/active_record/schema_dumper.rb | 22 ++++++++++++++++++++++ .../test/cases/migration/foreign_key_test.rb | 7 +++++++ activerecord/test/cases/schema_dumper_test.rb | 12 +++++++++++- 4 files changed, 43 insertions(+), 2 deletions(-) diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index 01c001e692..12eaf36156 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -645,7 +645,9 @@ module ActiveRecord unless @connection.respond_to? :revert unless arguments.empty? || [:execute, :enable_extension, :disable_extension].include?(method) arguments[0] = proper_table_name(arguments.first, table_name_options) - arguments[1] = proper_table_name(arguments.second, table_name_options) if method == :rename_table + if [:rename_table, :add_foreign_key].include?(method) + arguments[1] = proper_table_name(arguments.second, table_name_options) + end end end return super unless connection.respond_to?(method) diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index e055d571ab..3db36458db 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -171,6 +171,8 @@ HEADER indexes(table, tbl) + foreign_keys(table, tbl) + tbl.rewind stream.print tbl.read rescue => e @@ -212,6 +214,26 @@ HEADER end end + def foreign_keys(table, stream) + return unless @connection.supports_foreign_keys? + + if (foreign_keys = @connection.foreign_keys(table)).any? + add_foreign_key_statements = foreign_keys.map do |foreign_key| + parts = [ + 'add_foreign_key ' + remove_prefix_and_suffix(foreign_key.from_table).inspect, + remove_prefix_and_suffix(foreign_key.to_table).inspect, + 'column: ' + foreign_key.column.inspect, + 'primary_key: ' + foreign_key.primary_key.inspect, + 'name: ' + foreign_key.name.inspect + ] + ' ' + parts.join(', ') + end + + stream.puts add_foreign_key_statements.sort.join("\n") + stream.puts + end + end + def remove_prefix_and_suffix(table) table.gsub(/^(#{@options[:table_name_prefix]})(.+)(#{@options[:table_name_suffix]})$/, "\\2") end diff --git a/activerecord/test/cases/migration/foreign_key_test.rb b/activerecord/test/cases/migration/foreign_key_test.rb index 2b99ab6ecd..f299762b42 100644 --- a/activerecord/test/cases/migration/foreign_key_test.rb +++ b/activerecord/test/cases/migration/foreign_key_test.rb @@ -1,11 +1,13 @@ require 'cases/helper' require 'support/ddl_helper' +require 'support/schema_dumping_helper' if ActiveRecord::Base.connection.supports_foreign_keys? module ActiveRecord class Migration class ForeignKeyTest < ActiveRecord::TestCase include DdlHelper + include SchemaDumpingHelper class Rocket < ActiveRecord::Base end @@ -90,6 +92,11 @@ module ActiveRecord @connection.remove_foreign_key :astronauts, name: "fancy_named_fk" assert_equal [], @connection.foreign_keys("astronauts") end + + def test_schema_dumping + 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: "id", name: "fk_name"$}, output + end end end end diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index 5f02d39e32..b1e7420c66 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -374,13 +374,19 @@ class SchemaDumperTest < ActiveRecord::TestCase class CreateDogMigration < ActiveRecord::Migration def up + create_table("dog_owners") do |t| + end + create_table("dogs") do |t| t.column :name, :string + t.column :owner_id, :integer end add_index "dogs", [:name] + add_foreign_key :dogs, :dog_owners, column: "owner_id" if supports_foreign_keys? end def down drop_table("dogs") + drop_table("dog_owners") end end @@ -396,13 +402,17 @@ class SchemaDumperTest < ActiveRecord::TestCase assert_no_match %r{create_table "foo_.+_bar"}, output assert_no_match %r{add_index "foo_.+_bar"}, output assert_no_match %r{create_table "schema_migrations"}, output + + if ActiveRecord::Base.connection.supports_foreign_keys? + assert_no_match %r{add_foreign_key "foo_.+_bar"}, output + assert_no_match %r{add_foreign_key "[^"]+", "foo_.+_bar"}, output + end ensure migration.migrate(:down) ActiveRecord::Base.table_name_suffix = ActiveRecord::Base.table_name_prefix = '' $stdout = original end - end class SchemaDumperDefaultsTest < ActiveRecord::TestCase -- cgit v1.2.3 From 402f303f1d938cf2c7781d7734c4ff8e6b874f35 Mon Sep 17 00:00:00 2001 From: Yves Senn Date: Tue, 10 Jun 2014 14:26:50 +0200 Subject: fk: support dependent option (:delete, :nullify and :restrict). --- .../abstract/schema_creation.rb | 13 ++++++- .../abstract/schema_definitions.rb | 4 +++ .../abstract/schema_statements.rb | 3 +- .../connection_adapters/abstract_mysql_adapter.rb | 14 +++++++- .../postgresql/schema_statements.rb | 9 ++++- activerecord/lib/active_record/schema_dumper.rb | 2 ++ .../test/cases/migration/foreign_key_test.rb | 42 ++++++++++++++++++++++ 7 files changed, 83 insertions(+), 4 deletions(-) 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 ad62eab4d2..57790d5667 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb @@ -19,11 +19,13 @@ module ActiveRecord end def visit_AddForeignKey(o) - <<-SQL + sql = <<-SQL ADD CONSTRAINT #{quote_column_name(o.name)} FOREIGN KEY (#{quote_column_name(o.column)}) REFERENCES #{quote_table_name(o.to_table)} (#{quote_column_name(o.primary_key)}) SQL + sql << " #{dependency_sql(o.dependent)}" if o.dependent + sql end def visit_DropForeignKey(name) @@ -98,6 +100,15 @@ FOREIGN KEY (#{quote_column_name(o.column)}) def options_include_default?(options) options.include?(:default) && !(options[:null] == false && options[:default].nil?) end + + def dependency_sql(dependency) + case dependency + when :nullify then "ON DELETE SET NULL" + when :delete then "ON DELETE CASCADE" + when :restrict then "ON DELETE RESTRICT" + else "" + end + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb index c18ebf1014..66ebf82971 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -37,6 +37,10 @@ module ActiveRecord def primary_key options[:primary_key] end + + def dependent + options[:dependent] + end end # Represents the schema of an SQL table in an abstract way. This class diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb index db04ebc802..fe752126ad 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -652,7 +652,8 @@ module ActiveRecord options = { column: options.fetch(:column), primary_key: primary_key, - name: foreign_key_name(from_table, options) + name: foreign_key_name(from_table, options), + dependent: options.fetch(:dependent, nil) } at = create_alter_table from_table at.add_foreign_key to_table, options diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb index 6ba226765c..9610296043 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -524,7 +524,19 @@ module ActiveRecord create_table_info = select_one("SHOW CREATE TABLE #{quote_table_name(table_name)}")["Create Table"] fk_info.map do |row| - options = {column: row['column'], name: row['name'], primary_key: row['primary_key']} + options = { + column: row['column'], + name: row['name'], + primary_key: row['primary_key'] + } + + if create_table_info =~ /CONSTRAINT #{quote_column_name(row['name'])} FOREIGN KEY .* REFERENCES .* ON DELETE (CASCADE|SET NULL|RESTRICT)/ + options[:dependent] = case $1 + when 'CASCADE' then :delete + when 'SET NULL' then :nullify + end + end + ForeignKeyDefinition.new(table_name, row['to_table'], options) end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb index c061337e71..7b61ff81ba 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -467,7 +467,14 @@ ORDER BY c.conname options = { column: row['column'], name: row['name'], - primary_key: row['primary_key'] } + primary_key: row['primary_key'] + } + + options[:dependent] = case row['dependency'] + when 'c'; :delete + when 'n'; :nullify + when 'r'; :restrict + end ForeignKeyDefinition.new(table_name, row["to_table"], options) end end diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index 3db36458db..c415236c45 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -226,6 +226,8 @@ HEADER 'primary_key: ' + foreign_key.primary_key.inspect, 'name: ' + foreign_key.name.inspect ] + parts << ('dependent: ' + foreign_key.dependent.inspect) if foreign_key.dependent + ' ' + parts.join(', ') end diff --git a/activerecord/test/cases/migration/foreign_key_test.rb b/activerecord/test/cases/migration/foreign_key_test.rb index f299762b42..fbdb921334 100644 --- a/activerecord/test/cases/migration/foreign_key_test.rb +++ b/activerecord/test/cases/migration/foreign_key_test.rb @@ -77,6 +77,41 @@ module ActiveRecord end end + def test_add_dependent_restrict_foreign_key + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", dependent: :restrict + + foreign_keys = @connection.foreign_keys("astronauts") + assert_equal 1, foreign_keys.size + + fk = foreign_keys.first + if current_adapter?(:MysqlAdapter, :Mysql2Adapter) + # ON DELETE RESTRICT is the default on MySQL + assert_equal nil, fk.dependent + else + assert_equal :restrict, fk.dependent + end + end + + def test_add_dependent_delete_foreign_key + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", dependent: :delete + + foreign_keys = @connection.foreign_keys("astronauts") + assert_equal 1, foreign_keys.size + + fk = foreign_keys.first + assert_equal :delete, fk.dependent + end + + def test_add_dependent_nullify_foreign_key + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", dependent: :nullify + + foreign_keys = @connection.foreign_keys("astronauts") + assert_equal 1, foreign_keys.size + + fk = foreign_keys.first + assert_equal :nullify, fk.dependent + end + def test_remove_foreign_key @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id" @@ -97,6 +132,13 @@ module ActiveRecord 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: "id", name: "fk_name"$}, output end + + def test_schema_dumping_dependent_option + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", dependent: :nullify + + output = dump_table_schema "astronauts" + assert_match %r{\s+add_foreign_key "astronauts",.+dependent: :nullify$}, output + end end end end -- cgit v1.2.3 From 6073d7c683b19fc7394baa9a93bc44e71e071129 Mon Sep 17 00:00:00 2001 From: Yves Senn Date: Tue, 10 Jun 2014 15:00:59 +0200 Subject: fk: make `add_foreign_key` reversible. --- .../lib/active_record/migration/command_recorder.rb | 17 ++++++++++++++++- .../test/cases/migration/command_recorder_test.rb | 20 ++++++++++++++++++++ .../test/cases/migration/foreign_key_test.rb | 19 +++++++++++++++++++ 3 files changed, 55 insertions(+), 1 deletion(-) diff --git a/activerecord/lib/active_record/migration/command_recorder.rb b/activerecord/lib/active_record/migration/command_recorder.rb index c44d8c1665..bd66c941a2 100644 --- a/activerecord/lib/active_record/migration/command_recorder.rb +++ b/activerecord/lib/active_record/migration/command_recorder.rb @@ -74,7 +74,9 @@ module ActiveRecord :rename_index, :rename_column, :add_index, :remove_index, :add_timestamps, :remove_timestamps, :change_column_default, :add_reference, :remove_reference, :transaction, :drop_join_table, :drop_table, :execute_block, :enable_extension, - :change_column, :execute, :remove_columns, :change_column_null # irreversible methods need to be here too + :change_column, :execute, :remove_columns, :change_column_null, + :add_foreign_key, :remove_foreign_key + # irreversible methods need to be here too ].each do |method| class_eval <<-EOV, __FILE__, __LINE__ + 1 def #{method}(*args, &block) # def create_table(*args, &block) @@ -167,6 +169,19 @@ module ActiveRecord [:change_column_null, args] end + def invert_add_foreign_key(args) + from_table, _to_table, add_options = *args + add_options ||= {} + + if add_options[:name] + options = {name: add_options[:name]} + elsif add_options[:column] + options = {column: add_options[:column]} + end + + [:remove_foreign_key, [from_table, options]] + end + # Forwards any missing method call to the \target. def method_missing(method, *args, &block) if @delegate.respond_to?(method) diff --git a/activerecord/test/cases/migration/command_recorder_test.rb b/activerecord/test/cases/migration/command_recorder_test.rb index 1c0134843b..64a1b9a84e 100644 --- a/activerecord/test/cases/migration/command_recorder_test.rb +++ b/activerecord/test/cases/migration/command_recorder_test.rb @@ -270,6 +270,26 @@ module ActiveRecord enable = @recorder.inverse_of :disable_extension, ['uuid-ossp'] assert_equal [:enable_extension, ['uuid-ossp'], nil], enable end + + def test_invert_add_foreign_key_with_column + enable = @recorder.inverse_of :add_foreign_key, [:dogs, :people, column: "owner_id"] + assert_equal [:remove_foreign_key, [:dogs, column: "owner_id"]], enable + end + + def test_invert_add_foreign_key_with_column_and_name + enable = @recorder.inverse_of :add_foreign_key, [:dogs, :people, column: "owner_id", name: "fk"] + assert_equal [:remove_foreign_key, [:dogs, name: "fk"]], enable + end + + def test_remove_foreign_key_is_irreversible + assert_raises ActiveRecord::IrreversibleMigration do + @recorder.inverse_of :remove_foreign_key, [:dogs, column: "owner_id"] + end + + assert_raises ActiveRecord::IrreversibleMigration do + @recorder.inverse_of :remove_foreign_key, [:dogs, name: "fk"] + end + end end end end diff --git a/activerecord/test/cases/migration/foreign_key_test.rb b/activerecord/test/cases/migration/foreign_key_test.rb index fbdb921334..5935062efb 100644 --- a/activerecord/test/cases/migration/foreign_key_test.rb +++ b/activerecord/test/cases/migration/foreign_key_test.rb @@ -139,6 +139,25 @@ module ActiveRecord output = dump_table_schema "astronauts" assert_match %r{\s+add_foreign_key "astronauts",.+dependent: :nullify$}, output end + + class CreateCitiesAndHousesMigration < ActiveRecord::Migration + def change + create_table("cities") { |t| } + + create_table("houses") do |t| + t.column :city_id, :integer + end + add_foreign_key :houses, :cities, column: "city_id" + end + end + + def test_add_foreign_key_is_reversible + migration = CreateCitiesAndHousesMigration.new + silence_stream($stdout) { migration.migrate(:up) } + assert_equal ["houses_city_id_fk"], @connection.foreign_keys("houses").map(&:name) + ensure + silence_stream($stdout) { migration.migrate(:down) } + end end end end -- cgit v1.2.3 From d074b821489b6d58101d1474dd514990f4bdf0fa Mon Sep 17 00:00:00 2001 From: Yves Senn Date: Tue, 10 Jun 2014 15:29:19 +0200 Subject: fk: infere column name from table names. This allows to create and remove foreign keys without specifying a column. --- .../abstract/schema_statements.rb | 18 +++++++++++---- .../active_record/migration/command_recorder.rb | 4 +++- .../test/cases/migration/command_recorder_test.rb | 5 +++++ .../test/cases/migration/foreign_key_test.rb | 26 ++++++++++++++++++++-- 4 files changed, 46 insertions(+), 7 deletions(-) diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb index fe752126ad..0f2af2c6d2 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -647,10 +647,11 @@ module ActiveRecord end def add_foreign_key(from_table, to_table, options = {}) + options[:column] ||= foreign_key_column_for(to_table) primary_key = options.fetch(:primary_key, "id") options = { - column: options.fetch(:column), + column: options[:column], primary_key: primary_key, name: foreign_key_name(from_table, options), dependent: options.fetch(:dependent, nil) @@ -661,17 +662,26 @@ module ActiveRecord execute schema_creation.accept at end - def remove_foreign_key(from_table, options = {}) + def remove_foreign_key(from_table, options_or_to_table = {}) + if options_or_to_table.is_a?(Hash) + options = options_or_to_table + else + options = { column: foreign_key_column_for(options_or_to_table) } + end + at = create_alter_table from_table at.drop_foreign_key foreign_key_name(from_table, options) execute schema_creation.accept at end + def foreign_key_column_for(table_name) # :nodoc: + "#{table_name.to_s.singularize}_id" + end + def foreign_key_name(table_name, options) # :nodoc: options.fetch(:name) do - column_name = options.fetch(:column) - "#{table_name}_#{column_name}_fk" + "#{table_name}_#{options.fetch(:column)}_fk" end end diff --git a/activerecord/lib/active_record/migration/command_recorder.rb b/activerecord/lib/active_record/migration/command_recorder.rb index bd66c941a2..ad726a3c02 100644 --- a/activerecord/lib/active_record/migration/command_recorder.rb +++ b/activerecord/lib/active_record/migration/command_recorder.rb @@ -170,13 +170,15 @@ module ActiveRecord end def invert_add_foreign_key(args) - from_table, _to_table, add_options = *args + from_table, to_table, add_options = *args add_options ||= {} if add_options[:name] options = {name: add_options[:name]} elsif add_options[:column] options = {column: add_options[:column]} + else + options = to_table end [:remove_foreign_key, [from_table, options]] diff --git a/activerecord/test/cases/migration/command_recorder_test.rb b/activerecord/test/cases/migration/command_recorder_test.rb index 64a1b9a84e..e955beae1a 100644 --- a/activerecord/test/cases/migration/command_recorder_test.rb +++ b/activerecord/test/cases/migration/command_recorder_test.rb @@ -271,6 +271,11 @@ module ActiveRecord assert_equal [:enable_extension, ['uuid-ossp'], nil], enable end + def test_invert_add_foreign_key + enable = @recorder.inverse_of :add_foreign_key, [:dogs, :people] + assert_equal [:remove_foreign_key, [:dogs, :people]], enable + end + def test_invert_add_foreign_key_with_column enable = @recorder.inverse_of :add_foreign_key, [:dogs, :people, column: "owner_id"] assert_equal [:remove_foreign_key, [:dogs, column: "owner_id"]], enable diff --git a/activerecord/test/cases/migration/foreign_key_test.rb b/activerecord/test/cases/migration/foreign_key_test.rb index 5935062efb..a43f7e48c8 100644 --- a/activerecord/test/cases/migration/foreign_key_test.rb +++ b/activerecord/test/cases/migration/foreign_key_test.rb @@ -46,7 +46,21 @@ module ActiveRecord assert_equal "fk_name", fk.name end - def test_add_foreign_key + def test_add_foreign_key_inferes_column + @connection.add_foreign_key :astronauts, :rockets + + foreign_keys = @connection.foreign_keys("astronauts") + assert_equal 1, foreign_keys.size + + fk = foreign_keys.first + assert_equal "astronauts", fk.from_table + assert_equal "rockets", fk.to_table + assert_equal "rocket_id", fk.column + assert_equal "id", fk.primary_key + assert_equal "astronauts_rocket_id_fk", fk.name + end + + def test_add_foreign_key_with_column @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id" foreign_keys = @connection.foreign_keys("astronauts") @@ -112,7 +126,15 @@ module ActiveRecord assert_equal :nullify, fk.dependent end - def test_remove_foreign_key + def test_remove_foreign_key_inferes_column + @connection.add_foreign_key :astronauts, :rockets + + assert_equal 1, @connection.foreign_keys("astronauts").size + @connection.remove_foreign_key :astronauts, :rockets + assert_equal [], @connection.foreign_keys("astronauts") + end + + def test_remove_foreign_key_by_column @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id" assert_equal 1, @connection.foreign_keys("astronauts").size -- cgit v1.2.3 From 6955d864ceb0ba994ef4fb4c5e866463f247944b Mon Sep 17 00:00:00 2001 From: Yves Senn Date: Wed, 11 Jun 2014 08:23:17 +0200 Subject: fk: rename `dependent` to `on_delete` --- .../abstract/schema_creation.rb | 11 +++++---- .../abstract/schema_definitions.rb | 4 ++-- .../abstract/schema_statements.rb | 2 +- .../connection_adapters/abstract_mysql_adapter.rb | 4 ++-- .../postgresql/schema_statements.rb | 4 ++-- activerecord/lib/active_record/schema_dumper.rb | 2 +- .../test/cases/migration/foreign_key_test.rb | 26 +++++++++++----------- 7 files changed, 26 insertions(+), 27 deletions(-) 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 57790d5667..aad4431910 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb @@ -24,7 +24,7 @@ ADD CONSTRAINT #{quote_column_name(o.name)} FOREIGN KEY (#{quote_column_name(o.column)}) REFERENCES #{quote_table_name(o.to_table)} (#{quote_column_name(o.primary_key)}) SQL - sql << " #{dependency_sql(o.dependent)}" if o.dependent + sql << " #{action_sql(o.on_delete)}" if o.on_delete sql end @@ -101,12 +101,11 @@ FOREIGN KEY (#{quote_column_name(o.column)}) options.include?(:default) && !(options[:null] == false && options[:default].nil?) end - def dependency_sql(dependency) + def action_sql(action = "DELETE", dependency) case dependency - when :nullify then "ON DELETE SET NULL" - when :delete then "ON DELETE CASCADE" - when :restrict then "ON DELETE RESTRICT" - else "" + when :nullify then "ON #{action} SET NULL" + when :cascade then "ON #{action} CASCADE" + when :restrict then "ON #{action} RESTRICT" end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb index 66ebf82971..2d6cf2427c 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -38,8 +38,8 @@ module ActiveRecord options[:primary_key] end - def dependent - options[:dependent] + def on_delete + options[:on_delete] end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb index 0f2af2c6d2..3f72e35bb5 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -654,7 +654,7 @@ module ActiveRecord column: options[:column], primary_key: primary_key, name: foreign_key_name(from_table, options), - dependent: options.fetch(:dependent, nil) + on_delete: options.fetch(:on_delete, nil) } at = create_alter_table from_table at.add_foreign_key to_table, options diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb index 9610296043..f3b7fe172e 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -531,8 +531,8 @@ module ActiveRecord } if create_table_info =~ /CONSTRAINT #{quote_column_name(row['name'])} FOREIGN KEY .* REFERENCES .* ON DELETE (CASCADE|SET NULL|RESTRICT)/ - options[:dependent] = case $1 - when 'CASCADE' then :delete + options[:on_delete] = case $1 + when 'CASCADE' then :cascade when 'SET NULL' then :nullify end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb index 7b61ff81ba..bf87395ef1 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -470,8 +470,8 @@ ORDER BY c.conname primary_key: row['primary_key'] } - options[:dependent] = case row['dependency'] - when 'c'; :delete + options[:on_delete] = case row['dependency'] + when 'c'; :cascade when 'n'; :nullify when 'r'; :restrict end diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index c415236c45..cdbb374510 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -226,7 +226,7 @@ HEADER 'primary_key: ' + foreign_key.primary_key.inspect, 'name: ' + foreign_key.name.inspect ] - parts << ('dependent: ' + foreign_key.dependent.inspect) if foreign_key.dependent + parts << ('on_delete: ' + foreign_key.on_delete.inspect) if foreign_key.on_delete ' ' + parts.join(', ') end diff --git a/activerecord/test/cases/migration/foreign_key_test.rb b/activerecord/test/cases/migration/foreign_key_test.rb index a43f7e48c8..f391e9ef41 100644 --- a/activerecord/test/cases/migration/foreign_key_test.rb +++ b/activerecord/test/cases/migration/foreign_key_test.rb @@ -91,8 +91,8 @@ module ActiveRecord end end - def test_add_dependent_restrict_foreign_key - @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", dependent: :restrict + def test_add_on_delete_restrict_foreign_key + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_delete: :restrict foreign_keys = @connection.foreign_keys("astronauts") assert_equal 1, foreign_keys.size @@ -100,30 +100,30 @@ module ActiveRecord fk = foreign_keys.first if current_adapter?(:MysqlAdapter, :Mysql2Adapter) # ON DELETE RESTRICT is the default on MySQL - assert_equal nil, fk.dependent + assert_equal nil, fk.on_delete else - assert_equal :restrict, fk.dependent + assert_equal :restrict, fk.on_delete end end - def test_add_dependent_delete_foreign_key - @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", dependent: :delete + def test_add_on_delete_cascade_foreign_key + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_delete: :cascade foreign_keys = @connection.foreign_keys("astronauts") assert_equal 1, foreign_keys.size fk = foreign_keys.first - assert_equal :delete, fk.dependent + assert_equal :cascade, fk.on_delete end - def test_add_dependent_nullify_foreign_key - @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", dependent: :nullify + def test_add_on_delete_nullify_foreign_key + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_delete: :nullify foreign_keys = @connection.foreign_keys("astronauts") assert_equal 1, foreign_keys.size fk = foreign_keys.first - assert_equal :nullify, fk.dependent + assert_equal :nullify, fk.on_delete end def test_remove_foreign_key_inferes_column @@ -155,11 +155,11 @@ module ActiveRecord assert_match %r{\s+add_foreign_key "fk_test_has_fk", "fk_test_has_pk", column: "fk_id", primary_key: "id", name: "fk_name"$}, output end - def test_schema_dumping_dependent_option - @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", dependent: :nullify + def test_schema_dumping_on_delete_option + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_delete: :nullify output = dump_table_schema "astronauts" - assert_match %r{\s+add_foreign_key "astronauts",.+dependent: :nullify$}, output + assert_match %r{\s+add_foreign_key "astronauts",.+on_delete: :nullify$}, output end class CreateCitiesAndHousesMigration < ActiveRecord::Migration -- cgit v1.2.3 From acd0287dc18a3fbba6fa4301cb31a7aecd22922b Mon Sep 17 00:00:00 2001 From: Yves Senn Date: Wed, 11 Jun 2014 08:47:53 +0200 Subject: fk: support for on_update --- .../connection_adapters/abstract/schema_creation.rb | 5 +++-- .../abstract/schema_definitions.rb | 4 ++++ .../connection_adapters/abstract/schema_statements.rb | 3 ++- .../connection_adapters/abstract_mysql_adapter.rb | 17 +++++++++++------ .../postgresql/schema_statements.rb | 19 ++++++++++++------- activerecord/lib/active_record/schema_dumper.rb | 1 + activerecord/test/cases/migration/foreign_key_test.rb | 16 +++++++++++++--- 7 files changed, 46 insertions(+), 19 deletions(-) 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 aad4431910..b896bd25e4 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb @@ -24,7 +24,8 @@ ADD CONSTRAINT #{quote_column_name(o.name)} FOREIGN KEY (#{quote_column_name(o.column)}) REFERENCES #{quote_table_name(o.to_table)} (#{quote_column_name(o.primary_key)}) SQL - sql << " #{action_sql(o.on_delete)}" if o.on_delete + sql << " #{action_sql('DELETE', o.on_delete)}" if o.on_delete + sql << " #{action_sql('UPDATE', o.on_update)}" if o.on_update sql end @@ -101,7 +102,7 @@ FOREIGN KEY (#{quote_column_name(o.column)}) options.include?(:default) && !(options[:null] == false && options[:default].nil?) end - def action_sql(action = "DELETE", dependency) + def action_sql(action, dependency) case dependency when :nullify then "ON #{action} SET NULL" when :cascade then "ON #{action} CASCADE" diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb index 2d6cf2427c..2e47e68754 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -41,6 +41,10 @@ module ActiveRecord def on_delete options[:on_delete] end + + def on_update + options[:on_update] + end end # Represents the schema of an SQL table in an abstract way. This class diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb index 3f72e35bb5..18e73b0200 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -654,7 +654,8 @@ module ActiveRecord column: options[:column], primary_key: primary_key, name: foreign_key_name(from_table, options), - on_delete: options.fetch(:on_delete, nil) + on_delete: options[:on_delete], + on_update: options[:on_update] } at = create_alter_table from_table at.add_foreign_key to_table, options diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb index f3b7fe172e..868181e677 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -530,17 +530,22 @@ module ActiveRecord primary_key: row['primary_key'] } - if create_table_info =~ /CONSTRAINT #{quote_column_name(row['name'])} FOREIGN KEY .* REFERENCES .* ON DELETE (CASCADE|SET NULL|RESTRICT)/ - options[:on_delete] = case $1 - when 'CASCADE' then :cascade - when 'SET NULL' then :nullify - end - end + options[:on_update] = extract_foreign_key_action(create_table_info, row['name'], "UPDATE") + options[:on_delete] = extract_foreign_key_action(create_table_info, row['name'], "DELETE") ForeignKeyDefinition.new(table_name, row['to_table'], options) end end + def extract_foreign_key_action(structure, name, action) # :nodoc: + if structure =~ /CONSTRAINT #{quote_column_name(name)} FOREIGN KEY .* REFERENCES .* ON #{action} (CASCADE|SET NULL|RESTRICT)/ + case $1 + when 'CASCADE'; :cascade + when 'SET NULL'; :nullify + end + end + end + # Maps logical Rails types to MySQL-specific data types. def type_to_sql(type, limit = nil, precision = nil, scale = nil) case type.to_s diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb index bf87395ef1..7ffbe4434f 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -450,7 +450,7 @@ module ActiveRecord def foreign_keys(table_name) fk_info = select_all <<-SQL -SELECT t2.relname AS to_table, a1.attname AS column, a2.attname AS primary_key, c.conname AS name, c.confdeltype AS dependency +SELECT t2.relname AS to_table, a1.attname AS column, a2.attname AS primary_key, c.conname AS name, c.confupdtype AS on_update, c.confdeltype AS on_delete FROM pg_constraint c JOIN pg_class t1 ON c.conrelid = t1.oid JOIN pg_class t2 ON c.confrelid = t2.oid @@ -470,12 +470,17 @@ ORDER BY c.conname primary_key: row['primary_key'] } - options[:on_delete] = case row['dependency'] - when 'c'; :cascade - when 'n'; :nullify - when 'r'; :restrict - end - ForeignKeyDefinition.new(table_name, row["to_table"], options) + options[:on_delete] = extract_foreign_key_action(row['on_delete']) + options[:on_update] = extract_foreign_key_action(row['on_update']) + ForeignKeyDefinition.new(table_name, row['to_table'], options) + end + end + + def extract_foreign_key_action(specifier) # :nodoc: + case specifier + when 'c'; :cascade + when 'n'; :nullify + when 'r'; :restrict end end diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index cdbb374510..9b46f7751b 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -226,6 +226,7 @@ HEADER 'primary_key: ' + foreign_key.primary_key.inspect, 'name: ' + foreign_key.name.inspect ] + parts << ('on_update: ' + foreign_key.on_update.inspect) if foreign_key.on_update parts << ('on_delete: ' + foreign_key.on_delete.inspect) if foreign_key.on_delete ' ' + parts.join(', ') diff --git a/activerecord/test/cases/migration/foreign_key_test.rb b/activerecord/test/cases/migration/foreign_key_test.rb index f391e9ef41..c69fc18d82 100644 --- a/activerecord/test/cases/migration/foreign_key_test.rb +++ b/activerecord/test/cases/migration/foreign_key_test.rb @@ -126,6 +126,16 @@ module ActiveRecord assert_equal :nullify, fk.on_delete end + def test_add_foreign_key_with_on_update + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_update: :nullify + + foreign_keys = @connection.foreign_keys("astronauts") + assert_equal 1, foreign_keys.size + + fk = foreign_keys.first + assert_equal :nullify, fk.on_update + end + def test_remove_foreign_key_inferes_column @connection.add_foreign_key :astronauts, :rockets @@ -155,11 +165,11 @@ module ActiveRecord assert_match %r{\s+add_foreign_key "fk_test_has_fk", "fk_test_has_pk", column: "fk_id", primary_key: "id", name: "fk_name"$}, output end - def test_schema_dumping_on_delete_option - @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_delete: :nullify + def test_schema_dumping_on_delete_and_on_update_options + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_delete: :nullify, on_update: :cascade output = dump_table_schema "astronauts" - assert_match %r{\s+add_foreign_key "astronauts",.+on_delete: :nullify$}, output + assert_match %r{\s+add_foreign_key "astronauts",.+on_update: :cascade,.+on_delete: :nullify$}, output end class CreateCitiesAndHousesMigration < ActiveRecord::Migration -- cgit v1.2.3 From 9ae1a2c69f51a9065090a9c505f4d22ffbb84094 Mon Sep 17 00:00:00 2001 From: Yves Senn Date: Wed, 11 Jun 2014 11:16:31 +0200 Subject: fk: raise when identifiers are longer than `allowed_index_name_length`. --- .../connection_adapters/abstract/schema_statements.rb | 6 +++++- activerecord/test/cases/migration/foreign_key_test.rb | 9 +++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb index 18e73b0200..5a863717e5 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -682,7 +682,11 @@ module ActiveRecord def foreign_key_name(table_name, options) # :nodoc: options.fetch(:name) do - "#{table_name}_#{options.fetch(:column)}_fk" + identifier = "#{table_name}_#{options.fetch(:column)}_fk" + if identifier.length > allowed_index_name_length + raise ArgumentError, "Foreign key name '#{identifier}' is too long; the limit is #{allowed_index_name_length} characters" + end + identifier end end diff --git a/activerecord/test/cases/migration/foreign_key_test.rb b/activerecord/test/cases/migration/foreign_key_test.rb index c69fc18d82..815c6b2955 100644 --- a/activerecord/test/cases/migration/foreign_key_test.rb +++ b/activerecord/test/cases/migration/foreign_key_test.rb @@ -136,6 +136,15 @@ module ActiveRecord assert_equal :nullify, fk.on_update end + def test_add_foreign_key_with_too_long_identifier + with_example_table @connection, "long_table_name_will_result_in_a_long_foreign_key_name", "rocket_id integer" do + e = assert_raises(ArgumentError) do + @connection.add_foreign_key "long_table_name_will_result_in_a_long_foreign_key_name", "rockets" + end + assert_match(/^Foreign key name 'long_table_name_will_result_in_a_long_foreign_key_name_rocket_id_fk' is too long;/, e.message) + end + end + def test_remove_foreign_key_inferes_column @connection.add_foreign_key :astronauts, :rockets -- cgit v1.2.3 From 0938d5305758dfcbec4e813c777bb627e82a5906 Mon Sep 17 00:00:00 2001 From: Yves Senn Date: Wed, 11 Jun 2014 11:35:26 +0200 Subject: fk: dump foreign keys at the bottom to make sure tables exist. --- activerecord/lib/active_record/schema_dumper.rb | 15 +++++++++------ activerecord/test/cases/schema_dumper_test.rb | 7 +++++++ 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index 9b46f7751b..84ce725409 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -91,7 +91,8 @@ HEADER end def tables(stream) - @connection.tables.sort.each do |tbl| + sorted_tables = @connection.tables.sort + sorted_tables.each do |tbl| next if ['schema_migrations', ignore_tables].flatten.any? do |ignored| case ignored when String; remove_prefix_and_suffix(tbl) == ignored @@ -102,6 +103,13 @@ HEADER end table(tbl, stream) end + + # dump foreign keys at the end to make sure all dependent tables exist. + if @connection.supports_foreign_keys? + sorted_tables.each do |tbl| + foreign_keys(tbl, stream) + end + end end def table(table, stream) @@ -171,8 +179,6 @@ HEADER indexes(table, tbl) - foreign_keys(table, tbl) - tbl.rewind stream.print tbl.read rescue => e @@ -215,8 +221,6 @@ HEADER end def foreign_keys(table, stream) - return unless @connection.supports_foreign_keys? - if (foreign_keys = @connection.foreign_keys(table)).any? add_foreign_key_statements = foreign_keys.map do |foreign_key| parts = [ @@ -233,7 +237,6 @@ HEADER end stream.puts add_foreign_key_statements.sort.join("\n") - stream.puts end end diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index b1e7420c66..4e71d04bc0 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -372,6 +372,13 @@ class SchemaDumperTest < ActiveRecord::TestCase assert_match %r{create_table "subscribers", id: false}, output end + if ActiveRecord::Base.connection.supports_foreign_keys? + def test_foreign_keys_are_dumped_at_the_bottom_to_circumvent_dependency_issues + output = standard_dump + assert_match(/^\s+add_foreign_key "fk_test_has_fk"[^\n]+\n\s+add_foreign_key "lessons_students"/, output) + end + end + class CreateDogMigration < ActiveRecord::Migration def up create_table("dog_owners") do |t| -- cgit v1.2.3 From 31e4c19331c9262574d354250675bba7dcf9dba2 Mon Sep 17 00:00:00 2001 From: Yves Senn Date: Wed, 11 Jun 2014 17:26:51 +0200 Subject: fk: `add/remove_foreign_key` are noop for adapters that don't support fk --- .../abstract/schema_statements.rb | 4 ++++ .../test/cases/migration/foreign_key_test.rb | 24 ++++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb index 5a863717e5..4da42717c1 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -647,6 +647,8 @@ module ActiveRecord end def add_foreign_key(from_table, to_table, options = {}) + return unless supports_foreign_keys? + options[:column] ||= foreign_key_column_for(to_table) primary_key = options.fetch(:primary_key, "id") @@ -664,6 +666,8 @@ module ActiveRecord end def remove_foreign_key(from_table, options_or_to_table = {}) + return unless supports_foreign_keys? + if options_or_to_table.is_a?(Hash) options = options_or_to_table else diff --git a/activerecord/test/cases/migration/foreign_key_test.rb b/activerecord/test/cases/migration/foreign_key_test.rb index 815c6b2955..7dad67ef88 100644 --- a/activerecord/test/cases/migration/foreign_key_test.rb +++ b/activerecord/test/cases/migration/foreign_key_test.rb @@ -202,4 +202,28 @@ module ActiveRecord end end end +else +module ActiveRecord + class Migration + class NoForeignKeySupportTest < ActiveRecord::TestCase + setup do + @connection = ActiveRecord::Base.connection + end + + def test_add_foreign_key_should_be_noop + @connection.add_foreign_key :clubs, :categories + end + + def test_remove_foreign_key_should_be_noop + @connection.remove_foreign_key :clubs, :categories + end + + def test_foreign_keys_should_raise_not_implemented + assert_raises NotImplementedError do + @connection.foreign_keys("clubs") + end + end + end + end +end end -- cgit v1.2.3 From 8550ba307d712ebede0d0695b5172bb3e9af16c9 Mon Sep 17 00:00:00 2001 From: Yves Senn Date: Fri, 20 Jun 2014 08:56:30 +0200 Subject: fk: raise for invalid :on_update / :on_delete values --- .../connection_adapters/abstract/schema_creation.rb | 11 ++++++++--- activerecord/test/cases/migration/foreign_key_test.rb | 10 ++++++++++ 2 files changed, 18 insertions(+), 3 deletions(-) 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 b896bd25e4..e495304376 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb @@ -104,9 +104,14 @@ FOREIGN KEY (#{quote_column_name(o.column)}) def action_sql(action, dependency) case dependency - when :nullify then "ON #{action} SET NULL" - when :cascade then "ON #{action} CASCADE" - when :restrict then "ON #{action} RESTRICT" + when :nullify then "ON #{action} SET NULL" + when :cascade then "ON #{action} CASCADE" + when :restrict then "ON #{action} RESTRICT" + else + raise ArgumentError, <<-MSG +'#{dependency}' is not supported for :on_update or :on_delete. +Supported values are: :nullify, :cascade, :restrict + MSG end end end diff --git a/activerecord/test/cases/migration/foreign_key_test.rb b/activerecord/test/cases/migration/foreign_key_test.rb index 7dad67ef88..6a24df076d 100644 --- a/activerecord/test/cases/migration/foreign_key_test.rb +++ b/activerecord/test/cases/migration/foreign_key_test.rb @@ -126,6 +126,16 @@ module ActiveRecord assert_equal :nullify, fk.on_delete end + def test_on_update_and_on_delete_raises_with_invalid_values + assert_raises ArgumentError do + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_delete: :invalid + end + + assert_raises ArgumentError do + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_update: :invalid + end + end + def test_add_foreign_key_with_on_update @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_update: :nullify -- cgit v1.2.3 From 8768305f20d12c40241396092a63e0d56269fefe Mon Sep 17 00:00:00 2001 From: Yves Senn Date: Fri, 20 Jun 2014 11:54:17 +0200 Subject: fk: use random digest names The name of the foreign key is not relevant from a users perspective. Using random names resolves the urge to rename the foreign key when the respective table or column is renamed. --- .../abstract/schema_creation.rb | 2 +- .../abstract/schema_definitions.rb | 11 +++++++- .../abstract/schema_statements.rb | 20 ++++++++------ activerecord/lib/active_record/schema_dumper.rb | 16 ++++++++--- .../test/cases/migration/foreign_key_test.rb | 31 ++++++++++++---------- activerecord/test/fixtures/fk_test_has_pk.yml | 2 +- activerecord/test/schema/schema.rb | 4 +-- activerecord/test/schema/sqlite_specific_schema.rb | 6 ++--- 8 files changed, 59 insertions(+), 33 deletions(-) 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 e495304376..5d13ee3633 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb @@ -30,7 +30,7 @@ FOREIGN KEY (#{quote_column_name(o.column)}) end def visit_DropForeignKey(name) - "DROP CONSTRAINT #{name}" + "DROP CONSTRAINT #{quote_column_name(name)}" end private diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb index 2e47e68754..785cfd9dbc 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -35,7 +35,7 @@ module ActiveRecord end def primary_key - options[:primary_key] + options[:primary_key] || default_primary_key end def on_delete @@ -45,6 +45,15 @@ module ActiveRecord def on_update options[:on_update] end + + def custom_primary_key? + options[:primary_key] != default_primary_key + end + + private + def default_primary_key + "id" + end end # Represents the schema of an SQL table in an abstract way. This class diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb index 4da42717c1..e203767992 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -650,11 +650,10 @@ module ActiveRecord return unless supports_foreign_keys? options[:column] ||= foreign_key_column_for(to_table) - primary_key = options.fetch(:primary_key, "id") options = { column: options[:column], - primary_key: primary_key, + primary_key: options[:primary_key], name: foreign_key_name(from_table, options), on_delete: options[:on_delete], on_update: options[:on_update] @@ -674,8 +673,17 @@ module ActiveRecord options = { column: foreign_key_column_for(options_or_to_table) } end + fk_name_to_delete = options.fetch(:name) do + fk_to_delete = foreign_keys(from_table).detect {|fk| fk.column == options[:column] } + if fk_to_delete + fk_to_delete.name + else + raise ArgumentError, "Table '#{from_table}' has no foreign key on column '#{options[:column]}'" + end + end + at = create_alter_table from_table - at.drop_foreign_key foreign_key_name(from_table, options) + at.drop_foreign_key fk_name_to_delete execute schema_creation.accept at end @@ -686,11 +694,7 @@ module ActiveRecord def foreign_key_name(table_name, options) # :nodoc: options.fetch(:name) do - identifier = "#{table_name}_#{options.fetch(:column)}_fk" - if identifier.length > allowed_index_name_length - raise ArgumentError, "Foreign key name '#{identifier}' is too long; the limit is #{allowed_index_name_length} characters" - end - identifier + "fk_rails_#{SecureRandom.hex(5)}" end end diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index 84ce725409..64bc68eefd 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -226,10 +226,20 @@ HEADER parts = [ 'add_foreign_key ' + remove_prefix_and_suffix(foreign_key.from_table).inspect, remove_prefix_and_suffix(foreign_key.to_table).inspect, - 'column: ' + foreign_key.column.inspect, - 'primary_key: ' + foreign_key.primary_key.inspect, - 'name: ' + foreign_key.name.inspect ] + + if foreign_key.column != @connection.foreign_key_column_for(foreign_key.to_table) + parts << ('column: ' + foreign_key.column.inspect) + end + + if foreign_key.custom_primary_key? + parts << ('primary_key: ' + foreign_key.primary_key.inspect) + end + + if foreign_key.name !~ /^fk_rails_[0-9a-f]{10}$/ + parts << ('name: ' + foreign_key.name.inspect) + end + parts << ('on_update: ' + foreign_key.on_update.inspect) if foreign_key.on_update parts << ('on_delete: ' + foreign_key.on_delete.inspect) if foreign_key.on_delete diff --git a/activerecord/test/cases/migration/foreign_key_test.rb b/activerecord/test/cases/migration/foreign_key_test.rb index 6a24df076d..c985092b4c 100644 --- a/activerecord/test/cases/migration/foreign_key_test.rb +++ b/activerecord/test/cases/migration/foreign_key_test.rb @@ -42,7 +42,7 @@ module ActiveRecord 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 "id", fk.primary_key + assert_equal "pk_id", fk.primary_key assert_equal "fk_name", fk.name end @@ -57,7 +57,7 @@ module ActiveRecord assert_equal "rockets", fk.to_table assert_equal "rocket_id", fk.column assert_equal "id", fk.primary_key - assert_equal "astronauts_rocket_id_fk", fk.name + assert_match(/^fk_rails_.{10}$/, fk.name) end def test_add_foreign_key_with_column @@ -71,7 +71,7 @@ module ActiveRecord assert_equal "rockets", fk.to_table assert_equal "rocket_id", fk.column assert_equal "id", fk.primary_key - assert_equal "astronauts_rocket_id_fk", fk.name + assert_match(/^fk_rails_.{10}$/, fk.name) end def test_add_foreign_key_with_non_standard_primary_key @@ -146,15 +146,6 @@ module ActiveRecord assert_equal :nullify, fk.on_update end - def test_add_foreign_key_with_too_long_identifier - with_example_table @connection, "long_table_name_will_result_in_a_long_foreign_key_name", "rocket_id integer" do - e = assert_raises(ArgumentError) do - @connection.add_foreign_key "long_table_name_will_result_in_a_long_foreign_key_name", "rockets" - end - assert_match(/^Foreign key name 'long_table_name_will_result_in_a_long_foreign_key_name_rocket_id_fk' is too long;/, e.message) - end - end - def test_remove_foreign_key_inferes_column @connection.add_foreign_key :astronauts, :rockets @@ -179,9 +170,21 @@ module ActiveRecord assert_equal [], @connection.foreign_keys("astronauts") end + def test_remove_foreign_non_existing_foreign_key_raises + assert_raises ArgumentError do + @connection.remove_foreign_key :astronauts, :rockets + end + end + def test_schema_dumping + @connection.add_foreign_key :astronauts, :rockets + output = dump_table_schema "astronauts" + assert_match %r{\s+add_foreign_key "astronauts", "rockets"$}, output + end + + 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: "id", name: "fk_name"$}, output + 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 def test_schema_dumping_on_delete_and_on_update_options @@ -205,7 +208,7 @@ module ActiveRecord def test_add_foreign_key_is_reversible migration = CreateCitiesAndHousesMigration.new silence_stream($stdout) { migration.migrate(:up) } - assert_equal ["houses_city_id_fk"], @connection.foreign_keys("houses").map(&:name) + assert_equal 1, @connection.foreign_keys("houses").size ensure silence_stream($stdout) { migration.migrate(:down) } end diff --git a/activerecord/test/fixtures/fk_test_has_pk.yml b/activerecord/test/fixtures/fk_test_has_pk.yml index c93952180b..73882bac41 100644 --- a/activerecord/test/fixtures/fk_test_has_pk.yml +++ b/activerecord/test/fixtures/fk_test_has_pk.yml @@ -1,2 +1,2 @@ first: - id: 1 \ No newline at end of file + pk_id: 1 \ No newline at end of file diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 6ce82c71a8..03d33c151a 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -855,10 +855,10 @@ ActiveRecord::Schema.define do t.integer :fk_id, null: false end - create_table :fk_test_has_pk, force: true do |t| + create_table :fk_test_has_pk, force: true, primary_key: "pk_id" do |t| end - execute "ALTER TABLE fk_test_has_fk ADD CONSTRAINT fk_name FOREIGN KEY (#{quote_column_name 'fk_id'}) REFERENCES #{quote_table_name 'fk_test_has_pk'} (#{quote_column_name 'id'})" + execute "ALTER TABLE fk_test_has_fk ADD CONSTRAINT fk_name FOREIGN KEY (#{quote_column_name 'fk_id'}) REFERENCES #{quote_table_name 'fk_test_has_pk'} (#{quote_column_name 'pk_id'})" execute "ALTER TABLE lessons_students ADD CONSTRAINT student_id_fk FOREIGN KEY (#{quote_column_name 'student_id'}) REFERENCES #{quote_table_name 'students'} (#{quote_column_name 'id'})" end diff --git a/activerecord/test/schema/sqlite_specific_schema.rb b/activerecord/test/schema/sqlite_specific_schema.rb index b7aff4f47d..b5552c2755 100644 --- a/activerecord/test/schema/sqlite_specific_schema.rb +++ b/activerecord/test/schema/sqlite_specific_schema.rb @@ -7,7 +7,7 @@ ActiveRecord::Schema.define do execute "DROP TABLE fk_test_has_pk" rescue nil execute <<_SQL CREATE TABLE 'fk_test_has_pk' ( - 'id' INTEGER NOT NULL PRIMARY KEY + 'pk_id' INTEGER NOT NULL PRIMARY KEY ); _SQL @@ -16,7 +16,7 @@ _SQL 'id' INTEGER NOT NULL PRIMARY KEY, 'fk_id' INTEGER NOT NULL, - FOREIGN KEY ('fk_id') REFERENCES 'fk_test_has_pk'('id') + FOREIGN KEY ('fk_id') REFERENCES 'fk_test_has_pk'('pk_id') ); _SQL -end \ No newline at end of file +end -- cgit v1.2.3 From 24e1aefb4b2d7b2b4babfd4bae1e9e613283b003 Mon Sep 17 00:00:00 2001 From: Yves Senn Date: Thu, 26 Jun 2014 13:09:45 +0200 Subject: fk: review corrections: indent, visibility, syntax, wording. --- .../abstract/schema_creation.rb | 36 +++++++++++----------- .../abstract/schema_statements.rb | 17 +++++----- .../connection_adapters/abstract_mysql_adapter.rb | 22 ++++++------- .../postgresql/schema_statements.rb | 24 +++++++-------- .../active_record/migration/command_recorder.rb | 6 ++-- 5 files changed, 53 insertions(+), 52 deletions(-) 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 5d13ee3633..c1379f6bec 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb @@ -18,21 +18,6 @@ module ActiveRecord add_column_options!(sql, column_options(o)) end - def visit_AddForeignKey(o) - sql = <<-SQL -ADD CONSTRAINT #{quote_column_name(o.name)} -FOREIGN KEY (#{quote_column_name(o.column)}) - REFERENCES #{quote_table_name(o.to_table)} (#{quote_column_name(o.primary_key)}) - SQL - sql << " #{action_sql('DELETE', o.on_delete)}" if o.on_delete - sql << " #{action_sql('UPDATE', o.on_update)}" if o.on_update - sql - end - - def visit_DropForeignKey(name) - "DROP CONSTRAINT #{quote_column_name(name)}" - end - private def visit_AlterTable(o) @@ -58,6 +43,21 @@ FOREIGN KEY (#{quote_column_name(o.column)}) create_sql end + def visit_AddForeignKey(o) + sql = <<-SQL.strip_heredoc + ADD CONSTRAINT #{quote_column_name(o.name)} + FOREIGN KEY (#{quote_column_name(o.column)}) + REFERENCES #{quote_table_name(o.to_table)} (#{quote_column_name(o.primary_key)}) + SQL + sql << " #{action_sql('DELETE', o.on_delete)}" if o.on_delete + sql << " #{action_sql('UPDATE', o.on_update)}" if o.on_update + sql + end + + def visit_DropForeignKey(name) + "DROP CONSTRAINT #{quote_column_name(name)}" + end + def column_options(o) column_options = {} column_options[:null] = o.null unless o.null.nil? @@ -108,9 +108,9 @@ FOREIGN KEY (#{quote_column_name(o.column)}) when :cascade then "ON #{action} CASCADE" when :restrict then "ON #{action} RESTRICT" else - raise ArgumentError, <<-MSG -'#{dependency}' is not supported for :on_update or :on_delete. -Supported values are: :nullify, :cascade, :restrict + raise ArgumentError, <<-MSG.strip_heredoc + '#{dependency}' is not supported for :on_update or :on_delete. + Supported values are: :nullify, :cascade, :restrict MSG end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb index e203767992..1789cce123 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -661,7 +661,7 @@ module ActiveRecord at = create_alter_table from_table at.add_foreign_key to_table, options - execute schema_creation.accept at + execute schema_creation.accept(at) end def remove_foreign_key(from_table, options_or_to_table = {}) @@ -675,6 +675,7 @@ module ActiveRecord fk_name_to_delete = options.fetch(:name) do fk_to_delete = foreign_keys(from_table).detect {|fk| fk.column == options[:column] } + if fk_to_delete fk_to_delete.name else @@ -685,19 +686,13 @@ module ActiveRecord at = create_alter_table from_table at.drop_foreign_key fk_name_to_delete - execute schema_creation.accept at + execute schema_creation.accept(at) end def foreign_key_column_for(table_name) # :nodoc: "#{table_name.to_s.singularize}_id" end - def foreign_key_name(table_name, options) # :nodoc: - options.fetch(:name) do - "fk_rails_#{SecureRandom.hex(5)}" - end - end - def dump_schema_information #:nodoc: sm_table = ActiveRecord::Migrator.schema_migrations_table_name @@ -908,6 +903,12 @@ module ActiveRecord def create_alter_table(name) AlterTable.new create_table_definition(name, false, {}) end + + def foreign_key_name(table_name, options) # :nodoc: + options.fetch(:name) do + "fk_rails_#{SecureRandom.hex(5)}" + end + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb index 868181e677..4924f345fc 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -510,7 +510,7 @@ module ActiveRecord end def foreign_keys(table_name) - fk_info = select_all %{ + fk_info = select_all <<-SQL.strip_heredoc SELECT fk.referenced_table_name as 'to_table' ,fk.referenced_column_name as 'primary_key' ,fk.column_name as 'column' @@ -519,7 +519,7 @@ module ActiveRecord WHERE fk.referenced_column_name is not null AND fk.table_schema = '#{@config[:database]}' AND fk.table_name = '#{table_name}' - } + SQL create_table_info = select_one("SHOW CREATE TABLE #{quote_table_name(table_name)}")["Create Table"] @@ -537,15 +537,6 @@ module ActiveRecord end end - def extract_foreign_key_action(structure, name, action) # :nodoc: - if structure =~ /CONSTRAINT #{quote_column_name(name)} FOREIGN KEY .* REFERENCES .* ON #{action} (CASCADE|SET NULL|RESTRICT)/ - case $1 - when 'CASCADE'; :cascade - when 'SET NULL'; :nullify - end - end - end - # Maps logical Rails types to MySQL-specific data types. def type_to_sql(type, limit = nil, precision = nil, scale = nil) case type.to_s @@ -824,6 +815,15 @@ module ActiveRecord # ...and send them all in one query @connection.query "SET #{encoding} #{variable_assignments}" end + + def extract_foreign_key_action(structure, name, action) # :nodoc: + if structure =~ /CONSTRAINT #{quote_column_name(name)} FOREIGN KEY .* REFERENCES .* ON #{action} (CASCADE|SET NULL|RESTRICT)/ + case $1 + when 'CASCADE'; :cascade + when 'SET NULL'; :nullify + end + end + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb index 7ffbe4434f..30df98be1b 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -449,18 +449,18 @@ module ActiveRecord end def foreign_keys(table_name) - fk_info = select_all <<-SQL -SELECT t2.relname AS to_table, a1.attname AS column, a2.attname AS primary_key, c.conname AS name, c.confupdtype AS on_update, c.confdeltype AS on_delete -FROM pg_constraint c -JOIN pg_class t1 ON c.conrelid = t1.oid -JOIN pg_class t2 ON c.confrelid = t2.oid -JOIN pg_attribute a1 ON a1.attnum = c.conkey[1] AND a1.attrelid = t1.oid -JOIN pg_attribute a2 ON a2.attnum = c.confkey[1] AND a2.attrelid = t2.oid -JOIN pg_namespace t3 ON c.connamespace = t3.oid -WHERE c.contype = 'f' - AND t1.relname = #{quote(table_name)} - AND t3.nspname = ANY (current_schemas(false)) -ORDER BY c.conname + fk_info = select_all <<-SQL.strip_heredoc + SELECT t2.relname AS to_table, a1.attname AS column, a2.attname AS primary_key, c.conname AS name, c.confupdtype AS on_update, c.confdeltype AS on_delete + FROM pg_constraint c + JOIN pg_class t1 ON c.conrelid = t1.oid + JOIN pg_class t2 ON c.confrelid = t2.oid + JOIN pg_attribute a1 ON a1.attnum = c.conkey[1] AND a1.attrelid = t1.oid + JOIN pg_attribute a2 ON a2.attnum = c.confkey[1] AND a2.attrelid = t2.oid + JOIN pg_namespace t3 ON c.connamespace = t3.oid + WHERE c.contype = 'f' + AND t1.relname = #{quote(table_name)} + AND t3.nspname = ANY (current_schemas(false)) + ORDER BY c.conname SQL fk_info.map do |row| diff --git a/activerecord/lib/active_record/migration/command_recorder.rb b/activerecord/lib/active_record/migration/command_recorder.rb index ad726a3c02..f833caaab6 100644 --- a/activerecord/lib/active_record/migration/command_recorder.rb +++ b/activerecord/lib/active_record/migration/command_recorder.rb @@ -170,13 +170,13 @@ module ActiveRecord end def invert_add_foreign_key(args) - from_table, to_table, add_options = *args + from_table, to_table, add_options = args add_options ||= {} if add_options[:name] - options = {name: add_options[:name]} + options = { name: add_options[:name] } elsif add_options[:column] - options = {column: add_options[:column]} + options = { column: add_options[:column] } else options = to_table end -- cgit v1.2.3 From a5b3f372ab30e043d25b25b05e603e6ed33c0ee9 Mon Sep 17 00:00:00 2001 From: Yves Senn Date: Thu, 12 Jun 2014 08:42:00 +0200 Subject: fk: add docs --- activerecord/CHANGELOG.md | 12 ++ .../abstract/schema_statements.rb | 58 +++++++++ guides/source/4_2_release_notes.md | 25 ++++ guides/source/active_record_migrations.md | 129 ++++++++++++--------- 4 files changed, 166 insertions(+), 58 deletions(-) diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 4b39791b73..9a9c0367e1 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,15 @@ +* Support for adding and removing foreign keys. Foreign keys are now + a part of `schema.rb`. This is supported by Mysql2Adapter, MysqlAdapter + and PostgreSQLAdapter. + + Example: + + # within your migrations: + add_foreign_key :articles, :authors + remove_foreign_key :articles, :authors + + *Yves Senn* + * Fix subtle bugs regarding attribute assignment on models with no primary key. `'id'` will no longer be part of the attributes hash. diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb index 1789cce123..5814c2b711 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -642,10 +642,54 @@ module ActiveRecord end alias :remove_belongs_to :remove_reference + # Returns an array of foreign keys for the given table. + # The foreign keys are represented as +ForeignKeyDefinition+ objects. def foreign_keys(table_name) raise NotImplementedError, "foreign_keys is not implemented" end + # Adds a new foreign key. +from_table+ is the table with the key column, + # +to_table+ contains the referenced primary key. + # + # The foreign key will be named after the following pattern: fk_rails_. + # +identifier+ is a 10 character long random string. A custom name can be specified with + # the :name option. + # + # ====== Creating a simple foreign key + # + # add_foreign_key :articles, :authors + # + # generates: + # + # ALTER TABLE "articles" ADD CONSTRAINT articles_author_id_fk FOREIGN KEY ("author_id") REFERENCES "authors" ("id") + # + # ====== Creating a foreign key on a specific column + # + # add_foreign_key :articles, :users, column: :author_id, primary_key: "lng_id" + # + # generates: + # + # ALTER TABLE "articles" ADD CONSTRAINT fk_rails_58ca3d3a82 FOREIGN KEY ("author_id") REFERENCES "users" ("lng_id") + # + # ====== Creating a cascading foreign key + # + # add_foreign_key :articles, :authors, on_delete: :cascade + # + # generates: + # + # ALTER TABLE "articles" ADD CONSTRAINT articles_author_id_fk FOREIGN KEY ("author_id") REFERENCES "authors" ("id") ON DELETE CASCADE + # + # The +options+ hash can include the following keys: + # [:column] + # The foreign key column name on +from_table+. Defaults to to_table.singularize + "_id" + # [:primary_key] + # The primary key column name on +to_table+. Defaults to +id+. + # [:name] + # The constraint name. Defaults to fk_rails_. + # [:on_delete] + # Action that happens ON DELETE. Valid values are +:nullify+, +:cascade:+ and +:restrict+ + # [:on_update] + # Action that happens ON UPDATE. Valid values are +:nullify+, +:cascade:+ and +:restrict+ def add_foreign_key(from_table, to_table, options = {}) return unless supports_foreign_keys? @@ -664,6 +708,20 @@ module ActiveRecord execute schema_creation.accept(at) end + # Removes the given foreign key from the table. + # + # Removes the foreign key on +accounts.branch_id+. + # + # remove_foreign_key :accounts, :branches + # + # Removes the foreign key on +accounts.owner_id+. + # + # remove_foreign_key :accounts, column: :owner_id + # + # Removes the foreign key named +special_fk_name+ on the +accounts+ table. + # + # remove_foreign_key :accounts, name: :special_fk_name + # def remove_foreign_key(from_table, options_or_to_table = {}) return unless supports_foreign_keys? diff --git a/guides/source/4_2_release_notes.md b/guides/source/4_2_release_notes.md index dd484ccee2..ef294f55d7 100644 --- a/guides/source/4_2_release_notes.md +++ b/guides/source/4_2_release_notes.md @@ -25,6 +25,31 @@ guide. Major Features -------------- +### Foreign key support + +The migration DSL now supports adding and removing foreign keys. They are dumped +to `schema.rb` as well. At this time, only the `mysql`, `mysql2` and `postgresql` +adapters support foreign keys. + +```ruby +# add a foreign key to `articles.author_id` referencing `authors.id` +add_foreign_key :articles, :authors + +# add a foreign key to `articles.author_id` referencing `users.lng_id` +add_foreign_key :articles, :users, column: :author_id, primary_key: "lng_id" + +# remove the foreign key on `accounts.branch_id` +remove_foreign_key :accounts, :branches + +# remove the foreign key on `accounts.owner_id` +remove_foreign_key :accounts, column: :owner_id +``` + +See the API documentation on +[add_foreign_key](http://api.rubyonrails.org/v4.2.0/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html#method-i-add_foreign_key) +and +[remove_foreign_key](http://api.rubyonrails.org/v4.2.0/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html#method-i-remove_foreign_key) +for a full description. Railties diff --git a/guides/source/active_record_migrations.md b/guides/source/active_record_migrations.md index fd2125424b..1b0ae3ec7e 100644 --- a/guides/source/active_record_migrations.md +++ b/guides/source/active_record_migrations.md @@ -452,6 +452,40 @@ Column modifiers can be applied when creating or changing a column: Some adapters may support additional options; see the adapter specific API docs for further information. +### Foreign Keys + +While it's not required you might want to add foreign key constraints to +[guarantee referential integrity](#active-record-and-referential-integrity). + +```ruby +add_foreign_key :articles, :authors +``` + +This adds a new foreign key to the `author_id` column of the `articles` +table. The key references the `id` column of the `articles` table. If the +column names can not be derived from the table names, you can use the +`:column` and `:primary_key` options. + +Rails will generate a name for every foreign key starting with +`fk_rails_` followed by 10 random characters. +There is a `:name` option to specify a different name if needed. + +NOTE: Active Record only supports single column foreign keys. `execute` and +`structure.sql` are required to use composite foreign keys. + +Removing a foreign key is easy as well: + +```ruby +# let Active Record figure out the column name +remove_foreign_key :accounts, :branches + +# remove foreign key for a specific column +remove_foreign_key :accounts, column: :owner_id + +# remove foreign key by name +remove_foreign_key :accounts, name: :special_fk_name +``` + ### When Helpers aren't Enough If the helpers provided by Active Record aren't enough you can use the `execute` @@ -482,6 +516,7 @@ definitions: * `add_index` * `add_reference` * `add_timestamps` +* `add_foreign_key` * `create_table` * `create_join_table` * `drop_table` (must supply a block) @@ -507,24 +542,23 @@ migration what else to do when reverting it. For example: ```ruby class ExampleMigration < ActiveRecord::Migration def change - create_table :products do |t| - t.references :category + create_table :distributors do |t| + t.string :zipcode end reversible do |dir| dir.up do - #add a foreign key + # add a CHECK constraint execute <<-SQL - ALTER TABLE products - ADD CONSTRAINT fk_products_categories - FOREIGN KEY (category_id) - REFERENCES categories(id) + ALTER TABLE distributors + ADD CONSTRAINT zipchk + CHECK (char_length(zipcode) = 5) NO INHERIT; SQL end dir.down do execute <<-SQL - ALTER TABLE products - DROP FOREIGN KEY fk_products_categories + ALTER TABLE distributors + DROP CONSTRAINT zipchk SQL end end @@ -538,7 +572,7 @@ end Using `reversible` will ensure that the instructions are executed in the right order too. If the previous example migration is reverted, the `down` block will be run after the `home_page_url` column is removed and -right before the table `products` is dropped. +right before the table `distributors` is dropped. Sometimes your migration will do something which is just plain irreversible; for example, it might destroy some data. In such cases, you can raise @@ -561,16 +595,15 @@ made in the `up` method. The example in the `reversible` section is equivalent t ```ruby class ExampleMigration < ActiveRecord::Migration def up - create_table :products do |t| - t.references :category + create_table :distributors do |t| + t.string :zipcode end - # add a foreign key + # add a CHECK constraint execute <<-SQL - ALTER TABLE products - ADD CONSTRAINT fk_products_categories - FOREIGN KEY (category_id) - REFERENCES categories(id) + ALTER TABLE distributors + ADD CONSTRAINT zipchk + CHECK (char_length(zipcode) = 5); SQL add_column :users, :home_page_url, :string @@ -582,11 +615,11 @@ class ExampleMigration < ActiveRecord::Migration remove_column :users, :home_page_url execute <<-SQL - ALTER TABLE products - DROP FOREIGN KEY fk_products_categories + ALTER TABLE distributors + DROP CONSTRAINT zipchk SQL - drop_table :products + drop_table :distributors end end ``` @@ -617,43 +650,27 @@ end The `revert` method also accepts a block of instructions to reverse. This could be useful to revert selected parts of previous migrations. For example, let's imagine that `ExampleMigration` is committed and it -is later decided it would be best to serialize the product list instead. -One could write: +is later decided it would be best to use Active Record validations, +in place of the `CHECK` constraint, to verify the zipcode. ```ruby -class SerializeProductListMigration < ActiveRecord::Migration +class DontUseConstraintForZipcodeValidationMigration < ActiveRecord::Migration def change - add_column :categories, :product_list - - reversible do |dir| - dir.up do - # transfer data from Products to Category#product_list - end - dir.down do - # create Products from Category#product_list - end - end - revert do # copy-pasted code from ExampleMigration - create_table :products do |t| - t.references :category - end - reversible do |dir| dir.up do - #add a foreign key + # add a CHECK constraint execute <<-SQL - ALTER TABLE products - ADD CONSTRAINT fk_products_categories - FOREIGN KEY (category_id) - REFERENCES categories(id) + ALTER TABLE distributors + ADD CONSTRAINT zipchk + CHECK (char_length(zipcode) = 5); SQL end dir.down do execute <<-SQL - ALTER TABLE products - DROP FOREIGN KEY fk_products_categories + ALTER TABLE distributors + DROP CONSTRAINT zipchk SQL end end @@ -918,10 +935,10 @@ that Active Record supports. This could be very useful if you were to distribute an application that is able to run against multiple databases. There is however a trade-off: `db/schema.rb` cannot express database specific -items such as foreign key constraints, triggers, or stored procedures. While in -a migration you can execute custom SQL statements, the schema dumper cannot -reconstitute those statements from the database. If you are using features like -this, then you should set the schema format to `:sql`. +items such as triggers, or stored procedures. While in a migration you can +execute custom SQL statements, the schema dumper cannot reconstitute those +statements from the database. If you are using features like this, then you +should set the schema format to `:sql`. Instead of using Active Record's schema dumper, the database's structure will be dumped using a tool specific to the database (via the `db:structure:dump` @@ -948,7 +965,7 @@ Active Record and Referential Integrity --------------------------------------- The Active Record way claims that intelligence belongs in your models, not in -the database. As such, features such as triggers or foreign key constraints, +the database. As such, features such as triggers or constraints, which push some of that intelligence back into the database, are not heavily used. @@ -957,14 +974,10 @@ which models can enforce data integrity. The `:dependent` option on associations allows models to automatically destroy child objects when the parent is destroyed. Like anything which operates at the application level, these cannot guarantee referential integrity and so some people augment them -with foreign key constraints in the database. - -Although Active Record does not provide any tools for working directly with -such features, the `execute` method can be used to execute arbitrary SQL. You -can also use a gem like -[foreigner](https://github.com/matthuhiggins/foreigner) which adds foreign key -support to Active Record (including support for dumping foreign keys in -`db/schema.rb`). +with [foreign key constraints](#foreign-keys) in the database. + +Although Active Record does not provide all the tools for working directly with +such features, the `execute` method can be used to execute arbitrary SQL. Migrations and Seed Data ------------------------ -- cgit v1.2.3