Skip to content

Commit 7cbae1e

Browse files
rstamDmitryLukyanov
authored andcommitted
CSHARP-4500: Move some simplifications from Render to AstSimplifier.
1 parent 5e27219 commit 7cbae1e

File tree

10 files changed

+388
-34
lines changed

10 files changed

+388
-34
lines changed

src/MongoDB.Driver/Linq/Linq3Implementation/Ast/AstNodeType.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ internal enum AstNodeType
7575
GetFieldExpression,
7676
GraphLookupStage,
7777
GroupStage,
78+
ImpliedOperationFilterOperation,
7879
IndexOfArrayExpression,
7980
IndexOfBytesExpression,
8081
IndexOfCPExpression,

src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Filters/AstFieldOperationFilter.cs

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -66,31 +66,6 @@ public override BsonValue Render()
6666
fieldPath = fieldPath.Substring(8);
6767
}
6868

69-
if (_operation is AstComparisonFilterOperation comparisonOperation &&
70-
comparisonOperation.Operator == AstComparisonFilterOperator.Eq &&
71-
comparisonOperation.Value.BsonType != BsonType.RegularExpression)
72-
{
73-
return new BsonDocument(fieldPath, comparisonOperation.Value); // implied $eq
74-
}
75-
76-
if (
77-
_operation is AstElemMatchFilterOperation elemMatchOperation &&
78-
elemMatchOperation.Filter is AstFieldOperationFilter fieldOperationFilter &&
79-
fieldOperationFilter.Field.Path == "@<elem>")
80-
{
81-
if (fieldOperationFilter.Operation is AstComparisonFilterOperation elemMatchComparisonOperation &&
82-
elemMatchComparisonOperation.Operator == AstComparisonFilterOperator.Eq &&
83-
elemMatchComparisonOperation.Value.BsonType != BsonType.RegularExpression)
84-
{
85-
return new BsonDocument(fieldPath, elemMatchComparisonOperation.Value); // implied $elemMatch with $eq
86-
}
87-
88-
if (fieldOperationFilter.Operation is AstRegexFilterOperation elemMatchRegexOperation)
89-
{
90-
return new BsonDocument(fieldPath, elemMatchRegexOperation.Render()); // implied $elemMatch with $regex
91-
}
92-
}
93-
9469
return new BsonDocument(fieldPath, _operation.Render());
9570
}
9671

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
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 MongoDB.Bson;
17+
using MongoDB.Driver.Core.Misc;
18+
using MongoDB.Driver.Linq.Linq3Implementation.Ast.Visitors;
19+
20+
namespace MongoDB.Driver.Linq.Linq3Implementation.Ast.Filters
21+
{
22+
internal sealed class AstImpliedOperationFilterOperation : AstFilterOperation
23+
{
24+
private readonly BsonValue _value;
25+
26+
public AstImpliedOperationFilterOperation(BsonValue value)
27+
{
28+
_value = Ensure.IsNotNull(value, nameof(value));
29+
}
30+
31+
public override AstNodeType NodeType => AstNodeType.ImpliedOperationFilterOperation;
32+
public BsonValue Value => _value;
33+
34+
public override AstNode Accept(AstNodeVisitor visitor)
35+
{
36+
return visitor.VisitImpliedOperationFilterOperation(this);
37+
}
38+
39+
public override BsonValue Render()
40+
{
41+
return _value;
42+
}
43+
44+
public AstImpliedOperationFilterOperation Update(BsonValue value)
45+
{
46+
if (value == _value)
47+
{
48+
return this;
49+
}
50+
51+
return new AstImpliedOperationFilterOperation(value);
52+
}
53+
}
54+
}

src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Filters/AstRegexFilterOperation.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ internal sealed class AstRegexFilterOperation : AstFilterOperation
2727
public AstRegexFilterOperation(string pattern, string options)
2828
{
2929
_pattern = Ensure.IsNotNull(pattern, nameof(pattern));
30-
_options = Ensure.IsNotNull(options, nameof(options));
30+
_options = options;
3131
}
3232

