thinking-sphinx 3.1.0 → 3.1.1

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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -1
  3. data/.travis.yml +4 -7
  4. data/HISTORY +27 -0
  5. data/README.textile +38 -218
  6. data/gemfiles/rails_3_2.gemfile +2 -3
  7. data/gemfiles/rails_4_0.gemfile +2 -3
  8. data/gemfiles/rails_4_1.gemfile +2 -3
  9. data/lib/thinking_sphinx.rb +1 -0
  10. data/lib/thinking_sphinx/active_record.rb +1 -0
  11. data/lib/thinking_sphinx/active_record/association_proxy.rb +1 -0
  12. data/lib/thinking_sphinx/active_record/association_proxy/attribute_finder.rb +5 -10
  13. data/lib/thinking_sphinx/active_record/association_proxy/attribute_matcher.rb +38 -0
  14. data/lib/thinking_sphinx/active_record/attribute/type.rb +19 -8
  15. data/lib/thinking_sphinx/active_record/base.rb +3 -1
  16. data/lib/thinking_sphinx/active_record/callbacks/delta_callbacks.rb +1 -1
  17. data/lib/thinking_sphinx/active_record/callbacks/update_callbacks.rb +4 -1
  18. data/lib/thinking_sphinx/active_record/index.rb +4 -4
  19. data/lib/thinking_sphinx/active_record/property_query.rb +57 -27
  20. data/lib/thinking_sphinx/active_record/simple_many_query.rb +35 -0
  21. data/lib/thinking_sphinx/capistrano/v3.rb +11 -10
  22. data/lib/thinking_sphinx/configuration.rb +23 -6
  23. data/lib/thinking_sphinx/connection.rb +8 -9
  24. data/lib/thinking_sphinx/errors.rb +7 -2
  25. data/lib/thinking_sphinx/facet.rb +2 -2
  26. data/lib/thinking_sphinx/facet_search.rb +4 -2
  27. data/lib/thinking_sphinx/logger.rb +7 -0
  28. data/lib/thinking_sphinx/masks/group_enumerators_mask.rb +4 -4
  29. data/lib/thinking_sphinx/middlewares/inquirer.rb +2 -2
  30. data/lib/thinking_sphinx/middlewares/sphinxql.rb +6 -2
  31. data/lib/thinking_sphinx/middlewares/stale_id_filter.rb +1 -1
  32. data/lib/thinking_sphinx/real_time/callbacks/real_time_callbacks.rb +6 -1
  33. data/lib/thinking_sphinx/real_time/property.rb +2 -1
  34. data/lib/thinking_sphinx/real_time/transcriber.rb +7 -3
  35. data/lib/thinking_sphinx/search.rb +14 -4
  36. data/lib/thinking_sphinx/search/context.rb +0 -6
  37. data/lib/thinking_sphinx/test.rb +11 -2
  38. data/lib/thinking_sphinx/wildcard.rb +7 -1
  39. data/spec/acceptance/association_scoping_spec.rb +55 -15
  40. data/spec/acceptance/geosearching_spec.rb +8 -2
  41. data/spec/acceptance/real_time_updates_spec.rb +9 -0
  42. data/spec/acceptance/specifying_sql_spec.rb +31 -17
  43. data/spec/internal/app/indices/car_index.rb +5 -0
  44. data/spec/internal/app/models/car.rb +5 -0
  45. data/spec/internal/app/models/category.rb +2 -1
  46. data/spec/internal/app/models/manufacturer.rb +3 -0
  47. data/spec/internal/db/schema.rb +9 -0
  48. data/spec/thinking_sphinx/active_record/attribute/type_spec.rb +7 -0
  49. data/spec/thinking_sphinx/active_record/base_spec.rb +17 -0
  50. data/spec/thinking_sphinx/active_record/callbacks/update_callbacks_spec.rb +2 -1
  51. data/spec/thinking_sphinx/configuration_spec.rb +40 -2
  52. data/spec/thinking_sphinx/errors_spec.rb +21 -0
  53. data/spec/thinking_sphinx/facet_search_spec.rb +6 -6
  54. data/spec/thinking_sphinx/middlewares/inquirer_spec.rb +0 -4
  55. data/spec/thinking_sphinx/middlewares/sphinxql_spec.rb +25 -0
  56. data/spec/thinking_sphinx/middlewares/stale_id_filter_spec.rb +2 -2
  57. data/spec/thinking_sphinx/real_time/callbacks/real_time_callbacks_spec.rb +2 -1
  58. data/spec/thinking_sphinx/search_spec.rb +56 -0
  59. data/spec/thinking_sphinx/wildcard_spec.rb +5 -0
  60. data/thinking-sphinx.gemspec +2 -2
  61. metadata +40 -32
  62. data/sketchpad.rb +0 -58
  63. data/spec/internal/log/.gitignore +0 -1
