diff --git a/Directory.Packages.props b/Directory.Packages.props index 6f1fcac6a5..5df2e4c85d 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 diff --git a/global.json b/global.json index ad870e940e..28189fd630 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/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMethodCallTranslatorProvider.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMethodCallTranslatorProvider.cs index 7f448c5405..e2573d4ae4 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 0000000000..5a30b1721a --- /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 749e3d3a0a..2c368bb0c7 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/AdHocMiscellaneousQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/AdHocMiscellaneousQueryNpgsqlTest.cs index a5bfc25441..b1a6feed77 100644 --- a/test/EFCore.PG.FunctionalTests/Query/AdHocMiscellaneousQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/AdHocMiscellaneousQueryNpgsqlTest.cs @@ -25,6 +25,71 @@ 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(); + } + + // 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; diff --git a/test/EFCore.PG.FunctionalTests/Query/AdHocPrecompiledQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/AdHocPrecompiledQueryNpgsqlTest.cs index 71e8eb8fff..6eadc00371 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 bfb85a7989..a253caac82 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 af2f209223..dec1c84159 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 f9ad75ff73..3933327b8c 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 3b5b316873..1650f1e467 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 84e24d957b..0629ad3f6f 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 dd5d2daeeb..d57e78f922 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 b4465ca72a..4a42f262ed 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 9b140d844a..f8ab3af339 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 22929ce408..b085f34120 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 5be988ccad..577c74425b 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 0dc0bff9d1..08c4f6f232 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 3d9cbb7e68..dab9837afd 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 bc0bc558c3..1646e8419f 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 c255150642..44c4737dcc 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 2a06de91de..f69b4d886f 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 a410dc72a4..3504965936 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 4cc7bf81f2..585d733677 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 2522f98aca..d5ee57be4c 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();