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

Query: Refactor SelectExpression for referential integrity #7950

Merged
merged 1 commit into from
Mar 29, 2017

Conversation

smitpatel
Copy link
Contributor

@smitpatel smitpatel commented Mar 22, 2017

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:

  • 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, 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 various expression which can appear in SelectExpression. 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 projection expression so that each expression knows its root from subqueries.
    - Introduction of ProjectStarExpression which stores all the projection which are actually being used by outer SelectExpression. (Though they are not printed in SQL. Just * will be printed)
  • 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 order by 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 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.
  • 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.

Pending tasks in this PR:

  • XML doc comments are pending. (I will write them tomorrow, created PR so that code review can start)
  • More tests? Suggestions?

Future tasks:

  • Investigate visiting NewExpression in RelationalProjectionExpressionVisitor. 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 )
  • Removal of IncludeExpressionVisitor and associated helper methods from SelectExpression which are just to support current Include pipeline.
  • More robust way to deal with Anonymous expressions in projection which helps us identify an anonymous projection from an actual client eval method.
  • Implement Equals method on all custom expression. Uniform or need based?
  • Investigate and consolidate the handling of star projection. Currently we have IsProjectStar, ProjectStarExpression, ProjectStarTable.

This PR is part of #7520
Partially fix #6703

@tuespetre
Copy link
Contributor

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.

omgyes

Copy link
Contributor

@tuespetre tuespetre left a 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)
Copy link
Contributor

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[]> _);
Copy link
Contributor

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[]> _);
Copy link
Contributor

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();
Copy link
Contributor

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];
Copy link
Contributor

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;
Copy link
Contributor

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>();
Copy link
Contributor

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?

@smitpatel smitpatel force-pushed the thisnotgonnaendwell7520 branch 3 times, most recently from be8bf91 to 2d04e6b Compare March 22, 2017 18:31
Check.NotNull(expression, nameof(expression));
Check.NotNull(table, nameof(table));

return expression is ColumnExpression columnExpression
Copy link
Contributor

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
Copy link
Contributor

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))
Copy link
Contributor

Choose a reason for hiding this comment

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

Braces

Copy link
Contributor Author

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,
Copy link
Contributor

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);
Copy link
Contributor

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.

Copy link
Contributor

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.

@smitpatel smitpatel force-pushed the thisnotgonnaendwell7520 branch 2 times, most recently from 04606e8 to 1467451 Compare March 23, 2017 23:56
@anpete anpete mentioned this pull request Mar 23, 2017
12 tasks
@smitpatel smitpatel force-pushed the thisnotgonnaendwell7520 branch 3 times, most recently from 8ba26c9 to bfb93f0 Compare March 25, 2017 00:10
@smitpatel
Copy link
Contributor Author

Rebased.

@smitpatel
Copy link
Contributor Author

Ping!

{
return binaryExpression.Right.TryGetColumnExpression()
?? binaryExpression.Left.TryGetColumnExpression();
nonColumnExpression = isLeftConstantOrParameter ? left : right;
Copy link
Contributor

@maumar maumar Mar 27, 2017

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();
Copy link
Contributor

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);
Copy link
Contributor

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

Copy link
Contributor Author

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.

Copy link
Contributor Author

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);
Copy link
Contributor

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)
Copy link
Contributor

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Lets talk in person.

Copy link
Contributor Author

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.

@smitpatel smitpatel force-pushed the thisnotgonnaendwell7520 branch from bfb93f0 to bb66f16 Compare March 28, 2017 00:26
+ string.Join(",", methodCallExpression.Arguments.Skip(1)) + ")";
}

return "?" + AccessOperation + "?";
Copy link
Contributor

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;
Copy link
Contributor

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

@smitpatel smitpatel force-pushed the thisnotgonnaendwell7520 branch from bb66f16 to 2d66ab3 Compare March 28, 2017 19:35
@@ -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
Copy link
Contributor

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?

Copy link
Contributor Author

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.

@smitpatel
Copy link
Contributor Author

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;
Copy link
Contributor

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

@smitpatel smitpatel force-pushed the thisnotgonnaendwell7520 branch 2 times, most recently from 43cee32 to 3390b87 Compare March 29, 2017 17:18
@smitpatel smitpatel force-pushed the thisnotgonnaendwell7520 branch 3 times, most recently from b403876 to b16e1b2 Compare March 29, 2017 20:59
- 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants