sunspot 2.5.0 → 2.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/lib/sunspot/adapters.rb +14 -3
  3. data/lib/sunspot/data_extractor.rb +1 -1
  4. data/lib/sunspot/dsl/fulltext.rb +1 -1
  5. data/lib/sunspot/dsl/standard_query.rb +29 -1
  6. data/lib/sunspot/dsl.rb +2 -2
  7. data/lib/sunspot/field.rb +15 -4
  8. data/lib/sunspot/indexer.rb +37 -8
  9. data/lib/sunspot/query/abstract_fulltext.rb +7 -3
  10. data/lib/sunspot/query/abstract_json_field_facet.rb +3 -0
  11. data/lib/sunspot/query/composite_fulltext.rb +21 -2
  12. data/lib/sunspot/query/date_field_json_facet.rb +2 -16
  13. data/lib/sunspot/query/dismax.rb +10 -4
  14. data/lib/sunspot/query/function_query.rb +25 -1
  15. data/lib/sunspot/query/join.rb +1 -1
  16. data/lib/sunspot/query/range_json_facet.rb +5 -2
  17. data/lib/sunspot/query/restriction.rb +10 -9
  18. data/lib/sunspot/query/standard_query.rb +12 -0
  19. data/lib/sunspot/search/field_json_facet.rb +14 -3
  20. data/lib/sunspot/session.rb +6 -4
  21. data/lib/sunspot/setup.rb +38 -0
  22. data/lib/sunspot/util.rb +4 -11
  23. data/lib/sunspot/version.rb +1 -1
  24. data/lib/sunspot.rb +9 -1
  25. data/spec/api/indexer/removal_spec.rb +87 -0
  26. data/spec/api/query/connective_boost_examples.rb +85 -0
  27. data/spec/api/query/join_spec.rb +2 -2
  28. data/spec/api/query/standard_spec.rb +10 -0
  29. data/spec/api/setup_spec.rb +99 -0
  30. data/spec/helpers/indexer_helper.rb +22 -0
  31. data/spec/integration/atomic_updates_spec.rb +169 -5
  32. data/spec/integration/faceting_spec.rb +68 -34
  33. data/spec/integration/join_spec.rb +22 -3
  34. data/spec/integration/scoped_search_spec.rb +78 -0
  35. data/spec/mocks/connection.rb +6 -0
  36. data/spec/mocks/photo.rb +11 -7
  37. data/spec/mocks/post.rb +35 -1
  38. data/sunspot.gemspec +0 -2
  39. metadata +10 -6
data/lib/sunspot.rb CHANGED
@@ -40,6 +40,12 @@ module Sunspot
40
40
  NoSetupError = Class.new(StandardError)
41
41
  IllegalSearchError = Class.new(StandardError)
42
42
  NotImplementedError = Class.new(StandardError)
43
+ AtomicUpdateRequireInstanceForCompositeIdMessage = lambda do |class_name|
44
+ "WARNING: `id_prefix` is defined for #{class_name}. Use instance as key for `atomic_update` instead of ID."
45
+ end
46
+ RemoveByIdNotSupportCompositeIdMessage = lambda do |class_name|
47
+ "WARNING: `id_prefix` is defined for #{class_name}. `remove_by_id` does not support it. Use `remove` instead."
48
+ end
43
49
 
44
50
  autoload :Installer, File.join(File.dirname(__FILE__), 'sunspot', 'installer')
45
51
 
@@ -208,6 +214,8 @@ module Sunspot
208
214
  #
209
215
  # post1, post2 = new Array(2) { Post.create }
210
216
  # Sunspot.atomic_update(Post, post1.id => {title: 'New Title'}, post2.id => {description: 'new description'})
217
+ # Or
218
+ # Sunspot.atomic_update(Post, post1 => {title: 'New Title'}, post2 => {description: 'new description'})
211
219
  #
212
220
  # Note that indexed objects won't be reflected in search until a commit is
213
221
  # sent - see Sunspot.index! and Sunspot.commit
