thinking-sphinx 3.0.1 → 3.0.2

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 (45) hide show
  1. checksums.yaml +7 -0
  2. data/.travis.yml +5 -2
  3. data/Appraisals +4 -0
  4. data/Gemfile +1 -1
  5. data/HISTORY +18 -0
  6. data/README.textile +7 -7
  7. data/gemfiles/rails_3_1.gemfile +1 -1
  8. data/gemfiles/rails_3_2.gemfile +1 -1
  9. data/gemfiles/rails_4_0.gemfile +11 -0
  10. data/lib/thinking_sphinx.rb +1 -0
  11. data/lib/thinking_sphinx/active_record/associations.rb +8 -1
  12. data/lib/thinking_sphinx/active_record/database_adapters/abstract_adapter.rb +4 -0
  13. data/lib/thinking_sphinx/active_record/database_adapters/mysql_adapter.rb +4 -0
  14. data/lib/thinking_sphinx/active_record/filtered_reflection.rb +32 -14
  15. data/lib/thinking_sphinx/active_record/sql_builder.rb +4 -4
  16. data/lib/thinking_sphinx/active_record/sql_source.rb +3 -1
  17. data/lib/thinking_sphinx/active_record/sql_source/template.rb +5 -1
  18. data/lib/thinking_sphinx/capistrano.rb +6 -7
  19. data/lib/thinking_sphinx/configuration.rb +56 -33
  20. data/lib/thinking_sphinx/core/index.rb +1 -1
  21. data/lib/thinking_sphinx/errors.rb +2 -0
  22. data/lib/thinking_sphinx/masks/group_enumerators_mask.rb +4 -0
  23. data/lib/thinking_sphinx/masks/pagination_mask.rb +4 -0
  24. data/lib/thinking_sphinx/masks/scopes_mask.rb +2 -2
  25. data/lib/thinking_sphinx/masks/weight_enumerator_mask.rb +4 -0
  26. data/lib/thinking_sphinx/middlewares/active_record_translator.rb +4 -3
  27. data/lib/thinking_sphinx/middlewares/sphinxql.rb +15 -3
  28. data/lib/thinking_sphinx/railtie.rb +13 -0
  29. data/lib/thinking_sphinx/search.rb +3 -2
  30. data/lib/thinking_sphinx/search/query.rb +4 -0
  31. data/spec/acceptance/specifying_sql_spec.rb +4 -4
  32. data/spec/internal/app/models/tweet.rb +1 -1
  33. data/spec/thinking_sphinx/active_record/associations_spec.rb +49 -4
  34. data/spec/thinking_sphinx/active_record/sql_builder_spec.rb +3 -3
  35. data/spec/thinking_sphinx/active_record/sql_source_spec.rb +14 -1
  36. data/spec/thinking_sphinx/configuration_spec.rb +25 -1
  37. data/spec/thinking_sphinx/connection_spec.rb +4 -4
  38. data/spec/thinking_sphinx/errors_spec.rb +7 -0
  39. data/spec/thinking_sphinx/middlewares/active_record_translator_spec.rb +3 -3
  40. data/spec/thinking_sphinx/middlewares/sphinxql_spec.rb +35 -4
  41. data/spec/thinking_sphinx/search/query_spec.rb +20 -0
  42. data/spec/thinking_sphinx/search_spec.rb +5 -0
  43. data/thinking-sphinx.gemspec +3 -3
  44. metadata +22 -48
  45. data/lib/thinking_sphinx/search/translator.rb +0 -50
@@ -2,7 +2,7 @@ module ThinkingSphinx::Core::Index
2
2
  extend ActiveSupport::Concern
3
3
 
4
4
  included do
5
- attr_reader :reference, :offset
5
+ attr_reader :reference, :offset, :options
6
6
  attr_writer :definition_block
7
7
  end
8
8
 
@@ -5,6 +5,8 @@ class ThinkingSphinx::SphinxError < StandardError
5
5
  replacement = ThinkingSphinx::ParseError.new(error.message)
6
6
  when /syntax error/
7
7
  replacement = ThinkingSphinx::SyntaxError.new(error.message)
8
+ when /query error/
9
+ replacement = ThinkingSphinx::QueryError.new(error.message)
8
10
  else
9
11
  replacement = new(error.message)
10
12
  end
@@ -3,6 +3,10 @@ class ThinkingSphinx::Masks::GroupEnumeratorsMask
3
3
  @search = search
4
4
  end
5
5
 