@@ -175,7 +175,7 @@ describe 'separate queries for MVAs' do
175
175
  declaration, query = attribute.split(/;\s+/)
176
176
 
177
177
  declaration.should == 'uint tag_ids from query'
178
- query.should match(/^SELECT .taggings.\..article_id. \* #{count} \+ #{source.offset} AS .id., .taggings.\..tag_id. AS .tag_ids. FROM .taggings.\s?$/)
178
+ 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. IS NOT NULL\)$/)
179
179
  end
180
180
 
181
181
  it "generates a SQL query with joins when appropriate for MVAs" do
@@ -191,7 +191,7 @@ describe 'separate queries for MVAs' do
191
191
  declaration, query = attribute.split(/;\s+/)
192
192
 
193
193
  declaration.should == 'uint tag_ids from query'
194
- 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?$/)
194
+ 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. WHERE \(.taggings.\..article_id. IS NOT NULL\)\s?$/)
195
195
  end
196
196
 
197
197
  it "respects has_many :through joins for MVA queries" do
@@ -207,7 +207,7 @@ describe 'separate queries for MVAs' do
207
207
  declaration, query = attribute.split(/;\s+/)
208
208
 
209
209
  declaration.should == 'uint tag_ids from query'
210
- 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?$/)
210
+ 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. WHERE \(.taggings.\..article_id. IS NOT NULL\)\s?$/)
211
211
  end
212
212
 
213
213
  it "can handle multiple joins for MVA queries" do
@@ -225,15 +225,10 @@ describe 'separate queries for MVAs' do
225
225
  declaration, query = attribute.split(/;\s+/)
226
226
 
227
227
  declaration.should == 'uint tag_ids from query'
228
- 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?$/)
228
+ 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. WHERE \(.articles.\..user_id. IS NOT NULL\)\s?$/)
229
229
  end
230
230
 
231
- it "can handle HABTM joins for MVA queries" do
232
- pending "Efficient HABTM queries are tricky."
233
- # We don't really have any need for other tables, but that doesn't lend
234
- # itself nicely to Thinking Sphinx's DSL, nor ARel SQL generation. This is
235
- # a low priority - manual SQL queries for this situation may work better.
236
-
231
+ it "can handle simple HABTM joins for MVA queries" do
237
232
  index = ThinkingSphinx::ActiveRecord::Index.new(:book)
238
233
  index.definition_block = Proc.new {
239
234
  indexes title
@@ -248,7 +243,7 @@ describe 'separate queries for MVAs' do
248
243
  declaration, query = attribute.split(/;\s+/)
249
244
 
250
245
  declaration.should == 'uint genre_ids from query'
251
- 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?$/)
246
+ query.should match(/^SELECT .books_genres.\..book_id. \* #{count} \+ #{source.offset} AS .id., .books_genres.\..genre_id. AS .genre_ids. FROM .books_genres.\s?$/)
252
247
  end
253
248
 
254
249
  it "generates an appropriate range SQL queries for an MVA" do
@@ -264,7 +259,7 @@ describe 'separate queries for MVAs' do
264
259
  declaration, query, range = attribute.split(/;\s+/)
265
260
 
266
261
  declaration.should == 'uint tag_ids from ranged-query'
267
- 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. BETWEEN \$start AND \$end\)$/)
262
+ 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. BETWEEN \$start AND \$end\) AND \(.taggings.\..article_id. IS NOT NULL\)$/)
268
263
  range.should match(/^SELECT MIN\(.taggings.\..article_id.\), MAX\(.taggings.\..article_id.\) FROM .taggings.\s?$/)