@@ -223,7 +231,7 @@ module Sunspot
223
231
  # ==== Parameters
224
232
  #
225
233
  # clazz<Class>:: the class of the objects to be updated
226
- # updates<Hash>:: hash of updates where keys are model ids
234
+ # updates<Hash>:: hash of updates where keys are models or model ids
227
235
  # and values are hash with property name/values to be updated
228
236
  #
229
237
  def atomic_update!(clazz, updates = {})
@@ -60,4 +60,91 @@ describe 'document removal', :type => :indexer do
60
60
  end
61
61
  expect(connection).to have_delete_by_query("(type:Post AND title_ss:monkeys)")
62
62
  end
63
+
64
+ context 'when call #remove_by_id' do
65
+ let(:post) { clazz.new(title: 'A Title') }
66
+ before(:each) { index_post(post) }
67
+
68
+ context 'and `id_prefix` is defined on model' do
69
+ context 'as Proc' do
70
+ let(:clazz) { PostWithProcPrefixId }
71
+ let(:id_prefix) { lambda { |post| "USERDATA-#{post.id}!" } }
72
+
73
+ it 'prints warning' do
74
+ expect do
75
+ session.remove_by_id(clazz, post.id)
76
+ end.to output(Sunspot::RemoveByIdNotSupportCompositeIdMessage.call(clazz) + "\n").to_stderr
77
+ end
78
+
79
+ it 'does not remove record' do
80
+ session.remove_by_id(clazz, post.id)
81
+ expect(connection).to have_no_delete(post_solr_id)
82
+ end
83
+ end
84
+
85
+ context 'as Symbol' do
86
+ let(:clazz) { PostWithSymbolPrefixId }
87
+ let(:id_prefix) { lambda { |post| "#{post.title}!" } }
88
+
89
+ it 'prints warning' do
90
+ expect do
91
+ session.remove_by_id(clazz, post.id)
92
+ end.to output(Sunspot::RemoveByIdNotSupportCompositeIdMessage.call(clazz) + "\n").to_stderr
93
+ end
94
+
95
+ it 'does not remove record' do
96
+ session.remove_by_id(clazz, post.id)
97
+ expect(connection).to have_no_delete(post_solr_id)
98
+ end
99
+ end
100
+
101
+ context 'as String' do
102
+ let(:clazz) { PostWithStringPrefixId }
103
+ let(:id_prefix) { 'USERDATA!' }
104
+
105
+ it 'does not print warning' do
106
+ expect do
107
+ session.remove_by_id(clazz, post.id)
108
+ end.to_not output(Sunspot::RemoveByIdNotSupportCompositeIdMessage.call(clazz) + "\n").to_stderr
109
+ end
110
+
111
+ it 'removes record' do
112
+ session.remove_by_id(clazz, post.id)
113
+ expect(connection).to have_delete(post_solr_id)
114
+ end
115
+ end
116
+ end
117
+
118
+ context 'and `id_prefix` is not defined on model' do
119
+ let(:clazz) { PostWithoutPrefixId }
120
+ let(:id_prefix) { nil }
121
+
122
+ it 'does not print warning' do
123
+ expect do
124
+ session.remove_by_id(clazz, post.id)
125
+ end.to_not output(Sunspot::RemoveByIdNotSupportCompositeIdMessage.call(clazz) + "\n").to_stderr
126
+ end
127
+
128
+ it 'removes record' do
129
+ session.remove_by_id(clazz, post.id)
130
+ expect(connection).to have_delete(post_solr_id)
131
+ end
132
+ end
133
+
134
+ context 'and `id_prefix` is passed along with `class_name`' do
135
+ let(:clazz) { PostWithProcPrefixId }
136
+ let(:id_prefix) { lambda { |post| "USERDATA-#{post.id}!" } }
137
+
138
+ it 'does not print warning' do
139
+ expect do
140
+ session.remove_by_id("USERDATA-#{post.id}!#{clazz.name}", post.id)
141
+ end.to_not output(Sunspot::RemoveByIdNotSupportCompositeIdMessage.call(clazz) + "\n").to_stderr
142
+ end
143
+
144
+ it 'removes record' do
145
+ session.remove_by_id("USERDATA-#{post.id}!#{clazz.name}", post.id)
146
+ expect(connection).to have_delete(post_solr_id)
147
+ end
148
+ end
149
+ end
63
150
  end
