Skip to content

Commit 2d60432

Browse files
authored
CSHARP-5785: Optimize LINQ translation for First() and FirstOrDefault() methods with predicates (#1817)
1 parent 223f7f6 commit 2d60432

File tree

5 files changed

+226
-45
lines changed

5 files changed

+226
-45
lines changed

src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/FirstOrLastMethodToAggregationExpressionTranslator.cs

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
using System.Linq;
1717
using System.Linq.Expressions;
1818
using System.Reflection;
19+
using MongoDB.Driver.Core.Misc;
1920
using MongoDB.Driver.Linq.Linq3Implementation.Ast.Expressions;
2021
using MongoDB.Driver.Linq.Linq3Implementation.Misc;
2122
using MongoDB.Driver.Linq.Linq3Implementation.Reflection;
@@ -94,16 +95,30 @@ public static TranslatedExpression Translate(TranslationContext context, MethodC
9495
var sourceAst = sourceTranslation.Ast;
9596
var itemSerializer = ArraySerializerHelper.GetItemSerializer(sourceTranslation.Serializer);
9697

98+
var isFirstMethod = method.IsOneOf(__firstMethods);
99+
97100
if (method.IsOneOf(__withPredicateMethods))
98101
{
99102
var predicateLambda = ExpressionHelper.UnquoteLambdaIfQueryableMethod(method, arguments[1]);
100103
var parameterExpression = predicateLambda.Parameters.Single();
101104
var parameterSymbol = context.CreateSymbol(parameterExpression, itemSerializer, isCurrent: false);
102105
var predicateTranslation = ExpressionToAggregationExpressionTranslator.TranslateLambdaBody(context, predicateLambda, parameterSymbol);
106+
107+
AstExpression limit = null;
108+
if (isFirstMethod)
109+
{
110+
var compatibilityLevel = context.TranslationOptions.CompatibilityLevel;
111+
if (Feature.FilterLimit.IsSupported(compatibilityLevel.ToWireVersion()))
112+
{
113+
limit = AstExpression.Constant(1);
114+
}
115+
}
116+
103117
sourceAst = AstExpression.Filter(
104118
input: sourceAst,
105119
cond: predicateTranslation.Ast,
106-
@as: parameterSymbol.Var.Name);
120+
@as: parameterSymbol.Var.Name,
121+
limit: limit);
107122
}
108123

109124
AstExpression ast;
@@ -118,11 +133,11 @@ public static TranslatedExpression Translate(TranslationContext context, MethodC
118133
@in: AstExpression.Cond(
119134
@if: AstExpression.Eq(AstExpression.Size(valuesAst), 0),
120135
then: serializedDefaultValue,
121-
@else: method.IsOneOf(__firstMethods) ? AstExpression.First(valuesAst) : AstExpression.Last(valuesAst)));
136+
@else: isFirstMethod ? AstExpression.First(valuesAst) : AstExpression.Last(valuesAst)));
122137
}
123138
else
124139
{
125-
ast = method.Name == "First" ? AstExpression.First(sourceAst) : AstExpression.Last(sourceAst);
140+
ast = isFirstMethod ? AstExpression.First(sourceAst) : AstExpression.Last(sourceAst);
126141
}
127142

128143
return new TranslatedExpression(expression, ast, itemSerializer);

tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp4048Tests.cs

