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/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..c64f357b1 --- /dev/null +++ b/test/EFCore.PG.FunctionalTests/Query/PrecompiledQueryNpgsqlTest.cs @@ -0,0 +1,2013 @@ +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 @__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_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."BlogId", u."Title" +FROM ( + SELECT p."Id", p."BlogId", p."Title" + FROM "Posts" AS p + WHERE p."Id" > 11 + UNION + SELECT p0."Id", p0."BlogId", p0."Title" + FROM "Posts" AS p0 + WHERE p0."Id" < 21 +) AS u +ORDER BY u."Id" NULLS FIRST +"""); + } + + 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."BlogId", u."Title" +FROM ( + SELECT p."Id", p."BlogId", p."Title" + FROM "Posts" AS p + WHERE p."Id" > 11 + UNION ALL + SELECT p0."Id", p0."BlogId", p0."Title" + FROM "Posts" AS p0 + WHERE p0."Id" < 21 +) AS u +ORDER BY u."Id" NULLS FIRST +"""); + } + + 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."BlogId", i."Title" +FROM ( + SELECT p."Id", p."BlogId", p."Title" + FROM "Posts" AS p + WHERE p."Id" > 11 + INTERSECT + SELECT p0."Id", p0."BlogId", p0."Title" + FROM "Posts" AS p0 + WHERE p0."Id" < 22 +) AS i +ORDER BY i."Id" NULLS FIRST +"""); + } + + 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."BlogId", e."Title" +FROM ( + SELECT p."Id", p."BlogId", p."Title" + FROM "Posts" AS p + WHERE p."Id" > 11 + EXCEPT + SELECT p0."Id", p0."BlogId", p0."Title" + FROM "Posts" AS p0 + WHERE p0."Id" > 21 +) AS e +ORDER BY e."Id" NULLS FIRST +"""); + } + + 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( + """ +@__ids_0={ '1', '2', '3' } (DbType = Object) + +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = ANY (@__ids_0) +"""); + } + + 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 +"""); + } + + #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( + """ +SELECT COALESCE(b."Name", '') || 'Foo' AS "Foo" +FROM "Blogs" AS b +"""); + } + + 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() + { + //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()); + +// 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() + { + 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 npgsqlOptionsBuilder = new NpgsqlDbContextOptionsBuilder(builder); + npgsqlOptionsBuilder.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..37958e5b0 --- /dev/null +++ b/test/EFCore.PG.FunctionalTests/Query/PrecompiledSqlPregenerationQueryNpgsqlTest.cs @@ -0,0 +1,246 @@ +using Microsoft.EntityFrameworkCore.Query.Internal; +using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; +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 + + 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 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 new file mode 100644 index 000000000..104096b41 --- /dev/null +++ b/test/EFCore.PG.FunctionalTests/TestUtilities/NpgsqlPrecompiledQueryTestHelpers.cs @@ -0,0 +1,16 @@ +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(typeof(NpgsqlConnection).Assembly.Location); + yield return MetadataReference.CreateFromFile(Assembly.GetExecutingAssembly().Location); + } +}