6
+ def can_handle?(method)
7
+ public_methods(false).include?(method)
8
+ end
9
+
6
10
  def each_with_count(&block)
7
11
  @search.raw.each_with_index do |row, index|
8
12
  yield @search[index], row['@count']
@@ -3,6 +3,10 @@ class ThinkingSphinx::Masks::PaginationMask
3
3
  @search = search
4
4
  end
5
5
 
6
+ def can_handle?(method)
7
+ public_methods(false).include?(method)
8
+ end
9
+
6
10
  def current_page
7
11
  search.options[:page] = 1 if search.options[:page].blank?
8
12
  search.options[:page].to_i
@@ -3,8 +3,8 @@ class ThinkingSphinx::Masks::ScopesMask
3
3
  @search = search
4
4
  end
5
5
 
6
- def respond_to?(method, include_private = false)
7
- super || can_apply_scope?(method)
6
+ def can_handle?(method)
7
+ public_methods(false).include?(method) || can_apply_scope?(method)
8
8
  end
9
9
 
10
10
  def search(query = nil, options = {})
@@ -3,6 +3,10 @@ class ThinkingSphinx::Masks::WeightEnumeratorMask
3
3
  @search = search
4
4
  end
5
5
 
6
+ def can_handle?(method)
7
+ public_methods(false).include?(method)
8
+ end
9
+
6
10
  def each_with_weight(&block)
7
11
  @search.raw.each_with_index do |row, index|
8
12
  yield @search[index], row['@weight']
@@ -52,15 +52,16 @@ class ThinkingSphinx::Middlewares::ActiveRecordTranslator <
52
52
 
53
53
  def results_for_models
54
54
  @results_for_models ||= model_names.inject({}) { |hash, name|
55
- ids = ids_for_model(name)
56
- relation = name.constantize.unscoped
55
+ ids = ids_for_model(name)
56
+ model = name.constantize
57
+ relation = model.unscoped
57
58
 
58
59
  relation = relation.includes sql_options[:include] if sql_options[:include]
59
60
  relation = relation.joins sql_options[:joins] if sql_options[:joins]
60
61
  relation = relation.order sql_options[:order] if sql_options[:order]
61
62
  relation = relation.select sql_options[:select] if sql_options[:select]
62
63
 
63
- hash[name] = relation.where(:id => ids)
64
+ hash[name] = relation.where(model.primary_key => ids)
64
65
 
65
66
  hash
66
67
  }
@@ -1,7 +1,9 @@
1
1
  class ThinkingSphinx::Middlewares::SphinxQL <
2
2
  ThinkingSphinx::Middlewares::Middleware
3
3
 
4
- SELECT_OPTIONS = [:field_weights, :ranker]
4
+ SELECT_OPTIONS = [:ranker, :max_matches, :cutoff, :max_query_time,
5
+ :retry_count, :retry_delay, :field_weights, :index_weights, :reverse_scan,
6
+ :comment]
5
7
 
6
8
  def call(contexts)
7
9
  contexts.each do |context|
@@ -102,6 +104,10 @@ class ThinkingSphinx::Middlewares::SphinxQL <
102
104
  indices.collect(&:name)
103
105
  end
104
106
 
107
+ def index_options
108
+ indices.first.options
109
+ end
110
+
105
111
  def indices
106
112
  @indices ||= ThinkingSphinx::IndexSet.new classes, options[:indices]
107
113
  end
@@ -120,12 +126,18 @@ class ThinkingSphinx::Middlewares::SphinxQL <
120
126
  end
121
127
 
122
128
  def select_options
123
- @select_options ||= options.keys.inject({}) do |hash, key|
124
- hash[key] = options[key] if SELECT_OPTIONS.include?(key)
129
+ @select_options ||= SELECT_OPTIONS.inject({}) do |hash, key|
130
+ hash[key] = settings[key.to_s] if settings.key? key.to_s
131
+ hash[key] = index_options[key] if index_options.key? key
132
+ hash[key] = options[key] if options.key? key
125
133
  hash
126
134
  end
127
135
  end
128
136
 
137
+ def settings
138
+ context.configuration.settings
139
+ end
140
+
129
141
  def statement
130
142
  Riddle::Query::Select.new.tap do |select|
131
143
  select.from *index_names.collect { |index| "`#{index}`" }
@@ -7,3 +7,16 @@ class ThinkingSphinx::Railtie < Rails::Railtie
7
7
  load File.expand_path('../tasks.rb', __FILE__)
8
8
  end
9
9
  end
