thinking-sphinx 3.0.0.pre → 3.0.0.rc

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. data/Gemfile +4 -1
  2. data/HISTORY +16 -0
  3. data/README.textile +41 -23
  4. data/lib/thinking_sphinx.rb +9 -0
  5. data/lib/thinking_sphinx/active_record.rb +1 -0
  6. data/lib/thinking_sphinx/active_record/attribute/sphinx_presenter.rb +20 -3
  7. data/lib/thinking_sphinx/active_record/base.rb +15 -2
  8. data/lib/thinking_sphinx/active_record/callbacks/delete_callbacks.rb +2 -2
  9. data/lib/thinking_sphinx/active_record/callbacks/update_callbacks.rb +1 -1
  10. data/lib/thinking_sphinx/active_record/field.rb +5 -0
  11. data/lib/thinking_sphinx/active_record/index.rb +10 -2
  12. data/lib/thinking_sphinx/active_record/interpreter.rb +9 -0
  13. data/lib/thinking_sphinx/active_record/property.rb +4 -0
  14. data/lib/thinking_sphinx/active_record/property_query.rb +112 -0
  15. data/lib/thinking_sphinx/active_record/sql_source.rb +10 -8
  16. data/lib/thinking_sphinx/active_record/sql_source/template.rb +3 -1
  17. data/lib/thinking_sphinx/configuration.rb +21 -24
  18. data/lib/thinking_sphinx/connection.rb +71 -0
  19. data/lib/thinking_sphinx/core.rb +1 -0
  20. data/lib/thinking_sphinx/core/field.rb +9 -0
  21. data/lib/thinking_sphinx/core/index.rb +8 -3
  22. data/lib/thinking_sphinx/deltas.rb +3 -0
  23. data/lib/thinking_sphinx/deltas/default_delta.rb +1 -1
  24. data/lib/thinking_sphinx/excerpter.rb +1 -1
  25. data/lib/thinking_sphinx/frameworks.rb +9 -0
  26. data/lib/thinking_sphinx/frameworks/plain.rb +8 -0
  27. data/lib/thinking_sphinx/frameworks/rails.rb +9 -0
  28. data/lib/thinking_sphinx/index.rb +5 -1
  29. data/lib/thinking_sphinx/real_time/callbacks/real_time_callbacks.rb +2 -2
  30. data/lib/thinking_sphinx/real_time/field.rb +2 -0
  31. data/lib/thinking_sphinx/real_time/property.rb +2 -0
  32. data/lib/thinking_sphinx/scopes.rb +6 -0
  33. data/lib/thinking_sphinx/search.rb +4 -0
  34. data/lib/thinking_sphinx/search/batch_inquirer.rb +1 -9
  35. data/lib/thinking_sphinx/search/merger.rb +3 -1
  36. data/lib/thinking_sphinx/sinatra.rb +5 -0
  37. data/lib/thinking_sphinx/test.rb +2 -2
  38. data/spec/acceptance/index_options_spec.rb +87 -0
  39. data/spec/acceptance/searching_within_a_model_spec.rb +7 -0
  40. data/spec/acceptance/specifying_sql_spec.rb +277 -0
  41. data/spec/acceptance/support/sphinx_controller.rb +4 -3
  42. data/spec/fixtures/database.yml +4 -0
  43. data/spec/internal/app/indices/admin_person_index.rb +3 -0
  44. data/spec/internal/app/models/admin/person.rb +3 -0
  45. data/spec/internal/app/models/book.rb +2 -0
  46. data/spec/internal/app/models/genre.rb +3 -0
  47. data/spec/internal/db/schema.rb +14 -0
  48. data/spec/thinking_sphinx/active_record/base_spec.rb +20 -14
  49. data/spec/thinking_sphinx/active_record/callbacks/delete_callbacks_spec.rb +6 -5
  50. data/spec/thinking_sphinx/active_record/callbacks/update_callbacks_spec.rb +6 -5
  51. data/spec/thinking_sphinx/active_record/index_spec.rb +1 -1
  52. data/spec/thinking_sphinx/active_record/sql_source_spec.rb +35 -27
  53. data/spec/thinking_sphinx/configuration_spec.rb +8 -45
  54. data/spec/thinking_sphinx/deltas/default_delta_spec.rb +4 -5
  55. data/spec/thinking_sphinx/deltas_spec.rb +6 -0
  56. data/spec/thinking_sphinx/excerpter_spec.rb +1 -2
  57. data/spec/thinking_sphinx/index_spec.rb +23 -10
  58. data/spec/thinking_sphinx/real_time/callbacks/real_time_callbacks_spec.rb +4 -4
  59. data/spec/thinking_sphinx/scopes_spec.rb +7 -0
  60. data/thinking-sphinx.gemspec +2 -3
  61. metadata +66 -26
