Skip to content

Commit

Permalink
Reimplement via ProjectionExpression.IsValueProjection
Browse files Browse the repository at this point in the history
  • Loading branch information
roji committed Jul 5, 2024
1 parent e2ff6ba commit 3070d21
Show file tree
Hide file tree
Showing 3 changed files with 52 additions and 22 deletions.
40 changes: 23 additions & 17 deletions src/EFCore.Cosmos/Query/Internal/CosmosQuerySqlGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -209,16 +209,28 @@ protected override Expression VisitScalarSubquery(ScalarSubqueryExpression scala
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
protected override Expression VisitProjection(ProjectionExpression projectionExpression)
=> VisitProjection(projectionExpression, objectProjectionStyle: false);
{
GenerateProjection(projectionExpression, objectProjectionStyle: false);
return projectionExpression;
}

/// <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>
protected virtual Expression VisitProjection(ProjectionExpression projectionExpression, bool objectProjectionStyle)
private void GenerateProjection(ProjectionExpression projectionExpression, bool objectProjectionStyle)
{
// If the SELECT projects a single value out, we just project that with the Cosmos VALUE keyword (without VALUE,
// Cosmos projects a JSON object containing the value).
if (projectionExpression.IsValueProjection)
{
_sqlBuilder.Append("VALUE ");
Visit(projectionExpression.Expression);
return;
}

if (objectProjectionStyle)
{
_sqlBuilder.Append('"').Append(projectionExpression.Alias).Append("\" : ");
Expand All @@ -232,8 +244,6 @@ protected virtual Expression VisitProjection(ProjectionExpression projectionExpr
{
_sqlBuilder.Append(" AS " + projectionExpression.Alias);
}

return projectionExpression;
}

/// <summary>
Expand Down Expand Up @@ -279,26 +289,22 @@ protected override Expression VisitSelect(SelectExpression selectExpression)

if (selectExpression.Projection is { Count: > 0 } projection)
{
// If the SELECT projects a single value out, we just project that with the Cosmos VALUE keyword (without VALUE,
// Cosmos projects a JSON object containing the value).
if (projection is [var singleProjection])
{
_sqlBuilder.Append("VALUE ");
Check.DebugAssert(
projection.Count == 1 || !projection.Any(p => p.IsValueProjection),
"Multiple projections with IsValueProjection");

Visit(singleProjection.Expression);
}
// Otherwise, we'll project a JSON object; Cosmos has two syntaxes for doing so:
// 1. Project out a JSON object as a value (SELECT VALUE { 'a': a, 'b': b }), or
// 2. Project a set of properties with optional AS+aliases (SELECT 'a' AS a, 'b' AS b)
// Both methods produce the exact same results; we usually prefer the 1st, but in some cases we use the 2nd.
else if ((projection.Count > 1
// Cosmos does not support "AS Value" projections, specifically for the alias "Value"
|| projection is [{ Alias: var alias }] && alias.Equals("value", StringComparison.OrdinalIgnoreCase))
&& projection.Any(p => !string.IsNullOrEmpty(p.Alias) && p.Alias != p.Name)
&& !projection.Any(p => p.Expression is SqlFunctionExpression)) // Aggregates are not allowed
if ((projection.Count > 1
// Cosmos does not support "AS Value" projections, specifically for the alias "Value"
|| projection is [{ Alias: var alias }] && alias.Equals("value", StringComparison.OrdinalIgnoreCase))
&& projection.Any(p => !string.IsNullOrEmpty(p.Alias) && p.Alias != p.Name)
&& !projection.Any(p => p.Expression is SqlFunctionExpression)) // Aggregates are not allowed
{
_sqlBuilder.AppendLine("VALUE").AppendLine("{").IncrementIndent();
GenerateList(projection, e => VisitProjection(e, objectProjectionStyle: true), joinAction: sql => sql.AppendLine(","));
GenerateList(projection, e => GenerateProjection(e, objectProjectionStyle: true), joinAction: sql => sql.AppendLine(","));
_sqlBuilder.AppendLine().DecrementIndent().Append("}");
}
else
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,17 @@ namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal;
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
[DebuggerDisplay("{Microsoft.EntityFrameworkCore.Query.ExpressionPrinter.Print(this), nq}")]
public class ProjectionExpression(Expression expression, string alias)
public class ProjectionExpression(Expression expression, string alias, bool isValueProjection)
: Expression, IPrintableExpression
{
/// <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>
public virtual Expression Expression { get; } = expression;

/// <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
Expand All @@ -28,7 +36,7 @@ public class ProjectionExpression(Expression expression, string alias)
/// 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>
public virtual Expression Expression { get; } = expression;
public virtual bool IsValueProjection { get; } = isValueProjection;

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
Expand Down Expand Up @@ -74,7 +82,7 @@ protected override Expression VisitChildren(ExpressionVisitor visitor)
/// </summary>
public virtual ProjectionExpression Update(Expression expression)
=> expression != Expression
? new ProjectionExpression(expression, Alias)
? new ProjectionExpression(expression, Alias, IsValueProjection)
: this;

/// <summary>
Expand Down
20 changes: 18 additions & 2 deletions src/EFCore.Cosmos/Query/Internal/Expressions/SelectExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ public static SelectExpression CreateForCollection(Expression sourceExpression,
if (!SourceExpression.IsCompatible(sourceExpression))
{
sourceExpression = new SelectExpression(
[new ProjectionExpression(sourceExpression, null!)],
[new ProjectionExpression(sourceExpression, alias: null!, isValueProjection: true)],
sources: [],
orderings: []);
}
Expand Down Expand Up @@ -262,6 +262,15 @@ public void ReplaceProjectionMapping(IDictionary<ProjectionMember, Expression> p
public int AddToProjection(Expression sqlExpression)
=> AddToProjection(sqlExpression, null);

/// <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>
public int AddToProjection(EntityProjectionExpression entityProjection)
=> AddToProjection(entityProjection, null);

private int AddToProjection(Expression expression, string? alias)
{
var existingIndex = _projection.FindIndex(pe => pe.Expression.Equals(expression));
Expand All @@ -281,7 +290,14 @@ private int AddToProjection(Expression expression, string? alias)
currentAlias = $"{baseAlias}{counter++}";
}

_projection.Add(new ProjectionExpression(expression, currentAlias));
// Add the projection; if it's the only one, then it's a Cosmos VALUE projection (i.e. SELECT VALUE f).
// If we add a 2nd projection, go back and remove the VALUE modifier from the 1st one. This is also why we need to have a valid
// alias for the 1st projection, even if it's a VALUE projection (where no alias actually gets generated in SQL).
_projection.Add(new ProjectionExpression(expression, currentAlias, isValueProjection: _projection.Count == 0));
if (_projection.Count == 2)
{
_projection[0] = new ProjectionExpression(_projection[0].Expression, _projection[0].Alias, isValueProjection: false);
}

return _projection.Count - 1;
}
Expand Down

0 comments on commit 3070d21

Please sign in to comment.