From 88b22286f8b16685df8b86f68ea3d4db963ed40b Mon Sep 17 00:00:00 2001 From: Christopher Jolly Date: Sun, 22 Sep 2024 01:34:10 +0800 Subject: [PATCH 1/6] Initial add of Precompiled query tests --- .../Internal/NpgsqlQueryCompilationContext.cs | 23 +- .../NpgsqlQueryCompilationContextFactory.cs | 11 + .../NpgsqlSqlTranslatingExpressionVisitor.cs | 36 +- .../NpgsqlComplianceTest.cs | 5 - .../Query/AdHocPrecompiledQueryNpgsqlTest.cs | 91 + .../Query/PrecompiledQueryNpgsqlTest.cs | 2100 +++++++++++++++++ ...compiledSqlPregenerationQueryNpgsqlTest.cs | 265 +++ .../NpgsqlPrecompiledQueryTestHelpers.cs | 15 + 8 files changed, 2538 insertions(+), 8 deletions(-) create mode 100644 test/EFCore.PG.FunctionalTests/Query/AdHocPrecompiledQueryNpgsqlTest.cs create mode 100644 test/EFCore.PG.FunctionalTests/Query/PrecompiledQueryNpgsqlTest.cs create mode 100644 test/EFCore.PG.FunctionalTests/Query/PrecompiledSqlPregenerationQueryNpgsqlTest.cs create mode 100644 test/EFCore.PG.FunctionalTests/TestUtilities/NpgsqlPrecompiledQueryTestHelpers.cs diff --git a/src/EFCore.PG/Query/Internal/NpgsqlQueryCompilationContext.cs b/src/EFCore.PG/Query/Internal/NpgsqlQueryCompilationContext.cs index 517462731..259cd1ec7 100644 --- a/src/EFCore.PG/Query/Internal/NpgsqlQueryCompilationContext.cs +++ b/src/EFCore.PG/Query/Internal/NpgsqlQueryCompilationContext.cs @@ -18,7 +18,25 @@ public NpgsqlQueryCompilationContext( QueryCompilationContextDependencies dependencies, RelationalQueryCompilationContextDependencies relationalDependencies, bool async) - : base(dependencies, relationalDependencies, async) + : this( + dependencies, relationalDependencies, async, precompiling: false, + nonNullableReferenceTypeParameters: null) + { + } + + /// + /// 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 NpgsqlQueryCompilationContext( + QueryCompilationContextDependencies dependencies, + RelationalQueryCompilationContextDependencies relationalDependencies, + bool async, + bool precompiling, + IReadOnlySet? nonNullableReferenceTypeParameters) + : base(dependencies, relationalDependencies, async, precompiling, nonNullableReferenceTypeParameters) { } @@ -30,4 +48,7 @@ public NpgsqlQueryCompilationContext( /// public override bool IsBuffering => base.IsBuffering || QuerySplittingBehavior == Microsoft.EntityFrameworkCore.QuerySplittingBehavior.SplitQuery; + + /// + public override bool SupportsPrecompiledQuery => true; } diff --git a/src/EFCore.PG/Query/Internal/NpgsqlQueryCompilationContextFactory.cs b/src/EFCore.PG/Query/Internal/NpgsqlQueryCompilationContextFactory.cs index f3a43623b..7f6bbfc3a 100644 --- a/src/EFCore.PG/Query/Internal/NpgsqlQueryCompilationContextFactory.cs +++ b/src/EFCore.PG/Query/Internal/NpgsqlQueryCompilationContextFactory.cs @@ -36,4 +36,15 @@ public NpgsqlQueryCompilationContextFactory( /// public virtual QueryCompilationContext Create(bool async) => new NpgsqlQueryCompilationContext(_dependencies, _relationalDependencies, async); + + /// + /// 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 QueryCompilationContext CreatePrecompiled(bool async, IReadOnlySet nonNullableReferenceTypeParameters) + => new NpgsqlQueryCompilationContext( + _dependencies, _relationalDependencies, async, precompiling: true, + nonNullableReferenceTypeParameters); } diff --git a/src/EFCore.PG/Query/Internal/NpgsqlSqlTranslatingExpressionVisitor.cs b/src/EFCore.PG/Query/Internal/NpgsqlSqlTranslatingExpressionVisitor.cs index 9ce217f02..39c670f2b 100644 --- a/src/EFCore.PG/Query/Internal/NpgsqlSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.PG/Query/Internal/NpgsqlSqlTranslatingExpressionVisitor.cs @@ -525,7 +525,13 @@ when patternParameter.Name.StartsWith(QueryCompilationContext.QueryParameterPref } } - private static string? ConstructLikePatternParameter( + /// + /// 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 static string? ConstructLikePatternParameter( QueryContext queryContext, string baseParameterName, StartsEndsWithContains methodType) @@ -548,10 +554,36 @@ when patternParameter.Name.StartsWith(QueryCompilationContext.QueryParameterPref _ => throw new UnreachableException() }; - private enum StartsEndsWithContains + /// + /// 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 enum StartsEndsWithContains { + /// + /// 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. + /// StartsWith, + + /// + /// 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. + /// EndsWith, + + /// + /// 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. + /// Contains } diff --git a/test/EFCore.PG.FunctionalTests/NpgsqlComplianceTest.cs b/test/EFCore.PG.FunctionalTests/NpgsqlComplianceTest.cs index 76e12c800..f5e354650 100644 --- a/test/EFCore.PG.FunctionalTests/NpgsqlComplianceTest.cs +++ b/test/EFCore.PG.FunctionalTests/NpgsqlComplianceTest.cs @@ -10,11 +10,6 @@ public class NpgsqlComplianceTest : RelationalComplianceTestBase typeof(UdfDbFunctionTestBase<>), typeof(UpdateSqlGeneratorTestBase), - // Precompiled query/NativeAOT (#3257) - typeof(AdHocPrecompiledQueryRelationalTestBase), - typeof(PrecompiledQueryRelationalTestBase), - typeof(PrecompiledSqlPregenerationQueryRelationalTestBase), - // Disabled typeof(GraphUpdatesTestBase<>), typeof(ProxyGraphUpdatesTestBase<>), diff --git a/test/EFCore.PG.FunctionalTests/Query/AdHocPrecompiledQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/AdHocPrecompiledQueryNpgsqlTest.cs new file mode 100644 index 000000000..f97055924 --- /dev/null +++ b/test/EFCore.PG.FunctionalTests/Query/AdHocPrecompiledQueryNpgsqlTest.cs @@ -0,0 +1,91 @@ +using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; +using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query; + +public class AdHocPrecompiledQueryNpgsqlTest(ITestOutputHelper testOutputHelper) + : AdHocPrecompiledQueryRelationalTestBase(testOutputHelper) +{ + protected override bool AlwaysPrintGeneratedSources + => false; + + public override async Task Index_no_evaluatability() + { + await base.Index_no_evaluatability(); + + AssertSql( + """ +SELECT j."Id", j."IntList", j."JsonThing" +FROM "JsonEntities" AS j +WHERE j."IntList"[j."Id" + 1] = 2 +"""); + } + + public override async Task Index_with_captured_variable() + { + await base.Index_with_captured_variable(); + + AssertSql( + """ +@__id_0='1' + +SELECT j."Id", j."IntList", j."JsonThing" +FROM "JsonEntities" AS j +WHERE j."IntList"[@__id_0 + 1] = 2 +"""); + } + + public override async Task JsonScalar() + { + await base.JsonScalar(); + + AssertSql( + """ +SELECT j."Id", j."IntList", j."JsonThing" +FROM "JsonEntities" AS j +WHERE (j."JsonThing" ->> 'StringProperty') = 'foo' +"""); + } + + public override async Task Materialize_non_public() + { + await base.Materialize_non_public(); + + AssertSql( + """ +@p0='10' (Nullable = true) +@p1='9' (Nullable = true) +@p2='8' (Nullable = true) + +INSERT INTO "NonPublicEntities" ("PrivateAutoProperty", "PrivateProperty", "_privateField") +VALUES (@p0, @p1, @p2) +RETURNING "Id"; +""", + // + """ +SELECT n."Id", n."PrivateAutoProperty", n."PrivateProperty", n."_privateField" +FROM "NonPublicEntities" AS n +LIMIT 2 +"""); + } + + [ConditionalFact] + public virtual void Check_all_tests_overridden() + => TestHelpers.AssertAllMethodsOverridden(GetType()); + + protected override ITestStoreFactory TestStoreFactory + => NpgsqlTestStoreFactory.Instance; + + protected override PrecompiledQueryTestHelpers PrecompiledQueryTestHelpers + => NpgsqlPrecompiledQueryTestHelpers.Instance; + + protected override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) + { + builder = base.AddOptions(builder); + + // TODO: Figure out if there's a nice way to continue using the retrying strategy + var sqlServerOptionsBuilder = new NpgsqlDbContextOptionsBuilder(builder); + sqlServerOptionsBuilder.ExecutionStrategy(d => new NonRetryingExecutionStrategy(d)); + return builder; + } +} diff --git a/test/EFCore.PG.FunctionalTests/Query/PrecompiledQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/PrecompiledQueryNpgsqlTest.cs new file mode 100644 index 000000000..de89b2912 --- /dev/null +++ b/test/EFCore.PG.FunctionalTests/Query/PrecompiledQueryNpgsqlTest.cs @@ -0,0 +1,2100 @@ +using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; +using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query; + +public class PrecompiledQueryNpgsqlTest( + PrecompiledQueryNpgsqlTest.PrecompiledQueryNpgsqlFixture fixture, + ITestOutputHelper testOutputHelper) + : PrecompiledQueryRelationalTestBase(fixture, testOutputHelper), + IClassFixture +{ + protected override bool AlwaysPrintGeneratedSources + => true; + + #region Expression types + + public override async Task BinaryExpression() + { + await base.BinaryExpression(); + + AssertSql( + """ +@__id_0='3' + +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" > @__id_0 +"""); + } + + public override async Task Conditional_no_evaluatable() + { + await base.Conditional_no_evaluatable(); + + AssertSql( + """ +SELECT CASE + WHEN b."Id" = 2 THEN 'yes' + ELSE 'no' +END +FROM "Blogs" AS b +"""); + } + + public override async Task Conditional_contains_captured_variable() + { + await base.Conditional_contains_captured_variable(); + + AssertSql( + """ +@__yes_0='yes' + +SELECT CASE + WHEN b."Id" = 2 THEN @__yes_0 + ELSE 'no' +END +FROM "Blogs" AS b +"""); + } + + public override async Task Invoke_no_evaluatability_is_not_supported() + { + await base.Invoke_no_evaluatability_is_not_supported(); + + AssertSql(); + } + + public override async Task ListInit_no_evaluatability() + { + await base.ListInit_no_evaluatability(); + + AssertSql( + """ +SELECT b."Id", b."Id" + 1 +FROM "Blogs" AS b +"""); + } + + public override async Task ListInit_with_evaluatable_with_captured_variable() + { + await base.ListInit_with_evaluatable_with_captured_variable(); + + AssertSql( + """ +SELECT b."Id" +FROM "Blogs" AS b +"""); + } + + public override async Task ListInit_with_evaluatable_without_captured_variable() + { + await base.ListInit_with_evaluatable_without_captured_variable(); + + AssertSql( + """ +SELECT b."Id" +FROM "Blogs" AS b +"""); + } + + public override async Task ListInit_fully_evaluatable() + { + await base.ListInit_fully_evaluatable(); + + AssertSql( + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" IN (7, 8) +LIMIT 2 +"""); + } + + public override async Task MethodCallExpression_no_evaluatability() + { + await base.MethodCallExpression_no_evaluatability(); + + AssertSql( + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Name" IS NOT NULL AND left(b."Name", length(b."Name")) = b."Name" +"""); + } + + public override async Task MethodCallExpression_with_evaluatable_with_captured_variable() + { + await base.MethodCallExpression_with_evaluatable_with_captured_variable(); + + AssertSql( + """ +@__pattern_0_startswith='foo%' + +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Name" LIKE @__pattern_0_startswith +"""); + } + + public override async Task MethodCallExpression_with_evaluatable_without_captured_variable() + { + await base.MethodCallExpression_with_evaluatable_without_captured_variable(); + + AssertSql( + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Name" LIKE 'foo%' +"""); + } + + public override async Task MethodCallExpression_fully_evaluatable() + { + await base.MethodCallExpression_fully_evaluatable(); + + AssertSql( + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +"""); + } + + public override async Task New_with_no_arguments() + { + await base.New_with_no_arguments(); + + AssertSql( + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 0 +"""); + } + + public override async Task Where_New_with_captured_variable() + { + await base.Where_New_with_captured_variable(); + + AssertSql(); + } + + public override async Task Select_New_with_captured_variable() + { + await base.Select_New_with_captured_variable(); + + AssertSql( + """ +SELECT b."Name" +FROM "Blogs" AS b +"""); + } + + public override async Task MemberInit_no_evaluatable() + { + await base.MemberInit_no_evaluatable(); + + AssertSql( + """ +SELECT b."Id", b."Name" +FROM "Blogs" AS b +"""); + } + + public override async Task MemberInit_contains_captured_variable() + { + await base.MemberInit_contains_captured_variable(); + + AssertSql( + """ +@__id_0='8' + +SELECT @__id_0 AS "Id", b."Name" +FROM "Blogs" AS b +"""); + } + + public override async Task MemberInit_evaluatable_as_constant() + { + await base.MemberInit_evaluatable_as_constant(); + + AssertSql( + """ +SELECT 1 AS "Id", 'foo' AS "Name" +FROM "Blogs" AS b +"""); + } + + public override async Task MemberInit_evaluatable_as_parameter() + { + await base.MemberInit_evaluatable_as_parameter(); + + AssertSql( + """ +SELECT 1 +FROM "Blogs" AS b +"""); + } + + public override async Task NewArray() + { + await base.NewArray(); + + AssertSql( + """ +@__i_0='8' + +SELECT ARRAY[b."Id",b."Id" + @__i_0]::integer[] +FROM "Blogs" AS b +"""); + } + + public override async Task Unary() + { + await base.Unary(); + + AssertSql( + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id"::smallint = 8 +"""); + } + + public virtual async Task Collate() + { + await Test("""_ = context.Blogs.Where(b => EF.Functions.Collate(b.Name, "German_PhoneBook_CI_AS") == "foo").ToList();"""); + + AssertSql(); + } + + #endregion Expression types + + #region Terminating operators + + public override async Task Terminating_AsEnumerable() + { + await base.Terminating_AsEnumerable(); + + AssertSql( + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +"""); + } + + public override async Task Terminating_AsAsyncEnumerable_on_DbSet() + { + await base.Terminating_AsAsyncEnumerable_on_DbSet(); + + AssertSql( + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +"""); + } + + public override async Task Terminating_AsAsyncEnumerable_on_IQueryable() + { + await base.Terminating_AsAsyncEnumerable_on_IQueryable(); + + AssertSql( + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" > 8 +"""); + } + + public override async Task Foreach_sync_over_operator() + { + await base.Foreach_sync_over_operator(); + + AssertSql( + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" > 8 +"""); + } + + public override async Task Terminating_ToArray() + { + await base.Terminating_ToArray(); + + AssertSql( + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +"""); + } + + public override async Task Terminating_ToArrayAsync() + { + await base.Terminating_ToArrayAsync(); + + AssertSql( + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +"""); + } + + public override async Task Terminating_ToDictionary() + { + await base.Terminating_ToDictionary(); + + AssertSql( + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +"""); + } + + public override async Task Terminating_ToDictionaryAsync() + { + await base.Terminating_ToDictionaryAsync(); + + AssertSql( + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +"""); + } + + public override async Task ToDictionary_over_anonymous_type() + { + await base.ToDictionary_over_anonymous_type(); + + AssertSql( + """ +SELECT b."Id", b."Name" +FROM "Blogs" AS b +"""); + } + + public override async Task ToDictionaryAsync_over_anonymous_type() + { + await base.ToDictionaryAsync_over_anonymous_type(); + + AssertSql( + """ +SELECT b."Id", b."Name" +FROM "Blogs" AS b +"""); + } + + public override async Task Terminating_ToHashSet() + { + await base.Terminating_ToHashSet(); + + AssertSql( + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +"""); + } + + public override async Task Terminating_ToHashSetAsync() + { + await base.Terminating_ToHashSetAsync(); + + AssertSql( + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +"""); + } + + public override async Task Terminating_ToLookup() + { + await base.Terminating_ToLookup(); + + AssertSql( + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +"""); + } + + public override async Task Terminating_ToList() + { + await base.Terminating_ToList(); + + AssertSql( + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +"""); + } + + public override async Task Terminating_ToListAsync() + { + await base.Terminating_ToListAsync(); + + AssertSql( + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +"""); + } + + public override async Task Foreach_sync_over_DbSet_property_is_not_supported() + { + await base.Foreach_sync_over_DbSet_property_is_not_supported(); + + AssertSql(); + } + + public override async Task Foreach_async_is_not_supported() + { + await base.Foreach_async_is_not_supported(); + + AssertSql(); + } + + #endregion Terminating operators + + #region Reducing terminating operators + + public override async Task Terminating_All() + { + await base.Terminating_All(); + + AssertSql( + """ +SELECT NOT EXISTS ( + SELECT 1 + FROM "Blogs" AS b + WHERE b."Id" <= 7) +""", + // + """ +SELECT NOT EXISTS ( + SELECT 1 + FROM "Blogs" AS b + WHERE b."Id" <= 8) +"""); + } + + public override async Task Terminating_AllAsync() + { + await base.Terminating_AllAsync(); +AssertSql( + """ +SELECT NOT EXISTS ( + SELECT 1 + FROM "Blogs" AS b + WHERE b."Id" <= 7) +""", + // + """ +SELECT NOT EXISTS ( + SELECT 1 + FROM "Blogs" AS b + WHERE b."Id" <= 8) +"""); + } + + public override async Task Terminating_Any() + { + await base.Terminating_Any(); +AssertSql( + """ +SELECT EXISTS ( + SELECT 1 + FROM "Blogs" AS b + WHERE b."Id" > 7) +""", + // + """ +SELECT EXISTS ( + SELECT 1 + FROM "Blogs" AS b + WHERE b."Id" < 7) +""", + // + """ +SELECT EXISTS ( + SELECT 1 + FROM "Blogs" AS b + WHERE b."Id" > 7) +""", + // + """ +SELECT EXISTS ( + SELECT 1 + FROM "Blogs" AS b + WHERE b."Id" < 7) +"""); + } + + public override async Task Terminating_AnyAsync() + { + await base.Terminating_AnyAsync(); + + AssertSql( + """ +SELECT EXISTS ( + SELECT 1 + FROM "Blogs" AS b + WHERE b."Id" > 7) +""", + // + """ +SELECT EXISTS ( + SELECT 1 + FROM "Blogs" AS b + WHERE b."Id" < 7) +""", + // + """ +SELECT EXISTS ( + SELECT 1 + FROM "Blogs" AS b + WHERE b."Id" > 7) +""", + // + """ +SELECT EXISTS ( + SELECT 1 + FROM "Blogs" AS b + WHERE b."Id" < 7) +"""); + } + + public override async Task Terminating_Average() + { + await base.Terminating_Average(); + + AssertSql( + """ +SELECT avg(b."Id"::double precision) +FROM "Blogs" AS b +""", + // + """ +SELECT avg(b."Id"::double precision) +FROM "Blogs" AS b +"""); + } + + public override async Task Terminating_AverageAsync() + { + await base.Terminating_AverageAsync(); + + AssertSql( + """ +SELECT avg(b."Id"::double precision) +FROM "Blogs" AS b +""", + // + """ +SELECT avg(b."Id"::double precision) +FROM "Blogs" AS b +"""); + } + + public override async Task Terminating_Contains() + { + await base.Terminating_Contains(); + + AssertSql( + """ +@__p_0='8' + +SELECT IIF(@__p_0 IN ( + SELECT `b`.`Id` + FROM `Blogs` AS `b` + ), TRUE, FALSE) +FROM (SELECT COUNT(*) FROM `#Dual`) +""", + // + """ +@__p_0='7' + +SELECT IIF(@__p_0 IN ( + SELECT `b`.`Id` + FROM `Blogs` AS `b` + ), TRUE, FALSE) +FROM (SELECT COUNT(*) FROM `#Dual`) +"""); + } + + public override async Task Terminating_ContainsAsync() + { + await base.Terminating_ContainsAsync(); + + AssertSql( + """ +@__p_0='8' + +SELECT @__p_0 IN ( + SELECT b."Id" + FROM "Blogs" AS b +) +""", + // + """ +@__p_0='7' + +SELECT @__p_0 IN ( + SELECT b."Id" + FROM "Blogs" AS b +) +"""); + } + + public override async Task Terminating_Count() + { + await base.Terminating_Count(); + + AssertSql( + """ +SELECT count(*)::int +FROM "Blogs" AS b +""", + // + """ +SELECT count(*)::int +FROM "Blogs" AS b +WHERE b."Id" > 8 +"""); + } + + public override async Task Terminating_CountAsync() + { + await base.Terminating_CountAsync(); + + AssertSql( + """ +SELECT count(*)::int +FROM "Blogs" AS b +""", + // + """ +SELECT count(*)::int +FROM "Blogs" AS b +WHERE b."Id" > 8 +"""); + } + + public override async Task Terminating_ElementAt() + { + await base.Terminating_ElementAt(); + + AssertSql( + """ +@__p_0='1' + +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +ORDER BY b."Id" NULLS FIRST +LIMIT 1 OFFSET @__p_0 +""", + // + """ +@__p_0='3' + +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +ORDER BY b."Id" NULLS FIRST +LIMIT 1 OFFSET @__p_0 +"""); + } + + public override async Task Terminating_ElementAtAsync() + { + await base.Terminating_ElementAtAsync(); + + AssertSql( + """ +@__p_0='1' + +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +ORDER BY b."Id" NULLS FIRST +LIMIT 1 OFFSET @__p_0 +""", + // + """ +@__p_0='3' + +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +ORDER BY b."Id" NULLS FIRST +LIMIT 1 OFFSET @__p_0 +"""); + } + + public override async Task Terminating_ElementAtOrDefault() + { + await base.Terminating_ElementAtOrDefault(); + + AssertSql( + """ +@__p_0='1' + +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +ORDER BY b."Id" NULLS FIRST +LIMIT 1 OFFSET @__p_0 +""", + // + """ +@__p_0='3' + +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +ORDER BY b."Id" NULLS FIRST +LIMIT 1 OFFSET @__p_0 +"""); + } + + public override async Task Terminating_ElementAtOrDefaultAsync() + { + await base.Terminating_ElementAtOrDefaultAsync(); + + AssertSql( + """ +@__p_0='1' + +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +ORDER BY b."Id" NULLS FIRST +LIMIT 1 OFFSET @__p_0 +""", + // + """ +@__p_0='3' + +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +ORDER BY b."Id" NULLS FIRST +LIMIT 1 OFFSET @__p_0 +"""); + } + + public override async Task Terminating_First() + { + await base.Terminating_First(); + + AssertSql( + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 8 +LIMIT 1 +""", + // + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 7 +LIMIT 1 +""", + // + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 8 +LIMIT 1 +""", + // + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 7 +LIMIT 1 +"""); + } + + public override async Task Terminating_FirstAsync() + { + await base.Terminating_FirstAsync(); + + AssertSql( + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 8 +LIMIT 1 +""", + // + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 7 +LIMIT 1 +""", + // + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 8 +LIMIT 1 +""", + // + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 7 +LIMIT 1 +"""); + } + + public override async Task Terminating_FirstOrDefault() + { + await base.Terminating_FirstOrDefault(); + + AssertSql( + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 8 +LIMIT 1 +""", + // + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 7 +LIMIT 1 +""", + // + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 8 +LIMIT 1 +""", + // + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 7 +LIMIT 1 +"""); + } + + public override async Task Terminating_FirstOrDefaultAsync() + { + await base.Terminating_FirstOrDefaultAsync(); + + AssertSql( + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 8 +LIMIT 1 +""", + // + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 7 +LIMIT 1 +""", + // + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 8 +LIMIT 1 +""", + // + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 7 +LIMIT 1 +"""); + } + + public override async Task Terminating_GetEnumerator() + { + await base.Terminating_GetEnumerator(); + + AssertSql( + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 8 +"""); + } + + public override async Task Terminating_Last() + { + await base.Terminating_Last(); + + AssertSql( + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +ORDER BY b."Id" DESC NULLS LAST +LIMIT 1 +""", + // + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 7 +ORDER BY b."Id" DESC NULLS LAST +LIMIT 1 +""", + // + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 8 +ORDER BY b."Id" DESC NULLS LAST +LIMIT 1 +""", + // + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 7 +ORDER BY b."Id" DESC NULLS LAST +LIMIT 1 +"""); + } + + public override async Task Terminating_LastAsync() + { + await base.Terminating_LastAsync(); + + AssertSql( + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +ORDER BY b."Id" DESC NULLS LAST +LIMIT 1 +""", + // + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 7 +ORDER BY b."Id" DESC NULLS LAST +LIMIT 1 +""", + // + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 8 +ORDER BY b."Id" DESC NULLS LAST +LIMIT 1 +""", + // + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 7 +ORDER BY b."Id" DESC NULLS LAST +LIMIT 1 +"""); + } + + public override async Task Terminating_LastOrDefault() + { + await base.Terminating_LastOrDefault(); + + AssertSql( + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +ORDER BY b."Id" DESC NULLS LAST +LIMIT 1 +""", + // + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 7 +ORDER BY b."Id" DESC NULLS LAST +LIMIT 1 +""", + // + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 8 +ORDER BY b."Id" DESC NULLS LAST +LIMIT 1 +""", + // + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 7 +ORDER BY b."Id" DESC NULLS LAST +LIMIT 1 +"""); + } + + public override async Task Terminating_LastOrDefaultAsync() + { + await base.Terminating_LastOrDefaultAsync(); + + AssertSql( + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +ORDER BY b."Id" DESC NULLS LAST +LIMIT 1 +""", + // + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 7 +ORDER BY b."Id" DESC NULLS LAST +LIMIT 1 +""", + // + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 8 +ORDER BY b."Id" DESC NULLS LAST +LIMIT 1 +""", + // + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 7 +ORDER BY b."Id" DESC NULLS LAST +LIMIT 1 +"""); + } + + public override async Task Terminating_LongCount() + { + await base.Terminating_LongCount(); + + AssertSql( + """ +SELECT count(*) +FROM "Blogs" AS b +""", + // + """ +SELECT count(*) +FROM "Blogs" AS b +WHERE b."Id" = 8 +"""); + } + + public override async Task Terminating_LongCountAsync() + { + await base.Terminating_LongCountAsync(); + + AssertSql( + """ +SELECT count(*) +FROM "Blogs" AS b +""", + // + """ +SELECT count(*) +FROM "Blogs" AS b +WHERE b."Id" = 8 +"""); + } + + public override async Task Terminating_Max() + { + await base.Terminating_Max(); +AssertSql( + """ +SELECT max(b."Id") +FROM "Blogs" AS b +""", + // + """ +SELECT max(b."Id") +FROM "Blogs" AS b +"""); + } + + public override async Task Terminating_MaxAsync() + { + await base.Terminating_MaxAsync(); + + AssertSql( + """ +SELECT max(b."Id") +FROM "Blogs" AS b +""", + // + """ +SELECT max(b."Id") +FROM "Blogs" AS b +"""); + } + + public override async Task Terminating_Min() + { + await base.Terminating_Min(); + + AssertSql( + """ +SELECT min(b."Id") +FROM "Blogs" AS b +""", + // + """ +SELECT min(b."Id") +FROM "Blogs" AS b +"""); + } + + public override async Task Terminating_MinAsync() + { + await base.Terminating_MinAsync(); + + AssertSql( + """ +SELECT min(b."Id") +FROM "Blogs" AS b +""", + // + """ +SELECT min(b."Id") +FROM "Blogs" AS b +"""); + } + + public override async Task Terminating_Single() + { + await base.Terminating_Single(); + + AssertSql( + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 8 +LIMIT 2 +""", + // + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 7 +LIMIT 2 +""", + // + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 8 +LIMIT 2 +""", + // + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 7 +LIMIT 2 +"""); + } + + public override async Task Terminating_SingleAsync() + { + await base.Terminating_SingleAsync(); + + AssertSql( + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 8 +LIMIT 2 +""", + // + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 7 +LIMIT 2 +""", + // + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 8 +LIMIT 2 +""", + // + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 7 +LIMIT 2 +"""); + } + + public override async Task Terminating_SingleOrDefault() + { + await base.Terminating_SingleOrDefault(); + + AssertSql( + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 8 +LIMIT 2 +""", + // + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 7 +LIMIT 2 +""", + // + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 8 +LIMIT 2 +""", + // + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 7 +LIMIT 2 +"""); + } + + public override async Task Terminating_SingleOrDefaultAsync() + { + await base.Terminating_SingleOrDefaultAsync(); + + AssertSql( + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 8 +LIMIT 2 +""", + // + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 7 +LIMIT 2 +""", + // + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 8 +LIMIT 2 +""", + // + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 7 +LIMIT 2 +"""); + } + + public override async Task Terminating_Sum() + { + await base.Terminating_Sum(); + + AssertSql( + """ +SELECT COALESCE(sum(b."Id"), 0)::int +FROM "Blogs" AS b +""", + // + """ +SELECT COALESCE(sum(b."Id"), 0)::int +FROM "Blogs" AS b +"""); + } + + public override async Task Terminating_SumAsync() + { + await base.Terminating_SumAsync(); + + AssertSql( + """ +SELECT COALESCE(sum(b."Id"), 0)::int +FROM "Blogs" AS b +""", + // + """ +SELECT COALESCE(sum(b."Id"), 0)::int +FROM "Blogs" AS b +"""); + } + + public override async Task Terminating_ExecuteDelete() + { + await base.Terminating_ExecuteDelete(); + + AssertSql( + """ +DELETE FROM "Blogs" AS b +WHERE b."Id" > 8 +""", + // + """ +SELECT count(*)::int +FROM "Blogs" AS b +"""); + } + + public override async Task Terminating_ExecuteDeleteAsync() + { + await base.Terminating_ExecuteDeleteAsync(); + + AssertSql( + """ +DELETE FROM "Blogs" AS b +WHERE b."Id" > 8 +""", + // + """ +SELECT count(*)::int +FROM "Blogs" AS b +"""); + } + + public override async Task Terminating_ExecuteUpdate() + { + await base.Terminating_ExecuteUpdate(); + + AssertSql( + """ +@__suffix_0='Suffix' + +UPDATE "Blogs" AS b +SET "Name" = COALESCE(b."Name", '') || @__suffix_0 +WHERE b."Id" > 8 +""", + // + """ +SELECT count(*)::int +FROM "Blogs" AS b +WHERE b."Id" = 9 AND b."Name" = 'Blog2Suffix' +"""); + } + + public override async Task Terminating_ExecuteUpdateAsync() + { + await base.Terminating_ExecuteUpdateAsync(); + + AssertSql( + """ +@__suffix_0='Suffix' + +UPDATE "Blogs" AS b +SET "Name" = COALESCE(b."Name", '') || @__suffix_0 +WHERE b."Id" > 8 +""", + // + """ +SELECT count(*)::int +FROM "Blogs" AS b +WHERE b."Id" = 9 AND b."Name" = 'Blog2Suffix' +"""); + } + + #endregion Reducing terminating operators + + #region SQL expression quotability + + public override async Task Union() + { + await base.Union(); + + AssertSql( + """ +SELECT `u`.`Id`, `u`.`Name` +FROM ( + SELECT `b`.`Id`, `b`.`Name` + FROM `Blogs` AS `b` + WHERE `b`.`Id` > 7 + UNION + SELECT `b0`.`Id`, `b0`.`Name` + FROM `Blogs` AS `b0` + WHERE `b0`.`Id` < 10 +) AS `u` +ORDER BY `u`.`Id` +"""); + } + + public override async Task UnionOnEntitiesWithJson() + { + await base.UnionOnEntitiesWithJson(); + + AssertSql( + """ +SELECT [u].[Id], [u].[Name], [u].[Json] +FROM ( + SELECT [b].[Id], [b].[Name], [b].[Json] + FROM [Blogs] AS [b] + WHERE [b].[Id] > 7 + UNION + SELECT [b0].[Id], [b0].[Name], [b0].[Json] + FROM [Blogs] AS [b0] + WHERE [b0].[Id] < 10 +) AS [u] +ORDER BY [u].[Id] +"""); + } + + public override async Task Concat() + { + await base.Concat(); + + AssertSql( + """ +SELECT `u`.`Id`, `u`.`Name` +FROM ( + SELECT `b`.`Id`, `b`.`Name` + FROM `Blogs` AS `b` + WHERE `b`.`Id` > 7 + UNION ALL + SELECT `b0`.`Id`, `b0`.`Name` + FROM `Blogs` AS `b0` + WHERE `b0`.`Id` < 10 +) AS `u` +ORDER BY `u`.`Id` +"""); + } + + public override async Task ConcatOnEntitiesWithJson() + { + await base.ConcatOnEntitiesWithJson(); + + AssertSql( + """ +SELECT [u].[Id], [u].[Name], [u].[Json] +FROM ( + SELECT [b].[Id], [b].[Name], [b].[Json] + FROM [Blogs] AS [b] + WHERE [b].[Id] > 7 + UNION ALL + SELECT [b0].[Id], [b0].[Name], [b0].[Json] + FROM [Blogs] AS [b0] + WHERE [b0].[Id] < 10 +) AS [u] +ORDER BY [u].[Id] +"""); + } + + public override async Task Intersect() + { + await base.Intersect(); + + AssertSql( + """ +SELECT [i].[Id], [i].[Name] +FROM ( + SELECT [b].[Id], [b].[Name] + FROM [Blogs] AS [b] + WHERE [b].[Id] > 7 + INTERSECT + SELECT [b0].[Id], [b0].[Name] + FROM [Blogs] AS [b0] + WHERE [b0].[Id] > 8 +) AS [i] +ORDER BY [i].[Id] +"""); + } + + public override async Task IntersectOnEntitiesWithJson() + { + await base.IntersectOnEntitiesWithJson(); + + AssertSql( + """ +SELECT [i].[Id], [i].[Name], [i].[Json] +FROM ( + SELECT [b].[Id], [b].[Name], [b].[Json] + FROM [Blogs] AS [b] + WHERE [b].[Id] > 7 + INTERSECT + SELECT [b0].[Id], [b0].[Name], [b0].[Json] + FROM [Blogs] AS [b0] + WHERE [b0].[Id] > 8 +) AS [i] +ORDER BY [i].[Id] +"""); + } + + public override async Task Except() + { + await base.Except(); + + AssertSql( + """ +SELECT [e].[Id], [e].[Name] +FROM ( + SELECT [b].[Id], [b].[Name] + FROM [Blogs] AS [b] + WHERE [b].[Id] > 7 + EXCEPT + SELECT [b0].[Id], [b0].[Name] + FROM [Blogs] AS [b0] + WHERE [b0].[Id] > 8 +) AS [e] +ORDER BY [e].[Id] +"""); + } + + public override async Task ExceptOnEntitiesWithJson() + { + await base.ExceptOnEntitiesWithJson(); + + AssertSql( + """ +SELECT [e].[Id], [e].[Name], [e].[Json] +FROM ( + SELECT [b].[Id], [b].[Name], [b].[Json] + FROM [Blogs] AS [b] + WHERE [b].[Id] > 7 + EXCEPT + SELECT [b0].[Id], [b0].[Name], [b0].[Json] + FROM [Blogs] AS [b0] + WHERE [b0].[Id] > 8 +) AS [e] +ORDER BY [e].[Id] +"""); + } + + public override async Task ValuesExpression() + { + await base.ValuesExpression(); + + AssertSql( + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE ( + SELECT count(*)::int + FROM (VALUES (7::int), (b."Id")) AS v("Value") + WHERE v."Value" > 8) = 2 +"""); + } + + public override async Task Contains_with_parameterized_collection() + { + await base.Contains_with_parameterized_collection(); + + AssertSql( + """ +SELECT `b`.`Id`, `b`.`Name` +FROM `Blogs` AS `b` +WHERE `b`.`Id` IN (1, 2, 3) +"""); + } + + public override async Task FromSqlRaw() + { + await base.FromSqlRaw(); + +AssertSql( + """ +SELECT m."Id", m."Name", m."Json" +FROM ( + SELECT * FROM "Blogs" WHERE "Id" > 8 +) AS m +ORDER BY m."Id" NULLS FIRST +"""); + } + + public override async Task FromSql_with_FormattableString_parameters() + { + await base.FromSql_with_FormattableString_parameters(); + + AssertSql( + """ +p0='8' +p1='9' + +SELECT m."Id", m."Name", m."Json" +FROM ( + SELECT * FROM "Blogs" WHERE "Id" > @p0 AND "Id" < @p1 +) AS m +ORDER BY m."Id" NULLS FIRST +"""); + } + + // SqlServerOpenJsonExpression is covered by PrecompiledQueryRelationalTestBase.Contains_with_parameterized_collection + +// [ConditionalFact] +// public virtual Task TableValuedFunctionExpression_toplevel() +// => Test( +// "_ = context.GetBlogsWithAtLeast(9).ToList();", +// modelSourceCode: providerOptions => $$""" +// public class BlogContext : DbContext +// { +// public DbSet Blogs { get; set; } +// +// protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) +// => optionsBuilder +// {{providerOptions}} +// .ReplaceService(); +// +// protected override void OnModelCreating(ModelBuilder modelBuilder) +// { +// modelBuilder.HasDbFunction(typeof(BlogContext).GetMethod(nameof(GetBlogsWithAtLeast))); +// } +// +// public IQueryable GetBlogsWithAtLeast(int minBlogId) => FromExpression(() => GetBlogsWithAtLeast(minBlogId)); +// } +// +// public class Blog +// { +// [DatabaseGenerated(DatabaseGeneratedOption.None)] +// public int Id { get; set; } +// public string StringProperty { get; set; } +// } +// """, +// setupSql: """ +// CREATE FUNCTION dbo.GetBlogsWithAtLeast(@minBlogId int) +// RETURNS TABLE AS RETURN +// ( +// SELECT [b].[Id], [b].[Name] FROM [Blogs] AS [b] WHERE [b].[Id] >= @minBlogId +// ) +// """, +// cleanupSql: "DROP FUNCTION dbo.GetBlogsWithAtLeast;"); +// +// [ConditionalFact] +// public virtual Task TableValuedFunctionExpression_non_toplevel() +// => Test( +// "_ = context.Blogs.Where(b => context.GetPosts(b.Id).Count() == 2).ToList();", +// modelSourceCode: providerOptions => $$""" +// public class BlogContext : DbContext +// { +// public DbSet Blogs { get; set; } +// +// protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) +// => optionsBuilder +// {{providerOptions}} +// .ReplaceService(); +// +// protected override void OnModelCreating(ModelBuilder modelBuilder) +// { +// modelBuilder.HasDbFunction(typeof(BlogContext).GetMethod(nameof(GetPosts))); +// } +// +// public IQueryable GetPosts(int blogId) => FromExpression(() => GetPosts(blogId)); +// } +// +// public class Blog +// { +// public int Id { get; set; } +// public string StringProperty { get; set; } +// public List Post { get; set; } +// } +// +// public class Post +// { +// public int Id { get; set; } +// public string Title { get; set; } +// +// public Blog Blog { get; set; } +// } +// """, +// setupSql: """ +// CREATE FUNCTION dbo.GetPosts(@blogId int) +// RETURNS TABLE AS RETURN +// ( +// SELECT [p].[Id], [p].[Title], [p].[BlogId] FROM [Posts] AS [p] WHERE [p].[BlogId] = @blogId +// ) +// """, +// cleanupSql: "DROP FUNCTION dbo.GetPosts;"); + + #endregion SQL expression quotability + + #region Different query roots + + public override async Task DbContext_as_local_variable() + { + await base.DbContext_as_local_variable(); + + AssertSql( + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +"""); + } + + public override async Task DbContext_as_field() + { + await base.DbContext_as_field(); + + AssertSql( + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +"""); + } + + public override async Task DbContext_as_property() + { + await base.DbContext_as_property(); + + AssertSql( + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +"""); + } + + public override async Task DbContext_as_captured_variable() + { + await base.DbContext_as_captured_variable(); + + AssertSql( + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +"""); + } + + public override async Task DbContext_as_method_invocation_result() + { + await base.DbContext_as_method_invocation_result(); + + AssertSql( + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +"""); + } + + #endregion Different query roots + + #region Negative cases + + public override async Task Dynamic_query_does_not_get_precompiled() + { + await base.Dynamic_query_does_not_get_precompiled(); + + AssertSql(); + } + + public override async Task ToList_over_objects_does_not_get_precompiled() + { + await base.ToList_over_objects_does_not_get_precompiled(); + + AssertSql(); + } + + public override async Task Query_compilation_failure() + { + await base.Query_compilation_failure(); + + AssertSql(); + } + + public override async Task EF_Constant_is_not_supported() + { + await base.EF_Constant_is_not_supported(); + + AssertSql(); + } + + public override async Task NotParameterizedAttribute_with_constant() + { + await base.NotParameterizedAttribute_with_constant(); +AssertSql( + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Name" = 'Blog2' +LIMIT 2 +"""); + } + + public override async Task NotParameterizedAttribute_is_not_supported_with_non_constant_argument() + { + await base.NotParameterizedAttribute_is_not_supported_with_non_constant_argument(); + + AssertSql(); + } + + public override async Task Query_syntax_is_not_supported() + { + await base.Query_syntax_is_not_supported(); + + AssertSql(); + } + + #endregion Negative cases + + public override async Task Select_changes_type() + { + await base.Select_changes_type(); + + AssertSql( + """ +SELECT b."Name" +FROM "Blogs" AS b +"""); + } + + public override async Task OrderBy() + { + await base.OrderBy(); + +AssertSql( + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +ORDER BY b."Name" NULLS FIRST +"""); + } + + public override async Task Skip() + { + await base.Skip(); + + AssertSql( + """ +@__p_0='1' + +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +ORDER BY b."Name" NULLS FIRST +OFFSET @__p_0 +"""); + } + + public override async Task Take() + { + await base.Take(); + + AssertSql( + """ +@__p_0='1' + +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +ORDER BY b."Name" NULLS FIRST +LIMIT @__p_0 +"""); + } + + public override async Task Project_anonymous_object() + { + await base.Project_anonymous_object(); + + AssertSql( + """ +@__p_0='1' + +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +ORDER BY b."Name" NULLS FIRST +LIMIT @__p_0 +"""); + } + + public override async Task Two_captured_variables_in_same_lambda() + { + await base.Two_captured_variables_in_same_lambda(); + + AssertSql( + """ +@__yes_0='yes' +@__no_1='no' + +SELECT CASE + WHEN b."Id" = 3 THEN @__yes_0 + ELSE @__no_1 +END +FROM "Blogs" AS b +"""); + } + + public override async Task Two_captured_variables_in_different_lambdas() + { + await base.Two_captured_variables_in_different_lambdas(); + + AssertSql( + """ +@__starts_0_startswith='blog%' (Size = 255) +@__ends_1_endswith='%2' (Size = 255) + +SELECT TOP 2 `b`.`Id`, `b`.`Name` +FROM `Blogs` AS `b` +WHERE (`b`.`Name` LIKE @__starts_0_startswith) AND (`b`.`Name` LIKE @__ends_1_endswith) +"""); + } + + public override async Task Same_captured_variable_twice_in_same_lambda() + { + await base.Same_captured_variable_twice_in_same_lambda(); + + AssertSql( + """ +@__foo_0_startswith='X%' +@__foo_0_endswith='%X' + +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Name" LIKE @__foo_0_startswith AND b."Name" LIKE @__foo_0_endswith +"""); + } + + public override async Task Same_captured_variable_twice_in_different_lambdas() + { + await base.Same_captured_variable_twice_in_different_lambdas(); + + AssertSql( + """ +@__foo_0_startswith='X%' +@__foo_0_endswith='%X' + +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Name" LIKE @__foo_0_startswith AND b."Name" LIKE @__foo_0_endswith +"""); + } + + public override async Task Include_single() + { + await base.Include_single(); + + AssertSql( + """ +SELECT b."Id", b."Name", b."Json", p."Id", p."BlogId", p."Title" +FROM "Blogs" AS b +LEFT JOIN "Posts" AS p ON b."Id" = p."BlogId" +WHERE b."Id" > 8 +ORDER BY b."Id" NULLS FIRST +"""); + } + + public override async Task Include_split() + { + await base.Include_split(); + + AssertSql( + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +ORDER BY b."Id" NULLS FIRST +""", + // + """ +SELECT p."Id", p."BlogId", p."Title", b."Id" +FROM "Blogs" AS b +INNER JOIN "Posts" AS p ON b."Id" = p."BlogId" +ORDER BY b."Id" NULLS FIRST +"""); + } + + public override async Task Final_GroupBy() + { + await base.Final_GroupBy(); + + AssertSql( + """ +SELECT b."Name", b."Id", b."Json" +FROM "Blogs" AS b +ORDER BY b."Name" NULLS FIRST +"""); + } + + public override async Task Multiple_queries_with_captured_variables() + { + await base.Multiple_queries_with_captured_variables(); + + AssertSql( + """ +@__id1_0='8' +@__id2_1='9' + +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = @__id1_0 OR b."Id" = @__id2_1 +""", + // + """ +@__id1_0='8' + +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = @__id1_0 +LIMIT 2 +"""); + } + + public override async Task Unsafe_accessor_gets_generated_once_for_multiple_queries() + { + await base.Unsafe_accessor_gets_generated_once_for_multiple_queries(); + + AssertSql( + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +""", + // + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +"""); + } + + [ConditionalFact] + public virtual void Check_all_tests_overridden() + => TestHelpers.AssertAllMethodsOverridden(GetType()); + + public class PrecompiledQueryNpgsqlFixture : PrecompiledQueryRelationalFixture + { + protected override ITestStoreFactory TestStoreFactory + => NpgsqlTestStoreFactory.Instance; + + public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) + { + builder = base.AddOptions(builder); + + // TODO: Figure out if there's a nice way to continue using the retrying strategy + var jetOptionsBuilder = new NpgsqlDbContextOptionsBuilder(builder); + jetOptionsBuilder.ExecutionStrategy(d => new NonRetryingExecutionStrategy(d)); + return builder; + } + + protected override async Task SeedAsync(PrecompiledQueryContext context) + { + var blog1 = new Blog { Id = 8, Name = "Blog1", Json = [] }; + var blog2 = new Blog + { + Id = 9, + Name = "Blog2", + Json = + [ + new JsonRoot { Number = 1, Text = "One", Inner = new JsonBranch { Date = new DateTime(2001, 1, 1,0, 0, 0, DateTimeKind.Utc) } }, + new JsonRoot { Number = 2, Text = "Two", Inner = new JsonBranch { Date = new DateTime(2002, 2, 2,0, 0, 0, DateTimeKind.Utc) } }, + ] + }; + + context.Blogs.AddRange(blog1, blog2); + + var post11 = new Post { Id = 11, Title = "Post11", Blog = blog1 }; + var post12 = new Post { Id = 12, Title = "Post12", Blog = blog1 }; + var post21 = new Post { Id = 21, Title = "Post21", Blog = blog2 }; + var post22 = new Post { Id = 22, Title = "Post22", Blog = blog2 }; + var post23 = new Post { Id = 23, Title = "Post23", Blog = blog2 }; + + context.Posts.AddRange(post11, post12, post21, post22, post23); + await context.SaveChangesAsync(); + } + + public override PrecompiledQueryTestHelpers PrecompiledQueryTestHelpers => NpgsqlPrecompiledQueryTestHelpers.Instance; + } +} diff --git a/test/EFCore.PG.FunctionalTests/Query/PrecompiledSqlPregenerationQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/PrecompiledSqlPregenerationQueryNpgsqlTest.cs new file mode 100644 index 000000000..5e45fc50b --- /dev/null +++ b/test/EFCore.PG.FunctionalTests/Query/PrecompiledSqlPregenerationQueryNpgsqlTest.cs @@ -0,0 +1,265 @@ +using Microsoft.EntityFrameworkCore.Query.Internal; +using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query; + +// ReSharper disable InconsistentNaming + +public class PrecompiledSqlPregenerationQueryNpgsqlTest( + PrecompiledSqlPregenerationQueryNpgsqlTest.PrecompiledSqlPregenerationQueryNpgsqlFixture fixture, + ITestOutputHelper testOutputHelper) + : PrecompiledSqlPregenerationQueryRelationalTestBase(fixture, testOutputHelper), + IClassFixture +{ + protected override bool AlwaysPrintGeneratedSources + => false; + + public override async Task No_parameters() + { + await base.No_parameters(); + + AssertSql( + """ +SELECT b."Id", b."Name" +FROM "Blogs" AS b +WHERE b."Name" = 'foo' +"""); + } + + public override async Task Non_nullable_value_type() + { + await base.Non_nullable_value_type(); + + AssertSql( + """ +@__id_0='8' + +SELECT b."Id", b."Name" +FROM "Blogs" AS b +WHERE b."Id" = @__id_0 +"""); + } + + public override async Task Nullable_value_type() + { + await base.Nullable_value_type(); + + AssertSql( + """ +@__id_0='8' (Nullable = true) + +SELECT b."Id", b."Name" +FROM "Blogs" AS b +WHERE b."Id" = @__id_0 +"""); + } + + public override async Task Nullable_reference_type() + { + await base.Nullable_reference_type(); + + AssertSql( + """ +@__name_0='bar' + +SELECT b."Id", b."Name" +FROM "Blogs" AS b +WHERE b."Name" = @__name_0 +"""); + } + + public override async Task Non_nullable_reference_type() + { + await base.Non_nullable_reference_type(); + + AssertSql( + """ +@__name_0='bar' (Nullable = false) + +SELECT b."Id", b."Name" +FROM "Blogs" AS b +WHERE b."Name" = @__name_0 +"""); + } + + public override async Task Nullable_and_non_nullable_value_types() + { + await base.Nullable_and_non_nullable_value_types(); + + AssertSql( + """ +@__id1_0='8' (Nullable = true) +@__id2_1='9' + +SELECT b."Id", b."Name" +FROM "Blogs" AS b +WHERE b."Id" = @__id1_0 OR b."Id" = @__id2_1 +"""); + } + + public override async Task Two_nullable_reference_types() + { + await base.Two_nullable_reference_types(); + + AssertSql( + """ +@__name1_0='foo' +@__name2_1='bar' + +SELECT b."Id", b."Name" +FROM "Blogs" AS b +WHERE b."Name" = @__name1_0 OR b."Name" = @__name2_1 +"""); + } + + public override async Task Two_non_nullable_reference_types() + { + await base.Two_non_nullable_reference_types(); + + AssertSql( + """ +@__name1_0='foo' (Nullable = false) +@__name2_1='bar' (Nullable = false) + +SELECT b."Id", b."Name" +FROM "Blogs" AS b +WHERE b."Name" = @__name1_0 OR b."Name" = @__name2_1 +"""); + } + + public override async Task Nullable_and_non_nullable_reference_types() + { + await base.Nullable_and_non_nullable_reference_types(); + + AssertSql( + """ +@__name1_0='foo' +@__name2_1='bar' (Nullable = false) + +SELECT b."Id", b."Name" +FROM "Blogs" AS b +WHERE b."Name" = @__name1_0 OR b."Name" = @__name2_1 +"""); + } + + public override async Task Too_many_nullable_parameters_prevent_pregeneration() + { + await base.Too_many_nullable_parameters_prevent_pregeneration(); + + AssertSql( + """ +@__name1_0='foo' +@__name2_1='bar' +@__name3_2='baz' +@__name4_3='baq' + +SELECT b."Id", b."Name" +FROM "Blogs" AS b +WHERE b."Name" = @__name1_0 OR b."Name" = @__name2_1 OR b."Name" = @__name3_2 OR b."Name" = @__name4_3 +"""); + } + + public override async Task Many_non_nullable_parameters_do_not_prevent_pregeneration() + { + await base.Many_non_nullable_parameters_do_not_prevent_pregeneration(); + + AssertSql( + """ +@__name1_0='foo' (Nullable = false) +@__name2_1='bar' (Nullable = false) +@__name3_2='baz' (Nullable = false) +@__name4_3='baq' (Nullable = false) + +SELECT b."Id", b."Name" +FROM "Blogs" AS b +WHERE b."Name" = @__name1_0 OR b."Name" = @__name2_1 OR b."Name" = @__name3_2 OR b."Name" = @__name4_3 +"""); + } + + #region Tests for the different querying enumerables + + public override async Task Include_single_query() + { + await base.Include_single_query(); + + AssertSql( + """ +SELECT b."Id", b."Name", p."Id", p."BlogId", p."Title" +FROM "Blogs" AS b +LEFT JOIN "Post" AS p ON b."Id" = p."BlogId" +ORDER BY b."Id" NULLS FIRST +"""); + } + + public override async Task Include_split_query() + { + await base.Include_split_query(); + + AssertSql( + """ +SELECT b."Id", b."Name" +FROM "Blogs" AS b +ORDER BY b."Id" NULLS FIRST +""", + // + """ +SELECT p."Id", p."BlogId", p."Title", b."Id" +FROM "Blogs" AS b +INNER JOIN "Post" AS p ON b."Id" = p."BlogId" +ORDER BY b."Id" NULLS FIRST +"""); + } + + public override async Task Final_GroupBy() + { + await base.Final_GroupBy(); + + AssertSql( + """ +SELECT b."Name", b."Id" +FROM "Blogs" AS b +ORDER BY b."Name" NULLS FIRST +"""); + } + + #endregion Tests for the different querying enumerables + + [ConditionalFact] + public virtual async Task Do_not_cache_is_respected() + { + // The "do not cache" flag in the 2nd part of the query pipeline is turned on in provider-specific situations, so we test it + // here in SQL Server; note that SQL Server compatibility mode is set low to trigger this. + await Test( + """ +string[] names = ["foo", "bar"]; +var blogs = await context.Blogs.Where(b => names.Contains(b.Name)).ToListAsync(); +""", + interceptorCodeAsserter: code => Assert.Contains(nameof(RelationalCommandCache), code)); + + AssertSql( + """ +SELECT `b`.`Id`, `b`.`Name` +FROM `Blogs` AS `b` +WHERE `b`.`Name` IN ('foo', 'bar') +"""); + } + + public class PrecompiledSqlPregenerationQueryNpgsqlFixture : PrecompiledSqlPregenerationQueryRelationalFixture + { + protected override ITestStoreFactory TestStoreFactory + => NpgsqlTestStoreFactory.Instance; + + /*public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) + { + builder = base.AddOptions(builder); + + // TODO: Figure out if there's a nice way to continue using the retrying strategy + var jetOptionsBuilder = new JetDbContextOptionsBuilder(builder); + jetOptionsBuilder + .ExecutionStrategy(d => new NonRetryingExecutionStrategy(d)); + return builder; + }*/ + + public override PrecompiledQueryTestHelpers PrecompiledQueryTestHelpers => NpgsqlPrecompiledQueryTestHelpers.Instance; + } +} diff --git a/test/EFCore.PG.FunctionalTests/TestUtilities/NpgsqlPrecompiledQueryTestHelpers.cs b/test/EFCore.PG.FunctionalTests/TestUtilities/NpgsqlPrecompiledQueryTestHelpers.cs new file mode 100644 index 000000000..5d5c14cb6 --- /dev/null +++ b/test/EFCore.PG.FunctionalTests/TestUtilities/NpgsqlPrecompiledQueryTestHelpers.cs @@ -0,0 +1,15 @@ +using Microsoft.CodeAnalysis; +using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities; + +public class NpgsqlPrecompiledQueryTestHelpers : PrecompiledQueryTestHelpers +{ + public static NpgsqlPrecompiledQueryTestHelpers Instance = new(); + + protected override IEnumerable BuildProviderMetadataReferences() + { + yield return MetadataReference.CreateFromFile(typeof(NpgsqlOptionsExtension).Assembly.Location); + yield return MetadataReference.CreateFromFile(Assembly.GetExecutingAssembly().Location); + } +} From 62c3fdee4aa2e507c9835a9cbb1d2c6bd8f3a182 Mon Sep 17 00:00:00 2001 From: Christopher Jolly Date: Sun, 22 Sep 2024 04:17:02 +0800 Subject: [PATCH 2/6] Add reference to Npgsql in the test helpers so things like NpgsqlTsVector etc can work --- .../Query/PrecompiledQueryNpgsqlTest.cs | 96 +++++++++---------- ...compiledSqlPregenerationQueryNpgsqlTest.cs | 9 +- .../NpgsqlPrecompiledQueryTestHelpers.cs | 1 + 3 files changed, 51 insertions(+), 55 deletions(-) diff --git a/test/EFCore.PG.FunctionalTests/Query/PrecompiledQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/PrecompiledQueryNpgsqlTest.cs index de89b2912..51469d12f 100644 --- a/test/EFCore.PG.FunctionalTests/Query/PrecompiledQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/PrecompiledQueryNpgsqlTest.cs @@ -603,21 +603,19 @@ public override async Task Terminating_Contains() """ @__p_0='8' -SELECT IIF(@__p_0 IN ( - SELECT `b`.`Id` - FROM `Blogs` AS `b` - ), TRUE, FALSE) -FROM (SELECT COUNT(*) FROM `#Dual`) +SELECT @__p_0 IN ( + SELECT b."Id" + FROM "Blogs" AS b +) """, // """ @__p_0='7' -SELECT IIF(@__p_0 IN ( - SELECT `b`.`Id` - FROM `Blogs` AS `b` - ), TRUE, FALSE) -FROM (SELECT COUNT(*) FROM `#Dual`) +SELECT @__p_0 IN ( + SELECT b."Id" + FROM "Blogs" AS b +) """); } @@ -1418,17 +1416,17 @@ public override async Task Union() AssertSql( """ -SELECT `u`.`Id`, `u`.`Name` +SELECT u."Id", u."BlogId", u."Title" FROM ( - SELECT `b`.`Id`, `b`.`Name` - FROM `Blogs` AS `b` - WHERE `b`.`Id` > 7 + SELECT p."Id", p."BlogId", p."Title" + FROM "Posts" AS p + WHERE p."Id" > 11 UNION - SELECT `b0`.`Id`, `b0`.`Name` - FROM `Blogs` AS `b0` - WHERE `b0`.`Id` < 10 -) AS `u` -ORDER BY `u`.`Id` + SELECT p0."Id", p0."BlogId", p0."Title" + FROM "Posts" AS p0 + WHERE p0."Id" < 21 +) AS u +ORDER BY u."Id" NULLS FIRST """); } @@ -1458,17 +1456,17 @@ public override async Task Concat() AssertSql( """ -SELECT `u`.`Id`, `u`.`Name` +SELECT u."Id", u."BlogId", u."Title" FROM ( - SELECT `b`.`Id`, `b`.`Name` - FROM `Blogs` AS `b` - WHERE `b`.`Id` > 7 + SELECT p."Id", p."BlogId", p."Title" + FROM "Posts" AS p + WHERE p."Id" > 11 UNION ALL - SELECT `b0`.`Id`, `b0`.`Name` - FROM `Blogs` AS `b0` - WHERE `b0`.`Id` < 10 -) AS `u` -ORDER BY `u`.`Id` + SELECT p0."Id", p0."BlogId", p0."Title" + FROM "Posts" AS p0 + WHERE p0."Id" < 21 +) AS u +ORDER BY u."Id" NULLS FIRST """); } @@ -1498,17 +1496,17 @@ public override async Task Intersect() AssertSql( """ -SELECT [i].[Id], [i].[Name] +SELECT i."Id", i."BlogId", i."Title" FROM ( - SELECT [b].[Id], [b].[Name] - FROM [Blogs] AS [b] - WHERE [b].[Id] > 7 + SELECT p."Id", p."BlogId", p."Title" + FROM "Posts" AS p + WHERE p."Id" > 11 INTERSECT - SELECT [b0].[Id], [b0].[Name] - FROM [Blogs] AS [b0] - WHERE [b0].[Id] > 8 -) AS [i] -ORDER BY [i].[Id] + SELECT p0."Id", p0."BlogId", p0."Title" + FROM "Posts" AS p0 + WHERE p0."Id" < 22 +) AS i +ORDER BY i."Id" NULLS FIRST """); } @@ -1538,17 +1536,17 @@ public override async Task Except() AssertSql( """ -SELECT [e].[Id], [e].[Name] +SELECT e."Id", e."BlogId", e."Title" FROM ( - SELECT [b].[Id], [b].[Name] - FROM [Blogs] AS [b] - WHERE [b].[Id] > 7 + SELECT p."Id", p."BlogId", p."Title" + FROM "Posts" AS p + WHERE p."Id" > 11 EXCEPT - SELECT [b0].[Id], [b0].[Name] - FROM [Blogs] AS [b0] - WHERE [b0].[Id] > 8 -) AS [e] -ORDER BY [e].[Id] + SELECT p0."Id", p0."BlogId", p0."Title" + FROM "Posts" AS p0 + WHERE p0."Id" > 21 +) AS e +ORDER BY e."Id" NULLS FIRST """); } @@ -1894,12 +1892,8 @@ public override async Task Project_anonymous_object() AssertSql( """ -@__p_0='1' - -SELECT b."Id", b."Name", b."Json" +SELECT COALESCE(b."Name", '') || 'Foo' AS "Foo" FROM "Blogs" AS b -ORDER BY b."Name" NULLS FIRST -LIMIT @__p_0 """); } diff --git a/test/EFCore.PG.FunctionalTests/Query/PrecompiledSqlPregenerationQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/PrecompiledSqlPregenerationQueryNpgsqlTest.cs index 5e45fc50b..2b7c192ff 100644 --- a/test/EFCore.PG.FunctionalTests/Query/PrecompiledSqlPregenerationQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/PrecompiledSqlPregenerationQueryNpgsqlTest.cs @@ -1,4 +1,5 @@ using Microsoft.EntityFrameworkCore.Query.Internal; +using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities; namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query; @@ -249,16 +250,16 @@ public class PrecompiledSqlPregenerationQueryNpgsqlFixture : PrecompiledSqlPrege protected override ITestStoreFactory TestStoreFactory => NpgsqlTestStoreFactory.Instance; - /*public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) + public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) { builder = base.AddOptions(builder); // TODO: Figure out if there's a nice way to continue using the retrying strategy - var jetOptionsBuilder = new JetDbContextOptionsBuilder(builder); - jetOptionsBuilder + var npgsqlOptionsBuilder = new NpgsqlDbContextOptionsBuilder(builder); + npgsqlOptionsBuilder .ExecutionStrategy(d => new NonRetryingExecutionStrategy(d)); return builder; - }*/ + } public override PrecompiledQueryTestHelpers PrecompiledQueryTestHelpers => NpgsqlPrecompiledQueryTestHelpers.Instance; } diff --git a/test/EFCore.PG.FunctionalTests/TestUtilities/NpgsqlPrecompiledQueryTestHelpers.cs b/test/EFCore.PG.FunctionalTests/TestUtilities/NpgsqlPrecompiledQueryTestHelpers.cs index 5d5c14cb6..653a7c5a2 100644 --- a/test/EFCore.PG.FunctionalTests/TestUtilities/NpgsqlPrecompiledQueryTestHelpers.cs +++ b/test/EFCore.PG.FunctionalTests/TestUtilities/NpgsqlPrecompiledQueryTestHelpers.cs @@ -10,6 +10,7 @@ public class NpgsqlPrecompiledQueryTestHelpers : PrecompiledQueryTestHelpers protected override IEnumerable BuildProviderMetadataReferences() { yield return MetadataReference.CreateFromFile(typeof(NpgsqlOptionsExtension).Assembly.Location); + yield return MetadataReference.CreateFromFile(typeof(NpgsqlTsVector).Assembly.Location); yield return MetadataReference.CreateFromFile(Assembly.GetExecutingAssembly().Location); } } From 9b343aef29f0747c99851bbb063754423a4d8d63 Mon Sep 17 00:00:00 2001 From: Christopher Jolly Date: Sun, 22 Sep 2024 04:17:26 +0800 Subject: [PATCH 3/6] Remove unneeded test --- ...compiledSqlPregenerationQueryNpgsqlTest.cs | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/test/EFCore.PG.FunctionalTests/Query/PrecompiledSqlPregenerationQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/PrecompiledSqlPregenerationQueryNpgsqlTest.cs index 2b7c192ff..37958e5b0 100644 --- a/test/EFCore.PG.FunctionalTests/Query/PrecompiledSqlPregenerationQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/PrecompiledSqlPregenerationQueryNpgsqlTest.cs @@ -225,26 +225,6 @@ ORDER BY b."Name" NULLS FIRST #endregion Tests for the different querying enumerables - [ConditionalFact] - public virtual async Task Do_not_cache_is_respected() - { - // The "do not cache" flag in the 2nd part of the query pipeline is turned on in provider-specific situations, so we test it - // here in SQL Server; note that SQL Server compatibility mode is set low to trigger this. - await Test( - """ -string[] names = ["foo", "bar"]; -var blogs = await context.Blogs.Where(b => names.Contains(b.Name)).ToListAsync(); -""", - interceptorCodeAsserter: code => Assert.Contains(nameof(RelationalCommandCache), code)); - - AssertSql( - """ -SELECT `b`.`Id`, `b`.`Name` -FROM `Blogs` AS `b` -WHERE `b`.`Name` IN ('foo', 'bar') -"""); - } - public class PrecompiledSqlPregenerationQueryNpgsqlFixture : PrecompiledSqlPregenerationQueryRelationalFixture { protected override ITestStoreFactory TestStoreFactory From fb00b564ab7cab035d1849edb70f4450e9dbbe75 Mon Sep 17 00:00:00 2001 From: Christopher Jolly Date: Sun, 22 Sep 2024 04:33:14 +0800 Subject: [PATCH 4/6] Wrong argument type for PgAnyExpression constructor --- .../Query/Expressions/Internal/PgAnyExpression.cs | 2 +- .../Query/PrecompiledQueryNpgsqlTest.cs | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/EFCore.PG/Query/Expressions/Internal/PgAnyExpression.cs b/src/EFCore.PG/Query/Expressions/Internal/PgAnyExpression.cs index 3d57bc228..40c732644 100644 --- a/src/EFCore.PG/Query/Expressions/Internal/PgAnyExpression.cs +++ b/src/EFCore.PG/Query/Expressions/Internal/PgAnyExpression.cs @@ -80,7 +80,7 @@ public virtual PgAnyExpression Update(SqlExpression item, SqlExpression array) public override Expression Quote() => New( _quotingConstructor ??= typeof(PgAnyExpression).GetConstructor( - [typeof(SqlExpression), typeof(SqlExpression), typeof(PgAllOperatorType), typeof(RelationalTypeMapping)])!, + [typeof(SqlExpression), typeof(SqlExpression), typeof(PgAnyOperatorType), typeof(RelationalTypeMapping)])!, Item.Quote(), Array.Quote(), Constant(OperatorType), diff --git a/test/EFCore.PG.FunctionalTests/Query/PrecompiledQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/PrecompiledQueryNpgsqlTest.cs index 51469d12f..64fa40cf2 100644 --- a/test/EFCore.PG.FunctionalTests/Query/PrecompiledQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/PrecompiledQueryNpgsqlTest.cs @@ -1591,9 +1591,11 @@ public override async Task Contains_with_parameterized_collection() AssertSql( """ -SELECT `b`.`Id`, `b`.`Name` -FROM `Blogs` AS `b` -WHERE `b`.`Id` IN (1, 2, 3) +@__ids_0={ '1', '2', '3' } (DbType = Object) + +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = ANY (@__ids_0) """); } From 101a028d91f523bfe190b30e6f9dbbe6560b0211 Mon Sep 17 00:00:00 2001 From: Christopher Jolly Date: Sun, 22 Sep 2024 16:11:12 +0800 Subject: [PATCH 5/6] Update test/EFCore.PG.FunctionalTests/TestUtilities/NpgsqlPrecompiledQueryTestHelpers.cs Co-authored-by: Shay Rojansky --- .../TestUtilities/NpgsqlPrecompiledQueryTestHelpers.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/EFCore.PG.FunctionalTests/TestUtilities/NpgsqlPrecompiledQueryTestHelpers.cs b/test/EFCore.PG.FunctionalTests/TestUtilities/NpgsqlPrecompiledQueryTestHelpers.cs index 653a7c5a2..104096b41 100644 --- a/test/EFCore.PG.FunctionalTests/TestUtilities/NpgsqlPrecompiledQueryTestHelpers.cs +++ b/test/EFCore.PG.FunctionalTests/TestUtilities/NpgsqlPrecompiledQueryTestHelpers.cs @@ -10,7 +10,7 @@ public class NpgsqlPrecompiledQueryTestHelpers : PrecompiledQueryTestHelpers protected override IEnumerable BuildProviderMetadataReferences() { yield return MetadataReference.CreateFromFile(typeof(NpgsqlOptionsExtension).Assembly.Location); - yield return MetadataReference.CreateFromFile(typeof(NpgsqlTsVector).Assembly.Location); + yield return MetadataReference.CreateFromFile(typeof(NpgsqlConnection).Assembly.Location); yield return MetadataReference.CreateFromFile(Assembly.GetExecutingAssembly().Location); } } From 8595042e429f6691e2698f4fd2052d7db1f376bd Mon Sep 17 00:00:00 2001 From: Christopher Jolly Date: Mon, 23 Sep 2024 14:10:52 +0800 Subject: [PATCH 6/6] Tidy up some unneeded and fix last test --- .../Query/PrecompiledQueryNpgsqlTest.cs | 113 +++--------------- 1 file changed, 15 insertions(+), 98 deletions(-) diff --git a/test/EFCore.PG.FunctionalTests/Query/PrecompiledQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/PrecompiledQueryNpgsqlTest.cs index 64fa40cf2..c64f357b1 100644 --- a/test/EFCore.PG.FunctionalTests/Query/PrecompiledQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/PrecompiledQueryNpgsqlTest.cs @@ -1630,92 +1630,6 @@ ORDER BY m."Id" NULLS FIRST """); } - // SqlServerOpenJsonExpression is covered by PrecompiledQueryRelationalTestBase.Contains_with_parameterized_collection - -// [ConditionalFact] -// public virtual Task TableValuedFunctionExpression_toplevel() -// => Test( -// "_ = context.GetBlogsWithAtLeast(9).ToList();", -// modelSourceCode: providerOptions => $$""" -// public class BlogContext : DbContext -// { -// public DbSet Blogs { get; set; } -// -// protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) -// => optionsBuilder -// {{providerOptions}} -// .ReplaceService(); -// -// protected override void OnModelCreating(ModelBuilder modelBuilder) -// { -// modelBuilder.HasDbFunction(typeof(BlogContext).GetMethod(nameof(GetBlogsWithAtLeast))); -// } -// -// public IQueryable GetBlogsWithAtLeast(int minBlogId) => FromExpression(() => GetBlogsWithAtLeast(minBlogId)); -// } -// -// public class Blog -// { -// [DatabaseGenerated(DatabaseGeneratedOption.None)] -// public int Id { get; set; } -// public string StringProperty { get; set; } -// } -// """, -// setupSql: """ -// CREATE FUNCTION dbo.GetBlogsWithAtLeast(@minBlogId int) -// RETURNS TABLE AS RETURN -// ( -// SELECT [b].[Id], [b].[Name] FROM [Blogs] AS [b] WHERE [b].[Id] >= @minBlogId -// ) -// """, -// cleanupSql: "DROP FUNCTION dbo.GetBlogsWithAtLeast;"); -// -// [ConditionalFact] -// public virtual Task TableValuedFunctionExpression_non_toplevel() -// => Test( -// "_ = context.Blogs.Where(b => context.GetPosts(b.Id).Count() == 2).ToList();", -// modelSourceCode: providerOptions => $$""" -// public class BlogContext : DbContext -// { -// public DbSet Blogs { get; set; } -// -// protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) -// => optionsBuilder -// {{providerOptions}} -// .ReplaceService(); -// -// protected override void OnModelCreating(ModelBuilder modelBuilder) -// { -// modelBuilder.HasDbFunction(typeof(BlogContext).GetMethod(nameof(GetPosts))); -// } -// -// public IQueryable GetPosts(int blogId) => FromExpression(() => GetPosts(blogId)); -// } -// -// public class Blog -// { -// public int Id { get; set; } -// public string StringProperty { get; set; } -// public List Post { get; set; } -// } -// -// public class Post -// { -// public int Id { get; set; } -// public string Title { get; set; } -// -// public Blog Blog { get; set; } -// } -// """, -// setupSql: """ -// CREATE FUNCTION dbo.GetPosts(@blogId int) -// RETURNS TABLE AS RETURN -// ( -// SELECT [p].[Id], [p].[Title], [p].[BlogId] FROM [Posts] AS [p] WHERE [p].[BlogId] = @blogId -// ) -// """, -// cleanupSql: "DROP FUNCTION dbo.GetPosts;"); - #endregion SQL expression quotability #region Different query roots @@ -1918,17 +1832,20 @@ ELSE @__no_1 public override async Task Two_captured_variables_in_different_lambdas() { - await base.Two_captured_variables_in_different_lambdas(); - - AssertSql( - """ -@__starts_0_startswith='blog%' (Size = 255) -@__ends_1_endswith='%2' (Size = 255) + //Throws because the base startswith is uses a different case "Blog" vs "blog" and postgresql LIKE is case sensitive unlike SQL Server + //Base test fixed upstream for later versions + await Assert.ThrowsAsync(() => base.Two_captured_variables_in_different_lambdas()); -SELECT TOP 2 `b`.`Id`, `b`.`Name` -FROM `Blogs` AS `b` -WHERE (`b`.`Name` LIKE @__starts_0_startswith) AND (`b`.`Name` LIKE @__ends_1_endswith) -"""); +// AssertSql( +// """ +//@__starts_0_startswith='Blog%' +//@__ends_1_endswith='%2' +// +//SELECT b."Id", b."Name", b."Json" +//FROM "Blogs" AS b +//WHERE b."Name" LIKE @__starts_0_startswith AND b."Name" LIKE @__ends_1_endswith +//LIMIT 2 +//"""); } public override async Task Same_captured_variable_twice_in_same_lambda() @@ -2060,8 +1977,8 @@ public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder build builder = base.AddOptions(builder); // TODO: Figure out if there's a nice way to continue using the retrying strategy - var jetOptionsBuilder = new NpgsqlDbContextOptionsBuilder(builder); - jetOptionsBuilder.ExecutionStrategy(d => new NonRetryingExecutionStrategy(d)); + var npgsqlOptionsBuilder = new NpgsqlDbContextOptionsBuilder(builder); + npgsqlOptionsBuilder.ExecutionStrategy(d => new NonRetryingExecutionStrategy(d)); return builder; }