@@ -0,0 +1,87 @@
1
+ require 'acceptance/spec_helper'
2
+
3
+ describe 'Index options' do
4
+ let(:index) { ThinkingSphinx::ActiveRecord::Index.new(:article) }
5
+
6
+ %w( infix prefix ).each do |type|
7
+ context "all fields are #{type}ed" do
8
+ before :each do
9
+ index.definition_block = Proc.new {
10
+ indexes title
11
+ set_property "min_#{type}_len".to_sym => 3
12
+ }
13
+ index.render
14
+ end
15
+
16
+ it "keeps #{type}_fields blank" do
17
+ index.send("#{type}_fields").should be_nil
18
+ end
19
+
20
+ it "sets min_#{type}_len" do
21
+ index.send("min_#{type}_len").should == 3
22
+ end
23
+ end
24
+
25
+ context "some fields are #{type}ed" do
26
+ before :each do
27
+ index.definition_block = Proc.new {
28
+ indexes title, "#{type}es".to_sym => true
29
+ indexes content
30
+ set_property "min_#{type}_len".to_sym => 3
31
+ }
32
+ index.render
33
+ end
34
+
35
+ it "#{type}_fields should contain the field" do
36
+ index.send("#{type}_fields").should == 'title'
37
+ end
38
+
39
+ it "sets min_#{type}_len" do
40
+ index.send("min_#{type}_len").should == 3
41
+ end
42
+ end
43
+ end
44
+
45
+ context "multiple source definitions" do
46
+ before :each do
47
+ index.definition_block = Proc.new {
48
+ define_source do
49
+ indexes title
50
+ end
51
+
52
+ define_source do
53
+ indexes title, content
54
+ end
55
+ }
56
+ index.render
57
+ end
58
+
59
+ it "stores each source definition" do
60
+ index.sources.length.should == 2
61
+ end
62
+
63
+ it "treats each source as separate" do
64
+ index.sources.first.fields.length.should == 2
65
+ index.sources.last.fields.length.should == 3
66
+ end
67
+ end
68
+
69
+ context 'wordcount fields and attributes' do
70
+ before :each do
71
+ index.definition_block = Proc.new {
72
+ indexes title, :wordcount => true
73
+
74
+ has content, :type => :wordcount
75
+ }
76
+ index.render
77
+ end
78
+
79
+ it "declares wordcount fields" do
80
+ index.sources.first.sql_field_str2wordcount.should == ['title']
81
+ end
82
+
83
+ it "declares wordcount attributes" do
84
+ index.sources.first.sql_attr_str2wordcount.should == ['content']
85
+ end
86
+ end
87
+ end
@@ -40,6 +40,13 @@ describe 'Searching within a model', :live => true do
40
40
  articles = Article.search('pancake', :indices => ['stemmed_article_core'])
41
41
  articles.to_a.should == [article]
42
42
  end
43
+
44
+ it "can search on namespaced models" do
45
+ person = Admin::Person.create :name => 'James Bond'
46
+ index
47
+
48
+ Admin::Person.search('Bond').to_a.should == [person]
49
+ end
43
50
  end
44
51
 
45
52
  describe 'Searching within a model with a realtime index', :live => true do
@@ -24,6 +24,18 @@ describe 'specifying SQL for index definitions' do
24
24
  query.should match(/LEFT OUTER JOIN .articles./)
25
25
  end
26
26
 
27
+ it "handles has-many :through joins" do
28
+ index = ThinkingSphinx::ActiveRecord::Index.new(:article)
29
+ index.definition_block = Proc.new {
30
+ indexes tags.name
31
+ }
32
+ index.render
33
+
34
+ query = index.sources.first.sql_query
35
+ query.should match(/LEFT OUTER JOIN .taggings./)
36
+ query.should match(/LEFT OUTER JOIN .tags./)
37
+ end
38
+
27
39
  it "handles GROUP BY clauses" do
