chewy 0.6.2 → 0.7.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 (101) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -1
  3. data/.travis.yml +35 -29
  4. data/Appraisals +37 -0
  5. data/CHANGELOG.md +115 -4
  6. data/Gemfile +2 -3
  7. data/README.md +135 -40
  8. data/chewy.gemspec +4 -3
  9. data/gemfiles/rails.3.2.activerecord.gemfile +13 -0
  10. data/gemfiles/rails.3.2.activerecord.kaminari.gemfile +14 -0
  11. data/gemfiles/rails.3.2.activerecord.will_paginate.gemfile +14 -0
  12. data/gemfiles/rails.4.0.activerecord.gemfile +13 -0
  13. data/gemfiles/rails.4.0.activerecord.kaminari.gemfile +14 -0
  14. data/gemfiles/rails.4.0.activerecord.will_paginate.gemfile +14 -0
  15. data/gemfiles/rails.4.0.mongoid.gemfile +13 -0
  16. data/gemfiles/rails.4.0.mongoid.kaminari.gemfile +14 -0
  17. data/gemfiles/rails.4.0.mongoid.will_paginate.gemfile +14 -0
  18. data/gemfiles/rails.4.1.activerecord.gemfile +13 -0
  19. data/gemfiles/rails.4.1.activerecord.kaminari.gemfile +14 -0
  20. data/gemfiles/rails.4.1.activerecord.will_paginate.gemfile +14 -0
  21. data/gemfiles/rails.4.1.mongoid.gemfile +13 -0
  22. data/gemfiles/rails.4.1.mongoid.kaminari.gemfile +14 -0
  23. data/gemfiles/rails.4.1.mongoid.will_paginate.gemfile +14 -0
  24. data/gemfiles/rails.4.2.activerecord.gemfile +13 -0
  25. data/gemfiles/rails.4.2.activerecord.kaminari.gemfile +14 -0
  26. data/gemfiles/rails.4.2.activerecord.will_paginate.gemfile +14 -0
  27. data/gemfiles/rails.4.2.mongoid.gemfile +13 -0
  28. data/gemfiles/rails.4.2.mongoid.kaminari.gemfile +14 -0
  29. data/gemfiles/rails.4.2.mongoid.will_paginate.gemfile +14 -0
  30. data/lib/chewy.rb +65 -0
  31. data/lib/chewy/config.rb +44 -93
  32. data/lib/chewy/errors.rb +14 -5
  33. data/lib/chewy/fields/base.rb +8 -7
  34. data/lib/chewy/fields/root.rb +2 -2
  35. data/lib/chewy/index.rb +7 -9
  36. data/lib/chewy/log_subscriber.rb +34 -0
  37. data/lib/chewy/query.rb +41 -27
  38. data/lib/chewy/query/criteria.rb +28 -23
  39. data/lib/chewy/query/scoping.rb +20 -0
  40. data/lib/chewy/railtie.rb +51 -13
  41. data/lib/chewy/repository.rb +61 -0
  42. data/lib/chewy/rspec/update_index.rb +3 -6
  43. data/lib/chewy/search.rb +28 -7
  44. data/lib/chewy/strategy.rb +60 -0
  45. data/lib/chewy/strategy/atomic.rb +31 -0
  46. data/lib/chewy/strategy/base.rb +27 -0
  47. data/lib/chewy/strategy/bypass.rb +15 -0
  48. data/lib/chewy/strategy/urgent.rb +17 -0
  49. data/lib/chewy/type.rb +19 -5
  50. data/lib/chewy/type/adapter/active_record.rb +28 -117
  51. data/lib/chewy/type/adapter/base.rb +35 -0
  52. data/lib/chewy/type/adapter/mongoid.rb +23 -123
  53. data/lib/chewy/type/adapter/object.rb +41 -19
  54. data/lib/chewy/type/adapter/orm.rb +142 -0
  55. data/lib/chewy/type/import.rb +43 -16
  56. data/lib/chewy/type/observe.rb +8 -21
  57. data/lib/chewy/version.rb +1 -1
  58. data/lib/tasks/chewy.rake +8 -4
  59. data/spec/chewy/config_spec.rb +20 -97
  60. data/spec/chewy/fields/base_spec.rb +24 -11
  61. data/spec/chewy/fields/time_fields_spec.rb +27 -0
  62. data/spec/chewy/index/settings_spec.rb +2 -1
  63. data/spec/chewy/index_spec.rb +98 -79
  64. data/spec/chewy/query/criteria_spec.rb +14 -0
  65. data/spec/chewy/query_spec.rb +1 -1
  66. data/spec/chewy/repository_spec.rb +50 -0
  67. data/spec/chewy/search_spec.rb +100 -0
  68. data/spec/chewy/strategy_spec.rb +109 -0
  69. data/spec/chewy/type/adapter/active_record_spec.rb +110 -46
  70. data/spec/chewy/type/adapter/mongoid_spec.rb +123 -74
  71. data/spec/chewy/type/adapter/object_spec.rb +51 -34
  72. data/spec/chewy/type/import_spec.rb +21 -21
  73. data/spec/chewy/type/observe_spec.rb +26 -29
  74. data/spec/chewy/type_spec.rb +19 -0
  75. data/spec/chewy_spec.rb +19 -3
  76. data/spec/spec_helper.rb +1 -1
  77. data/spec/support/active_record.rb +2 -1
  78. data/spec/support/mongoid.rb +29 -38
  79. metadata +85 -55
  80. data/gemfiles/Gemfile.rails-3.2.active_record +0 -6
  81. data/gemfiles/Gemfile.rails-3.2.active_record.kaminari +0 -7
  82. data/gemfiles/Gemfile.rails-3.2.active_record.will_paginate +0 -7
  83. data/gemfiles/Gemfile.rails-4.0.active_record +0 -6
  84. data/gemfiles/Gemfile.rails-4.0.active_record.kaminari +0 -7
  85. data/gemfiles/Gemfile.rails-4.0.active_record.will_paginate +0 -7
  86. data/gemfiles/Gemfile.rails-4.0.mongoid +0 -6
  87. data/gemfiles/Gemfile.rails-4.0.mongoid.kaminari +0 -7
  88. data/gemfiles/Gemfile.rails-4.0.mongoid.will_paginate +0 -7
  89. data/gemfiles/Gemfile.rails-4.1.active_record +0 -6
  90. data/gemfiles/Gemfile.rails-4.1.active_record.kaminari +0 -7
  91. data/gemfiles/Gemfile.rails-4.1.active_record.will_paginate +0 -7
  92. data/gemfiles/Gemfile.rails-4.1.mongoid +0 -6
  93. data/gemfiles/Gemfile.rails-4.1.mongoid.kaminari +0 -7
  94. data/gemfiles/Gemfile.rails-4.1.mongoid.will_paginate +0 -7
  95. data/gemfiles/Gemfile.rails-4.2.active_record +0 -6
  96. data/gemfiles/Gemfile.rails-4.2.active_record.kaminari +0 -7
  97. data/gemfiles/Gemfile.rails-4.2.active_record.will_paginate +0 -7
  98. data/gemfiles/Gemfile.rails-4.2.mongoid +0 -6
  99. data/gemfiles/Gemfile.rails-4.2.mongoid.kaminari +0 -7
  100. data/gemfiles/Gemfile.rails-4.2.mongoid.will_paginate +0 -7
  101. data/spec/chewy/index/search_spec.rb +0 -46
