diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMethodCallTranslatorProvider.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMethodCallTranslatorProvider.cs index 11dbd1555f300d2a9fb7d8be861e78900f6172f0..db02c37f409b2d5e9c2b37ccc1caa6770e9019e5 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMethodCallTranslatorProvider.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMethodCallTranslatorProvider.cs @@ -37,7 +37,7 @@ public class NpgsqlMethodCallTranslatorProvider : RelationalMethodCallTranslator new NpgsqlRandomTranslator(sqlExpressionFactory), new NpgsqlRangeTranslator(typeMappingSource, sqlExpressionFactory, model), new NpgsqlRegexIsMatchTranslator(sqlExpressionFactory), - new NpgsqlRowValueComparisonTranslator(sqlExpressionFactory), + new NpgsqlRowValueTranslator(sqlExpressionFactory), new NpgsqlStringMethodTranslator(typeMappingSource, sqlExpressionFactory, model), new NpgsqlTrigramsMethodTranslator(typeMappingSource, sqlExpressionFactory, model), }); diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRowValueComparisonTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRowValueTranslator.cs similarity index 80% rename from src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRowValueComparisonTranslator.cs rename to src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRowValueTranslator.cs index b336d34a6858f9faef9c13788fad2a38489402b8..f7bc935c94bbe188fec03083bd7fe4a828dc2347 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRowValueComparisonTranslator.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRowValueTranslator.cs @@ -1,13 +1,14 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using Npgsql.EntityFrameworkCore.PostgreSQL.Internal; using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal; namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Internal; -public class NpgsqlRowValueComparisonTranslator : IMethodCallTranslator +public class NpgsqlRowValueTranslator : IMethodCallTranslator { private readonly NpgsqlSqlExpressionFactory _sqlExpressionFactory; @@ -28,7 +29,7 @@ public class NpgsqlRowValueComparisonTranslator : IMethodCallTranslator typeof(NpgsqlDbFunctionsExtensions).GetMethods() .Single(m => m.Name == nameof(NpgsqlDbFunctionsExtensions.LessThanOrEqual)); - private static readonly Dictionary Methods = new() + private static readonly Dictionary ComparisonMethods = new() { { GreaterThan, ExpressionType.GreaterThan }, { LessThan, ExpressionType.LessThan }, @@ -37,19 +38,27 @@ public class NpgsqlRowValueComparisonTranslator : IMethodCallTranslator }; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - public NpgsqlRowValueComparisonTranslator(NpgsqlSqlExpressionFactory sqlExpressionFactory) + public NpgsqlRowValueTranslator(NpgsqlSqlExpressionFactory sqlExpressionFactory) => _sqlExpressionFactory = sqlExpressionFactory; /// + [DynamicDependency(DynamicallyAccessedMemberTypes.PublicMethods, typeof(ValueType))] // For ValueTuple.Create public virtual SqlExpression? Translate( SqlExpression? instance, MethodInfo method, IReadOnlyList arguments, IDiagnosticsLogger logger) { - if (method.DeclaringType != typeof(NpgsqlDbFunctionsExtensions) || !Methods.TryGetValue(method, out var expressionType)) + // Translate ValueTuple.Create + if (method.DeclaringType == typeof(ValueTuple) && method.IsStatic && method.Name == nameof(ValueTuple.Create)) + { + return new PostgresRowValueExpression(arguments, method.ReturnType); + } + + // Translate EF.Functions.GreaterThan and other comparisons + if (method.DeclaringType != typeof(NpgsqlDbFunctionsExtensions) || !ComparisonMethods.TryGetValue(method, out var expressionType)) { return null; } diff --git a/src/EFCore.PG/Query/Internal/NpgsqlEvaluatableExpressionFilter.cs b/src/EFCore.PG/Query/Internal/NpgsqlEvaluatableExpressionFilter.cs index 2128253af1f530c0ed386ec3251563b9fa82d2d3..cfeed3d2141e751bfd6cdde36998215e206f9394 100644 --- a/src/EFCore.PG/Query/Internal/NpgsqlEvaluatableExpressionFilter.cs +++ b/src/EFCore.PG/Query/Internal/NpgsqlEvaluatableExpressionFilter.cs @@ -23,15 +23,18 @@ public override bool IsEvaluatableExpression(Expression expression, IModel model { case MethodCallExpression methodCallExpression: var declaringType = methodCallExpression.Method.DeclaringType; + var method = methodCallExpression.Method; - if (methodCallExpression.Method == TsQueryParse - || methodCallExpression.Method == TsVectorParse + if (method == TsQueryParse + || method == TsVectorParse || declaringType == typeof(NpgsqlDbFunctionsExtensions) || declaringType == typeof(NpgsqlFullTextSearchDbFunctionsExtensions) || declaringType == typeof(NpgsqlFullTextSearchLinqExtensions) || declaringType == typeof(NpgsqlNetworkDbFunctionsExtensions) || declaringType == typeof(NpgsqlJsonDbFunctionsExtensions) - || declaringType == typeof(NpgsqlRangeDbFunctionsExtensions)) + || declaringType == typeof(NpgsqlRangeDbFunctionsExtensions) + // Prevent evaluation of ValueTuple.Create, see NewExpression of ITuple below + || declaringType == typeof(ValueTuple) && method.Name == nameof(ValueTuple.Create)) { return false; } diff --git a/test/EFCore.PG.FunctionalTests/Query/NorthwindWhereQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/NorthwindWhereQueryNpgsqlTest.cs index d18c4747fd90ff603cb9cab763c8080a39e576da..bcb3c3e99ace573919d3190edce8169b6349fd91 100644 --- a/test/EFCore.PG.FunctionalTests/Query/NorthwindWhereQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/NorthwindWhereQueryNpgsqlTest.cs @@ -245,8 +245,8 @@ public async Task Row_value_GreaterThan() _ = await ctx.Customers .Where(c => EF.Functions.GreaterThan( - new ValueTuple(c.City, c.CustomerID), - new ValueTuple("Buenos Aires", "OCEAN"))) + ValueTuple.Create(c.City, c.CustomerID), + ValueTuple.Create("Buenos Aires", "OCEAN"))) .CountAsync(); AssertSql( @@ -262,8 +262,8 @@ public async Task Row_value_GreaterThan_with_differing_types() _ = await ctx.Orders .Where(o => EF.Functions.GreaterThan( - new ValueTuple(o.CustomerID, o.OrderID), - new ValueTuple("ALFKI", 10702))) + ValueTuple.Create(o.CustomerID, o.OrderID), + ValueTuple.Create("ALFKI", 10702))) .CountAsync(); AssertSql( @@ -281,8 +281,8 @@ public async Task Row_value_GreaterThan_with_parameter() _ = await ctx.Customers .Where(c => EF.Functions.GreaterThan( - new ValueTuple(c.City, c.CustomerID), - new ValueTuple(city1, "OCEAN"))) + ValueTuple.Create(c.City, c.CustomerID), + ValueTuple.Create(city1, "OCEAN"))) .CountAsync(); AssertSql( @@ -300,8 +300,8 @@ public async Task Row_value_LessThan() _ = await ctx.Customers .Where(c => EF.Functions.LessThan( - new ValueTuple(c.City, c.CustomerID), - new ValueTuple("Buenos Aires", "OCEAN"))) + ValueTuple.Create(c.City, c.CustomerID), + ValueTuple.Create("Buenos Aires", "OCEAN"))) .CountAsync(); AssertSql( @@ -317,8 +317,8 @@ public async Task Row_value_GreaterThanOrEqual() _ = await ctx.Customers .Where(c => EF.Functions.GreaterThanOrEqual( - new ValueTuple(c.City, c.CustomerID), - new ValueTuple("Buenos Aires", "OCEAN"))) + ValueTuple.Create(c.City, c.CustomerID), + ValueTuple.Create("Buenos Aires", "OCEAN"))) .CountAsync(); AssertSql( @@ -334,6 +334,23 @@ public async Task Row_value_LessThanOrEqual() _ = await ctx.Customers .Where(c => EF.Functions.LessThanOrEqual( + ValueTuple.Create(c.City, c.CustomerID), + ValueTuple.Create("Buenos Aires", "OCEAN"))) + .CountAsync(); + + AssertSql( + @"SELECT COUNT(*)::INT +FROM ""Customers"" AS c +WHERE (c.""City"", c.""CustomerID"") <= ('Buenos Aires', 'OCEAN')"); + } + + [ConditionalFact] + public async Task Row_value_with_ValueTuple_constructor() + { + await using var ctx = CreateContext(); + + _ = await ctx.Customers + .Where(c => EF.Functions.GreaterThan( new ValueTuple(c.City, c.CustomerID), new ValueTuple("Buenos Aires", "OCEAN"))) .CountAsync(); @@ -341,7 +358,7 @@ public async Task Row_value_LessThanOrEqual() AssertSql( @"SELECT COUNT(*)::INT FROM ""Customers"" AS c -WHERE (c.""City"", c.""CustomerID"") <= ('Buenos Aires', 'OCEAN')"); +WHERE (c.""City"", c.""CustomerID"") > ('Buenos Aires', 'OCEAN')"); } [ConditionalFact] @@ -352,8 +369,8 @@ public async Task Row_value_parameter_count_mismatch() var exception = await Assert.ThrowsAsync( () => ctx.Customers .Where(c => EF.Functions.LessThanOrEqual( - new ValueTuple(c.City, c.CustomerID), - new ValueTuple("Buenos Aires", "OCEAN", "foo"))) + ValueTuple.Create(c.City, c.CustomerID), + ValueTuple.Create("Buenos Aires", "OCEAN", "foo"))) .CountAsync()); Assert.Equal(NpgsqlStrings.RowValueComparisonRequiresTuplesOfSameLength, exception.Message); @@ -366,8 +383,8 @@ public async Task Row_value_equals() _ = await ctx.Customers .Where(c => - new ValueTuple(c.City, c.CustomerID).Equals( - new ValueTuple("Buenos Aires", "OCEAN"))) + ValueTuple.Create(c.City, c.CustomerID).Equals( + ValueTuple.Create("Buenos Aires", "OCEAN"))) .CountAsync(); AssertSql( @@ -382,9 +399,7 @@ public async Task Row_value_not_equals() await using var ctx = CreateContext(); _ = await ctx.Customers - .Where(c => - !new ValueTuple(c.City, c.CustomerID).Equals( - new ValueTuple("Buenos Aires", "OCEAN"))) + .Where(c => !ValueTuple.Create(c.City, c.CustomerID).Equals(ValueTuple.Create("Buenos Aires", "OCEAN"))) .CountAsync(); AssertSql(