269
264
  end
270
265
 
@@ -281,10 +276,29 @@ describe 'separate queries for MVAs' do
281
276
  declaration, query, range = attribute.split(/;\s+/)
282
277
 
283
278
  declaration.should == 'uint tag_ids from ranged-query'
284
- 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. BETWEEN \$start AND \$end\)$/)
279
+ 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. BETWEEN \$start AND \$end\) AND \(.taggings.\..article_id. IS NOT NULL\)$/)
285
280
  range.should match(/^SELECT MIN\(.taggings.\..article_id.\), MAX\(.taggings.\..article_id.\) FROM .taggings.\s?$/)
286
281
  end
287
282
 
283
+ it "can handle ranged queries for simple HABTM joins for MVA queries" do
284
+ index = ThinkingSphinx::ActiveRecord::Index.new(:book)
285
+ index.definition_block = Proc.new {
286
+ indexes title
287
+ has genres.id, :as => :genre_ids, :source => :ranged_query
288
+ }
289
+ index.render
290
+ source = index.sources.first
291
+
292
+ attribute = source.sql_attr_multi.detect { |attribute|
293
+ attribute[/genre_ids/]
294
+ }
295
+ declaration, query, range = attribute.split(/;\s+/)
296
+
297
+ declaration.should == 'uint genre_ids from ranged-query'
298
+ query.should match(/^SELECT .books_genres.\..book_id. \* #{count} \+ #{source.offset} AS .id., .books_genres.\..genre_id. AS .genre_ids. FROM .books_genres. WHERE \(.books_genres.\..book_id. BETWEEN \$start AND \$end\)$/)
299
+ range.should match(/^SELECT MIN\(.books_genres.\..book_id.\), MAX\(.books_genres.\..book_id.\) FROM .books_genres.$/)
300
+ end
301
+
288
302
  it "respects custom SQL snippets as the query value" do
289
303
  index.definition_block = Proc.new {
290
304
  indexes title
@@ -355,7 +369,7 @@ describe 'separate queries for field' do
355
369
  declaration, query = field.split(/;\s+/)
356
370
 
357
371
  declaration.should == 'tags from query'
358
- 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? ORDER BY .taggings.\..article_id. ASC\s?$/)
372
+ 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. IS NOT NULL\)\s? ORDER BY .taggings.\..article_id. ASC\s?$/)
359
373
  end
360
374
 
361
375
  it "respects has_many :through joins for MVF queries" do
@@ -368,7 +382,7 @@ describe 'separate queries for field' do
368
382
  declaration, query = field.split(/;\s+/)
369
383
 
370
384
  declaration.should == 'tags from query'
371
- 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? ORDER BY .taggings.\..article_id. ASC\s?$/)
385
+ 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. IS NOT NULL\)\s? ORDER BY .taggings.\..article_id. ASC\s?$/)
372
386
  end
373
387
 
374
388
  it "can handle multiple joins for MVF queries" do
@@ -383,7 +397,7 @@ describe 'separate queries for field' do
383
397
  declaration, query = field.split(/;\s+/)
384
398
 
385
399
  declaration.should == 'tags from query'
386
- 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.\s? ORDER BY .articles.\..user_id. ASC\s?$/)
400
+ 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.\s? WHERE \(.articles.\..user_id. IS NOT NULL\)\s? ORDER BY .articles.\..user_id. ASC\s?$/)
387
401
  end
