thinking-sphinx 3.0.0.pre → 3.0.0.rc

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