schema_auto_foreign_keys 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,18 @@
1
+ module SchemaAutoForeignKeys
2
+ module Middleware
3
+ module Schema
4
+ module Define
5
+ def around(env)
6
+ fk_override = { :auto_create => false, :auto_index => false }
7
+ save = Hash[fk_override.keys.collect{|key| [key, SchemaPlus::ForeignKeys.config.send(key)]}]
8
+ begin
9
+ SchemaPlus::ForeignKeys.config.update_attributes(fk_override)
10
+ yield env
11
+ ensure
12
+ SchemaPlus::ForeignKeys.config.update_attributes(save)
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,3 @@
1
+ module SchemaAutoForeignKeys
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,30 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'schema_auto_foreign_keys/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "schema_auto_foreign_keys"
8
+ gem.version = SchemaAutoForeignKeys::VERSION
9
+ gem.authors = ["ronen barzel"]
10
+ gem.email = ["ronen@barzel.org"]
11
+ gem.summary = %q{Automatically define foreign key constraints in ActiveRecord}
12
+ gem.description = %q{In an ActiveRecord migration, set the default to create a foreign key and index for all columns that define relatoins.}
13
+ gem.homepage = "https://github.com/SchemaPlus/schema_auto_foreign_keys"
14
+ gem.license = "MIT"
15
+
16
+ gem.files = `git ls-files -z`.split("\x0")
17
+ gem.executables = gem.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
19
+ gem.require_paths = ["lib"]
20
+
21
+ gem.add_dependency "schema_plus_foreign_keys", "~> 0.1"
22
+ gem.add_dependency "schema_plus_indexes", "~> 0.2"
23
+
24
+ gem.add_development_dependency "bundler", "~> 1.7"
25
+ gem.add_development_dependency "rake", "~> 10.0"
26
+ gem.add_development_dependency "rspec", "~> 3.0"
27
+ gem.add_development_dependency "schema_dev", "~> 3.5"
28
+ gem.add_development_dependency "simplecov"
29
+ gem.add_development_dependency "simplecov-gem-profile"
30
+ end
@@ -0,0 +1,9 @@
1
+ ruby:
2
+ - 2.1.5
3
+ activerecord:
4
+ - 4.2.0
5
+ - 4.2.1
6
+ db:
7
+ - mysql2
8
+ - sqlite3
9
+ - postgresql
@@ -0,0 +1,403 @@
1
+ # encoding: utf-8
2
+ require 'spec_helper'
3
+
4
+ describe ActiveRecord::Migration do
5
+
6
+ before(:each) do
7
+ define_schema do
8
+
9
+ create_table :users do |t|
10
+ t.string :login, :index => { :unique => true }
11
+ end
12
+
13
+ create_table :members do |t|
14
+ t.string :login
15
+ end
16
+
17
+ create_table :comments do |t|
18
+ t.string :content
19
+ t.integer :user
20
+ t.integer :user_id
21
+ t.foreign_key :user_id, :users, :primary_key => :id
22
+ end
23
+
24
+ create_table :posts do |t|
25
+ t.string :content
26
+ end
27
+ end
28
+ class User < ::ActiveRecord::Base ; end
29
+ class Post < ::ActiveRecord::Base ; end
30
+ class Comment < ::ActiveRecord::Base ; end
31
+ end
32
+
33
+ around(:each) do |example|
34
+ with_fk_config(:auto_create => true, :auto_index => true) { example.run }
35
+ end
36
+
37
+ context "when table is created" do
38
+
39
+ before(:each) do
40
+ @model = Post
41
+ end
42
+
43
+ it "creates auto foreign keys" do
44
+ create_table(@model) do |t|
45
+ t.integer :user_id
46
+ end
47
+ expect(@model).to reference(:users, :id).on(:user_id)
48
+ end
49
+
50
+ it "respects explicit foreign key" do
51
+ create_table(@model) do |t|
52
+ t.integer :author_id, :foreign_key => { :references => :users }
53
+ end
54
+ expect(@model).to reference(:users, :id).on(:author_id)
55
+ expect(@model).to have_index.on(:author_id)
56
+ end
57
+
58
+ it "suppresses auto foreign key" do
59
+ create_table(@model) do |t|
60
+ t.integer :member_id, :foreign_key => false
61
+ end
62
+ expect(@model).not_to reference.on(:member_id)
63
+ expect(@model).not_to have_index.on(:member_id)
64
+ end
65
+
66
+ it "suppresses auto foreign key using shortcut" do
67
+ create_table(@model) do |t|
68
+ t.integer :member_id, :references => nil
69
+ end
70
+ expect(@model).not_to reference.on(:member_id)
71
+ expect(@model).not_to have_index.on(:member_id)
72
+ end
73
+
74
+ [:references, :belongs_to].each do |reftype|
75
+
76
+ context "when define #{reftype}" do
77
+
78
+ before(:each) do
79
+ @model = Comment
80
+ end
81
+
82
+ it "auto creates foreign key" do
83
+ create_reference(reftype, :post)
84
+ expect(@model).to reference(:posts, :id).on(:post_id)
85
+ end
86
+
87
+ it "does not create a foreign_key if polymorphic" do
88
+ create_reference(reftype, :post, :polymorphic => true)
89
+ expect(@model).not_to reference(:posts, :id).on(:post_id)
90
+ end
91
+
92
+ it "does not create a foreign_key with :foreign_key => false" do
93
+ create_reference(reftype, :post, :foreign_key => false)
94
+ expect(@model).not_to reference(:posts, :id).on(:post_id)
95
+ end
96
+
97
+ it "should create an index implicitly" do
98
+ create_reference(reftype, :post)
99
+ expect(@model).to have_index.on(:post_id)
100
+ end
101
+
102
+ it "should create exactly one index explicitly (#157)" do
103
+ create_reference(reftype, :post, :index => true)
104
+ expect(@model).to have_index.on(:post_id)
105
+ end
106
+
107
+ it "should respect :unique (#157)" do
108
+ create_reference(reftype, :post, :index => :unique)
109
+ expect(@model).to have_unique_index.on(:post_id)
110
+ end
111
+
112
+ it "should create a two-column index if polymophic and index requested" do
113
+ create_reference(reftype, :post, :polymorphic => true, :index => true)
114
+ expect(@model).to have_index.on([:post_id, :post_type])
115
+ end
116
+
117
+ protected
118
+
119
+ def create_reference(reftype, column_name, *args)
120
+ create_table(@model) do |t|
121
+ t.send reftype, column_name, *args
122
+ end
123
+ end
124
+
125
+ end
126
+ end
127
+
128
+ it "creates auto-index on foreign keys only" do
129
+ create_table(@model) do |t|
130
+ t.integer :user_id
131
+ t.integer :application_id, :references => nil
132
+ t.integer :state
133
+ end
134
+ expect(@model).to have_index.on(:user_id)
135
+ expect(@model).not_to have_index.on(:application_id)
136
+ expect(@model).not_to have_index.on(:state)
137
+ end
138
+
139
+ it "handles very long index names" do
140
+ table = ("ta"*15 + "_id")
141
+ column = ("co"*15 + "_id")
142
+ expect {
143
+ ActiveRecord::Migration.create_table table do |t|
144
+ t.integer column, foreign_key: { references: :members, name: "verylong" }
145
+ end
146
+ }.not_to raise_error
147
+ expect(ActiveRecord::Base.connection.indexes(table).first.columns.first).to eq column
148
+ end
149
+
150
+ it "overrides foreign key auto_create positively" do
151
+ with_fk_config(:auto_create => false) do
152
+ create_table @model, :foreign_keys => {:auto_create => true} do |t|
153
+ t.integer :user_id
154
+ end
155
+ expect(@model).to reference(:users, :id).on(:user_id)
156
+ end
157
+ end
158
+
159
+ it "overrides foreign key auto_create negatively" do
160
+ with_fk_config(:auto_create => true) do
161
+ create_table @model, :foreign_keys => {:auto_create => false} do |t|
162
+ t.integer :user_id
163
+ end
164
+ expect(@model).not_to reference.on(:user_id)
165
+ end
166
+ end
167
+
168
+ it "overrides foreign key auto_index positively" do
169
+ with_fk_config(:auto_index => false) do
170
+ create_table @model, :foreign_keys => {:auto_index => true} do |t|
171
+ t.integer :user_id
172
+ end
173
+ expect(@model).to have_index.on(:user_id)
174
+ end
175
+ end
176
+
177
+ it "overrides foreign key auto_index negatively", :mysql => :skip do
178
+ with_fk_config(:auto_index => true) do
179
+ create_table @model, :foreign_keys => {:auto_index => false} do |t|
180
+ t.integer :user_id
181
+ end
182
+ expect(@model).not_to have_index.on(:user_id)
183
+ end
184
+ end
185
+
186
+ it "disables auto-index for a column", :mysql => :skip do
187
+ with_fk_config(:auto_index => true) do
188
+ create_table @model do |t|
189
+ t.integer :user_id, :index => false
190
+ end
191
+ expect(@model).not_to have_index.on(:user_id)
192
+ end
193
+ end
194
+
195
+ end
196
+
197
+ context "when table is changed", :sqlite3 => :skip do
198
+ before(:each) do
199
+ @model = Post
200
+ end
201
+ [false, true].each do |bulk|
202
+ suffix = bulk ? ' with :bulk option' : ""
203
+
204
+ it "auto creates a foreign key constraint"+suffix do
205
+ change_table(@model, :bulk => bulk) do |t|
206
+ t.integer :user_id
207
+ end
208
+ expect(@model).to reference(:users, :id).on(:user_id)
209
+ end
210
+
211
+ context "migrate down" do
212
+ it "removes an auto foreign key and index"+suffix do
213
+ create_table Comment do |t|
214
+ t.integer :user_id
215
+ end
216
+ expect(Comment).to reference(:users, :id).on(:user_id)
217
+ expect(Comment).to have_index.on(:user_id)
218
+ migration = Class.new ::ActiveRecord::Migration do
219
+ define_method(:change) {
220
+ change_table("comments", :bulk => bulk) do |t|
221
+ t.integer :user_id
222
+ end
223
+ }
224
+ end
225
+ migration.migrate(:down)
226
+ Comment.reset_column_information
227
+ expect(Comment).not_to reference(:users, :id).on(:user_id)
228
+ expect(Comment).not_to have_index.on(:user_id)
229
+ end
230
+ end
231
+ end
232
+ end
233
+
234
+ context "when table is renamed", :postgresql => :only do
235
+
236
+ before(:each) do
237
+ @model = Comment
238
+ create_table @model do |t|
239
+ t.integer :user_id
240
+ t.integer :xyz, :index => true
241
+ end
242
+ ActiveRecord::Migration.rename_table @model.table_name, :newname
243
+ end
244
+
245
+ it "should rename fk indexes" do
246
+ index = ActiveRecord::Base.connection.indexes(:newname).find(&its.columns == ['user_id'])
247
+ expect(index.name).to match(/^fk__newname_/)
248
+ end
249
+
250
+ end
251
+
252
+ context "when column is added", :sqlite3 => :skip do
253
+
254
+ before(:each) do
255
+ @model = Comment
256
+ end
257
+
258
+ it "auto creates foreign key" do
259
+ add_column(:post_id, :integer) do
260
+ expect(@model).to reference(:posts, :id).on(:post_id)
261
+ end
262
+ end
263
+
264
+ it "respects explicit foreign key" do
265
+ add_column(:author_id, :integer, :foreign_key => { :references => :users }) do
266
+ expect(@model).to reference(:users, :id).on(:author_id)
267
+ end
268
+ end
269
+
270
+ it "doesn't create foreign key if column doesn't look like foreign key" do
271
+ add_column(:views_count, :integer) do
272
+ expect(@model).not_to reference.on(:views_count)
273
+ end
274
+ end
275
+
276
+ it "doesn't create foreign key if declined explicitly" do
277
+ add_column(:post_id, :integer, :foreign_key => false) do
278
+ expect(@model).not_to reference.on(:post_id)
279
+ end
280
+ end
281
+
282
+ it "shouldn't create foreign key if declined explicitly by shorthand" do
283
+ add_column(:post_id, :integer, :references => nil) do
284
+ expect(@model).not_to reference.on(:post_id)
285
+ end
286
+ end
287
+
288
+ it "creates auto index" do
289
+ add_column(:post_id, :integer) do
290
+ expect(@model).to have_index.on(:post_id)
291
+ end
292
+ end
293
+
294
+ it "does not create auto-index for non-foreign keys" do
295
+ add_column(:state, :integer) do
296
+ expect(@model).not_to have_index.on(:state)
297
+ end
298
+ end
299
+
300
+ # MySQL creates an index on foreign key and we can't override that
301
+ it "doesn't create auto-index if declined explicitly", :mysql => :skip do
302
+ add_column(:post_id, :integer, :index => false) do
303
+ expect(@model).not_to have_index.on(:post_id)
304
+ end
305
+ end
306
+
307
+ protected
308
+ def add_column(column_name, *args)
309
+ table = @model.table_name
310
+ ActiveRecord::Migration.add_column(table, column_name, *args)
311
+ @model.reset_column_information
312
+ yield if block_given?
313
+ ActiveRecord::Migration.remove_column(table, column_name)
314
+ end
315
+
316
+ end
317
+
318
+ context "when column is changed" do
319
+
320
+ before(:each) do
321
+ @model = Comment
322
+ end
323
+
324
+ context "with foreign keys", :sqlite3 => :skip do
325
+
326
+ context "and initially references to users table" do
327
+
328
+ before(:each) do
329
+ create_table @model do |t|
330
+ t.integer :user_id
331
+ end
332
+ end
333
+
334
+ it "should have foreign key" do
335
+ expect(@model).to reference(:users)
336
+ end
337
+
338
+ it "should drop foreign key if requested to do so" do
339
+ change_column :user_id, :integer, :foreign_key => { :references => nil }
340
+ expect(@model).not_to reference(:users)
341
+ end
342
+
343
+ it "should remove auto-created index if foreign key is removed", :mysql => :skip do
344
+ expect(@model).to have_index.on(:user_id) # sanity check that index was auto-created
345
+ change_column :user_id, :integer, :foreign_key => { :references => nil }
346
+ expect(@model).not_to have_index.on(:user_id)
347
+ end
348
+
349
+ end
350
+
351
+ context "if column defined without foreign key but with index" do
352
+ before(:each) do
353
+ create_table @model do |t|
354
+ t.integer :user_id, :foreign_key => false, :index => true
355
+ end
356
+ end
357
+
358
+ it "should create the index" do
359
+ expect(@model).to have_index.on(:user_id)
360
+ end
361
+
362
+ it "adding foreign key should not fail due to attempt to auto-create existing index" do
363
+ expect { change_column :user_id, :integer, :foreign_key => true }.to_not raise_error
364
+ end
365
+ end
366
+ end
367
+
368
+ context "without foreign keys" do
369
+
370
+ it "doesn't auto-add foreign keys" do
371
+ create_table @model do |t|
372
+ t.integer :user_id, :foreign_key => false
373
+ t.string :other_column
374
+ end
375
+ with_fk_auto_create do
376
+ change_column :other_column, :text
377
+ end
378
+ expect(@model).to_not reference(:users)
379
+ end
380
+
381
+ end
382
+
383
+ protected
384
+ def change_column(column_name, *args)
385
+ table = @model.table_name
386
+ ActiveRecord::Migration.change_column(table, column_name, *args)
387
+ @model.reset_column_information
388
+ end
389
+
390
+ end
391
+
392
+ def create_table(model, opts={}, &block)
393
+ ActiveRecord::Migration.create_table model.table_name, opts.merge(:force => true), &block
394
+ model.reset_column_information
395
+ end
396
+
397
+ def change_table(model, opts={}, &block)
398
+ ActiveRecord::Migration.change_table model.table_name, opts, &block
399
+ model.reset_column_information
400
+ end
401
+
402
+ end
403
+