@@ -8,6 +8,7 @@ describe Chewy::Query::Criteria do
8
8
  its(:facets) { should == {} }
9
9
  its(:scores) { should == [] }
10
10
  its(:aggregations) { should == {} }
11
+ its(:script_fields) { should == {} }
11
12
  its(:queries) { should == [] }
12
13
  its(:filters) { should == [] }
13
14
  its(:post_filters) { should == [] }
@@ -19,6 +20,7 @@ describe Chewy::Query::Criteria do
19
20
  its(:facets?) { should eq(false) }
20
21
  its(:scores?) { should eq(false) }
21
22
  its(:aggregations?) { should eq(false) }
23
+ its(:script_fields?) { should eq(false) }
22
24
  its(:queries?) { should eq(false) }
23
25
  its(:filters?) { should eq(false) }
24
26
  its(:post_filters?) { should eq(false) }
@@ -54,6 +56,11 @@ describe Chewy::Query::Criteria do
54
56
  specify { expect { subject.update_aggregations(field: 'hello') }.to change { subject.aggregations }.to(field: 'hello') }
55
57
  end
56
58
 
59
+ describe '#update_script_fields' do
60
+ specify { expect { subject.update_script_fields(distance: {script: "doc['coordinates'].distanceInMiles(lat, lon)"}) }.to change { subject.script_fields? }.to(true) }
61
+ specify { expect { subject.update_script_fields(distance_km: {script: "doc['coordinates'].distanceInKm(lat, lon)"}) }.to change { subject.script_fields }.to(distance_km: {script: "doc['coordinates'].distanceInKm(lat, lon)"}) }
62
+ end
63
+
57
64
  describe '#update_queries' do
58
65
  specify { expect { subject.update_queries(field: 'hello') }.to change { subject.queries? }.to(true) }
