-
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
Query: Refactor SelectExpression for referential integrity #7950
Conversation
omgyes |
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.
Excited to work with the improvements you made!
if (relationalQueryModelVisitor.RequiresClientEval | ||
|| relationalQueryModelVisitor.RequiresClientSelectMany | ||
|| relationalQueryModelVisitor.RequiresClientJoin | ||
|| relationalQueryModelVisitor.RequiresClientFilter | ||
|| relationalQueryModelVisitor.RequiresClientOrderBy | ||
|| relationalQueryModelVisitor.RequiresClientResultOperator | ||
|| !_resultHandlers.TryGetValue(resultOperator.GetType(), out resultHandler) | ||
|| !_resultHandlers.TryGetValue(resultOperator.GetType(), out Func<HandlerContext, Expression> resultHandler) |
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.
out var resultHandler
/*querySource:*/ null, | ||
out _); | ||
out Dictionary<Type, int[]> _); |
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.
out _
p, | ||
querySource), | ||
/*querySource:*/ null, | ||
out _); | ||
out Dictionary<Type, int[]> _); |
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.
out _
p, | ||
_querySource), | ||
_querySource, | ||
out typeIndexMap).Compile(); | ||
out Dictionary<Type, int[]> typeIndexMap).Compile(); |
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.
out var typeIndexMap
return selectExpression.Projection | ||
.OfType<AliasExpression>() | ||
.SingleOrDefault(ae => Equals(ae.SourceMember, memberExpression.Member)); | ||
return selectExpression.MemberInfoProjectionMapping[memberExpression.Member]; |
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.
Can these lines be encapsulated into something like selectExpression?.GetMappedProjectionExpression(member)
? Remove burden of ContainsKey
from the caller, etc.
Check.NotNull(tableExpression, nameof(tableExpression)); | ||
|
||
Name = name; | ||
Type = type; | ||
_property = property; |
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.
Thank you 😭
@@ -85,6 +90,8 @@ public class SelectExpression : TableExpressionBase | |||
/// </value> | |||
public virtual Expression Predicate { get; [param: CanBeNull] set; } | |||
|
|||
public virtual Dictionary<MemberInfo, Expression> MemberInfoProjectionMapping { get; private set; } = new Dictionary<MemberInfo, Expression>(); |
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.
Make private readonly
field and use methods to manipulate?
be8bf91
to
2d04e6b
Compare
Check.NotNull(expression, nameof(expression)); | ||
Check.NotNull(table, nameof(table)); | ||
|
||
return expression is ColumnExpression columnExpression |
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.
Consider switch..case for clarity here.
|
||
private static IProperty TryGetProperty(Expression expression) | ||
{ | ||
return expression is ColumnExpression columnExpression |
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.
Same here for switch..case
/// </returns> | ||
public override bool Equals(object obj) | ||
{ | ||
if (ReferenceEquals(null, obj)) |
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.
Braces
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.
Added everywhere needed. Resharper 👍
/// <param name="projections"> The list of existing expressions being used from this star projection. </param> | ||
/// <param name="tableExpression"> The target table expression being represented by this star projection. </param> | ||
public ProjectStarExpression( | ||
[NotNull]List<Expression> projections, |
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.
Formatting
return obj.GetType() == GetType() && Equals((ProjectStarExpression)obj); | ||
} | ||
|
||
private bool Equals([NotNull] ProjectStarExpression other) => _projections.Equals(other._projections) && _tableExpression.Equals(other._tableExpression); |
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.
Are you sure you want to use reference equality on _projections here? We typically do SequenceEquals here - same for GetHashCode below - aggregate the hash codes of the elements.
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.
Might want to review any other possible instances of this.
04606e8
to
1467451
Compare
8ba26c9
to
bfb93f0
Compare
Rebased. |
Ping! |
{ | ||
return binaryExpression.Right.TryGetColumnExpression() | ||
?? binaryExpression.Left.TryGetColumnExpression(); | ||
nonColumnExpression = isLeftConstantOrParameter ? left : right; |
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: add newline before return #Resolved
/// This API supports the Entity Framework Core infrastructure and is not intended to be used | ||
/// directly from your code. This API may change or be removed in future releases. | ||
/// </summary> | ||
public override int GetHashCode() => _orderings.GetHashCode(); |
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.
Equals and GHC over collection
@@ -564,6 +563,9 @@ private static bool CompareMemberList(IReadOnlyList<MemberInfo> a, IReadOnlyList | |||
private bool CompareNewArray(NewArrayExpression a, NewArrayExpression b) | |||
=> CompareExpressionList(a.Expressions, b.Expressions); | |||
|
|||
private bool CompareExtension(Expression a, Expression b) | |||
=> a.Equals(b); |
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 don't see Equals implemented on NullConditionalExpression
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.
There are few expressions where Equals in not implemented. We can chat more about it in person.
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.
Implemented Equals
for all expressions of extension type except abstract classes & SelectExpression.
{ | ||
goto default; | ||
hashCode += (hashCode * 397) ^ GetHashCode(nullConditionalExpression.AccessOperation); |
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.
Implement this on NullConditionalExpression
|| unwrappedExpression is AliasExpression; | ||
} | ||
|
||
public static ColumnReferenceExpression LiftExpressionFromSubquery([NotNull] this Expression expression, [NotNull] TableExpressionBase table) |
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.
Polymorphic method on SQL extension nodes? What happens in null case?
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.
Lets talk in person.
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.
For now only this types would need it. By design it should cover all cases and should not require anymore types to be evaluated.
bfb93f0
to
bb66f16
Compare
+ string.Join(",", methodCallExpression.Arguments.Skip(1)) + ")"; | ||
} | ||
|
||
return "?" + AccessOperation + "?"; |
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.
use string interpolation
+ "?." + methodCallExpression.Method.Name | ||
+ "(" + string.Join(",", methodCallExpression.Arguments) + ")"; | ||
} | ||
var method = methodCallExpression.Method; |
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: newline after {, and before return
bb66f16
to
2d66ab3
Compare
@@ -106,7 +86,7 @@ protected override Expression Accept(ExpressionVisitor visitor) | |||
} | |||
|
|||
/// <summary> | |||
/// Reduces the node and then calls the <see cref="ExpressionVisitor.Visit(Expression)" /> method passing the | |||
/// Reduces the node and then calls the <see cref="ExpressionVisitor.Visit(System.Linq.Expressions.Expression)" /> method passing the |
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.
why fully qualified name 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.
Some auto-formatting may have loved it.
Updated. |
|
||
foreach (var expression in _projection) | ||
{ | ||
var aliasExpression = expression as AliasExpression; | ||
var expressionToAdd = expression; | ||
var memberInfo = _memberInfoProjectionMapping.FirstOrDefault(kvp => ExpressionEqualityComparer.Equals(kvp.Value, expression)).Key; |
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: this can be moved down
43cee32
to
3390b87
Compare
b403876
to
b16e1b2
Compare
- No ad-hoc creation of ColumnExpression. When binding a property to SelectExpression, SelectExpression will generate appropriate expression for it. (BindPropertyToSelectExpression method which binds property even if it is not added to projection) - Removal of non-property ColumnExpression - ColumnExpression represent a column which is directly backed by EntityType.Property. For any other case, a different kind of expression should be used. - AliasExpression requires alias to be defined always. Hence unless column needs to aliased, it will not be made AliasExpression. - Implementation of Equals method of all extension expressions and using ExpressionEqualityComparer to compare expressions in Projection/OrderBy to avoid duplicates. This removes the complex logic in AddToProjection. - Removal of TryGetColumnExpression - All the places, which relied on assumption of having a column backed expression are changed to work with any kind of expression. - Introduction of ColumnReferenceExpression which can store reference to inner projected expression so that each expression knows its root inside the subqueries. - SelectExpression._starProjection stores list of expressions being used by outer SelectExpression. - Simplification of AddToProjection method - Method adds expression straight to projection. Bind method will create the expression to be added from property. Method also unique-fy alias if it is inside subquery. This method also update OrderBy list if a complex expression is getting added to projection and present in orderby by alias-ing it. - Removal of SourceMember/SourceExpression from AliasExpression, (which brought requirement of having every ColumnExpression as AliasExpression in projection). While visiting NewExpression in RelationalProjectionExpressionVisitor, we record SourceExpression to projected expression mapping and then we write MemberInfo to projected expression mapping in SelectExpression. Which is appropriately lifted during PushDownSubquery. This makes it possible for us to bind members after projecting into anonymous types. - Moved RelationalAnnotationsProvider to SelectExpression to find column name so that all the visitors trying to bind property with select expression does not need it to generate column name.
b16e1b2
to
111b095
Compare
In current query pipeline, we create various column/alias expressions in ad-hoc manner and add them to SelectExpression, which create lack of detailed metadata info. e.g. In nested SelectExpression, we don't know which column from inner subqueries is being projected out in outer projection. That is a likely cause to create invalid SQL since it creates disconnect between the sql expressions and selectexpression. This also gives rise to various assumptions which goes viral across the stack like
TryGetColumnExpression
.Above issue is huge blocker for immutable SelectExpression. This PR is first step in that direction.
Changes involved:
Overall design:
Every sql expression added to SelectExpression knows its source metadata. It is either a column expression backing property on an entity type or it is a .net expression built by combining column expressions. In nested queries, expressions on outer queries have reference to inner expression from where they are getting values. All parts of SelectExpression can be any kind of expression (which was previously restricted to AliasExpression).
Changes:
BindPropertyToSelectExpression
method which binds property even if it is not added to projection)Equals
method of various expression which can appear in SelectExpression. And usingExpressionEqualityComparer
to compare expressions in Projection/OrderBy to avoid duplicates. This removes the complex logic in AddToProjection.TryGetColumnExpression
- All the places, which relied on assumption of having a column backed expression are changed to work with any kind of expression.ColumnReferenceExpression
which can store reference to inner projection expression so that each expression knows its root from subqueries.- Introduction ofProjectStarExpression
which stores all the projection which are actually being used by outer SelectExpression. (Though they are not printed in SQL. Just * will be printed)AddToProjection
method - Method adds expression straight to projection. Bind method will create the expression to be added from property. Method also unique-fy alias if it is inside subquery. This method also update OrderBy list if a complex expression is getting added to projection and present in order by by alias-ing it.NewExpression
inRelationalProjectionExpressionVisitor
, we record SourceExpression to sqlExpression mapping and then we write MemberInfo to sqlExpression mapping in SelectExpression. Which is appropriately lifted during Subquery Pushdown. This makes it possible for us to bind operators after projecting into anonymous types.Pending tasks in this PR:
XML doc comments are pending. (I will write them tomorrow, created PR so that code review can start)Future tasks:
NewExpression
inRelationalProjectionExpressionVisitor
. At present we visit it twice. The only info we get out of first visit is about client projection. We throw away translated expression. (this would fix Query: Query containing subquery with annonymous type fails to translate to server #7844 )Equals
method on all custom expression. Uniform or need based?This PR is part of #7520
Partially fix #6703