chewy 0.6.2 → 0.7.0

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