chewy 0.8.1 → 0.8.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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +10 -8
  3. data/Appraisals +8 -0
  4. data/CHANGELOG.md +33 -6
  5. data/Gemfile +6 -4
  6. data/README.md +240 -111
  7. data/gemfiles/rails.4.2.activerecord.gemfile +1 -0
  8. data/gemfiles/rails.4.2.activerecord.kaminari.gemfile +1 -0
  9. data/gemfiles/rails.4.2.activerecord.will_paginate.gemfile +1 -0
  10. data/gemfiles/sequel.4.23.gemfile +13 -0
  11. data/lib/chewy.rb +24 -11
  12. data/lib/chewy/config.rb +4 -4
  13. data/lib/chewy/index.rb +1 -1
  14. data/lib/chewy/index/settings.rb +1 -1
  15. data/lib/chewy/query.rb +43 -4
  16. data/lib/chewy/railtie.rb +2 -2
  17. data/lib/chewy/rake_helper.rb +62 -0
  18. data/lib/chewy/rspec/update_index.rb +3 -3
  19. data/lib/chewy/strategy.rb +6 -0
  20. data/lib/chewy/strategy/active_job.rb +28 -0
  21. data/lib/chewy/strategy/atomic.rb +1 -1
  22. data/lib/chewy/strategy/sidekiq.rb +3 -1
  23. data/lib/chewy/type.rb +2 -1
  24. data/lib/chewy/type/adapter/active_record.rb +9 -2
  25. data/lib/chewy/type/adapter/base.rb +6 -0
  26. data/lib/chewy/type/adapter/mongoid.rb +7 -1
  27. data/lib/chewy/type/adapter/orm.rb +1 -1
  28. data/lib/chewy/type/adapter/sequel.rb +125 -0
  29. data/lib/chewy/type/mapping.rb +26 -1
  30. data/lib/chewy/type/observe.rb +40 -17
  31. data/lib/chewy/version.rb +1 -1
  32. data/lib/sequel/plugins/chewy_observe.rb +71 -0
  33. data/lib/tasks/chewy.rake +19 -61
  34. data/spec/chewy/config_spec.rb +9 -5
  35. data/spec/chewy/fields/base_spec.rb +21 -7
  36. data/spec/chewy/index/actions_spec.rb +5 -5
  37. data/spec/chewy/query_spec.rb +69 -0
  38. data/spec/chewy/runtime_spec.rb +1 -1
  39. data/spec/chewy/strategy/active_job_spec.rb +49 -0
  40. data/spec/chewy/strategy_spec.rb +2 -2
  41. data/spec/chewy/type/adapter/sequel_spec.rb +46 -0
  42. data/spec/chewy/type/import_spec.rb +4 -2
  43. data/spec/chewy/type/mapping_spec.rb +19 -0
  44. data/spec/chewy/type/observe_spec.rb +43 -14
  45. data/spec/chewy_spec.rb +2 -3
  46. data/spec/spec_helper.rb +6 -3
  47. data/spec/support/active_record.rb +5 -8
  48. data/spec/support/mongoid.rb +5 -8
  49. data/spec/support/sequel.rb +69 -0
  50. metadata +14 -3
@@ -21,15 +21,19 @@ describe Chewy::Config do
21
21
  .to change { Chewy.client.transport.logger }.to(logger) }
22
22
  specify { expect { subject.transport_logger = logger }
23
23
  .to change { subject.transport_logger }.to(logger) }
24
+ specify { expect { subject.transport_logger = logger }
25
+ .to change { subject.configuration[:logger] }.from(nil).to(logger) }
24
26
  end
25
27
 
26
28
  describe '#transport_tracer=' do
27
- let(:logger) { Logger.new('/dev/null') }
29
+ let(:tracer) { Logger.new('/dev/null') }
28
30
  after { subject.transport_tracer = nil }
29
31
 