10
+
11
+ # Add 'app/indices' path to Rails Engines
12
+ module ThinkingSphinx::EnginePaths
13
+ extend ActiveSupport::Concern
14
+
15
+ included do
16
+ initializer :add_indices_path do
17
+ paths.add "app/indices"
18
+ end
19
+ end
20
+ end
21
+
22
+ Rails::Engine.send :include, ThinkingSphinx::EnginePaths
@@ -45,7 +45,8 @@ class ThinkingSphinx::Search < Array
45
45
 
46
46
  alias_method :offset_value, :offset
47
47
 
48
- def per_page
48
+ def per_page(value = nil)
49
+ @options[:limit] = value unless value.nil?
49
50
  @options[:limit] ||= (@options[:per_page] || 20)
50
51
  @options[:limit].to_i
51
52
  end
@@ -93,7 +94,7 @@ class ThinkingSphinx::Search < Array
93
94
 
94
95
  def method_missing(method, *args, &block)
95
96
  mask_stack.each do |mask|
96
- return mask.send(method, *args, &block) if mask.respond_to?(method)
97
+ return mask.send(method, *args, &block) if mask.can_handle?(method)
97
98
  end
98
99
 
99
100
  populate if !SAFE_METHODS.include?(method.to_s)
@@ -1,3 +1,5 @@
1
+ # encoding: utf-8
2
+
1
3
  class ThinkingSphinx::Search::Query
2
4
  if Regexp.instance_methods.include?(:encoding)
3
5
  DefaultToken = Regexp.new('\p{Word}+')
@@ -13,6 +15,8 @@ class ThinkingSphinx::Search::Query
13
15
 
14
16
  def to_s
15
17
  (star_keyword(keywords) + ' ' + conditions.keys.collect { |key|
18
+ next if conditions[key].blank?
19
+
16
20
  "@#{key} #{star_keyword conditions[key], key}"
17
21
  }.join(' ')).strip
18
22
  end
@@ -355,7 +355,7 @@ describe 'separate queries for field' do
355
355
  declaration, query = field.split(/;\s+/)
356
356
 
357
357
  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. ORDER BY .taggings.\..article_id. ASC\s?$/)
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?$/)
359
359
  end
360
360
 
361
361
  it "respects has_many :through joins for MVF queries" do
@@ -368,7 +368,7 @@ describe 'separate queries for field' do
368
368
  declaration, query = field.split(/;\s+/)
369
369
 
370
370
  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. ORDER BY .taggings.\..article_id. ASC\s?$/)
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?$/)
372
372
  end
373
373
 
374
374
  it "can handle multiple joins for MVF queries" do
@@ -383,7 +383,7 @@ describe 'separate queries for field' do
383
383
  declaration, query = field.split(/;\s+/)
384
384
 
385
385
  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. ORDER BY .articles.\..user_id. ASC\s?$/)
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?$/)
387
387
  end
388
388
 
389
389
  it "generates a SQL query with joins when appropriate for MVFs" do
@@ -396,7 +396,7 @@ describe 'separate queries for field' do
396
396
  declaration, query, range = field.split(/;\s+/)
397
397
 
398
398
  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. >= \$start\) AND \(.taggings.\..article_id. <= \$end\) ORDER BY .taggings.\..article_id. ASC$/)
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. >= \$start\) AND \(.taggings.\..article_id. <= \$end\)\s? ORDER BY .taggings.\..article_id. ASC$/)
400
400
  range.should match(/^SELECT MIN\(.taggings.\..article_id.\), MAX\(.taggings.\..article_id.\) FROM .taggings.\s?$/)
401
401
  end
402
402
 
@@ -1,3 +1,3 @@
1
1
  class Tweet < ActiveRecord::Base
2
- #
2
+ self.primary_key = :id
3
3
  end
@@ -34,8 +34,11 @@ describe ThinkingSphinx::ActiveRecord::Associations do
34
34
  end
35
35
 
36
36
  describe '#add_join_to' do
37
- it "adds just one join for a stack with a single association" do
37
+ before :each do
38
38
  JoinDependency::JoinAssociation.unstub :new
39
+ end
40
+
41
+ it "adds just one join for a stack with a single association" do
39
42
  JoinDependency::JoinAssociation.should_receive(:new).
40
43
  with(join.reflection, base, join_base).once.and_return(join)
41
44
 
@@ -43,7 +46,6 @@ describe ThinkingSphinx::ActiveRecord::Associations do
43
46
  end
44
47
 
45
48
  it "does not duplicate joins when given the same stack twice" do
