sunspot 2.5.0 → 2.6.0

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