@@ -0,0 +1,85 @@
1
+ shared_examples_for "query with connective scope and boost" do
2
+ it 'creates a boost query' do
3
+ search do
4
+ boost(10) do
5
+ any_of do
6
+ with(:coordinates_new).in_bounding_box([23, -46], [25, -44])
7
+ with(:coordinates_new).in_bounding_box([42, 56], [43, 58])
8
+ end
9
+ end
10
+ end
11
+
12
+ expect(connection).to have_last_search_including(
13
+ :bq, '(coordinates_new_ll:[23,-46 TO 25,-44] OR coordinates_new_ll:[42,56 TO 43,58])^10'
14
+ )
15
+
16
+ expect(connection).to have_last_search_including(
17
+ :defType, 'edismax'
18
+ )
19
+ end
20
+
21
+ it 'creates a boost function' do
22
+ search do
23
+ boost(function() { field(:average_rating) })
24
+ end
25
+
26
+ expect(connection).to have_last_search_including(
27
+ :bf, 'field(average_rating_ft)'
28
+ )
29
+
30
+ expect(connection).to have_last_search_including(
31
+ :defType, 'edismax'
32
+ )
33
+ end
34
+
35
+ it 'creates a multiplicative boost function' do
36
+ search do
37
+ boost_multiplicative(function() { field(:average_rating) })
38
+ end
39
+
40
+ expect(connection).to have_last_search_including(
41
+ :boost, 'field(average_rating_ft)'
42
+ )
43
+
44
+ expect(connection).to have_last_search_including(
45
+ :defType, 'edismax'
46
+ )
47
+ end
48
+
49
+ it 'creates combined boost search' do
50
+ search do
51
+ boost(10) do
52
+ any_of do
53
+ with(:coordinates_new).in_bounding_box([23, -46], [25, -44])
54
+ with(:coordinates_new).in_bounding_box([42, 56], [43, 58])
55
+ end
56
+ end
57
+
58
+ boost(function() { field(:average_rating) })
59
+ boost_multiplicative(function() { field(:average_rating) })
60
+ end
61
+
62
+ expect(connection).to have_last_search_including(
63
+ :bq, '(coordinates_new_ll:[23,-46 TO 25,-44] OR coordinates_new_ll:[42,56 TO 43,58])^10'
64
+ )
65
+
66
+ expect(connection).to have_last_search_including(
67
+ :bf, 'field(average_rating_ft)'
68
+ )
69
+
70
+ expect(connection).to have_last_search_including(
71
+ :boost, 'field(average_rating_ft)'
72
+ )
73
+ end
74
+
75
+ it 'avoids duplicate boost functions' do
76
+ search do
77
+ boost(function() { field(:average_rating) })
78
+ boost(function() { field(:average_rating) })
79
+ boost_multiplicative(function() { field(:average_rating) })
80
+ end
81
+
82
+ expect(connection.searches.last[:bf]).to eq ['field(average_rating_ft)']
83
+ expect(connection.searches.last[:boost]).to eq ['field(average_rating_ft)']
84
+ end
85
+ end
@@ -6,7 +6,7 @@ describe 'join' do
6
6
  with(:caption, 'blah')
7
7
  end
8
8
  expect(connection).to have_last_search_including(
9
- :fq, "{!join from=photo_container_id_i to=id_i}caption_s:blah")
9
+ :fq, "{!join from=photo_container_id_i to=id_i v='type:\"Photo\" AND caption_s:\"blah\"'}")
10
10
  end
11
11
 
12
12
  it 'should greater_than search by join' do
