foreign_key_saver 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1 @@
1
+ test/log/*
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2007-2008 Will Bryant, Sekuda Ltd
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README ADDED
@@ -0,0 +1,36 @@
1
+ ForeignKeySaver
2
+ ===============
3
+
4
+ This plugin adds an add_foreign_key_constraint schema method, and extends the
5
+ schema dump code to output these foreign key constraints.
6
+
7
+ Only MySQL and PostgreSQL are currently supported.
8
+
9
+
10
+ Examples
11
+ ========
12
+
13
+ # adds a constraint on projects.customer_id with parent customers.id
14
+ add_foreign_key :projects, :customer_id, :customers, :id
15
+
16
+ # adds a constraint on projects(a, b) with parent(a, b) with the default RESTRICT update/delete actions
17
+ add_foreign_key "child", ["a", "b"], "parent", ["a", "b"]
18
+
19
+ # adds a constraint with the ON UPDATE action set to CASCADE and the ON DELETE action set to SET NULL
20
+ add_foreign_key 'projects', 'customer_id', 'customers', 'id', :on_update => :cascade, :on_delete => :set_null
21
+
22
+ The following actions are defined:
23
+ :restrict
24
+ :no_action
25
+ :cascade
26
+ :set_null (aka :nullify)
27
+ :set_default
28
+ Note that MySQL does not support :set_default, and also treats :no_action as :restrict.
29
+
30
+
31
+ Compatibility
32
+ =============
33
+
34
+ Supports mysql, mysql2, postgresql.
35
+
36
+ Currently tested against Rails 3.2.13 on 2.0.0p0 and Rails 3.2.13, 3.1.8, 3.0.17, and 2.3.14 on Ruby 1.8.7.
@@ -0,0 +1,12 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+
4
+ desc 'Default: run unit tests.'
5
+ task :default => :test
6
+
7
+ desc 'Test the foreign_key_constraints plugin.'
8
+ Rake::TestTask.new(:test) do |t|
9
+ t.libs << 'lib'
10
+ t.pattern = 'test/**/*_test.rb'
11
+ t.verbose = true
12
+ end
@@ -0,0 +1,58 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/foreign_key_saver/version', __FILE__)
3
+
4
+ spec = Gem::Specification.new do |gem|
5
+ gem.name = 'foreign_key_saver'
6
+ gem.version = ForeignKeySaver::VERSION
7
+ gem.summary = "Adds support for foreign key constraints to ActiveRecord schema operations."
8
+ gem.description = <<-EOF
9
+ Adds a add_foreign_key_constraint schema method, and extends the schema dump code to output these
10
+ foreign key constraints.
11
+
12
+ Only MySQL and PostgreSQL are currently supported.
13
+
14
+
15
+ Examples
16
+ ========
17
+
18
+ # adds a constraint on projects.customer_id with parent customers.id
19
+ add_foreign_key :projects, :customer_id, :customers, :id
20
+
21
+ # adds a constraint on projects(a, b) with parent(a, b) with the default RESTRICT update/delete actions
22
+ add_foreign_key "child", ["a", "b"], "parent", ["a", "b"]
23
+
24
+ # adds a constraint with the ON UPDATE action set to CASCADE and the ON DELETE action set to SET NULL
25
+ add_foreign_key 'projects', 'customer_id', 'customers', 'id', :on_update => :cascade, :on_delete => :set_null
26
+
27
+ The following actions are defined:
28
+ :restrict
29
+ :no_action
30
+ :cascade
31
+ :set_null (aka :nullify)
32
+ :set_default
33
+ Note that MySQL does not support :set_default, and also treats :no_action as :restrict.
34
+
35
+
36
+ Compatibility
37
+ =============
38
+
39
+ Supports mysql, mysql2, postgresql.
40
+
41
+ Currently tested against Rails 3.2.13 on 2.0.0p0 and Rails 3.2.13, 3.1.8, 3.0.17, and 2.3.14 on Ruby 1.8.7.
42
+ EOF
43
+ gem.has_rdoc = false
44
+ gem.author = "Will Bryant"
45
+ gem.email = "will.bryant@gmail.com"
46
+ gem.homepage = "http://github.com/willbryant/foreign_key_saver"
47
+
48
+ gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
49
+ gem.files = `git ls-files`.split("\n")
50
+ gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
51
+ gem.require_path = "lib"
52
+
53
+ gem.add_dependency "activerecord"
54
+ gem.add_development_dependency "rake"
55
+ gem.add_development_dependency "mysql"
56
+ gem.add_development_dependency "mysql2"
57
+ gem.add_development_dependency "pg"
58
+ end
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'foreign_key_saver/foreign_key_saver_patches'
@@ -0,0 +1,3 @@
1
+ Rails::Application.initializer :load_foreign_key_saver, :before => :load_config_initializers do
2
+ require 'foreign_key_saver/foreign_key_saver_patches'
3
+ end
@@ -0,0 +1,202 @@
1
+ require 'active_record'
2
+
3
+ module ActiveRecord
4
+ class ForeignKeyConstraint
5
+ ACTIONS = { :restrict => 'RESTRICT', :no_action => 'NO ACTION', :cascade => 'CASCADE',
6
+ :set_null => 'SET NULL', :set_default => 'SET DEFAULT' }.freeze
7
+
8
+ attr_accessor :name, :referenced_table
9
+ attr_reader :key, :referenced_key, :update_action, :delete_action
10
+
11
+ def initialize(name, key, referenced_table, referenced_key, update_action = nil, delete_action = nil)
12
+ self.name = name
13
+ self.key = key
14
+ self.referenced_table = referenced_table
15
+ self.referenced_key = referenced_key
16
+ self.update_action = update_action
17
+ self.delete_action = delete_action
18
+ end
19
+
20
+ def key=(columns)
21
+ @key = columns.is_a?(Enumerable) && columns.length == 1 ? columns.first : columns
22
+ end
23
+
24
+ def referenced_key=(columns)
25
+ @referenced_key = columns.is_a?(Enumerable) && columns.length == 1 ? columns.first : columns
26
+ end
27
+
28
+ def update_action=(action)
29
+ @update_action = ACTIONS.invert[action] || action || :restrict
30
+ end
31
+
32
+ def delete_action=(action)
33
+ @delete_action = ACTIONS.invert[action] || action || :restrict
34
+ end
35
+
36
+ def ==(other)
37
+ name == other.name && key == other.key &&
38
+ referenced_table == other.referenced_table && referenced_key == other.referenced_key &&
39
+ update_action == other.update_action && delete_action == other.delete_action
40
+ end
41
+
42
+ def quote_constraint_action(action)
43
+ ACTIONS[action.to_sym] || action.to_s
44
+ end
45
+
46
+ def to_sql(connection)
47
+ (name.blank? ? "" : "CONSTRAINT #{connection.quote_column_name(name)} ") +
48
+ "FOREIGN KEY (#{connection.quote_column_names(key)}) " +
49
+ "REFERENCES #{connection.quote_table_name(referenced_table)} (#{connection.quote_column_names(referenced_key)}) " +
50
+ "ON UPDATE #{quote_constraint_action(update_action)} " +
51
+ "ON DELETE #{quote_constraint_action(delete_action)}"
52
+ end
53
+
54
+ def to_dump
55
+ dump = "#{key.inspect}, #{referenced_table.inspect}, #{referenced_key.inspect}"
56
+ dump << ", :name => #{name.inspect}" unless name.blank?
57
+ dump << ", :on_update => #{update_action.inspect}" if update_action != :restrict
58
+ dump << ", :on_delete => #{delete_action.inspect}" if delete_action != :restrict
59
+ dump
60
+ end
61
+
62
+ def to_s
63
+ name
64
+ end
65
+
66
+ def self.constraint_action_from_sql(action)
67
+ ACTIONS.invert[action] || action
68
+ end
69
+ end
70
+
71
+ module ConnectionAdapters
72
+ module SchemaStatements
73
+ VALID_FOREIGN_KEY_OPTIONS = [:name, :on_update, :on_delete]
74
+
75
+ def add_foreign_key_constraint(table_name, key, referenced_table, referenced_key, options = {})
76
+ options.assert_valid_keys(VALID_FOREIGN_KEY_OPTIONS)
77
+ execute "ALTER TABLE #{quote_table_name(table_name)} ADD #{ForeignKeyConstraint.new(options[:name], key, referenced_table, referenced_key, options[:on_update], options[:on_delete]).to_sql(self)}"
78
+ end
79
+
80
+ def remove_foreign_key_constraint(table_name, constraint)
81
+ execute "ALTER TABLE #{quote_table_name(table_name)} DROP CONSTRAINT #{quote_column_name(constraint)}"
82
+ end
83
+ end
84
+
85
+ class AbstractAdapter
86
+ def quote_column_names(*column_names)
87
+ column_names.flatten.map {|column_name| quote_column_name(column_name)} * ', '
88
+ end
89
+
90
+ def drop_table_with_foreign_keys(table_name, options = {})
91
+ remove_foreign_key_constraints_referencing(table_name) if tables.include?(table_name) # the if is an optimization for the sake of create_table with :force => true, which is crucial for db:test:prepare performance on mysql as mysql's INFORMATION_SCHEMA implementation is excrutiatingly slow; it's not needed for postgres, but does give a small boost to performance there too
92
+ drop_table_without_foreign_keys(table_name, options)
93
+ end
94
+ alias_method_chain :drop_table, :foreign_keys
95
+ end
96
+
97
+ module MysqlAdapterForeignKeyMethods # common to mysql & mysql2
98
+ def remove_foreign_key_constraint(table_name, constraint)
99
+ execute "ALTER TABLE #{quote_table_name(table_name)} DROP FOREIGN KEY #{quote_column_name(constraint)}"
100
+ end
101
+
102
+ def remove_foreign_key_constraints_referencing(table_name)
103
+ select_rows(
104
+ "SELECT DISTINCT TABLE_NAME, CONSTRAINT_NAME" +
105
+ " FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE" +
106
+ " WHERE REFERENCED_TABLE_SCHEMA = SCHEMA()" +
107
+ " AND REFERENCED_TABLE_NAME = #{quote(table_name)}").each do |table_name, constraint_name|
108
+ remove_foreign_key_constraint(table_name, constraint_name)
109
+ end
110
+ end
111
+
112
+ def foreign_key_constraints_on(table_name)
113
+ self.class.constraints_from_sql(select_one("SHOW CREATE TABLE #{quote_table_name(table_name)}")["Create Table"])
114
+ end
115
+
116
+ def self.constraints_from_sql(create_table_sql)
117
+ # the clauses look like this: CONSTRAINT `ab` FOREIGN KEY (`ac`, `bc`) REFERENCES `parent` (`a`, `b`) ON DELETE SET NULL ON UPDATE CASCADE
118
+ create_table_sql.scan(/CONSTRAINT `([^`]+)` FOREIGN KEY \((`(?:[^`]+)`(?:, `(?:[^`]+)`)*)\) REFERENCES `([^`]+)` \((`(?:[^`]+)`(?:, `(?:[^`]+)`)*)\)(?: ON DELETE (CASCADE|RESTRICT|NO ACTION|SET NULL|SET DEFAULT))?(?: ON UPDATE (CASCADE|RESTRICT|NO ACTION|SET NULL|SET DEFAULT))?/).collect do |capture|
119
+ ForeignKeyConstraint.new(capture[0], columns_from_sql(capture[1]), capture[2], columns_from_sql(capture[3]), capture[5], capture[4])
120
+ end
121
+ end
122
+
123
+ def self.columns_from_sql(column_list_sql)
124
+ column_list_sql.scan(/`([^`]+)`/).collect(&:first)
125
+ end
126
+ end
127
+
128
+ if const_defined?(:MysqlAdapter)
129
+ class MysqlAdapter
130
+ include MysqlAdapterForeignKeyMethods
131
+
132
+ def self.constraints_from_sql(create_table_sql)
133
+ MysqlAdapterForeignKeyMethods.constraints_from_sql(create_table_sql)
134
+ end
135
+
136
+ def self.columns_from_sql(column_list_sql)
137
+ MysqlAdapterForeignKeyMethods.columns_from_sql(column_list_sql)
138
+ end
139
+ end
140
+ end
141
+
142
+ if const_defined?(:Mysql2Adapter)
143
+ class Mysql2Adapter
144
+ include MysqlAdapterForeignKeyMethods
145
+
146
+ def self.constraints_from_sql(create_table_sql)
147
+ MysqlAdapterForeignKeyMethods.constraints_from_sql(create_table_sql)
148
+ end
149
+
150
+ def self.columns_from_sql(column_list_sql)
151
+ MysqlAdapterForeignKeyMethods.columns_from_sql(column_list_sql)
152
+ end
153
+ end
154
+ end
155
+
156
+ if const_defined?(:PostgreSQLAdapter)
157
+ class PostgreSQLAdapter
158
+ def remove_foreign_key_constraints_referencing(table_name)
159
+ select_rows(
160
+ "SELECT referenced.relname, pg_constraint.conname" +
161
+ " FROM pg_constraint, pg_class, pg_class referenced" +
162
+ " WHERE pg_constraint.confrelid = pg_class.oid" +
163
+ " AND pg_class.relname = #{quote(table_name)}" +
164
+ " AND referenced.oid = pg_constraint.conrelid").each do |table_name, constraint_name|
165
+ remove_foreign_key_constraint(table_name, constraint_name)
166
+ end
167
+ end
168
+
169
+ def foreign_key_constraints_on(table_name)
170
+ select_rows(
171
+ "SELECT pg_constraint.conname, pg_get_constraintdef(pg_constraint.oid)" +
172
+ " FROM pg_constraint, pg_class" +
173
+ " WHERE pg_constraint.conrelid = pg_class.oid" +
174
+ " AND pg_class.relname = #{quote(table_name)}").collect do |name, constraintdef|
175
+ self.class.foreign_key_from_sql(name, constraintdef)
176
+ end.compact
177
+ end
178
+
179
+ def self.foreign_key_from_sql(name, foreign_key_sql)
180
+ # the clauses look like this: FOREIGN KEY (ac, bc) REFERENCES parent(ap, bp) ON UPDATE CASCADE ON DELETE SET NULL
181
+ capture = foreign_key_sql.match(/FOREIGN KEY \(((?:\w+)(?:, \w+)*)\) REFERENCES (\w+)\(((?:\w+)(?:, \w+)*)\)(?: ON UPDATE (CASCADE|RESTRICT|NO ACTION|SET NULL|SET DEFAULT))?(?: ON DELETE (CASCADE|RESTRICT|NO ACTION|SET NULL|SET DEFAULT))?/)
182
+ ForeignKeyConstraint.new(name, capture[1].split(', '), capture[2], capture[3].split(', '), capture[4], capture[5]) if capture
183
+ end
184
+ end
185
+ end
186
+ end
187
+
188
+ class SchemaDumper
189
+ def foreign_key_constraints_on(table_name, stream)
190
+ constraints = @connection.foreign_key_constraints_on(table_name)
191
+ constraints.each {|constraint| stream.puts " add_foreign_key_constraint #{table_name.inspect}, #{constraint.to_dump}"}
192
+ stream.puts unless constraints.empty?
193
+ end
194
+
195
+ def tables_with_foreign_key_constraints(stream)
196
+ tables_without_foreign_key_constraints(stream)
197
+ @connection.tables.sort.each {|table| foreign_key_constraints_on(table, stream)}
198
+ end
199
+
200
+ alias_method_chain :tables, :foreign_key_constraints
201
+ end
202
+ end
@@ -0,0 +1,3 @@
1
+ module ForeignKeySaver
2
+ VERSION = '2.0.0'
3
+ end
@@ -0,0 +1,11 @@
1
+ mysql:
2
+ adapter: mysql
3
+ database: fkc_test
4
+ username: root
5
+ mysql2:
6
+ adapter: mysql2
7
+ database: fkc_test
8
+ username: root
9
+ postgresql:
10
+ adapter: postgresql
11
+ database: fkc_test
@@ -0,0 +1,226 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), 'test_helper'))
2
+
3
+ class ForeignKeyConstraintsTest < Test::Unit::TestCase
4
+ def schema_dump
5
+ stream = StringIO.new
6
+ ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream)
7
+ stream.rewind
8
+ stream.read
9
+ end
10
+
11
+ def schema(&block)
12
+ ActiveRecord::Schema.define(:version => 1, &block)
13
+ schema_dump
14
+ end
15
+
16
+ def test_constraints_to_dump
17
+ assert_equal '"ac", "parent", "ap", :name => "cn"',
18
+ ActiveRecord::ForeignKeyConstraint.new('cn', 'ac', 'parent', 'ap').to_dump
19
+
20
+ assert_equal '["ac", "bc"], "parent", ["ap", "bp"], :name => "cn"',
21
+ ActiveRecord::ForeignKeyConstraint.new('cn', ['ac', 'bc'], 'parent', ['ap', 'bp']).to_dump
22
+
23
+ assert_equal '"ac", "parent", "ap", :name => "cn", :on_update => :cascade',
24
+ ActiveRecord::ForeignKeyConstraint.new('cn', 'ac', 'parent', 'ap', :cascade).to_dump
25
+
26
+ assert_equal '"ac", "parent", "ap", :name => "cn", :on_delete => :set_default',
27
+ ActiveRecord::ForeignKeyConstraint.new('cn', 'ac', 'parent', 'ap', nil, :set_default).to_dump
28
+
29
+ assert_equal '"ac", "parent", "ap", :name => "cn", :on_update => :set_null, :on_delete => :no_action',
30
+ ActiveRecord::ForeignKeyConstraint.new('cn', 'ac', 'parent', 'ap', :set_null, :no_action).to_dump
31
+ end
32
+
33
+ if ActiveRecord::Base.connection.class.name == 'ActiveRecord::ConnectionAdapters::PostgreSQLAdapter'
34
+ def test_postgresql_constraints_to_sql
35
+ assert_equal 'CONSTRAINT "cn" FOREIGN KEY ("ac") REFERENCES "parent" ("ap") ON UPDATE RESTRICT ON DELETE RESTRICT',
36
+ ActiveRecord::ForeignKeyConstraint.new('cn', 'ac', 'parent', 'ap').to_sql(ActiveRecord::Base.connection)
37
+
38
+ assert_equal 'CONSTRAINT "cn" FOREIGN KEY ("ac", "bc") REFERENCES "parent" ("ap", "bp") ON UPDATE RESTRICT ON DELETE RESTRICT',
39
+ ActiveRecord::ForeignKeyConstraint.new('cn', ['ac', 'bc'], 'parent', ['ap', 'bp']).to_sql(ActiveRecord::Base.connection)
40
+
41
+ assert_equal 'CONSTRAINT "cn" FOREIGN KEY ("ac") REFERENCES "parent" ("ap") ON UPDATE CASCADE ON DELETE RESTRICT',
42
+ ActiveRecord::ForeignKeyConstraint.new('cn', 'ac', 'parent', 'ap', :cascade).to_sql(ActiveRecord::Base.connection)
43
+
44
+ assert_equal 'CONSTRAINT "cn" FOREIGN KEY ("ac") REFERENCES "parent" ("ap") ON UPDATE RESTRICT ON DELETE SET DEFAULT',
45
+ ActiveRecord::ForeignKeyConstraint.new('cn', 'ac', 'parent', 'ap', nil, :set_default).to_sql(ActiveRecord::Base.connection)
46
+
47
+ assert_equal 'CONSTRAINT "cn" FOREIGN KEY ("ac") REFERENCES "parent" ("ap") ON UPDATE SET NULL ON DELETE NO ACTION',
48
+ ActiveRecord::ForeignKeyConstraint.new('cn', 'ac', 'parent', 'ap', :set_null, :no_action).to_sql(ActiveRecord::Base.connection)
49
+ end
50
+ else
51
+ def test_mysql_constraints_to_sql
52
+ assert_equal 'CONSTRAINT `cn` FOREIGN KEY (`ac`) REFERENCES `parent` (`ap`) ON UPDATE RESTRICT ON DELETE RESTRICT',
53
+ ActiveRecord::ForeignKeyConstraint.new('cn', 'ac', 'parent', 'ap').to_sql(ActiveRecord::Base.connection)
54
+
55
+ assert_equal 'CONSTRAINT `cn` FOREIGN KEY (`ac`, `bc`) REFERENCES `parent` (`ap`, `bp`) ON UPDATE RESTRICT ON DELETE RESTRICT',
56
+ ActiveRecord::ForeignKeyConstraint.new('cn', ['ac', 'bc'], 'parent', ['ap', 'bp']).to_sql(ActiveRecord::Base.connection)
57
+
58
+ assert_equal 'CONSTRAINT `cn` FOREIGN KEY (`ac`) REFERENCES `parent` (`ap`) ON UPDATE CASCADE ON DELETE RESTRICT',
59
+ ActiveRecord::ForeignKeyConstraint.new('cn', 'ac', 'parent', 'ap', :cascade).to_sql(ActiveRecord::Base.connection)
60
+
61
+ assert_equal 'CONSTRAINT `cn` FOREIGN KEY (`ac`) REFERENCES `parent` (`ap`) ON UPDATE RESTRICT ON DELETE SET DEFAULT',
62
+ ActiveRecord::ForeignKeyConstraint.new('cn', 'ac', 'parent', 'ap', nil, :set_default).to_sql(ActiveRecord::Base.connection)
63
+
64
+ assert_equal 'CONSTRAINT `cn` FOREIGN KEY (`ac`) REFERENCES `parent` (`ap`) ON UPDATE SET NULL ON DELETE NO ACTION',
65
+ ActiveRecord::ForeignKeyConstraint.new('cn', 'ac', 'parent', 'ap', :set_null, :no_action).to_sql(ActiveRecord::Base.connection)
66
+ end
67
+
68
+ def test_mysql_columns_from_sql
69
+ assert_equal ['abc'], ActiveRecord::Base.connection.class.columns_from_sql('`abc`')
70
+ assert_equal ['abc', 'def'], ActiveRecord::Base.connection.class.columns_from_sql('`abc`, `def`')
71
+ assert_equal ['abc', 'def', 'ghi'], ActiveRecord::Base.connection.class.columns_from_sql('`abc`, `def`, `ghi`')
72
+ end
73
+
74
+ def test_mysql_constraints_from_sql
75
+ assert_equal [ActiveRecord::ForeignKeyConstraint.new('cn', 'ac', 'parent', 'ap')], ActiveRecord::Base.connection.class.
76
+ constraints_from_sql('CONSTRAINT `cn` FOREIGN KEY (`ac`) REFERENCES `parent` (`ap`)')
77
+
78
+ assert_equal [ActiveRecord::ForeignKeyConstraint.new('cn', ['ac', 'bc'], 'parent', ['ap', 'bp'])], ActiveRecord::Base.connection.class.
79
+ constraints_from_sql('CONSTRAINT `cn` FOREIGN KEY (`ac`, `bc`) REFERENCES `parent` (`ap`, `bp`)')
80
+
81
+ assert_equal [ActiveRecord::ForeignKeyConstraint.new('cn', 'ac', 'parent', 'ap', :cascade)], ActiveRecord::Base.connection.class.
82
+ constraints_from_sql('CONSTRAINT `cn` FOREIGN KEY (`ac`) REFERENCES `parent` (`ap`) ON UPDATE CASCADE')
83
+
84
+ assert_equal [ActiveRecord::ForeignKeyConstraint.new('cn', 'ac', 'parent', 'ap', nil, :set_default)], ActiveRecord::Base.connection.class.
85
+ constraints_from_sql('CONSTRAINT `cn` FOREIGN KEY (`ac`) REFERENCES `parent` (`ap`) ON DELETE SET DEFAULT')
86
+
87
+ assert_equal [ActiveRecord::ForeignKeyConstraint.new('cn', 'ac', 'parent', 'ap', :set_null, :no_action)], ActiveRecord::Base.connection.class.
88
+ constraints_from_sql('CONSTRAINT `cn` FOREIGN KEY (`ac`) REFERENCES `parent` (`ap`) ON DELETE NO ACTION ON UPDATE SET NULL') # mysql has update and delete kinda around the wrong way
89
+ end
90
+ end
91
+
92
+ def test_fkc_define_roundtrip
93
+ dump = schema do
94
+ drop_table :child rescue nil
95
+ drop_table :parent rescue nil
96
+ create_table "parent" do end
97
+ create_table "child" do |t| t.integer :parent_id end
98
+ add_foreign_key_constraint "child", "parent_id", "parent", "id"
99
+ end
100
+ assert_match /create_table "parent"/, dump
101
+ assert_match /add_foreign_key_constraint "child", "parent_id", "parent", "id"/, dump
102
+ end
103
+
104
+ def test_remove_fkc
105
+ dump = schema do
106
+ drop_table :child rescue nil
107
+ drop_table :parent rescue nil
108
+ create_table "parent" do end
109
+ create_table "child" do |t| t.integer :parent_id end
110
+ add_foreign_key_constraint "child", "parent_id", "parent", "id", :name => 'test_fk_name'
111
+ remove_foreign_key_constraint "child", "test_fk_name"
112
+ end
113
+ assert_no_match /add_foreign_key_constraint "child"/, dump
114
+ end
115
+
116
+ def test_drop_parent_with_child_fkcs
117
+ dump = schema do
118
+ drop_table :child rescue nil
119
+ drop_table :parent rescue nil
120
+ create_table "parent" do end
121
+ create_table "child" do |t| t.integer :parent_id end
122
+ add_foreign_key_constraint "child", "parent_id", "parent", "id"
123
+ drop_table :parent
124
+ end
125
+ assert_no_match /create_table "parent"/, dump
126
+ assert_no_match /add_foreign_key_constraint "child", "parent_id", "parent", "id"/, dump
127
+ end
128
+
129
+ def test_force_table_create
130
+ dump = schema do
131
+ drop_table :child rescue nil
132
+ drop_table :parent rescue nil
133
+ create_table "parent" do end
134
+ create_table "child" do |t| t.integer :parent_id end
135
+ add_foreign_key_constraint "child", "parent_id", "parent", "id"
136
+ create_table "parent", :force => true do end
137
+ create_table "child", :force => true do |t| t.integer :parent_id end
138
+ add_foreign_key_constraint "child", "parent_id", "parent", "id"
139
+ create_table "child", :force => true do |t| t.integer :parent_id end
140
+ create_table "parent", :force => true do end
141
+ add_foreign_key_constraint "child", "parent_id", "parent", "id"
142
+ end
143
+ assert_match /create_table "parent"/, dump
144
+ assert_match /add_foreign_key_constraint "child", "parent_id", "parent", "id"/, dump
145
+ end
146
+
147
+ def test_fkc_composite_key
148
+ dump = schema do
149
+ create_table :parent, :force => true do |t| t.integer :afield end
150
+ add_index :parent, [:id, :afield], :unique => true
151
+ create_table :child, :force => true do |t| t.integer :parent_id, :afield end
152
+ add_foreign_key_constraint :child, [:parent_id, :afield], :parent, [:id, :afield]
153
+ end
154
+ assert_match /add_foreign_key_constraint "child", \["parent_id", "afield"\], "parent", \["id", "afield"\]/, dump
155
+ end
156
+
157
+ def test_define_actions
158
+ dump = schema do
159
+ create_table "parent", :force => true do end
160
+ create_table "child", :force => true do |t| t.integer :parent_id end
161
+ add_foreign_key_constraint "child", "parent_id", "parent", "id", :on_update => :cascade, :on_delete => :set_null
162
+ end
163
+ assert_match /add_foreign_key_constraint "child", "parent_id", "parent", "id", :name => "\w+", :on_update => :cascade, :on_delete => :set_null/, dump
164
+ end
165
+
166
+ def test_no_extraneous_actions_in_dump
167
+ dump = schema do
168
+ create_table "parent", :force => true do end
169
+ create_table "child", :force => true do |t| t.integer :parent_id end
170
+ add_foreign_key_constraint "child", "parent_id", "parent", "id", :on_delete => :restrict
171
+ end
172
+ assert_no_match /add_foreign_key_constraint "child".*on_update/, dump
173
+ assert_no_match /add_foreign_key_constraint "child".*on_delete/, dump
174
+ end
175
+
176
+ def test_explicit_names
177
+ dump = schema do
178
+ create_table "parent", :force => true do end
179
+ create_table "child", :force => true do |t| t.integer :parent_id end
180
+ add_foreign_key_constraint "child", "parent_id", "parent", "id", :name => 'test_fk_name'
181
+ end
182
+ assert_match /add_foreign_key_constraint "child", "parent_id", "parent", "id", :name => "test_fk_name"/, dump
183
+ end
184
+
185
+ def test_multiple_parents
186
+ dump = schema do
187
+ create_table "parent1", :force => true do end
188
+ create_table "parent2", :force => true do end
189
+ create_table "child", :force => true do |t| t.integer :parent1_id, :parent2_id end
190
+ add_foreign_key_constraint "child", "parent1_id", "parent1", "id"
191
+ add_foreign_key_constraint "child", "parent2_id", "parent2", "id"
192
+ end
193
+ assert_match /add_foreign_key_constraint "child", "parent1_id", "parent1", "id"/, dump
194
+ assert_match /add_foreign_key_constraint "child", "parent2_id", "parent2", "id"/, dump
195
+ end
196
+
197
+ def test_multiple_children
198
+ dump = schema do
199
+ create_table "parent", :force => true do end
200
+ create_table "child1", :force => true do |t| t.integer :parent_id end
201
+ create_table "child2", :force => true do |t| t.integer :parent_id end
202
+ add_foreign_key_constraint "child1", "parent_id", "parent", "id"
203
+ add_foreign_key_constraint "child2", "parent_id", "parent", "id"
204
+ end
205
+ assert_match /add_foreign_key_constraint "child1", "parent_id", "parent", "id"/, dump
206
+ assert_match /add_foreign_key_constraint "child2", "parent_id", "parent", "id"/, dump
207
+ end
208
+
209
+ def test_remove_only_named
210
+ dump = schema do
211
+ create_table "parent1", :force => true do end
212
+ create_table "parent2", :force => true do end
213
+ create_table "child1", :force => true do |t| t.integer :parent1_id, :parent2_id end
214
+ create_table "child2", :force => true do |t| t.integer :parent1_id, :parent2_id end
215
+ add_foreign_key_constraint "child1", "parent1_id", "parent1", "id", :name => "test_a"
216
+ add_foreign_key_constraint "child1", "parent2_id", "parent2", "id", :name => "test_b"
217
+ add_foreign_key_constraint "child2", "parent1_id", "parent1", "id", :name => "test_c"
218
+ add_foreign_key_constraint "child2", "parent2_id", "parent2", "id", :name => "test_d"
219
+ remove_foreign_key_constraint :child1, :test_b
220
+ end
221
+ assert_match /add_foreign_key_constraint "child1", "parent1_id", "parent1", "id", :name => "test_a"/, dump
222
+ assert_no_match /add_foreign_key_constraint "child1", "parent2_id", "parent2", "id", :name => "test_b"/, dump
223
+ assert_match /add_foreign_key_constraint "child2", "parent1_id", "parent1", "id", :name => "test_c"/, dump
224
+ assert_match /add_foreign_key_constraint "child2", "parent2_id", "parent2", "id", :name => "test_d"/, dump
225
+ end
226
+ end
@@ -0,0 +1,29 @@
1
+ RAILS_ROOT = File.expand_path("../../..")
2
+ if File.exist?("#{RAILS_ROOT}/config/boot.rb")
3
+ require "#{RAILS_ROOT}/config/boot.rb"
4
+ else
5
+ require 'rubygems'
6
+ end
7
+
8
+ puts "Rails: #{ENV['RAILS_VERSION'] || 'default'}"
9
+ gem 'activesupport', ENV['RAILS_VERSION']
10
+ gem 'activerecord', ENV['RAILS_VERSION']
11
+
12
+ require 'test/unit'
13
+ require 'active_support'
14
+ require 'active_support/test_case'
15
+ require 'active_record'
16
+
17
+ begin
18
+ require 'ruby-debug'
19
+ Debugger.start
20
+ rescue LoadError
21
+ # ruby-debug not installed, no debugging for you
22
+ end
23
+
24
+ ActiveRecord::Base.configurations = YAML::load(IO.read(File.join(File.dirname(__FILE__), "database.yml")))
25
+ configuration = ActiveRecord::Base.configurations[ENV['RAILS_ENV']]
26
+ raise "use RAILS_ENV=#{ActiveRecord::Base.configurations.keys.sort.join '/'} to test this plugin" unless configuration
27
+ ActiveRecord::Base.establish_connection configuration
28
+
29
+ require File.expand_path(File.join(File.dirname(__FILE__), '../init')) # load foreign_key_constraints
metadata ADDED
@@ -0,0 +1,181 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: foreign_key_saver
3
+ version: !ruby/object:Gem::Version
4
+ hash: 15
5
+ prerelease:
6
+ segments:
7
+ - 2
8
+ - 0
9
+ - 0
10
+ version: 2.0.0
11
+ platform: ruby
12
+ authors:
13
+ - Will Bryant
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2013-04-21 00:00:00 Z
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: activerecord
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ hash: 3
29
+ segments:
30
+ - 0
31
+ version: "0"
32
+ type: :runtime
33
+ version_requirements: *id001
34
+ - !ruby/object:Gem::Dependency
35
+ name: rake
36
+ prerelease: false
37
+ requirement: &id002 !ruby/object:Gem::Requirement
38
+ none: false
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ hash: 3
43
+ segments:
44
+ - 0
45
+ version: "0"
46
+ type: :development
47
+ version_requirements: *id002
48
+ - !ruby/object:Gem::Dependency
49
+ name: mysql
50
+ prerelease: false
51
+ requirement: &id003 !ruby/object:Gem::Requirement
52
+ none: false
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ hash: 3
57
+ segments:
58
+ - 0
59
+ version: "0"
60
+ type: :development
61
+ version_requirements: *id003
62
+ - !ruby/object:Gem::Dependency
63
+ name: mysql2
64
+ prerelease: false
65
+ requirement: &id004 !ruby/object:Gem::Requirement
66
+ none: false
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ hash: 3
71
+ segments:
72
+ - 0
73
+ version: "0"
74
+ type: :development
75
+ version_requirements: *id004
76
+ - !ruby/object:Gem::Dependency
77
+ name: pg
78
+ prerelease: false
79
+ requirement: &id005 !ruby/object:Gem::Requirement
80
+ none: false
81
+ requirements:
82
+ - - ">="
83
+ - !ruby/object:Gem::Version
84
+ hash: 3
85
+ segments:
86
+ - 0
87
+ version: "0"
88
+ type: :development
89
+ version_requirements: *id005
90
+ description: |
91
+ Adds a add_foreign_key_constraint schema method, and extends the schema dump code to output these
92
+ foreign key constraints.
93
+
94
+ Only MySQL and PostgreSQL are currently supported.
95
+
96
+
97
+ Examples
98
+ ========
99
+
100
+ # adds a constraint on projects.customer_id with parent customers.id
101
+ add_foreign_key :projects, :customer_id, :customers, :id
102
+
103
+ # adds a constraint on projects(a, b) with parent(a, b) with the default RESTRICT update/delete actions
104
+ add_foreign_key "child", ["a", "b"], "parent", ["a", "b"]
105
+
106
+ # adds a constraint with the ON UPDATE action set to CASCADE and the ON DELETE action set to SET NULL
107
+ add_foreign_key 'projects', 'customer_id', 'customers', 'id', :on_update => :cascade, :on_delete => :set_null
108
+
109
+ The following actions are defined:
110
+ :restrict
111
+ :no_action
112
+ :cascade
113
+ :set_null (aka :nullify)
114
+ :set_default
115
+ Note that MySQL does not support :set_default, and also treats :no_action as :restrict.
116
+
117
+
118
+ Compatibility
119
+ =============
120
+
121
+ Supports mysql, mysql2, postgresql.
122
+
123
+ Currently tested against Rails 3.2.13 on 2.0.0p0 and Rails 3.2.13, 3.1.8, 3.0.17, and 2.3.14 on Ruby 1.8.7.
124
+
125
+ email: will.bryant@gmail.com
126
+ executables: []
127
+
128
+ extensions: []
129
+
130
+ extra_rdoc_files: []
131
+
132
+ files:
133
+ - .gitignore
134
+ - MIT-LICENSE
135
+ - README
136
+ - Rakefile
137
+ - foreign_key_saver.gemspec
138
+ - init.rb
139
+ - lib/foreign_key_saver.rb
140
+ - lib/foreign_key_saver/foreign_key_saver_patches.rb
141
+ - lib/foreign_key_saver/version.rb
142
+ - test/database.yml
143
+ - test/foreign_key_constraints_test.rb
144
+ - test/test_helper.rb
145
+ homepage: http://github.com/willbryant/foreign_key_saver
146
+ licenses: []
147
+
148
+ post_install_message:
149
+ rdoc_options: []
150
+
151
+ require_paths:
152
+ - lib
153
+ required_ruby_version: !ruby/object:Gem::Requirement
154
+ none: false
155
+ requirements:
156
+ - - ">="
157
+ - !ruby/object:Gem::Version
158
+ hash: 3
159
+ segments:
160
+ - 0
161
+ version: "0"
162
+ required_rubygems_version: !ruby/object:Gem::Requirement
163
+ none: false
164
+ requirements:
165
+ - - ">="
166
+ - !ruby/object:Gem::Version
167
+ hash: 3
168
+ segments:
169
+ - 0
170
+ version: "0"
171
+ requirements: []
172
+
173
+ rubyforge_project:
174
+ rubygems_version: 1.8.15
175
+ signing_key:
176
+ specification_version: 3
177
+ summary: Adds support for foreign key constraints to ActiveRecord schema operations.
178
+ test_files:
179
+ - test/database.yml
180
+ - test/foreign_key_constraints_test.rb
181
+ - test/test_helper.rb