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);
+ }
+}