@@ -14,6 +14,6 @@ describe 'join' do
14
14
  with(:photo_rating).greater_than(3)
15
15
  end
16
16
  expect(connection).to have_last_search_including(
17
- :fq, "{!join from=photo_container_id_i to=id_i}average_rating_ft:{3\\.0 TO *}")
17
+ :fq, "{!join from=photo_container_id_i to=id_i v='type:\"Photo\" AND average_rating_ft:{3\\.0 TO *}'}")
18
18
  end
19
19
  end
@@ -4,6 +4,7 @@ describe 'standard query', :type => :query do
4
4
  it_should_behave_like "scoped query"
5
5
  it_should_behave_like "query with advanced manipulation"
6
6
  it_should_behave_like "query with connective scope"
7
+ it_should_behave_like "query with connective scope and boost"
7
8
  it_should_behave_like "query with dynamic field support"
8
9
  it_should_behave_like "facetable query"
9
10
  it_should_behave_like "fulltext query"
@@ -22,6 +23,15 @@ describe 'standard query', :type => :query do
22
23
  expect(connection).to have_last_search_with(:q => '*:*')
23
24
  end
24
25
 
26
+ it 'adds a no-op query to :q parameter when only a boost query provided' do
27
+ session.search Post do
28
+ boost(2) do
29
+ with :title, 'My Pet Post'
30
+ end
31
+ end
32
+ expect(connection).to have_last_search_with(:q => '*:*')
33
+ end
34
+
25
35
  private
26
36
 
27
37
  def search(*classes, &block)
@@ -0,0 +1,99 @@
1
+ require File.expand_path('spec_helper', File.dirname(__FILE__))
2
+
3
+ describe Sunspot::Setup do
4
+ context '#id_prefix_for_class' do
5
+ subject { Sunspot::Setup.for(clazz).id_prefix_for_class }
6
+
7
+ context 'when `id_prefix` is defined on model' do
8
+ context 'as Proc' do
9
+ let(:clazz) { PostWithProcPrefixId }
10
+
11
+ it 'returns nil' do
12
+ is_expected.to be_nil
13
+ end
14
+ end
15
+
16
+ context 'as Symbol' do
17
+ let(:clazz) { PostWithSymbolPrefixId }
18
+
19
+ it 'returns nil' do
20
+ is_expected.to be_nil
21
+ end
22
+ end
23
+
24
+ context 'as String' do
25
+ let(:clazz) { PostWithStringPrefixId }
26
+
27
+ it 'returns `id_prefix` value' do
28
+ is_expected.to eq('USERDATA!')
29
+ end
30
+ end
31
+ end
32
+
33
+ context 'when `id_prefix` is not defined on model' do
34
+ let(:clazz) { PostWithoutPrefixId }
35
+
36
+ it 'returns nil' do
37
+ is_expected.to be_nil
38
+ end
39
+ end
40
+ end
41
+
42
+ context '#id_prefix_defined?' do
43
+ subject { Sunspot::Setup.for(clazz).id_prefix_defined? }
44
+
45
+ context 'when `id_prefix` is defined on model' do
46
+ let(:clazz) { PostWithProcPrefixId }
47
+
48
+ it 'returns true' do
49
+ is_expected.to be_truthy
50
+ end
51
+ end
52
+
53
+ context 'when `id_prefix` is not defined on model' do
54
+ let(:clazz) { PostWithoutPrefixId }
55
+
56
+ it 'returns false' do
57
+ is_expected.to be_falsey
58
+ end
59
+ end
60
+ end
61
+
62
+ context '#id_prefix_requires_instance?' do
63
+ subject { Sunspot::Setup.for(clazz).id_prefix_requires_instance? }
64
+
65
+ context 'when `id_prefix` is defined on model' do
66
+ context 'as Proc' do
67
+ let(:clazz) { PostWithProcPrefixId }
68
+
69
+ it 'returns true' do
70
+ is_expected.to be_truthy
71
+ end
72
+ end
73
+
74
+ context 'as Symbol' do
75
+ let(:clazz) { PostWithSymbolPrefixId }
76
+
77
+ it 'returns true' do
78
+ is_expected.to be_truthy
79
+ end
80
+ end
81
+
82
+ context 'as String' do
83
+ let(:clazz) { PostWithStringPrefixId }
84
+
85
+ it 'returns false' do
86
+ is_expected.to be_falsey
87
+ end
88
+ end
89
+ end
90
+
91
+ context 'when `id_prefix` is not defined on model' do
92
+ let(:clazz) { PostWithoutPrefixId }
93
+
94
+ it 'returns false' do
95
+ is_expected.to be_falsey
96
+ end
97
+ end
98
+ end
99
+ end
@@ -14,4 +14,26 @@ module IndexerHelper
14
14
  def values_in_last_document_for(field_name)
