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

SQL row value comparison syntax #26936

Closed
wants to merge 12 commits into from
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,72 @@ public static TProperty Collate<TProperty>(
TProperty operand,
[NotParameterized] string collation)
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Collate)));

/// <summary>
/// A DbFunction method stub that can be used in LINQ queries to target SQL row value comparisons.
/// </summary>
/// <remarks>
/// <para>
/// See <see href="https://aka.ms/efcore-docs-database-functions">Database functions</see> for more information and examples.
/// </para>
/// </remarks>
/// <param name="_">The <see cref="DbFunctions" /> instance.</param>
/// <param name="columns">The columns on which the comparison will be performed.</param>
/// <param name="values">The values to compare with.</param>
public static bool LessThan(
this DbFunctions _,
object[] columns,
[NotParameterized] object[] values)
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(LessThan)));

/// <summary>
/// A DbFunction method stub that can be used in LINQ queries to target SQL row value comparisons.
/// </summary>
/// <remarks>
/// <para>
/// See <see href="https://aka.ms/efcore-docs-database-functions">Database functions</see> for more information and examples.
/// </para>
/// </remarks>
/// <param name="_">The <see cref="DbFunctions" /> instance.</param>
/// <param name="columns">The columns on which the comparison will be performed.</param>
/// <param name="values">The values to compare with.</param>
public static bool LessThanOrEqual(
this DbFunctions _,
object[] columns,
[NotParameterized] object[] values)
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(LessThanOrEqual)));

/// <summary>
/// A DbFunction method stub that can be used in LINQ queries to target SQL row value comparisons.
/// </summary>
/// <remarks>
/// <para>
/// See <see href="https://aka.ms/efcore-docs-database-functions">Database functions</see> for more information and examples.
/// </para>
/// </remarks>
/// <param name="_">The <see cref="DbFunctions" /> instance.</param>
/// <param name="columns">The columns on which the comparison will be performed.</param>
/// <param name="values">The values to compare with.</param>
public static bool GreaterThan(
this DbFunctions _,
object[] columns,
[NotParameterized] object[] values)
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(GreaterThan)));