388
402
 
389
403
  it "generates a SQL query with joins when appropriate for MVFs" do
@@ -396,7 +410,7 @@ describe 'separate queries for field' do
396
410
  declaration, query, range = field.split(/;\s+/)
397
411
 
398
412
  declaration.should == 'tags from ranged-query'
399
- 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. BETWEEN \$start AND \$end\)\s? ORDER BY .taggings.\..article_id. ASC$/)
413
+ 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. BETWEEN \$start AND \$end\) AND \(.taggings.\..article_id. IS NOT NULL\)\s? ORDER BY .taggings.\..article_id. ASC$/)
400
414
  range.should match(/^SELECT MIN\(.taggings.\..article_id.\), MAX\(.taggings.\..article_id.\) FROM .taggings.\s?$/)
401
415
  end
402
416
 
@@ -0,0 +1,5 @@
1
+ ThinkingSphinx::Index.define :car, :with => :real_time do
2
+ indexes name, :sortable => true
3
+
4
+ has manufacturer_id, :type => :integer
5
+ end
@@ -0,0 +1,5 @@
1
+ class Car < ActiveRecord::Base
2
+ belongs_to :manufacturer
3
+
4
+ after_save ThinkingSphinx::RealTime.callback_for(:car)
5
+ end
@@ -1,3 +1,4 @@
1
1
  class Category < ActiveRecord::Base
2
- #
2
+ has_many :categorisations
3
+ has_many :products, :through => :categorisations
3
4
  end
@@ -0,0 +1,3 @@
1
+ class Manufacturer < ActiveRecord::Base
2
+ has_many :cars
3
+ end
@@ -17,6 +17,15 @@ ActiveRecord::Schema.define do
17
17
  t.timestamps
18
18
  end
19
19
 
20
+ create_table(:manufacturers, :force => true) do |t|
21
+ t.string :name
22
+ end
23
+
24
+ create_table(:cars, :force => true) do |t|
25
+ t.integer :manufacturer_id
26
+ t.string :name
27
+ end
28
+
20
29
  create_table(:books, :force => true) do |t|
21
30
  t.string :title
22
31
  t.string :author
@@ -4,6 +4,7 @@ module ThinkingSphinx
4
4
  end
5
5
  end
6
6
 
7
+ require 'thinking_sphinx/errors'
7
8
  require 'thinking_sphinx/active_record/attribute/type'
8
9
 
9
10
  describe ThinkingSphinx::ActiveRecord::Attribute::Type do
@@ -143,5 +144,11 @@ describe ThinkingSphinx::ActiveRecord::Attribute::Type do
143
144
 
144
145
  type.type.should == :timestamp
145
146
  end
147
+
148
+ it 'raises an error if the database column does not exist' do
149
+ model.columns.clear
150
+
151
+ expect { type.type }.to raise_error(ThinkingSphinx::MissingColumnError)
152
+ end
146
153
  end
147
154
  end
@@ -48,6 +48,12 @@ describe ThinkingSphinx::ActiveRecord::Base do
48
48
  end
49
49
 
50
50
  describe '.search' do
51
+ let(:stack) { double('stack', :call => true) }
52
+
53
+ before :each do
54
+ stub_const 'ThinkingSphinx::Middlewares::DEFAULT', stack
55
+ end
56
+
51
57
  it "returns a new search object" do
52
58
  model.search.should be_a(ThinkingSphinx::Search)
53
59
  end
@@ -60,6 +66,17 @@ describe ThinkingSphinx::ActiveRecord::Base do
60
66
  model.search('pancakes').options[:classes].should == [model]
61
67
  end
62
68
 
69
+ it "passes through options to the search object" do
70
+ model.search('pancakes', populate: true).
71
+ options[:populate].should be_true
72
+ end
73
+
74
+ it "should automatically populate when :populate is set to true" do
75
+ stack.should_receive(:call).and_return(true)
76
+
77
+ model.search('pancakes', populate: true)
78
+ end
79
+
63
80
  it "merges the :classes option with the model" do
