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,136 @@
1
+ require 'spec_helper'
2
+
3
+ describe "with multiple schemas" do
4
+ def connection
5
+ ActiveRecord::Base.connection
6
+ end
7
+
8
+ before(:all) do
9
+ newdb = case connection.adapter_name
10
+ when /^mysql/i then "CREATE SCHEMA IF NOT EXISTS schema_plus_test2"
11
+ when /^postgresql/i then "CREATE SCHEMA schema_plus_test2"
12
+ when /^sqlite/i then "ATTACH ':memory:' AS schema_plus_test2"
13
+ end
14
+ begin
15
+ ActiveRecord::Base.connection.execute newdb
16
+ rescue ActiveRecord::StatementInvalid => e
17
+ raise unless e.message =~ /already exists/
18
+ end
19
+
20
+ class User < ::ActiveRecord::Base ; end
21
+ end
22
+
23
+ before(:each) do
24
+ define_schema do
25
+ create_table :users, :force => true do |t|
26
+ t.string :login
27
+ end
28
+ end
29
+
30
+ connection.execute 'DROP TABLE IF EXISTS schema_plus_test2.users'
31
+ connection.execute 'CREATE TABLE schema_plus_test2.users (id ' + case connection.adapter_name
32
+ when /^mysql/i then "integer primary key auto_increment"
33
+ when /^postgresql/i then "serial primary key"
34
+ when /^sqlite/i then "integer primary key autoincrement"
35
+ end + ", login varchar(255))"
36
+ end
37
+
38
+ context "with foreign key in each schema" do
39
+ before(:each) do
40
+ class Comment < ::ActiveRecord::Base ; end
41
+ connection.execute 'DROP TABLE IF EXISTS schema_plus_test2.comments'
42
+ connection.execute 'CREATE TABLE schema_plus_test2.comments (user_id integer,' + case connection.adapter_name
43
+ when /^mysql/i then "foreign key (user_id) references schema_plus_test2.users (id))"
44
+ when /^postgresql/i then "foreign key (user_id) references schema_plus_test2.users (id))"
45
+ when /^sqlite/i then "foreign key (user_id) references users (id))"
46
+ end
47
+ end
48
+
49
+ around(:each) do |example|
50
+ begin
51
+ example.run
52
+ ensure
53
+ connection.execute 'DROP TABLE IF EXISTS comments'
54
+ connection.execute 'DROP TABLE IF EXISTS schema_plus_test2.comments'
55
+ end
56
+ end
57
+
58
+ it "should not find foreign keys in other schema" do
59
+ connection.create_table :comments, :force => true do |t|
60
+ t.integer :user_id, :foreign_key => false
61
+ end
62
+ Comment.reset_column_information
63
+ expect(Comment.foreign_keys.length).to eq(0)
64
+ User.reset_column_information
65
+ expect(User.reverse_foreign_keys.length).to eq(0)
66
+ end
67
+
68
+ it "should find foreign keys in this schema" do
69
+ connection.create_table :comments, :force => true do |t|
70
+ t.integer :user_id, :foreign_key => true
71
+ end
72
+ Comment.reset_column_information
73
+ expect(Comment.foreign_keys.map(&:column).flatten).to eq(["user_id"])
74
+ User.reset_column_information
75
+ expect(User.reverse_foreign_keys.map(&:column).flatten).to eq(["user_id"])
76
+ end
77
+
78
+ end
79
+
80
+ context "foreign key migrations" do
81
+ before(:each) do
82
+ define_schema do
83
+ create_table "items", :force => true do |t|
84
+ end
85
+ create_table "schema_plus_test2.groups", :force => true do |t|
86
+ end
87
+ create_table "schema_plus_test2.members", :force => true do |t|
88
+ t.integer :item_id, :foreign_key => true unless SchemaDev::Rspec::Helpers.mysql?
89
+ t.integer :group_id, :foreign_key => { references: "schema_plus_test2.groups" }
90
+ end
91
+ end
92
+ class Group < ::ActiveRecord::Base
93
+ self.table_name = "schema_plus_test2.groups"
94
+ end
95
+ class Item < ::ActiveRecord::Base
96
+ self.table_name = "items"
97
+ end
98
+ class Member < ::ActiveRecord::Base
99
+ self.table_name = "schema_plus_test2.members"
100
+ end
101
+ end
102
+
103
+ around(:each) do |example|
104
+ begin
105
+ example.run
106
+ ensure
107
+ connection.execute 'DROP TABLE IF EXISTS schema_plus_test2.members'
108
+ connection.execute 'DROP TABLE IF EXISTS schema_plus_test2.groups'
109
+ connection.execute 'DROP TABLE IF EXISTS items'
110
+ end
111
+ end
112
+
113
+ it "should find foreign keys" do
114
+ expect(Member.foreign_keys).not_to be_empty
115
+ end
116
+
117
+ it "should find reverse foreign keys" do
118
+ expect(Group.reverse_foreign_keys).not_to be_empty
119
+ end
120
+
121
+ it "should reference table in same schema" do
122
+ expect(Member.foreign_keys.map(&:to_table)).to include "schema_plus_test2.groups"
123
+ end
124
+
125
+ it "should reference table in default schema", :mysql => :skip do
126
+ expect(Member.foreign_keys.map(&:to_table)).to include "items"
127
+ end
128
+
129
+ it "should include the schema in the constraint name" do
130
+ expected_names = ["fk_schema_plus_test2_members_group_id"]
131
+ expected_names << "fk_schema_plus_test2_members_item_id" unless SchemaDev::Rspec::Helpers.mysql?
132
+ expect(Member.foreign_keys.map(&:name).sort).to match_array(expected_names.sort)
133
+ end
134
+ end
135
+
136
+ end
@@ -0,0 +1,257 @@
1
+ require 'spec_helper'
2
+ require 'stringio'
3
+
4
+ describe "Schema dump" do
5
+
6
+ before(:each) do
7
+ ActiveRecord::Migration.suppress_messages do
8
+ ActiveRecord::Schema.define do
9
+ connection.tables.each do |table| drop_table table, force: :cascade end
10
+
11
+ create_table :users, :force => true do |t|
12
+ t.string :login
13
+ t.datetime :deleted_at
14
+ t.integer :first_post_id, index: { unique: true }
15
+ end
16
+
17
+ create_table :posts, :force => true do |t|
18
+ t.text :body
19
+ t.integer :user_id
20
+ t.integer :first_comment_id
21
+ t.string :string_no_default
22
+ t.integer :short_id
23
+ t.string :str_short
24
+ t.integer :integer_col
25
+ t.float :float_col
26
+ t.decimal :decimal_col
27
+ t.datetime :datetime_col
28
+ t.timestamp :timestamp_col
29
+ t.time :time_col
30
+ t.date :date_col
31
+ t.binary :binary_col
32
+ t.boolean :boolean_col
33
+ end
34
+
35
+ create_table :comments, :force => true do |t|
36
+ t.text :body
37
+ t.integer :post_id
38
+ t.integer :commenter_id
39
+ end
40
+ end
41
+ end
42
+ class ::User < ActiveRecord::Base ; end
43
+ class ::Post < ActiveRecord::Base ; end
44
+ class ::Comment < ActiveRecord::Base ; end
45
+ end
46
+
47
+ it "should enable foreign keys if any", sqlite3: :only do
48
+ expect(dump_schema).to_not match(/PRAGMA FOREIGN_KEYS = ON/m)
49
+ with_foreign_key Post, :user_id, :users, :id do
50
+ expect(dump_schema).to match(/PRAGMA FOREIGN_KEYS = ON/m)
51
+ end
52
+ end
53
+
54
+ it "should include foreign_key definition" do
55
+ with_foreign_key Post, :user_id, :users, :id do
56
+ expect(dump_posts).to match(%r{t.integer\s+"user_id".*foreign_key.*users})
57
+ end
58
+ end
59
+
60
+ it "should include foreign_key name" do
61
+ with_foreign_key Post, :user_id, :users, :id, :name => "yippee" do
62
+ expect(dump_posts).to match(/user_id.*foreign_key.*users.*name: "yippee"/)
63
+ end
64
+ end
65
+
66
+ it "should respect foreign key's primary key" do
67
+ with_foreign_key Post, :user_id, :users, :first_post_id do
68
+ expect(dump_posts).to match(%r{t.integer\s+"user_id".*foreign_key.*primary_key: "first_post_id"})
69
+ end
70
+ end
71
+
72
+
73
+ it "should include foreign_key exactly once" do
74
+ with_foreign_key Post, :user_id, :users, :id, :name => "yippee" do
75
+ expect(dump_posts.scan(/foreign_key.*yippee"/).length).to eq 1
76
+ end
77
+ end
78
+
79
+
80
+ xit "should sort foreign_key definitions" do
81
+ with_foreign_keys Comment, [ [ :post_id, :posts, :id ], [ :commenter_id, :users, :id ]] do
82
+ expect(dump_schema).to match(/foreign_key.+commenter_id.+foreign_key.+post_id/m)
83
+ end
84
+ end
85
+
86
+ context "with constraint dependencies" do
87
+ it "should sort in Posts => Comments direction" do
88
+ with_foreign_key Comment, :post_id, :posts, :id do
89
+ expect(dump_schema).to match(%r{create_table "posts".*create_table "comments"}m)
90
+ end
91
+ end
92
+ it "should sort in Comments => Posts direction" do
93
+ with_foreign_key Post, :first_comment_id, :comments, :id do
94
+ expect(dump_schema).to match(%r{create_table "comments".*create_table "posts"}m)
95
+ end
96
+ end
97
+
98
+ it "should handle regexp in ignore_tables" do
99
+ with_foreign_key Comment, :post_id, :posts, :id do
100
+ dump = dump_schema(:ignore => /post/)
101
+ expect(dump).to match(/create_table "comments"/)
102
+ expect(dump).not_to match(/create_table "posts"/)
103
+ end
104
+ end
105
+
106
+ end
107
+
108
+ it "should include foreign_key options" do
109
+ with_foreign_key Post, :user_id, :users, :id, :on_update => :cascade, :on_delete => :nullify do
110
+ expect(dump_posts).to match(%q[t.integer\s*"user_id",.*foreign_key: {references: "users", name: "fk_posts_user_id", on_update: :cascade, on_delete: :nullify}])
111
+ end
112
+ end
113
+
114
+ context "with cyclic foreign key constraints", :sqlite3 => :skip do
115
+ before(:each) do
116
+ ActiveRecord::Base.connection.add_foreign_key(Comment.table_name, User.table_name, column: :commenter_id)
117
+ ActiveRecord::Base.connection.add_foreign_key(Comment.table_name, Post.table_name, column: :post_id)
118
+ ActiveRecord::Base.connection.add_foreign_key(Post.table_name, Comment.table_name, column: :first_comment_id)
119
+ ActiveRecord::Base.connection.add_foreign_key(Post.table_name, User.table_name, column: :user_id)
120
+ ActiveRecord::Base.connection.add_foreign_key(User.table_name, Post.table_name, column: :first_post_id)
121
+ end
122
+
123
+ it "should not raise an error" do
124
+ expect { dump_schema }.to_not raise_error
125
+ end
126
+
127
+ ["comments", "posts", "users"].each do |table|
128
+ it "should dump constraints for table #{table.inspect} after the table definition" do
129
+ dump = dump_schema.gsub(/#[^\n*]/m, '')
130
+ expect(dump =~ %r{create_table "#{table}"}).to be < (dump =~ %r{foreign_key.*"#{table}"})
131
+ end
132
+ end
133
+
134
+ ["comments", "posts"].each do |table|
135
+ qtable = table.inspect
136
+ it "should dump comments for delayed constraint definition referencing table #{qtable}" do
137
+ expect(dump_schema).to match(%r{# foreign key references #{qtable}.*create_table #{qtable}.*add_foreign_key \S+, #{qtable}}m)
138
+ end
139
+ end
140
+
141
+ context 'with complicated schemas' do
142
+ before(:each) do
143
+
144
+ ActiveRecord::Migration.suppress_messages do
145
+ ActiveRecord::Schema.define do
146
+ connection.tables.each do |table| drop_table table, force: :cascade end
147
+
148
+ create_table :grade_systems, force: true do |t|
149
+ t.string :name
150
+ t.integer :school_id
151
+ t.integer :parent_id
152
+ t.integer :profile_id
153
+ end
154
+
155
+ create_table :schools, force: true do |t|
156
+ t.string :name
157
+ t.integer :default_grade_system_id
158
+ end
159
+
160
+ create_table :academic_years, force: true do |t|
161
+ t.string :name
162
+ t.integer :school_id
163
+ end
164
+
165
+ create_table :buildings, force: true do |t|
166
+ t.string :name
167
+ t.integer :school_id
168
+ end
169
+
170
+ create_table :profiles, force: true do |t|
171
+ t.integer :school_id
172
+ t.integer :building_id
173
+ end
174
+
175
+ end
176
+ end
177
+
178
+ class ::AcademicYear < ActiveRecord::Base ; end
179
+ class ::Building < ActiveRecord::Base ; end
180
+ class ::GradeSystem < ActiveRecord::Base ; end
181
+ class ::Profile < ActiveRecord::Base ; end
182
+ class ::School < ActiveRecord::Base ; end
183
+
184
+ ActiveRecord::Base.connection.add_foreign_key(School.table_name, GradeSystem.table_name, column: :default_grade_system_id)
185
+ ActiveRecord::Base.connection.add_foreign_key(GradeSystem.table_name, School.table_name, column: :school_id)
186
+ ActiveRecord::Base.connection.add_foreign_key(GradeSystem.table_name, GradeSystem.table_name, column: :parent_id)
187
+ ActiveRecord::Base.connection.add_foreign_key(GradeSystem.table_name, Profile.table_name, column: :profile_id)
188
+ ActiveRecord::Base.connection.add_foreign_key(Profile.table_name, Building.table_name, column: :building_id)
189
+ ActiveRecord::Base.connection.add_foreign_key(Profile.table_name, School.table_name, column: :school_id)
190
+ ActiveRecord::Base.connection.add_foreign_key(Building.table_name, School.table_name, column: :school_id)
191
+ ActiveRecord::Base.connection.add_foreign_key(AcademicYear.table_name, School.table_name, column: :school_id)
192
+ end
193
+
194
+ it "should not raise an error" do
195
+ expect { dump_schema }.to_not raise_error
196
+ end
197
+
198
+ ["buildings", "grade_systems", "profiles", "schools"].each do |table|
199
+ it "should dump constraints for table #{table.inspect} after the table definition" do
200
+ expect(dump_schema =~ %r{create_table "#{table}"}).to be < (dump_schema =~ %r{foreign_key.*"#{table}"})
201
+ end
202
+ end
203
+ end
204
+ end
205
+
206
+ protected
207
+ def to_regexp(string)
208
+ Regexp.new(Regexp.escape(string))
209
+ end
210
+
211
+ def with_foreign_key(model, columns, referenced_table_name, referenced_columns, options = {}, &block)
212
+ with_foreign_keys(model, [[columns, referenced_table_name, referenced_columns, options]], &block)
213
+ end
214
+
215
+ def with_foreign_keys(model, columnsets)
216
+ table_columns = model.columns.reject{|column| column.name == 'id'}
217
+ ActiveRecord::Migration.suppress_messages do
218
+ ActiveRecord::Migration.create_table model.table_name, :force => true do |t|
219
+ table_columns.each do |column|
220
+ t.column column.name, column.type
221
+ end
222
+ columnsets.each do |columns, referenced_table_name, referenced_columns, options|
223
+ t.foreign_key columns, referenced_table_name, (options||{}).merge(primary_key: referenced_columns)
224
+ end
225
+ end
226
+ end
227
+ model.reset_column_information
228
+ begin
229
+ yield
230
+ ensure
231
+ ActiveRecord::Migration.suppress_messages do
232
+ ActiveRecord::Migration.create_table model.table_name, :force => true do |t|
233
+ table_columns.each do |column|
234
+ t.column column.name, column.type
235
+ end
236
+ end
237
+ end
238
+ end
239
+ end
240
+
241
+ def determine_foreign_key_name(model, columns, options)
242
+ name = options[:name]
243
+ name ||= model.foreign_keys.detect { |fk| fk.from_table == model.table_name.to_s && Array.wrap(fk.column) == Array.wrap(columns).collect(&:to_s) }.name
244
+ end
245
+
246
+ def dump_schema(opts={})
247
+ stream = StringIO.new
248
+ ActiveRecord::SchemaDumper.ignore_tables = Array.wrap(opts[:ignore]) || []
249
+ ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream)
250
+ stream.string
251
+ end
252
+
253
+ def dump_posts
254
+ dump_schema(:ignore => %w[users comments])
255
+ end
256
+
257
+ end
@@ -0,0 +1,60 @@
1
+ require 'simplecov'
2
+ require 'simplecov-gem-profile'
3
+ SimpleCov.start "gem"
4
+
5
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
6
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
7
+
8
+ require 'rspec'
9
+ require 'active_record'
10
+ require 'schema_plus_foreign_keys'
11
+ require 'schema_dev/rspec'
12
+
13
+ SchemaDev::Rspec.setup
14
+
15
+ Dir[File.dirname(__FILE__) + "/support/**/*.rb"].each {|f| require f}
16
+
17
+ RSpec.configure do |config|
18
+ config.include(SchemaPlus::Matchers)
19
+ config.warnings = true
20
+ config.around(:each) do |example|
21
+ ActiveRecord::Migration.suppress_messages do
22
+ begin
23
+ example.run
24
+ ensure
25
+ ActiveRecord::Base.connection.tables.each do |table|
26
+ ActiveRecord::Migration.drop_table table, force: :cascade
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+
33
+ def with_fk_config(opts, &block)
34
+ save = Hash[opts.keys.collect{|key| [key, SchemaPlus::ForeignKeys.config.send(key)]}]
35
+ begin
36
+ SchemaPlus::ForeignKeys.setup do |config|
37
+ config.update_attributes(opts)
38
+ end
39
+ yield
40
+ ensure
41
+ SchemaPlus::ForeignKeys.setup do |config|
42
+ config.update_attributes(save)
43
+ end
44
+ end
45
+ end
46
+
47
+ def define_schema(config={}, &block)
48
+ with_fk_config(config) do
49
+ ActiveRecord::Migration.suppress_messages do
50
+ ActiveRecord::Schema.define do
51
+ connection.tables.each do |table|
52
+ drop_table table, force: :cascade
53
+ end
54
+ instance_eval &block
55
+ end
56
+ end
57
+ end
58
+ end
59
+
60
+ SimpleCov.command_name "[ruby#{RUBY_VERSION}-activerecord#{::ActiveRecord.version}-#{ActiveRecord::Base.connection.adapter_name}]"