foreign_key_saver 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -0
- data/MIT-LICENSE +20 -0
- data/README +36 -0
- data/Rakefile +12 -0
- data/foreign_key_saver.gemspec +58 -0
- data/init.rb +1 -0
- data/lib/foreign_key_saver.rb +3 -0
- data/lib/foreign_key_saver/foreign_key_saver_patches.rb +202 -0
- data/lib/foreign_key_saver/version.rb +3 -0
- data/test/database.yml +11 -0
- data/test/foreign_key_constraints_test.rb +226 -0
- data/test/test_helper.rb +29 -0
- metadata +181 -0
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
test/log/*
|
data/MIT-LICENSE
ADDED
@@ -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.
|
data/Rakefile
ADDED
@@ -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,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
|
data/test/database.yml
ADDED
@@ -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
|
data/test/test_helper.rb
ADDED
@@ -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
|