64
81
  model.search('pancakes', :classes => [sub_model]).
65
82
  options[:classes].should == [sub_model, model]
@@ -20,7 +20,8 @@ describe ThinkingSphinx::ActiveRecord::Callbacks::UpdateCallbacks do
20
20
  :indices_for_references => [index]) }
21
21
  let(:connection) { double('connection', :execute => '') }
22
22
  let(:index) { double 'index', :name => 'article_core',
23
- :sources => [source], :document_id_for_key => 3, :distributed? => false }
23
+ :sources => [source], :document_id_for_key => 3, :distributed? => false,
24
+ :type => 'plain'}
24
25
  let(:source) { double('source', :attributes => []) }
25
26
 
26
27
  before :each do
@@ -128,6 +128,10 @@ describe ThinkingSphinx::Configuration do
128
128
  end
129
129
 
130
130
  describe '#initialize' do
131
+ before :each do
132
+ FileUtils.rm_rf Rails.root.join('log')
133
+ end
134
+
131
135
  it "sets the daemon pid file within log for the Rails app" do
132
136
  config.searchd.pid_file.
133
137
  should == File.join(Rails.root, 'log', 'test.sphinx.pid')
@@ -154,6 +158,19 @@ describe ThinkingSphinx::Configuration do
154
158
 
155
159
  config.searchd.workers.should == 'none'
156
160
  end
161
+
162
+ it 'adds settings to indexer without common section' do
163
+ write_configuration 'lemmatizer_base' => 'foo'
164
+
165
+ expect(config.indexer.lemmatizer_base).to eq('foo')
166
+ end
167
+
168
+ it 'adds settings to common section if requested' do
169
+ write_configuration 'lemmatizer_base' => 'foo',
170
+ 'common_sphinx_configuration' => true
171
+
172
+ expect(config.common.lemmatizer_base).to eq('foo')
173
+ end
157
174
  end
158
175
 
159
176
  describe '#next_offset' do
@@ -176,6 +193,13 @@ describe ThinkingSphinx::Configuration do
176
193
  end
177
194
 
178
195
  describe '#preload_indices' do
196
+ let(:distributor) { double :reconcile => true }
197
+
198
+ before :each do
199
+ stub_const 'ThinkingSphinx::Configuration::DistributedIndices',
200
+ double(:new => distributor)
201
+ end
202
+
179
203
  it "searches each index path for ruby files" do
180
204
  config.index_paths.replace ['/path/to/indices', '/path/to/other/indices']
181
205
 
@@ -217,6 +241,20 @@ describe ThinkingSphinx::Configuration do
217
241
  config.preload_indices
218
242
  config.preload_indices
219
243
  end
244
+
245
+ it 'adds distributed indices' do
246
+ distributor.should_receive(:reconcile)
247
+
248
+ config.preload_indices
249
+ end
250
+
251
+ it 'does not add distributed indices if disabled' do
252
+ write_configuration('distributed_indices' => false)
253
+
254
+ distributor.should_not_receive(:reconcile)
255
+
256
+ config.preload_indices
257
+ end
220
258
  end
221
259
 
222
260
  describe '#render' do
@@ -373,8 +411,8 @@ describe ThinkingSphinx::Configuration do
373
411
  end
374
412
 
375
413
  describe '#version' do
376
- it "defaults to 2.0.6" do
377
- config.version.should == '2.0.6'
414
+ it "defaults to 2.1.4" do
415
+ config.version.should == '2.1.4'
378
416
  end
379
417
 
380
418
  it "respects supplied YAML versions" do
@@ -33,6 +33,27 @@ describe ThinkingSphinx::SphinxError do
33
33
  should be_a(ThinkingSphinx::ConnectionError)
34
34
  end
35
35
 