59
66
  specify { expect { subject.update_queries(field: 'hello') }.to change { subject.queries }.to([field: 'hello']) }
@@ -132,6 +139,8 @@ describe Chewy::Query::Criteria do
132
139
  .merge(criteria.tap { |c| c.update_request_options(opt2: 'hello') }).request_options).to include(opt1: 'hello', opt2: 'hello') }
133
140
  specify { expect(subject.tap { |c| c.update_facets(field1: 'hello') }
134
141
  .merge(criteria.tap { |c| c.update_facets(field1: 'hello') }).facets).to eq({field1: 'hello', field1: 'hello'}) }
142
+ specify { expect(subject.tap { |c| c.update_script_fields(distance_m: {script: "doc['coordinates'].distanceInMiles(lat, lon)"}) }
143
+ .merge(criteria.tap { |c| c.update_script_fields(distance_km: {script: "doc['coordinates'].distanceInKm(lat, lon)"}) }).script_fields).to eq({distance_m: {script: "doc['coordinates'].distanceInMiles(lat, lon)"}, distance_km: {script: "doc['coordinates'].distanceInKm(lat, lon)"}}) }
135
144
  specify { expect(subject.tap { |c| c.update_scores(script: 'hello') }
136
145
  .merge(criteria.tap { |c| c.update_scores(script: 'foobar') }).scores).to eq([{script: 'hello'}, { script: 'foobar' } ]) }
137
146
  specify { expect(subject.tap { |c| c.update_aggregations(field1: 'hello') }
@@ -162,6 +171,8 @@ describe Chewy::Query::Criteria do
162
171
  .merge!(criteria.tap { |c| c.update_request_options(opt2: 'hello') }).request_options).to include(opt1: 'hello', opt2: 'hello') }
163
172
  specify { expect(subject.tap { |c| c.update_facets(field1: 'hello') }
164
173
  .merge!(criteria.tap { |c| c.update_facets(field1: 'hello') }).facets).to eq({field1: 'hello', field1: 'hello'}) }
174
+ specify { expect(subject.tap { |c| c.update_script_fields(distance_m: {script: "doc['coordinates'].distanceInMiles(lat, lon)"}) }
175
+ .merge(criteria.tap { |c| c.update_script_fields(distance_km: {script: "doc['coordinates'].distanceInKm(lat, lon)"}) }).script_fields).to eq({distance_m: {script: "doc['coordinates'].distanceInMiles(lat, lon)"}, distance_km: {script: "doc['coordinates'].distanceInKm(lat, lon)"}}) }
165
176
  specify { expect(subject.tap { |c| c.update_aggregations(field1: 'hello') }
166
177
  .merge!(criteria.tap { |c| c.update_aggregations(field1: 'hello') }).aggregations).to eq({field1: 'hello', field1: 'hello'}) }