15
15
  @connection.adds.last.last.fields_by_name(field_name).map { |field| field.value }
16
16
  end
17
+
18
+ def index_post(post)
19
+ Sunspot.index!(post)
20
+ hit = find_post(post)
21
+ expect(hit).not_to be_nil
22
+ hit
23
+ end
24
+
25
+ def find_post(post)
26
+ Sunspot.search(clazz).hits.find { |h| h.primary_key.to_i == post.id && h.id_prefix == id_prefix_value(post, id_prefix) }
27
+ end
28
+
29
+ def id_prefix_value(post, id_prefix)
30
+ return unless id_prefix
31
+ return id_prefix if id_prefix.is_a?(String)
32
+
33
+ id_prefix.call(post)
34
+ end
35
+
36
+ def post_solr_id
37
+ "#{id_prefix_value(post, id_prefix)}#{clazz} #{post.id}"
38
+ end
17
39
  end
@@ -1,6 +1,79 @@
1
1
  require File.expand_path('../spec_helper', File.dirname(__FILE__))
2
2
 
3
- describe 'atomic updates' do
3
+ shared_examples 'atomic update with instance as key' do
4
+ it 'updates record' do
5
+ post = clazz.new(title: 'A Title', featured: true)
6
+ Sunspot.index!(post)
7
+
8
+ validate_hit(find_and_validate_indexed_post_with_prefix_id(post, id_prefix), title: post.title, featured: post.featured)
9
+
10
+ Sunspot.atomic_update!(clazz, post => { title: 'A New Title' })
11
+ validate_hit(find_and_validate_indexed_post_with_prefix_id(post, id_prefix), title: 'A New Title', featured: true)
12
+
13
+ Sunspot.atomic_update!(clazz, post => { featured: false })
14
+ validate_hit(find_and_validate_indexed_post_with_prefix_id(post, id_prefix), title: 'A New Title', featured: false)
15
+ end
16
+
17
+ it 'does not print warning' do
18
+ post = clazz.new(title: 'A Title', featured: true)
19
+ Sunspot.index!(post)
20
+
21
+ validate_hit(find_and_validate_indexed_post_with_prefix_id(post, id_prefix), title: post.title, featured: post.featured)
22
+
23
+ expect do
24
+ Sunspot.atomic_update!(clazz, post => { title: 'A New Title' })
25
+ end.to_not output(Sunspot::AtomicUpdateRequireInstanceForCompositeIdMessage.call(clazz)).to_stderr
26
+ end
27
+
28
+ it 'does not create duplicate document' do
29
+ post = clazz.new(title: 'A Title', featured: true)
30
+ Sunspot.index!(post)
31
+
32
+ validate_hit(find_and_validate_indexed_post_with_prefix_id(post, id_prefix), title: post.title, featured: post.featured)
33
+
34
+ Sunspot.atomic_update!(clazz, post => { title: 'A New Title' })
35
+ hit = find_indexed_post_with_prefix_id(post, nil)
36
+ expect(hit).to be_nil
37
+ end
38
+ end
39
+
40
+ shared_examples 'atomic update with id as key' do
41
+ it 'does not update record' do
42
+ post = clazz.new(title: 'A Title', featured: true)
43
+ Sunspot.index!(post)
44
+
45
+ validate_hit(find_and_validate_indexed_post_with_prefix_id(post, id_prefix), title: post.title, featured: post.featured)
46
+
47
+ Sunspot.atomic_update!(clazz, post.id => { title: 'A New Title' })
48
+ validate_hit(find_and_validate_indexed_post_with_prefix_id(post, id_prefix), title: 'A Title', featured: true)
49
+
50
+ Sunspot.atomic_update!(clazz, post.id => { featured: false })
51
+ validate_hit(find_and_validate_indexed_post_with_prefix_id(post, id_prefix), title: 'A Title', featured: true)
52
+ end
53
+
54
+ it 'prints warning' do
55
+ post = clazz.new(title: 'A Title', featured: true)
56
+ Sunspot.index!(post)
57
+
58
+ validate_hit(find_and_validate_indexed_post_with_prefix_id(post, id_prefix), title: post.title, featured: post.featured)
59
+
60
+ expect do
61
+ Sunspot.atomic_update!(clazz, post.id => { title: 'A New Title' })
62
+ end.to output(Sunspot::AtomicUpdateRequireInstanceForCompositeIdMessage.call(clazz) + "\n").to_stderr
63
+ end
64
+
65
+ it 'creates duplicate document that have only fields provided for update' do
66
+ post = clazz.new(title: 'A Title', featured: true)
67
+ Sunspot.index!(post)
68
+
69
+ validate_hit(find_and_validate_indexed_post_with_prefix_id(post, id_prefix), title: post.title, featured: post.featured)
70
+
71
+ Sunspot.atomic_update!(clazz, post.id => { title: 'A New Title' })
72
+ validate_hit(find_and_validate_indexed_post_with_prefix_id(post, nil), title: 'A New Title', featured: nil)
73
+ end
74
+ end
75
+
76
+ describe 'Atomic Update feature' do
4
77
  before :all do
