-
Notifications
You must be signed in to change notification settings - Fork 1.3k
CSHARP-5785: Optimize LINQ translation for First() and FirstOrDefault() methods with predicates #1817
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
CSHARP-5785: Optimize LINQ translation for First() and FirstOrDefault() methods with predicates #1817
Changes from 3 commits
984874b
d5f7846
e13fd75
8cc0d37
fed12d1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,4 @@ | ||
| /* Copyright 2010-present MongoDB Inc. | ||
| /* Copyright 2010-present MongoDB Inc. | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
|
|
@@ -95,16 +95,31 @@ public static TranslatedExpression Translate(TranslationContext context, MethodC | |
| var sourceAst = sourceTranslation.Ast; | ||
| var itemSerializer = ArraySerializerHelper.GetItemSerializer(sourceTranslation.Serializer); | ||
|
|
||
| var isFirstMethod = method.Name.StartsWith("First"); | ||
|
|
||
| if (method.IsOneOf(__withPredicateMethods)) | ||
| { | ||
| var predicateLambda = ExpressionHelper.UnquoteLambdaIfQueryableMethod(method, arguments[1]); | ||
| var parameterExpression = predicateLambda.Parameters.Single(); | ||
| var parameterSymbol = context.CreateSymbol(parameterExpression, itemSerializer, isCurrent: false); | ||
| var predicateTranslation = ExpressionToAggregationExpressionTranslator.TranslateLambdaBody(context, predicateLambda, parameterSymbol); | ||
|
|
||
| // The $filter limit parameter was introduced in MongoDB 6.0 | ||
|
||
| AstExpression limit = null; | ||
| if (isFirstMethod) | ||
| { | ||
| var compatibilityLevel = context.TranslationOptions.CompatibilityLevel; | ||
| if (compatibilityLevel is >= ServerVersion.Server60) | ||
|
||
| { | ||
| limit = AstExpression.Constant(1); | ||
| } | ||
| } | ||
|
|
||
| sourceAst = AstExpression.Filter( | ||
| input: sourceAst, | ||
| cond: predicateTranslation.Ast, | ||
| @as: parameterSymbol.Var.Name); | ||
| @as: parameterSymbol.Var.Name, | ||
| limit: limit); | ||
| } | ||
|
|
||
| AstExpression ast; | ||
|
|
@@ -119,11 +134,11 @@ public static TranslatedExpression Translate(TranslationContext context, MethodC | |
| @in: AstExpression.Cond( | ||
| @if: AstExpression.Eq(AstExpression.Size(valuesAst), 0), | ||
| then: serializedDefaultValue, | ||
| @else: method.IsOneOf(__firstMethods) ? AstExpression.First(valuesAst) : AstExpression.Last(valuesAst))); | ||
| @else: isFirstMethod ? AstExpression.First(valuesAst) : AstExpression.Last(valuesAst))); | ||
| } | ||
| else | ||
| { | ||
| ast = method.Name == "First" ? AstExpression.First(sourceAst) : AstExpression.Last(sourceAst); | ||
| ast = isFirstMethod ? AstExpression.First(sourceAst) : AstExpression.Last(sourceAst); | ||
| } | ||
|
|
||
| return new TranslatedExpression(expression, ast, itemSerializer); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,6 +16,7 @@ | |
| using System.Collections.Generic; | ||
| using System.Linq; | ||
| using FluentAssertions; | ||
| using MongoDB.Driver.Core.Misc; | ||
| using MongoDB.Driver.Linq; | ||
| using MongoDB.Driver.TestHelpers; | ||
| using Xunit; | ||
|
|
@@ -24,6 +25,8 @@ namespace MongoDB.Driver.Tests.Linq.Linq3Implementation.Jira | |
| { | ||
| public class CSharp4048Tests : LinqIntegrationTest<CSharp4048Tests.ClassFixture> | ||
| { | ||
| private static readonly bool FilterLimitIsSupported = Feature.FilterLimit.IsSupported(CoreTestConfiguration.MaxWireVersion); | ||
|
|
||
| public CSharp4048Tests(ClassFixture fixture) | ||
| : base(fixture) | ||
| { | ||
|
|
@@ -118,7 +121,7 @@ public void List_get_Item_of_scalar_should_work() | |
| var expectedStages = new[] | ||
| { | ||
| "{ $group : { _id : '$_id', _elements : { $push: '$X' } } }", | ||
| "{ $project : { _id : '$_id' Result : { $arrayElemAt : ['$_elements', 0] } } }", | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How was this test not failing?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have no idea. I just happened to catch it. |
||
| "{ $project : { _id : '$_id', Result : { $arrayElemAt : ['$_elements', 0] } } }", | ||
| "{ $sort : { _id : 1 } }" | ||
| }; | ||
| AssertStages(stages, expectedStages); | ||
|
|
@@ -1040,12 +1043,21 @@ public void IGrouping_First_with_predicate_of_root_should_work() | |
| .OrderBy(x => x.Id); | ||
|
|
||
| var stages = Translate(collection, queryable); | ||
| var expectedStages = new[] | ||
| { | ||
| "{ $group : { _id : '$_id', _elements : { $push : '$$ROOT' } } }", | ||
| "{ $project : { _id : '$_id', Result : { $arrayElemAt : [{ $filter : { input : '$_elements', as : 'e', cond : { $ne : ['$$e.X', 1] } } }, 0] } } }", | ||
| "{ $sort : { _id : 1 } }" | ||
| }; | ||
|
|
||
| var expectedStages = FilterLimitIsSupported | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I almost find the longer version more readable: because I don't have to chase down how In some other files you still use the long form.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would rather keep the In some other files. there is only one use case, which is why I just used the longer form directly. |
||
| ? new[] | ||
| { | ||
| "{ $group : { _id : '$_id', _elements : { $push : '$$ROOT' } } }", | ||
| "{ $project : { _id : '$_id', Result : { $arrayElemAt : [{ $filter : { input : '$_elements', as : 'e', cond : { $ne : ['$$e.X', 1] }, limit : 1 } }, 0] } } }", | ||
| "{ $sort : { _id : 1 } }" | ||
| } | ||
| : new[] | ||
| { | ||
| "{ $group : { _id : '$_id', _elements : { $push : '$$ROOT' } } }", | ||
| "{ $project : { _id : '$_id', Result : { $arrayElemAt : [{ $filter : { input : '$_elements', as : 'e', cond : { $ne : ['$$e.X', 1] } } }, 0] } } }", | ||
| "{ $sort : { _id : 1 } }" | ||
| }; | ||
|
|
||
| AssertStages(stages, expectedStages); | ||
|
|
||
| var results = queryable.ToList(); | ||
|
|
@@ -1065,12 +1077,21 @@ public void IGrouping_First_with_predicate_of_scalar_should_work() | |
| .OrderBy(x => x.Id); | ||
|
|
||
| var stages = Translate(collection, queryable); | ||
| var expectedStages = new[] | ||
| { | ||
| "{ $group : { _id : '$_id', _elements : { $push : '$X' } } }", | ||
| "{ $project : { _id : '$_id', Result : { $arrayElemAt : [{ $filter : { input : '$_elements', as : 'e', cond : { $ne : ['$$e', 1] } } }, 0] } } }", | ||
| "{ $sort : { _id : 1 } }" | ||
| }; | ||
|
|
||
| var expectedStages = FilterLimitIsSupported | ||
| ? new[] | ||
| { | ||
| "{ $group : { _id : '$_id', _elements : { $push : '$X' } } }", | ||
| "{ $project : { _id : '$_id', Result : { $arrayElemAt : [{ $filter : { input : '$_elements', as : 'e', cond : { $ne : ['$$e', 1] }, limit : 1 } }, 0] } } }", | ||
| "{ $sort : { _id : 1 } }" | ||
| } | ||
| : new[] | ||
| { | ||
| "{ $group : { _id : '$_id', _elements : { $push : '$X' } } }", | ||
| "{ $project : { _id : '$_id', Result : { $arrayElemAt : [{ $filter : { input : '$_elements', as : 'e', cond : { $ne : ['$$e', 1] } } }, 0] } } }", | ||
| "{ $sort : { _id : 1 } }" | ||
| }; | ||
|
|
||
| AssertStages(stages, expectedStages); | ||
|
|
||
| var results = queryable.ToList(); | ||
|
|
@@ -1140,12 +1161,21 @@ public void IGrouping_FirstOrDefault_with_predicate_of_root_should_work() | |
| .OrderBy(x => x.Id); | ||
|
|
||
| var stages = Translate(collection, queryable); | ||
| var expectedStages = new[] | ||
| { | ||
| "{ $group : { _id : '$_id', _elements : { $push : '$$ROOT' } } }", | ||
| "{ $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] } } } } } } }", | ||
| "{ $sort : { _id : 1 } }" | ||
| }; | ||
|
|
||
| var expectedStages = FilterLimitIsSupported | ||
| ? new[] | ||
| { | ||
| "{ $group : { _id : '$_id', _elements : { $push : '$$ROOT' } } }", | ||
| "{ $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] } } } } } } }", | ||
| "{ $sort : { _id : 1 } }" | ||
| } | ||
| : new[] | ||
| { | ||
| "{ $group : { _id : '$_id', _elements : { $push : '$$ROOT' } } }", | ||
| "{ $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] } } } } } } }", | ||
| "{ $sort : { _id : 1 } }" | ||
| }; | ||
|
|
||
| AssertStages(stages, expectedStages); | ||
|
|
||
| var results = queryable.ToList(); | ||
|
|
@@ -1166,12 +1196,21 @@ public void IGrouping_FirstOrDefault_with_predicate_of_scalar_should_work() | |
| .OrderBy(x => x.Id); | ||
|
|
||
| var stages = Translate(collection, queryable); | ||
| var expectedStages = new[] | ||
| { | ||
| "{ $group : { _id : '$_id', _elements : { $push : '$X' } } }", | ||
| "{ $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] } } } } } } }", | ||
| "{ $sort : { _id : 1 } }" | ||
| }; | ||
|
|
||
| var expectedStages = FilterLimitIsSupported | ||
| ? new[] | ||
| { | ||
| "{ $group : { _id : '$_id', _elements : { $push : '$X' } } }", | ||
| "{ $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] } } } } } } }", | ||
| "{ $sort : { _id : 1 } }" | ||
| } | ||
| : new[] | ||
| { | ||
| "{ $group : { _id : '$_id', _elements : { $push : '$X' } } }", | ||
| "{ $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] } } } } } } }", | ||
| "{ $sort : { _id : 1 } }" | ||
| }; | ||
|
|
||
| AssertStages(stages, expectedStages); | ||
|
|
||
| var results = queryable.ToList(); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In general we prefer to test which method it is like this:
It is less likely to have false matches than string matching (partial or otherwise).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did it this way since I reasoned that if we passed the initial if statement:
if (method.IsOneOf(__firstOrLastMethods))then we already know that the method has matched one of the first methods so no need to test the methods again.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The statement you make is true, but even so we prefer to stick to
method.IsOneOfinstead of string matching.The only time we don't use
method.IsOneOfis when we are translating a method that might be implemented by classes we don't even know about (e.g.CompareTo).