schema_plus_foreign_keys 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +9 -0
  3. data/.travis.yml +21 -0
  4. data/Gemfile +5 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +200 -0
  7. data/Rakefile +9 -0
  8. data/gemfiles/Gemfile.base +4 -0
  9. data/gemfiles/activerecord-4.2.0/Gemfile.base +3 -0
  10. data/gemfiles/activerecord-4.2.0/Gemfile.mysql2 +10 -0
  11. data/gemfiles/activerecord-4.2.0/Gemfile.postgresql +10 -0
  12. data/gemfiles/activerecord-4.2.0/Gemfile.sqlite3 +10 -0
  13. data/gemfiles/activerecord-4.2.1/Gemfile.base +3 -0
  14. data/gemfiles/activerecord-4.2.1/Gemfile.mysql2 +10 -0
  15. data/gemfiles/activerecord-4.2.1/Gemfile.postgresql +10 -0
  16. data/gemfiles/activerecord-4.2.1/Gemfile.sqlite3 +10 -0
  17. data/lib/schema_plus/foreign_keys.rb +78 -0
  18. data/lib/schema_plus/foreign_keys/active_record/base.rb +33 -0
  19. data/lib/schema_plus/foreign_keys/active_record/connection_adapters/abstract_adapter.rb +168 -0
  20. data/lib/schema_plus/foreign_keys/active_record/connection_adapters/foreign_key_definition.rb +137 -0
  21. data/lib/schema_plus/foreign_keys/active_record/connection_adapters/mysql2_adapter.rb +126 -0
  22. data/lib/schema_plus/foreign_keys/active_record/connection_adapters/postgresql_adapter.rb +89 -0
  23. data/lib/schema_plus/foreign_keys/active_record/connection_adapters/sqlite3_adapter.rb +77 -0
  24. data/lib/schema_plus/foreign_keys/active_record/connection_adapters/table_definition.rb +108 -0
  25. data/lib/schema_plus/foreign_keys/active_record/migration/command_recorder.rb +29 -0
  26. data/lib/schema_plus/foreign_keys/middleware/dumper.rb +88 -0
  27. data/lib/schema_plus/foreign_keys/middleware/migration.rb +147 -0
  28. data/lib/schema_plus/foreign_keys/middleware/model.rb +15 -0
  29. data/lib/schema_plus/foreign_keys/middleware/mysql.rb +20 -0
  30. data/lib/schema_plus/foreign_keys/middleware/sql.rb +27 -0
  31. data/lib/schema_plus/foreign_keys/version.rb +5 -0
  32. data/lib/schema_plus_foreign_keys.rb +1 -0
  33. data/schema_dev.yml +9 -0
  34. data/schema_plus_foreign_keys.gemspec +31 -0
  35. data/spec/deprecation_spec.rb +161 -0
  36. data/spec/foreign_key_definition_spec.rb +34 -0
  37. data/spec/foreign_key_spec.rb +207 -0
  38. data/spec/migration_spec.rb +570 -0
  39. data/spec/named_schemas_spec.rb +136 -0
  40. data/spec/schema_dumper_spec.rb +257 -0
  41. data/spec/spec_helper.rb +60 -0
  42. data/spec/support/reference.rb +79 -0
  43. metadata +221 -0