5
78
  Sunspot.remove_all
6
79
  end
@@ -18,10 +91,27 @@ describe 'atomic updates' do
18
91
  hit
19
92
  end
20
93
 
21
- it 'should update single record fields one by one' do
94
+ def find_and_validate_indexed_post_with_prefix_id(post, id_prefix)
95
+ hit = find_indexed_post_with_prefix_id(post, id_prefix_value(post, id_prefix))
96
+ expect(hit).not_to be_nil
97
+ hit
98
+ end
99
+
100
+ def find_indexed_post_with_prefix_id(post, id_prefix)
101
+ Sunspot.search(post.class).hits.find { |h| h.primary_key.to_i == post.id && h.id_prefix == id_prefix }
102
+ end
103
+
104
+ def id_prefix_value(post, id_prefix)
105
+ return unless id_prefix
106
+ return id_prefix if id_prefix.is_a?(String)
107
+
108
+ id_prefix.call(post)
109
+ end
110
+
111
+ it 'updates single record fields one by one' do
22
112
  post = Post.new(title: 'A Title', featured: true)
23
113
  Sunspot.index!(post)
24
-
114
+
25
115
  validate_hit(find_indexed_post(post.id), title: post.title, featured: post.featured)
26
116
 
27
117
  Sunspot.atomic_update!(Post, post.id => {title: 'A New Title'})
@@ -31,7 +121,7 @@ describe 'atomic updates' do
31
121
  validate_hit(find_indexed_post(post.id), title: 'A New Title', featured: false)
32
122
  end
33
123
 
34
- it 'should update fields for multiple records' do
124
+ it 'updates fields for multiple records' do
35
125
  post1 = Post.new(title: 'A First Title', featured: true)
36
126
  post2 = Post.new(title: 'A Second Title', featured: false)
37
127
  Sunspot.index!(post1, post2)
@@ -44,7 +134,7 @@ describe 'atomic updates' do
44
134
  validate_hit(find_indexed_post(post2.id), title: 'A Second Title', featured: true)
