schema_plus_foreign_keys 0.1.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.
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
+