Skip to content
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

Return null when the type is nullable for Cosmos Max/Min/Average #35173

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

ajcvickers
Copy link
Member

Fixes #35094

This was a regression resulting from the major Cosmos query refactoring that happened in EF9. In EF8, the functions Min, Max, and Average would return null if the return type was nullable or was cast to a nullable when the collection is empty. In EF9, this started throwing, which is correct for non-nullable types, but a regression for nullable types.

Fixes #35094

This was a regression resulting from the major Cosmos query refactoring that happened in EF9. In EF8, the functions Min, Max, and Average would return null if the return type was nullable or was cast to a nullable when the collection is empty. In EF9, this started throwing, which is correct for non-nullable types, but a regression for nullable types.
@ajcvickers ajcvickers requested a review from a team as a code owner November 21, 2024 15:23
Copy link
Member

@roji roji left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM but see comments/suggestions.

One thought - do we have tests where we do Min/Max/Average over a nullable value type, and where the database actually does contain a null (as opposed to an empty result set)? If not, it might be worth adding those, as especially in Cosmos the returns-null vs. returns-nothing are two very distinct cases.

@@ -1706,29 +1712,7 @@ private static ShapedQueryExpression AggregateResultShaper(
var nullableResultType = resultType.MakeNullable();
Expression shaper = new ProjectionBindingExpression(source.QueryExpression, new ProjectionMember(), nullableResultType);

if (throwOnNullResult)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we have the same code in relational - we should look at removing it there (I'd actually do a quick test run without it to see what happens, maybe it'll shed light for why it might be needed on Cosmos too).

@@ -444,7 +444,25 @@ private ShapedQueryExpression CreateShapedQueryExpression(SelectExpression selec
/// </summary>
protected override ShapedQueryExpression? TranslateAverage(ShapedQueryExpression source, LambdaExpression? selector, Type resultType)
{
var selectExpression = (SelectExpression)source.QueryExpression;
var updatedSource = TranslateAggregateCommon(source, selector, resultType, out var selectExpression);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just noting that we should try to keep common code similar/identical between Cosmos and relational, as much as possible... Not sure that it makes sense in this specific case - but something to keep in mind.

return AggregateResultShaper(updatedSource, projection, resultType);
}

private ShapedQueryExpression? TranslateAggregateCommon(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: move this helper down below where all the private stuff lives?

@@ -457,10 +475,13 @@ private ShapedQueryExpression CreateShapedQueryExpression(SelectExpression selec
source = TranslateSelect(source, selector);
}

var projection = (SqlExpression)selectExpression.GetMappedProjection(new ProjectionMember());
projection = _sqlExpressionFactory.Function("AVG", new[] { projection }, resultType, _typeMappingSource.FindMapping(resultType));
if (resultType.IsNullableType())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Am noting that this changes to SingleOrDefault for reference types too, not just for nullable value types (for example, Max() can be used over strings). I think this is correct, but maybe worth ensuring we have tests for these?

return AggregateResultShaper(updatedSource, projection, resultType);
}

private ShapedQueryExpression? TranslateAggregateCommon(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure, but it seems that the three operators handled here (Average, Max, Min) have the same code in their Translate* method, aside from the name of the function. If so, this TranslateAggregateCommon can just do everything, and just accept the function name as a string, rather than returning here to Translate, which then checks for null and calls another common AggregateResultShaper() in Translate*...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Cosmos: Min/Max/Average over nullable value type throws on no data
2 participants