167
178
  specify { expect(subject.tap { |c| c.update_queries(field1: 'hello') }
@@ -217,6 +228,9 @@ describe Chewy::Query::Criteria do
217
228
  specify { expect(request_body {
218
229
  update_queries(:query); update_post_filters(:post_filter);
219
230
  }).to eq({body: {query: :query, post_filter: :post_filter}}) }
231
+ specify { expect(request_body {
232
+ update_script_fields(distance_m: {script: "doc['coordinates'].distanceInMiles(lat, lon)"})
233
+ }).to eq({:body=>{:script_fields=>{:distance_m=>{:script=>"doc['coordinates'].distanceInMiles(lat, lon)"}}}}) }
220
234
  end
221
235
 
222
236
  describe '#_filtered_query' do
@@ -106,7 +106,7 @@ describe Chewy::Query do
106
106
  specify { expect(subject.script_score('23')).not_to eq(subject) }
107
107
  specify { expect(subject.script_score('23').criteria.scores).to eq([ { script_score: { script: '23' } } ]) }
108
108
  specify { expect { subject.script_score('23') }.not_to change { subject.criteria.scores } }
109
- specify { expect(subject.script_score('23', filter: { foo: :bar}).criteria.scores).to eq([{ script_score: { script: '23' }, filter: { foo: :bar } }]) }
109
+ specify { expect(subject.script_score('23 * factor', params: { factor: 0.5}).criteria.scores).to eq([{ script_score: { script: '23 * factor', params: { factor: 0.5} } }]) }
110
110
  end
111
111
 
112
112
  describe '#boost_factor' do
@@ -0,0 +1,50 @@
1
+ require 'spec_helper'
2
+
3
+ describe Chewy::Repository do
4
+ subject { described_class.send(:new) }
5
+
6
+ its(:analyzers) { should == {} }
7
+ its(:tokenizers) { should == {} }
8
+ its(:filters) { should == {} }
9
+ its(:char_filters) { should == {} }
10
+
11
+ describe '#analyzer' do
12
+ specify { expect(subject.analyzer(:name)).to be_nil }
13
+
14
+ context do
15
+ before { subject.analyzer(:name, option: :foo) }
16
+ specify { expect(subject.analyzer(:name)).to eq({option: :foo}) }
17
+ specify { expect(subject.analyzers).to eq({name: {option: :foo}}) }
18
+ end
19
+ end
20
+
21
+ describe '#tokenizer' do
22
+ specify { expect(subject.tokenizer(:name)).to be_nil }
23
+
24
+ context do
25
+ before { subject.tokenizer(:name, option: :foo) }
26
+ specify { expect(subject.tokenizer(:name)).to eq({option: :foo}) }
27
+ specify { expect(subject.tokenizers).to eq({name: {option: :foo}}) }
28
+ end
29
+ end
30
+
31
+ describe '#filter' do
32
+ specify { expect(subject.filter(:name)).to be_nil }
33
+
34
+ context do
35
+ before { subject.filter(:name, option: :foo) }
36
+ specify { expect(subject.filter(:name)).to eq({option: :foo}) }
37
+ specify { expect(subject.filters).to eq({name: {option: :foo}}) }
38
+ end
39
+ end
40
+
41
+ describe '#char_filter' do
42
+ specify { expect(subject.char_filter(:name)).to be_nil }
43
+
44
+ context do
45
+ before { subject.char_filter(:name, option: :foo) }
46
+ specify { expect(subject.char_filter(:name)).to eq({option: :foo}) }
47
+ specify { expect(subject.char_filters).to eq({name: {option: :foo}}) }
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,100 @@
1
+ require 'spec_helper'
2
+
3
+ describe Chewy::Search do
4
+ before { Chewy.massacre }
5
+
6
+ before do
7
+ stub_index(:products) do
8
+ define_type :product
9
+ define_type :product2
10
+ end
11
+ end
12
+
13
+ let(:product) { ProductsIndex::Product }
14
+
15
+ describe '.all' do
16
+ specify { expect(ProductsIndex.all).to be_a(Chewy::Query) }
17
+ specify { expect(product.all).to be_a(Chewy::Query) }
18
+ end
19
+
20
+ describe '.search_string' do
21
+ specify do
22
+ expect(ProductsIndex.client).to receive(:search).with(hash_including(q: 'hello')).twice
23
+ ProductsIndex.search_string('hello')
24
+ product.search_string('hello')
25
+ end
26
+
27
+ specify do
28
+ expect(ProductsIndex.client).to receive(:search).with(hash_including(explain: true)).twice
29
+ ProductsIndex.search_string('hello', explain: true)
30
+ product.search_string('hello', explain: true)
31
+ end
32
+
33
+ specify do
34
+ expect(ProductsIndex.client).to receive(:search).with(hash_including(index: ['products'], type: []))
35
+ ProductsIndex.search_string('hello')
36
+ end
37
+
38
+ specify do
39
+ expect(ProductsIndex.client).to receive(:search).with(hash_including(index: ['products'], type: ['product']))
40
+ product.search_string('hello')
41
+ end
42
+ end
43
+
44
+ context 'named scopes' do
45
+ before do
46
+ stub_model(:city)
47
+ stub_model(:country)
48
+
49
+ stub_index(:places) do
50
+ def self.by_rating(value)
51
+ filter { rating == value }
52
+ end
53
+
54
+ def self.by_name(index)
55
+ filter { name == "Name#{index}" }
56
+ end
57
+
58
+ define_type City do
59
+ def self.by_rating
60
+ filter { rating == yield }
61
+ end
62
+
63
+ field :name, index: 'not_analyzed'
64
+ field :rating, type: :integer
65
+ end
66
+
67
+ define_type Country do
68
+ field :name, index: 'not_analyzed'
69
+ field :rating, type: :integer
70
+ end
71
+ end
72
+ end
73
+
74
+ let!(:cities) { 3.times.map { |i| City.create! rating: i + 1, name: "Name#{i+1}" } }
75
+ let!(:countries) { 3.times.map { |i| Country.create! rating: i + 1, name: "Name#{i+4}" } }
76
+
77
+ before { PlacesIndex.import! city: cities, country: countries }
78
+
79
+ specify { expect(PlacesIndex.by_rating(1).map(&:rating)).to eq([1, 1]) }
80
+ specify { expect(PlacesIndex.by_rating(1).map(&:class)).to match_array([PlacesIndex::City, PlacesIndex::Country]) }
81
+ specify { expect(PlacesIndex.by_rating(1).by_name(1).map(&:rating)).to eq([1]) }
82
+ specify { expect(PlacesIndex.by_rating(1).by_name(1).map(&:class)).to eq([PlacesIndex::City]) }
83
+ specify { expect(PlacesIndex.order(:name).by_rating(1).map(&:rating)).to eq([1, 1]) }
84
+ specify { expect(PlacesIndex.order(:name).by_rating(1).map(&:class)).to match_array([PlacesIndex::City, PlacesIndex::Country]) }
85
+
86
+ specify { expect(PlacesIndex::City.by_rating {2}.map(&:rating)).to eq([2]) }
87
+ specify { expect(PlacesIndex::City.by_rating {2}.map(&:class)).to eq([PlacesIndex::City]) }
88
+ specify { expect(PlacesIndex::City.by_rating {2}.by_name(2).map(&:rating)).to eq([2]) }
89
+ specify { expect(PlacesIndex::City.by_rating {2}.by_name(2).map(&:class)).to eq([PlacesIndex::City]) }
90
+ specify { expect(PlacesIndex::City.order(:name).by_rating {2}.map(&:rating)).to eq([2]) }
91
+ specify { expect(PlacesIndex::City.order(:name).by_rating {2}.map(&:class)).to eq([PlacesIndex::City]) }
92
+
93
+ specify { expect(PlacesIndex::Country.by_rating(3).map(&:rating)).to eq([3]) }
94
+ specify { expect(PlacesIndex::Country.by_rating(3).map(&:class)).to eq([PlacesIndex::Country]) }
95
+ specify { expect(PlacesIndex::Country.by_rating(3).by_name(6).map(&:rating)).to eq([3]) }
96
+ specify { expect(PlacesIndex::Country.by_rating(3).by_name(6).map(&:class)).to eq([PlacesIndex::Country]) }
97
+ specify { expect(PlacesIndex::Country.order(:name).by_rating(3).map(&:rating)).to eq([3]) }
98
+ specify { expect(PlacesIndex::Country.order(:name).by_rating(3).map(&:class)).to eq([PlacesIndex::Country]) }
99
+ end
100
+ end
@@ -0,0 +1,109 @@
1
+ require 'spec_helper'
2
+
3
+ describe Chewy::Strategy do
4
+ before { Chewy.massacre }
5
+ subject(:strategy) { Chewy::Strategy.new }
6
+
7
+ describe '#current' do
8
+ specify { expect(strategy.current).to be_a(Chewy::Strategy::Base) }
9
+
10
+ context do
11
+ before { allow(Chewy).to receive_messages(root_strategy: :bypass) }
12
+ specify { expect(strategy.current).to be_a(Chewy::Strategy::Bypass) }
13
+ end
14
+ end
15
+
16
+ describe '#push' do
17
+ specify { expect { strategy.push(:unexistant) }.to raise_error }
18
+
19
+ specify do
20
+ expect { strategy.push(:atomic) }
21
+ .to change { strategy.current }
22
+ .to(an_instance_of(Chewy::Strategy::Atomic))
23
+ end
24
+ end
25
+
26
+ describe '#pop' do
27
+ specify { expect { strategy.pop }.to raise_error }
28
+
29
+ specify do
30
+ strategy.push(:urgent)
31
+ expect { strategy.pop }
32
+ .to change { strategy.current }
33
+ .to(an_instance_of(Chewy::Strategy::Base))
34
+ end
35
+ end
36
+
37
+ context 'nesting', :orm do
38
+ before do
39
+ stub_model(:city) do
40
+ update_index('cities#city') { self }
41
+ end
42
+
43
+ stub_index(:cities) do
44
+ define_type City
45
+ end
46
+ end
47
+
48
+ let(:city) { City.create!(name: 'hello') }
49
+ let(:other_city) { City.create!(name: 'world') }
50
+
51
+ context do
52
+ around { |example| Chewy.strategy(:bypass) { example.run } }
53
+
54
+ specify do
55
+ expect(CitiesIndex::City).not_to receive(:import)
56
+ [city, other_city].map(&:save!)
57
+ end
58
+
59
+ specify do
60
+ expect(CitiesIndex::City).to receive(:import).with([city.id, other_city.id]).once
61
+ Chewy.strategy(:atomic) { [city, other_city].map(&:save!) }
62
+ end
63
+ end
64
+
65
+ context do
66
+ around { |example| Chewy.strategy(:urgent) { example.run } }
67
+
68
+ specify do
69
+ expect(CitiesIndex::City).to receive(:import).at_least(2).times
70
+ [city, other_city].map(&:save!)
71
+ end
72
+
73
+ specify do
74
+ expect(CitiesIndex::City).to receive(:import).with([city.id, other_city.id]).once
75
+ Chewy.strategy(:atomic) { [city, other_city].map(&:save!) }
76
+ end
77
+
78
+ context 'hash passed to urgent' do
79
+ before do
80
+ stub_index(:cities) do
81
+ define_type :city
82
+ end
83
+
84
+ stub_model(:city) do
85
+ update_index('cities#city') { { name: name } }
86
+ end
87
+ end
88
+
89
+ specify do
90
+ [city, other_city].map(&:save!)
91
+ expect(CitiesIndex::City.total_count).to eq(4)
92
+ end
93
+
94
+ context do
95
+ before do
96
+ stub_model(:city) do
97
+ update_index('cities#city') { { id: id.to_s, name: name } }
98
+ end
99
+ end
100
+
101
+ specify do
102
+ [city, other_city].map(&:save!)
103
+ expect(CitiesIndex::City.total_count).to eq(2)
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
@@ -16,6 +16,14 @@ describe Chewy::Type::Adapter::ActiveRecord, :active_record do
16
16
  end
17
17
  end
18
18
 
19
+ describe '#default_scope' do
20
+ specify { expect(described_class.new(City).default_scope).to eq(City.where(nil)) }
21
+ specify { expect(described_class.new(City.order(:id)).default_scope).to eq(City.where(nil)) }
22
+ specify { expect(described_class.new(City.limit(10)).default_scope).to eq(City.where(nil)) }
23
+ specify { expect(described_class.new(City.offset(10)).default_scope).to eq(City.where(nil)) }
24
+ specify { expect(described_class.new(City.where(rating: 10)).default_scope).to eq(City.where(rating: 10)) }
25
+ end
26
+
19
27
  describe '#type_name' do
20
28
  specify { expect(described_class.new(City).type_name).to eq('city') }
21
29
  specify { expect(described_class.new(City.order(:id)).type_name).to eq('city') }
@@ -29,6 +37,29 @@ describe Chewy::Type::Adapter::ActiveRecord, :active_record do
29
37
  end
30
38
  end
31
39
 
40
+ describe '#identify' do
41
+ context do
42
+ subject { described_class.new(City) }
43
+ let!(:cities) { 3.times.map { City.create! } }
44
+
45
+ specify { expect(subject.identify(City.where(nil))).to match_array(cities.map(&:id)) }
46
+ specify { expect(subject.identify(cities)).to eq(cities.map(&:id)) }
47
+ specify { expect(subject.identify(cities.first)).to eq([cities.first.id]) }
48
+ specify { expect(subject.identify(cities.first(2).map(&:id))).to eq(cities.first(2).map(&:id)) }
49
+ end
50
+
51
+ context 'custom primary_key' do
52
+ before { stub_model(:city) { self.primary_key = 'rating' } }
53
+ subject { described_class.new(City) }
54
+ let!(:cities) { 3.times.map { |i| City.create! { |c| c.rating = i } } }
55
+
56
+ specify { expect(subject.identify(City.where(nil))).to match_array([0, 1, 2]) }
57
+ specify { expect(subject.identify(cities)).to eq([0, 1, 2]) }
58
+ specify { expect(subject.identify(cities.first)).to eq([0]) }
59
+ specify { expect(subject.identify(cities.first(2).map(&:id))).to eq([0, 1]) }
60
+ end
61
+ end
62
+
32
63
  describe '#import' do
33
64
  def import(*args)
34
65
  result = []
@@ -37,11 +68,12 @@ describe Chewy::Type::Adapter::ActiveRecord, :active_record do
37
68
  end
38
69
 
39
70
  context do
40
- let!(:cities) { 3.times.map { |i| City.create! } }
41
- let!(:deleted) { 3.times.map { |i| City.create!.tap(&:destroy) } }
71
+ let!(:cities) { 3.times.map { City.create! } }
72
+ let!(:deleted) { 4.times.map { City.create!.tap(&:destroy) } }
42
73
  subject { described_class.new(City) }
43
74
 
44
75
  specify { expect(import).to eq([{index: cities}]) }
76
+ specify { expect(import nil).to eq([]) }
45
77
 
46
78
  specify { expect(import(City.order(:id))).to eq([{index: cities}]) }
47
79
  specify { expect(import(City.order(:id), batch_size: 2))
@@ -50,11 +82,13 @@ describe Chewy::Type::Adapter::ActiveRecord, :active_record do
50
82
  specify { expect(import(cities)).to eq([{index: cities}]) }
51
83
  specify { expect(import(cities, batch_size: 2))
52
84
  .to eq([{index: cities.first(2)}, {index: cities.last(1)}]) }
53
- specify { expect(import(cities, deleted)).to eq([{index: cities, delete: deleted}]) }
85
+ specify { expect(import(cities, deleted))
86
+ .to eq([{index: cities}, {delete: deleted}]) }
54
87
  specify { expect(import(cities, deleted, batch_size: 2)).to eq([
55
- {index: cities.first(2)},
56
- {index: cities.last(1), delete: deleted.first(1)},
57
- {delete: deleted.last(2)}]) }
88
+ {index: cities.first(2)},
89
+ {index: cities.last(1)},
90
+ {delete: deleted.first(2)},
91
+ {delete: deleted.last(2)}]) }
58
92
 