45
135
  end
46
136
 
47
- it 'should clear field value properly' do
137
+ it 'clears field value properly' do
48
138
  post = Post.new(title: 'A Title', tags: %w(tag1 tag2), featured: true)
49
139
  Sunspot.index!(post)
50
140
  validate_hit(find_indexed_post(post.id), title: post.title, tag_list: post.tags, featured: true)
@@ -55,4 +145,78 @@ describe 'atomic updates' do
55
145
  Sunspot.atomic_update!(Post, post.id => {featured: nil})
56
146
  validate_hit(find_indexed_post(post.id), title: post.title, tag_list: nil, featured: nil)
57
147
  end
148
+
149
+ context 'when `id_prefix` is defined on model' do
150
+ context 'as Proc' do
151
+ let(:clazz) { PostWithProcPrefixId }
152
+ let(:id_prefix) { lambda { |post| "USERDATA-#{post.id}!" } }
153
+
154
+ context 'and instance passed as key' do
155
+ include_examples 'atomic update with instance as key'
156
+ end
157
+
158
+ context 'and id passed as key' do
159
+ include_examples 'atomic update with id as key'
160
+ end
161
+ end
162
+
163
+ context 'as Symbol' do
164
+ let(:clazz) { PostWithSymbolPrefixId }
165
+ let(:id_prefix) { lambda { |post| "#{post.title}!" } }
166
+
167
+ context 'and instance passed as key' do
168
+ include_examples 'atomic update with instance as key'
169
+ end
170
+
171
+ context 'and id passed as key' do
172
+ include_examples 'atomic update with id as key'
173
+ end
174
+ end
175
+
176
+ context 'as String' do
177
+ let(:clazz) { PostWithStringPrefixId }
178
+ let(:id_prefix) { 'USERDATA!' }
179
+
180
+ context 'and instance passed as key' do
181
+ include_examples 'atomic update with instance as key'
182
+ end
183
+
184
+ context 'and id passed as key' do
185
+ it 'updates record' do
186
+ post = clazz.new(title: 'A Title', featured: true)
187
+ Sunspot.index!(post)
188
+
189
+ validate_hit(find_and_validate_indexed_post_with_prefix_id(post, id_prefix), title: post.title, featured: post.featured)
190
+
191
+ Sunspot.atomic_update!(clazz, post.id => { title: 'A New Title' })
192
+ validate_hit(find_and_validate_indexed_post_with_prefix_id(post, id_prefix), title: 'A New Title', featured: true)
193
+
194
+ Sunspot.atomic_update!(clazz, post.id => { featured: false })
195
+ validate_hit(find_and_validate_indexed_post_with_prefix_id(post, id_prefix), title: 'A New Title', featured: false)
196
+ end
197
+
198
+ it 'does not print warning' do
199
+ post = clazz.new(title: 'A Title', featured: true)
200
+ Sunspot.index!(post)
201
+
202
+ validate_hit(find_and_validate_indexed_post_with_prefix_id(post, id_prefix), title: post.title, featured: post.featured)
203
+
204
+ expect do
205
+ Sunspot.atomic_update!(clazz, post.id => { title: 'A New Title' })
206
+ end.to_not output(Sunspot::AtomicUpdateRequireInstanceForCompositeIdMessage.call(clazz) + "\n").to_stderr
207
+ end
208
+
209
+ it 'does not create duplicate document' do
210
+ post = clazz.new(title: 'A Title', featured: true)
211
+ Sunspot.index!(post)
212
+
213
+ validate_hit(find_and_validate_indexed_post_with_prefix_id(post, id_prefix), title: post.title, featured: post.featured)
214
+
215
+ Sunspot.atomic_update!(clazz, post.id => { title: 'A New Title' })
216
+ hit = find_indexed_post_with_prefix_id(post, nil)
217
+ expect(hit).to be_nil
218
+ end
219
+ end
220
+ end
221
+ end
58
222
  end