-
Notifications
You must be signed in to change notification settings - Fork 3.2k
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
Simplify COALESCE
based on the nullability of its arguments
#33938
Conversation
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.
This looks great - there's only the problem I mentioned in #33890:
The main difficulty here is that we have various tests exercising Coalesce functionality, which are implemented over non-nullable columns; the Coalesce node would be stripped away there, and the tests would become useless. These need to be updated.
In other words this reduces our test coverage for coalesce but optimizing it away... Ideally we'd modify the tests to ensure that they actually exercise coalesce etc.
I'd also implement the SqlExpressionFactory (optimize at the source) as in #33890 at the same time (though it could be done separately too of course).
/cc @maumar
@@ -105,7 +105,7 @@ public class SqlServerStringAggregateMethodTranslator : IAggregateMethodCallTran | |||
source, | |||
enumerableArgumentIndex: 0, | |||
nullable: true, | |||
argumentsPropagateNullability: new[] { false, true }, | |||
argumentsPropagateNullability: new[] { false, false }, |
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.
Thanks, good catch.
I'll look into adding/updating tests before marking this as ready for review. Currently I am thinking of:
We can do that, but This might change if |
Sounds great. Hopefully there are nullable alternatives in the model for the tests in question:::
Oh I completely agree - both optimizations make sense for sure. The SqlExpressionFactory really is more of a nice-to-have compared to the one implemented here in SqlNullabilityProcessor. I've updated #33890 to track both these things.
Yeah, that's definitely a long-term discussion more than anything immediately actionable - we should proceed for now with the current query architecture regardless of that possible direction. |
Assigning to myself but @maumar definitely let us know what you think etc. |
I actually don't think we have a "natural" null value in a bool column in gears model. But you can always make one by doing a left join. CogTag.Gear is optional 1:1 and there is a cog tag without corresponding gear, so
|
This change aims at ensuring that the expressions within the queries being tested cannot be trivially optimized away. Base on the suggestion by @maumar in dotnet#33938 (comment)
fea7752
to
8b16233
Compare
8b16233
to
a4e7fe3
Compare
This change aims at ensuring that the expressions within the queries being tested cannot be trivially optimized away. Based on the suggestion by @maumar in dotnet#33938 (comment)
be230cd
to
473eb2e
Compare
The first commit belongs to #34078 (I rebased on top of that to have a working build). Apart from that, it is ready for review 🚀 |
473eb2e
to
43ca30d
Compare
Rebased to resolve merge conflict |
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.
Looks great as always, thanks. See two minor suggestions below.
if (coalesceArguments.Count == 1) | ||
{ | ||
return coalesceArguments[0]; | ||
} | ||
else if (coalesceArguments.Count == sqlFunctionExpression.Arguments.Count) | ||
{ | ||
return sqlFunctionExpression.Update(sqlFunctionExpression.Instance, coalesceArguments); | ||
} | ||
else | ||
{ | ||
return _sqlExpressionFactory.Function( | ||
sqlFunctionExpression.Name, | ||
coalesceArguments, | ||
sqlFunctionExpression.IsNullable, | ||
argumentsPropagateNullability: coalesceArguments.Select(_ => false).ToArray(), | ||
sqlFunctionExpression.Type, | ||
sqlFunctionExpression.TypeMapping); | ||
} |
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.
nit: I'm a big fan of switches (especially expressions):
if (coalesceArguments.Count == 1) | |
{ | |
return coalesceArguments[0]; | |
} | |
else if (coalesceArguments.Count == sqlFunctionExpression.Arguments.Count) | |
{ | |
return sqlFunctionExpression.Update(sqlFunctionExpression.Instance, coalesceArguments); | |
} | |
else | |
{ | |
return _sqlExpressionFactory.Function( | |
sqlFunctionExpression.Name, | |
coalesceArguments, | |
sqlFunctionExpression.IsNullable, | |
argumentsPropagateNullability: coalesceArguments.Select(_ => false).ToArray(), | |
sqlFunctionExpression.Type, | |
sqlFunctionExpression.TypeMapping); | |
} | |
return coalesceArguments switch | |
{ | |
[var singleArgument] => singleArgument, | |
_ when coalesceArguments.Count == sqlFunctionExpression.Arguments.Count | |
=> sqlFunctionExpression.Update(sqlFunctionExpression.Instance, coalesceArguments), | |
_ => _sqlExpressionFactory.Function( | |
sqlFunctionExpression.Name, | |
coalesceArguments, | |
sqlFunctionExpression.IsNullable, | |
argumentsPropagateNullability: coalesceArguments.Select(_ => false).ToArray(), | |
sqlFunctionExpression.Type, | |
sqlFunctionExpression.TypeMapping) | |
}; |
} | ||
else | ||
{ | ||
return _sqlExpressionFactory.Function( |
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 think it makes sense to just have SqlFunctionExpression.Update accept an optional argumentsPropagateNullability argument
8d6a473
to
3387737
Compare
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.
One last tiny nit...
SqlExpression? instance, | ||
IReadOnlyList<SqlExpression>? arguments, | ||
IReadOnlyList<bool>? argumentsPropagateNullability = null) | ||
=> instance != Instance |
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.
We should do a pass to invert the logic of all of these across all expression types at some point (I hate negation + else)
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.
as we are handling this, should we also check that the arguments make sense?
Currently foo.Update(foo.Instance, null)
always returns foo
(which might not be intended).
Similarly, if bar
is niladic (aka bar.ArgumentsPropagateNullability
is null
), bar.Update(bar.Instance, [_factory.Constant(3)])
returns bar
(arguments
is ignored).
We could prevent switching from/to niladic, but I am unsure if this is a check we care about (it is an internal API).
} | ||
|
||
nullable = coalesceNullable; | ||
|
||
return sqlFunctionExpression.Update(sqlFunctionExpression.Instance, coalesceArguments); | ||
return coalesceArguments switch |
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.
nit: now that this has only two arms, it can be a conditional operator
3387737
to
1f18de8
Compare
{ | ||
argumentsPropagateNullability ??= ArgumentsPropagateNullability; | ||
|
||
if (IsNiladic) |
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.
Shouldn't these checks be in the constructor?
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.
We could add them to the constructor if we want to, but they would not be sufficient to capture the misuse as mentioned in #33938 (comment)
Specifically, if either arguments
or Arguments
is null
, the code would skip the constructor and just return this
.
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.
@roji what is the best way forward? should I add checks both to the constructor and here?
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.
Thanks for the ping.
I took another look... I'm not sure why SqlFunctionExpression has a separate IsNiladic property (I've been wanting to take a look at this for a while): the arguments being null (as opposed to empty) seems like the natural way to express a niladic function, and it indeed seems like whenever niladic is true, arguments is null and vice versa.
So unless I'm mistaken, we can probably just remove the IsNliadic property as an actual property (i.e. just make it return Arguments is not null
). At that point, it's fine for Update() to switch to/from niladic - that's just an operation to change Arguments from null to non-null etc. And in any case argumentsPropagateNullability should correspond to arguments (both in nullness and in number of arguments when not null).
How does that sound?
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 especially like defining IsNiladic
from arguments (and we can also annotate it so that the compiler knows about their relation).
I am not sure if it makes sense to ever switch between niladic (FOOBAR
) and non-niladic (FOOBAR()
, FOOBAR(1, 2, 3)
, ...), but I guess it should not hurt (well, as long as the caller knows what it is doing 😅 ).
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.
Yeah, I mean, if they can use Update() to switch between 1 to two arguments, or even to zero, I don't see why they shouldn't also switch to niladic... It's on them to get it right...
Is this something you want to do as part of this PR? Or separately? If you prefer I can submit a PR for the niladic change.
1f18de8
to
e7705b9
Compare
|
||
return instance == Instance | ||
&& ((arguments == Arguments) || (arguments != null && Arguments != null && arguments.SequenceEqual(Arguments))) | ||
&& ((argumentsPropagateNullability == ArgumentsPropagateNullability) |
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.
OK, the problem this creates is that argumentsPropagateNullability being null could mean two things: either the user is switching to niladic (and so arguments is null too), or they want to just change the arguments without changing argumentsPropagateNullability (which seems like a common scenario).
Maybe we should just have two overloads, one only for changing arguments (doesn't have argumentsPropagateNullability parameter, and importantly, cannot change the number of arguments), and another with arguments and argumentsPropagateNullability?
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'll do that (it was indeed my initial approach).
Note that the "common" scenario occurs just 3 times in the codebase (there are only 4 uses of this method)
e7705b9
to
1a76432
Compare
Redefined it based on `Arguments`, consistent with its annotation.
Drop all of the arguments after the first non-nullable sub-expressions. Simplify unary `COALESCE` to its argument.
1a76432
to
6a3488a
Compare
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.
Thanks @ranma42!
Drop all of the arguments after the first non-nullable sub-expressions.
Simplify unary
COALESCE
to its argument.