59
93
  specify { expect(import(cities.map(&:id))).to eq([{index: cities}]) }
60
94
  specify { expect(import(deleted.map(&:id))).to eq([{delete: deleted.map(&:id)}]) }
@@ -66,48 +100,61 @@ describe Chewy::Type::Adapter::ActiveRecord, :active_record do
66
100
  {index: cities.first(2)},
67
101
  {index: cities.last(1)},
68
102
  {delete: deleted.first(2).map(&:id)},
69
- {delete: deleted.last(1).map(&:id)}]) }
103
+ {delete: deleted.last(2).map(&:id)}]) }
70
104
 
71
105
  specify { expect(import(cities.first, nil)).to eq([{index: [cities.first]}]) }
72
106
  specify { expect(import(cities.first.id, nil)).to eq([{index: [cities.first]}]) }
107
+ end
73
108
 
74
- context do
75
- before { deleted.map { |object| allow(object).to receive_messages(delete_from_index?: true, destroyed?: true) } }
76
- specify { expect(import(deleted)).to eq([{delete: deleted}]) }
77
- end
109
+ context 'additional delete conitions' do
110
+ let!(:cities) { 4.times.map { |i| City.create! rating: i } }
111
+ before { cities.last(2).map(&:destroy) }
112
+ subject { described_class.new(City) }
78
113
 