46
- JoinDependency::JoinAssociation.unstub :new
47
49
  JoinDependency::JoinAssociation.should_receive(:new).once.and_return(join)
48
50
 
49
51
  associations.add_join_to([:user])
@@ -52,7 +54,6 @@ describe ThinkingSphinx::ActiveRecord::Associations do
52
54
 
53
55
  context 'multiple joins' do
54
56
  it "adds two joins for a stack with two associations" do
55
- JoinDependency::JoinAssociation.unstub :new
56
57
  JoinDependency::JoinAssociation.should_receive(:new).
57
58
  with(join.reflection, base, join_base).once.and_return(join)
58
59
  JoinDependency::JoinAssociation.should_receive(:new).
@@ -62,7 +63,6 @@ describe ThinkingSphinx::ActiveRecord::Associations do
62
63
  end
63
64
 
64
65
  it "extends upon existing joins when given stacks where parts are already mapped" do
65
- JoinDependency::JoinAssociation.unstub :new
66
66
  JoinDependency::JoinAssociation.should_receive(:new).twice.
67
67
  and_return(join, sub_join)
68
68
 
@@ -70,6 +70,51 @@ describe ThinkingSphinx::ActiveRecord::Associations do
70
70
  associations.add_join_to([:user, :posts])
71
71
  end
72
72
  end
73
+
74
+ context 'join with conditions' do
75
+ let(:connection) { double }
76
+ let(:parent) { double :aliased_table_name => 'qux' }
77
+
78
+ before :each do
79
+ JoinDependency::JoinAssociation.stub :new => join
80
+
81
+ join.stub :parent => parent
82
+ model.stub :connection => connection
83
+ connection.stub(:quote_table_name) { |table| "\"#{table}\"" }
84
+ end
85
+
86
+ it "leaves standard conditions untouched" do
87
+ join.stub :conditions => 'foo = bar'
88
+
89
+ associations.add_join_to [:user]
90
+
91
+ join.conditions.should == 'foo = bar'
92
+ end
93
+
94
+ it "modifies filtered polymorphic conditions" do
95
+ join.stub :conditions => '::ts_join_alias::.foo = bar'
96
+
97
+ associations.add_join_to [:user]
98
+
99
+ join.conditions.should == '"qux".foo = bar'
100
+ end
101
+
102
+ it "modifies filtered polymorphic conditions within arrays" do
103
+ join.stub :conditions => ['::ts_join_alias::.foo = bar']
104
+
105
+ associations.add_join_to [:user]
106
+
107
+ join.conditions.should == ['"qux".foo = bar']
108
+ end
109
+
110
+ it "does not modify conditions as hashes" do
111
+ join.stub :conditions => [{:foo => 'bar'}]
112
+
113
+ associations.add_join_to [:user]
114
+
115
+ join.conditions.should == [{:foo => 'bar'}]
116
+ end
117
+ end
73
118
  end
74
119
 
75
120
  describe '#aggregate_for?' do
@@ -503,7 +503,7 @@ describe ThinkingSphinx::ActiveRecord::SQLBuilder do
503
503
 
504
504
  before :each do
505
505
  source.stub :options => {}, :delta_processor => nil, :delta? => false
506
- adapter.stub :utf8_query_pre => 'SET UTF8'
506
+ adapter.stub :utf8_query_pre => ['SET UTF8']
507
507
  end
508
508
 
509
509
  it "adds a reset delta query if there is a delta processor and this is the core source" do
@@ -598,11 +598,11 @@ describe ThinkingSphinx::ActiveRecord::SQLBuilder do
598
598
  builder.sql_query_range
599
599
  end
600
600
 
601
- it "adds source conditions" do
601
+ it "does not add source conditions" do
602
602
  source.conditions << 'created_at > NOW()'
603
603
 
604
604
  relation.should_receive(:where) do |string|
605
- string.should match(/created_at > NOW()/)
605
+ string.should_not match(/created_at > NOW()/)
606
606
  relation
607
607
  end
608
608
 
@@ -2,7 +2,8 @@ require 'spec_helper'
2
2
 
3
3
  describe ThinkingSphinx::ActiveRecord::SQLSource do
4
4
  let(:model) { double('model', :connection => connection,
5
- :name => 'User', :column_names => [], :inheritance_column => 'type') }
5
+ :name => 'User', :column_names => [], :inheritance_column => 'type',
6
+ :primary_key => :id) }
6
7
  let(:connection) {
7
8
  double('connection', :instance_variable_get => db_config) }
