schema_auto_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.
@@ -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
+