79
114
  context do
80
- before { deleted.map { |object| allow(object).to receive_messages(delete_from_index?: true, destroyed?: false) } }
81
- specify { expect(import(deleted)).to eq([{delete: deleted}]) }
82
- end
115
+ before do
116
+ City.class_eval do
117
+ def delete_from_index?
118
+ rating.in?([1, 3])
119
+ end
120
+ end
121
+ end
83
122
 
84
- context do
85
- before { deleted.map { |object| allow(object).to receive_messages(delete_from_index?: false, destroyed?: true) } }
86
- specify { expect(import(deleted)).to eq([{delete: deleted}]) }
123
+ specify { expect(import(City.where(nil))).to eq([
124
+ { index: [cities[0]], delete: [cities[1]] }
125
+ ]) }
126
+ specify { expect(import(cities)).to eq([
127
+ { index: [cities[0]], delete: [cities[1]] },
128
+ { delete: cities.last(2) }
129
+ ]) }
130
+ specify { expect(import(cities.map(&:id))).to eq([
131
+ { index: [cities[0]], delete: [cities[1]] },
132
+ { delete: cities.last(2).map(&:id) }
133
+ ]) }
87
134
  end
88
135
 
89
136
  context do
90
- before { deleted.map { |object| allow(object).to receive_messages(delete_from_index?: false, destroyed?: false) } }
91
- specify { expect(import(deleted)).to eq([{index: deleted}]) }
92
- end
93
- end
94
-
95
- describe '#delete_from_index?' do
96
- before do
97
- stub_model(:city) do
98
- def delete_from_index?
99
- rating == 42
137
+ before do
138
+ City.class_eval do
139
+ def delete_already?
140
+ rating.in?([1, 3])
141
+ end
100
142
  end
