未验证 提交 423cc582 编写于 作者: S Shay Rojansky 提交者: GitHub

Pattern-match to PG row value comparison (#2112)

Closes #2111
上级 78e63c4c
......@@ -228,6 +228,7 @@
<s:Boolean x:Key="/Default/UserDictionary/Words/=noda/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=nummultirange/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=numrange/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Postgre/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=pushdown/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=remapper/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=requiredness/@EntryIndexedValue">True</s:Boolean>
......
......@@ -129,6 +129,7 @@ public static IServiceCollection AddEntityFrameworkNpgsql(this IServiceCollectio
.TryAdd<ISingletonOptions, INpgsqlOptions>(p => p.GetRequiredService<INpgsqlOptions>())
.TryAdd<IValueConverterSelector, NpgsqlValueConverterSelector>()
.TryAdd<IQueryCompilationContextFactory, NpgsqlQueryCompilationContextFactory>()
.TryAdd<IQueryTranslationPostprocessorFactory, NpgsqlQueryTranslationPostprocessorFactory>()
.TryAddProviderSpecificServices(
b => b
.TryAddSingleton<INpgsqlValueGeneratorCache, NpgsqlValueGeneratorCache>()
......
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using Microsoft.EntityFrameworkCore.Query;
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
using Microsoft.EntityFrameworkCore.Utilities;
namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal
{
/// <summary>
/// An expression that represents a PostgreSQL-specific row value expression in a SQL tree.
/// </summary>
/// <remarks>
/// See the <see href="https://www.postgresql.org/docs/current/sql-expressions.html#SQL-SYNTAX-ROW-CONSTRUCTORS">PostgreSQL docs</see>
/// for more information.
/// </remarks>
public class PostgresRowValueExpression : SqlExpression, IEquatable<PostgresRowValueExpression>
{
/// <summary>
/// The operator of this PostgreSQL binary operation.
/// </summary>
public virtual IReadOnlyList<SqlExpression> RowValues { get; }
/// <inheritdoc />
public PostgresRowValueExpression(IReadOnlyList<SqlExpression> rowValues)
: base(typeof(Array), typeMapping: null)
{
Check.NotNull(rowValues, nameof(rowValues));
RowValues = rowValues;
}
public virtual PostgresRowValueExpression Prepend(SqlExpression expression)
{
var newRowValues = new SqlExpression[RowValues.Count + 1];
newRowValues[0] = expression;
for (var i = 1; i < newRowValues.Length; i++)
{
newRowValues[i] = RowValues[i - 1];
}
return new PostgresRowValueExpression(newRowValues);
}
/// <inheritdoc />
protected override Expression VisitChildren(ExpressionVisitor visitor)
{
Check.NotNull(visitor, nameof(visitor));
SqlExpression[]? newRowValues = null;
for (var i = 0; i < RowValues.Count; i++)
{
var rowValue = RowValues[i];
var visited = (SqlExpression)visitor.Visit(rowValue);
if (visited != rowValue && newRowValues is null)
{
newRowValues = new SqlExpression[RowValues.Count];
for (var j = 0; j < i; i++)
{
newRowValues[j] = RowValues[j];
}
}
if (newRowValues is not null)
{
newRowValues[i] = visited;
}
}
return newRowValues is null ? this : new PostgresRowValueExpression(newRowValues);
}
/// <inheritdoc />
protected override void Print(ExpressionPrinter expressionPrinter)
{
expressionPrinter.Append("(");
var count = RowValues.Count;
for (var i = 0; i < count; i++)
{
expressionPrinter.Visit(RowValues[i]);
if (i < count - 1)
{
expressionPrinter.Append(", ");
}
}
expressionPrinter.Append(")");
}
/// <inheritdoc />
public override bool Equals(object? obj)
=> obj is PostgresRowValueExpression other && Equals(other);
/// <inheritdoc />
public virtual bool Equals(PostgresRowValueExpression? other)
{
if (other is null || !base.Equals(other) || other.RowValues.Count != RowValues.Count)
{
return false;
}
if (ReferenceEquals(this, other))
{
return true;
}
for (var i = 0; i < RowValues.Count; i++)
{
if (other.RowValues[i].Equals(RowValues[i]))
{
return false;
}
}
return true;
}
/// <inheritdoc />
public override int GetHashCode()
{
var hashCode = new HashCode();
foreach (var rowValue in RowValues)
{
hashCode.Add(rowValue);
}
return hashCode.ToHashCode();
}
}
}
\ No newline at end of file
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq.Expressions;
using Microsoft.EntityFrameworkCore.Query;
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal;
namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.Internal
{
/// <summary>
/// Performs various PostgreSQL-specific optimizations to the SQL expression tree.
/// </summary>
public class NpgsqlPostgresSqlOptimizingVisitor : ExpressionVisitor
{
private readonly ISqlExpressionFactory _sqlExpressionFactory;
public NpgsqlPostgresSqlOptimizingVisitor(ISqlExpressionFactory sqlExpressionFactory)
=> _sqlExpressionFactory = sqlExpressionFactory;
[return: NotNullIfNotNull("expression")]
public override Expression? Visit(Expression? expression)
{
switch (expression)
{
case ShapedQueryExpression shapedQueryExpression:
return shapedQueryExpression.Update(
Visit(shapedQueryExpression.QueryExpression),
shapedQueryExpression.ShaperExpression);
case SqlBinaryExpression { OperatorType: ExpressionType.OrElse } sqlBinaryExpression:
{
var visited = (SqlBinaryExpression)base.Visit(sqlBinaryExpression);
var x = TryConvertToRowValueComparison(visited, out var rowComparisonExpression)
? rowComparisonExpression
: visited;
return x;
}
default:
return base.Visit(expression);
}
}
private bool TryConvertToRowValueComparison(
SqlBinaryExpression sqlOrExpression,
[NotNullWhen(true)] out SqlBinaryExpression? rowComparisonExpression)
{
// This pattern matches x > 5 || x == 5 && y > 6, and converts it to (x, y) > (5, 6)
Debug.Assert(sqlOrExpression.OperatorType == ExpressionType.OrElse);
if (TryMatchTopExpression(sqlOrExpression.Left, sqlOrExpression.Right, out rowComparisonExpression)
|| TryMatchTopExpression(sqlOrExpression.Right, sqlOrExpression.Left, out rowComparisonExpression))
{
return true;
}
rowComparisonExpression = null;
return false;
bool TryMatchTopExpression(
SqlExpression first,
SqlExpression second,
[NotNullWhen(true)] out SqlBinaryExpression? rowComparisonExpression)
{
if (first is SqlBinaryExpression { OperatorType: ExpressionType.AndAlso } subExpression
&& second is SqlBinaryExpression comparisonExpression1
&& IsComparisonOperator(comparisonExpression1.OperatorType))
{
if (TryMatchBottomExpression(subExpression.Left, subExpression.Right, out var equalityExpression, out var comparisonExpression2)
|| TryMatchBottomExpression(subExpression.Right, subExpression.Left, out equalityExpression, out comparisonExpression2))
{
// We've found a structural match. Now make sure the operands and operators correspond
if (comparisonExpression1.Left.Equals(equalityExpression.Left)
&& comparisonExpression1.Right.Equals(equalityExpression.Right)
&& comparisonExpression1.OperatorType == comparisonExpression2.OperatorType)
{
// Bingo.
// If we're composing over an existing row value comparison, just prepend the new element to it.
// Otherwise create a new row value comparison expression.
if (comparisonExpression2.Left is PostgresRowValueExpression leftRowValueExpression)
{
var rightRowValueExpression = (PostgresRowValueExpression)comparisonExpression2.Right;
rowComparisonExpression = _sqlExpressionFactory.MakeBinary(
comparisonExpression1.OperatorType,
leftRowValueExpression.Prepend(comparisonExpression1.Left),
rightRowValueExpression.Prepend(comparisonExpression1.Right),
typeMapping: null)!;
}
else
{
rowComparisonExpression = _sqlExpressionFactory.MakeBinary(
comparisonExpression1.OperatorType,
new PostgresRowValueExpression(new[] { comparisonExpression1.Left, comparisonExpression2.Left }),
new PostgresRowValueExpression(new[] { comparisonExpression1.Right, comparisonExpression2.Right }),
typeMapping: null)!;
}
return true;
}
}
}
rowComparisonExpression = null;
return false;
static bool TryMatchBottomExpression(
SqlExpression first,
SqlExpression second,
[NotNullWhen(true)] out SqlBinaryExpression? equalityExpression,
[NotNullWhen(true)] out SqlBinaryExpression? comparisonExpression)
{
// This pattern matches the bottom expression of the pattern: x == 5 && y > 6
if (first is SqlBinaryExpression { OperatorType: ExpressionType.Equal } equalityExpression2
&& second is SqlBinaryExpression comparisonExpression2
&& IsComparisonOperator(comparisonExpression2.OperatorType))
{
equalityExpression = equalityExpression2;
comparisonExpression = comparisonExpression2;
return true;
}
equalityExpression = comparisonExpression = null;
return false;
}
}
}
private static bool IsComparisonOperator(ExpressionType expressionType)
=> expressionType switch
{
ExpressionType.GreaterThan => true,
ExpressionType.GreaterThanOrEqual => true,
ExpressionType.LessThan => true,
ExpressionType.LessThanOrEqual => true,
_ => false
};
}
}
......@@ -58,6 +58,7 @@ protected override Expression VisitExtension(Expression extensionExpression)
PostgresJsonTraversalExpression jsonTraversalExpression => VisitJsonPathTraversal(jsonTraversalExpression),
PostgresNewArrayExpression newArrayExpression => VisitPostgresNewArray(newArrayExpression),
PostgresRegexMatchExpression regexMatchExpression => VisitRegexMatch(regexMatchExpression),
PostgresRowValueExpression rowValueExpression => VisitRowValue(rowValueExpression),
PostgresUnknownBinaryExpression unknownBinaryExpression => VisitUnknownBinary(unknownBinaryExpression),
_ => base.VisitExtension(extensionExpression)
};
......@@ -556,6 +557,27 @@ public virtual Expression VisitRegexMatch(PostgresRegexMatchExpression expressio
return expression;
}
public virtual Expression VisitRowValue(PostgresRowValueExpression rowValueExpression)
{
Sql.Append("(");
var values = rowValueExpression.RowValues;
var count = values.Count;
for (var i = 0; i < count; i++)
{
Visit(values[i]);
if (i < count - 1)
{
Sql.Append(", ");
}
}
Sql.Append(")");
return rowValueExpression;
}
/// <summary>
/// Visits the children of an <see cref="PostgresILikeExpression"/>.
/// </summary>
......
using System.Linq.Expressions;
using Microsoft.EntityFrameworkCore.Query;
namespace Npgsql.EntityFrameworkCore.PostgreSQL.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 NpgsqlQueryTranslationPostprocessor : RelationalQueryTranslationPostprocessor
{
private readonly NpgsqlPostgresSqlOptimizingVisitor _npgsqlPostgresSqlOptimizingVisitor;
/// <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 NpgsqlQueryTranslationPostprocessor(
QueryTranslationPostprocessorDependencies dependencies,
RelationalQueryTranslationPostprocessorDependencies relationalDependencies,
QueryCompilationContext queryCompilationContext)
: base(dependencies, relationalDependencies, queryCompilationContext)
=> _npgsqlPostgresSqlOptimizingVisitor = new(relationalDependencies.SqlExpressionFactory);
/// <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 override Expression Process(Expression query)
{
query = _npgsqlPostgresSqlOptimizingVisitor.Visit(query);
query = base.Process(query);
return query;
}
}
}
using Microsoft.EntityFrameworkCore.Query;
namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.Internal
{
public class NpgsqlQueryTranslationPostprocessorFactory : IQueryTranslationPostprocessorFactory
{
/// <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 NpgsqlQueryTranslationPostprocessorFactory(
QueryTranslationPostprocessorDependencies dependencies,
RelationalQueryTranslationPostprocessorDependencies relationalDependencies)
{
Dependencies = dependencies;
RelationalDependencies = relationalDependencies;
}
/// <summary>
/// Dependencies for this service.
/// </summary>
protected virtual QueryTranslationPostprocessorDependencies Dependencies { get; }
/// <summary>
/// Relational provider-specific dependencies for this service.
/// </summary>
protected virtual RelationalQueryTranslationPostprocessorDependencies RelationalDependencies { 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 QueryTranslationPostprocessor Create(QueryCompilationContext queryCompilationContext)
{
return new NpgsqlQueryTranslationPostprocessor(
Dependencies,
RelationalDependencies,
queryCompilationContext);
}
}
}
......@@ -28,38 +28,35 @@ public class NpgsqlSqlNullabilityProcessor : SqlNullabilityProcessor
/// <inheritdoc />
protected override SqlExpression VisitCustomSqlExpression(
SqlExpression sqlExpression, bool allowOptimizedExpansion, out bool nullable)
SqlExpression sqlExpression,
bool allowOptimizedExpansion,
out bool nullable)
=> sqlExpression switch
{
PostgresAnyExpression postgresAnyExpression
=> VisitAny(postgresAnyExpression, allowOptimizedExpansion, out nullable),
=> VisitAny(postgresAnyExpression, allowOptimizedExpansion, out nullable),
PostgresAllExpression postgresAllExpression
=> VisitAll(postgresAllExpression, allowOptimizedExpansion, out nullable),
=> VisitAll(postgresAllExpression, allowOptimizedExpansion, out nullable),
PostgresArrayIndexExpression arrayIndexExpression
=> VisitArrayIndex(arrayIndexExpression, allowOptimizedExpansion, out nullable),
=> VisitArrayIndex(arrayIndexExpression, allowOptimizedExpansion, out nullable),
PostgresBinaryExpression binaryExpression
=> VisitBinary(binaryExpression, allowOptimizedExpansion, out nullable),
=> VisitBinary(binaryExpression, allowOptimizedExpansion, out nullable),
PostgresILikeExpression ilikeExpression
=> VisitILike(ilikeExpression, allowOptimizedExpansion, out nullable),
=> VisitILike(ilikeExpression, allowOptimizedExpansion, out nullable),
PostgresNewArrayExpression newArrayExpression
=> VisitNewArray(newArrayExpression, allowOptimizedExpansion, out nullable),
=> VisitNewArray(newArrayExpression, allowOptimizedExpansion, out nullable),
PostgresRegexMatchExpression regexMatchExpression
=> VisitRegexMatch(regexMatchExpression, allowOptimizedExpansion, out nullable),
=> VisitRegexMatch(regexMatchExpression, allowOptimizedExpansion, out nullable),
PostgresJsonTraversalExpression postgresJsonTraversalExpression
=> VisitJsonTraversal(postgresJsonTraversalExpression, allowOptimizedExpansion, out nullable),
=> VisitJsonTraversal(postgresJsonTraversalExpression, allowOptimizedExpansion, out nullable),
PostgresRowValueExpression postgresRowValueExpression
=> VisitRowValueExpression(postgresRowValueExpression, allowOptimizedExpansion, out nullable),
PostgresUnknownBinaryExpression postgresUnknownBinaryExpression
=> VisitUnknownBinary(postgresUnknownBinaryExpression, allowOptimizedExpansion, out nullable),
=> VisitUnknownBinary(postgresUnknownBinaryExpression, allowOptimizedExpansion, out nullable),
_ => base.VisitCustomSqlExpression(sqlExpression, allowOptimizedExpansion, out nullable)
};
/// <summary>
/// Visits a <see cref="PostgresAnyExpression" /> and computes its nullability.
/// </summary>
/// <param name="anyExpression">A <see cref="PostgresAnyExpression" /> expression to visit.</param>
/// <param name="allowOptimizedExpansion">A bool value indicating if optimized expansion which considers null value as false value is allowed.</param>
/// <param name="nullable">A bool value indicating whether the sql expression is nullable.</param>
/// <returns>An optimized sql expression.</returns>
protected virtual SqlExpression VisitAny(
PostgresAnyExpression anyExpression, bool allowOptimizedExpansion, out bool nullable)
{
......@@ -128,13 +125,6 @@ PostgresUnknownBinaryExpression postgresUnknownBinaryExpression
typeof(int)))));
}
/// <summary>
/// Visits a <see cref="PostgresAnyExpression" /> and computes its nullability.
/// </summary>
/// <param name="allExpression">A <see cref="PostgresAnyExpression" /> expression to visit.</param>
/// <param name="allowOptimizedExpansion">A bool value indicating if optimized expansion which considers null value as false value is allowed.</param>
/// <param name="nullable">A bool value indicating whether the sql expression is nullable.</param>
/// <returns>An optimized sql expression.</returns>
protected virtual SqlExpression VisitAll(
PostgresAllExpression allExpression, bool allowOptimizedExpansion, out bool nullable)
{
......@@ -157,15 +147,6 @@ PostgresUnknownBinaryExpression postgresUnknownBinaryExpression
return updated;
}
/// <summary>
/// Visits an <see cref="PostgresArrayIndexExpression" /> and computes its nullability.
/// </summary>
/// <remarks>
/// </remarks>
/// <param name="arrayIndexExpression">A <see cref="PostgresArrayIndexExpression" /> expression to visit.</param>
/// <param name="allowOptimizedExpansion">A bool value indicating if optimized expansion which considers null value as false value is allowed.</param>
/// <param name="nullable">A bool value indicating whether the sql expression is nullable.</param>
/// <returns>An optimized sql expression.</returns>
protected virtual SqlExpression VisitArrayIndex(
PostgresArrayIndexExpression arrayIndexExpression, bool allowOptimizedExpansion, out bool nullable)
{
......@@ -179,15 +160,6 @@ PostgresUnknownBinaryExpression postgresUnknownBinaryExpression
return arrayIndexExpression.Update(array, index);
}
/// <summary>
/// Visits a <see cref="PostgresBinaryExpression" /> and computes its nullability.
/// </summary>
/// <remarks>
/// </remarks>
/// <param name="binaryExpression">A <see cref="PostgresBinaryExpression" /> expression to visit.</param>
/// <param name="allowOptimizedExpansion">A bool value indicating if optimized expansion which considers null value as false value is allowed.</param>
/// <param name="nullable">A bool value indicating whether the sql expression is nullable.</param>
/// <returns>An optimized sql expression.</returns>
protected virtual SqlExpression VisitBinary(
PostgresBinaryExpression binaryExpression, bool allowOptimizedExpansion, out bool nullable)
{
......@@ -213,13 +185,6 @@ PostgresUnknownBinaryExpression postgresUnknownBinaryExpression
return binaryExpression.Update(left, right);
}
/// <summary>
/// Visits a <see cref="PostgresILikeExpression" /> and computes its nullability.
/// </summary>
/// <param name="iLikeExpression">A <see cref="PostgresILikeExpression" /> expression to visit.</param>
/// <param name="allowOptimizedExpansion">A bool value indicating if optimized expansion which considers null value as false value is allowed.</param>
/// <param name="nullable">A bool value indicating whether the sql expression is nullable.</param>
/// <returns>An optimized sql expression.</returns>
protected virtual SqlExpression VisitILike(
PostgresILikeExpression iLikeExpression, bool allowOptimizedExpansion, out bool nullable)
{
......@@ -240,15 +205,6 @@ PostgresUnknownBinaryExpression postgresUnknownBinaryExpression
: visited;
}
/// <summary>
/// Visits a <see cref="PostgresNewArrayExpression" /> and computes its nullability.
/// </summary>
/// <remarks>
/// </remarks>
/// <param name="newArrayExpression">A <see cref="PostgresNewArrayExpression" /> expression to visit.</param>
/// <param name="allowOptimizedExpansion">A bool value indicating if optimized expansion which considers null value as false value is allowed.</param>
/// <param name="nullable">A bool value indicating whether the sql expression is nullable.</param>
/// <returns>An optimized sql expression.</returns>
protected virtual SqlExpression VisitNewArray(
PostgresNewArrayExpression newArrayExpression, bool allowOptimizedExpansion, out bool nullable)
{
......@@ -280,15 +236,6 @@ PostgresUnknownBinaryExpression postgresUnknownBinaryExpression
: new PostgresNewArrayExpression(newInitializers, newArrayExpression.Type, newArrayExpression.TypeMapping);
}
/// <summary>
/// Visits a <see cref="PostgresRegexMatchExpression" /> and computes its nullability.
/// </summary>
/// <remarks>
/// </remarks>
/// <param name="regexMatchExpression">A <see cref="PostgresRegexMatchExpression" /> expression to visit.</param>
/// <param name="allowOptimizedExpansion">A bool value indicating if optimized expansion which considers null value as false value is allowed.</param>
/// <param name="nullable">A bool value indicating whether the sql expression is nullable.</param>
/// <returns>An optimized sql expression.</returns>
protected virtual SqlExpression VisitRegexMatch(
PostgresRegexMatchExpression regexMatchExpression, bool allowOptimizedExpansion, out bool nullable)
{
......@@ -302,15 +249,6 @@ PostgresUnknownBinaryExpression postgresUnknownBinaryExpression
return regexMatchExpression.Update(match, pattern);
}
/// <summary>
/// Visits a <see cref="PostgresJsonTraversalExpression" /> and computes its nullability.
/// </summary>
/// <remarks>
/// </remarks>
/// <param name="jsonTraversalExpression">A <see cref="PostgresJsonTraversalExpression" /> expression to visit.</param>
/// <param name="allowOptimizedExpansion">A bool value indicating if optimized expansion which considers null value as false value is allowed.</param>
/// <param name="nullable">A bool value indicating whether the sql expression is nullable.</param>
/// <returns>An optimized sql expression.</returns>
protected virtual SqlExpression VisitJsonTraversal(
PostgresJsonTraversalExpression jsonTraversalExpression, bool allowOptimizedExpansion, out bool nullable)
{
......@@ -342,11 +280,15 @@ PostgresUnknownBinaryExpression postgresUnknownBinaryExpression
// See #1851 for optimizing this for JSON POCO mapping.
nullable = true;
return jsonTraversalExpression.Update(
expression,
newPath is null
? jsonTraversalExpression.Path
: newPath.ToArray());
return jsonTraversalExpression.Update(expression, newPath?.ToArray() ?? jsonTraversalExpression.Path);
}
protected virtual SqlExpression VisitRowValueExpression(
PostgresRowValueExpression rowValueExpression, bool allowOptimizedExpansion, out bool nullable)
{
nullable = false;
return rowValueExpression;
}
/// <summary>
......
......@@ -418,7 +418,17 @@ protected override Expression VisitBinary(BinaryExpression binaryExpression)
_sqlExpressionFactory.ArrayIndex(sqlLeft!, _sqlExpressionFactory.GenerateOneBasedIndexExpression(sqlRight!));
}
return base.VisitBinary(binaryExpression);
var translated = base.VisitBinary(binaryExpression);
// Translate x > 5 || (x == 5 && y > 6) -> (x, y) > (5, 6) to SQL standard
// https://www.postgresql.org/docs/current/functions-comparisons.html#ROW-WISE-COMPARISON
if (translated is SqlBinaryExpression { OperatorType: ExpressionType.Or })
{
}
return translated;
}
protected override Expression VisitNew(NewExpression newExpression)
......
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Query;
using Microsoft.EntityFrameworkCore.TestUtilities;
using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities;
using Xunit;
using Xunit.Abstractions;
namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query
{
public class PostgresOptimizationsQueryTest : QueryTestBase<PostgresOptimizationsQueryTest.PostgresOptimizationsQueryFixture>
{
// ReSharper disable once UnusedParameter.Local
public PostgresOptimizationsQueryTest(PostgresOptimizationsQueryFixture fixture, ITestOutputHelper testOutputHelper)
: base(fixture)
{
Fixture.TestSqlLoggerFactory.Clear();
Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper);
}
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public async Task Row_value_comparison_two_items(bool async)
{
await AssertQuery(
async,
ss => ss.Set<Entity>().Where(e => e.X > 5 || e.X == 5 && e.Y > 6),
entryCount: 1);
AssertSql(
@"SELECT e.""Id"", e.""X"", e.""Y"", e.""Z""
FROM ""Entities"" AS e
WHERE (e.""X"", e.""Y"") > (5, 6)");
}
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public async Task Row_value_comparison_not_rewritten_with_incompatible_operators(bool async)
{
await AssertQuery(
async,
ss => ss.Set<Entity>().Where(e => e.X > 5 || e.X == 5 && e.Y < 6),
entryCount: 0);
AssertSql(
@"SELECT e.""Id"", e.""X"", e.""Y"", e.""Z""
FROM ""Entities"" AS e
WHERE (e.""X"" > 5) OR ((e.""X"" = 5) AND (e.""Y"" < 6))");
}
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public async Task Row_value_comparison_not_rewritten_with_incompatible_operands(bool async)
{
await AssertQuery(
async,
ss => ss.Set<Entity>().Where(e => e.Z > 5 || e.X == 5 && e.Y > 6),
entryCount: 2);
AssertSql(
@"SELECT e.""Id"", e.""X"", e.""Y"", e.""Z""
FROM ""Entities"" AS e
WHERE (e.""Z"" > 5) OR ((e.""X"" = 5) AND (e.""Y"" > 6))");
}
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public async Task Row_value_comparison_three_items(bool async)
{
await AssertQuery(
async,
ss => ss.Set<Entity>().Where(e => e.X > 5 || e.X == 5 && (e.Y > 6 || e.Y == 6 && e.Z > 7)),
entryCount: 1);
AssertSql(
@"SELECT e.""Id"", e.""X"", e.""Y"", e.""Z""
FROM ""Entities"" AS e
WHERE (e.""X"", e.""Y"", e.""Z"") > (5, 6, 7)");
}
#region Support
private void AssertSql(params string[] expected)
=> Fixture.TestSqlLoggerFactory.AssertBaseline(expected);
public class PostgresOptimizationsQueryContext : PoolableDbContext
{
public DbSet<Entity> Entities { get; set; }
public PostgresOptimizationsQueryContext(DbContextOptions options) : base(options) {}
public static void Seed(PostgresOptimizationsQueryContext context)
{
context.Entities.AddRange(PostgresOptimizationsData.CreateEntities());
context.SaveChanges();
}
}
public class Entity
{
public int Id { get; set; }
public int X { get; set; }
public int Y { get; set; }
public int Z { get; set; }
}
public class PostgresOptimizationsQueryFixture : SharedStoreFixtureBase<PostgresOptimizationsQueryContext>, IQueryFixtureBase
{
protected override string StoreName => "PostgresOptimizationsQueryTest";
// Set the PostgreSQL TimeZone parameter to something local, to ensure that operations which take TimeZone into account
// don't depend on the database's time zone, and also that operations which shouldn't take TimeZone into account indeed
// don't.
protected override ITestStoreFactory TestStoreFactory
=> NpgsqlTestStoreFactory.WithConnectionStringOptions("-c TimeZone=Europe/Berlin");
public TestSqlLoggerFactory TestSqlLoggerFactory => (TestSqlLoggerFactory)ListLoggerFactory;
private PostgresOptimizationsData _expectedData;
protected override void Seed(PostgresOptimizationsQueryContext context) => PostgresOptimizationsQueryContext.Seed(context);
public Func<DbContext> GetContextCreator()
=> CreateContext;
public ISetSource GetExpectedData()
=> _expectedData ??= new PostgresOptimizationsData();
public IReadOnlyDictionary<Type, object> GetEntitySorters()
=> new Dictionary<Type, Func<object, object>> { { typeof(Entity), e => ((Entity)e)?.Id } }
.ToDictionary(e => e.Key, e => (object)e.Value);
public IReadOnlyDictionary<Type, object> GetEntityAsserters()
=> new Dictionary<Type, Action<object, object>>
{
{
typeof(Entity), (e, a) =>
{
Assert.Equal(e is null, a is null);
if (a is not null)
{
var ee = (Entity)e;
var aa = (Entity)a;
Assert.Equal(ee.Id, aa.Id);
Assert.Equal(ee.X, aa.X);
Assert.Equal(ee.Y, aa.Y);
Assert.Equal(ee.Z, aa.Z);
}
}
}
}.ToDictionary(e => e.Key, e => (object)e.Value);
}
protected class PostgresOptimizationsData : ISetSource
{
public IReadOnlyList<Entity> Entities { get; }
public PostgresOptimizationsData()
=> Entities = CreateEntities();
public IQueryable<TEntity> Set<TEntity>()
where TEntity : class
{
if (typeof(TEntity) == typeof(Entity))
{
return (IQueryable<TEntity>)Entities.AsQueryable();
}
throw new InvalidOperationException("Invalid entity type: " + typeof(TEntity));
}
public static IReadOnlyList<Entity> CreateEntities()
=> new List<Entity>
{
new()
{
Id = 1,
X = 5,
Y = 7,
Z = 9
},
new()
{
Id = 2,
X = 4,
Y = 10,
Z = 10
}
};
}
#endregion
}
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册