diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlGuidTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlGuidTranslator.cs new file mode 100644 index 0000000000..f4e41f89fc --- /dev/null +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlGuidTranslator.cs @@ -0,0 +1,57 @@ +using static Npgsql.EntityFrameworkCore.PostgreSQL.Utilities.Statics; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Internal; + +/// +/// Provides translation services for PostgreSQL UUID functions. +/// +/// +/// See: https://www.postgresql.org/docs/current/datatype-uuid.html +/// +public class NpgsqlGuidTranslator(ISqlExpressionFactory sqlExpressionFactory, Version? postgresVersion) : IMethodCallTranslator +{ + private readonly string _uuidGenerationFunction = postgresVersion.AtLeast(13) ? "gen_random_uuid" : "uuid_generate_v4"; + + /// + /// 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. + /// + public virtual SqlExpression? Translate( + SqlExpression? instance, + MethodInfo method, + IReadOnlyList arguments, + IDiagnosticsLogger logger) + { + if (method.DeclaringType == typeof(Guid)) + { + return method.Name switch + { + nameof(Guid.NewGuid) + => sqlExpressionFactory.Function( + _uuidGenerationFunction, + [], + nullable: false, + argumentsPropagateNullability: FalseArrays[0], + method.ReturnType), + + // Note: uuidv7() was introduce in PostgreSQL 18. + // In NpgsqlEvaluatableExpressionFilter we only prevent local evaluation when targeting PG18 or later; + // that means that for lower version, the call gets evaluated locally and the result sent as a parameter + // (and we never see the method call here). + nameof(Guid.CreateVersion7) + => sqlExpressionFactory.Function( + "uuidv7", + [], + nullable: false, + argumentsPropagateNullability: FalseArrays[0], + method.ReturnType), + + _ => null + }; + } + + return null; + } +} diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMethodCallTranslatorProvider.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMethodCallTranslatorProvider.cs index 45e794efd1..964dd498da 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMethodCallTranslatorProvider.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMethodCallTranslatorProvider.cs @@ -55,7 +55,7 @@ public NpgsqlMethodCallTranslatorProvider( LTreeTranslator, new NpgsqlMathTranslator(typeMappingSource, sqlExpressionFactory, model), new NpgsqlNetworkTranslator(typeMappingSource, sqlExpressionFactory, model), - new NpgsqlNewGuidTranslator(sqlExpressionFactory, npgsqlOptions.PostgresVersion), + new NpgsqlGuidTranslator(sqlExpressionFactory, npgsqlOptions.PostgresVersion), new NpgsqlObjectToStringTranslator(typeMappingSource, sqlExpressionFactory), new NpgsqlRandomTranslator(sqlExpressionFactory), new NpgsqlRangeTranslator(typeMappingSource, sqlExpressionFactory, model, supportsMultiranges), diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlNewGuidTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlNewGuidTranslator.cs deleted file mode 100644 index 8762343ffc..0000000000 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlNewGuidTranslator.cs +++ /dev/null @@ -1,49 +0,0 @@ -using static Npgsql.EntityFrameworkCore.PostgreSQL.Utilities.Statics; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Internal; - -/// -/// Provides translation services for PostgreSQL UUID functions. -/// -/// -/// See: https://www.postgresql.org/docs/current/datatype-uuid.html -/// -public class NpgsqlNewGuidTranslator : IMethodCallTranslator -{ - private static readonly MethodInfo MethodInfo = typeof(Guid).GetRuntimeMethod(nameof(Guid.NewGuid), [])!; - - private readonly ISqlExpressionFactory _sqlExpressionFactory; - private readonly string _uuidGenerationFunction; - - /// - /// 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. - /// - public NpgsqlNewGuidTranslator(ISqlExpressionFactory sqlExpressionFactory, Version? postgresVersion) - { - _sqlExpressionFactory = sqlExpressionFactory; - _uuidGenerationFunction = postgresVersion.AtLeast(13) ? "gen_random_uuid" : "uuid_generate_v4"; - } - - /// - /// 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. - /// - public virtual SqlExpression? Translate( - SqlExpression? instance, - MethodInfo method, - IReadOnlyList arguments, - IDiagnosticsLogger logger) - => MethodInfo.Equals(method) - ? _sqlExpressionFactory.Function( - _uuidGenerationFunction, - [], - nullable: false, - argumentsPropagateNullability: FalseArrays[0], - method.ReturnType) - : null; -} diff --git a/src/EFCore.PG/Query/Internal/NpgsqlEvaluatableExpressionFilter.cs b/src/EFCore.PG/Query/Internal/NpgsqlEvaluatableExpressionFilter.cs index da5ebbbd18..ffb9528260 100644 --- a/src/EFCore.PG/Query/Internal/NpgsqlEvaluatableExpressionFilter.cs +++ b/src/EFCore.PG/Query/Internal/NpgsqlEvaluatableExpressionFilter.cs @@ -1,4 +1,5 @@ using System.Runtime.CompilerServices; +using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal; namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.Internal; @@ -10,6 +11,8 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.Internal; /// public class NpgsqlEvaluatableExpressionFilter : RelationalEvaluatableExpressionFilter { + private readonly Version _postgresVersion; + private static readonly MethodInfo TsQueryParse = typeof(NpgsqlTsQuery).GetRuntimeMethod(nameof(NpgsqlTsQuery.Parse), [typeof(string)])!; @@ -24,9 +27,11 @@ public class NpgsqlEvaluatableExpressionFilter : RelationalEvaluatableExpression /// public NpgsqlEvaluatableExpressionFilter( EvaluatableExpressionFilterDependencies dependencies, - RelationalEvaluatableExpressionFilterDependencies relationalDependencies) + RelationalEvaluatableExpressionFilterDependencies relationalDependencies, + INpgsqlSingletonOptions npgsqlSingletonOptions) : base(dependencies, relationalDependencies) { + _postgresVersion = npgsqlSingletonOptions.PostgresVersion; } /// @@ -51,6 +56,8 @@ public override bool IsEvaluatableExpression(Expression expression, IModel model || declaringType == typeof(NpgsqlNetworkDbFunctionsExtensions) || declaringType == typeof(NpgsqlJsonDbFunctionsExtensions) || declaringType == typeof(NpgsqlRangeDbFunctionsExtensions) + // PG18 introduced uuidv7(), so we prevent local evaluation when targeting PG18 or later. + || declaringType == typeof(Guid) && method.Name == nameof(Guid.CreateVersion7) && _postgresVersion.AtLeast(18) // Prevent evaluation of ValueTuple.Create, see NewExpression of ITuple below || declaringType == typeof(ValueTuple) && method.Name == nameof(ValueTuple.Create)) { diff --git a/test/EFCore.PG.FunctionalTests/Query/Translations/GuidTranslationsNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/Translations/GuidTranslationsNpgsqlTest.cs index 664e1b369f..e6b221cb84 100644 --- a/test/EFCore.PG.FunctionalTests/Query/Translations/GuidTranslationsNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/Translations/GuidTranslationsNpgsqlTest.cs @@ -1,3 +1,5 @@ +using Microsoft.EntityFrameworkCore.TestModels.BasicTypesModel; + namespace Microsoft.EntityFrameworkCore.Query.Translations; public class GuidTranslationsNpgsqlTest : GuidTranslationsTestBase @@ -71,6 +73,34 @@ WHERE uuid_generate_v4() <> '00000000-0000-0000-0000-000000000000' } } + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task CreateVersion7(bool async) + { + await AssertQuery( + async, + ss => ss.Set() + .Where(od => Guid.CreateVersion7() != default)); + + if (TestEnvironment.PostgresVersion >= new Version(18, 0)) + { + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE uuidv7() <> '00000000-0000-0000-0000-000000000000' +"""); + } + else + { + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +"""); + } + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); }