thinking-sphinx 3.0.1 → 3.0.2

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