Skip to content

Commit

Permalink
Refactor queryable collection support for non-relational providers (#…
Browse files Browse the repository at this point in the history
…32506)

Also fixing exception ordering issue.

Fixes #32505
  • Loading branch information
roji authored Dec 23, 2023
1 parent 2d7ed26 commit 5defd88
Show file tree
Hide file tree
Showing 8 changed files with 172 additions and 158 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public RelationalQueryRootProcessor(

/// <summary>
/// Indicates that a <see cref="ConstantExpression" /> can be converted to a <see cref="InlineQueryRootExpression" />;
/// the latter will end up in <see cref="RelationalQueryableMethodTranslatingExpressionVisitor.VisitInlineQueryRoot" /> for
/// the latter will end up in <see cref="RelationalQueryableMethodTranslatingExpressionVisitor.TranslateInlineQueryRoot" /> for
/// translation to a SQL <see cref="ValuesExpression" />.
/// </summary>
protected override bool ShouldConvertToInlineQueryRoot(NewArrayExpression newArrayExpression)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -210,19 +210,6 @@ when entityQueryRootExpression.GetType() == typeof(EntityQueryRootExpression)
return new ShapedQueryExpression(selectExpression, shaperExpression);
}

case InlineQueryRootExpression inlineQueryRootExpression:
return VisitInlineQueryRoot(inlineQueryRootExpression) ?? base.VisitExtension(extensionExpression);

case ParameterQueryRootExpression parameterQueryRootExpression:
var sqlParameterExpression =
_sqlTranslator.Visit(parameterQueryRootExpression.ParameterExpression) as SqlParameterExpression;
Check.DebugAssert(sqlParameterExpression is not null, "sqlParameterExpression is not null");
return TranslatePrimitiveCollection(
sqlParameterExpression,
property: null,
char.ToLowerInvariant(sqlParameterExpression.Name.First(c => c != '_')).ToString())
?? base.VisitExtension(extensionExpression);

case JsonQueryExpression jsonQueryExpression:
return TransformJsonQueryToTable(jsonQueryExpression) ?? base.VisitExtension(extensionExpression);

Expand Down Expand Up @@ -262,46 +249,64 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp

var translated = base.VisitMethodCall(methodCallExpression);

if (translated == QueryCompilationContext.NotTranslatedExpression)
// For Contains over a collection parameter, if the provider hasn't implemented TranslateCollection (e.g. OPENJSON on SQL
// Server), we need to fall back to the previous IN translation.
if (translated == QueryCompilationContext.NotTranslatedExpression
&& method.IsGenericMethod
&& method.GetGenericMethodDefinition() == QueryableMethods.Contains
&& methodCallExpression.Arguments[0] is ParameterQueryRootExpression parameterSource
&& TranslateExpression(methodCallExpression.Arguments[1]) is SqlExpression item
&& _sqlTranslator.Visit(parameterSource.ParameterExpression) is SqlParameterExpression sqlParameterExpression)
{
// Attempt to translate access into a primitive collection property (i.e. array column)
if (_sqlTranslator.TryTranslatePropertyAccess(methodCallExpression, out var translatedExpression, out var property)
&& property is IProperty { IsPrimitiveCollection: true } regularProperty
&& translatedExpression is SqlExpression sqlExpression)
{
var tableAlias = sqlExpression switch
{
ColumnExpression c => c.Name[..1].ToLowerInvariant(),
JsonScalarExpression { Path: [.., { PropertyName: string propertyName }] } => propertyName[..1].ToLowerInvariant(),
_ => "j"
};
var inExpression = _sqlExpressionFactory.In(item, sqlParameterExpression);
var selectExpression = new SelectExpression(inExpression);
var shaperExpression = Expression.Convert(
new ProjectionBindingExpression(selectExpression, new ProjectionMember(), typeof(bool?)), typeof(bool));
var shapedQueryExpression = new ShapedQueryExpression(selectExpression, shaperExpression)
.UpdateResultCardinality(ResultCardinality.Single);
return shapedQueryExpression;
}

if (TranslatePrimitiveCollection(sqlExpression, regularProperty, tableAlias) is
{ } primitiveCollectionTranslation)
{
return primitiveCollectionTranslation;
}
}
return translated;
}

