schema_plus 0.1.0.pre1

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 (55) hide show
  1. data/.gitignore +25 -0
  2. data/Gemfile +3 -0
  3. data/MIT-LICENSE +25 -0
  4. data/README.rdoc +147 -0
  5. data/Rakefile +70 -0
  6. data/init.rb +1 -0
  7. data/lib/schema_plus/active_record/associations.rb +211 -0
  8. data/lib/schema_plus/active_record/base.rb +81 -0
  9. data/lib/schema_plus/active_record/connection_adapters/abstract_adapter.rb +96 -0
  10. data/lib/schema_plus/active_record/connection_adapters/column.rb +55 -0
  11. data/lib/schema_plus/active_record/connection_adapters/foreign_key_definition.rb +115 -0
  12. data/lib/schema_plus/active_record/connection_adapters/index_definition.rb +51 -0
  13. data/lib/schema_plus/active_record/connection_adapters/mysql_adapter.rb +111 -0
  14. data/lib/schema_plus/active_record/connection_adapters/postgresql_adapter.rb +163 -0
  15. data/lib/schema_plus/active_record/connection_adapters/schema_statements.rb +39 -0
  16. data/lib/schema_plus/active_record/connection_adapters/sqlite3_adapter.rb +78 -0
  17. data/lib/schema_plus/active_record/connection_adapters/table_definition.rb +130 -0
  18. data/lib/schema_plus/active_record/migration.rb +220 -0
  19. data/lib/schema_plus/active_record/schema.rb +27 -0
  20. data/lib/schema_plus/active_record/schema_dumper.rb +122 -0
  21. data/lib/schema_plus/active_record/validations.rb +139 -0
  22. data/lib/schema_plus/railtie.rb +12 -0
  23. data/lib/schema_plus/version.rb +3 -0
  24. data/lib/schema_plus.rb +248 -0
  25. data/schema_plus.gemspec +37 -0
  26. data/schema_plus.gemspec.rails3.0 +36 -0
  27. data/schema_plus.gemspec.rails3.1 +36 -0
  28. data/spec/association_spec.rb +529 -0
  29. data/spec/connections/mysql/connection.rb +18 -0
  30. data/spec/connections/mysql2/connection.rb +18 -0
  31. data/spec/connections/postgresql/connection.rb +15 -0
  32. data/spec/connections/sqlite3/connection.rb +14 -0
  33. data/spec/foreign_key_definition_spec.rb +23 -0
  34. data/spec/foreign_key_spec.rb +142 -0
  35. data/spec/index_definition_spec.rb +139 -0
  36. data/spec/index_spec.rb +71 -0
  37. data/spec/migration_spec.rb +405 -0
  38. data/spec/models/comment.rb +2 -0
  39. data/spec/models/post.rb +2 -0
  40. data/spec/models/user.rb +2 -0
  41. data/spec/references_spec.rb +78 -0
  42. data/spec/schema/auto_schema.rb +23 -0
  43. data/spec/schema/core_schema.rb +21 -0
  44. data/spec/schema_dumper_spec.rb +167 -0
  45. data/spec/schema_spec.rb +71 -0
  46. data/spec/spec_helper.rb +59 -0
  47. data/spec/support/extensions/active_model.rb +13 -0
  48. data/spec/support/helpers.rb +16 -0
  49. data/spec/support/matchers/automatic_foreign_key_matchers.rb +2 -0
  50. data/spec/support/matchers/have_index.rb +52 -0
  51. data/spec/support/matchers/reference.rb +66 -0
  52. data/spec/support/reference.rb +66 -0
  53. data/spec/validations_spec.rb +294 -0
  54. data/spec/views_spec.rb +140 -0
  55. metadata +269 -0