@@ -0,0 +1,570 @@
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, :force => true do |t|
10
+ t.string :login, :index => { :unique => true }
11
+ end
12
+
13
+ create_table :members, :force => true do |t|
14
+ t.string :login
15
+ end
16
+
17
+ create_table :comments, :force => true 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, :force => true 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
+ context "when table is created" do
34
+
35
+ before(:each) do
36
+ @model = Post
37
+ end
38
+
39
+ it "should enable foreign keys", :sqlite3 => :only do
40
+ sql = []
41
+ allow(@model.connection).to receive(:execute) { |str| sql << str }
42
+ recreate_table(@model) do |t|
43
+ t.integer :user, :foreign_key => true
44
+ end
45
+ expect(sql.join('; ')).to match(/PRAGMA FOREIGN_KEYS = ON.*CREATE TABLE "posts"/)
46
+ end
47
+
48
+ it "should create foreign key with default reference" do
49
+ recreate_table(@model) do |t|
50
+ t.integer :user, :foreign_key => true
51
+ end
52
+ expect(@model).to reference(:users, :id).on(:user)
53
+ end
54
+
55
+ it "should create foreign key with default column" do
56
+ recreate_table(@model) do |t|
57
+ t.integer :user_id
58
+ t.foreign_key :users
59
+ end
60
+ expect(@model).to reference(:users, :id).on(:user_id)
61
+ end
62
+
63
+ it "should create foreign key with different reference" do
64
+ recreate_table(@model) do |t|
65
+ t.integer :author_id, :foreign_key => { :references => :users }
66
+ end
67
+ expect(@model).to reference(:users, :id).on(:author_id)
68
+ end
69
+
70
+ it "should create foreign key without modifying input hash" do
71
+ hash = { :references => :users }
72
+ hash_original = hash.dup
73
+ recreate_table(@model) do |t|
74
+ t.integer :author_id, :foreign_key => hash
75
+ end
76
+ expect(hash).to eq(hash_original)
77
+ end
78
+
79
+ it "should create foreign key without modifying input hash" do
80
+ hash = { :references => :users }
81
+ hash_original = hash.dup
82
+ recreate_table(@model) do |t|
83
+ t.references :author, :foreign_key => hash
84
+ end
85
+ expect(hash).to eq(hash_original)
86
+ end
87
+
88
+ it "should create foreign key with different reference using shortcut" do
89
+ recreate_table(@model) do |t|
90
+ t.integer :author_id, :references => :users
91
+ end
92
+ expect(@model).to reference(:users, :id).on(:author_id)
93
+ end
94
+
95
+ it "should create foreign key with default name" do
96
+ recreate_table @model do |t|
97
+ t.integer :user_id, :foreign_key => true
98
+ end
99
+ expect(@model).to reference(:users, :id).with_name("fk_#{@model.table_name}_user_id")
100
+ end
101
+
102
+ it "should create foreign key with specified name" do
103
+ recreate_table @model do |t|
104
+ t.integer :user_id, :foreign_key => { :name => "wugga" }
105
+ end
106
+ expect(@model).to reference(:users, :id).with_name("wugga")
107
+ end
108
+
109
+ it "should allow multiple foreign keys to be made" do
110
+ recreate_table(@model) do |t|
111
+ t.integer :user_id, :references => :users
112
+ t.integer :updater_id, :references => :users
113
+ end
114
+ expect(@model).to reference(:users, :id).on(:user_id)
115
+ expect(@model).to reference(:users, :id).on(:updater_id)
116
+ end
117
+
118
+ it "should suppress foreign key" do
119
+ recreate_table(@model) do |t|
120
+ t.integer :member_id, :foreign_key => false
121
+ end
122
+ expect(@model).not_to reference.on(:member_id)
123
+ end
124
+
125
+ it "should suppress foreign key using shortcut" do
126
+ recreate_table(@model) do |t|
127
+ t.integer :member_id, :references => nil
128
+ end
129
+ expect(@model).not_to reference.on(:member_id)
130
+ end
131
+
132
+ it "should create foreign key using t.belongs_to" do
133
+ recreate_table(@model) do |t|
134
+ t.belongs_to :user, :foreign_key => true
135
+ end
136
+ expect(@model).to reference(:users, :id).on(:user_id)
137
+ end
138
+
139
+ it "should not create foreign key using t.belongs_to with :polymorphic => true" do
140
+ recreate_table(@model) do |t|
141
+ t.belongs_to :user, :polymorphic => true
142
+ end
143
+ expect(@model).not_to reference(:users, :id).on(:user_id)
144
+ end
145
+
146
+ it "should create foreign key using t.references" do
147
+ recreate_table(@model) do |t|
148
+ t.references :user, :foreign_key => true
149
+ end
150
+ expect(@model).to reference(:users, :id).on(:user_id)
151
+ end
152
+
153
+ it "should not create foreign key using t.references with :foreign_key => false" do
154
+ recreate_table(@model) do |t|
155
+ t.references :user, :foreign_key => false
156
+ end
157
+ expect(@model).not_to reference(:users, :id).on(:user_id)
158
+ end
159
+
160
+ it "should not create foreign key using t.references with :polymorphic => true" do
161
+ recreate_table(@model) do |t|
162
+ t.references :user, :polymorphic => true
163
+ end
164
+ expect(@model).not_to reference(:users, :id).on(:user_id)
165
+ end
166
+
167
+ it "should create foreign key to the same table on parent_id" do
168
+ recreate_table(@model) do |t|
169
+ t.integer :parent_id, foreign_key: true
170
+ end
171
+ expect(@model).to reference(@model.table_name, :id).on(:parent_id)
172
+ end
173
+
174
+ actions = [:cascade, :restrict, :nullify, :set_default, :no_action]
175
+
176
+ actions.each do |action|
177
+ if action == :set_default
178
+ if_action_supported = { :mysql => :skip }
179
+ if_action_unsupported = { :mysql => :only }
180
+ else
181
+ if_action_supported = { :if => true }
182
+ if_action_unsupported = { :if => false }
183
+ end
184
+
185
+ it "should create and detect on_update #{action.inspect}", if_action_supported do
186
+ recreate_table @model do |t|
187
+ t.integer :user_id, :foreign_key => { :on_update => action }
188
+ end
189
+ expect(@model).to reference.on(:user_id).on_update(action)
190
+ end
191
+
192
+ it "should create and detect on_update #{action.inspect} using shortcut", if_action_supported do
193
+ recreate_table @model do |t|
194
+ t.integer :user_id, :on_update => action
195
+ end
196
+ expect(@model).to reference.on(:user_id).on_update(action)
197
+ end
198
+
199
+ it "should raise a not-implemented error for on_update => #{action}", if_action_unsupported do
200
+ expect {
201
+ recreate_table @model do |t|
202
+ t.integer :user_id, :foreign_key => { :on_update => action }
203
+ end
204
+ }.to raise_error(NotImplementedError)
205
+ end
206
+
207
+ it "should create and detect on_delete #{action.inspect}", if_action_supported do
208
+ recreate_table @model do |t|
209
+ t.integer :user_id, :foreign_key => { :on_delete => action }
210
+ end
211
+ expect(@model).to reference.on(:user_id).on_delete(action)
212
+ end
213
+
214
+ it "should create and detect on_delete #{action.inspect} using shortcut", if_action_supported do
215
+ recreate_table @model do |t|
216
+ t.integer :user_id, :on_delete => action
217
+ end
218
+ expect(@model).to reference.on(:user_id).on_delete(action)
219
+ end
220
+
221
+ it "should raise a not-implemented error for on_delete => #{action}", if_action_unsupported do
222
+ expect {
223
+ recreate_table @model do |t|
224
+ t.integer :user_id, :foreign_key => { :on_delete => action }
225
+ end
226
+ }.to raise_error(NotImplementedError)
227
+ end
228
+
229
+ end
230
+
231
+ [false, true, :initially_deferred].each do |status|
232
+ it "should create and detect deferrable #{status.inspect}", :mysql => :skip do
233
+ recreate_table @model do |t|
234
+ t.integer :user_id, :on_delete => :cascade, :deferrable => status
235
+ end
236
+ expect(@model).to reference.on(:user_id).deferrable(status)
237
+ end
238
+ end
239
+
240
+ it "should use default on_delete action" do
241
+ with_fk_config(:on_delete => :cascade) do
242
+ recreate_table @model do |t|
243
+ t.integer :user_id, foreign_key: true
244
+ end
245
+ expect(@model).to reference.on(:user_id).on_delete(:cascade)
246
+ end
247
+ end
248
+
249
+ it "should override on_update action per table" do
250
+ with_fk_config(:on_update => :cascade) do
251
+ recreate_table @model, :foreign_keys => {:on_update => :restrict} do |t|
252
+ t.integer :user_id, foreign_key: true
253
+ end
254
+ expect(@model).to reference.on(:user_id).on_update(:restrict)
255
+ end
256
+ end
257
+
258
+ it "should override on_delete action per table" do
259
+ with_fk_config(:on_delete => :cascade) do
260
+ recreate_table @model, :foreign_keys => {:on_delete => :restrict} do |t|
261
+ t.integer :user_id, foreign_key: true
262
+ end
263
+ expect(@model).to reference.on(:user_id).on_delete(:restrict)
264
+ end
265
+ end
266
+
267
+ it "should override on_update action per column" do
268
+ with_fk_config(:on_update => :cascade) do
269
+ recreate_table @model, :foreign_keys => {:on_update => :restrict} do |t|
270
+ t.integer :user_id, :foreign_key => { :on_update => :nullify }
271
+ end
272
+ expect(@model).to reference.on(:user_id).on_update(:nullify)
273
+ end
274
+ end
275
+
276
+ it "should override on_delete action per column" do
277
+ with_fk_config(:on_delete => :cascade) do
278
+ recreate_table @model, :foreign_keys => {:on_delete => :restrict} do |t|
279
+ t.integer :user_id, :foreign_key => { :on_delete => :nullify }
280
+ end
281
+ expect(@model).to reference.on(:user_id).on_delete(:nullify)
282
+ end
283
+ end
284
+
285
+ it "should raise an error for an invalid on_update action" do
286
+ expect {
287
+ recreate_table @model do |t|
288
+ t.integer :user_id, :foreign_key => { :on_update => :invalid }
289
+ end
290
+ }.to raise_error(ArgumentError)
291
+ end
292
+
293
+ it "should raise an error for an invalid on_delete action" do
294
+ expect {
295
+ recreate_table @model do |t|
296
+ t.integer :user_id, :foreign_key => { :on_delete => :invalid }
297
+ end
298
+ }.to raise_error(ArgumentError)
299
+ end
300
+
301
+ end
302
+
303
+ context "when table is changed" do
304
+ before(:each) do
305
+ @model = Post
306
+ end
307
+ [false, true].each do |bulk|
308
+ suffix = bulk ? ' with :bulk option' : ""
309
+
310
+ it "should create a foreign key constraint"+suffix, :sqlite3 => :skip do
311
+ change_table(@model, :bulk => bulk) do |t|
312
+ t.integer :user_id, foreign_key: true
313
+ end
314
+ expect(@model).to reference(:users, :id).on(:user_id)
315
+ end
316
+
317
+ context "migrate down" do
318
+ it "should remove a foreign key constraint"+suffix, :sqlite3 => :skip do
319
+ Comment.reset_column_information
320
+ expect(Comment).to reference(:users, :id).on(:user_id)
321
+ migration = Class.new ::ActiveRecord::Migration do
322
+ define_method(:change) {
323
+ change_table("comments", :bulk => bulk) do |t|
324
+ t.integer :user_id, foreign_key: true
325
+ end
326
+ }
327
+ end
328
+ ActiveRecord::Migration.suppress_messages do
329
+ migration.migrate(:down)
330
+ end
331
+ Comment.reset_column_information
332
+ expect(Comment).not_to reference(:users, :id).on(:user_id)
333
+ end
334
+ end
335
+
336
+ it "should create a foreign key constraint using :references"+suffix, :sqlite3 => :skip do
337
+ change_table(@model, :bulk => bulk) do |t|
338
+ t.references :user, foreign_key: true
339
+ end
340
+ expect(@model).to reference(:users, :id).on(:user_id)
341
+ end
342
+
343
+ it "should create a foreign key constraint using :belongs_to"+suffix, :sqlite3 => :skip do
344
+ change_table(@model, :bulk => bulk) do |t|
345
+ t.belongs_to :user, foreign_key: true
346
+ end
347
+ expect(@model).to reference(:users, :id).on(:user_id)
348
+ end
349
+ end
350
+ end
351
+
352
+ context "when column is added", :sqlite3 => :skip do
353
+
354
+ before(:each) do
355
+ @model = Comment
356
+ end
357
+
358
+ it "should create foreign key" do
359
+ add_column(:post_id, :integer, foreign_key: true) do
360
+ expect(@model).to reference(:posts, :id).on(:post_id)
361
+ end
362
+ end
363
+
364
+ it "should create foreign key to explicitly given table" do
365
+ add_column(:author_id, :integer, :foreign_key => { :references => :users }) do
366
+ expect(@model).to reference(:users, :id).on(:author_id)
367
+ end
368
+ end
369
+
370
+ it "should create foreign key to explicitly given table using shortcut" do
371
+ add_column(:author_id, :integer, :references => :users) do
372
+ expect(@model).to reference(:users, :id).on(:author_id)
373
+ end
374
+ end
375
+
376
+ it "should create foreign key to explicitly given table and column name" do
377
+ add_column(:author_login, :string, :foreign_key => { :references => [:users, :login]}) do
378
+ expect(@model).to reference(:users, :login).on(:author_login)
379
+ end
380
+ end
381
+
382
+ it "should create foreign key to the same table on parent_id" do
383
+ add_column(:parent_id, :integer, foreign_key: true) do
384
+ expect(@model).to reference(@model.table_name, :id).on(:parent_id)
385
+ end
386
+ end
387
+
388
+ it "should use default on_update action" do
389
+ SchemaPlus::ForeignKeys.config.on_update = :cascade
390
+ add_column(:post_id, :integer, foreign_key: true) do
391
+ expect(@model).to reference.on(:post_id).on_update(:cascade)
392
+ end
393
+ SchemaPlus::ForeignKeys.config.on_update = nil
394
+ end
395
+
396
+ it "should use default on_delete action" do
397
+ SchemaPlus::ForeignKeys.config.on_delete = :cascade
398
+ add_column(:post_id, :integer, foreign_key: true) do
399
+ expect(@model).to reference.on(:post_id).on_delete(:cascade)
400
+ end
401
+ SchemaPlus::ForeignKeys.config.on_delete = nil
402
+ end
403
+
404
+ it "should allow to overwrite default actions" do
405
+ SchemaPlus::ForeignKeys.config.on_delete = :cascade
406
+ SchemaPlus::ForeignKeys.config.on_update = :restrict
407
+ add_column(:post_id, :integer, :foreign_key => { :on_update => :nullify, :on_delete => :nullify}) do
408
+ expect(@model).to reference.on(:post_id).on_delete(:nullify).on_update(:nullify)
409
+ end
410
+ SchemaPlus::ForeignKeys.config.on_delete = nil
411
+ end
412
+
413
+ it "should create foreign key with default name" do
414
+ add_column(:post_id, :integer, foreign_key: true) do
415
+ expect(@model).to reference(:posts, :id).with_name("fk_#{@model.table_name}_post_id")
416
+ end
417
+ end
418
+
419
+ protected
420
+ def add_column(column_name, *args)
421
+ table = @model.table_name
422
+ ActiveRecord::Migration.add_column(table, column_name, *args)
423
+ @model.reset_column_information
424
+ yield if block_given?
425
+ ActiveRecord::Migration.remove_column(table, column_name)
426
+ end
427
+
428
+ end
429
+
430
+
431
+ context "when column is changed" do
432
+
433
+ before(:each) do
434
+ @model = Comment
435
+ end
436
+
437
+ context "with foreign keys", :sqlite3 => :skip do
438
+
439
+ it "should create foreign key" do
440
+ change_column :user, :string, :foreign_key => { :references => [:users, :login] }
441
+ expect(@model).to reference(:users, :login).on(:user)
442
+ end
443
+
444
+ context "and initially references to users table" do
445
+
446
+ before(:each) do
447
+ recreate_table @model do |t|
448
+ t.integer :user_id, foreign_key: true
449
+ end
450
+ end
451
+
452
+ it "should have foreign key" do
453
+ expect(@model).to reference(:users)
454
+ end
455
+
456
+ it "should drop foreign key if it is no longer valid" do
457
+ change_column :user_id, :integer, :foreign_key => { :references => :members }
458
+ expect(@model).not_to reference(:users)
459
+ end
460
+
461
+ it "should drop foreign key if requested to do so" do
462
+ change_column :user_id, :integer, :foreign_key => { :references => nil }
463
+ expect(@model).not_to reference(:users)
464
+ end
465
+
466
+ it "should reference pointed table afterwards if new one is created" do
467
+ change_column :user_id, :integer, :foreign_key => { :references => :members }
468
+ expect(@model).to reference(:members)
469
+ end
470
+
471
+ it "should maintain foreign key if it's unaffected by change" do
472
+ change_column :user_id, :integer, :default => 0
473
+ expect(@model).to reference(:users)
474
+ end
475
+
476
+ end
477
+
478
+ end
479
+
480
+ protected
481
+ def change_column(column_name, *args)
482
+ table = @model.table_name
483
+ ActiveRecord::Migration.suppress_messages do
484
+ ActiveRecord::Migration.change_column(table, column_name, *args)
485
+ @model.reset_column_information
486
+ end
487
+ end
488
+
489
+ end
490
+
491
+ context "when column is removed", :sqlite3 => :skip do
492
+ before(:each) do
493
+ @model = Comment
494
+ recreate_table @model do |t|
495
+ t.integer :post_id, foreign_key: true
496
+ end
497
+ end
498
+
499
+ it "should remove a foreign key" do
500
+ expect(@model).to reference(:posts)
501
+ remove_column(:post_id)
502
+ expect(@model).not_to reference(:posts)
503
+ end
504
+
505
+ protected
506
+ def remove_column(column_name)
507
+ table = @model.table_name
508
+ ActiveRecord::Migration.suppress_messages do
509
+ ActiveRecord::Migration.remove_column(table, column_name)
510
+ @model.reset_column_information
511
+ end
512
+ end
513
+ end
514
+
515
+
516
+ context "when table is renamed" do
517
+
518
+ before(:each) do
519
+ @model = Comment
520
+ recreate_table @model do |t|
521
+ t.integer :user_id, foreign_key: true
522
+ t.integer :xyz, :index => true
523
+ end
524
+ ActiveRecord::Migration.suppress_messages do
525
+ ActiveRecord::Migration.rename_table @model.table_name, :newname
526
+ end
527
+ end
528
+
529
+ it "should rename foreign key constraints", :sqlite3 => :skip do
530
+ expect(ActiveRecord::Base.connection.foreign_keys(:newname).first.name).to match(/newname/)
531
+ end
532
+
533
+ end
534
+
535
+
536
+ context "when table with more than one fk constraint is renamed", :sqlite3 => :skip do
537
+
538
+ before(:each) do
539
+ @model = Comment
540
+ recreate_table @model do |t|
541
+ t.integer :user_id
542
+ t.integer :member_id
543
+ end
544
+ ActiveRecord::Migration.suppress_messages do
545
+ ActiveRecord::Migration.rename_table @model.table_name, :newname
546
+ end
547
+ end
548
+
549
+ it "should rename foreign key constraints" do
550
+ names = ActiveRecord::Base.connection.foreign_keys(:newname).map(&:name)
551
+ expect(names.grep(/newname/)).to eq(names)
552
+ end
553
+ end
554
+
555
+ def recreate_table(model, opts={}, &block)
556
+ ActiveRecord::Migration.suppress_messages do
557
+ ActiveRecord::Migration.create_table model.table_name, opts.merge(:force => true), &block
558
+ end
559
+ model.reset_column_information
560
+ end
561
+
562
+ def change_table(model, opts={}, &block)
563
+ ActiveRecord::Migration.suppress_messages do
564
+ ActiveRecord::Migration.change_table model.table_name, opts, &block
565
+ end
566
+ model.reset_column_information
567
+ end
568
+
569
+ end
570
+