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,15 @@
1
+ module SchemaPlus::ForeignKeys
2
+ module Middleware
3
+
4
+ module Model
5
+ module ResetColumnInformation
6
+
7
+ def after(env)
8
+ env.model.reset_foreign_key_information
9
+ end
10
+
11
+ end
12
+ end
13
+
14
+ end
15
+ end
@@ -0,0 +1,20 @@
1
+ module SchemaPlus::ForeignKeys
2
+ module Middleware
3
+
4
+ module Mysql
5
+ module Migration
6
+ module DropTable
7
+
8
+ def around(env)
9
+ if (env.options[:force] == :cascade)
10
+ env.connection.reverse_foreign_keys(env.table_name).each do |foreign_key|
11
+ env.connection.remove_foreign_key(foreign_key.from_table, name: foreign_key.name)
12
+ end
13
+ end
14
+ yield env
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,27 @@
1
+ module SchemaPlus::ForeignKeys
2
+ module Middleware
3
+ module Sql
4
+ module Table
5
+ def after(env)
6
+ foreign_keys = case ::ActiveRecord.version
7
+ when Gem::Version.new("4.2.0") then env.table_definition.foreign_keys
8
+ else env.table_definition.foreign_keys.values.tap { |v| v.flatten! }
9
+ end
10
+
11
+ # create foreign key constraints inline in table definition
12
+ env.sql.body = ([env.sql.body] + foreign_keys.map(&:to_sql)).join(', ')
13
+
14
+ # prevents AR >= 4.2.1 from emitting add_foreign_key after the table
15
+ env.table_definition.foreign_keys.clear
16
+ end
17
+
18
+ module SQLite3
19
+
20
+ def before(env)
21
+ env.connection.execute('PRAGMA FOREIGN_KEYS = ON') if env.table_definition.foreign_keys.any?
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,5 @@
1
+ module SchemaPlus
2
+ module ForeignKeys
3
+ VERSION = "0.1.0"
4
+ end
5
+ end
@@ -0,0 +1 @@
1
+ require_relative 'schema_plus/foreign_keys'
@@ -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,31 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'schema_plus/foreign_keys/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "schema_plus_foreign_keys"
8
+ gem.version = SchemaPlus::ForeignKeys::VERSION
9
+ gem.authors = ["ronen barzel"]
10
+ gem.email = ["ronen@barzel.org"]
11
+ gem.summary = %q{Extended support for foreign key constraints in ActiveRecord}
12
+ gem.description = %q{Extended support for foreign key constraints in ActiveRecord, including: definition as column attribute; deferrable; and SQLite3 support; cleaner dumps; and more!}
13
+ gem.homepage = "https://github.com/SchemaPlus/schema_plus_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 "activerecord", "~> 4.2"
22
+ gem.add_dependency "schema_plus_core", "~> 0.4"
23
+ gem.add_dependency "valuable"
24
+
25
+ gem.add_development_dependency "bundler", "~> 1.7"
26
+ gem.add_development_dependency "rake", "~> 10.0"
27
+ gem.add_development_dependency "rspec", "~> 3.0"
28
+ gem.add_development_dependency "schema_dev", "~> 3.5"
29
+ gem.add_development_dependency "simplecov"
30
+ gem.add_development_dependency "simplecov-gem-profile"
31
+ end
@@ -0,0 +1,161 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'Deprecations' do
4
+
5
+ let(:migration) { ActiveRecord::Migration }
6
+
7
+ describe "on add_foreign_key", sqlite3: :skip do
8
+ before(:each) do
9
+ define_schema do
10
+ create_table :posts
11
+ create_table :comments do |t|
12
+ t.integer :post_id
13
+ end
14
+ end
15
+ class Comment < ::ActiveRecord::Base ; end
16
+ end
17
+
18
+ it "deprecates 4-argument form" do
19
+ expect(ActiveSupport::Deprecation).to receive(:warn).with(/4-argument/)
20
+ migration.add_foreign_key "comments", "post_id", "posts", "id"
21
+ expect(Comment).to reference(:posts, :id).on(:post_id)
22
+ end
23
+
24
+ end
25
+
26
+ describe "on remove_foreign_key", sqlite3: :skip do
27
+ before(:each) do
28
+ define_schema do
29
+ create_table :posts
30
+ create_table :comments do |t|
31
+ t.integer :post_id, foreign_key: true
32
+ end
33
+ end
34
+ class Comment < ::ActiveRecord::Base ; end
35
+ end
36
+
37
+ it "deprecates :column_names option" do
38
+ expect(ActiveSupport::Deprecation).to receive(:warn).with(/column_names/)
39
+ migration.remove_foreign_key "comments", "posts", column_names: "post_id"
40
+ Comment.reset_column_information
41
+ expect(Comment).to_not reference(:posts, :id).on(:post_id)
42
+ end
43
+
44
+ it "deprecates :references_column_names option" do
45
+ expect(ActiveSupport::Deprecation).to receive(:warn).with(/references_column_names.*primary_key/)
46
+ migration.remove_foreign_key "comments", "posts", references_column_names: "id"
47
+ Comment.reset_column_information
48
+ expect(Comment).to_not reference(:posts, :id).on(:post_id)
49
+ end
50
+
51
+ it "deprecates :references_table_name option" do
52
+ expect(ActiveSupport::Deprecation).to receive(:warn).with(/references_table_name.*to_table/)
53
+ migration.remove_foreign_key "comments", references_table_name: "posts"
54
+ Comment.reset_column_information
55
+ expect(Comment).to_not reference(:posts, :id).on(:post_id)
56
+ end
57
+
58
+ it "deprecates table-and-name form" do
59
+ Comment.reset_column_information
60
+ name = Comment.foreign_keys.first.name
61
+ expect(ActiveSupport::Deprecation).to receive(:warn).with(/name.*name: name/)
62
+ migration.remove_foreign_key "comments", name
63
+ Comment.reset_column_information
64
+ expect(Comment).to_not reference(:posts, :id).on(:post_id)
65
+ end
66
+
67
+ it "deprecates 3-argument form" do
68
+ expect(ActiveSupport::Deprecation).to receive(:warn).with(/3.*-argument/)
69
+ migration.remove_foreign_key "comments", "post_id", "posts"
70
+ Comment.reset_column_information
71
+ expect(Comment).to_not reference(:posts, :id).on(:post_id)
72
+ end
73
+
74
+ it "deprecates 4-argument form" do
75
+ expect(ActiveSupport::Deprecation).to receive(:warn).with(/4.*-argument/)
76
+ migration.remove_foreign_key "comments", "post_id", "posts", "id"
77
+ Comment.reset_column_information
78
+ expect(Comment).to_not reference(:posts, :id).on(:post_id)
79
+ end
80
+
81
+ it "raises error for 5 arguments" do
82
+ expect { migration.remove_foreign_key "zip", "a", "dee", "do", "da" }.to raise_error /Wrong number of arguments.*5/
83
+ end
84
+
85
+ end
86
+
87
+ describe "on foreign key definition" do
88
+ before(:each) do
89
+ define_schema do
90
+ create_table :posts
91
+ create_table :comments do |t|
92
+ t.integer :post_id, foreign_key: true
93
+ end
94
+ end
95
+ class Comment < ::ActiveRecord::Base ; end
96
+ end
97
+
98
+ let(:definition) {
99
+ Comment.reset_column_information
100
+ Comment.foreign_keys.first
101
+ }
102
+
103
+ it "deprecates column_names" do
104
+ expect(ActiveSupport::Deprecation).to receive(:warn).with(/column_names/)
105
+ expect(definition.column_names).to eq(["post_id"])
106
+ end
107
+
108
+ it "deprecates references_column_names" do
109
+ expect(ActiveSupport::Deprecation).to receive(:warn).with(/references_column_names.*primary_key/)
110
+ expect(definition.references_column_names).to eq(["id"])
111
+ end
112
+
113
+ it "deprecates references_table_name" do
114
+ expect(ActiveSupport::Deprecation).to receive(:warn).with(/references_table_name.*to_table/)
115
+ expect(definition.references_table_name).to eq("posts")
116
+ end
117
+
118
+ it "deprecates table_name" do
119
+ expect(ActiveSupport::Deprecation).to receive(:warn).with(/table_name.*from_table/)
120
+ expect(definition.table_name).to eq("comments")
121
+ end
122
+
123
+ it "deprecates :set_null" do
124
+ expect(ActiveSupport::Deprecation).to receive(:warn).with(/set_null.*nullify/)
125
+ define_schema do
126
+ create_table :posts
127
+ create_table :comments do |t|
128
+ t.integer :post_id, references: :posts, on_delete: :set_null
129
+ end
130
+ end
131
+ expect(definition.on_delete).to eq(:nullify)
132
+ end
133
+
134
+ end
135
+
136
+ describe "in table definition" do
137
+ it "deprecates 3-column form" do
138
+ expect(ActiveSupport::Deprecation).to receive(:warn).with(/positional arg.*primary_key/)
139
+ define_schema do
140
+ create_table :posts, primary_key: :funky
141
+ create_table :comments do |t|
142
+ t.integer :post_id
143
+ t.foreign_key :post_id, :posts, :funky
144
+ end
145
+ end
146
+ expect(migration.foreign_keys("comments").first.primary_key).to eq("funky")
147
+ end
148
+
149
+ it "raises error for 4 arguments" do
150
+ expect {
151
+ define_schema do
152
+ create_table :posts, primary_key: :funky
153
+ create_table :comments do |t|
154
+ t.integer :post_id
155
+ t.foreign_key :post_id, :posts, :funky, :town
156
+ end
157
+ end
158
+ }.to raise_error /wrong number of arguments/i
159
+ end
160
+ end
161
+ end
@@ -0,0 +1,34 @@
1
+ require 'spec_helper'
2
+
3
+ describe "Foreign Key definition" do
4
+
5
+ let(:definition) {
6
+ options = {:name => "posts_user_fkey", :column => :user, :primary_key => :id}
7
+ ::ActiveRecord::ConnectionAdapters::ForeignKeyDefinition.new(:posts, :users, options)
8
+ }
9
+
10
+ it "dumps to sql with quoted values" do
11
+ expect(definition.to_sql).to eq(%Q{CONSTRAINT posts_user_fkey FOREIGN KEY (#{quote_column_name('user')}) REFERENCES #{quote_table_name('users')} (#{quote_column_name('id')})})
12
+ end
13
+
14
+ it "dumps to sql with deferrable values" do
15
+ options = {:name => "posts_user_fkey", :column => :user, :primary_key => :id, :deferrable => true}
16
+ deferred_definition = ::ActiveRecord::ConnectionAdapters::ForeignKeyDefinition.new(:posts, :users, options)
17
+ expect(deferred_definition.to_sql).to eq(%Q{CONSTRAINT posts_user_fkey FOREIGN KEY (#{quote_column_name('user')}) REFERENCES #{quote_table_name('users')} (#{quote_column_name('id')}) DEFERRABLE})
18
+ end
19
+
20
+ it "dumps to sql with initially deferrable values" do
21
+ options = {:name => "posts_user_fkey", :column => :user, :primary_key => :id, :deferrable => :initially_deferred}
22
+ initially_deferred_definition = ::ActiveRecord::ConnectionAdapters::ForeignKeyDefinition.new(:posts, :users, options)
23
+ expect(initially_deferred_definition.to_sql).to eq(%Q{CONSTRAINT posts_user_fkey FOREIGN KEY (#{quote_column_name('user')}) REFERENCES #{quote_table_name('users')} (#{quote_column_name('id')}) DEFERRABLE INITIALLY DEFERRED})
24
+ end
25
+
26
+ def quote_table_name(table)
27
+ ActiveRecord::Base.connection.quote_table_name(table)
28
+ end
29
+
30
+ def quote_column_name(column)
31
+ ActiveRecord::Base.connection.quote_column_name(column)
32
+ end
33
+
34
+ end
@@ -0,0 +1,207 @@
1
+ require 'spec_helper'
2
+
3
+ describe "Foreign Key" do
4
+
5
+ let(:migration) { ::ActiveRecord::Migration }
6
+
7
+ context "created with table" do
8
+ before(:each) do
9
+ define_schema do
10
+ create_table :users, :force => true do |t|
11
+ t.string :login
12
+ end
13
+ create_table :comments, :force => true do |t|
14
+ t.integer :user_id
15
+ t.foreign_key :user_id, :users
16
+ end
17
+ end
18
+ class User < ::ActiveRecord::Base ; end
19
+ class Comment < ::ActiveRecord::Base ; end
20
+ Comment.reset_column_information
21
+ end
22
+
23
+ it "should report foreign key constraints" do
24
+ expect(Comment.foreign_keys.collect(&:column).flatten).to eq([ "user_id" ])
25
+ end
26
+
27
+ it "should report reverse foreign key constraints" do
28
+ expect(User.reverse_foreign_keys.collect(&:column).flatten).to eq([ "user_id" ])
29
+ end
30
+
31
+ end
32
+
33
+ context "modification" do
34
+
35
+ before(:each) do
36
+ define_schema do
37
+ create_table :users, :force => true do |t|
38
+ t.string :login
39
+ t.datetime :deleted_at
40
+ end
41
+
42
+ create_table :posts, :force => true do |t|
43
+ t.text :body
44
+ t.integer :user_id
45
+ t.integer :author_id
46
+ end
47
+
48
+ create_table :comments, :force => true do |t|
49
+ t.text :body
50
+ t.integer :post_id
51
+ t.foreign_key :post_id, :posts
52
+ end
53
+ end
54
+ class User < ::ActiveRecord::Base ; end
55
+ class Post < ::ActiveRecord::Base ; end
56
+ class Comment < ::ActiveRecord::Base ; end
57
+ Comment.reset_column_information
58
+ end
59
+
60
+
61
+ context "works", :sqlite3 => :skip do
62
+
63
+ context "when is added", "posts(author_id)" do
64
+
65
+ before(:each) do
66
+ add_foreign_key(:posts, :users, :column => :author_id, :on_update => :cascade, :on_delete => :restrict)
67
+ end
68
+
69
+ it "references users(id)" do
70
+ expect(Post).to reference(:users, :id).on(:author_id)
71
+ end
72
+
73
+ it "cascades on update" do
74
+ expect(Post).to reference(:users).on_update(:cascade)
75
+ end
76
+
77
+ it "restricts on delete" do
78
+ expect(Post).to reference(:users).on_delete(:restrict)
79
+ end
80
+
81
+ it "is available in Post.foreign_keys" do
82
+ expect(Post.foreign_keys.collect(&:column)).to include('author_id')
83
+ end
84
+
85
+ it "is available in User.reverse_foreign_keys" do
86
+ expect(User.reverse_foreign_keys.collect(&:column)).to include('author_id')
87
+ end
88
+
89
+ end
90
+
91
+ context "when is dropped", "comments(post_id)" do
92
+
93
+ let(:foreign_key_name) { fk = Comment.foreign_keys.detect(&its.column == 'post_id') and fk.name }
94
+
95
+ before(:each) do
96
+ remove_foreign_key(:comments, name: foreign_key_name)
97
+ end
98
+
99
+ it "doesn't reference posts(id)" do
100
+ expect(Comment).not_to reference(:posts).on(:post_id)
101
+ end
102
+
103
+ it "is no longer available in Post.foreign_keys" do
104
+ expect(Comment.foreign_keys.collect(&:column)).not_to include('post_id')
105
+ end
106
+
107
+ it "is no longer available in User.reverse_foreign_keys" do
108
+ expect(Post.reverse_foreign_keys.collect(&:column)).not_to include('post_id')
109
+ end
110
+
111
+ end
112
+
113
+ context "when drop using hash", "comments(post_id)" do
114
+
115
+ let(:foreign_key_name) { fk = Comment.foreign_keys.detect(&its.column == 'post_id') and fk.name }
116
+
117
+ it "finds by name" do
118
+ remove_foreign_key(:comments, name: foreign_key_name)
119
+ expect(Comment).not_to reference(:posts).on(:post_id)
120
+ end
121
+
122
+ it "finds by column_names" do
123
+ remove_foreign_key(:comments, column: "post_id", to_table: "posts")
124
+ expect(Comment).not_to reference(:posts).on(:post_id)
125
+ end
126
+ end
127
+
128
+ context "when attempt to drop nonexistent foreign key" do
129
+ it "raises error" do
130
+ expect{remove_foreign_key(:comments, "posts", column: "nonesuch")}.to raise_error(/no foreign key/i)
131
+ end
132
+
133
+ it "does not error with :if_exists" do
134
+ expect{remove_foreign_key(:comments, "posts", column: "nonesuch", :if_exists => true)}.to_not raise_error
135
+ end
136
+ end
137
+
138
+ context "when referencing column and column is removed" do
139
+
140
+ let(:foreign_key_name) { Comment.foreign_keys.detect { |definition| definition.column == 'post_id' }.name }
141
+
142
+ it "should remove foreign keys" do
143
+ remove_foreign_key(:comments, name: foreign_key_name)
144
+ expect(Post.reverse_foreign_keys.collect { |fk| fk.column == 'post_id' && fk.from_table == "comments" }).to be_empty
145
+ end
146
+
147
+ end
148
+
149
+ context "when table name is a reserved word" do
150
+ before(:each) do
151
+ migration.suppress_messages do
152
+ migration.create_table :references, :force => true do |t|
153
+ t.integer :post_id, :foreign_key => false
154
+ end
155
+ end
156
+ end
157
+
158
+ it "can add, detect, and remove a foreign key without error" do
159
+ migration.suppress_messages do
160
+ expect {
161
+ migration.add_foreign_key(:references, :posts)
162
+ foreign_key = migration.foreign_keys(:references).detect{|definition| definition.column == "post_id"}
163
+ migration.remove_foreign_key(:references, name: foreign_key.name)
164
+ }.to_not raise_error
165
+ end
166
+ end
167
+ end
168
+
169
+ end
170
+
171
+ context "raises an exception", :sqlite3 => :only do
172
+
173
+ it "when attempting to add" do
174
+ expect {
175
+ add_foreign_key(:posts, :users, :column => :author_id, :on_update => :cascade, :on_delete => :restrict)
176
+ }.to raise_error(NotImplementedError)
177
+ end
178
+
179
+ it "when attempting to remove" do
180
+ expect {
181
+ remove_foreign_key(:posts, name: "dummy")
182
+ }.to raise_error(NotImplementedError)
183
+ end
184
+
185
+ end
186
+ end
187
+
188
+ protected
189
+ def add_foreign_key(*args)
190
+ migration.suppress_messages do
191
+ migration.add_foreign_key(*args)
192
+ end
193
+ User.reset_column_information
194
+ Post.reset_column_information
195
+ Comment.reset_column_information
196
+ end
197
+
198
+ def remove_foreign_key(*args)
199
+ migration.suppress_messages do
200
+ migration.remove_foreign_key(*args)
201
+ end
202
+ User.reset_column_information
203
+ Post.reset_column_information
204
+ Comment.reset_column_information
205
+ end
206
+
207
+ end