/// <summary>
/// A DbFunction method stub that can be used in LINQ queries to target SQL row value comparisons.
/// </summary>
/// <remarks>
/// <para>
/// See <see href="https://aka.ms/efcore-docs-database-functions">Database functions</see> for more information and examples.
/// </para>
/// </remarks>
/// <param name="_">The <see cref="DbFunctions" /> instance.</param>
/// <param name="columns">The columns on which the comparison will be performed.</param>
/// <param name="values">The values to compare with.</param>
public static bool GreaterThanOrEqual(
this DbFunctions _,
object[] columns,
[NotParameterized] object[] values)
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(GreaterThanOrEqual)));
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/EFCore.Relational/Properties/RelationalStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,9 @@
<data name="InvalidMinBatchSize" xml:space="preserve">
<value>The specified 'MinBatchSize' value '{value}' is not valid. It must be a positive number.</value>
</data>
<data name="InvalidRowValueArgumentsCount" xml:space="preserve">
<value>Columns count and values count should be equal.</value>
</data>
<data name="LastUsedWithoutOrderBy" xml:space="preserve">
<value>Queries performing '{method}' operation must have a deterministic sort order. Rewrite the query to apply an 'OrderBy' operation on the sequence before calling '{method}'.</value>
</data>
Expand Down
24 changes: 24 additions & 0 deletions src/EFCore.Relational/Query/ISqlExpressionFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -469,4 +469,28 @@ SqlFunctionExpression NiladicFunction(
/// <param name="tableExpressionBase">A table source to project from.</param>
/// <returns>An expression representing a SELECT in a SQL tree.</returns>
SelectExpression Select(IEntityType entityType, TableExpressionBase tableExpressionBase);

/// <summary>
/// Creates a new <see cref="RowValueExpression" /> which represents a row value comparison in a SQL tree.
/// </summary>
/// <param name="operatorType">The operator to apply.</param>
/// <param name="columns">The columns on which the comparison will be performed.</param>
/// <param name="values">The values to compare with.</param>
/// <returns>An expression representing a row value comparison in a SQL tree.</returns>
RowValueExpression RowValue(
ExpressionType operatorType,
IReadOnlyList<SqlExpression> columns,
IReadOnlyList<object> values);

/// <summary>
/// Creates a <see cref="SqlBinaryExpression" /> which represents a row value expanded comparison in a SQL tree.
/// </summary>
/// <param name="operatorType">The operator to apply.</param>
/// <param name="columns">The columns on which the comparison will be performed.</param>
/// <param name="values">The values to compare with.</param>
/// <returns>An expression representing a row value expanded comparison in a SQL tree.</returns>
SqlBinaryExpression RowValueComparison(
ExpressionType operatorType,
IReadOnlyList<SqlExpression> columns,
IReadOnlyList<object> values);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.EntityFrameworkCore.Query.SqlExpressions;

namespace Microsoft.EntityFrameworkCore.Query.Internal;

/// <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 class RowValueComparisonTranslator : RowValueTranslator
{
/// <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 RowValueComparisonTranslator(ISqlExpressionFactory sqlExpressionFactory)
: base(sqlExpressionFactory)
{
}

/// <inheritdoc/>
protected override SqlExpression? CreateSqlExpression(
ExpressionType operatorType,
IReadOnlyList<SqlExpression> columns,
IReadOnlyList<object> values)
=> SqlExpressionFactory.RowValueComparison(operatorType, columns, values);
}
112 changes: 112 additions & 0 deletions src/EFCore.Relational/Query/Internal/RowValueTranslator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.EntityFrameworkCore.Query.SqlExpressions;

namespace Microsoft.EntityFrameworkCore.Query.Internal;

/// <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 class RowValueTranslator : IMethodCallTranslator
{
private static readonly MethodInfo _lessThanMethodInfo =
typeof(RelationalDbFunctionsExtensions).GetRequiredRuntimeMethod(
nameof(RelationalDbFunctionsExtensions.LessThan),
typeof(DbFunctions), typeof(object[]), typeof(object[]));

private static readonly MethodInfo _lessThanOrEqualMethodInfo =
typeof(RelationalDbFunctionsExtensions).GetRequiredRuntimeMethod(
nameof(RelationalDbFunctionsExtensions.LessThanOrEqual),
typeof(DbFunctions), typeof(object[]), typeof(object[]));

private static readonly MethodInfo _greaterThanMethodInfo =
typeof(RelationalDbFunctionsExtensions).GetRequiredRuntimeMethod(
nameof(RelationalDbFunctionsExtensions.GreaterThan),
typeof(DbFunctions), typeof(object[]), typeof(object[]));

private static readonly MethodInfo _greaterThanOrEqualMethodInfo =
typeof(RelationalDbFunctionsExtensions).GetRequiredRuntimeMethod(
nameof(RelationalDbFunctionsExtensions.GreaterThanOrEqual),
typeof(DbFunctions), typeof(object[]), typeof(object[]));

private static readonly Dictionary<MethodInfo, ExpressionType> _methodInfoOperatorTypeMap =
new()
{
{ _lessThanMethodInfo, ExpressionType.LessThan },
{ _lessThanOrEqualMethodInfo, ExpressionType.LessThanOrEqual },
{ _greaterThanMethodInfo, ExpressionType.GreaterThan },
{ _greaterThanOrEqualMethodInfo, ExpressionType.GreaterThanOrEqual },
};

/// <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 RowValueTranslator(ISqlExpressionFactory sqlExpressionFactory)
{
SqlExpressionFactory = sqlExpressionFactory;
}

/// <summary>
/// The <see cref="ISqlExpressionFactory"/>.
/// </summary>
protected ISqlExpressionFactory SqlExpressionFactory { get; }

/// <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 SqlExpression? Translate(
SqlExpression? instance,
MethodInfo method,
IReadOnlyList<SqlExpression> arguments,
IDiagnosticsLogger<DbLoggerCategory.Query> logger)
{
if (_methodInfoOperatorTypeMap.TryGetValue(method, out var operatorType))
{
var columns = UnwrapColumns(arguments[1]);
var values = UnwrapValues(arguments[2]);
return CreateSqlExpression(operatorType, columns, values);
}

return null;
}

private IReadOnlyList<SqlExpression> UnwrapColumns(SqlExpression sqlExpression)
{
return ((ArrayExpression)sqlExpression).Values
.Select(x => RemoveObjectConvert(x))
.ToList();
}

private IReadOnlyList<object> UnwrapValues(SqlExpression sqlExpression)
{
var constantValue = ((SqlConstantExpression)sqlExpression).Value!;
var valuesArray = (object[])constantValue;
return valuesArray.ToList();
}

/// <summary>
/// Creates the sql expression from the row value info.
/// </summary>
protected virtual SqlExpression? CreateSqlExpression(
ExpressionType operatorType,
IReadOnlyList<SqlExpression> columns,
IReadOnlyList<object> values)
=> SqlExpressionFactory.RowValue(operatorType, columns, values);

private SqlExpression RemoveObjectConvert(SqlExpression expression)
=> expression is SqlUnaryExpression sqlUnaryExpression
&& sqlUnaryExpression.OperatorType == ExpressionType.Convert
&& sqlUnaryExpression.Type == typeof(object)
? sqlUnaryExpression.Operand
: expression;
}
36 changes: 36 additions & 0 deletions src/EFCore.Relational/Query/QuerySqlGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1140,4 +1140,40 @@ protected override Expression VisitUnion(UnionExpression unionExpression)