30
- specify { expect { subject.transport_tracer = logger }
31
- .to change { Chewy.client.transport.tracer }.to(logger) }
32
- specify { expect { subject.transport_tracer = logger }
33
- .to change { subject.transport_tracer }.to(logger) }
32
+ specify { expect { subject.transport_tracer = tracer }
33
+ .to change { Chewy.client.transport.tracer }.to(tracer) }
34
+ specify { expect { subject.transport_tracer = tracer }
35
+ .to change { subject.transport_tracer }.to(tracer) }
36
+ specify { expect { subject.transport_tracer = tracer }
37
+ .to change { subject.configuration[:tracer] }.from(nil).to(tracer) }
34
38
  end
35
39
  end
@@ -314,16 +314,20 @@ describe Chewy::Fields::Base do
314
314
  stub_model(:city)
315
315
  stub_model(:country)
316
316
 
317
- City.belongs_to :country
318
-
319
- if active_record?
317
+ case adapter
318
+ when :active_record
319
+ City.belongs_to :country
320
320
  if ActiveRecord::VERSION::MAJOR >= 4
321
321
  Country.has_many :cities, -> { order :id }
322
322
  else
323
323
  Country.has_many :cities, order: :id
324
324
  end
325
- else # mongoid
325
+ when :mongoid
326
+ City.belongs_to :country
326
327
  Country.has_many :cities, order: :id.asc
328
+ when :sequel
329
+ City.many_to_one :country
330
+ Country.one_to_many :cities, order: :id
327
331
  end
328
332
 
329
333
  stub_index(:countries) do
@@ -337,10 +341,20 @@ describe Chewy::Fields::Base do
337
341
  end
338
342
  end
339
343
 
344
+ let(:country_with_cities) do
345
+ cities = [City.create!(id: 1, name: 'City1'), City.create!(id: 2, name: 'City2')]
346
+
347
+ if adapter == :sequel
348
+ Country.create(id: 1).tap do |country|
349
+ cities.each { |city| country.add_city(city) }
350
+ end
351
+ else
352
+ Country.create!(id: 1, cities: cities)
353
+ end
354
+ end
355
+
340
356
  specify do
