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 (limited to 'activerecord') 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(-) (limited to 'activerecord') 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(-) (limited to 'activerecord') 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(-) (limited to 'activerecord') 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(-) (limited to 'activerecord') 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(-) (limited to 'activerecord') 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(-) (limited to 'activerecord') 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(-) (limited to 'activerecord') 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(-) (limited to 'activerecord') 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(-) (limited to 'activerecord') 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(-) (limited to 'activerecord') 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(-) (limited to 'activerecord') 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(-) (limited to 'activerecord') 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(+) (limited to 'activerecord') 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(-) (limited to 'activerecord') 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(-) (limited to 'activerecord') 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(-) (limited to 'activerecord') 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 ++++++++++++++++++++++ 2 files changed, 70 insertions(+) (limited to 'activerecord') 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? -- cgit v1.2.3