101
143
  end
144
+ subject { described_class.new(City, delete_if: ->{ delete_already? }) }
145
+
146
+ specify { expect(import(City.where(nil))).to eq([
147
+ { index: [cities[0]], delete: [cities[1]] }
148
+ ]) }
149
+ specify { expect(import(cities)).to eq([
150
+ { index: [cities[0]], delete: [cities[1]] },
151
+ { delete: cities.last(2) }
152
+ ]) }
153
+ specify { expect(import(cities.map(&:id))).to eq([
154
+ { index: [cities[0]], delete: [cities[1]] },
155
+ { delete: cities.last(2).map(&:id) }
156
+ ]) }
102
157
  end
103
- let!(:cities) { 3.times.map { |i| City.create! } }
104
- let!(:deleted) { 3.times.map { |i| City.create!(rating: 42) } }
105
- subject { described_class.new(City) }
106
-
107
- specify { expect(import(cities, deleted)).to eq([{index: cities, delete: deleted}]) }
108
- specify { expect(import(cities.map(&:id), deleted.map(&:id)))
109
- .to eq([{index: cities, delete: deleted}]) }
110
- specify { expect(import(City.order(:id))).to eq([{index: cities, delete: deleted}]) }
111
158
  end
112
159
 
113
160
  context 'custom primary_key' do
