Skip to content

Commit 5545c17

Browse files
p-mongop
andauthored
Fix MONGOID-5020 Condition lifting breaks with symbol operators on Ruby <= 2.6 (#4915)
* MONGOID-5020 test cases for operators with symbols * MONGOID-5020 convert hashes to indifferent access prior to query manipulation * MONGOID-5020 queries are now more uniform * MONGOID-5020 combine with $eq when conditions are given in the same argument * MONGOID-5020 test all matrix of string/symbol operator types * fix straggler test * document the situation * document $eq lifting Co-authored-by: Oleg Pudeyev <[email protected]>
1 parent b4fa616 commit 5545c17

File tree

5 files changed

+250
-22
lines changed

5 files changed

+250
-22
lines changed

docs/tutorials/mongoid-upgrade.txt

Lines changed: 154 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,123 @@ please consult GitHub releases for detailed release notes and JIRA for
1818
the complete list of issues fixed in each release, including bug fixes.
1919

2020

21+
Upgrading to Mongoid 7.3
22+
========================
23+
24+
The following sections describe significant changes in Mongoid 7.3.
25+
26+
Field Operator Stringification
27+
------------------------------
28+
29+
Minor change: the ``and`` logical operator now stringifies field operators
30+
in its arguments. Mongoid 7.3 behavior:
31+
32+
.. code-block:: ruby
33+
34+
Band.and(year: {'$in': [2020]})
35+
# =>
36+
# #<Mongoid::Criteria
37+
# selector: {"year"=>{"$in"=>[2020]}}
38+
# options: {}
39+
# class: Band
40+
# embedded: false>
41+
42+
Mongoid 7.2 behavior:
43+
44+
.. code-block:: ruby
45+
46+
Band.and(year: {'$in': [2020]})
47+
# =>
48+
# #<Mongoid::Criteria
49+
# selector: {"year"=>{:$in=>[2020]}}
50+
# options: {}
51+
# class: Band
52+
# embedded: false>
53+
54+
Note that this stringification does not yet happen for all query construction
55+
paths. For example, ``where`` does not stringify operators in both Mongoid 7.3
56+
and Mongoid 7.2:
57+
58+
.. code-block:: ruby
59+
60+
Band.where(year: {'$in': [2020]})
61+
# =>
62+
# #<Mongoid::Criteria
63+
# selector: {"year"=>{:$in=>[2020]}}
64+
# options: {}
65+
# class: Band
66+
# embedded: false>
67+
68+
It is expected that over time, all operators will stringify the keys in their
69+
arguments.
70+
71+
72+
Condition Combination Using ``$eq``
73+
-----------------------------------
74+
75+
Minor change: when using the ``and`` method on ``Criteria`` objects and
76+
providing multiple conditions on the same field in the same argument to
77+
``and``, conditions may be combined using ``$eq`` instead of ``$and``.
78+
Mongoid 7.3 behavior:
79+
80+
.. code-block:: ruby
81+
82+
Band.and(year: 2020, :year.gt => 1960)
83+
# =>
84+
# #<Mongoid::Criteria
85+
# selector: {"year"=>{"$eq"=>2020, "$gt"=>1960}}
86+
# options: {}
87+
# class: Band
88+
# embedded: false>
89+
90+
Band.and(:year.gt => 1960, year: 2020)
91+
# =>
92+
# #<Mongoid::Criteria
93+
# selector: {"year"=>{"$gt"=>1960, "$eq"=>2020}}
94+
# options: {}
95+
# class: Band
96+
# embedded: false>
97+
98+
Mongoid 7.2 behavior:
99+
100+
.. code-block:: ruby
101+
102+
Band.and(year: 2020, :year.gt => 1960)
103+
# =>
104+
# #<Mongoid::Criteria
105+
# selector: {"year"=>2020, "$and"=>[{"year"=>{"$gt"=>1960}}]}
106+
# options: {}
107+
# class: Band
108+
# embedded: false>
109+
110+
Band.and(:year.gt => 1960, year: 2020)
111+
# =>
112+
# #<Mongoid::Criteria
113+
# selector: {"year"=>{"$gt"=>1960}, "$and"=>[{"year"=>2020}]}
114+
# options: {}
115+
# class: Band
116+
# embedded: false>
117+
118+
This combination is not yet performed for ``where`` and other query methods:
119+
120+
.. code-block:: ruby
121+
122+
Band.where(:year.gt => 1960, year: 2020)
123+
# =>
124+
# #<Mongoid::Criteria
125+
# selector: {"year"=>{"$gt"=>1960}, "$and"=>[{"year"=>2020}]}
126+
# options: {}
127+
# class: Band
128+
# embedded: false>
129+
130+
Tt is expected that in the future other query methods will also favor
131+
using ``$eq`` over ``$and``.
132+
133+
21134
Upgrading to Mongoid 7.2
22135
========================
23136

24-
The following sections describe major changes in Mongoid 7.2.
137+
The following sections describe significant changes in Mongoid 7.2.
25138

26139
Embedded Document Matching
27140
--------------------------
@@ -416,7 +529,7 @@ of the legacy query cache, see :ref:`the query cache documentation <query-cache>
416529
Upgrading to Mongoid 7.1
417530
========================
418531

419-
The following sections describe major changes in Mongoid 7.1.
532+
The following sections describe significant changes in Mongoid 7.1.
420533

421534
Condition Combination
422535
---------------------
@@ -448,6 +561,45 @@ Corresponding Mongoid 7.0 behavior:
448561
# class: Band
449562
# embedded: false>
450563

564+
**Known issue:** When using Ruby 2.6 and lower, when adding multiple conditions
565+
on the same field using the same operator, the operator must be given as a
566+
string, not as a symbol. The following invocations fail:
567+
568+
.. code-block:: ruby
569+
570+
Band.and({year: {'$in': [2020]}}, {year: {'$in': [2020]}})
571+
# Traceback (most recent call last):
572+
# 2: from (irb):10
573+
# 1: from (irb):10:in `rescue in irb_binding'
574+
# NoMethodError (undefined method `start_with?' for :$in:Symbol)
575+
576+
Band.and(year: {'$in': [2020]}).and(year: {'$in': [2020]})
577+
# Traceback (most recent call last):
578+
# 2: from (irb):11
579+
# 1: from (irb):11:in `rescue in irb_binding'
580+
# NoMethodError (undefined method `start_with?' for :$in:Symbol)
581+
582+
Use string keys instead:
583+
584+
.. code-block:: ruby
585+
586+
# Band.and({year: {'$in' => [2020]}}, {year: {'$in' => [2020]}})
587+
# => #<Mongoid::Criteria
588+
# selector: {"year"=>{"$in"=>[2020]}, "$and"=>[{"year"=>{"$in"=>[2020]}}]}
589+
# options: {}
590+
# class: Band
591+
# embedded: false>
592+
593+
Band.and(year: {'$in' => [2020]}).and(year: {'$in' => [2020]})
594+
# => #<Mongoid::Criteria
595+
# selector: {"year"=>{"$in"=>[2020]}, "$and"=>[{"year"=>{"$in"=>[2020]}}]}
596+
# options: {}
597+
# class: Band
598+
# embedded: false>
599+
600+
This issue is rectified in Mongoid 7.3.
601+
602+
451603
Logical Operations
452604
------------------
453605

lib/mongoid/criteria/queryable/mergeable.rb

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,8 @@ def __multi__(criteria, operator)
227227
end
228228

229229
# Takes a criteria hash and expands Key objects into hashes containing
230-
# MQL corresponding to said key objects.
230+
# MQL corresponding to said key objects. Also converts the input to
231+
# BSON::Document to permit indifferent access.
231232
#
232233
# Ruby does not permit multiple symbol operators. For example,
233234
# {:foo.gt => 1, :foo.gt => 2} is collapsed to {:foo.gt => 2} by the
@@ -237,15 +238,19 @@ def __multi__(criteria, operator)
237238
# Similarly, this method should never need to expand a literal value
238239
# and an operator at the same time.
239240
#
241+
# This method effectively converts symbol keys to string keys in
242+
# the input +expr+, such that the downstream code can assume that
243+
# conditions always contain string keys.
244+
#
240245
# @param [ Hash ] expr Criteria including Key instances.
241246
#
242-
# @return [ Hash ] The expanded criteria.
247+
# @return [ BSON::Document ] The expanded criteria.
243248
private def _mongoid_expand_keys(expr)
244249
unless expr.is_a?(Hash)
245250
raise ArgumentError, 'Argument must be a Hash'
246251
end
247252

248-
result = {}
253+
result = BSON::Document.new
249254
expr.each do |field, value|
250255
field.__expr_part__(value.__expand_complex__).each do |k, v|
251256
if result[k]

spec/integration/matcher_operator_data/in.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,3 +192,19 @@
192192
end: 20
193193
excl: false
194194
error: true
195+
196+
- name: scalar field - symbol operator - matches
197+
document:
198+
count: 10
199+
query:
200+
count:
201+
:$in: [10, 11]
202+
matches: true
203+
204+
- name: scalar field - symbol operator - does not match
205+
document:
206+
count: 8
207+
query:
208+
count:
209+
:$in: [10, 11]
210+
matches: false

spec/mongoid/criteria/queryable/mergeable_spec.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444

4545
describe '#_mongoid_expand_keys' do
4646
it 'expands simple keys' do
47-
query.send(:_mongoid_expand_keys, {a: 1}).should == {a: 1}
47+
query.send(:_mongoid_expand_keys, {a: 1}).should == {'a' => 1}
4848
end
4949

5050
let(:gt) do

spec/mongoid/criteria/queryable/selectable_logical_spec.rb

Lines changed: 71 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,40 @@
417417
it_behaves_like 'returns a cloned query'
418418
end
419419

420+
context 'when criteria use operators' do
421+
shared_examples 'behave correctly' do
422+
let(:selection) do
423+
query.and(
424+
{ field: {first_operator => [ 1, 2 ] }},
425+
{ field: {second_operator => [ 3, 4 ] }},
426+
)
427+
end
428+
429+
it "combines via $and operator and stringifies all keys" do
430+
expect(selection.selector).to eq({
431+
"field" => {'$in' => [ 1, 2 ]},
432+
"$and" => [
433+
{ "field" => {'$in' => [ 3, 4 ] }}
434+
]
435+
})
436+
end
437+
end
438+
439+
[
440+
['$in', '$in'],
441+
[:$in, '$in'],
442+
['$in', :$in],
443+
[:$in, :$in],
444+
].each do |first_operator, second_operator|
445+
context "when first operator is #{first_operator.inspect} and second operator is #{second_operator.inspect}" do
446+
let(:first_operator) { first_operator }
447+
let(:second_operator) { second_operator }
448+
449+
include_examples 'behave correctly'
450+
end
451+
end
452+
end
453+
420454
context 'when criteria are handled via Key' do
421455
shared_examples_for 'adds the conditions to top level' do
422456

@@ -467,12 +501,23 @@
467501
it_behaves_like 'returns a cloned query'
468502
end
469503

504+
shared_examples_for 'combines conditions with $eq' do
505+
506+
it "combines conditions with $eq" do
507+
expect(selection.selector).to eq({
508+
"field" => {'$eq' => 3, '$lt' => 5},
509+
})
510+
end
511+
512+
it_behaves_like 'returns a cloned query'
513+
end
514+
470515
context 'criteria are provided in the same hash' do
471516
let(:selection) do
472517
query.send(tested_method, :field => 3, :field.lt => 5)
473518
end
474519

475-
it_behaves_like 'combines conditions with $and'
520+
it_behaves_like 'combines conditions with $eq'
476521
end
477522

478523
context 'criteria are provided in separate hashes' do
@@ -505,12 +550,23 @@
505550
it_behaves_like 'returns a cloned query'
506551
end
507552

553+
shared_examples_for 'combines conditions with $eq' do
554+
555+
it "combines conditions with $eq" do
556+
expect(selection.selector).to eq({
557+
"field" => {'$gt' => 3, '$eq' => 5},
558+
})
559+
end
560+
561+
it_behaves_like 'returns a cloned query'
562+
end
563+
508564
context 'criteria are provided in the same hash' do
509565
let(:selection) do
510566
query.send(tested_method, :field.gt => 3, :field => 5)
511567
end
512568

513-
it_behaves_like 'combines conditions with $and'
569+
it_behaves_like 'combines conditions with $eq'
514570
end
515571

516572
context 'criteria are provided in separate hashes' do
@@ -1270,14 +1326,14 @@
12701326
it_behaves_like 'returns a cloned query'
12711327
end
12721328

1273-
shared_examples_for 'adds one condition' do
1329+
shared_examples_for 'combines conditions with $eq' do
12741330

1275-
it "adds one condition" do
1331+
it "combines conditions with $eq" do
12761332
expect(selection.selector).to eq({
1277-
'field' => 3,
1278-
'$and' => [
1279-
{'field' => {'$lt' => 5}},
1280-
],
1333+
'field' => {
1334+
'$eq' => 3,
1335+
'$lt' => 5,
1336+
},
12811337
})
12821338
end
12831339

@@ -1289,7 +1345,7 @@
12891345
query.send(tested_method, :field => 3, :field.lt => 5)
12901346
end
12911347

1292-
it_behaves_like 'adds one condition'
1348+
it_behaves_like 'combines conditions with $eq'
12931349
end
12941350

12951351
context 'criteria are provided in separate hashes' do
@@ -1324,13 +1380,12 @@
13241380
it_behaves_like 'returns a cloned query'
13251381
end
13261382

1327-
shared_examples_for 'adds one condition' do
1383+
shared_examples_for 'combines conditions with $eq' do
13281384

1329-
it "adds one condition" do
1330-
expect(selection.selector).to eq({
1331-
'field' => {'$gt' => 3},
1332-
'$and' => ['field' => 5],
1333-
})
1385+
it "combines conditions with $eq" do
1386+
expect(selection.selector).to eq(
1387+
'field' => {'$gt' => 3, '$eq' => 5},
1388+
)
13341389
end
13351390

13361391
it_behaves_like 'returns a cloned query'
@@ -1341,7 +1396,7 @@
13411396
query.send(tested_method, :field.gt => 3, :field => 5)
13421397
end
13431398

1344-
it_behaves_like 'adds one condition'
1399+
it_behaves_like 'combines conditions with $eq'
13451400
end
13461401

13471402
context 'criteria are provided in separate hashes' do

0 commit comments

Comments
 (0)