36
+ it 'prefixes the connection error message' do
37
+ error.stub :message => "Can't connect to MySQL server on '127.0.0.1' (61)"
38
+
39
+ ThinkingSphinx::SphinxError.new_from_mysql(error).message.
40
+ should == "Error connecting to Sphinx via the MySQL protocol. Can't connect to MySQL server on '127.0.0.1' (61)"
41
+ end
42
+
43
+ it "translates jdbc connection errors" do
44
+ error.stub :message => "Communications link failure"
45
+
46
+ ThinkingSphinx::SphinxError.new_from_mysql(error).
47
+ should be_a(ThinkingSphinx::ConnectionError)
48
+ end
49
+
50
+ it 'prefixes the jdbc connection error message' do
51
+ error.stub :message => "Communications link failure"
52
+
53
+ ThinkingSphinx::SphinxError.new_from_mysql(error).message.
54
+ should == "Error connecting to Sphinx via the MySQL protocol. Communications link failure"
55
+ end
56
+
36
57
  it "defaults to sphinx errors" do
37
58
  error.stub :message => 'index foo: unknown error: something is wrong'
38
59
 
@@ -26,12 +26,12 @@ describe ThinkingSphinx::FacetSearch do
26
26
  DumbSearch = ::Struct.new(:query, :options) do
27
27
  def raw
28
28
  [{
29
- 'sphinx_internal_class' => 'Foo',
30
- 'price_bracket' => 3,
31
- 'tag_ids' => '1,2',
32
- 'category_id' => 11,
33
- ThinkingSphinx::SphinxQL.count => 5,
34
- ThinkingSphinx::SphinxQL.group_by => 2
29
+ 'sphinx_internal_class' => 'Foo',
30
+ 'price_bracket' => 3,
31
+ 'tag_ids' => '1,2',
32
+ 'category_id' => 11,
33
+ 'sphinx_internal_count' => 5,
34
+ 'sphinx_internal_group' => 2
35
35
  }]
36
36
  end
37
37
  end
@@ -15,10 +15,6 @@ describe ThinkingSphinx::Middlewares::Inquirer do
15
15
  :results => [[:raw], [{'Variable_name' => 'meta', 'Value' => 'value'}]]) }
16
16
 
17
17
  before :each do
18
- context.stub(:log) do |notification, message, &block|
19
- block.call unless block.nil?
20
- end
21
-
22
18
  batch_class = double
23
19
  batch_class.stub(:new).and_return(batch_inquirer)
24
20
 
@@ -6,6 +6,9 @@ module ActiveRecord
6
6
  class Base; end
7
7
  end
8
8
 
9
+ class SphinxQLSubclass
10
+ end
11
+
9
12
  require 'active_support/core_ext/module/attribute_accessors'
10
13
  require 'active_support/core_ext/module/delegation'
11
14
  require 'active_support/core_ext/object/blank'
@@ -185,6 +188,28 @@ describe ThinkingSphinx::Middlewares::SphinxQL do
185
188
  middleware.call [context]
186
189
  end
187
190
 
191
+ it "ignores blank subclasses" do
192
+ db_connection = double('db connection', :select_values => [''],
193
+ :schema_cache => double('cache', :table_exists? => false))
194
+ supermodel = Class.new(ActiveRecord::Base) do
195
+ def self.name; 'Cat'; end
196
+ def self.inheritance_column; 'type'; end
197
+ end
198
+ supermodel.stub :connection => db_connection, :column_names => ['type']
199
+ submodel = Class.new(supermodel) do
200
+ def self.name; 'Lion'; end
201
+ def self.inheritance_column; 'type'; end
202
+ def self.table_name; 'cats'; end
203
+ end
204
+ submodel.stub :connection => db_connection, :column_names => ['type'],
205
+ :descendants => []
206
+ index_set.first.stub :reference => :cat
207
+
208
+ search.options[:classes] = [submodel]
209
+
210
+ expect { middleware.call [context] }.to_not raise_error
211
+ end
212
+
188
213
  it "filters out deleted values by default" do