@@ -125,11 +172,13 @@ describe Chewy::Type::Adapter::ActiveRecord, :active_record do
125
172
  specify { expect(import(cities)).to eq([{index: cities}]) }
126
173
  specify { expect(import(cities, batch_size: 2))
127
174
  .to eq([{index: cities.first(2)}, {index: cities.last(1)}]) }
128
- specify { expect(import(cities, deleted)).to eq([{index: cities, delete: deleted}]) }
175
+ specify { expect(import(cities, deleted))
176
+ .to eq([{index: cities}, {delete: deleted}]) }
129
177
  specify { expect(import(cities, deleted, batch_size: 2)).to eq([
130
- {index: cities.first(2)},
131
- {index: cities.last(1), delete: deleted.first(1)},
132
- {delete: deleted.last(2)}]) }
178
+ {index: cities.first(2)},
179
+ {index: cities.last(1)},
180
+ {delete: deleted.first(2)},
181
+ {delete: deleted.last(1)}]) }
133
182
 
134
183
  specify { expect(import(cities.map(&:id))).to eq([{index: cities}]) }
135
184
  specify { expect(import(cities.map(&:id), batch_size: 2))
@@ -150,14 +199,29 @@ describe Chewy::Type::Adapter::ActiveRecord, :active_record do
150
199
 
151
200
  specify { expect(import).to eq([{index: cities.first(3)}]) }
152
201
 
153
- specify { expect(import(City.order(:id))).to eq([{index: cities.first(3)}]) }
154
- specify { expect(import(City.order(:id), batch_size: 2))
202
+ specify { expect(import(City.where('rating < 2')))
203
+ .to eq([{index: cities.first(3)}]) }
204
+ specify { expect(import(City.where('rating < 2'), batch_size: 2))
155
205
  .to eq([{index: cities.first(2)}, {index: [cities[2]]}]) }
206
+ specify { expect(import(City.where('rating < 1')))
207
+ .to eq([{index: cities.first(3)}]) }
208
+ specify { expect(import(City.where('rating > 1'))).to eq([]) }
209
+
210
+ specify { expect(import(cities.first(2)))
211
+ .to eq([{index: cities.first(2)}]) }
212
+ specify { expect(import(cities))
213
+ .to eq([{index: cities.first(3)}, {delete: cities.last(1)}]) }
214
+ specify { expect(import(cities, batch_size: 2))
215
+ .to eq([{index: cities.first(2)}, {index: [cities[2]]}, {delete: cities.last(1)}]) }
216
+ specify { expect(import(cities, deleted))
217
+ .to eq([{index: cities.first(3)}, {delete: cities.last(1) + deleted}]) }
218
+ specify { expect(import(cities, deleted, batch_size: 3)).to eq([
219
+ {index: cities.first(3)},
220
+ {delete: cities.last(1) + deleted.first(2)},
221
+ {delete: deleted.last(1)}]) }
156
222
 
157
- specify { expect(import(cities)).to eq([{index: cities}]) }
158
- specify { expect(import(cities, batch_size: 3))
159
- .to eq([{index: cities.first(3)}, {index: cities.last(1)}]) }
160
-
223
+ specify { expect(import(cities.first(2).map(&:id)))
224
+ .to eq([{index: cities.first(2)}]) }
161
225
  specify { expect(import(cities.map(&:id)))
162
226
  .to eq([{index: cities.first(3)}, {delete: [cities.last.id]}]) }
163
227
  specify { expect(import(cities.map(&:id), batch_size: 2))
@@ -177,7 +241,7 @@ describe Chewy::Type::Adapter::ActiveRecord, :active_record do
177
241
  subject { described_class.new(City) }
178
242
 
179
243
  let(:data_comparer) do
180
- ->(id, data) { object = (data[:index] || data[:delete]).first; (object.respond_to?(:id) ? object.id : object) != id }
244
+ ->(id, data) { objects = data[:index] || data[:delete]; !objects.map { |o| o.respond_to?(:id) ? o.id : o }.include?(id) }
181
245
  end
182
246
 
183
247
  context 'implicit scope' do