@@ -0,0 +1,66 @@
1
+ module SchemaPlusMatchers
2
+
3
+ class Reference
4
+ def initialize(expected)
5
+ @column_names = nil
6
+ unless expected.empty?
7
+ @references_column_names = Array(expected).collect(&:to_s)
8
+ @references_table_name = @references_column_names.shift
9
+ end
10
+ end
11
+
12
+ def matches?(model)
13
+ @model = model
14
+ if @references_table_name
15
+ @result = @model.foreign_keys.select do |fk|
16
+ fk.references_table_name == @references_table_name &&
17
+ @references_column_names.empty? ? true : fk.references_column_names == @references_column_names
18
+ end
19
+ else
20
+ @result = @model.foreign_keys
21
+ end
22
+ if @column_names
23
+ @result.any? do |fk|
24
+ fk.column_names == @column_names &&
25
+ (@on_update ? fk.on_update == @on_update : true) &&
26
+ (@on_delete ? fk.on_delete == @on_delete : true)
27
+ end
28
+ else
29
+ !@result.empty?
30
+ end
31
+ end
32
+
33
+ def failure_message_for_should(should_not = false)
34
+ target_column_names = @column_names.present? ? "(#{@column_names.join(', ')})" : ""
35
+ destinantion_column_names = @references_table_name ? "#{@references_table_name}(#{@references_column_names.join(', ')})" : "anything"
36
+ invert = should_not ? 'not' : ''
37
+ "Expected #{@model.table_name}#{target_column_names} to #{invert} reference #{destinantion_column_names}"
38
+ end
39
+
40
+ def failure_message_for_should_not
41
+ failure_message_for_should(true)
42
+ end
43
+
44
+ def on(*column_names)
45
+ @column_names = column_names.collect(&:to_s)
46
+ self
47
+ end
48
+
49
+ def on_update(action)
50
+ @on_update = action
51
+ self
52
+ end
53
+
54
+ def on_delete(action)
55
+ @on_delete = action
56
+ self
57
+ end
58
+
59
+ end
60
+
61
+ def reference(*expect)
62
+ Reference.new(expect)
63
+ end
64
+
65
+ end
66
+
@@ -0,0 +1,294 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe "Validations" do
4
+
5
+ before(:all) do
6
+ define_schema
7
+ # TODO: it should work regardless of auto-associations
8
+ SchemaPlus.config.associations.auto_create = false
9
+ end
10
+
11
+ after(:each) do
12
+ remove_all_models
13
+ end
14
+
15
+ context "auto-created" do
16
+ before(:each) do
17
+ with_auto_validations do
18
+ class Article < ActiveRecord::Base ; end
19
+
20
+ class Review < ActiveRecord::Base
21
+ belongs_to :article
22
+ belongs_to :news_article, :class_name => 'Article', :foreign_key => :article_id
23
+ schema_plus :validations => { :except => :content }
24
+ end
25
+ end
26
+ end
27
+
28
+ it "should be valid with valid attributes" do
29
+ Article.new(valid_attributes).should be_valid
30
+ end
31
+
32
+ it "should validate content presence" do
33
+ post = Article.new.should have(1).error_on(:content)
34
+ end
35
+
36
+ it "should check title length" do
37
+ Article.new(:title => 'a' * 100).should have(1).error_on(:title)
38
+ end
39
+
40
+ it "should validate state numericality" do
41
+ Article.new(:state => 'unknown').should have(1).error_on(:state)
42
+ end
43
+
44
+ it "should validate if state is integer" do
45
+ Article.new(:state => 1.23).should have(1).error_on(:state)
46
+ end
47
+
48
+ it "should validate average_mark numericality" do
49
+ Article.new(:average_mark => "high").should have(1).error_on(:average_mark)
50
+ end
51
+
52
+ it "should validate boolean fields" do
53
+ Article.new(:active => nil).should have(1).error_on(:active)
54
+ end
55
+
56
+ it "should validate title uniqueness" do
57
+ article1 = Article.create(valid_attributes)
58
+ article2 = Article.new(:title => valid_attributes[:title])
59
+ article2.should have(1).error_on(:title)
60
+ article1.destroy
61
+ end
62
+
63
+ it "should validate state uniqueness in scope of 'active' value" do
64
+ article1 = Article.create(valid_attributes)
65
+ article2 = Article.new(valid_attributes.merge(:title => 'SchemaPlus 2.0 released'))
66
+ article2.should_not be_valid
67
+ article2.toggle(:active)
68
+ article2.should be_valid
69
+ article1.destroy
70
+ end
71
+
72
+ it "should validate presence of belongs_to association" do
73
+ review = Review.new
74
+ review.should have(1).error_on(:article)
75
+ end
76
+
77
+ it "should validate uniqueness of belongs_to association" do
78
+ article = Article.create(valid_attributes)
79
+ article.should be_valid
80
+ review1 = Review.create(:article => article, :author => 'michal')
81
+ review1.should be_valid
82
+ review2 = Review.new(:article => article, :author => 'michal')
83
+ review2.should have_at_least(1).error_on(:article_id)
84
+ end
85
+
86
+ it "should validate associations with unmatched column and name" do
87
+ Review.new.should have(1).error_on(:news_article)
88
+ end
89
+
90
+ def valid_attributes
91
+ {
92
+ :title => 'SchemaPlus released!',
93
+ :content => "Database matters. Get full use of it but don't write unecessary code. Get SchemaPlus!",
94
+ :state => 3,
95
+ :average_mark => 9.78,
96
+ :active => true
97
+ }
98
+ end
99
+
100
+ end
101
+
102
+ context "auto-created but changed" do
103
+ before(:each) do
104
+ with_auto_validations do
105
+ class Article < ActiveRecord::Base ; end
106
+ class Review < ActiveRecord::Base
107
+ belongs_to :article
108
+ belongs_to :news_article, :class_name => 'Article', :foreign_key => :article_id
109
+ end
110
+ end
111
+ too_big_content = 'a' * 1000
112
+ @review = Review.new(:content => too_big_content)
113
+ end
114
+
115
+ it "would normally have an error" do
116
+ @review.should have(1).error_on(:content)
117
+ @review.should have(1).error_on(:author)
118
+ end
119
+
120
+ it "shouldn't validate fields passed to :except option" do
121
+ Review.schema_plus :validations => { :except => :content }
122
+ @review.should have(:no).errors_on(:content)
123
+ @review.should have(1).error_on(:author)
124
+ end
125
+
126
+ it "shouldn't validate types passed to :except_type option using full validation" do
127
+ Review.schema_plus :validations => { :except_type => :validates_length_of }
128
+ @review.should have(:no).errors_on(:content)
129
+ @review.should have(1).error_on(:author)
130
+ end
131
+
132
+ it "shouldn't validate types passed to :except_type option using shorthand" do
133
+ Review.schema_plus :validations => { :except_type => :length }
134
+ @review.should have(:no).errors_on(:content)
135
+ @review.should have(1).error_on(:author)
136
+ end
137
+
138
+ it "should only validate type passed to :only_type option" do
139
+ Review.schema_plus :validations => { :only_type => :length }
140
+ @review.should have(1).error_on(:content)
141
+ @review.should have(:no).errors_on(:author)
142
+ end
143
+
144
+
145
+ it "shouldn't create validations if locally disabled" do
146
+ Review.schema_plus :validations => { :auto_create => false }
147
+ @review.should have(:no).errors_on(:content)
148
+ @review.should have(:no).error_on(:author)
149
+ end
150
+ end
151
+
152
+ context "auto-created disabled" do
153
+ around(:each) do |example|
154
+ with_auto_validations(false, &example)
155
+ end
156
+
157
+ before(:each) do
158
+ class Article < ActiveRecord::Base ; end
159
+
160
+ class Review < ActiveRecord::Base
161
+ belongs_to :article
162
+ belongs_to :news_article, :class_name => 'Article', :foreign_key => :article_id
163
+ end
164
+ too_big_content = 'a' * 1000
165
+ @review = Review.new(:content => too_big_content)
166
+ end
167
+
168
+ it "should not create validation" do
169
+ @review.should have(:no).errors_on(:content)
170
+ end
171
+
172
+ it "should create validation if locally enabled" do
173
+ Review.schema_plus :validations => { :auto_create => true }
174
+ @review.should have(1).error_on(:content)
175
+ end
176
+
177
+ end
178
+
179
+ context "manually invoked" do
180
+ before(:each) do
181
+ class Article < ActiveRecord::Base ; end
182
+ Article.schema_plus :validations => { :only => [:title, :state] }
183
+
184
+ class Review < ActiveRecord::Base
185
+ belongs_to :dummy_association
186
+ schema_plus :validations => { :except => :content }
187
+ end
188
+ end
189
+
190
+ it "should validate fields passed to :only option" do
191
+ too_big_title = 'a' * 100
192
+ wrong_state = 'unknown'
193
+ article = Article.new(:title => too_big_title, :state => wrong_state)
194
+ article.should have(1).error_on(:title)
195
+ article.should have(1).error_on(:state)
196
+ end
197
+
198
+ it "shouldn't validate skipped fields" do
199
+ article = Article.new
200
+ article.should have(:no).errors_on(:content)
201
+ article.should have(:no).errors_on(:average_mark)
202
+ end
203
+
204
+ it "shouldn't validate association on unexisting column" do
205
+ Review.new.should have(:no).errors_on(:dummy_association)
206
+ end
207
+
208
+ it "shouldn't validate fields passed to :except option" do
209
+ Review.new.should have(:no).errors_on(:content)
210
+ end
211
+
212
+ it "should validate all fields but passed to :except option" do
213
+ Review.new.should have(1).error_on(:author)
214
+ end
215
+
216
+ end
217
+
218
+ context "manually invoked" do
219
+ before(:each) do
220
+ class Review < ActiveRecord::Base
221
+ belongs_to :article
222
+ end
223
+ @columns = Review.content_columns.dup
224
+ Review.schema_plus :validations => { :only => [:title] }
225
+ end
226
+
227
+ it "shouldn't validate associations not included in :only option" do
228
+ Review.new.should have(:no).errors_on(:article)
229
+ end
230
+
231
+ it "shouldn't change content columns of the model" do
232
+ @columns.should == Review.content_columns
233
+ end
234
+
235
+ end
236
+
237
+ context "when used with STI" do
238
+ around(:each) { |example| with_auto_validations(&example) }
239
+
240
+ it "should set validations on base class" do
241
+ class Review < ActiveRecord::Base ; end
242
+ class PremiumReview < Review ; end
243
+ PremiumReview.new
244
+ Review.new.should have(1).error_on(:author)
245
+ end
246
+
247
+ it "shouldn't create doubled validations" do
248
+ class Review < ActiveRecord::Base ; end
249
+ Review.new
250
+ class PremiumReview < Review ; end
251
+ PremiumReview.new.should have(1).error_on(:author)
252
+ end
253
+
254
+ end
255
+
256
+ protected
257
+ def with_auto_validations(value = true)
258
+ old_value = SchemaPlus.config.validations.auto_create
259
+ begin
260
+ SchemaPlus.config.validations.auto_create = value
261
+ yield
262
+ ensure
263
+ SchemaPlus.config.validations.auto_create = old_value
264
+ end
265
+ end
266
+
267
+ def define_schema
268
+ ActiveRecord::Migration.suppress_messages do
269
+ ActiveRecord::Schema.define do
270
+ connection.tables.each do |table| drop_table table end
271
+
272
+ create_table :articles, :force => true do |t|
273
+ t.string :title, :limit => 50
274
+ t.text :content, :null => false
275
+ t.integer :state
276
+ t.float :average_mark, :null => false
277
+ t.boolean :active, :null => false
278
+ end
279
+ add_index :articles, :title, :unique => true
280
+ add_index :articles, [:state, :active], :unique => true
281
+
282
+ create_table :reviews, :force => true do |t|
283
+ t.integer :article_id, :null => false
284
+ t.string :author, :null => false
285
+ t.string :content, :limit => 200
286
+ t.string :type
287
+ end
288
+ add_index :reviews, :article_id, :unique => true
289
+
290
+ end
291
+ end
292
+ end
293
+
294
+ end
@@ -0,0 +1,140 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe ActiveRecord do
4
+
5
+ let(:schema) { ActiveRecord::Schema }
6
+
7
+ let(:migration) { ActiveRecord::Migration }
8
+
9
+ let(:connection) { ActiveRecord::Base.connection }
10
+
11
+ let (:dump) {
12
+ StringIO.open { |stream|
13
+ ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream)
14
+ stream.string
15
+ }
16
+ }
17
+
18
+ context "views" do
19
+
20
+ around (:each) do |example|
21
+ define_schema_and_data
22
+ example.run
23
+ drop_definitions
24
+ end
25
+
26
+ it "should query correctly" do
27
+ @a_ones.all.collect(&:s).should == %W[one_one one_two]
28
+ @ab_ones.all.collect(&:s).should == %W[one_one]
29
+ end
30
+
31
+ it "should instrospect" do
32
+ connection.views.sort.should == %W[a_ones ab_ones]
33
+ connection.view_definition('a_ones').should match(%r{^SELECT .*b.*,.*s.* FROM .*items.* WHERE .*a.* = 1}i)
34
+ connection.view_definition('ab_ones').should match(%r{^SELECT .*s.* FROM .*a_ones.* WHERE .*b.* = 1}i)
35
+ end
36
+
37
+ it "should not be listed as a table" do
38
+ connection.tables.should_not include('a_ones')
39
+ connection.tables.should_not include('ab_ones')
40
+ end
41
+
42
+
43
+ it "should be included in schema dump" do
44
+ dump.should match(%r{create_view "a_ones", "SELECT .*b.*,.*s.* FROM .*items.* WHERE .*a.* = 1}i)
45
+ dump.should match(%r{create_view "ab_ones", "SELECT .*s.* FROM .*a_ones.* WHERE .*b.* = 1}i)
46
+ end
47
+
48
+ it "should be included in schema dump in dependency order" do
49
+ dump.should match(%r{create_table "items".*create_view "a_ones".*create_view "ab_ones"}m)
50
+ end
51
+
52
+ it "dump should not reference current database" do
53
+ # why check this? mysql default to providing the view definition
54
+ # with tables explicitly scoped to the current database, which
55
+ # resulted in the dump being bound to the current database. this
56
+ # caused trouble for rails, in which creates the schema dump file
57
+ # when in the (say) development database, but then uses it to
58
+ # initialize the test database when testing. this meant that the
59
+ # test database had views into the development database.
60
+ db = connection.respond_to?(:current_database)? connection.current_database : ActiveRecord::Base.configurations['schema_plus'][:database]
61
+ dump.should_not match(%r{#{connection.quote_table_name(db)}[.]})
62
+ end
63
+
64
+
65
+ if SchemaPlusHelpers.mysql?
66
+ context "in mysql" do
67
+
68
+ around(:each) do |example|
69
+ migration.suppress_messages do
70
+ begin
71
+ migration.drop_view :check if connection.views.include? 'check'
72
+ example.run
73
+ ensure
74
+ migration.drop_view :check if connection.views.include? 'check'
75
+ end
76
+ end
77
+ end
78
+
79
+ it "should introspect WITH CHECK OPTION" do
80
+ migration.create_view :check, 'SELECT * FROM items WHERE (a=2) WITH CHECK OPTION'
81
+ connection.view_definition('check').should match(%r{WITH CASCADED CHECK OPTION$})
82
+ end
83
+
84
+ it "should introspect WITH CASCADED CHECK OPTION" do
85
+ migration.create_view :check, 'SELECT * FROM items WHERE (a=2) WITH CASCADED CHECK OPTION'
86
+ connection.view_definition('check').should match(%r{WITH CASCADED CHECK OPTION$})
87
+ end
88
+
89
+ it "should introspect WITH LOCAL CHECK OPTION" do
90
+ migration.create_view :check, 'SELECT * FROM items WHERE (a=2) WITH LOCAL CHECK OPTION'
91
+ connection.view_definition('check').should match(%r{WITH LOCAL CHECK OPTION$})
92
+ end
93
+ end
94
+ end
95
+ end
96
+
97
+ protected
98
+
99
+ def define_schema_and_data
100
+ migration.suppress_messages do
101
+
102
+ schema.define do
103
+
104
+ create_table :items, :force => true do |t|
105
+ t.integer :a
106
+ t.integer :b
107
+ t.string :s
108
+ end
109
+
110
+ create_view :a_ones, "select b, s from items where a = 1"
111
+ create_view :ab_ones, "select s from a_ones where b = 1"
112
+ end
113
+ end
114
+ connection.execute "insert into items (a, b, s) values (1, 1, 'one_one')"
115
+ connection.execute "insert into items (a, b, s) values (1, 2, 'one_two')"
116
+ connection.execute "insert into items (a, b, s) values (2, 1, 'two_one')"
117
+ connection.execute "insert into items (a, b, s) values (2, 2, 'two_two')"
118
+
119
+ @a_ones = Class.new(ActiveRecord::Base) do set_table_name "a_ones" end
120
+ @ab_ones = Class.new(ActiveRecord::Base) do set_table_name "ab_ones" end
121
+ end
122
+
123
+ def drop_definitions
124
+ migration.suppress_messages do
125
+ schema.define do
126
+ drop_view "ab_ones"
127
+ drop_view "a_ones"
128
+ drop_table "items"
129
+ end
130
+ end
131
+ end
132
+
133
+ def dump
134
+ StringIO.open { |stream|
135
+ ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream)
136
+ stream.string
137
+ }
138
+ end
139
+
140
+ end