/// <inheritdoc />
protected override ShapedQueryExpression? TranslateMemberAccess(Expression source, MemberIdentity member)
{
// Attempt to translate access into a primitive collection property (i.e. array column)
if (_sqlTranslator.TryBindMember(_sqlTranslator.Visit(source), member, out var translatedExpression, out var property)
&& property is IProperty { IsPrimitiveCollection: true } regularProperty
&& translatedExpression is SqlExpression sqlExpression)
{
var tableAlias = sqlExpression switch
{
ColumnExpression c => c.Name[..1].ToLowerInvariant(),
JsonScalarExpression { Path: [.., { PropertyName: string propertyName }] } => propertyName[..1].ToLowerInvariant(),
_ => "j"
};

// For Contains over a collection parameter, if the provider hasn't implemented TranslateCollection (e.g. OPENJSON on SQL
// Server), we need to fall back to the previous IN translation.
if (method.IsGenericMethod
&& method.GetGenericMethodDefinition() == QueryableMethods.Contains
&& methodCallExpression.Arguments[0] is ParameterQueryRootExpression parameterSource
&& TranslateExpression(methodCallExpression.Arguments[1]) is SqlExpression item
&& _sqlTranslator.Visit(parameterSource.ParameterExpression) is SqlParameterExpression sqlParameterExpression)
if (TranslatePrimitiveCollection(sqlExpression, regularProperty, tableAlias) is
{ } primitiveCollectionTranslation)
{
var inExpression = _sqlExpressionFactory.In(item, sqlParameterExpression);
var selectExpression = new SelectExpression(inExpression);
var shaperExpression = Expression.Convert(
new ProjectionBindingExpression(selectExpression, new ProjectionMember(), typeof(bool?)), typeof(bool));
var shapedQueryExpression = new ShapedQueryExpression(selectExpression, shaperExpression)
.UpdateResultCardinality(ResultCardinality.Single);
return shapedQueryExpression;
return primitiveCollectionTranslation;
}
}

return translated;
return null;
}

/// <inheritdoc />
protected override ShapedQueryExpression? TranslateParameterQueryRoot(ParameterQueryRootExpression parameterQueryRootExpression)
{
var sqlParameterExpression =
_sqlTranslator.Visit(parameterQueryRootExpression.ParameterExpression) as SqlParameterExpression;

Check.DebugAssert(sqlParameterExpression is not null, "sqlParameterExpression is not null");

return TranslatePrimitiveCollection(
sqlParameterExpression,
property: null,
char.ToLowerInvariant(sqlParameterExpression.Name.First(c => c != '_')).ToString());
}

/// <summary>
Expand All @@ -310,7 +315,7 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp
/// <see langword="null" /> (no translation).
/// </summary>
/// <remarks>
/// Inline collections aren't passed to this method; see <see cref="VisitInlineQueryRoot" /> for the translation of inline
/// Inline collections aren't passed to this method; see <see cref="TranslateInlineQueryRoot" /> for the translation of inline
/// collections.
/// </remarks>
/// <param name="sqlExpression">The expression to try to translate as a primitive collection expression.</param>
Expand Down Expand Up @@ -346,7 +351,7 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp
/// </summary>
/// <param name="inlineQueryRootExpression">The inline collection to be translated.</param>
/// <returns>A queryable SQL VALUES expression.</returns>
protected virtual ShapedQueryExpression? VisitInlineQueryRoot(InlineQueryRootExpression inlineQueryRootExpression)
protected override ShapedQueryExpression? TranslateInlineQueryRoot(InlineQueryRootExpression inlineQueryRootExpression)
{
var elementType = inlineQueryRootExpression.ElementType;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -760,38 +760,6 @@ protected override Expression VisitMemberInit(MemberInitExpression memberInitExp
? sqlConstantExpression
: QueryCompilationContext.NotTranslatedExpression;

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
[EntityFrameworkInternal]
public virtual bool TryTranslatePropertyAccess(
Expression expression,
[NotNullWhen(true)] out Expression? translatedExpression,
[NotNullWhen(true)] out IPropertyBase? property)
{
if (expression is MethodCallExpression methodCallExpression)
{
if (methodCallExpression.TryGetEFPropertyArguments(out var source, out var propertyName)
&& TryBindMember(Visit(source), MemberIdentity.Create(propertyName), out translatedExpression, out property))
{
return true;
}

if (methodCallExpression.TryGetIndexerArguments(_model, out source, out propertyName)
&& TryBindMember(Visit(source), MemberIdentity.Create(propertyName), out translatedExpression, out property))
{
return true;
}
}

translatedExpression = null;
property = null;
return false;
}

/// <inheritdoc />
protected override Expression VisitMethodCall(MethodCallExpression methodCallExpression)
{
Expand Down Expand Up @@ -1258,7 +1226,14 @@ private bool TryBindMember(
[NotNullWhen(true)] out Expression? expression)
=> TryBindMember(source, member, out expression, out _);

private bool TryBindMember(
/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
[EntityFrameworkInternal]
public virtual bool TryBindMember(
Expression? source,
MemberIdentity member,
[NotNullWhen(true)] out Expression? expression,
Expand Down
Loading

0 comments on commit 5defd88

Please sign in to comment.