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.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.travis.yml +21 -0
- data/Gemfile +5 -0
- data/LICENSE.txt +22 -0
- data/README.md +144 -0
- data/Rakefile +9 -0
- data/gemfiles/Gemfile.base +4 -0
- data/gemfiles/activerecord-4.2.0/Gemfile.base +3 -0
- data/gemfiles/activerecord-4.2.0/Gemfile.mysql2 +10 -0
- data/gemfiles/activerecord-4.2.0/Gemfile.postgresql +10 -0
- data/gemfiles/activerecord-4.2.0/Gemfile.sqlite3 +10 -0
- data/gemfiles/activerecord-4.2.1/Gemfile.base +3 -0
- data/gemfiles/activerecord-4.2.1/Gemfile.mysql2 +10 -0
- data/gemfiles/activerecord-4.2.1/Gemfile.postgresql +10 -0
- data/gemfiles/activerecord-4.2.1/Gemfile.sqlite3 +10 -0
- data/lib/schema_auto_foreign_keys.rb +31 -0
- data/lib/schema_auto_foreign_keys/active_record/connection_adapters/sqlite3_adapter.rb +22 -0
- data/lib/schema_auto_foreign_keys/middleware/migration.rb +90 -0
- data/lib/schema_auto_foreign_keys/middleware/schema.rb +18 -0
- data/lib/schema_auto_foreign_keys/version.rb +3 -0
- data/schema_auto_foreign_keys.gemspec +30 -0
- data/schema_dev.yml +9 -0
- data/spec/migration_spec.rb +403 -0
- data/spec/schema_spec.rb +42 -0
- data/spec/spec_helper.rb +52 -0
- data/spec/support/matchers/automatic_foreign_key_matchers.rb +2 -0
- data/spec/support/matchers/have_index.rb +60 -0
- data/spec/support/matchers/reference.rb +79 -0
- metadata +191 -0
@@ -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,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
|
data/schema_dev.yml
ADDED
@@ -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
|
+
|