return unionExpression;
}

/// <inheritdoc />
protected override Expression VisitRowValue(RowValueExpression rowValueExpression)
{
var count = rowValueExpression.Columns.Count;

_relationalCommandBuilder.Append("(");
for (var i = 0; i < count; i++)
{
Visit(rowValueExpression.Columns[i]);

if (i < count - 1)
{
_relationalCommandBuilder.Append(", ");
}
}
_relationalCommandBuilder.Append(")");

var @operator = _operatorMap[rowValueExpression.OperatorType];
_relationalCommandBuilder.Append(@operator);

_relationalCommandBuilder.Append("(");
for (var i = 0; i < count; i++)
{
_relationalCommandBuilder.Append(
rowValueExpression.ValuesTypeMappings[i].GenerateSqlLiteral(rowValueExpression.Values[i]));

if (i < count - 1)
{
_relationalCommandBuilder.Append(", ");
}
}
_relationalCommandBuilder.Append(")");

return rowValueExpression;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ public RelationalMethodCallTranslatorProvider(RelationalMethodCallTranslatorProv
new GetValueOrDefaultTranslator(sqlExpressionFactory),
new ComparisonTranslator(sqlExpressionFactory),
new ByteArraySequenceEqualTranslator(sqlExpressionFactory),
new RandomTranslator(sqlExpressionFactory)
new RandomTranslator(sqlExpressionFactory),
new RowValueTranslator(sqlExpressionFactory),
});
_sqlExpressionFactory = sqlExpressionFactory;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -667,7 +667,20 @@ protected override Expression VisitNew(NewExpression newExpression)

/// <inheritdoc />
protected override Expression VisitNewArray(NewArrayExpression newArrayExpression)
=> QueryCompilationContext.NotTranslatedExpression;
{
var sqlExpressions = new List<SqlExpression>(newArrayExpression.Expressions.Count);
foreach (var expression in newArrayExpression.Expressions)
{
if (TranslationFailed(expression, Visit(expression), out var sqlExpression))
{
return QueryCompilationContext.NotTranslatedExpression;
}

sqlExpressions.Add(sqlExpression!);
}

return new ArrayExpression(sqlExpressions);
}

/// <inheritdoc />
protected override Expression VisitParameter(ParameterExpression parameterExpression)
Expand Down
Loading