3333
public override AstNodeType NodeType => AstNodeType.RegexFilterOperation;
@@ -41,7 +41,11 @@ public override AstNode Accept(AstNodeVisitor visitor)
4141

4242
public override BsonValue Render()
4343
{
44-
return new BsonRegularExpression(_pattern, _options);
44+
return new BsonDocument
45+
{
46+
{ "$regex", _pattern },
47+
{ "$options", _options, _options != null },
48+
};
4549
}
4650
}
4751
}

src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Optimizers/AstSimplifier.cs

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,205 @@ public static TNode SimplifyAndConvert<TNode>(TNode node)
3737
}
3838
#endregion
3939

40+
public override AstNode VisitFieldOperationFilter(AstFieldOperationFilter node)
41+
{
42+
node = (AstFieldOperationFilter)base.VisitFieldOperationFilter(node);
43+
44+
if (node.Field.Path != "@<elem>")
45+
{
46+
// { field : { $eq : value } } => { field : value } where value is not a regex
47+
if (IsFieldEqValue(node, out var value))
48+
{
49+
var impliedOperation = new AstImpliedOperationFilterOperation(value);
50+
return new AstFieldOperationFilter(node.Field, impliedOperation);
51+
}
52+
53+
// { field : { $regex : "pattern", $options : "options" } } => { field : /pattern/options }
54+
if (IsFieldRegex(node, out var regex))
55+
{
56+
var impliedOperation = new AstImpliedOperationFilterOperation(regex);
57+
return new AstFieldOperationFilter(node.Field, impliedOperation);
58+
}
59+
60+
// { field : { $not : { $regex : "pattern", $options : "options" } } } => { field : { $not : /pattern/options } }
61+
if (IsFieldNotRegex(node, out regex))
62+
{
63+
var notImpliedOperation = new AstNotFilterOperation(new AstImpliedOperationFilterOperation(regex));
64+
return new AstFieldOperationFilter(node.Field, notImpliedOperation);
65+
}
66+
67+
// { field : { $elemMatch : { $eq : value } } } => { field : value } where value is not regex
68+
if (IsFieldElemMatchEqValue(node, out value))
69+
{
70+
var impliedOperation = new AstImpliedOperationFilterOperation(value);
71+
return new AstFieldOperationFilter(node.Field, impliedOperation);
72+
}
73+
74+
// { field : { $elemMatch : { $regex : "pattern", $options : "options" } } } => { field : /pattern/options }
75+
if (IsFieldElemMatchRegex(node, out regex))
76+
{
77+
var impliedOperation = new AstImpliedOperationFilterOperation(regex);
78+
return new AstFieldOperationFilter(node.Field, impliedOperation);
79+
}
80+
81+
// { field : { $elemMatch : { $not : { $regex : "pattern", $options : "options" } } } } => { field : { $elemMatch : { $not : /pattern/options } } }
82+
if (IsFieldElemMatchNotRegex(node, out var elemField, out regex))
83+
{
84+
var notRegexOperation = new AstNotFilterOperation(new AstImpliedOperationFilterOperation(regex));
85+
var elemFilter = new AstFieldOperationFilter(elemField, notRegexOperation);
86+
var elemMatchOperation = new AstElemMatchFilterOperation(elemFilter);
87+
return new AstFieldOperationFilter(node.Field, elemMatchOperation);
88+
}
89+
90+
// { field : { $not : { $elemMatch : { $eq : value } } } } => { field : { $ne : value } } where value is not regex
91+
if (IsFieldNotElemMatchEqValue(node, out value))
92+
{
93+
var impliedOperation = new AstComparisonFilterOperation(AstComparisonFilterOperator.Ne, value);
94+
return new AstFieldOperationFilter(node.Field, impliedOperation);
95+
}
96+
97+
// { field : { $not : { $elemMatch : { $regex : "pattern", $options : "options" } } } } => { field : { $not : /pattern/options } }
98+
if (IsFieldNotElemMatchRegex(node, out regex))
99+
{
100+
var notImpliedOperation = new AstNotFilterOperation(new AstImpliedOperationFilterOperation(regex));
101+
return new AstFieldOperationFilter(node.Field, notImpliedOperation);
102+
}
103+
}
104+
105+
return node;
106+
107+
static bool IsFieldEqValue(AstFieldOperationFilter node, out BsonValue value)
108+
{
109+
// { field : { $eq : value } } where value is not a a regex
110+
if (node.Operation is AstComparisonFilterOperation comparisonOperation &&
111+
comparisonOperation.Operator == AstComparisonFilterOperator.Eq &&
112+
comparisonOperation.Value.BsonType != BsonType.RegularExpression)
113+
{
114+
value = comparisonOperation.Value;
115+
return true;
116+
}
117+
118+
value = null;
119+
return false;
120+
}
121+
122+
static bool IsFieldRegex(AstFieldOperationFilter node, out BsonRegularExpression regex)
123+
{
124+
// { field : { $regex : "pattern", $options : "options" } }
125+
if (node.Operation is AstRegexFilterOperation regexOperation)
126+
{
127+
regex = new BsonRegularExpression(regexOperation.Pattern, regexOperation.Options);
128+
return true;
129+
}
130+
131+
regex = null;
132+
return false;
133+
}
134+
135+
static bool IsFieldNotRegex(AstFieldOperationFilter node, out BsonRegularExpression regex)
136+
{
137+
// { field : { $not : { $regex : "pattern", $options : "options" } } }
138+
if (node.Operation is AstNotFilterOperation notOperation &&
139+
notOperation.Operation is AstRegexFilterOperation regexOperation)
140+
{
141+
regex = new BsonRegularExpression(regexOperation.Pattern, regexOperation.Options);
142+
return true;
143+
}
144+
145+
regex = null;
146+
return false;
147+
}
148+
149+
static bool IsFieldElemMatchEqValue(AstFieldOperationFilter node, out BsonValue value)
150+
{
151+
// { field : { $elemMatch : { $eq : value } } } where value is not regex
152+
if (node.Operation is AstElemMatchFilterOperation elemMatchOperation &&
153+
elemMatchOperation.Filter is AstFieldOperationFilter elemFilter &&
154+
elemFilter.Field.Path == "@<elem>" &&
155+
elemFilter.Operation is AstComparisonFilterOperation comparisonOperation &&
156+
comparisonOperation.Operator == AstComparisonFilterOperator.Eq &&
157+
comparisonOperation.Value.BsonType != BsonType.RegularExpression)
158+
{
159+
value = comparisonOperation.Value;
160+
return true;
161+
}
162+
163+
value = null;
164+
return false;
165+
}
166+
167+
static bool IsFieldElemMatchRegex(AstFieldOperationFilter node, out BsonRegularExpression regex)
168+
{
169+
// { field : { $elemMatch : { $regex : "pattern", $options : "options" } } }
170+
if (node.Operation is AstElemMatchFilterOperation elemMatchOperation &&
171+
elemMatchOperation.Filter is AstFieldOperationFilter elemFilter &&
172+
elemFilter.Field.Path == "@<elem>" &&
173+
elemFilter.Operation is AstRegexFilterOperation regexOperation)
174+
{
175+
regex = new BsonRegularExpression(regexOperation.Pattern, regexOperation.Options);
176+
return true;
177+
}
178+
179+
regex = null;
180+
return false;
181+
}
182+
183+
static bool IsFieldElemMatchNotRegex(AstFieldOperationFilter node, out AstFilterField elemField, out BsonRegularExpression regex)
184+
{
185+
// { field : { $elemMatch : { $not : { $regex : "pattern", $options : "options" } } } }
186+
if (node.Operation is AstElemMatchFilterOperation elemMatch &&
187+
elemMatch.Filter is AstFieldOperationFilter elemFilter &&
188+
elemFilter.Field.Path == "@<elem>" &&
189+
elemFilter.Operation is AstNotFilterOperation notOperation &&
190+
notOperation.Operation is AstRegexFilterOperation regexOperation)
191+
{
192+
elemField = elemFilter.Field;
193+
regex = new BsonRegularExpression(regexOperation.Pattern, regexOperation.Options);
194+
return true;
195+
}
196+
197+
elemField = null;
198+
regex = null;
199+
return false;
200+
}
201+
202+
static bool IsFieldNotElemMatchEqValue(AstFieldOperationFilter node, out BsonValue value)
203+
{
204+
// { field : { $not : { $elemMatch : { $eq : value } } } } where value is not regex
205+
if (node.Operation is AstNotFilterOperation notFilterOperation &&
206+
notFilterOperation.Operation is AstElemMatchFilterOperation elemMatchOperation &&
207+
elemMatchOperation.Filter is AstFieldOperationFilter elemFilter &&
208+
elemFilter.Field.Path == "@<elem>" &&
209+
elemFilter.Operation is AstComparisonFilterOperation comparisonOperation &&
210+
comparisonOperation.Operator == AstComparisonFilterOperator.Eq &&
211+
comparisonOperation.Value.BsonType != BsonType.RegularExpression)
212+
{
213+
value = comparisonOperation.Value;
214+
return true;
215+
}
216+
217+
value = null;
218+
return false;
219+
}
220+
221+
static bool IsFieldNotElemMatchRegex(AstFieldOperationFilter node, out BsonRegularExpression regex)
222+
{
223+
// { field : { $not : { $elemMatch : { $regex : "pattern", $options : "options" } } } }
224+
if (node.Operation is AstNotFilterOperation notFilterOperation &&
225+
notFilterOperation.Operation is AstElemMatchFilterOperation elemMatchOperation &&
226+
elemMatchOperation.Filter is AstFieldOperationFilter elemFilter &&
227+
elemFilter.Field.Path == "@<elem>" &&
228+
elemFilter.Operation is AstRegexFilterOperation regexOperation)
229+
{
230+
regex = new BsonRegularExpression(regexOperation.Pattern, regexOperation.Options);
231+
return true;
232+
}
233+
234+
regex = null;
235+
return false;
236+
}
237+
}
238+
40239
public override AstNode VisitGetFieldExpression(AstGetFieldExpression node)
41240
{
42241
if (TrySimplifyAsFieldPath(node, out var simplified))

src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Visitors/AstNodeVisitor.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,11 @@ public virtual AstNode VisitGroupStage(AstGroupStage node)
409409
return node.Update(VisitAndConvert(node.Id), VisitAndConvert(node.Fields));
410410
}
411411