8
9
  let(:db_config) { {:host => 'localhost', :user => 'root',
@@ -132,6 +133,18 @@ describe ThinkingSphinx::ActiveRecord::SQLSource do
132
133
  end
133
134
  end
134
135
 
136
+ describe '#options' do
137
+ it "defaults to having utf8? set to false" do
138
+ source.options[:utf8?].should be_false
139
+ end
140
+
141
+ it "sets utf8? to true if the database encoding is utf8" do
142
+ db_config[:encoding] = 'utf8'
143
+
144
+ source.options[:utf8?].should be_true
145
+ end
146
+ end
147
+
135
148
  describe '#render' do
136
149
  let(:builder) { double('builder', :sql_query_pre => []).as_null_object }
137
150
  let(:config) { double('config', :settings => {}) }
@@ -28,6 +28,12 @@ describe ThinkingSphinx::Configuration do
28
28
  config.configuration_file.
29
29
  should == File.join(Rails.root, 'config', 'test.sphinx.conf')
30
30
  end
31
+
32
+ it "respects provided settings" do
33
+ write_configuration 'configuration_file' => '/path/to/foo.conf'
34
+
35
+ config.configuration_file.should == '/path/to/foo.conf'
36
+ end
31
37
  end
32
38
 
33
39
  describe '#controller' do
@@ -58,7 +64,19 @@ describe ThinkingSphinx::Configuration do
58
64
 
59
65
  describe '#index_paths' do
60
66
  it "uses app/indices in the Rails app by default" do
61
- config.index_paths.should == [File.join(Rails.root, 'app', 'indices')]
67
+ config.index_paths.should include(File.join(Rails.root, 'app', 'indices'))
68
+ end
69
+
70
+ it "uses app/indices in the Rails engines" do
71
+ engine =
72
+ stub(:engine, { :paths => { 'app/indices' =>
73
+ stub(:path, { :existent => '/engine/app/indices' } )
74
+ } } )
75
+
76
+ Rails::Engine::Railties.should_receive(:engines).
77
+ and_return([ engine ])
78
+
79
+ config.index_paths.should include('/engine/app/indices')
62
80
  end
63
81
  end
64
82
 
@@ -77,6 +95,12 @@ describe ThinkingSphinx::Configuration do
77
95
  config.indices_location.
78
96
  should == File.join(Rails.root, 'db', 'sphinx', 'test')
79
97
  end
98
+
99
+ it "respects provided settings" do
100
+ write_configuration 'indices_location' => '/my/index/files'
101
+
102
+ config.indices_location.should == '/my/index/files'
103
+ end
80
104
  end
81
105
 
82
106
  describe '#initialize' do
@@ -2,8 +2,8 @@ require 'spec_helper'
2
2
 
3
3
  describe ThinkingSphinx::Connection do
4
4
  describe '.take' do
5
- let(:pool) { double }
6
- let(:connection) { double }
5
+ let(:pool) { double 'pool' }
6
+ let(:connection) { double 'connection' }
7
7
  let(:error) { Mysql2::Error.new '' }
8
8
  let(:translated_error) { ThinkingSphinx::SphinxError.new }
9
9
 
@@ -49,7 +49,7 @@ describe ThinkingSphinx::Connection do
49
49
  tries += 1
50
50
  raise error if tries < 4
51
51
  end
52
- }.should raise_error(translated_error)
52
+ }.should raise_error(ThinkingSphinx::SphinxError)
53
53
  end
54
54
 
55
55
  [ThinkingSphinx::SyntaxError, ThinkingSphinx::ParseError].each do |klass|
@@ -59,7 +59,7 @@ describe ThinkingSphinx::Connection do
59
59
  it "raises the error" do
60
60
  lambda {
61
61
  ThinkingSphinx::Connection.take { |c| raise error }
62
- }.should raise_error(translated_error)
62
+ }.should raise_error(klass)
63
63
  end
64
64
 
65
65
  it "does not yield the connection more than once" do
@@ -19,6 +19,13 @@ describe ThinkingSphinx::SphinxError do
19
19
  should be_a(ThinkingSphinx::ParseError)
20
20
  end
21
21
 
22
+ it "translates query errors" do
23
+ error.stub :message => 'index foo: query error: something is wrong'
24
+
25
+ ThinkingSphinx::SphinxError.new_from_mysql(error).
26
+ should be_a(ThinkingSphinx::QueryError)
27
+ end
28
+
22
29
  it "defaults to sphinx errors" do
23
30
  error.stub :message => 'index foo: unknown error: something is wrong'
24
31