28
40
  index = ThinkingSphinx::ActiveRecord::Index.new(:article)
29
41
  index.definition_block = Proc.new {
@@ -59,4 +71,269 @@ describe 'specifying SQL for index definitions' do
59
71
 
60
72
  index.sources.first.sql_attr_multi.should == ['uint tag_ids from field']
61
73
  end
74
+
75
+ it "provides the sanitize_sql helper within the index definition block" do
76
+ index = ThinkingSphinx::ActiveRecord::Index.new(:article)
77
+ index.definition_block = Proc.new {
78
+ indexes title
79
+ where sanitize_sql(["title != ?", 'secret'])
80
+ }
81
+ index.render
82
+
83
+ query = index.sources.first.sql_query
84
+ query.should match(/WHERE .+title != 'secret'.+ GROUP BY/)
85
+ end
86
+ end
87
+
88
+ describe 'separate queries for MVAs' do
89
+ let(:index) { ThinkingSphinx::ActiveRecord::Index.new(:article) }
90
+ let(:count) { ThinkingSphinx::Configuration.instance.indices.count }
91
+ let(:source) { index.sources.first }
92
+
93
+ it "generates an appropriate SQL query for an MVA" do
94
+ index.definition_block = Proc.new {
95
+ indexes title
96
+ has taggings.tag_id, :as => :tag_ids, :source => :query
97
+ }
98
+ index.render
99
+
100
+ attribute = source.sql_attr_multi.detect { |attribute|
101
+ attribute[/tag_ids/]
102
+ }
103
+ declaration, query = attribute.split(/;\s+/)
104
+
105
+ declaration.should == 'uint tag_ids from query'
106
+ query.should match(/^SELECT .taggings.\..article_id. \* #{count} \+ #{source.offset} AS .id., .taggings.\..tag_id. AS .tag_ids. FROM .taggings.\s?$/)
107
+ end
108
+
109
+ it "generates a SQL query with joins when appropriate for MVAs" do
110
+ index.definition_block = Proc.new {
111
+ indexes title
112
+ has taggings.tag.id, :as => :tag_ids, :source => :query
113
+ }
114
+ index.render
115
+
116
+ attribute = source.sql_attr_multi.detect { |attribute|
117
+ attribute[/tag_ids/]
118
+ }
119
+ declaration, query = attribute.split(/;\s+/)
120
+
121
+ declaration.should == 'uint tag_ids from query'
122
+ query.should match(/^SELECT .taggings.\..article_id. \* #{count} \+ #{source.offset} AS .id., .tags.\..id. AS .tag_ids. FROM .taggings. INNER JOIN .tags. ON .tags.\..id. = .taggings.\..tag_id.\s?$/)
123
+ end
124
+
125
+ it "respects has_many :through joins for MVA queries" do
126
+ index.definition_block = Proc.new {
127
+ indexes title
128
+ has tags.id, :as => :tag_ids, :source => :query
129
+ }
130
+ index.render
131
+
132
+ attribute = source.sql_attr_multi.detect { |attribute|
133
+ attribute[/tag_ids/]
134
+ }
135
+ declaration, query = attribute.split(/;\s+/)
136
+
137
+ declaration.should == 'uint tag_ids from query'
138
+ query.should match(/^SELECT .taggings.\..article_id. \* #{count} \+ #{source.offset} AS .id., .tags.\..id. AS .tag_ids. FROM .taggings. INNER JOIN .tags. ON .tags.\..id. = .taggings.\..tag_id.\s?$/)
139
+ end
140
+
141
+ it "can handle multiple joins for MVA queries" do
142
+ index = ThinkingSphinx::ActiveRecord::Index.new(:user)
143
+ index.definition_block = Proc.new {
144
+ indexes name
145
+ has articles.tags.id, :as => :tag_ids, :source => :query
146
+ }
147
+ index.render
148
+ source = index.sources.first
149
+
150
+ attribute = source.sql_attr_multi.detect { |attribute|
151
+ attribute[/tag_ids/]
152
+ }
153
+ declaration, query = attribute.split(/;\s+/)
154
+
155
+ declaration.should == 'uint tag_ids from query'
156
+ query.should match(/^SELECT .articles.\..user_id. \* #{count} \+ #{source.offset} AS .id., .tags.\..id. AS .tag_ids. FROM .articles. INNER JOIN .taggings. ON .taggings.\..article_id. = .articles.\..id. INNER JOIN .tags. ON .tags.\..id. = .taggings.\..tag_id.\s?$/)
157
+ end
158
+
159
+ it "can handle HABTM joins for MVA queries" do
160
+ pending "Efficient HABTM queries are tricky."
161
+ # We don't really have any need for other tables, but that doesn't lend
162
+ # itself nicely to Thinking Sphinx's DSL, nor ARel SQL generation. This is
163
+ # a low priority - manual SQL queries for this situation may work better.
164
+
165
+ index = ThinkingSphinx::ActiveRecord::Index.new(:book)
166
+ index.definition_block = Proc.new {
167
+ indexes title
168
+ has genres.id, :as => :genre_ids, :source => :query
169
+ }
170
+ index.render
171
+ source = index.sources.first
172
+
173
+ attribute = source.sql_attr_multi.detect { |attribute|
174
+ attribute[/genre_ids/]
175
+ }
176
+ declaration, query = attribute.split(/;\s+/)
177
+
178
+ declaration.should == 'uint genre_ids from query'
179
+ query.should match(/^SELECT .books_genres.\..book_id. \* #{count} \+ #{source.offset} AS .id., .genres.\..id. AS .genre_ids. FROM .books_genres. INNER JOIN .genres. ON .genres.\..id. = .books_genres.\..genre_id.\s?$/)
180
+ end
181
+
182
+ it "generates an appropriate range SQL queries for an MVA" do
183
+ index.definition_block = Proc.new {
184
+ indexes title
185
+ has taggings.tag_id, :as => :tag_ids, :source => :ranged_query
186
+ }
187
+ index.render
188
+
189
+ attribute = source.sql_attr_multi.detect { |attribute|
190
+ attribute[/tag_ids/]
191
+ }
192
+ declaration, query, range = attribute.split(/;\s+/)
193
+
194
+ declaration.should == 'uint tag_ids from ranged-query'
195
+ query.should match(/^SELECT .taggings.\..article_id. \* #{count} \+ #{source.offset} AS .id., .taggings.\..tag_id. AS .tag_ids. FROM .taggings. \s?WHERE \(.taggings.\..article_id. >= \$start\) AND \(.taggings.\..article_id. <= \$end\)$/)
196
+ range.should match(/^SELECT MIN\(.taggings.\..article_id.\), MAX\(.taggings.\..article_id.\) FROM .taggings.\s?$/)
197
+ end
198
+
199
+ it "generates a SQL query with joins when appropriate for MVAs" do
200
+ index.definition_block = Proc.new {
201
+ indexes title
202
+ has taggings.tag.id, :as => :tag_ids, :source => :ranged_query
203
+ }
204
+ index.render
205
+
206
+ attribute = source.sql_attr_multi.detect { |attribute|
207
+ attribute[/tag_ids/]
208
+ }
209
+ declaration, query, range = attribute.split(/;\s+/)
210
+
211
+ declaration.should == 'uint tag_ids from ranged-query'
212
+ query.should match(/^SELECT .taggings.\..article_id. \* #{count} \+ #{source.offset} AS .id., .tags.\..id. AS .tag_ids. FROM .taggings. INNER JOIN .tags. ON .tags.\..id. = .taggings.\..tag_id. \s?WHERE \(.taggings.\..article_id. >= \$start\) AND \(.taggings.\..article_id. <= \$end\)$/)
213
+ range.should match(/^SELECT MIN\(.taggings.\..article_id.\), MAX\(.taggings.\..article_id.\) FROM .taggings.\s?$/)
214
+ end
215
+
216
+ it "respects custom SQL snippets as the query value" do
217
+ index.definition_block = Proc.new {
218
+ indexes title
219
+ has 'My Custom SQL Query', :as => :tag_ids, :source => :query,
220
+ :type => :integer, :multi => true
221
+ }
222
+ index.render
223
+
224
+ attribute = source.sql_attr_multi.detect { |attribute|
225
+ attribute[/tag_ids/]
226
+ }
227
+ declaration, query = attribute.split(/;\s+/)
228
+
229
+ declaration.should == 'uint tag_ids from query'
230
+ query.should == 'My Custom SQL Query'
231
+ end
232
+
233
+ it "respects custom SQL snippets as the ranged query value" do
234
+ index.definition_block = Proc.new {
235
+ indexes title
236
+ has 'My Custom SQL Query; And a Range', :as => :tag_ids,
237
+ :source => :ranged_query, :type => :integer, :multi => true
238
+ }
239
+ index.render
240
+
241
+ attribute = source.sql_attr_multi.detect { |attribute|
242
+ attribute[/tag_ids/]
243
+ }
244
+ declaration, query, range = attribute.split(/;\s+/)
245
+
246
+ declaration.should == 'uint tag_ids from ranged-query'
247
+ query.should == 'My Custom SQL Query'
248
+ range.should == 'And a Range'
249
+ end
250
+ end
251
+
252
+ describe 'separate queries for field' do
253
+ let(:index) { ThinkingSphinx::ActiveRecord::Index.new(:article) }
254
+ let(:count) { ThinkingSphinx::Configuration.instance.indices.count }
255
+ let(:source) { index.sources.first }
256
+
257
+ it "generates a SQL query with joins when appropriate for MVF" do
258
+ index.definition_block = Proc.new {
259
+ indexes taggings.tag.name, :as => :tags, :source => :query
260
+ }
261
+ index.render
262
+
263
+ field = source.sql_joined_field.detect { |field| field[/tags/] }
264
+ declaration, query = field.split(/;\s+/)
265
+
266
+ declaration.should == 'tags from query'
267
+ query.should match(/^SELECT .taggings.\..article_id. \* #{count} \+ #{source.offset} AS .id., .tags.\..name. AS .tags. FROM .taggings. INNER JOIN .tags. ON .tags.\..id. = .taggings.\..tag_id. ORDER BY .taggings.\..article_id. ASC\s?$/)
268
+ end
269
+
270
+ it "respects has_many :through joins for MVF queries" do
271
+ index.definition_block = Proc.new {
272
+ indexes tags.name, :as => :tags, :source => :query
273
+ }
274
+ index.render
275
+
276
+ field = source.sql_joined_field.detect { |field| field[/tags/] }
277
+ declaration, query = field.split(/;\s+/)
278
+
279
+ declaration.should == 'tags from query'
280
+ query.should match(/^SELECT .taggings.\..article_id. \* #{count} \+ #{source.offset} AS .id., .tags.\..name. AS .tags. FROM .taggings. INNER JOIN .tags. ON .tags.\..id. = .taggings.\..tag_id. ORDER BY .taggings.\..article_id. ASC\s?$/)
281
+ end
282
+
283
+ it "can handle multiple joins for MVF queries" do
284
+ index = ThinkingSphinx::ActiveRecord::Index.new(:user)
285
+ index.definition_block = Proc.new {
286
+ indexes articles.tags.name, :as => :tags, :source => :query
287
+ }
288
+ index.render
289
+ source = index.sources.first
290
+
291
+ field = source.sql_joined_field.detect { |field| field[/tags/] }
292
+ declaration, query = field.split(/;\s+/)
293
+
294
+ declaration.should == 'tags from query'
295
+ query.should match(/^SELECT .articles.\..user_id. \* #{count} \+ #{source.offset} AS .id., .tags.\..name. AS .tags. FROM .articles. INNER JOIN .taggings. ON .taggings.\..article_id. = .articles.\..id. INNER JOIN .tags. ON .tags.\..id. = .taggings.\..tag_id. ORDER BY .articles.\..user_id. ASC\s?$/)
296
+ end
297
+
298
+ it "generates a SQL query with joins when appropriate for MVFs" do
299
+ index.definition_block = Proc.new {
300
+ indexes taggings.tag.name, :as => :tags, :source => :ranged_query
301
+ }
302
+ index.render
303
+
304
+ field = source.sql_joined_field.detect { |field| field[/tags/] }
305
+ declaration, query, range = field.split(/;\s+/)
306
+
307
+ declaration.should == 'tags from ranged-query'
308
+ query.should match(/^SELECT .taggings.\..article_id. \* #{count} \+ #{source.offset} AS .id., .tags.\..name. AS .tags. FROM .taggings. INNER JOIN .tags. ON .tags.\..id. = .taggings.\..tag_id. \s?WHERE \(.taggings.\..article_id. >= \$start\) AND \(.taggings.\..article_id. <= \$end\) ORDER BY .taggings.\..article_id. ASC$/)
309
+ range.should match(/^SELECT MIN\(.taggings.\..article_id.\), MAX\(.taggings.\..article_id.\) FROM .taggings.\s?$/)
310
+ end
311
+
312
+ it "respects custom SQL snippets as the query value" do
313
+ index.definition_block = Proc.new {
314
+ indexes 'My Custom SQL Query', :as => :tags, :source => :query
315
+ }
316
+ index.render
317
+
318
+ field = source.sql_joined_field.detect { |field| field[/tags/] }
319
+ declaration, query = field.split(/;\s+/)
320
+
321
+ declaration.should == 'tags from query'
322
+ query.should == 'My Custom SQL Query'
323
+ end
324
+
325
+ it "respects custom SQL snippets as the ranged query value" do
326
+ index.definition_block = Proc.new {
327
+ indexes 'My Custom SQL Query; And a Range', :as => :tags,
328
+ :source => :ranged_query
329
+ }
330
+ index.render
331
+
332
+ field = source.sql_joined_field.detect { |field| field[/tags/] }
333
+ declaration, query, range = field.split(/;\s+/)
334
+
335
+ declaration.should == 'tags from ranged-query'
336
+ query.should == 'My Custom SQL Query'
337
+ range.should == 'And a Range'
338
+ end
62
339
  end
@@ -8,12 +8,13 @@ class SphinxController
8
8
  config.render_to_file && index
9
9
 
10
10
  ThinkingSphinx::Configuration.reset
11
- ActiveSupport::Dependencies.clear
12
11
 
13
- config.index_paths.each do |path|
14
- Dir["#{path}/**/*.rb"].each { |file| $LOADED_FEATURES.delete file }
12
+ ActiveSupport::Dependencies.loaded.each do |path|
13
+ $LOADED_FEATURES.delete "#{path}.rb"
15
14
  end
16
15
 
16
+ ActiveSupport::Dependencies.clear
17
+
17
18
  config.searchd.mysql41 = 9307
18
19
  config.settings['quiet_deltas'] = true
19
20
  config.settings['attribute_updates'] = true
@@ -0,0 +1,4 @@
1
+ username: root
2
+ password:
3
+ host: localhost
4
+ database: thinking_sphinx
@@ -0,0 +1,3 @@
1
+ ThinkingSphinx::Index.define 'admin/person', :with => :active_record do
2
+ indexes name
3
+ end
@@ -0,0 +1,3 @@
1
+ class Admin::Person < ActiveRecord::Base
2
+ self.table_name = 'admin_people'
3
+ end
@@ -1,6 +1,8 @@
1
1
  class Book < ActiveRecord::Base
2
2
  include ThinkingSphinx::Scopes
3
3
 
4
+ has_and_belongs_to_many :genres
5
+
4
6
  sphinx_scope(:by_query) { |query| query }
5
7
  sphinx_scope(:by_year) do |year|
6
8
  {:with => {:year => year}}
@@ -0,0 +1,3 @@
1
+ class Genre < ActiveRecord::Base
2
+ #
3
+ end
@@ -1,4 +1,9 @@
1
1
  ActiveRecord::Schema.define do
2
+ create_table(:admin_people, :force => true) do |t|
3
+ t.string :name
4
+ t.timestamps
5
+ end
6
+
2
7
  create_table(:animals, :force => true) do |t|
3
8
  t.string :name
4
9
  t.string :type
@@ -21,6 +26,11 @@ ActiveRecord::Schema.define do
21
26
  t.timestamps
22
27
  end
23
28
 
29
+ create_table(:books_genres, :force => true, :id => false) do |t|
30
+ t.integer :book_id
31
+ t.integer :genre_id
32
+ end
33
+
24
34
  create_table(:cities, :force => true) do |t|
25
35
  t.string :name
26
36
  t.float :lat
@@ -32,6 +42,10 @@ ActiveRecord::Schema.define do
32
42
  t.timestamps
33
43
  end
34
44
 
45
+ create_table(:genres, :force => true) do |t|
46
+ t.string :name
47
+ end
48
+
35
49
  create_table(:products, :force => true) do |t|
36
50
  t.string :name
37
51
  end