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,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