aspgems-foreign_key_migrations 2.0.0.beta1
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.
- data/.gitignore +3 -0
- data/CHANGELOG +109 -0
- data/Gemfile +3 -0
- data/MIT-LICENSE +20 -0
- data/README.md +91 -0
- data/Rakefile +69 -0
- data/foreign_key_migrations.gemspec +28 -0
- data/init.rb +1 -0
- data/install.rb +1 -0
- data/lib/foreign_key_migrations.rb +48 -0
- data/lib/foreign_key_migrations/active_record/base.rb +36 -0
- data/lib/foreign_key_migrations/active_record/connection_adapters/schema_statements.rb +23 -0
- data/lib/foreign_key_migrations/active_record/connection_adapters/table_definition.rb +50 -0
- data/lib/foreign_key_migrations/active_record/migration.rb +62 -0
- data/lib/foreign_key_migrations/version.rb +3 -0
- data/lib/generators/foreign_key_migrations/migration_generator.rb +38 -0
- data/lib/generators/foreign_key_migrations/templates/migration.rb +10 -0
- data/spec/aaa_create_tables_spec.rb +9 -0
- data/spec/connections/mysql/connection.rb +16 -0
- data/spec/connections/mysql2/connection.rb +16 -0
- data/spec/connections/postgresql/connection.rb +13 -0
- data/spec/migration_spec.rb +257 -0
- data/spec/models/comment.rb +3 -0
- data/spec/models/post.rb +3 -0
- data/spec/models/user.rb +2 -0
- data/spec/references_spec.rb +48 -0
- data/spec/schema/schema.rb +24 -0
- data/spec/schema_dumper_spec.rb +32 -0
- data/spec/schema_spec.rb +65 -0
- data/spec/spec_helper.rb +15 -0
- data/spec/support/helpers.rb +8 -0
- data/spec/support/matchers/foreign_key_migrations_matchers.rb +2 -0
- data/spec/support/matchers/have_index.rb +52 -0
- data/spec/support/matchers/reference.rb +66 -0
- metadata +144 -0
@@ -0,0 +1,50 @@
|
|
1
|
+
module ForeignKeyMigrations::ActiveRecord::ConnectionAdapters
|
2
|
+
module TableDefinition
|
3
|
+
def self.included(base)
|
4
|
+
base.class_eval do
|
5
|
+
alias_method_chain :column, :foreign_key_migrations
|
6
|
+
alias_method_chain :primary_key, :foreign_key_migrations
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def primary_key_with_foreign_key_migrations(name, options = {})
|
11
|
+
column(name, :primary_key, options)
|
12
|
+
end
|
13
|
+
|
14
|
+
def indices
|
15
|
+
@indices ||= []
|
16
|
+
end
|
17
|
+
|
18
|
+
def column_with_foreign_key_migrations(name, type, options = {})
|
19
|
+
column_without_foreign_key_migrations(name, type, options)
|
20
|
+
references = ActiveRecord::Base.references(self.name, name, options)
|
21
|
+
if references
|
22
|
+
ForeignKeyMigrations.set_default_update_and_delete_actions!(options)
|
23
|
+
foreign_key(name, references.first, references.last, options)
|
24
|
+
if index = fkm_index_options(options)
|
25
|
+
# append [column_name, index_options] pair
|
26
|
+
self.indices << [name, ForeignKeyMigrations.options_for_index(index)]
|
27
|
+
end
|
28
|
+
elsif options[:index]
|
29
|
+
self.indices << [name, ForeignKeyMigrations.options_for_index(options[:index])]
|
30
|
+
end
|
31
|
+
self
|
32
|
+
end
|
33
|
+
|
34
|
+
# Some people liked this; personally I've decided against using it but I'll keep it nonetheless
|
35
|
+
def belongs_to(table, options = {})
|
36
|
+
options = options.merge(:references => table)
|
37
|
+
options[:on_delete] = options.delete(:dependent) if options.has_key?(:dependent)
|
38
|
+
column("#{table.to_s.singularize}_id".to_sym, :integer, options)
|
39
|
+
end
|
40
|
+
|
41
|
+
protected
|
42
|
+
def fkm_index_options(options)
|
43
|
+
options.fetch(:index, fkm_use_auto_index?)
|
44
|
+
end
|
45
|
+
|
46
|
+
def fkm_use_auto_index?
|
47
|
+
ForeignKeyMigrations.auto_index && !ActiveRecord::Schema.defining?
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module ForeignKeyMigrations::ActiveRecord
|
2
|
+
module Migration
|
3
|
+
def self.included(base) # :nodoc
|
4
|
+
base.extend(ClassMethods)
|
5
|
+
end
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
# Overrides ActiveRecord#add_column and adds foreign key if column references other column
|
9
|
+
#
|
10
|
+
# add_column('comments', 'post_id', :integer)
|
11
|
+
# # creates a column and adds foreign key on posts(id)
|
12
|
+
#
|
13
|
+
# add_column('comments', 'post_id', :integer, :on_update => :cascade, :on_delete => :cascade)
|
14
|
+
# # creates a column and adds foreign key on posts(id) with cascade actions on update and on delete
|
15
|
+
#
|
16
|
+
# add_column('comments', 'post_id', :integer, :index => true)
|
17
|
+
# # creates a column and adds foreign key on posts(id)
|
18
|
+
# # additionally adds index on posts(id)
|
19
|
+
#
|
20
|
+
# add_column('comments', 'post_id', :integer, :index => { :unique => true, :name => 'comments_post_id_unique_index' }))
|
21
|
+
# # creates a column and adds foreign key on posts(id)
|
22
|
+
# # additionally adds unique index on posts(id) named comments_post_id_unique_index
|
23
|
+
#
|
24
|
+
# add_column('addresses', 'citizen_id', :integer, :references => :users
|
25
|
+
# # creates a column and adds foreign key on users(id)
|
26
|
+
#
|
27
|
+
# add_column('addresses', 'citizen_id', :integer, :references => [:users, :uuid]
|
28
|
+
# # creates a column and adds foreign key on users(uuid)
|
29
|
+
#
|
30
|
+
def add_column(table_name, column_name, type, options = {})
|
31
|
+
super
|
32
|
+
handle_column_options(table_name, column_name, options)
|
33
|
+
end
|
34
|
+
|
35
|
+
def change_column(table_name, column_name, type, options = {})
|
36
|
+
super
|
37
|
+
remove_foreign_key_if_exists(table_name, column_name)
|
38
|
+
handle_column_options(table_name, column_name, options)
|
39
|
+
end
|
40
|
+
|
41
|
+
protected
|
42
|
+
def handle_column_options(table_name, column_name, options)
|
43
|
+
references = ActiveRecord::Base.references(table_name, column_name, options)
|
44
|
+
if references
|
45
|
+
ForeignKeyMigrations.set_default_update_and_delete_actions!(options)
|
46
|
+
add_foreign_key(table_name, column_name, references.first, references.last, options)
|
47
|
+
if index = options.fetch(:index, ForeignKeyMigrations.auto_index)
|
48
|
+
add_index(table_name, column_name, ForeignKeyMigrations.options_for_index(index))
|
49
|
+
end
|
50
|
+
elsif options[:index]
|
51
|
+
add_index(table_name, column_name, ForeignKeyMigrations.options_for_index(options[:index]))
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def remove_foreign_key_if_exists(table_name, column_name)
|
56
|
+
foreign_keys = ActiveRecord::Base.connection.foreign_keys(table_name.to_s)
|
57
|
+
fk = foreign_keys.detect { |fk| fk.table_name == table_name.to_s && fk.column_names == Array(column_name).collect(&:to_s) }
|
58
|
+
remove_foreign_key(table_name, fk.name) if fk
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'rails/generators'
|
2
|
+
require 'rails/generators/active_record'
|
3
|
+
|
4
|
+
module ForeignKeyMigrations
|
5
|
+
module Generators
|
6
|
+
class MigrationGenerator < ::ActiveRecord::Generators::Base
|
7
|
+
argument :name, :default => 'create_foreign_keys'
|
8
|
+
|
9
|
+
def self.source_root
|
10
|
+
File.expand_path(File.join(File.dirname(__FILE__), 'templates'))
|
11
|
+
end
|
12
|
+
|
13
|
+
def create_migration_file
|
14
|
+
set_local_assigns!
|
15
|
+
migration_template 'migration.rb', "db/migrate/#{file_name}"
|
16
|
+
end
|
17
|
+
|
18
|
+
protected
|
19
|
+
attr_reader :foreign_keys
|
20
|
+
|
21
|
+
def set_local_assigns!
|
22
|
+
@foreign_keys = determine_foreign_keys
|
23
|
+
end
|
24
|
+
|
25
|
+
def determine_foreign_keys
|
26
|
+
foreign_keys = []
|
27
|
+
connection = ::ActiveRecord::Base.connection
|
28
|
+
connection.tables.each do |table_name|
|
29
|
+
connection.columns(table_name).each do |column|
|
30
|
+
references = ::ActiveRecord::Base.references(table_name, column.name)
|
31
|
+
foreign_keys << ::RedhillonrailsCore::ActiveRecord::ConnectionAdapters::ForeignKeyDefinition.new(nil, table_name, column.name, references.first, references.last) if references
|
32
|
+
end
|
33
|
+
end
|
34
|
+
foreign_keys
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
3
|
+
|
4
|
+
# This is not a test really. It just here to create schema
|
5
|
+
# The filename begins with "aaa" to ensure this is executed initially
|
6
|
+
|
7
|
+
ActiveRecord::Migration.suppress_messages do
|
8
|
+
load_schema
|
9
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
print "Using MySQL\n"
|
2
|
+
require 'logger'
|
3
|
+
|
4
|
+
ActiveRecord::Base.configurations = {
|
5
|
+
'fkm' => {
|
6
|
+
:adapter => 'mysql',
|
7
|
+
:database => 'fkm_unittest',
|
8
|
+
:username => 'fkm',
|
9
|
+
:encoding => 'utf8',
|
10
|
+
:socket => '/var/run/mysqld/mysqld.sock',
|
11
|
+
:min_messages => 'warning'
|
12
|
+
}
|
13
|
+
|
14
|
+
}
|
15
|
+
|
16
|
+
ActiveRecord::Base.establish_connection 'fkm'
|
@@ -0,0 +1,16 @@
|
|
1
|
+
print "Using MySQL2\n"
|
2
|
+
require 'logger'
|
3
|
+
|
4
|
+
ActiveRecord::Base.configurations = {
|
5
|
+
'fkm' => {
|
6
|
+
:adapter => 'mysql2',
|
7
|
+
:database => 'fkm_unittest',
|
8
|
+
:username => 'fkm',
|
9
|
+
:encoding => 'utf8',
|
10
|
+
:socket => '/var/run/mysqld/mysqld.sock',
|
11
|
+
:min_messages => 'warning'
|
12
|
+
}
|
13
|
+
|
14
|
+
}
|
15
|
+
|
16
|
+
ActiveRecord::Base.establish_connection 'fkm'
|
@@ -0,0 +1,257 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
3
|
+
|
4
|
+
require 'models/post'
|
5
|
+
require 'models/comment'
|
6
|
+
|
7
|
+
describe ActiveRecord::Base do
|
8
|
+
|
9
|
+
it "should respond to references" do
|
10
|
+
ActiveRecord::Base.should respond_to :references
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
14
|
+
|
15
|
+
describe ActiveRecord::Migration do
|
16
|
+
include ForeignKeyMigrationsHelpers
|
17
|
+
|
18
|
+
context "when table is created" do
|
19
|
+
|
20
|
+
before(:each) do
|
21
|
+
@model = Post
|
22
|
+
end
|
23
|
+
|
24
|
+
it "should create foreign keys" do
|
25
|
+
create_table(@model, :user_id => {},
|
26
|
+
:author_id => { :references => :users },
|
27
|
+
:member_id => { :references => nil } )
|
28
|
+
@model.should reference(:users, :id).on(:user_id)
|
29
|
+
@model.should reference(:users, :id).on(:author_id)
|
30
|
+
@model.should_not reference.on(:member_id)
|
31
|
+
end
|
32
|
+
|
33
|
+
it "should use default on_cascade action" do
|
34
|
+
ForeignKeyMigrations.on_update = :cascade
|
35
|
+
create_table(@model, :user_id => {})
|
36
|
+
ForeignKeyMigrations.on_update = nil
|
37
|
+
@model.should reference.on(:user_id).on_update(:cascade)
|
38
|
+
end
|
39
|
+
|
40
|
+
it "should use default on_cascade action" do
|
41
|
+
ForeignKeyMigrations.on_delete = :cascade
|
42
|
+
create_table(@model, :user_id => {})
|
43
|
+
ForeignKeyMigrations.on_delete = nil
|
44
|
+
@model.should reference.on(:user_id).on_delete(:cascade)
|
45
|
+
end
|
46
|
+
|
47
|
+
it "should create an index if specified" do
|
48
|
+
create_table(@model, :state => { :index => true })
|
49
|
+
@model.should have_index.on(:state)
|
50
|
+
end
|
51
|
+
|
52
|
+
it "should create a multiple-column index if specified" do
|
53
|
+
create_table(@model, :city => {},
|
54
|
+
:state => { :index => {:with => :city} } )
|
55
|
+
@model.should have_index.on([:state, :city])
|
56
|
+
end
|
57
|
+
|
58
|
+
it "should auto-index foreign keys only" do
|
59
|
+
ForeignKeyMigrations.auto_index = true
|
60
|
+
create_table(@model, :user_id => {},
|
61
|
+
:application_id => { :references => nil },
|
62
|
+
:state => {})
|
63
|
+
@model.should have_index.on(:user_id)
|
64
|
+
@model.should_not have_index.on(:application_id)
|
65
|
+
@model.should_not have_index.on(:state)
|
66
|
+
ForeignKeyMigrations.auto_index = nil
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
|
71
|
+
context "when column is added" do
|
72
|
+
|
73
|
+
before(:each) do
|
74
|
+
@model = Comment
|
75
|
+
end
|
76
|
+
|
77
|
+
it "should create foreign key" do
|
78
|
+
add_column(:post_id, :integer) do
|
79
|
+
@model.should reference(:posts, :id).on(:post_id)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
it "should create foreign key to explicity given table" do
|
84
|
+
add_column(:author_id, :integer, :references => :users) do
|
85
|
+
@model.should reference(:users, :id).on(:author_id)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
it "should create foreign key to explicity given table and column name" do
|
90
|
+
add_column(:author_login, :string, :references => [:users, :login]) do
|
91
|
+
@model.should reference(:users, :login).on(:author_login)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
it "should create foreign key to the same table on parent_id" do
|
96
|
+
add_column(:parent_id, :integer) do
|
97
|
+
@model.should reference(@model.table_name, :id).on(:parent_id)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
it "shouldn't create foreign key if column doesn't look like foreign key" do
|
102
|
+
add_column(:views_count, :integer) do
|
103
|
+
@model.should_not reference.on(:views_count)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
it "shouldnt't create foreign key if specified explicity" do
|
108
|
+
add_column(:post_id, :integer, :references => nil) do
|
109
|
+
@model.should_not reference.on(:post_id)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
it "should create an index if specified" do
|
114
|
+
add_column(:post_id, :integer, :index => true) do
|
115
|
+
@model.should have_index.on(:post_id)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
it "should create a unique index if specified" do
|
120
|
+
add_column(:post_id, :integer, :index => { :unique => true }) do
|
121
|
+
@model.should have_unique_index.on(:post_id)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
it "should allow custom name for index" do
|
126
|
+
index_name = 'comments_post_id_unique_index'
|
127
|
+
add_column(:post_id, :integer, :index => { :unique => true, :name => index_name }) do
|
128
|
+
@model.should have_unique_index(:name => index_name).on(:post_id)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
it "should auto-index if specified in global options" do
|
133
|
+
ForeignKeyMigrations.auto_index = true
|
134
|
+
add_column(:post_id, :integer) do
|
135
|
+
@model.should have_index.on(:post_id)
|
136
|
+
end
|
137
|
+
ForeignKeyMigrations.auto_index = false
|
138
|
+
end
|
139
|
+
|
140
|
+
it "should auto-index foreign keys only" do
|
141
|
+
ForeignKeyMigrations.auto_index = true
|
142
|
+
add_column(:state, :integer) do
|
143
|
+
@model.should_not have_index.on(:state)
|
144
|
+
end
|
145
|
+
ForeignKeyMigrations.auto_index = false
|
146
|
+
end
|
147
|
+
|
148
|
+
it "should allow to overwrite auto_index options in column definition" do
|
149
|
+
ForeignKeyMigrations.auto_index = true
|
150
|
+
add_column(:post_id, :integer, :index => false) do
|
151
|
+
# MySQL creates an index on foreign by default
|
152
|
+
# and we can do nothing with that
|
153
|
+
unless mysql?
|
154
|
+
@model.should_not have_index.on(:post_id)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
ForeignKeyMigrations.auto_index = false
|
158
|
+
end
|
159
|
+
|
160
|
+
it "should use default on_update action" do
|
161
|
+
ForeignKeyMigrations.on_update = :cascade
|
162
|
+
add_column(:post_id, :integer) do
|
163
|
+
@model.should reference.on(:post_id).on_update(:cascade)
|
164
|
+
end
|
165
|
+
ForeignKeyMigrations.on_update = nil
|
166
|
+
end
|
167
|
+
|
168
|
+
it "should use default on_delete action" do
|
169
|
+
ForeignKeyMigrations.on_delete = :cascade
|
170
|
+
add_column(:post_id, :integer) do
|
171
|
+
@model.should reference.on(:post_id).on_delete(:cascade)
|
172
|
+
end
|
173
|
+
ForeignKeyMigrations.on_delete = nil
|
174
|
+
end
|
175
|
+
|
176
|
+
it "should allow to overwrite default actions" do
|
177
|
+
ForeignKeyMigrations.on_delete = :cascade
|
178
|
+
ForeignKeyMigrations.on_update = :restrict
|
179
|
+
add_column(:post_id, :integer, :on_update => :set_null, :on_delete => :set_null) do
|
180
|
+
@model.should reference.on(:post_id).on_delete(:set_null).on_update(:set_null)
|
181
|
+
end
|
182
|
+
ForeignKeyMigrations.on_delete = nil
|
183
|
+
end
|
184
|
+
|
185
|
+
protected
|
186
|
+
def add_column(column_name, *args)
|
187
|
+
table = @model.table_name
|
188
|
+
ActiveRecord::Migration.suppress_messages do
|
189
|
+
ActiveRecord::Migration.add_column(table, column_name, *args)
|
190
|
+
@model.reset_column_information
|
191
|
+
yield if block_given?
|
192
|
+
ActiveRecord::Migration.remove_column(table, column_name)
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
end
|
197
|
+
|
198
|
+
context "when column is changed" do
|
199
|
+
|
200
|
+
before(:each) do
|
201
|
+
@model = Comment
|
202
|
+
end
|
203
|
+
|
204
|
+
it "should create foreign key" do
|
205
|
+
change_column :user, :string, :references => [:users, :login]
|
206
|
+
@model.should reference(:users, :login).on(:user)
|
207
|
+
change_column :user, :string, :references => nil
|
208
|
+
end
|
209
|
+
|
210
|
+
context "and initially references to users table" do
|
211
|
+
|
212
|
+
it "should have foreign key" do
|
213
|
+
@model.should reference(:users)
|
214
|
+
end
|
215
|
+
|
216
|
+
it "should drop foreign key afterwards" do
|
217
|
+
change_column :user_id, :integer, :references => :members
|
218
|
+
@model.should_not reference(:users)
|
219
|
+
change_column :user_id, :integer, :references => :users
|
220
|
+
end
|
221
|
+
|
222
|
+
it "should reference pointed table afterwards" do
|
223
|
+
change_column :user_id, :integer, :references => :members
|
224
|
+
@model.should reference(:members)
|
225
|
+
end
|
226
|
+
|
227
|
+
end
|
228
|
+
|
229
|
+
protected
|
230
|
+
def change_column(column_name, *args)
|
231
|
+
table = @model.table_name
|
232
|
+
ActiveRecord::Migration.suppress_messages do
|
233
|
+
ActiveRecord::Migration.change_column(table, column_name, *args)
|
234
|
+
@model.reset_column_information
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
end
|
239
|
+
|
240
|
+
def foreign_key(model, column)
|
241
|
+
columns = Array(column).collect(&:to_s)
|
242
|
+
model.foreign_keys.detect { |fk| fk.table_name == model.table_name && fk.column_names == columns }
|
243
|
+
end
|
244
|
+
|
245
|
+
def create_table(model, columns_with_options)
|
246
|
+
ActiveRecord::Migration.suppress_messages do
|
247
|
+
ActiveRecord::Migration.create_table model.table_name, :force => true do |t|
|
248
|
+
columns_with_options.each_pair do |column, options|
|
249
|
+
t.integer column, options
|
250
|
+
end
|
251
|
+
end
|
252
|
+
model.reset_column_information
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
end
|
257
|
+
|