412+
public virtual AstNode VisitImpliedOperationFilterOperation(AstImpliedOperationFilterOperation node)
413+
{
414+
return node;
415+
}
416+
412417
public virtual AstNode VisitIndexOfArrayExpression(AstIndexOfArrayExpression node)
413418
{
414419
return node.Update(VisitAndConvert(node.Array), VisitAndConvert(node.Value), VisitAndConvert(node.Start), VisitAndConvert(node.End));

tests/MongoDB.Driver.Tests/Linq/Linq2ImplementationTestsOnLinq3/Translators/LegacyPredicateTranslatorTests.cs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
using MongoDB.Bson.Serialization;
2424
using MongoDB.Bson.Serialization.Attributes;
2525
using MongoDB.Driver;
26+
using MongoDB.Driver.Linq.Linq3Implementation.Ast.Optimizers;
2627
using MongoDB.Driver.Linq.Linq3Implementation.Misc;
2728
using MongoDB.Driver.Linq.Linq3Implementation.Translators;
2829
using MongoDB.Driver.Linq.Linq3Implementation.Translators.ExpressionToFilterTranslators;
@@ -109,7 +110,7 @@ public void TestWhereAContains2()
109110
[Fact]
110111
public void TestWhereAContains2Not()
111112
{
112-
Assert<C>(c => !c.A.Contains(2), 4, "{ a : { $not : { $elemMatch : { $eq : 2 } } } }");
113+
Assert<C>(c => !c.A.Contains(2), 4, "{ \"a\" : { \"$ne\" : 2 } }");
113114
}
114115

115116
[Fact]
@@ -303,7 +304,7 @@ public void TestWhereEAContainsB()
303304
[Fact]
304305
public void TestWhereEAContainsBNot()
305306
{
306-
Assert<C>(c => !c.EA.Contains(E.B), 4, "{ ea : { $not : { $elemMatch : { $eq : 2 } } } }");
307+
Assert<C>(c => !c.EA.Contains(E.B), 4, "{ \"ea\" : { \"$ne\" : 2 } }");
307308
}
308309

309310
[Fact]
@@ -393,7 +394,7 @@ public void TestWhereLContains2()
393394
[Fact]
394395
public void TestWhereLContains2Not()
395396
{
396-
Assert<C>(c => !c.L.Contains(2), 4, "{ l : { $not : { $elemMatch : { $eq : 2 } } } }");
397+
Assert<C>(c => !c.L.Contains(2), 4, "{ \"l\" : { \"$ne\" : 2 } }");
397398
}
398399

399400
[Fact]
@@ -1187,7 +1188,8 @@ private void Assert<TDocument>(Expression<Func<TDocument, bool>> expression, int
11871188
var symbol = context.CreateSymbol(parameter, serializer, isCurrent: true);
11881189
context = context.WithSymbol(symbol);
11891190
var filterAst = ExpressionToFilterTranslator.Translate(context, expression.Body);
1190-
var renderedFilter = (BsonDocument)filterAst.Render();
1191+
var simplifiedFilterAst = AstSimplifier.Simplify(filterAst);
1192+
var renderedFilter = (BsonDocument)simplifiedFilterAst.Render();
11911193
renderedFilter.Should().Be(expectedFilter);
11921194

11931195
var filter = new BsonDocumentFilterDefinition<C>(renderedFilter);

tests/MongoDB.Driver.Tests/Linq/Linq2ImplementationTestsOnLinq3/Translators/PredicateTranslatorTests.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
using MongoDB.Driver;
2525
using MongoDB.Driver.Core.TestHelpers.XunitExtensions;
2626
using MongoDB.Driver.Linq;
27+
using MongoDB.Driver.Linq.Linq3Implementation.Ast.Optimizers;
2728
using MongoDB.Driver.Linq.Linq3Implementation.Misc;
2829
using MongoDB.Driver.Linq.Linq3Implementation.Translators;
2930
using MongoDB.Driver.Linq.Linq3Implementation.Translators.ExpressionToFilterTranslators;
@@ -1162,7 +1163,8 @@ public List<TDocument> Assert<TDocument>(IMongoCollection<TDocument> collection,
11621163
var symbol = context.CreateSymbol(parameter, serializer, isCurrent: true);
11631164
context = context.WithSymbol(symbol);
11641165
var filterAst = ExpressionToFilterTranslator.Translate(context, filter.Body);
1165-
var filterDocument = (BsonDocument)filterAst.Render();
1166+
var simplifiedFilterAst = AstSimplifier.Simplify(filterAst);
1167+
var filterDocument = (BsonDocument)simplifiedFilterAst.Render();
11661168
filterDocument.Should().Be(expectedFilter);
11671169

11681170
var list = collection.FindSync(filterDocument).ToList();

0 commit comments

Comments
 (0)