Lines changed: 64 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
using System.Collections.Generic;
1717
using System.Linq;
1818
using FluentAssertions;
19+
using MongoDB.Driver.Core.Misc;
1920
using MongoDB.Driver.Linq;
2021
using MongoDB.Driver.TestHelpers;
2122
using Xunit;
@@ -24,6 +25,8 @@ namespace MongoDB.Driver.Tests.Linq.Linq3Implementation.Jira
2425
{
2526
public class CSharp4048Tests : LinqIntegrationTest<CSharp4048Tests.ClassFixture>
2627
{
28+
private static readonly bool FilterLimitIsSupported = Feature.FilterLimit.IsSupported(CoreTestConfiguration.MaxWireVersion);
29+
2730
public CSharp4048Tests(ClassFixture fixture)
2831
: base(fixture)
2932
{
@@ -118,7 +121,7 @@ public void List_get_Item_of_scalar_should_work()
118121
var expectedStages = new[]
119122
{
120123
"{ $group : { _id : '$_id', _elements : { $push: '$X' } } }",
121-
"{ $project : { _id : '$_id' Result : { $arrayElemAt : ['$_elements', 0] } } }",
124+
"{ $project : { _id : '$_id', Result : { $arrayElemAt : ['$_elements', 0] } } }",
122125
"{ $sort : { _id : 1 } }"
123126
};
124127
AssertStages(stages, expectedStages);
@@ -1040,12 +1043,21 @@ public void IGrouping_First_with_predicate_of_root_should_work()
10401043
.OrderBy(x => x.Id);
10411044

10421045
var stages = Translate(collection, queryable);
1043-
var expectedStages = new[]
1044-
{
1045-
"{ $group : { _id : '$_id', _elements : { $push : '$$ROOT' } } }",
1046-
"{ $project : { _id : '$_id', Result : { $arrayElemAt : [{ $filter : { input : '$_elements', as : 'e', cond : { $ne : ['$$e.X', 1] } } }, 0] } } }",
1047-
"{ $sort : { _id : 1 } }"
1048-
};
1046+
1047+
var expectedStages = FilterLimitIsSupported
1048+
? new[]
1049+
{
1050+
"{ $group : { _id : '$_id', _elements : { $push : '$$ROOT' } } }",
1051+
"{ $project : { _id : '$_id', Result : { $arrayElemAt : [{ $filter : { input : '$_elements', as : 'e', cond : { $ne : ['$$e.X', 1] }, limit : 1 } }, 0] } } }",
1052+
"{ $sort : { _id : 1 } }"
1053+
}
1054+
: new[]
1055+
{
1056+
"{ $group : { _id : '$_id', _elements : { $push : '$$ROOT' } } }",
1057+
"{ $project : { _id : '$_id', Result : { $arrayElemAt : [{ $filter : { input : '$_elements', as : 'e', cond : { $ne : ['$$e.X', 1] } } }, 0] } } }",
1058+
"{ $sort : { _id : 1 } }"
1059+
};
1060+
10491061
AssertStages(stages, expectedStages);
10501062

10511063
var results = queryable.ToList();
@@ -1065,12 +1077,21 @@ public void IGrouping_First_with_predicate_of_scalar_should_work()
10651077
.OrderBy(x => x.Id);
10661078

10671079
var stages = Translate(collection, queryable);
1068-
var expectedStages = new[]
1069-
{
1070-
"{ $group : { _id : '$_id', _elements : { $push : '$X' } } }",
1071-
"{ $project : { _id : '$_id', Result : { $arrayElemAt : [{ $filter : { input : '$_elements', as : 'e', cond : { $ne : ['$$e', 1] } } }, 0] } } }",
1072-
"{ $sort : { _id : 1 } }"
1073-
};
1080+
1081+
var expectedStages = FilterLimitIsSupported
1082+
? new[]
1083+
{
1084+
"{ $group : { _id : '$_id', _elements : { $push : '$X' } } }",
1085+
"{ $project : { _id : '$_id', Result : { $arrayElemAt : [{ $filter : { input : '$_elements', as : 'e', cond : { $ne : ['$$e', 1] }, limit : 1 } }, 0] } } }",
1086+
"{ $sort : { _id : 1 } }"
1087+
}
1088+
: new[]
1089+
{
1090+
"{ $group : { _id : '$_id', _elements : { $push : '$X' } } }",
1091+
"{ $project : { _id : '$_id', Result : { $arrayElemAt : [{ $filter : { input : '$_elements', as : 'e', cond : { $ne : ['$$e', 1] } } }, 0] } } }",
1092+
"{ $sort : { _id : 1 } }"
1093+
};
1094+
10741095
AssertStages(stages, expectedStages);
10751096

10761097
var results = queryable.ToList();
@@ -1140,12 +1161,21 @@ public void IGrouping_FirstOrDefault_with_predicate_of_root_should_work()
11401161
.OrderBy(x => x.Id);
11411162

11421163
var stages = Translate(collection, queryable);
1143-
var expectedStages = new[]
1144-
{
1145-
"{ $group : { _id : '$_id', _elements : { $push : '$$ROOT' } } }",
1146-
"{ $project : { _id : '$_id', Result : { $let : { vars : { values : { $filter : { input : '$_elements', as : 'e', cond : { $ne : ['$$e.X', 1] } } } }, in : { $cond : { if : { $eq : [{ $size : '$$values' }, 0] }, then : null, else : { $arrayElemAt : ['$$values', 0] } } } } } } }",
1147-
"{ $sort : { _id : 1 } }"
1148-
};
1164+
1165+
var expectedStages = FilterLimitIsSupported
1166+
? new[]
1167+
{
1168+
"{ $group : { _id : '$_id', _elements : { $push : '$$ROOT' } } }",
1169+
"{ $project : { _id : '$_id', Result : { $let : { vars : { values : { $filter : { input : '$_elements', as : 'e', cond : { $ne : ['$$e.X', 1] }, limit : 1 } } }, in : { $cond : { if : { $eq : [{ $size : '$$values' }, 0] }, then : null, else : { $arrayElemAt : ['$$values', 0] } } } } } } }",
1170+
"{ $sort : { _id : 1 } }"
1171+
}
1172+
: new[]
1173+
{
1174+
"{ $group : { _id : '$_id', _elements : { $push : '$$ROOT' } } }",
1175+
"{ $project : { _id : '$_id', Result : { $let : { vars : { values : { $filter : { input : '$_elements', as : 'e', cond : { $ne : ['$$e.X', 1] } } } }, in : { $cond : { if : { $eq : [{ $size : '$$values' }, 0] }, then : null, else : { $arrayElemAt : ['$$values', 0] } } } } } } }",
1176+
"{ $sort : { _id : 1 } }"
1177+
};
1178+
11491179
AssertStages(stages, expectedStages);
11501180

11511181
var results = queryable.ToList();
@@ -1166,12 +1196,21 @@ public void IGrouping_FirstOrDefault_with_predicate_of_scalar_should_work()
11661196
.OrderBy(x => x.Id);
11671197

11681198
var stages = Translate(collection, queryable);
1169-
var expectedStages = new[]
1170-
{
1171-
"{ $group : { _id : '$_id', _elements : { $push : '$X' } } }",
1172-
"{ $project : { _id : '$_id', Result : { $let : { vars : { values : { $filter : { input : '$_elements', as : 'e', cond : { $ne : ['$$e', 1] } } } }, in : { $cond : { if : { $eq : [{ $size : '$$values' }, 0] }, then : 0, else : { $arrayElemAt : ['$$values', 0] } } } } } } }",
1173-
"{ $sort : { _id : 1 } }"
1174-
};
1199+
1200+
var expectedStages = FilterLimitIsSupported
1201+
? new[]
1202+
{
1203+
"{ $group : { _id : '$_id', _elements : { $push : '$X' } } }",
1204+
"{ $project : { _id : '$_id', Result : { $let : { vars : { values : { $filter : { input : '$_elements', as : 'e', cond : { $ne : ['$$e', 1] }, limit : 1 } } }, in : { $cond : { if : { $eq : [{ $size : '$$values' }, 0] }, then : 0, else : { $arrayElemAt : ['$$values', 0] } } } } } } }",
1205+
"{ $sort : { _id : 1 } }"
1206+
}
1207+
: new[]
1208+
{
1209+
"{ $group : { _id : '$_id', _elements : { $push : '$X' } } }",
1210+
"{ $project : { _id : '$_id', Result : { $let : { vars : { values : { $filter : { input : '$_elements', as : 'e', cond : { $ne : ['$$e', 1] } } } }, in : { $cond : { if : { $eq : [{ $size : '$$values' }, 0] }, then : 0, else : { $arrayElemAt : ['$$values', 0] } } } } } } }",
1211+
"{ $sort : { _id : 1 } }"
1212+
};
1213+
11751214
AssertStages(stages, expectedStages);
11761215

11771216
var results = queryable.ToList();

tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp5258Tests.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
using System.Collections.Generic;
1717
using System.Linq;
1818
using FluentAssertions;
19+
using MongoDB.Driver.Core.Misc;
1920
using MongoDB.Driver.TestHelpers;
2021
using Xunit;
2122

@@ -37,7 +38,15 @@ public void Select_First_with_predicate_should_work()
3738
.Select(_x => _x.List.First(_y => _y > 2));
3839

3940
var stages = Translate(collection, queryable);
40-
AssertStages(stages, "{ $project : { _v : { $arrayElemAt : [{ $filter : { input : '$List', as : 'v__0', cond : { $gt : ['$$v__0', 2] } } }, 0] }, _id : 0 } }");
41+
42+
if (Feature.FilterLimit.IsSupported(CoreTestConfiguration.MaxWireVersion))
43+
{
44+
AssertStages(stages, "{ $project : { _v : { $arrayElemAt : [{ $filter : { input : '$List', as : 'v__0', cond : { $gt : ['$$v__0', 2] }, limit : 1 } }, 0] }, _id : 0 } }");
45+
}
46+
else
47+
{
48+
AssertStages(stages, "{ $project : { _v : { $arrayElemAt : [{ $filter : { input : '$List', as : 'v__0', cond : { $gt : ['$$v__0', 2] } } }, 0] }, _id : 0 } }");
49+
}
4150

4251
var results = queryable.ToList();
4352
results.Should().Equal(3, 4);

0 commit comments

Comments
 (0)