schema_plus 0.1.0.pre1

Sign up to get free protection for your applications and to get access to all the features.
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