Skip to content

Commit 5a973c2

Browse files
sanych-sunrstam
authored andcommitted
CSHARP-4493: Fix incorrect flattening of $expr in $and operator (#1072)
1 parent ad5700b commit 5a973c2

File tree

4 files changed

+190
-136
lines changed

4 files changed

+190
-136
lines changed

src/MongoDB.Driver/FilterDefinitionBuilder.cs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1711,7 +1711,9 @@ public override BsonDocument Render(IBsonSerializer<TDocument> documentSerialize
17111711

17121712
private static void AddClause(BsonDocument document, BsonElement clause)
17131713
{
1714-
if (clause.Name == "$and")
1714+
var fieldOrOperatorName = clause.Name;
1715+
1716+
if (fieldOrOperatorName == "$and")
17151717
{
17161718
// flatten out nested $and
17171719
foreach (var item in (BsonArray)clause.Value)
@@ -1726,10 +1728,12 @@ private static void AddClause(BsonDocument document, BsonElement clause)
17261728
{
17271729
((BsonArray)document[0]).Add(new BsonDocument(clause));
17281730
}
1729-
else if (document.Contains(clause.Name))
1731+
else if (document.Contains(fieldOrOperatorName))
17301732
{
1731-
var existingClause = document.GetElement(clause.Name);
1732-
if (existingClause.Value is BsonDocument existingClauseValue && clause.Value is BsonDocument clauseValue)
1733+
var existingClause = document.GetElement(fieldOrOperatorName);
1734+
if (IsFieldName(fieldOrOperatorName) &&
1735+
existingClause.Value is BsonDocument existingClauseValue &&
1736+
clause.Value is BsonDocument clauseValue)
17331737
{
17341738
var clauseOperator = clauseValue.ElementCount > 0 ? clauseValue.GetElement(0).Name : null;
17351739
if (clauseValue.Names.Any(op => existingClauseValue.Contains(op)) ||
@@ -1751,6 +1755,8 @@ private static void AddClause(BsonDocument document, BsonElement clause)
17511755
{
17521756
document.Add(clause);
17531757
}
1758+
1759+
static bool IsFieldName(string fieldOrOperatorName) => !fieldOrOperatorName.StartsWith("$");
17541760
}
17551761

17561762
private static void PromoteFilterToDollarForm(BsonDocument document, BsonElement clause)

tests/MongoDB.Driver.Tests/FilterDefinitionBuilderTests.cs

Lines changed: 55 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -46,145 +46,68 @@ public void All_Typed()
4646
}
4747

4848
[Fact]
49-
public void And()
49+
public void FilterDefinition_and_operator()
5050
{
5151
var subject = CreateSubject<BsonDocument>();
52-
var filter = subject.And(
53-
subject.Eq("a", 1),
54-
subject.Eq("b", 2));
52+
var filter = subject.Eq("a", 1) & "{ a: 2 }";
5553

56-
Assert(filter, "{a: 1, b: 2}");
57-
}
58-
59-
[Fact]
60-
public void And_with_clashing_keys_should_get_promoted_to_dollar_form()
61-
{
62-
var subject = CreateSubject<BsonDocument>();
63-
var filter = subject.And(
64-
subject.Eq("a", 1),
65-
subject.Eq("a", 2));
66-
67-
Assert(filter, "{$and: [{a: 1}, {a: 2}]}");
68-
}
69-
70-
[Fact]
71-
public void And_with_clashing_keys_but_different_operators_should_get_merged()
72-
{
73-
var subject = CreateSubject<BsonDocument>();
74-
var filter = subject.And(
75-
subject.Gt("a", 1),
76-
subject.Lt("a", 10));
77-
78-
Assert(filter, "{a: {$gt: 1, $lt: 10}}");
54+
Assert(filter, "{ $and : [ { a: 1 }, { a: 2 } ] }");
7955
}
8056

8157
[Theory]
82-
[InlineData("{ geoField : { $geoWithin : { $box : [ [ 1.0, 2.0 ], [ 3.0, 4.0 ] ] } } }", "{ geoField : { $near : [ 5.0, 6.0 ] } }")]
83-
[InlineData("{ geoField : { $near : [ 5.0, 6.0 ] } }", "{ geoField : { $geoWithin : { $box : [ [ 1.0, 2.0 ], [ 3.0, 4.0 ] ] } } }")]
84-
[InlineData("{ geoField : { $nearSphere : { $geometry : { type : 'Point', coordinates : [ 1, 2 ] } } } }", "{ geoField : { $geoIntersects : { $geometry : { type : 'Polygon', coordinates: [ [ [ 1, 2 ], [ 3, 4 ], [ 5, 6 ], [ 7, 8 ] ] ] } } } }")]
85-
[InlineData("{ geoField : { $geoIntersects : { $geometry : { type : 'Polygon', coordinates: [ [ [ 1, 2 ], [ 3, 4 ], [ 5, 6 ], [ 7, 8 ] ] ] } } } }", "{ geoField : { $nearSphere : { $geometry : { type : 'Point', coordinates : [ 1, 2 ] } } } }")]
86-
public void And_with_clashing_keys_and_different_operators_but_with_filters_that_support_only_dollar_form_should_get_promoted_to_dollar_form(string firstFilter, string secondFilter)
87-
{
88-
var subject = CreateSubject<BsonDocument>();
89-
90-
var combinedFilter = subject.And(firstFilter, secondFilter);
91-
92-
Assert(combinedFilter, $"{{ $and : [ {firstFilter}, {secondFilter} ] }}");
93-
}
94-
95-
[Fact]
96-
public void And_with_clashing_keys_and_different_operators_but_with_filters_that_support_only_dollar_form_and_empty_filter_should_ignore_empty_filter()
97-
{
98-
var subject = CreateSubject<BsonDocument>();
99-
100-
var combinedFilter = subject.And(
101-
"{ geoField : { $near : [ 5.0, 6.0 ] } }",
102-
"{ geoField : { } }");
103-
104-
Assert(combinedFilter, "{ geoField : { $near : [ 5.0, 6.0 ] } }");
105-
}
106-
107-
[Fact]
108-
public void And_with_an_empty_filter()
109-
{
110-
var subject = CreateSubject<BsonDocument>();
111-
var filter = subject.And(
112-
"{}",
113-
subject.Eq("a", 10));
114-
115-
Assert(filter, "{a: 10}");
116-
}
117-
118-
[Fact]
119-
public void And_with_a_nested_and_should_get_flattened()
120-
{
121-
var subject = CreateSubject<BsonDocument>();
122-
var filter = subject.And(
123-
subject.And("{a: 1}", new BsonDocument("b", 2)),
124-
subject.Eq("c", 3));
125-
126-
Assert(filter, "{a: 1, b: 2, c: 3}");
127-
}
128-
129-
[Fact]
130-
public void And_with_a_nested_and_and_clashing_keys()
131-
{
132-
var subject = CreateSubject<BsonDocument>();
133-
var filter = subject.And(
134-
subject.And(subject.Eq("a", 1), subject.Eq("a", 2)),
135-
subject.Eq("c", 3));
136-
137-
Assert(filter, "{$and: [{a: 1}, {a: 2}, {c: 3}]}");
138-
}
139-
140-
[Fact]
141-
public void And_with_a_nested_and_and_clashing_operators_on_the_same_key()
142-
{
143-
var subject = CreateSubject<BsonDocument>();
144-
var filter = subject.Lt("a", 1) & subject.Lt("a", 2);
145-
146-
Assert(filter, "{$and: [{a: {$lt: 1}}, {a: {$lt: 2}}]}");
147-
}
148-
149-
[Fact]
150-
public void And_with_a_nested_and_and_clashing_keys_using_ampersand()
151-
{
152-
var subject = CreateSubject<BsonDocument>();
153-
var filter = subject.Eq("a", 1) & "{a: 2}" & new BsonDocument("c", 3);
154-
155-
Assert(filter, "{$and: [{a: 1}, {a: 2}, {c: 3}]}");
156-
}
157-
158-
[Fact]
159-
public void And_with_no_clauses()
160-
{
161-
var subject = CreateSubject<BsonDocument>();
162-
163-
var filter = subject.And();
164-
165-
Assert(filter, "{ $and : [] }");
166-
}
167-
168-
[Fact]
169-
public void And_with_one_empty_clause()
170-
{
171-
var subject = CreateSubject<BsonDocument>();
172-
var empty = Builders<BsonDocument>.Filter.Empty;
173-
174-
var filter = subject.And(empty);
58+
[InlineData("{ $and : [] }")]
59+
[InlineData("{}", "{}")]
60+
[InlineData("{}", "{}", "{}")]
61+
[InlineData("{ a : 10 }", "{}", "{ a : 10 }")]
62+
[InlineData("{ a : 10 }", "{ a : 10 }", "{}")]
63+
[InlineData("{ a : 1, b : 2, c : 3 }", "{ a : 1 }", "{ b : 2 }", "{ c : 3 }")]
64+
[InlineData("{ a : 1, b : 2, c : 3 }", "{ a : 1 }", "{ $and: [{ b : 2 }, { c : 3 }] }")]
65+
[InlineData("{ a : { $gt : 1, $lt : 10 } }", "{ a : { $gt : 1 } }", "{ a : { $lt : 10 } }")]
66+
[InlineData("{ $and : [{ a : { $lt : 1 } }, { a : { $lt : 2 } }] }", "{ a : { $lt : 1 } }", "{ a : { $lt : 2 } }")]
67+
[InlineData("{ $and : [{ a : 1 }, { a : 2 }] }", "{ a : 1 }", "{ a : 2 }")]
68+
[InlineData("{ $and : [{ a : 1 }, { a : 2 }, { c : 3 }] }", "{ a : 1 }", "{ a : 2 }", "{ c : 3 }")]
69+
[InlineData("{ $and : [{ c : 3 }, { a : 1 }, { a : 2 }] }", "{ c : 3 }", "{ a : 1 }", "{ a : 2 }")]
70+
[InlineData("{ $and : [{ a : 1 }, { a : 2 }, { c : 3 }] }", "{ $and : [{ a : 1 }, { a : 2 }] }", "{ c : 3}")]
71+
[InlineData("{ $and : [{ a : 1 }, { a : 2 }, { c : 3 }] }", "{ $and : [{ a : 1 }, { $and : [{ a : 2 }] }] }", "{ c : 3 }")]
72+
[InlineData("{ $and : [{ a : 1 }, { a : 2 }, { c : 3 }] }", "{ a : 1 }", "{ $and : [{ a : 2 }, { c : 3 }] }")]
73+
74+
[InlineData("{ geoField : { $near : [5.0, 6.0] } }", "{ geoField : { $near : [5.0, 6.0] } }", "{}")]
75+
[InlineData(
76+
"{ $and : [{ geoField : { $near : [40.0, 18.0] } }, { geoField : { $near : [42.0, 10.0] } }] }",
77+
"{ geoField: { $near : [40.0, 18.0] } }",
78+
"{ geoField : { $near : [42.0, 10.0] } }")]
79+
[InlineData(
80+
"{ $and : [{ geoField : { $geoWithin : { $box : [[1.0, 2.0], [3.0, 4.0]] } } }, { geoField : { $near : [5.0, 6.0] } }] }",
81+
"{ geoField : { $geoWithin : { $box : [[1.0, 2.0], [3.0, 4.0]] } } }",
82+
"{ geoField : { $near : [5.0, 6.0] } }")]
83+
[InlineData(
84+
"{ $and : [{ geoField : { $near : [5.0, 6.0] } }, { geoField : { $geoWithin : { $box : [[1.0, 2.0], [3.0, 4.0]] } } }] }",
85+
"{ geoField : { $near : [5.0, 6.0] } }",
86+
"{ geoField : { $geoWithin : { $box : [[1.0, 2.0], [3.0, 4.0]] } } }")]
87+
[InlineData(
88+
"{ $and : [{ geoField : { $nearSphere : { $geometry : { type : 'Point', coordinates : [1, 2] } } } }, { geoField : { $geoIntersects : { $geometry : { type : 'Polygon', coordinates: [[[1, 2], [3, 4], [5, 6], [7, 8]]] } } } }] }",
89+
"{ geoField : { $nearSphere : { $geometry : { type : 'Point', coordinates : [1, 2] } } } }",
90+
"{ geoField : { $geoIntersects : { $geometry : { type : 'Polygon', coordinates: [[[1, 2], [3, 4], [5, 6], [7, 8]]] } } } }")]
91+
[InlineData(
92+
"{ $and : [{ geoField : { $geoIntersects : { $geometry : { type : 'Polygon', coordinates: [[[1, 2], [3, 4], [5, 6], [7, 8]]] } } } }, { geoField : { $nearSphere : { $geometry : { type : 'Point', coordinates : [1, 2] } } } }] }",
93+
"{ geoField : { $geoIntersects : { $geometry : { type : 'Polygon', coordinates: [[[1, 2], [3, 4], [5, 6], [7, 8]]] } } } }",
94+
"{ geoField : { $nearSphere : { $geometry : { type : 'Point', coordinates : [1, 2] } } } }")]
95+
96+
[InlineData("{ a : 1 , $expr : { $eq : ['$_id', 1] } }", "{ a : 1 }", "{ $expr : { $eq : ['$_id', 1] } }")]
97+
[InlineData("{ $expr : { $eq : ['$_id', 1] }, a: 1 }", "{ $expr : { $eq : ['$_id', 1] } }", "{ a: 1 }")]
98+
[InlineData("{ $expr : { $eq : ['$_id', 1] } }", "{ $expr : { $eq : ['$_id', 1] } }", "{}")]
99+
[InlineData(
100+
"{ $and : [{ $expr : { $eq : ['$_id', 1] } }, { $expr : { $ne : ['$a', ''] } }] }",
101+
"{ $expr : { $eq : ['$_id', 1] } }",
102+
"{ $expr : { $ne : ['$a', ''] } }")]
103+
public void And(string expected, params string[] clauses)
104+
{
105+
var subject = CreateSubject<BsonDocument>();
106+
var args = clauses.Select(c => new JsonFilterDefinition<BsonDocument>(c));
107+
108+
var filter = subject.And(args);
175109

176-
Assert(filter, "{ }");
177-
}
178-
179-
[Fact]
180-
public void And_with_two_empty_clauses()
181-
{
182-
var subject = CreateSubject<BsonDocument>();
183-
var empty = Builders<BsonDocument>.Filter.Empty;
184-
185-
var filter = subject.And(empty, empty);
186-
187-
Assert(filter, "{ }");
110+
Assert(filter, expected);
188111
}
189112

190113
[Fact]
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
/* Copyright 2010-present MongoDB Inc.
2+
*
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
using System;
17+
using System.Collections.Generic;
18+
using System.Linq;
19+
using System.Linq.Expressions;
20+
using FluentAssertions;
21+
using Xunit;
22+
23+
namespace MongoDB.Driver.Tests.Linq.Linq3ImplementationTests.Jira
24+
{
25+
public class CSharp4493Tests : Linq3IntegrationTest
26+
{
27+
[Fact]
28+
public void Find_with_predicate_should_work()
29+
{
30+
var collection = CreateCollection();
31+
string[] emails = { "[email protected]" };
32+
string[] addresses = { "495 pacific street plymouth, ma 02360" };
33+
Expression<Func<Customer, bool>> filterByAddressAndEmails =
34+
c => c.Emails.Any(ce => emails.Contains(ce.Email.ToLower())) &&
35+
addresses.Contains(c.Address.FullAddress.ToLower());
36+
FilterDefinition<Customer> emptyPredicate = Builders<Customer>.Filter.Empty;
37+
38+
var find = collection.Find(filterByAddressAndEmails & emptyPredicate);
39+
40+
var translatedFilter = TranslateFindFilter(collection, find);
41+
translatedFilter.Should().Be("{$and: [{$expr: {$anyElementTrue: {$map: {input: '$Emails', as: 'ce', in: {$in: [{$toLower: '$$ce.Email'}, ['[email protected]']]}}}}}, {$expr: {$in: [{$toLower: '$Address.FullAddress'}, ['495 pacific street plymouth, ma 02360']]}}]}");
42+
43+
var results = find.ToList();
44+
Assert.Equal(1, results.Count);
45+
results.Select(x => x.Id).Should().Equal(2);
46+
}
47+
48+
private IMongoCollection<Customer> CreateCollection()
49+
{
50+
var collection = GetCollection<Customer>("C");
51+
52+
CreateCollection(
53+
collection,
54+
new Customer {
55+
Id = 1,
56+
Address = new AddressMetadata
57+
{
58+
FullAddress = "111 atlantic street plymouth, ma 02345"
59+
},
60+
Emails = new[]
61+
{
62+
new EmailMetadata
63+
{
64+
65+
}
66+
}
67+
},
68+
new Customer
69+
{
70+
Id = 2,
71+
Address = new AddressMetadata
72+
{
73+
FullAddress = "495 pacific street plymouth, ma 02360"
74+
},
75+
Emails = new[]
76+
{
77+
new EmailMetadata
78+
{
79+
80+
}
81+
}
82+
},
83+
new Customer
84+
{
85+
Id = 3,
86+
Address = new AddressMetadata
87+
{
88+
FullAddress = "495 pacific street plymouth, ma 02360"
89+
},
90+
Emails = new[]
91+
{
92+
new EmailMetadata
93+
{
94+
95+
}
96+
}
97+
});
98+
99+
return collection;
100+
}
101+
102+
public class Customer
103+
{
104+
public int Id { get; set; }
105+
public AddressMetadata Address { get; set; }
106+
public IList<EmailMetadata> Emails { get; set; }
107+
}
108+
109+
public sealed record AddressMetadata
110+
{
111+
public string FullAddress { get; set; }
112+
}
113+
114+
public sealed record EmailMetadata
115+
{
116+
public string Email { get; set; }
117+
}
118+
}
119+
}

tests/MongoDB.Driver.Tests/Linq/Linq3ImplementationTests/Linq3IntegrationTest.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,12 @@ protected BsonDocument TranslateFilter<TDocument>(IMongoCollection<TDocument> co
168168
return filter.Render(documentSerializer, serializerRegistry, linqProvider);
169169
}
170170

171+
protected BsonDocument TranslateFindFilter<TDocument, TProjection>(IMongoCollection<TDocument> collection, IFindFluent<TDocument, TProjection> find)
172+
{
173+
var filterDefinition = ((FindFluent<TDocument, TProjection>)find).Filter;
174+
return filterDefinition.Render(collection.DocumentSerializer, BsonSerializer.SerializerRegistry, LinqProvider.V3);
175+
}
176+
171177
protected BsonDocument TranslateFindProjection<TDocument, TProjection>(
172178
IMongoCollection<TDocument> collection,
173179
IFindFluent<TDocument, TProjection> find)

0 commit comments

Comments
 (0)