189
214
  sphinx_sql.should_receive(:where).with(:sphinx_deleted => false).
190
215
  and_return(sphinx_sql)
@@ -15,7 +15,7 @@ describe ThinkingSphinx::Middlewares::StaleIdFilter do
15
15
 
16
16
  describe '#call' do
17
17
  before :each do
18
- context.stub :search => search, :log => true
18
+ context.stub :search => search
19
19
  end
20
20
 
21
21
  context 'one stale ids exception' do
@@ -88,4 +88,4 @@ describe ThinkingSphinx::Middlewares::StaleIdFilter do
88
88
  end
89
89
  end
90
90
  end
91
- end
91
+ end
@@ -5,7 +5,8 @@ describe ThinkingSphinx::RealTime::Callbacks::RealTimeCallbacks do
5
5
  ThinkingSphinx::RealTime::Callbacks::RealTimeCallbacks.new :article
6
6
  }
7
7
  let(:instance) { double('instance', :id => 12) }
8
- let(:config) { double('config', :indices_for_references => [index]) }
8
+ let(:config) { double('config', :indices_for_references => [index],
9
+ :settings => {}) }
9
10
  let(:index) { double('index', :name => 'my_index', :is_a? => true,
10
11
  :document_id_for_key => 123, :fields => [], :attributes => [],
11
12
  :conditions => []) }
@@ -5,6 +5,34 @@ describe ThinkingSphinx::Search do
5
5
  let(:context) { {:results => []} }
6
6
  let(:stack) { double('stack', :call => true) }
7
7
 
8
+ let(:pagination_mask_methods) do
9
+ [:first_page?,
10
+ :last_page?,
11
+ :next_page,
12
+ :next_page?,
13
+ :page,
14
+ :per,
15
+ :previous_page,
16
+ :total_entries,
17
+ :total_count,
18
+ :count,
19
+ :total_pages,
20
+ :page_count,
21
+ :num_pages]
22
+ end
23
+
24
+ let(:scopes_mask_methods) do
25
+ [:facets,
26
+ :search,
27
+ :search_for_ids]
28
+ end
29
+
30
+ let(:group_enumerator_mask_methods) do
31
+ [:each_with_count,
32
+ :each_with_group,
33
+ :each_with_group_and_count]
34
+ end
35
+
8
36
  before :each do
9
37
  ThinkingSphinx::Search::Context.stub :new => context
10
38
 
@@ -141,6 +169,24 @@ describe ThinkingSphinx::Search do
141
169
  it "should respond to Search methods" do
142
170
  search.respond_to?(:per_page).should be_true
143
171
  end
172
+
173
+ it "should return true for methods delegated to pagination mask by method_missing" do
174
+ pagination_mask_methods.each do |method|
175
+ expect(search).to respond_to method
176
+ end
177
+ end
178
+
179
+ it "should return true for methods delegated to scopes mask by method_missing" do
180
+ scopes_mask_methods.each do |method|
181
+ expect(search).to respond_to method
182
+ end
183
+ end
184
+
185
+ it "should return true for methods delegated to group enumerators mask by method_missing" do
186
+ group_enumerator_mask_methods.each do |method|
187
+ expect(search).to respond_to method
188
+ end
189
+ end
144
190
  end
145
191
 
146
192
  describe '#to_a' do
@@ -153,4 +199,14 @@ describe ThinkingSphinx::Search do
153
199
  search.to_a.first.__id__.should == unglazed.__id__
154
200
  end
155
201
  end
202
+
203
+ it "correctly handles access to methods delegated to masks through 'method' call" do
204
+ [
205
+ pagination_mask_methods,
206
+ scopes_mask_methods,
207
+ group_enumerator_mask_methods
208
+ ].flatten.each do |method|
209
+ expect { search.method method }.to_not raise_exception
210
+ end
211
+ end
156
212
  end