From 29695b65ff4bb8bb7590f791015cc2c119e62261 Mon Sep 17 00:00:00 2001 From: Brian Buvinghausen Date: Wed, 12 Aug 2026 06:55:19 -0400 Subject: [PATCH 1/4] Bump preview 7 to released 11.0.0-preview.7.26324.112 --- Directory.Packages.props | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 6f1fcac6a..5df2e4c85 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,8 +1,8 @@ - 11.0.0-preview.7.26324.112 - 11.0.0-preview.7.26324.112 - 11.0.0-preview.7.26324.112 + 11.0.0-preview.7.26381.103 + 11.0.0-preview.7.26381.103 + 11.0.0-preview.7.26381.103 10.0.3 4.0.0-pre.154 From 9e02d937b12d0d16f66f6dd81e209f94aea9f6f3 Mon Sep 17 00:00:00 2001 From: Brian Buvinghausen Date: Wed, 12 Aug 2026 08:03:45 -0400 Subject: [PATCH 2/4] Implement Seed30915 required by EF Core preview 7 specification tests The released preview 7 build (11.0.0-preview.7.26381.103) adds GroupBy + DefaultIfEmpty projection tests for dotnet/efcore#30915, with a new abstract Seed30915 member on AdHocMiscellaneousQueryRelationalTestBase. Implement it following the SQL Server provider's seeding --- .../Query/AdHocMiscellaneousQueryNpgsqlTest.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/test/EFCore.PG.FunctionalTests/Query/AdHocMiscellaneousQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/AdHocMiscellaneousQueryNpgsqlTest.cs index a5bfc2544..6ce52ec47 100644 --- a/test/EFCore.PG.FunctionalTests/Query/AdHocMiscellaneousQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/AdHocMiscellaneousQueryNpgsqlTest.cs @@ -25,6 +25,21 @@ protected override Task Seed2951(Context2951 context) INSERT INTO "ZeroKey" VALUES (NULL) """); + protected override async Task Seed30915(Context30915 context) + { + context.Statuses.AddRange( + new Context30915.PickupStatus30915 { PickupStatusId = 1, Name = "Active" }, + new Context30915.PickupStatus30915 { PickupStatusId = 2, Name = "NoRequests" }, + new Context30915.PickupStatus30915 { PickupStatusId = 3, Name = "Busy" }); + + context.Requests.AddRange( + new Context30915.PickupRequest30915 { PickupStatusId = 1, Priority = 5 }, + new Context30915.PickupRequest30915 { PickupStatusId = 1, Priority = null }, + new Context30915.PickupRequest30915 { PickupStatusId = 3, Priority = 7 }); + + await context.SaveChangesAsync(); + } + // Writes DateTime with Kind=Unspecified to timestamptz public override Task SelectMany_where_Select(bool async) => Task.CompletedTask; From 8412eb8ce1f7f37259a1590e51163f351dd63ac0 Mon Sep 17 00:00:00 2001 From: Brian Buvinghausen Date: Wed, 12 Aug 2026 08:30:52 -0400 Subject: [PATCH 3/4] Bump SDK to preview 7 and align new #30915 specification tests The preview 7 runtime is required at test time: Microsoft.Extensions.Primitives moved into the shared framework, so running preview 7 packages on the preview 6 runtime fails with MissingMethodException (ChangeToken.OnChange) as soon as the test configuration loads. Also override Correlated_SelectMany_DefaultIfEmpty_whole_object: the base asserts a translation failure for providers without APPLY support, but PostgreSQL supports LATERAL, so the query translates and materializes correctly. Mirrors the SQL Server override, with the Npgsql SQL baseline. --- global.json | 2 +- .../AdHocMiscellaneousQueryNpgsqlTest.cs | 50 +++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/global.json b/global.json index ad870e940..28189fd63 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "11.0.100-preview.6.26359.118", + "version": "11.0.100-preview.7.26381.103", "allowPrerelease": true }, "test": { diff --git a/test/EFCore.PG.FunctionalTests/Query/AdHocMiscellaneousQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/AdHocMiscellaneousQueryNpgsqlTest.cs index 6ce52ec47..b1a6feed7 100644 --- a/test/EFCore.PG.FunctionalTests/Query/AdHocMiscellaneousQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/AdHocMiscellaneousQueryNpgsqlTest.cs @@ -40,6 +40,56 @@ protected override async Task Seed30915(Context30915 context) await context.SaveChangesAsync(); } + // PROVIDER DIVERGENCE: PostgreSQL supports LATERAL, so unlike SQLite (which throws because it + // has no APPLY) the correlated whole-object DefaultIfEmpty actually translates and materializes + // correctly here. Override the base (SQLite-shaped) assert-throws with the real-results assertion. + public override async Task Correlated_SelectMany_DefaultIfEmpty_whole_object() + { + var contextFactory = await InitializeNonSharedTest(seed: Seed30915); + using var context = contextFactory.CreateDbContext(); + + var query = from s in context.Statuses + from countInfo in context.Requests + .Where(r => r.PickupStatusId == s.PickupStatusId) + .GroupBy(r => r.PickupStatusId, (k, els) => new { pickupStatusId = k, Count = els.Count() }) + .DefaultIfEmpty() + orderby s.PickupStatusId + select new { s.PickupStatusId, countInfo }; + + var result = await query.ToListAsync(); + + Assert.Equal(3, result.Count); + + // status 1 -> matched, Count 2 + Assert.Equal(1, result[0].PickupStatusId); + Assert.NotNull(result[0].countInfo); + Assert.Equal(1, result[0].countInfo.pickupStatusId); + Assert.Equal(2, result[0].countInfo.Count); + + // status 2 -> no match: whole non-entity object is null + Assert.Equal(2, result[1].PickupStatusId); + Assert.Null(result[1].countInfo); + + // status 3 -> matched, Count 1 + Assert.Equal(3, result[2].PickupStatusId); + Assert.NotNull(result[2].countInfo); + Assert.Equal(3, result[2].countInfo.pickupStatusId); + Assert.Equal(1, result[2].countInfo.Count); + + AssertSql( + """ +SELECT s."PickupStatusId", r0."pickupStatusId", r0."Count", r0.marker +FROM "Statuses" AS s +LEFT JOIN LATERAL ( + SELECT r."PickupStatusId" AS "pickupStatusId", count(*)::int AS "Count", 1 AS marker + FROM "Requests" AS r + WHERE r."PickupStatusId" = s."PickupStatusId" + GROUP BY r."PickupStatusId" +) AS r0 ON TRUE +ORDER BY s."PickupStatusId" NULLS FIRST +"""); + } + // Writes DateTime with Kind=Unspecified to timestamptz public override Task SelectMany_where_Select(bool async) => Task.CompletedTask; From 9b5c4571a63c8fcf87ac565c99203304432c9a54 Mon Sep 17 00:00:00 2001 From: Brian Buvinghausen Date: Wed, 12 Aug 2026 10:24:46 -0400 Subject: [PATCH 4/4] Fix provider-side test failures against EF Core preview 7 - Add NpgsqlParseTranslator: numeric Parse(string) -> CAST, mirroring SQL Server - Override NpgsqlArrayTypeMapping.GetDefaultProviderValue: native arrays get an array default, not the JSON "[]" string the base now assumes (dotnet/efcore#38796) - Add ~55 missing test overrides with PostgreSQL baselines; refresh 6 drifted ones - Override Join_local_*_closure tests (PG char-cast semantics; byte[] joins translate) - Reimplement owned-entity inconsistent-data test with quoted identifiers - Seed EntitiesWithPrimitiveCollection in the precompiled-query fixture Remaining failures are all dotnet/efcore#38796 (shaper assumes primitive collections are stored as JSON strings), pending an upstream fix. --- .../NpgsqlMethodCallTranslatorProvider.cs | 1 + .../Internal/NpgsqlParseTranslator.cs | 44 ++ .../Mapping/NpgsqlArrayTypeMapping.cs | 18 + .../Query/AdHocPrecompiledQueryNpgsqlTest.cs | 22 + .../ComplexJsonProjectionNpgsqlTest.cs | 33 ++ ...mplexTableSplittingProjectionNpgsqlTest.cs | 33 ++ .../NavigationsProjectionNpgsqlTest.cs | 54 ++ .../OwnedJsonProjectionNpgsqlTest.cs | 42 ++ .../OwnedNavigationsProjectionNpgsqlTest.cs | 63 +++ ...OwnedTableSplittingProjectionNpgsqlTest.cs | 54 ++ .../Query/ComplexTypeQueryNpgsqlTest.cs | 13 +- .../Query/EntitySplittingQueryNpgsqlTest.cs | 16 + .../TPCInheritanceQueryNpgsqlTest.cs | 74 +++ .../Query/JsonQueryNpgsqlTest.cs | 10 + .../Query/NorthwindGroupByQueryNpgsqlTest.cs | 482 +++++++++++++++++- .../Query/NorthwindJoinQueryNpgsqlTest.cs | 49 ++ .../Query/OwnedEntityQueryNpgsqlTest.cs | 50 ++ .../Query/PrecompiledQueryNpgsqlTest.cs | 42 ++ .../PrimitiveCollectionsQueryNpgsqlTest.cs | 15 + .../MathTranslationsNpgsqlTest.cs | 34 ++ .../MiscellaneousTranslationsNpgsqlTest.cs | 72 +++ ...ellaneousOperatorTranslationsNpgsqlTest.cs | 84 +++ 22 files changed, 1286 insertions(+), 19 deletions(-) create mode 100644 src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlParseTranslator.cs diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMethodCallTranslatorProvider.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMethodCallTranslatorProvider.cs index 7f448c540..e2573d4ae 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMethodCallTranslatorProvider.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMethodCallTranslatorProvider.cs @@ -57,6 +57,7 @@ public NpgsqlMethodCallTranslatorProvider( new NpgsqlNetworkTranslator(typeMappingSource, sqlExpressionFactory, model), new NpgsqlGuidTranslator(sqlExpressionFactory, npgsqlOptions.PostgresVersion), new NpgsqlObjectToStringTranslator(typeMappingSource, sqlExpressionFactory), + new NpgsqlParseTranslator(sqlExpressionFactory), new NpgsqlRandomTranslator(sqlExpressionFactory), new NpgsqlRangeTranslator(typeMappingSource, sqlExpressionFactory, model, supportsMultiranges), new NpgsqlRegexTranslator(typeMappingSource, sqlExpressionFactory, supportRegexCount), diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlParseTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlParseTranslator.cs new file mode 100644 index 000000000..5a30b1721 --- /dev/null +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlParseTranslator.cs @@ -0,0 +1,44 @@ +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Internal; + +/// +/// Translates single-argument numeric Parse methods (e.g. ) into PostgreSQL CAST +/// expressions. +/// +public class NpgsqlParseTranslator(ISqlExpressionFactory sqlExpressionFactory) : IMethodCallTranslator +{ + private static readonly Type[] SupportedClrTypes = + [ + typeof(bool), // boolean + typeof(byte), // smallint + typeof(decimal), // numeric + typeof(double), // double precision + typeof(float), // real + typeof(short), // smallint + typeof(int), // integer + typeof(long) // bigint + ]; + + private static readonly MethodInfo[] SupportedMethods + = SupportedClrTypes + .SelectMany( + t => t.GetTypeInfo().GetDeclaredMethods(nameof(int.Parse)) + .Where( + m => m.GetParameters().Length == 1 + && m.GetParameters().First().ParameterType == typeof(string))) + .ToArray(); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual SqlExpression? Translate( + SqlExpression? instance, + MethodInfo method, + IReadOnlyList arguments, + IDiagnosticsLogger logger) + => SupportedMethods.Contains(method) + ? sqlExpressionFactory.Convert(arguments[0], method.ReturnType) + : null; +} diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlArrayTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlArrayTypeMapping.cs index 749e3d3a0..2c368bb0c 100644 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlArrayTypeMapping.cs +++ b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlArrayTypeMapping.cs @@ -48,6 +48,24 @@ public override RelationalTypeMapping ElementTypeMapping return (RelationalTypeMapping)elementTypeMapping; } } + + /// + /// 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 override object? GetDefaultProviderValue() + { + // The base implementation assumes any mapping with an element type mapping and a JsonValueReaderWriter stores its + // collection as a JSON string in the column, and returns "[]"; PostgreSQL arrays are natively-typed columns, so + // return an actual collection default instead (see https://github.com/dotnet/efcore/issues/38796). + var providerType = (Converter?.ProviderClrType ?? ClrType).UnwrapNullableType(); + + return providerType.IsArray + ? Array.CreateInstance(providerType.GetElementType()!, 0) + : providerType.GetDefaultValue(); + } } /// diff --git a/test/EFCore.PG.FunctionalTests/Query/AdHocPrecompiledQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/AdHocPrecompiledQueryNpgsqlTest.cs index 71e8eb8ff..6eadc0037 100644 --- a/test/EFCore.PG.FunctionalTests/Query/AdHocPrecompiledQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/AdHocPrecompiledQueryNpgsqlTest.cs @@ -8,6 +8,28 @@ public class AdHocPrecompiledQueryNpgsqlTest(NonSharedFixture fixture, ITestOutp protected override bool AlwaysPrintGeneratedSources => false; + public override async Task Invalid_identifier_json_property_name() + { + await base.Invalid_identifier_json_property_name(); + + AssertSql( + """ +SELECT e."Id", e."Nested" +FROM "Entities" AS e +"""); + } + + public override async Task Invalid_identifier_shadow_property_name() + { + await base.Invalid_identifier_shadow_property_name(); + + AssertSql( + """ +SELECT e."Id", e."NOT VALID !!!1" +FROM "Entities" AS e +"""); + } + public override async Task Index_no_evaluatability() { await base.Index_no_evaluatability(); diff --git a/test/EFCore.PG.FunctionalTests/Query/Associations/ComplexJson/ComplexJsonProjectionNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/Associations/ComplexJson/ComplexJsonProjectionNpgsqlTest.cs index bfb85a798..a253caac8 100644 --- a/test/EFCore.PG.FunctionalTests/Query/Associations/ComplexJson/ComplexJsonProjectionNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/Associations/ComplexJson/ComplexJsonProjectionNpgsqlTest.cs @@ -6,6 +6,39 @@ namespace Microsoft.EntityFrameworkCore.Query.Associations.ComplexJson; public class ComplexJsonProjectionNpgsqlTest(ComplexJsonNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) : ComplexJsonProjectionRelationalTestBase(fixture, testOutputHelper) { + public override async Task Select_required_associate_duplicated(QueryTrackingBehavior queryTrackingBehavior) + { + await base.Select_required_associate_duplicated(queryTrackingBehavior); + + AssertSql( + """ +SELECT r."RequiredAssociate", r."RequiredAssociate" +FROM "RootEntity" AS r +"""); + } + + public override async Task Select_required_associate_and_optional_associate(QueryTrackingBehavior queryTrackingBehavior) + { + await base.Select_required_associate_and_optional_associate(queryTrackingBehavior); + + AssertSql( + """ +SELECT r."RequiredAssociate", r."OptionalAssociate" +FROM "RootEntity" AS r +"""); + } + + public override async Task Select_optional_associate_and_ints(QueryTrackingBehavior queryTrackingBehavior) + { + await base.Select_optional_associate_and_ints(queryTrackingBehavior); + + AssertSql( + """ +SELECT r."OptionalAssociate", r."RequiredAssociate" -> 'Ints' AS "Ints" +FROM "RootEntity" AS r +"""); + } + public override async Task Select_root(QueryTrackingBehavior queryTrackingBehavior) { await base.Select_root(queryTrackingBehavior); diff --git a/test/EFCore.PG.FunctionalTests/Query/Associations/ComplexTableSplitting/ComplexTableSplittingProjectionNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/Associations/ComplexTableSplitting/ComplexTableSplittingProjectionNpgsqlTest.cs index af2f20922..dec1c8415 100644 --- a/test/EFCore.PG.FunctionalTests/Query/Associations/ComplexTableSplitting/ComplexTableSplittingProjectionNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/Associations/ComplexTableSplitting/ComplexTableSplittingProjectionNpgsqlTest.cs @@ -8,6 +8,39 @@ public class ComplexTableSplittingProjectionNpgsqlTest( ITestOutputHelper testOutputHelper) : ComplexTableSplittingProjectionRelationalTestBase(fixture, testOutputHelper) { + public override async Task Select_required_associate_duplicated(QueryTrackingBehavior queryTrackingBehavior) + { + await base.Select_required_associate_duplicated(queryTrackingBehavior); + + AssertSql( + """ +SELECT r."RequiredAssociate_Id", r."RequiredAssociate_Int", r."RequiredAssociate_Ints", r."RequiredAssociate_Name", r."RequiredAssociate_String", r."RequiredAssociate_OptionalNestedAssociate_Id", r."RequiredAssociate_OptionalNestedAssociate_Int", r."RequiredAssociate_OptionalNestedAssociate_Ints", r."RequiredAssociate_OptionalNestedAssociate_Name", r."RequiredAssociate_OptionalNestedAssociate_String", r."RequiredAssociate_RequiredNestedAssociate_Id", r."RequiredAssociate_RequiredNestedAssociate_Int", r."RequiredAssociate_RequiredNestedAssociate_Ints", r."RequiredAssociate_RequiredNestedAssociate_Name", r."RequiredAssociate_RequiredNestedAssociate_String" +FROM "RootEntity" AS r +"""); + } + + public override async Task Select_required_associate_and_optional_associate(QueryTrackingBehavior queryTrackingBehavior) + { + await base.Select_required_associate_and_optional_associate(queryTrackingBehavior); + + AssertSql( + """ +SELECT r."RequiredAssociate_Id", r."RequiredAssociate_Int", r."RequiredAssociate_Ints", r."RequiredAssociate_Name", r."RequiredAssociate_String", r."RequiredAssociate_OptionalNestedAssociate_Id", r."RequiredAssociate_OptionalNestedAssociate_Int", r."RequiredAssociate_OptionalNestedAssociate_Ints", r."RequiredAssociate_OptionalNestedAssociate_Name", r."RequiredAssociate_OptionalNestedAssociate_String", r."RequiredAssociate_RequiredNestedAssociate_Id", r."RequiredAssociate_RequiredNestedAssociate_Int", r."RequiredAssociate_RequiredNestedAssociate_Ints", r."RequiredAssociate_RequiredNestedAssociate_Name", r."RequiredAssociate_RequiredNestedAssociate_String", r."OptionalAssociate_Id", r."OptionalAssociate_Int", r."OptionalAssociate_Ints", r."OptionalAssociate_Name", r."OptionalAssociate_String", r."OptionalAssociate_OptionalNestedAssociate_Id", r."OptionalAssociate_OptionalNestedAssociate_Int", r."OptionalAssociate_OptionalNestedAssociate_Ints", r."OptionalAssociate_OptionalNestedAssociate_Name", r."OptionalAssociate_OptionalNestedAssociate_String", r."OptionalAssociate_RequiredNestedAssociate_Id", r."OptionalAssociate_RequiredNestedAssociate_Int", r."OptionalAssociate_RequiredNestedAssociate_Ints", r."OptionalAssociate_RequiredNestedAssociate_Name", r."OptionalAssociate_RequiredNestedAssociate_String" +FROM "RootEntity" AS r +"""); + } + + public override async Task Select_optional_associate_and_ints(QueryTrackingBehavior queryTrackingBehavior) + { + await base.Select_optional_associate_and_ints(queryTrackingBehavior); + + AssertSql( + """ +SELECT r."OptionalAssociate_Id", r."OptionalAssociate_Int", r."OptionalAssociate_Ints", r."OptionalAssociate_Name", r."OptionalAssociate_String", r."OptionalAssociate_OptionalNestedAssociate_Id", r."OptionalAssociate_OptionalNestedAssociate_Int", r."OptionalAssociate_OptionalNestedAssociate_Ints", r."OptionalAssociate_OptionalNestedAssociate_Name", r."OptionalAssociate_OptionalNestedAssociate_String", r."OptionalAssociate_RequiredNestedAssociate_Id", r."OptionalAssociate_RequiredNestedAssociate_Int", r."OptionalAssociate_RequiredNestedAssociate_Ints", r."OptionalAssociate_RequiredNestedAssociate_Name", r."OptionalAssociate_RequiredNestedAssociate_String", r."RequiredAssociate_Ints" AS "Ints" +FROM "RootEntity" AS r +"""); + } + public override async Task Select_root(QueryTrackingBehavior queryTrackingBehavior) { await base.Select_root(queryTrackingBehavior); diff --git a/test/EFCore.PG.FunctionalTests/Query/Associations/Navigations/NavigationsProjectionNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/Associations/Navigations/NavigationsProjectionNpgsqlTest.cs index f9ad75ff7..3933327b8 100644 --- a/test/EFCore.PG.FunctionalTests/Query/Associations/Navigations/NavigationsProjectionNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/Associations/Navigations/NavigationsProjectionNpgsqlTest.cs @@ -6,6 +6,60 @@ namespace Microsoft.EntityFrameworkCore.Query.Associations.Navigations; public class NavigationsProjectionNpgsqlTest(NavigationsNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) : NavigationsProjectionRelationalTestBase(fixture, testOutputHelper) { + public override async Task Select_required_associate_duplicated(QueryTrackingBehavior queryTrackingBehavior) + { + await base.Select_required_associate_duplicated(queryTrackingBehavior); + + AssertSql( + """ +SELECT a."Id", a."CollectionRootId", a."Int", a."Ints", a."Name", a."OptionalNestedAssociateId", a."RequiredNestedAssociateId", a."String", r."Id", n1."Id", n1."CollectionAssociateId", n1."Int", n1."Ints", n1."Name", n1."String", n."Id", n."CollectionAssociateId", n."Int", n."Ints", n."Name", n."String", n0."Id", n0."CollectionAssociateId", n0."Int", n0."Ints", n0."Name", n0."String", n2."Id", n2."CollectionAssociateId", n2."Int", n2."Ints", n2."Name", n2."String" +FROM "RootEntity" AS r +INNER JOIN "AssociateType" AS a ON r."RequiredAssociateId" = a."Id" +LEFT JOIN "NestedAssociateType" AS n ON a."OptionalNestedAssociateId" = n."Id" +INNER JOIN "NestedAssociateType" AS n0 ON a."RequiredNestedAssociateId" = n0."Id" +LEFT JOIN "NestedAssociateType" AS n1 ON a."Id" = n1."CollectionAssociateId" +LEFT JOIN "NestedAssociateType" AS n2 ON a."Id" = n2."CollectionAssociateId" +ORDER BY r."Id" NULLS FIRST, n1."Id" NULLS FIRST +"""); + } + + public override async Task Select_required_associate_and_optional_associate(QueryTrackingBehavior queryTrackingBehavior) + { + await base.Select_required_associate_and_optional_associate(queryTrackingBehavior); + + AssertSql( + """ +SELECT a."Id", a."CollectionRootId", a."Int", a."Ints", a."Name", a."OptionalNestedAssociateId", a."RequiredNestedAssociateId", a."String", r."Id", n3."Id", n3."CollectionAssociateId", n3."Int", n3."Ints", n3."Name", n3."String", n."Id", n."CollectionAssociateId", n."Int", n."Ints", n."Name", n."String", n0."Id", n0."CollectionAssociateId", n0."Int", n0."Ints", n0."Name", n0."String", a0."Id", a0."CollectionRootId", a0."Int", a0."Ints", a0."Name", a0."OptionalNestedAssociateId", a0."RequiredNestedAssociateId", a0."String", n4."Id", n4."CollectionAssociateId", n4."Int", n4."Ints", n4."Name", n4."String", n1."Id", n1."CollectionAssociateId", n1."Int", n1."Ints", n1."Name", n1."String", n2."Id", n2."CollectionAssociateId", n2."Int", n2."Ints", n2."Name", n2."String" +FROM "RootEntity" AS r +INNER JOIN "AssociateType" AS a ON r."RequiredAssociateId" = a."Id" +LEFT JOIN "AssociateType" AS a0 ON r."OptionalAssociateId" = a0."Id" +LEFT JOIN "NestedAssociateType" AS n ON a."OptionalNestedAssociateId" = n."Id" +INNER JOIN "NestedAssociateType" AS n0 ON a."RequiredNestedAssociateId" = n0."Id" +LEFT JOIN "NestedAssociateType" AS n1 ON a0."OptionalNestedAssociateId" = n1."Id" +LEFT JOIN "NestedAssociateType" AS n2 ON a0."RequiredNestedAssociateId" = n2."Id" +LEFT JOIN "NestedAssociateType" AS n3 ON a."Id" = n3."CollectionAssociateId" +LEFT JOIN "NestedAssociateType" AS n4 ON a0."Id" = n4."CollectionAssociateId" +ORDER BY r."Id" NULLS FIRST, n3."Id" NULLS FIRST +"""); + } + + public override async Task Select_optional_associate_and_ints(QueryTrackingBehavior queryTrackingBehavior) + { + await base.Select_optional_associate_and_ints(queryTrackingBehavior); + + AssertSql( + """ +SELECT a."Id", a."CollectionRootId", a."Int", a."Ints", a."Name", a."OptionalNestedAssociateId", a."RequiredNestedAssociateId", a."String", r."Id", n1."Id", n1."CollectionAssociateId", n1."Int", n1."Ints", n1."Name", n1."String", n."Id", n."CollectionAssociateId", n."Int", n."Ints", n."Name", n."String", n0."Id", n0."CollectionAssociateId", n0."Int", n0."Ints", n0."Name", n0."String", a0."Ints" +FROM "RootEntity" AS r +LEFT JOIN "AssociateType" AS a ON r."OptionalAssociateId" = a."Id" +INNER JOIN "AssociateType" AS a0 ON r."RequiredAssociateId" = a0."Id" +LEFT JOIN "NestedAssociateType" AS n ON a."OptionalNestedAssociateId" = n."Id" +LEFT JOIN "NestedAssociateType" AS n0 ON a."RequiredNestedAssociateId" = n0."Id" +LEFT JOIN "NestedAssociateType" AS n1 ON a."Id" = n1."CollectionAssociateId" +ORDER BY r."Id" NULLS FIRST +"""); + } + public override async Task Select_root(QueryTrackingBehavior queryTrackingBehavior) { await base.Select_root(queryTrackingBehavior); diff --git a/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedJson/OwnedJsonProjectionNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedJson/OwnedJsonProjectionNpgsqlTest.cs index 3b5b31687..1650f1e46 100644 --- a/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedJson/OwnedJsonProjectionNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedJson/OwnedJsonProjectionNpgsqlTest.cs @@ -6,6 +6,48 @@ namespace Microsoft.EntityFrameworkCore.Query.Associations.OwnedJson; public class OwnedJsonProjectionNpgsqlTest(OwnedJsonNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) : OwnedJsonProjectionRelationalTestBase(fixture, testOutputHelper) { + public override async Task Select_required_associate_duplicated(QueryTrackingBehavior queryTrackingBehavior) + { + await base.Select_required_associate_duplicated(queryTrackingBehavior); + + if (queryTrackingBehavior is not QueryTrackingBehavior.TrackAll) + { + AssertSql( + """ +SELECT r."RequiredAssociate", r."Id", r."RequiredAssociate" +FROM "RootEntity" AS r +"""); + } + } + + public override async Task Select_required_associate_and_optional_associate(QueryTrackingBehavior queryTrackingBehavior) + { + await base.Select_required_associate_and_optional_associate(queryTrackingBehavior); + + if (queryTrackingBehavior is not QueryTrackingBehavior.TrackAll) + { + AssertSql( + """ +SELECT r."RequiredAssociate", r."Id", r."OptionalAssociate" +FROM "RootEntity" AS r +"""); + } + } + + public override async Task Select_optional_associate_and_ints(QueryTrackingBehavior queryTrackingBehavior) + { + await base.Select_optional_associate_and_ints(queryTrackingBehavior); + + if (queryTrackingBehavior is not QueryTrackingBehavior.TrackAll) + { + AssertSql( + """ +SELECT r."OptionalAssociate", r."Id", r."RequiredAssociate" -> 'Ints' +FROM "RootEntity" AS r +"""); + } + } + public override async Task Select_root(QueryTrackingBehavior queryTrackingBehavior) { await base.Select_root(queryTrackingBehavior); diff --git a/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsProjectionNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsProjectionNpgsqlTest.cs index 84e24d957..0629ad3f6 100644 --- a/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsProjectionNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsProjectionNpgsqlTest.cs @@ -6,6 +6,69 @@ namespace Microsoft.EntityFrameworkCore.Query.Associations.OwnedNavigations; public class OwnedNavigationsProjectionNpgsqlTest(OwnedNavigationsNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) : OwnedNavigationsProjectionRelationalTestBase(fixture, testOutputHelper) { + public override async Task Select_required_associate_duplicated(QueryTrackingBehavior queryTrackingBehavior) + { + await base.Select_required_associate_duplicated(queryTrackingBehavior); + + if (queryTrackingBehavior is not QueryTrackingBehavior.TrackAll) + { + AssertSql( + """ +SELECT r0."RootEntityId", r0."Id", r0."Int", r0."Ints", r0."Name", r0."String", r."Id", r1."AssociateTypeRootEntityId", r2."AssociateTypeRootEntityId", r3."AssociateTypeRootEntityId", r3."Id", r3."Int", r3."Ints", r3."Name", r3."String", r1."Id", r1."Int", r1."Ints", r1."Name", r1."String", r2."Id", r2."Int", r2."Ints", r2."Name", r2."String", r4."AssociateTypeRootEntityId", r4."Id", r4."Int", r4."Ints", r4."Name", r4."String" +FROM "RootEntity" AS r +LEFT JOIN "RequiredRelated" AS r0 ON r."Id" = r0."RootEntityId" +LEFT JOIN "RequiredRelated_OptionalNested" AS r1 ON r0."RootEntityId" = r1."AssociateTypeRootEntityId" +LEFT JOIN "RequiredRelated_RequiredNested" AS r2 ON r0."RootEntityId" = r2."AssociateTypeRootEntityId" +LEFT JOIN "RequiredRelated_NestedCollection" AS r3 ON r0."RootEntityId" = r3."AssociateTypeRootEntityId" +LEFT JOIN "RequiredRelated_NestedCollection" AS r4 ON r0."RootEntityId" = r4."AssociateTypeRootEntityId" +ORDER BY r."Id" NULLS FIRST, r0."RootEntityId" NULLS FIRST, r1."AssociateTypeRootEntityId" NULLS FIRST, r2."AssociateTypeRootEntityId" NULLS FIRST, r3."AssociateTypeRootEntityId" NULLS FIRST, r3."Id" NULLS FIRST, r4."AssociateTypeRootEntityId" NULLS FIRST +"""); + } + } + + public override async Task Select_required_associate_and_optional_associate(QueryTrackingBehavior queryTrackingBehavior) + { + await base.Select_required_associate_and_optional_associate(queryTrackingBehavior); + + if (queryTrackingBehavior is not QueryTrackingBehavior.TrackAll) + { + AssertSql( + """ +SELECT r0."RootEntityId", r0."Id", r0."Int", r0."Ints", r0."Name", r0."String", r."Id", r1."AssociateTypeRootEntityId", r2."AssociateTypeRootEntityId", o."RootEntityId", o0."AssociateTypeRootEntityId", o1."AssociateTypeRootEntityId", r3."AssociateTypeRootEntityId", r3."Id", r3."Int", r3."Ints", r3."Name", r3."String", r1."Id", r1."Int", r1."Ints", r1."Name", r1."String", r2."Id", r2."Int", r2."Ints", r2."Name", r2."String", o."Id", o."Int", o."Ints", o."Name", o."String", o2."AssociateTypeRootEntityId", o2."Id", o2."Int", o2."Ints", o2."Name", o2."String", o0."Id", o0."Int", o0."Ints", o0."Name", o0."String", o1."Id", o1."Int", o1."Ints", o1."Name", o1."String" +FROM "RootEntity" AS r +LEFT JOIN "RequiredRelated" AS r0 ON r."Id" = r0."RootEntityId" +LEFT JOIN "RequiredRelated_OptionalNested" AS r1 ON r0."RootEntityId" = r1."AssociateTypeRootEntityId" +LEFT JOIN "RequiredRelated_RequiredNested" AS r2 ON r0."RootEntityId" = r2."AssociateTypeRootEntityId" +LEFT JOIN "OptionalRelated" AS o ON r."Id" = o."RootEntityId" +LEFT JOIN "OptionalRelated_OptionalNested" AS o0 ON o."RootEntityId" = o0."AssociateTypeRootEntityId" +LEFT JOIN "OptionalRelated_RequiredNested" AS o1 ON o."RootEntityId" = o1."AssociateTypeRootEntityId" +LEFT JOIN "RequiredRelated_NestedCollection" AS r3 ON r0."RootEntityId" = r3."AssociateTypeRootEntityId" +LEFT JOIN "OptionalRelated_NestedCollection" AS o2 ON o."RootEntityId" = o2."AssociateTypeRootEntityId" +ORDER BY r."Id" NULLS FIRST, r0."RootEntityId" NULLS FIRST, r1."AssociateTypeRootEntityId" NULLS FIRST, r2."AssociateTypeRootEntityId" NULLS FIRST, o."RootEntityId" NULLS FIRST, o0."AssociateTypeRootEntityId" NULLS FIRST, o1."AssociateTypeRootEntityId" NULLS FIRST, r3."AssociateTypeRootEntityId" NULLS FIRST, r3."Id" NULLS FIRST, o2."AssociateTypeRootEntityId" NULLS FIRST +"""); + } + } + + public override async Task Select_optional_associate_and_ints(QueryTrackingBehavior queryTrackingBehavior) + { + await base.Select_optional_associate_and_ints(queryTrackingBehavior); + + if (queryTrackingBehavior is not QueryTrackingBehavior.TrackAll) + { + AssertSql( + """ +SELECT o."RootEntityId", o."Id", o."Int", o."Ints", o."Name", o."String", r."Id", o0."AssociateTypeRootEntityId", o1."AssociateTypeRootEntityId", r0."RootEntityId", o2."AssociateTypeRootEntityId", o2."Id", o2."Int", o2."Ints", o2."Name", o2."String", o0."Id", o0."Int", o0."Ints", o0."Name", o0."String", o1."Id", o1."Int", o1."Ints", o1."Name", o1."String", r0."Ints" +FROM "RootEntity" AS r +LEFT JOIN "OptionalRelated" AS o ON r."Id" = o."RootEntityId" +LEFT JOIN "OptionalRelated_OptionalNested" AS o0 ON o."RootEntityId" = o0."AssociateTypeRootEntityId" +LEFT JOIN "OptionalRelated_RequiredNested" AS o1 ON o."RootEntityId" = o1."AssociateTypeRootEntityId" +LEFT JOIN "RequiredRelated" AS r0 ON r."Id" = r0."RootEntityId" +LEFT JOIN "OptionalRelated_NestedCollection" AS o2 ON o."RootEntityId" = o2."AssociateTypeRootEntityId" +ORDER BY r."Id" NULLS FIRST, o."RootEntityId" NULLS FIRST, o0."AssociateTypeRootEntityId" NULLS FIRST, o1."AssociateTypeRootEntityId" NULLS FIRST, r0."RootEntityId" NULLS FIRST, o2."AssociateTypeRootEntityId" NULLS FIRST +"""); + } + } + public override async Task Select_root(QueryTrackingBehavior queryTrackingBehavior) { await base.Select_root(queryTrackingBehavior); diff --git a/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedTableSplitting/OwnedTableSplittingProjectionNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedTableSplitting/OwnedTableSplittingProjectionNpgsqlTest.cs index dd5d2daee..d57e78f92 100644 --- a/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedTableSplitting/OwnedTableSplittingProjectionNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedTableSplitting/OwnedTableSplittingProjectionNpgsqlTest.cs @@ -6,6 +6,60 @@ namespace Microsoft.EntityFrameworkCore.Query.Associations.OwnedTableSplitting; public class OwnedTableSplittingProjectionNpgsqlTest(OwnedTableSplittingNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) : OwnedTableSplittingProjectionRelationalTestBase(fixture, testOutputHelper) { + public override async Task Select_required_associate_duplicated(QueryTrackingBehavior queryTrackingBehavior) + { + await base.Select_required_associate_duplicated(queryTrackingBehavior); + + if (queryTrackingBehavior is not QueryTrackingBehavior.TrackAll) + { + AssertSql( + """ +SELECT r."Id", r."RequiredAssociate_Id", r."RequiredAssociate_Int", r."RequiredAssociate_Ints", r."RequiredAssociate_Name", r."RequiredAssociate_String", r0."AssociateTypeRootEntityId", r0."Id", r0."Int", r0."Ints", r0."Name", r0."String", r."RequiredAssociate_OptionalNestedAssociate_Id", r."RequiredAssociate_OptionalNestedAssociate_Int", r."RequiredAssociate_OptionalNestedAssociate_Ints", r."RequiredAssociate_OptionalNestedAssociate_Name", r."RequiredAssociate_OptionalNestedAssociate_String", r."RequiredAssociate_RequiredNestedAssociate_Id", r."RequiredAssociate_RequiredNestedAssociate_Int", r."RequiredAssociate_RequiredNestedAssociate_Ints", r."RequiredAssociate_RequiredNestedAssociate_Name", r."RequiredAssociate_RequiredNestedAssociate_String", r1."AssociateTypeRootEntityId", r1."Id", r1."Int", r1."Ints", r1."Name", r1."String" +FROM "RootEntity" AS r +LEFT JOIN "RequiredRelated_NestedCollection" AS r0 ON r."Id" = r0."AssociateTypeRootEntityId" +LEFT JOIN "RequiredRelated_NestedCollection" AS r1 ON r."Id" = r1."AssociateTypeRootEntityId" +ORDER BY r."Id" NULLS FIRST, r0."AssociateTypeRootEntityId" NULLS FIRST, r0."Id" NULLS FIRST, r1."AssociateTypeRootEntityId" NULLS FIRST +"""); + } + } + + public override async Task Select_required_associate_and_optional_associate(QueryTrackingBehavior queryTrackingBehavior) + { + await base.Select_required_associate_and_optional_associate(queryTrackingBehavior); + + if (queryTrackingBehavior is not QueryTrackingBehavior.TrackAll) + { + AssertSql( + """ +SELECT r."Id", r."RequiredAssociate_Id", r."RequiredAssociate_Int", r."RequiredAssociate_Ints", r."RequiredAssociate_Name", r."RequiredAssociate_String", r0."AssociateTypeRootEntityId", r0."Id", r0."Int", r0."Ints", r0."Name", r0."String", r."RequiredAssociate_OptionalNestedAssociate_Id", r."RequiredAssociate_OptionalNestedAssociate_Int", r."RequiredAssociate_OptionalNestedAssociate_Ints", r."RequiredAssociate_OptionalNestedAssociate_Name", r."RequiredAssociate_OptionalNestedAssociate_String", r."RequiredAssociate_RequiredNestedAssociate_Id", r."RequiredAssociate_RequiredNestedAssociate_Int", r."RequiredAssociate_RequiredNestedAssociate_Ints", r."RequiredAssociate_RequiredNestedAssociate_Name", r."RequiredAssociate_RequiredNestedAssociate_String", r."OptionalAssociate_Id", r."OptionalAssociate_Int", r."OptionalAssociate_Ints", r."OptionalAssociate_Name", r."OptionalAssociate_String", o."AssociateTypeRootEntityId", o."Id", o."Int", o."Ints", o."Name", o."String", r."OptionalAssociate_OptionalNestedAssociate_Id", r."OptionalAssociate_OptionalNestedAssociate_Int", r."OptionalAssociate_OptionalNestedAssociate_Ints", r."OptionalAssociate_OptionalNestedAssociate_Name", r."OptionalAssociate_OptionalNestedAssociate_String", r."OptionalAssociate_RequiredNestedAssociate_Id", r."OptionalAssociate_RequiredNestedAssociate_Int", r."OptionalAssociate_RequiredNestedAssociate_Ints", r."OptionalAssociate_RequiredNestedAssociate_Name", r."OptionalAssociate_RequiredNestedAssociate_String" +FROM "RootEntity" AS r +LEFT JOIN "RequiredRelated_NestedCollection" AS r0 ON r."Id" = r0."AssociateTypeRootEntityId" +LEFT JOIN "OptionalRelated_NestedCollection" AS o ON CASE + WHEN r."OptionalAssociate_Id" IS NOT NULL AND r."OptionalAssociate_Int" IS NOT NULL AND r."OptionalAssociate_Ints" IS NOT NULL AND r."OptionalAssociate_Name" IS NOT NULL AND r."OptionalAssociate_String" IS NOT NULL THEN r."Id" +END = o."AssociateTypeRootEntityId" +ORDER BY r."Id" NULLS FIRST, r0."AssociateTypeRootEntityId" NULLS FIRST, r0."Id" NULLS FIRST, o."AssociateTypeRootEntityId" NULLS FIRST +"""); + } + } + + public override async Task Select_optional_associate_and_ints(QueryTrackingBehavior queryTrackingBehavior) + { + await base.Select_optional_associate_and_ints(queryTrackingBehavior); + + if (queryTrackingBehavior is not QueryTrackingBehavior.TrackAll) + { + AssertSql( + """ +SELECT r."Id", r."OptionalAssociate_Id", r."OptionalAssociate_Int", r."OptionalAssociate_Ints", r."OptionalAssociate_Name", r."OptionalAssociate_String", o."AssociateTypeRootEntityId", o."Id", o."Int", o."Ints", o."Name", o."String", r."OptionalAssociate_OptionalNestedAssociate_Id", r."OptionalAssociate_OptionalNestedAssociate_Int", r."OptionalAssociate_OptionalNestedAssociate_Ints", r."OptionalAssociate_OptionalNestedAssociate_Name", r."OptionalAssociate_OptionalNestedAssociate_String", r."OptionalAssociate_RequiredNestedAssociate_Id", r."OptionalAssociate_RequiredNestedAssociate_Int", r."OptionalAssociate_RequiredNestedAssociate_Ints", r."OptionalAssociate_RequiredNestedAssociate_Name", r."OptionalAssociate_RequiredNestedAssociate_String", r."RequiredAssociate_Ints" +FROM "RootEntity" AS r +LEFT JOIN "OptionalRelated_NestedCollection" AS o ON CASE + WHEN r."OptionalAssociate_Id" IS NOT NULL AND r."OptionalAssociate_Int" IS NOT NULL AND r."OptionalAssociate_Ints" IS NOT NULL AND r."OptionalAssociate_Name" IS NOT NULL AND r."OptionalAssociate_String" IS NOT NULL THEN r."Id" +END = o."AssociateTypeRootEntityId" +ORDER BY r."Id" NULLS FIRST, o."AssociateTypeRootEntityId" NULLS FIRST +"""); + } + } + public override async Task Select_root(QueryTrackingBehavior queryTrackingBehavior) { await base.Select_root(queryTrackingBehavior); diff --git a/test/EFCore.PG.FunctionalTests/Query/ComplexTypeQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/ComplexTypeQueryNpgsqlTest.cs index b4465ca72..4a42f262e 100644 --- a/test/EFCore.PG.FunctionalTests/Query/ComplexTypeQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/ComplexTypeQueryNpgsqlTest.cs @@ -1004,7 +1004,18 @@ public override async Task Same_complex_type_projected_twice_with_pushdown_as_pa { await base.Same_complex_type_projected_twice_with_pushdown_as_part_of_another_projection(async); - AssertSql(""); + AssertSql( + """ +SELECT c."Id", s."BillingAddress_AddressLine1", s."BillingAddress_AddressLine2", s."BillingAddress_Tags", s."BillingAddress_ZipCode", s."BillingAddress_Country_Code", s."BillingAddress_Country_FullName", s."BillingAddress_AddressLine10", s."BillingAddress_AddressLine20", s."BillingAddress_Tags0", s."BillingAddress_ZipCode0", s."BillingAddress_Country_Code0", s."BillingAddress_Country_FullName0", s.c +FROM "Customer" AS c +LEFT JOIN LATERAL ( + SELECT c0."BillingAddress_AddressLine1", c0."BillingAddress_AddressLine2", c0."BillingAddress_Tags", c0."BillingAddress_ZipCode", c0."BillingAddress_Country_Code", c0."BillingAddress_Country_FullName", c1."BillingAddress_AddressLine1" AS "BillingAddress_AddressLine10", c1."BillingAddress_AddressLine2" AS "BillingAddress_AddressLine20", c1."BillingAddress_Tags" AS "BillingAddress_Tags0", c1."BillingAddress_ZipCode" AS "BillingAddress_ZipCode0", c1."BillingAddress_Country_Code" AS "BillingAddress_Country_Code0", c1."BillingAddress_Country_FullName" AS "BillingAddress_Country_FullName0", 1 AS c + FROM "Customer" AS c0 + CROSS JOIN "Customer" AS c1 + ORDER BY c0."Id" NULLS FIRST, c1."Id" DESC NULLS LAST + LIMIT 1 +) AS s ON TRUE +"""); } #region GroupBy diff --git a/test/EFCore.PG.FunctionalTests/Query/EntitySplittingQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/EntitySplittingQueryNpgsqlTest.cs index 9b140d844..f8ab3af33 100644 --- a/test/EFCore.PG.FunctionalTests/Query/EntitySplittingQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/EntitySplittingQueryNpgsqlTest.cs @@ -4,6 +4,22 @@ public class EntitySplittingQueryNpgsqlTest(NonSharedFixture fixture) : EntitySplittingQueryTestBase(fixture) { + public override async Task FromSql_on_split_entity_with_renamed_columns_uses_default_mappings(bool async) + { + await base.FromSql_on_split_entity_with_renamed_columns_uses_default_mappings(async); + + AssertSql( + """ +SELECT m."Id", m."EntityThreeId", m."IntValue1", m."IntValue2", m."IntValue3", m."IntValue4", m."StringValue1", m."StringValue2", m."StringValue3", m."StringValue4" +FROM ( + SELECT "m".*, "s"."CustomStringValue3" AS "StringValue3", "s"."StringValue4", "s"."CustomIntValue3" AS "IntValue3", "s"."IntValue4" + FROM "EntityOne" AS "m" + INNER JOIN "SplitEntityOnePart" AS "s" ON "m"."Id" = "s"."Id" +) AS m +ORDER BY m."Id" NULLS FIRST +"""); + } + public override async Task Compare_split_entity_to_null(bool async) { await base.Compare_split_entity_to_null(async); diff --git a/test/EFCore.PG.FunctionalTests/Query/Inheritance/TPCInheritanceQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/Inheritance/TPCInheritanceQueryNpgsqlTest.cs index 22929ce40..b085f3412 100644 --- a/test/EFCore.PG.FunctionalTests/Query/Inheritance/TPCInheritanceQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/Inheritance/TPCInheritanceQueryNpgsqlTest.cs @@ -3,6 +3,80 @@ namespace Microsoft.EntityFrameworkCore.Query.Inheritance; public class TPCInheritanceQueryNpgsqlTest(TPCInheritanceQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) : TPCInheritanceQueryTestBase(fixture, testOutputHelper) { + public override async Task Can_insert_update_delete() + { + await base.Can_insert_update_delete(); + + AssertSql( + """ +SELECT u."Id", u."CountryId", u."Name", u."Species", u."EagleId", u."IsFlightless", u."Group", u."FoundOn", u."Discriminator" +FROM ( + SELECT e."Id", e."CountryId", e."Name", e."Species", e."EagleId", e."IsFlightless", e."Group", NULL AS "FoundOn", 'Eagle' AS "Discriminator" + FROM "Eagle" AS e + UNION ALL + SELECT k."Id", k."CountryId", k."Name", k."Species", k."EagleId", k."IsFlightless", NULL AS "Group", k."FoundOn", 'Kiwi' AS "Discriminator" + FROM "Kiwi" AS k +) AS u +WHERE u."Species" = 'Aquila chrysaetos canadensis' +LIMIT 2 +""", + // + """ +SELECT c."Id", c."Name" +FROM "Countries" AS c +WHERE c."Id" = 1 +LIMIT 2 +""", + // + """ +@p0='1' +@p1=NULL (DbType = Int32) +@p2='0' +@p3='True' +@p4='Little spotted kiwi' +@p5='Apteryx owenii' + +INSERT INTO "Kiwi" ("CountryId", "EagleId", "FoundOn", "IsFlightless", "Name", "Species") +VALUES (@p0, @p1, @p2, @p3, @p4, @p5) +RETURNING "Id"; +""", + // + """ +SELECT k."Id", k."CountryId", k."Name", k."Species", k."EagleId", k."IsFlightless", k."FoundOn" +FROM "Kiwi" AS k +WHERE k."Species" LIKE '%owenii' +LIMIT 2 +""", + // + """ +@p1='3' +@p0='2' (Nullable = true) + +UPDATE "Kiwi" SET "EagleId" = @p0 +WHERE "Id" = @p1; +""", + // + """ +SELECT k."Id", k."CountryId", k."Name", k."Species", k."EagleId", k."IsFlightless", k."FoundOn" +FROM "Kiwi" AS k +WHERE k."Species" LIKE '%owenii' +LIMIT 2 +""", + // + """ +@p0='3' + +DELETE FROM "Kiwi" +WHERE "Id" = @p0; +""", + // + """ +SELECT count(*)::int +FROM "Kiwi" AS k +WHERE k."Species" LIKE '%owenii' +"""); + } + public override async Task Byte_enum_value_constant_used_in_projection(bool async) { await base.Byte_enum_value_constant_used_in_projection(async); diff --git a/test/EFCore.PG.FunctionalTests/Query/JsonQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/JsonQueryNpgsqlTest.cs index 5be988cca..577c74425 100644 --- a/test/EFCore.PG.FunctionalTests/Query/JsonQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/JsonQueryNpgsqlTest.cs @@ -11,6 +11,16 @@ public JsonQueryNpgsqlTest(JsonQueryNpgsqlFixture fixture, ITestOutputHelper tes Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); } + public override async Task Entity_including_collection_with_json_AsNoTrackingWithIdentityResolution(bool async) + { + await base.Entity_including_collection_with_json_AsNoTrackingWithIdentityResolution(async); + } + + public override async Task Entity_including_collection_with_json_and_separate_json_projection_AsNoTrackingWithIdentityResolution(bool async) + { + await base.Entity_including_collection_with_json_and_separate_json_projection_AsNoTrackingWithIdentityResolution(async); + } + public override async Task Basic_json_projection_owner_entity_NoTrackingWithIdentityResolution(bool async) { await base.Basic_json_projection_owner_entity_NoTrackingWithIdentityResolution(async); diff --git a/test/EFCore.PG.FunctionalTests/Query/NorthwindGroupByQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/NorthwindGroupByQueryNpgsqlTest.cs index 0dc0bff9d..08c4f6f23 100644 --- a/test/EFCore.PG.FunctionalTests/Query/NorthwindGroupByQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/NorthwindGroupByQueryNpgsqlTest.cs @@ -20,6 +20,464 @@ public virtual void Check_all_tests_overridden() #region GroupByProperty + public override async Task GroupBy_multiple_aggregates_sharing_same_navigation(bool async) + { + await base.GroupBy_multiple_aggregates_sharing_same_navigation(async); + + AssertSql( + """ +SELECT o."EmployeeID" AS "Key", COALESCE(sum(CASE + WHEN c."City" = 'London' THEN 1 + ELSE 0 +END), 0)::int AS "Londons", COALESCE(sum(CASE + WHEN c."City" = 'Berlin' THEN 1 + ELSE 0 +END), 0)::int AS "Berlins", COALESCE(sum(o."OrderID"), 0)::int AS "Total", count(*)::int AS "Count" +FROM "Orders" AS o +LEFT JOIN "Customers" AS c ON o."CustomerID" = c."CustomerID" +GROUP BY o."EmployeeID" +"""); + } + + public override async Task GroupBy_aggregate_through_two_level_navigation(bool async) + { + await base.GroupBy_aggregate_through_two_level_navigation(async); + + AssertSql( + """ +SELECT o."ProductID" AS "Key", COALESCE(sum(CASE + WHEN c."City" = 'London' THEN 1 + ELSE 0 +END), 0)::int AS "Londons" +FROM "Order Details" AS o +INNER JOIN "Orders" AS o0 ON o."OrderID" = o0."OrderID" +LEFT JOIN "Customers" AS c ON o0."CustomerID" = c."CustomerID" +GROUP BY o."ProductID" +"""); + } + + public override async Task GroupBy_Count_with_predicate_through_navigation_property(bool async) + { + await base.GroupBy_Count_with_predicate_through_navigation_property(async); + + AssertSql( + """ +SELECT o."EmployeeID" AS "Key", count(*) FILTER (WHERE c."City" = 'London')::int AS "Londons" +FROM "Orders" AS o +LEFT JOIN "Customers" AS c ON o."CustomerID" = c."CustomerID" +GROUP BY o."EmployeeID" +"""); + } + + public override async Task GroupBy_key_and_aggregate_through_same_navigation(bool async) + { + await base.GroupBy_key_and_aggregate_through_same_navigation(async); + + AssertSql( + """ +SELECT c."City" AS "Key", count(*) FILTER (WHERE c."City" = 'London')::int AS "Londons" +FROM "Orders" AS o +LEFT JOIN "Customers" AS c ON o."CustomerID" = c."CustomerID" +GROUP BY c."City" +"""); + } + + public override async Task GroupBy_aggregate_through_navigation_in_intermediate_projection(bool async) + { + await base.GroupBy_aggregate_through_navigation_in_intermediate_projection(async); + + AssertSql( + """ +SELECT o."EmployeeID" AS "Key", COALESCE(sum(CASE + WHEN c."City" = 'London' THEN 1 + ELSE 0 +END), 0)::int AS "Londons" +FROM "Orders" AS o +LEFT JOIN "Customers" AS c ON o."CustomerID" = c."CustomerID" +GROUP BY o."EmployeeID" +"""); + } + + public override async Task GroupBy_ValueTuple_projection_joined_on_tuple_member(bool async) + { + await base.GroupBy_ValueTuple_projection_joined_on_tuple_member(async); + + AssertSql( + """ +SELECT c."CustomerID", o0.c AS "Count" +FROM ( + SELECT o."CustomerID", count(*)::int AS c + FROM "Orders" AS o + GROUP BY o."CustomerID" +) AS o0 +INNER JOIN "Customers" AS c ON o0."CustomerID" = c."CustomerID" +"""); + } + + public override async Task GroupBy_Select_Entire_Entity_Where(bool async) + { + await base.GroupBy_Select_Entire_Entity_Where(async); + + AssertSql( + """ +SELECT o4."OrderID", o4."CustomerID", o4."EmployeeID", o4."OrderDate" +FROM ( + SELECT o."CustomerID" + FROM "Orders" AS o + GROUP BY o."CustomerID" + HAVING ( + SELECT o1."EmployeeID" + FROM "Orders" AS o1 + WHERE o."CustomerID" = o1."CustomerID" OR (o."CustomerID" IS NULL AND o1."CustomerID" IS NULL) + LIMIT 1) = 6 +) AS o2 +LEFT JOIN ( + SELECT o3."OrderID", o3."CustomerID", o3."EmployeeID", o3."OrderDate" + FROM ( + SELECT o0."OrderID", o0."CustomerID", o0."EmployeeID", o0."OrderDate", ROW_NUMBER() OVER(PARTITION BY o0."CustomerID" ORDER BY o0."OrderID" NULLS FIRST) AS row + FROM "Orders" AS o0 + ) AS o3 + WHERE o3.row <= 1 +) AS o4 ON o2."CustomerID" = o4."CustomerID" +"""); + } + + public override async Task GroupBy_Select_Entire_Entity_Where_Select(bool async) + { + await base.GroupBy_Select_Entire_Entity_Where_Select(async); + + AssertSql( + """ +SELECT ( + SELECT o1."EmployeeID" + FROM "Orders" AS o1 + WHERE o."OrderID" = o1."OrderID" + LIMIT 1) +FROM "Orders" AS o +GROUP BY o."OrderID" +HAVING ( + SELECT o0."OrderID" + FROM "Orders" AS o0 + WHERE o."OrderID" = o0."OrderID" + LIMIT 1) > 10 +"""); + } + + public override async Task GroupBy_Select_Entire_Entity_Select(bool async) + { + await base.GroupBy_Select_Entire_Entity_Select(async); + + AssertSql( + """ +SELECT ( + SELECT o0."EmployeeID" + FROM "Orders" AS o0 + WHERE o."OrderID" = o0."OrderID" + LIMIT 1) +FROM "Orders" AS o +GROUP BY o."OrderID" +"""); + } + + public override async Task GroupBy_Select_Entire_Entity_Order(bool async) + { + await base.GroupBy_Select_Entire_Entity_Order(async); + + AssertSql( + """ +SELECT o5."OrderID", o5."CustomerID", o5."EmployeeID", o5."OrderDate" +FROM ( + SELECT o."CustomerID", ( + SELECT o1."EmployeeID" + FROM "Orders" AS o1 + WHERE o."CustomerID" = o1."CustomerID" OR (o."CustomerID" IS NULL AND o1."CustomerID" IS NULL) + LIMIT 1) AS c, ( + SELECT o2."OrderID" + FROM "Orders" AS o2 + WHERE o."CustomerID" = o2."CustomerID" OR (o."CustomerID" IS NULL AND o2."CustomerID" IS NULL) + LIMIT 1) AS c0 + FROM "Orders" AS o + GROUP BY o."CustomerID" +) AS o3 +LEFT JOIN ( + SELECT o4."OrderID", o4."CustomerID", o4."EmployeeID", o4."OrderDate" + FROM ( + SELECT o0."OrderID", o0."CustomerID", o0."EmployeeID", o0."OrderDate", ROW_NUMBER() OVER(PARTITION BY o0."CustomerID" ORDER BY o0."OrderID" NULLS FIRST) AS row + FROM "Orders" AS o0 + ) AS o4 + WHERE o4.row <= 1 +) AS o5 ON o3."CustomerID" = o5."CustomerID" +ORDER BY o3.c NULLS FIRST, o3.c0 NULLS FIRST +"""); + } + + public override async Task GroupBy_Select_Anonymous_Type_With_Entire_Entity(bool async) + { + await base.GroupBy_Select_Anonymous_Type_With_Entire_Entity(async); + + AssertSql( + """ +SELECT o2."CustomerID", o4."OrderID", o4."CustomerID", o4."EmployeeID", o4."OrderDate" +FROM ( + SELECT o."CustomerID" + FROM "Orders" AS o + GROUP BY o."CustomerID" + HAVING ( + SELECT o1."OrderID" + FROM "Orders" AS o1 + WHERE o."CustomerID" = o1."CustomerID" OR (o."CustomerID" IS NULL AND o1."CustomerID" IS NULL) + ORDER BY o1."OrderDate" DESC NULLS LAST + LIMIT 1) IS NOT NULL +) AS o2 +LEFT JOIN ( + SELECT o3."OrderID", o3."CustomerID", o3."EmployeeID", o3."OrderDate" + FROM ( + SELECT o0."OrderID", o0."CustomerID", o0."EmployeeID", o0."OrderDate", ROW_NUMBER() OVER(PARTITION BY o0."CustomerID" ORDER BY o0."OrderDate" DESC NULLS LAST) AS row + FROM "Orders" AS o0 + ) AS o3 + WHERE o3.row <= 1 +) AS o4 ON o2."CustomerID" = o4."CustomerID" +"""); + } + + public override async Task GroupBy_Select_Entire_Entity_FirstOrDefault_Where(bool async) + { + await base.GroupBy_Select_Entire_Entity_FirstOrDefault_Where(async); + + AssertSql( + """ +SELECT o4."OrderID", o4."CustomerID", o4."EmployeeID", o4."OrderDate" +FROM ( + SELECT o."CustomerID" + FROM "Orders" AS o + GROUP BY o."CustomerID" + HAVING ( + SELECT o1."EmployeeID" + FROM "Orders" AS o1 + WHERE o."CustomerID" = o1."CustomerID" OR (o."CustomerID" IS NULL AND o1."CustomerID" IS NULL) + ORDER BY o1."OrderDate" DESC NULLS LAST + LIMIT 1) = 5 +) AS o2 +LEFT JOIN ( + SELECT o3."OrderID", o3."CustomerID", o3."EmployeeID", o3."OrderDate" + FROM ( + SELECT o0."OrderID", o0."CustomerID", o0."EmployeeID", o0."OrderDate", ROW_NUMBER() OVER(PARTITION BY o0."CustomerID" ORDER BY o0."OrderDate" DESC NULLS LAST) AS row + FROM "Orders" AS o0 + ) AS o3 + WHERE o3.row <= 1 +) AS o4 ON o2."CustomerID" = o4."CustomerID" +"""); + } + + public override async Task GroupBy_ResultSelector_Entire_Entity_Where(bool async) + { + await base.GroupBy_ResultSelector_Entire_Entity_Where(async); + + AssertSql( + """ +SELECT o4."OrderID", o4."CustomerID", o4."EmployeeID", o4."OrderDate" +FROM ( + SELECT o."CustomerID" + FROM "Orders" AS o + GROUP BY o."CustomerID" + HAVING ( + SELECT o1."EmployeeID" + FROM "Orders" AS o1 + WHERE o."CustomerID" = o1."CustomerID" OR (o."CustomerID" IS NULL AND o1."CustomerID" IS NULL) + ORDER BY o1."OrderDate" DESC NULLS LAST + LIMIT 1) = 6 +) AS o2 +LEFT JOIN ( + SELECT o3."OrderID", o3."CustomerID", o3."EmployeeID", o3."OrderDate" + FROM ( + SELECT o0."OrderID", o0."CustomerID", o0."EmployeeID", o0."OrderDate", ROW_NUMBER() OVER(PARTITION BY o0."CustomerID" ORDER BY o0."OrderDate" DESC NULLS LAST) AS row + FROM "Orders" AS o0 + ) AS o3 + WHERE o3.row <= 1 +) AS o4 ON o2."CustomerID" = o4."CustomerID" +"""); + } + + public override async Task GroupBy_Select_Entire_Entity_GroupBy(bool async) + { + await base.GroupBy_Select_Entire_Entity_GroupBy(async); + + AssertSql( + """ +SELECT o2."Key", count(*)::int AS "Count" +FROM ( + SELECT ( + SELECT o1."EmployeeID" + FROM "Orders" AS o1 + WHERE (o0."CustomerID" = o1."CustomerID" OR (o0."CustomerID" IS NULL AND o1."CustomerID" IS NULL)) AND (o0."EmployeeID" = o1."EmployeeID" OR (o0."EmployeeID" IS NULL AND o1."EmployeeID" IS NULL)) + LIMIT 1) AS "Key" + FROM ( + SELECT o."CustomerID", o."EmployeeID" + FROM "Orders" AS o + GROUP BY o."CustomerID", o."EmployeeID" + ) AS o0 +) AS o2 +GROUP BY o2."Key" +"""); + } + + public override async Task GroupBy_Select_Entire_Entity_composite_key_Select(bool async) + { + await base.GroupBy_Select_Entire_Entity_composite_key_Select(async); + + AssertSql( + """ +SELECT ( + SELECT o0."OrderID" + FROM "Orders" AS o0 + WHERE (o."CustomerID" = o0."CustomerID" OR (o."CustomerID" IS NULL AND o0."CustomerID" IS NULL)) AND (o."EmployeeID" = o0."EmployeeID" OR (o."EmployeeID" IS NULL AND o0."EmployeeID" IS NULL)) + LIMIT 1) +FROM "Orders" AS o +GROUP BY o."CustomerID", o."EmployeeID" +"""); + } + + public override async Task GroupBy_Select_Entire_Entity_OrderBy_navigation(bool async) + { + await base.GroupBy_Select_Entire_Entity_OrderBy_navigation(async); + + AssertSql( + """ +SELECT o4."OrderID", o4."CustomerID", o4."EmployeeID", o4."OrderDate" +FROM ( + SELECT o."CustomerID" + FROM "Orders" AS o + GROUP BY o."CustomerID" +) AS o2 +LEFT JOIN "Customers" AS c ON ( + SELECT o1."CustomerID" + FROM "Orders" AS o1 + WHERE o2."CustomerID" = o1."CustomerID" OR (o2."CustomerID" IS NULL AND o1."CustomerID" IS NULL) + ORDER BY o1."OrderID" NULLS FIRST + LIMIT 1) = c."CustomerID" +LEFT JOIN ( + SELECT o3."OrderID", o3."CustomerID", o3."EmployeeID", o3."OrderDate" + FROM ( + SELECT o0."OrderID", o0."CustomerID", o0."EmployeeID", o0."OrderDate", ROW_NUMBER() OVER(PARTITION BY o0."CustomerID" ORDER BY o0."OrderID" NULLS FIRST) AS row + FROM "Orders" AS o0 + ) AS o3 + WHERE o3.row <= 1 +) AS o4 ON o2."CustomerID" = o4."CustomerID" +ORDER BY c."City" NULLS FIRST, o4."OrderID" NULLS FIRST +"""); + } + + public override async Task GroupBy_Select_Entire_Entity_Select_navigation_member(bool async) + { + await base.GroupBy_Select_Entire_Entity_Select_navigation_member(async); + + AssertSql( + """ +SELECT o4."OrderID", c."City" +FROM ( + SELECT o."CustomerID" + FROM "Orders" AS o + GROUP BY o."CustomerID" +) AS o2 +LEFT JOIN "Customers" AS c ON ( + SELECT o1."CustomerID" + FROM "Orders" AS o1 + WHERE o2."CustomerID" = o1."CustomerID" OR (o2."CustomerID" IS NULL AND o1."CustomerID" IS NULL) + ORDER BY o1."OrderID" NULLS FIRST + LIMIT 1) = c."CustomerID" +LEFT JOIN ( + SELECT o3."OrderID", o3."CustomerID" + FROM ( + SELECT o0."OrderID", o0."CustomerID", ROW_NUMBER() OVER(PARTITION BY o0."CustomerID" ORDER BY o0."OrderID" NULLS FIRST) AS row + FROM "Orders" AS o0 + ) AS o3 + WHERE o3.row <= 1 +) AS o4 ON o2."CustomerID" = o4."CustomerID" +"""); + } + + public override async Task GroupBy_Select_Entire_Entity_Where_navigation(bool async) + { + await base.GroupBy_Select_Entire_Entity_Where_navigation(async); + + AssertSql( + """ +SELECT o4."OrderID", o4."CustomerID", o4."EmployeeID", o4."OrderDate" +FROM ( + SELECT o."CustomerID" + FROM "Orders" AS o + GROUP BY o."CustomerID" +) AS o2 +LEFT JOIN "Customers" AS c ON ( + SELECT o1."CustomerID" + FROM "Orders" AS o1 + WHERE o2."CustomerID" = o1."CustomerID" OR (o2."CustomerID" IS NULL AND o1."CustomerID" IS NULL) + ORDER BY o1."OrderID" NULLS FIRST + LIMIT 1) = c."CustomerID" +LEFT JOIN ( + SELECT o3."OrderID", o3."CustomerID", o3."EmployeeID", o3."OrderDate" + FROM ( + SELECT o0."OrderID", o0."CustomerID", o0."EmployeeID", o0."OrderDate", ROW_NUMBER() OVER(PARTITION BY o0."CustomerID" ORDER BY o0."OrderID" NULLS FIRST) AS row + FROM "Orders" AS o0 + ) AS o3 + WHERE o3.row <= 1 +) AS o4 ON o2."CustomerID" = o4."CustomerID" +WHERE c."City" = 'London' +"""); + } + + public override async Task GroupBy_Select_Entire_Entity_Select_referenced_twice(bool async) + { + await base.GroupBy_Select_Entire_Entity_Select_referenced_twice(async); + + AssertSql( + """ +SELECT o3."OrderID", o3."CustomerID", o3."EmployeeID", o3."OrderDate" +FROM ( + SELECT o."CustomerID" + FROM "Orders" AS o + GROUP BY o."CustomerID" +) AS o1 +LEFT JOIN ( + SELECT o2."OrderID", o2."CustomerID", o2."EmployeeID", o2."OrderDate" + FROM ( + SELECT o0."OrderID", o0."CustomerID", o0."EmployeeID", o0."OrderDate", ROW_NUMBER() OVER(PARTITION BY o0."CustomerID" ORDER BY o0."OrderID" NULLS FIRST) AS row + FROM "Orders" AS o0 + ) AS o2 + WHERE o2.row <= 1 +) AS o3 ON o1."CustomerID" = o3."CustomerID" +"""); + } + + public override async Task GroupBy_Select_Entire_Entity_Join(bool async) + { + await base.GroupBy_Select_Entire_Entity_Join(async); + + AssertSql( + """ +SELECT o4."OrderID", c."City" +FROM ( + SELECT o."CustomerID" + FROM "Orders" AS o + GROUP BY o."CustomerID" +) AS o2 +INNER JOIN "Customers" AS c ON ( + SELECT o1."CustomerID" + FROM "Orders" AS o1 + WHERE o2."CustomerID" = o1."CustomerID" OR (o2."CustomerID" IS NULL AND o1."CustomerID" IS NULL) + ORDER BY o1."OrderID" NULLS FIRST + LIMIT 1) = c."CustomerID" +LEFT JOIN ( + SELECT o3."OrderID", o3."CustomerID" + FROM ( + SELECT o0."OrderID", o0."CustomerID", ROW_NUMBER() OVER(PARTITION BY o0."CustomerID" ORDER BY o0."OrderID" NULLS FIRST) AS row + FROM "Orders" AS o0 + ) AS o3 + WHERE o3.row <= 1 +) AS o4 ON o2."CustomerID" = o4."CustomerID" +"""); + } + public override async Task GroupBy_Property_Select_Average(bool async) { await base.GroupBy_Property_Select_Average(async); @@ -2310,12 +2768,9 @@ public override async Task GroupBy_with_aggregate_through_navigation_property(bo AssertSql( """ -SELECT ( - SELECT max(c."Region") - FROM "Orders" AS o0 - LEFT JOIN "Customers" AS c ON o0."CustomerID" = c."CustomerID" - WHERE o."EmployeeID" = o0."EmployeeID" OR (o."EmployeeID" IS NULL AND o0."EmployeeID" IS NULL)) AS max +SELECT max(c."Region") AS max FROM "Orders" AS o +LEFT JOIN "Customers" AS c ON o."CustomerID" = c."CustomerID" GROUP BY o."EmployeeID" """); } @@ -3592,27 +4047,18 @@ public override async Task Complex_query_with_groupBy_in_subquery4(bool async) AssertSql( """ -SELECT c."CustomerID", s1."Sum", s1."Count", s1."Key" +SELECT c."CustomerID", s0."Sum", s0."Count", s0."Key" FROM "Customers" AS c LEFT JOIN LATERAL ( - SELECT COALESCE(sum(s."OrderID"), 0)::int AS "Sum", ( - SELECT count(*)::int - FROM ( - SELECT o0."CustomerID", COALESCE(c1."City", '') || COALESCE(o0."CustomerID", '') AS "Key" - FROM "Orders" AS o0 - LEFT JOIN "Customers" AS c1 ON o0."CustomerID" = c1."CustomerID" - WHERE c."CustomerID" = o0."CustomerID" - ) AS s0 - LEFT JOIN "Customers" AS c2 ON s0."CustomerID" = c2."CustomerID" - WHERE (s."Key" = s0."Key" OR (s."Key" IS NULL AND s0."Key" IS NULL)) AND COALESCE(c2."City", '') || COALESCE(s0."CustomerID", '') LIKE 'Lon%') AS "Count", s."Key" + SELECT COALESCE(sum(s."OrderID"), 0)::int AS "Sum", count(*) FILTER (WHERE COALESCE(s."City", '') || COALESCE(s."CustomerID", '') LIKE 'Lon%')::int AS "Count", s."Key" FROM ( - SELECT o."OrderID", COALESCE(c0."City", '') || COALESCE(o."CustomerID", '') AS "Key" + SELECT o."OrderID", o."CustomerID", c0."City", COALESCE(c0."City", '') || COALESCE(o."CustomerID", '') AS "Key" FROM "Orders" AS o LEFT JOIN "Customers" AS c0 ON o."CustomerID" = c0."CustomerID" WHERE c."CustomerID" = o."CustomerID" ) AS s GROUP BY s."Key" -) AS s1 ON TRUE +) AS s0 ON TRUE ORDER BY c."CustomerID" NULLS FIRST """); } diff --git a/test/EFCore.PG.FunctionalTests/Query/NorthwindJoinQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/NorthwindJoinQueryNpgsqlTest.cs index 3d9cbb7e6..dab9837af 100644 --- a/test/EFCore.PG.FunctionalTests/Query/NorthwindJoinQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/NorthwindJoinQueryNpgsqlTest.cs @@ -1,3 +1,5 @@ +using Microsoft.EntityFrameworkCore.TestModels.Northwind; + namespace Microsoft.EntityFrameworkCore.Query; public class NorthwindJoinQueryNpgsqlTest : NorthwindJoinQueryRelationalTestBase> @@ -15,6 +17,53 @@ public override Task Join_local_collection_int_closure_is_cached_correctly(bool => base.Join_local_collection_int_closure_is_cached_correctly(async); // => Assert.ThrowsAsync(() => base.Join_local_collection_int_closure_is_cached_correctly(async)); + // PostgreSQL has no .NET-style char type; char maps to character(1), and casting a digit character to a + // numeric type parses it ('1' -> 1) rather than yielding its code point like .NET ((uint)'1' -> 49). + // The base in-memory expectation (empty result) therefore doesn't hold; assert the PostgreSQL semantics + // while still verifying that the updated closure value is picked up on re-execution. + public override async Task Join_local_string_closure_is_cached_correctly(bool async) + { + var ids = "12"; + await AssertQueryScalar( + async, + ss => from e in ss.Set() + join id in ids on e.EmployeeID equals id + select e.EmployeeID, + ss => from e in ss.Set() + where ids.Select(c => (uint)(c - '0')).Contains(e.EmployeeID) + select e.EmployeeID); + + ids = "3"; + await AssertQueryScalar( + async, + ss => from e in ss.Set() + join id in ids on e.EmployeeID equals id + select e.EmployeeID, + ss => from e in ss.Set() + where ids.Select(c => (uint)(c - '0')).Contains(e.EmployeeID) + select e.EmployeeID); + } + + // Unlike providers where byte[] maps to a scalar binary type and cannot be treated as a collection, + // Npgsql translates the byte[] parameter as a collection, so the join translates and executes fine + // (and byte-to-uint comparison semantics match .NET, so the base in-memory expectation holds). + public override async Task Join_local_bytes_closure_is_cached_correctly(bool async) + { + var ids = new byte[] { 1, 2 }; + await AssertQueryScalar( + async, + ss => from e in ss.Set() + join id in ids on e.EmployeeID equals id + select e.EmployeeID); + + ids = [3]; + await AssertQueryScalar( + async, + ss => from e in ss.Set() + join id in ids on e.EmployeeID equals id + select e.EmployeeID); + } + protected override void ClearLog() => Fixture.TestSqlLoggerFactory.Clear(); } diff --git a/test/EFCore.PG.FunctionalTests/Query/OwnedEntityQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/OwnedEntityQueryNpgsqlTest.cs index bc0bc558c..1646e8419 100644 --- a/test/EFCore.PG.FunctionalTests/Query/OwnedEntityQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/OwnedEntityQueryNpgsqlTest.cs @@ -4,4 +4,54 @@ public class OwnedEntityQueryNpgsqlTest(NonSharedFixture fixture) : OwnedEntityQ { protected override ITestStoreFactory NonSharedTestStoreFactory => NpgsqlTestStoreFactory.Instance; + + // The base test corrupts the seeded data with raw SQL using unquoted identifiers, which PostgreSQL + // folds to lowercase; reimplement with quoted identifiers. + public override async Task Inconsistent_owned_entity_data_logs_warning_and_does_not_cause_identity_conflict() + { + var contextFactory = await InitializeNonSharedTest( + shouldLogCategory: c => c == DbLoggerCategory.Query.Name, + onConfiguring: b => b.ConfigureWarnings(c => c.Log(CoreEventId.InconsistentOwnedDataWarning)), + seed: async c => + { + // Insert a valid entity via EF Core, then corrupt Outer's required property to NULL to + // create inconsistent data: Inner appears present but Outer's required property is null. + var rootEntity = new Context38223.RootEntity + { + Id = Guid.NewGuid(), + Outer = new Context38223.Outer + { + RequiredProperty = 1, + Inner = new Context38223.Inner { InnerProperty = 42 } + } + }; + c.Add(rootEntity); + await c.SaveChangesAsync(); + + await c.Database.ExecuteSqlRawAsync( + """UPDATE "RootEntity" SET "Outer_RequiredProperty" = NULL WHERE "Id" = {0}""", rootEntity.Id); + }); + + using var context = contextFactory.CreateDbContext(); + + ListLoggerFactory.Clear(); + + var root = await context.Set().SingleAsync(); + + Assert.NotNull(root); + Assert.Null(root.Outer); + + Assert.Contains( + ListLoggerFactory.Log, + l => l.Id == CoreEventId.InconsistentOwnedDataWarning && l.Level == LogLevel.Warning); + + // Replacing the owned entity should not throw an identity conflict exception + root.Outer = new Context38223.Outer + { + RequiredProperty = 1, + Inner = new Context38223.Inner { InnerProperty = 2 } + }; + + await context.SaveChangesAsync(); + } } diff --git a/test/EFCore.PG.FunctionalTests/Query/PrecompiledQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/PrecompiledQueryNpgsqlTest.cs index c25515064..44c4737dc 100644 --- a/test/EFCore.PG.FunctionalTests/Query/PrecompiledQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/PrecompiledQueryNpgsqlTest.cs @@ -13,6 +13,41 @@ protected override bool AlwaysPrintGeneratedSources #region Expression types + public override async Task RuntimeConstantExpression() + { + await base.RuntimeConstantExpression(); + + AssertSql( + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +"""); + } + + public override async Task Materialize_entity_with_primitive_collection_mapped_to_column() + { + await base.Materialize_entity_with_primitive_collection_mapped_to_column(); + + AssertSql( + """ +SELECT e."Id", e."Tags" +FROM "EntitiesWithPrimitiveCollection" AS e +ORDER BY e."Id" NULLS FIRST +"""); + } + + public override async Task Project_primitive_collection_mapped_to_column() + { + await base.Project_primitive_collection_mapped_to_column(); + + AssertSql( + """ +SELECT e."Tags" +FROM "EntitiesWithPrimitiveCollection" AS e +ORDER BY e."Id" NULLS FIRST +"""); + } + public override async Task BinaryExpression() { await base.BinaryExpression(); @@ -2102,6 +2137,13 @@ protected override async Task SeedAsync(PrecompiledQueryContext context) var post23 = new Post { Id = 23, Title = "Post23", Blog = blog2 }; context.Posts.AddRange(post11, post12, post21, post22, post23); + + // On PostgreSQL, primitive collections are mapped to native array columns rather than JSON strings, so the + // base fixture's scenario of a column holding a JSON 'null' token doesn't exist; the closest equivalent is a + // SQL NULL, which the base test assertions cover. + context.EntitiesWithPrimitiveCollection.AddRange( + new EntityWithPrimitiveCollection { Id = 1, Tags = ["a", "b"] }, + new EntityWithPrimitiveCollection { Id = 2, Tags = null }); await context.SaveChangesAsync(); } diff --git a/test/EFCore.PG.FunctionalTests/Query/PrimitiveCollectionsQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/PrimitiveCollectionsQueryNpgsqlTest.cs index 2a06de91d..f69b4d886 100644 --- a/test/EFCore.PG.FunctionalTests/Query/PrimitiveCollectionsQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/PrimitiveCollectionsQueryNpgsqlTest.cs @@ -12,6 +12,21 @@ public PrimitiveCollectionsQueryNpgsqlTest(PrimitiveCollectionsQueryNpgsqlFixtur Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); } + public override async Task Inline_collection_SelectMany_with_unreferenced_collection_value() + { + await base.Inline_collection_SelectMany_with_unreferenced_collection_value(); + } + + public override async Task Min_on_MemoryExtensions() + { + await base.Min_on_MemoryExtensions(); + } + + public override async Task Max_on_MemoryExtensions() + { + await base.Max_on_MemoryExtensions(); + } + public override async Task Inline_collection_of_ints_Contains() { await base.Inline_collection_of_ints_Contains(); diff --git a/test/EFCore.PG.FunctionalTests/Query/Translations/MathTranslationsNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/Translations/MathTranslationsNpgsqlTest.cs index a410dc72a..350496593 100644 --- a/test/EFCore.PG.FunctionalTests/Query/Translations/MathTranslationsNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/Translations/MathTranslationsNpgsqlTest.cs @@ -9,6 +9,40 @@ public MathTranslationsNpgsqlTest(BasicTypesQueryNpgsqlFixture fixture, ITestOut Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); } + public override async Task Sign_decimal() + { + await base.Sign_decimal(); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE sign(b."Decimal")::int > 0 +""", + // + """ +SELECT sign(b."Decimal")::int +FROM "BasicTypesEntities" AS b +"""); + } + + public override async Task Sign_int() + { + await base.Sign_int(); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE sign(b."Int")::int > 0 +""", + // + """ +SELECT sign(b."Int")::int +FROM "BasicTypesEntities" AS b +"""); + } + public override async Task Abs_decimal() { await base.Abs_decimal(); diff --git a/test/EFCore.PG.FunctionalTests/Query/Translations/MiscellaneousTranslationsNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/Translations/MiscellaneousTranslationsNpgsqlTest.cs index 4cc7bf81f..585d73367 100644 --- a/test/EFCore.PG.FunctionalTests/Query/Translations/MiscellaneousTranslationsNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/Translations/MiscellaneousTranslationsNpgsqlTest.cs @@ -13,6 +13,78 @@ public MiscellaneousTranslationsNpgsqlTest(BasicTypesQueryNpgsqlFixture fixture, #region Random + public override async Task Byte_Parse() + { + await base.Byte_Parse(); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."Int" >= 0 AND b."Int" <= 255 AND b."Int"::text::smallint = 12 +"""); + } + + public override async Task Decimal_Parse() + { + await base.Decimal_Parse(); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."Int"::text::numeric = 8.0 +"""); + } + + public override async Task Double_Parse() + { + await base.Double_Parse(); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."Int"::text::double precision = 8.0 +"""); + } + + public override async Task Short_Parse() + { + await base.Short_Parse(); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."Int"::text::smallint = 12 +"""); + } + + public override async Task Int_Parse() + { + await base.Int_Parse(); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."Int"::text::int = 12 +"""); + } + + public override async Task Long_Parse() + { + await base.Long_Parse(); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."Int"::text::bigint = 12 +"""); + } + public override async Task Random_on_EF_Functions() { await base.Random_on_EF_Functions(); diff --git a/test/EFCore.PG.FunctionalTests/Query/Translations/Operators/MiscellaneousOperatorTranslationsNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/Translations/Operators/MiscellaneousOperatorTranslationsNpgsqlTest.cs index 2522f98ac..d5ee57be4 100644 --- a/test/EFCore.PG.FunctionalTests/Query/Translations/Operators/MiscellaneousOperatorTranslationsNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/Translations/Operators/MiscellaneousOperatorTranslationsNpgsqlTest.cs @@ -9,6 +9,90 @@ public MiscellaneousOperatorTranslationsNpgsqlTest(BasicTypesQueryNpgsqlFixture Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); } + public override async Task Conditional_simplifiable_equality() + { + await base.Conditional_simplifiable_equality(); + + AssertSql( + """ +SELECT n."Id", n."Bool", n."Byte", n."ByteArray", n."DateOnly", n."DateTime", n."DateTimeOffset", n."Decimal", n."Double", n."Enum", n."FlagsEnum", n."Float", n."Guid", n."Int", n."Long", n."Short", n."String", n."TimeOnly", n."TimeSpan" +FROM "NullableBasicTypesEntities" AS n +WHERE n."Int" > 1 +"""); + } + + public override async Task Conditional_simplifiable_inequality() + { + await base.Conditional_simplifiable_inequality(); + + AssertSql( + """ +SELECT n."Id", n."Bool", n."Byte", n."ByteArray", n."DateOnly", n."DateTime", n."DateTimeOffset", n."Decimal", n."Double", n."Enum", n."FlagsEnum", n."Float", n."Guid", n."Int", n."Long", n."Short", n."String", n."TimeOnly", n."TimeSpan" +FROM "NullableBasicTypesEntities" AS n +WHERE n."Int" > 1 +"""); + } + + public override async Task Conditional_uncoalesce_with_equality_left() + { + await base.Conditional_uncoalesce_with_equality_left(); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE NULLIF(b."Int", 9) > 1 +"""); + } + + public override async Task Conditional_uncoalesce_with_equality_right() + { + await base.Conditional_uncoalesce_with_equality_right(); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE NULLIF(b."Int", 9) > 1 +"""); + } + + public override async Task Conditional_uncoalesce_with_inequality_left() + { + await base.Conditional_uncoalesce_with_inequality_left(); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE NULLIF(b."Int", 9) > 1 +"""); + } + + public override async Task Conditional_uncoalesce_with_inequality_right() + { + await base.Conditional_uncoalesce_with_inequality_right(); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE NULLIF(b."Int", 9) > 1 +"""); + } + + public override async Task Conditional_uncoalesce_with_string() + { + await base.Conditional_uncoalesce_with_string(); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE NULLIF(b."String", 'Seattle') = 'London' +"""); + } + public override async Task Conditional() { await base.Conditional();