foreign_key_saver 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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