341
- expect(CountriesIndex::Country.root_object.compose(
342
- Country.create!(id: 1, cities: [City.create!(id: 1, name: 'City1'), City.create!(id: 2, name: 'City2')])
343
- )).to eq({
357
+ expect(CountriesIndex::Country.root_object.compose(country_with_cities)).to eq({
344
358
  country: { 'id' => 1, 'cities' => [
345
359
  { 'id' => 1, 'name' => 'City1' }, { 'id' => 2, 'name' => 'City2' }
346
360
  ] }
@@ -54,8 +54,8 @@ describe Chewy::Index::Actions do
54
54
 
55
55
  context do
56
56
  before { DummiesIndex.create }
57
- specify { expect { DummiesIndex.create! }.to raise_error }
58
- specify { expect { DummiesIndex.create!('2013') }.to raise_error }
57
+ specify { expect { DummiesIndex.create! }.to raise_error(Elasticsearch::Transport::Transport::Errors::BadRequest).with_message(/\[\[dummies\] already exists\]/) }
58
+ specify { expect { DummiesIndex.create!('2013') }.to raise_error(Elasticsearch::Transport::Transport::Errors::BadRequest).with_message(/Invalid alias name \[dummies\]/) }
59
59
  end
60
60
 
61
61
  context do
@@ -64,7 +64,7 @@ describe Chewy::Index::Actions do
64
64
  specify { expect(Chewy.client.indices.exists(index: 'dummies_2013')).to eq(true) }
65
65
  specify { expect(DummiesIndex.aliases).to eq([]) }
66
66
  specify { expect(DummiesIndex.indexes).to eq(['dummies_2013']) }
67
- specify { expect { DummiesIndex.create!('2013') }.to raise_error }
67
+ specify { expect { DummiesIndex.create!('2013') }.to raise_error(Elasticsearch::Transport::Transport::Errors::BadRequest).with_message(/\[\[dummies_2013\] already exists\]/) }
68
68
  specify { expect(DummiesIndex.create!('2014')["acknowledged"]).to eq(true) }
69
69
 
70
70
  context do
@@ -128,8 +128,8 @@ describe Chewy::Index::Actions do
128
128
  end
129
129
 
130
130
  describe '.delete!' do
131
- specify { expect { DummiesIndex.delete! }.to raise_error }
132
- specify { expect { DummiesIndex.delete!('2013') }.to raise_error }
131
+ specify { expect { DummiesIndex.delete! }.to raise_error(Elasticsearch::Transport::Transport::Errors::NotFound) }
132
+ specify { expect { DummiesIndex.delete!('2013') }.to raise_error(Elasticsearch::Transport::Transport::Errors::NotFound) }
133
133
 
134
134
  context do
135
135
  before { DummiesIndex.create }
@@ -213,6 +213,75 @@ describe Chewy::Query do
213
213
  specify { expect(subject.aggregations(aggregation1: {field: 'hello'}).criteria.aggregations).to include(aggregation1: {field: 'hello'}) }
214
214
  specify { expect { subject.aggregations(aggregation1: {field: 'hello'}) }.not_to change { subject.criteria.aggregations } }
215
215
 
216
+ context 'when requesting a named aggregation' do
217
+
218
+ before do
219
+ stub_index(:products) do
220
+ define_type :product do
221
+ root do
222
+ field :name, 'surname'
223
+ field :title, type: 'string' do
224
+ field :subfield1
225
+ end
226
+ field 'price', type: 'float' do
227
+ field :subfield2
228
+ end
229
+ agg :uniquely_named_agg do
230
+ { min: { field: 'title.subfield1' } }
231
+ end
232
+ end
233
+ end
234
+ end
235
+ end
236
+ specify { expect(subject.aggregations(:uniquely_named_agg).criteria.aggregations).to include(uniquely_named_agg: { min: { field: 'title.subfield1' } }) }
237
+
238
+ context 'when more than one aggregation of the same name exists' do
239
+ before do
240
+ stub_index(:products) do
241
+ define_type :product do
242
+ root do
243
+ field :name, 'surname'
244
+ field :title, type: 'string' do
245
+ field :subfield1
246
+ end
247
+ field 'price', type: 'float' do
248
+ field :subfield2
249
+ end
250
+ agg :uniquely_named_agg do
251
+ { min: { field: 'title.subfield1' } }
252
+ end
253
+ agg :named_agg do
254
+ { avg: { field: 'title.subfield1' } }
255
+ end
256
+ end
257
+ end
258
+ define_type :review do
259
+ field :title, :body
260
+ field :comments do
261
+ field :message
262
+ field :rating, type: 'long'
263
+ end
264
+ agg :named_agg do
265
+ { avg: { field: 'comments.rating' } }
266
+ end
267
+ end
268
+ end
269
+ end
270
+
271
+ it "is the aggregation definition that was last defined" do
272
+ expect(subject.aggregations(:named_agg).criteria.aggregations).to include(named_agg: { avg: { field: 'comments.rating' } })
273
+ end
274
+
275
+ context "when the fully qualified aggregation name is provided" do
276
+ specify { expect(subject
277
+ .aggregations("products#product.named_agg")
278
+ .criteria
279
+ .aggregations)
280
+ .to include({ "products#product.named_agg" => { avg: { field: 'title.subfield1' } } }) }
281
+ end
282
+ end
283
+ end
284
+
216
285
  context 'results', :orm do
217
286
  before { stub_model(:city) }
218
287
  let(:cities) { 10.times.map { |i| City.create! id: i + 1, name: "name#{i}", rating: i % 3 } }
@@ -4,6 +4,6 @@ describe Chewy::Runtime do
4
4
  describe '.version' do
5
5
  specify { expect(described_class.version).to be_a(described_class::Version) }
6
6
  specify { expect(described_class.version).to be >= '1.0' }
7
- specify { expect(described_class.version).to be < '1.6' }
7
+ specify { expect(described_class.version).to be < '1.8' }
8
8
  end
9
9
  end
@@ -0,0 +1,49 @@
1
+ require 'spec_helper'
2
+
3
+ if defined?(::ActiveJob)
4
+ describe Chewy::Strategy::ActiveJob do
5
+ around { |example| Chewy.strategy(:bypass) { example.run } }
6
+ before(:all) do
7
+ ::ActiveJob::Base.logger = Chewy.logger
8
+ end
9
+ before do
10
+ ::ActiveJob::Base.queue_adapter = :test
11
+ ::ActiveJob::Base.queue_adapter.enqueued_jobs.clear
12
+ ::ActiveJob::Base.queue_adapter.performed_jobs.clear
13
+ end
14
+
15
+ before do
16
+ stub_model(:city) do
17
+ update_index('cities#city') { self }
18
+ end
19
+
20
+ stub_index(:cities) do
21
+ define_type City
22
+ end
23
+ end
24
+
25
+ let(:city) { City.create!(name: 'hello') }
26
+ let(:other_city) { City.create!(name: 'world') }
27
+
28
+ specify do
29
+ expect { [city, other_city].map(&:save!) }
30
+ .not_to update_index(CitiesIndex::City, strategy: :active_job)
31
+ end
32
+
33
+ specify do
34
+ Chewy.strategy(:active_job) do
35
+ [city, other_city].map(&:save!)
36
+ end
37
+ enqueued_job = ::ActiveJob::Base.queue_adapter.enqueued_jobs.first
38
+ expect(enqueued_job[:job]).to eq(Chewy::Strategy::ActiveJob::Worker)
39
+ expect(enqueued_job[:queue]).to eq('chewy')
40
+ end
41
+
42
+ specify do
43
+ ::ActiveJob::Base.queue_adapter = :inline
44
+ expect { [city, other_city].map(&:save!) }
45
+ .to update_index(CitiesIndex::City, strategy: :active_job)
46
+ .and_reindex(city, other_city)
47
+ end
48
+ end
49
+ end
@@ -14,7 +14,7 @@ describe Chewy::Strategy do
14
14
  end
15
15
 
16
16
  describe '#push' do
17
- specify { expect { strategy.push(:unexistant) }.to raise_error }
17
+ specify { expect { strategy.push(:unexistant) }.to raise_error(NameError).with_message(/uninitialized constant.*Unexistant/) }
18
18
 
19
19
  specify do
20
20
  expect { strategy.push(:atomic) }
@@ -24,7 +24,7 @@ describe Chewy::Strategy do
24
24
  end
25
25
 
26
26
  describe '#pop' do
27
- specify { expect { strategy.pop }.to raise_error }
27
+ specify { expect { strategy.pop }.to raise_error(RuntimeError).with_message(/Can't pop root strategy/) }
28
28
 
29
29
  specify do
30
30
  strategy.push(:urgent)
@@ -0,0 +1,46 @@
1
+ require 'spec_helper'
2
+
3
+ describe Chewy::Type::Adapter::Sequel, :sequel do
4
+
5
+ let(:adapter) { described_class }
6
+
7
+ before do
8
+ stub_model(:city)
9
+ stub_model(:country)
10
+ end
11
+
12
+ describe '#name' do
13
+
14
+ it { expect( adapter.new(City).name ).to eq 'City' }
15
+ it { expect( adapter.new(City.order(:id)).name ).to eq 'City' }
16
+ it { expect( adapter.new(City, name: 'town').name ).to eq 'Town' }
17
+
18
+ context do
19
+ before { stub_model('namespace/city') }
20
+
21
+ it { expect( adapter.new(Namespace::City).name ).to eq 'City' }
22
+ it { expect( adapter.new(Namespace::City.order(:id)).name ).to eq 'City' }
23
+ end
24
+ end
25
+
26
+ describe '#default_dataset' do
27
+
28
+ it { expect( adapter.new(City).default_dataset.sql ).to eql City.where(nil).sql }
29
+ it { expect( adapter.new(City.order(:id)).default_dataset.sql ).to eql City.where(nil).sql }
30
+ it { expect( adapter.new(City.limit(10)).default_dataset.sql ).to eql City.where(nil).sql }
31
+ it { expect( adapter.new(City.offset(10)).default_dataset.sql ).to eql City.where(nil).sql }
32
+ it { expect( adapter.new(City.where(rating: 10)).default_dataset.sql ).to eql City.where(rating: 10).sql }
33
+ end
34
+
35
+ describe '#identify' do
36
+ context do
37
+ subject(:s) { adapter.new(City) }
38
+ let!(:cities) { 3.times.map { City.new.save } }
39
+
40
+ it { expect( s.identify(City.where(nil)) ).to match_array cities.map(&:id) }
41
+ it { expect( s.identify(cities) ).to eq cities.map(&:id) }
42
+ it { expect( s.identify(cities.first) ).to eq([cities.first.id]) }
43
+ it { expect( s.identify(cities.first(2).map(&:pk)) ).to eq cities.first(2).map(&:id) }
44
+ end
45
+ end
46
+ end
@@ -164,7 +164,8 @@ describe Chewy::Type::Import do
164
164
  type: CitiesIndex::City,
165
165
  errors: {
166
166
  index: {
167
- 'MapperParsingException[object mapping for [city] tried to parse field [name] as object, but got EOF, has a concrete value been provided to it?]' => ['1', '2', '3']
167
+ "WriteFailureException; nested: MapperParsingException[object mapping for [city] tried to parse field [name] as object, but got EOF, has a concrete value been provided to it?]; " => ["1"],
168
+ "MapperParsingException[object mapping for [city] tried to parse field [name] as object, but got EOF, has a concrete value been provided to it?]" => ["2", "3"]
168
169
  }
169
170
  },
170
171
  import: {index: 3}
@@ -209,7 +210,8 @@ describe Chewy::Type::Import do
209
210
 
210
211
  before do
211
212
  stub_model(:country)
212
- stub_model(:city) { belongs_to :country }
213
+ stub_model(:city)
214
+ adapter == :sequel ? City.many_to_one(:country) : City.belongs_to(:country)
213
215
  end
214
216
 
215
217
  before do
@@ -2,6 +2,7 @@ require 'spec_helper'
2
2
 
3
3
  describe Chewy::Type::Mapping do
4
4
  let(:product) { ProductsIndex::Product }
5
+ let(:review) { ProductsIndex::Review }
5
6
 
6
7
  before do
7
8
  stub_index(:products) do
@@ -14,11 +15,29 @@ describe Chewy::Type::Mapping do
14
15
  field 'price', type: 'float' do
15
16
  field :subfield2
16
17
  end
18
+ agg :named_agg do
19
+ { avg: { field: 'title.subfield1' } }
20
+ end
21
+ end
22
+ end
23
+ define_type :review do
24
+ field :title, :body
25
+ field :comments do
26
+ field :message
27
+ field :rating, type: 'long'
28
+ end
29
+ agg :named_agg do
30
+ { avg: { field: 'comments.rating' } }
17
31
  end
18
32
  end
19
33
  end
20
34
  end
21
35
 
36
+ describe '.agg' do
37
+ specify { expect(product._agg_defs[:named_agg].call).to eq({ avg: { field: 'title.subfield1' } }) }
38
+ specify { expect(review._agg_defs[:named_agg].call).to eq({ avg: { field: 'comments.rating' } }) }
39
+ end
40
+
22
41
  describe '.field' do
23
42
  specify { expect(product.root_object.children.map(&:name)).to eq([:name, :surname, :title, :price]) }
24
43
  specify { expect(product.root_object.children.map(&:parent)).to eq([product.root_object] * 4) }
@@ -10,30 +10,43 @@ describe Chewy::Type::Import do
10
10
 
11
11
  let(:backreferenced) { 3.times.map { |i| double(id: i) } }
12
12
 
13
- specify { expect { DummiesIndex.dummy.update_index(backreferenced) }
13
+ specify { expect { DummiesIndex::Dummy.update_index(backreferenced) }
14
14
  .to raise_error Chewy::UndefinedUpdateStrategy }
15
- specify { expect { DummiesIndex.dummy.update_index([]) }
15
+ specify { expect { DummiesIndex::Dummy.update_index([]) }
16
16
  .not_to update_index('dummies#dummy') }
17
- specify { expect { DummiesIndex.dummy.update_index(nil) }
17
+ specify { expect { DummiesIndex::Dummy.update_index(nil) }
18
18
  .not_to update_index('dummies#dummy') }
19
19
  end
20
20
 
21
21
  context 'integration', :orm do
22
+ let(:update_condition) { true }
23
+
22
24
  before do
23
- stub_model(:city) do
24
- update_index('cities#city') { self }
25
- update_index 'countries#country' do
26
- changes['country_id'] || previous_changes['country_id'] || country
25
+ city_countries_update_proc = if adapter == :sequel
26
+ ->(*) { previous_changes.try(:[], :country_id) || country }
27
+ else
28
+ ->(*) { changes['country_id'] || previous_changes['country_id'] || country }
27
29
  end
30
+
31
+ stub_model(:city) do
32
+ update_index(->(city) { "cities##{city.class.name.underscore}" }) { self }
33
+ update_index 'countries#country', &city_countries_update_proc
28
34
  end
29
35
 
30
36
  stub_model(:country) do
31
- update_index('cities#city') { cities }
32
- update_index 'countries#country', :self
37
+ update_index('cities#city', if: -> { update_condition }) { cities }
38
+ update_index(->{ "countries##{self.class.name.underscore}" }, :self)
39
+ attr_accessor :update_condition
33
40
  end
34
41
 
35
- City.belongs_to :country
36
- Country.has_many :cities
42
+ if adapter == :sequel
43
+ City.many_to_one :country
44
+ Country.one_to_many :cities
45
+ City.plugin :dirty
46
+ else
47
+ City.belongs_to :country
48
+ Country.has_many :cities
49
+ end
37
50
 
38
51
  stub_index(:cities) do
39
52
  define_type City
@@ -45,8 +58,8 @@ describe Chewy::Type::Import do
45
58
  end
46
59
 
47
60
  context do
48
- let!(:country1) { Chewy.strategy(:atomic) { Country.create!(id: 1) } }
49
- let!(:country2) { Chewy.strategy(:atomic) { Country.create!(id: 2) } }
61
+ let!(:country1) { Chewy.strategy(:atomic) { Country.create!(id: 1, update_condition: update_condition) } }
62
+ let!(:country2) { Chewy.strategy(:atomic) { Country.create!(id: 2, update_condition: update_condition) } }
50
63
  let!(:city) { Chewy.strategy(:atomic) { City.create!(id: 1, country: country1) } }
51
64
 
52
65
  specify { expect { city.save! }.to update_index('cities#city').and_reindex(city) }
@@ -60,10 +73,26 @@ describe Chewy::Type::Import do
60
73
  end
61
74
 
62
75
  context do
63
- let!(:country) { Chewy.strategy(:atomic) { Country.create!(id: 1, cities: 2.times.map { |i| City.create!(id: i) }) } }
76
+ let!(:country) do
77
+ Chewy.strategy(:atomic) do
78
+ cities = 2.times.map { |i| City.create!(id: i) }
79
+ if adapter == :sequel
80
+ Country.create(id: 1, update_condition: update_condition).tap do |country|
81
+ cities.each { |city| country.add_city(city) }
82
+ end
83
+ else
84
+ Country.create!(id: 1, cities: cities, update_condition: update_condition)
85
+ end
86
+ end
87
+ end
64
88
 
65
89
  specify { expect { country.save! }.to update_index('cities#city').and_reindex(country.cities) }
66
90
  specify { expect { country.save! }.to update_index('countries#country').and_reindex(country) }
91
+
92
+ context 'conditional update' do
93
+ let(:update_condition) { false }
94
+ specify { expect { country.save! }.not_to update_index('cities#city') }
95
+ end
67
96
  end
68
97
  end
69
98