diff --git a/Directory.Packages.props b/Directory.Packages.props index 1df6204545..b6310ef70d 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,8 +1,8 @@ - 11.0.0-preview.4.26203.108 - 11.0.0-preview.4.26203.108 - 11.0.0-preview.4.26203.108 + 11.0.0-preview.4.26210.110 + 11.0.0-preview.4.26210.110 + 11.0.0-preview.4.26210.110 10.0.0 diff --git a/global.json b/global.json index 08894b78c1..973ee8c8d8 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "11.0.100-preview.2.26159.112", + "version": "11.0.100-preview.4.26210.111", "rollForward": "latestMinor", "allowPrerelease": true } diff --git a/src/EFCore.PG/Update/Internal/NpgsqlUpdateSqlGenerator.cs b/src/EFCore.PG/Update/Internal/NpgsqlUpdateSqlGenerator.cs index cecf95aee3..148feff911 100644 --- a/src/EFCore.PG/Update/Internal/NpgsqlUpdateSqlGenerator.cs +++ b/src/EFCore.PG/Update/Internal/NpgsqlUpdateSqlGenerator.cs @@ -127,7 +127,12 @@ protected override void AppendUpdateColumnValue( string name, string? schema) { - if (columnModification.JsonPath is not (null or "$")) + if (columnModification.JsonPath is null or { IsRoot: true }) + { + base.AppendUpdateColumnValue(updateSqlGeneratorHelper, columnModification, stringBuilder, name, schema); + return; + } + { Check.DebugAssert( columnModification.TypeMapping is NpgsqlStructuralJsonTypeMapping, @@ -141,45 +146,33 @@ protected override void AppendUpdateColumnValue( Check.DebugAssert(columnModification.TypeMapping.StoreType is "jsonb", "Non-jsonb type mapping in JSON partial update"); + var jsonPath = columnModification.JsonPath; + // TODO: Lax or not? stringBuilder .Append("jsonb_set(") .Append(updateSqlGeneratorHelper.DelimitIdentifier(columnModification.ColumnName)) .Append(", '{"); - // TODO: Unfortunately JsonPath is provided as a JSONPATH string, but PG's jsonb_set requires the path as an array. - // Parse the components back out (https://github.com/dotnet/efcore/issues/32185) - var components = columnModification.JsonPath.Split("."); + // PG's jsonb_set requires the path as an array, so we iterate over the structured JsonPath segments. + var ordinalIndex = 0; var needsComma = false; - for (var i = 0; i < components.Length; i++) + foreach (var segment in jsonPath.Segments) { if (needsComma) { stringBuilder.Append(','); } - var component = components[i]; - var bracketOpen = component.IndexOf('['); - if (bracketOpen == -1) + if (segment.IsArray) { - if (i > 0) // The first component is $, representing the root - { - stringBuilder.Append(component); - needsComma = true; - } - - continue; + stringBuilder.Append(jsonPath.Ordinals[ordinalIndex++]); } - - var propertyName = component[..bracketOpen]; - if (i > 0) // The first component is $, representing the root + else { - stringBuilder - .Append(propertyName) - .Append(','); + stringBuilder.Append(segment.PropertyName); } - stringBuilder.Append(component[(bracketOpen + 1)..^1]); needsComma = true; } @@ -197,10 +190,6 @@ protected override void AppendUpdateColumnValue( stringBuilder.Append(")"); } - else - { - base.AppendUpdateColumnValue(updateSqlGeneratorHelper, columnModification, stringBuilder, name, schema); - } } private FieldInfo? _columnModificationValueField; diff --git a/test/EFCore.PG.FunctionalTests/Query/Associations/Navigations/NavigationsBulkUpdateNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/Associations/Navigations/NavigationsBulkUpdateNpgsqlTest.cs new file mode 100644 index 0000000000..771237585e --- /dev/null +++ b/test/EFCore.PG.FunctionalTests/Query/Associations/Navigations/NavigationsBulkUpdateNpgsqlTest.cs @@ -0,0 +1,137 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit.Sdk; + +namespace Microsoft.EntityFrameworkCore.Query.Associations.Navigations; + +public class NavigationsBulkUpdateNpgsqlTest(NavigationsNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) + : NavigationsBulkUpdateRelationalTestBase(fixture, testOutputHelper) +{ + [ConditionalFact] + public virtual void Check_all_tests_overridden() + => TestHelpers.AssertAllMethodsOverridden(GetType()); + + // FK constraint failures + public override Task Delete_entity_with_associations() + => Assert.ThrowsAsync(base.Delete_entity_with_associations); + + public override Task Delete_required_associate() + => Assert.ThrowsAsync(base.Delete_required_associate); + + public override Task Delete_optional_associate() + => Assert.ThrowsAsync(base.Delete_optional_associate); + + // PostgreSQL generates valid SQL for these but produces wrong row counts due to self-joins in UPDATE ... FROM + public override Task Update_property_inside_associate() + => Assert.ThrowsAsync(base.Update_property_inside_associate); + + public override Task Update_property_inside_associate_with_special_chars() + => Assert.ThrowsAsync(base.Update_property_inside_associate_with_special_chars); + + public override Task Update_property_on_projected_associate() + => Assert.ThrowsAsync(base.Update_property_on_projected_associate); + + public override Task Update_property_on_projected_associate_with_OrderBy_Skip() + => Assert.ThrowsAsync(base.Update_property_on_projected_associate_with_OrderBy_Skip); + + public override Task Update_multiple_properties_inside_same_associate() + => Assert.ThrowsAsync(base.Update_multiple_properties_inside_same_associate); + + public override Task Update_primitive_collection_to_constant() + => Assert.ThrowsAsync(base.Update_primitive_collection_to_constant); + + public override Task Update_primitive_collection_to_parameter() + => Assert.ThrowsAsync(base.Update_primitive_collection_to_parameter); + + // Translation not yet supported for navigation-mapped associations + public override Task Update_associate_to_parameter() + => Assert.ThrowsAsync(base.Update_associate_to_parameter); + + public override Task Update_associate_to_inline() + => Assert.ThrowsAsync(base.Update_associate_to_inline); + + public override Task Update_associate_to_inline_with_lambda() + => Assert.ThrowsAsync(base.Update_associate_to_inline_with_lambda); + + public override Task Update_associate_to_another_associate() + => Assert.ThrowsAsync(base.Update_associate_to_another_associate); + + public override Task Update_associate_to_null() + => Assert.ThrowsAsync(base.Update_associate_to_null); + + public override Task Update_associate_to_null_with_lambda() + => Assert.ThrowsAsync(base.Update_associate_to_null_with_lambda); + + public override Task Update_associate_to_null_parameter() + => Assert.ThrowsAsync(base.Update_associate_to_null_parameter); + + public override Task Update_nested_associate_to_parameter() + => Assert.ThrowsAsync(base.Update_nested_associate_to_parameter); + + public override Task Update_nested_associate_to_inline_with_lambda() + => Assert.ThrowsAsync(base.Update_nested_associate_to_inline_with_lambda); + + public override Task Update_nested_associate_to_another_nested_associate() + => Assert.ThrowsAsync(base.Update_nested_associate_to_another_nested_associate); + + public override Task Update_nested_collection_to_parameter() + => Assert.ThrowsAsync(base.Update_nested_collection_to_parameter); + + public override Task Update_nested_collection_to_inline_with_lambda() + => Assert.ThrowsAsync(base.Update_nested_collection_to_inline_with_lambda); + + public override Task Update_nested_collection_to_another_nested_collection() + => Assert.ThrowsAsync(base.Update_nested_collection_to_another_nested_collection); + + public override Task Update_collection_to_parameter() + => Assert.ThrowsAsync(base.Update_collection_to_parameter); + + public override Task Update_collection_referencing_the_original_collection() + => Assert.ThrowsAsync(base.Update_collection_referencing_the_original_collection); + + public override Task Update_primitive_collection_to_another_collection() + => Assert.ThrowsAsync(base.Update_primitive_collection_to_another_collection); + + public override Task Update_inside_structural_collection() + => Assert.ThrowsAsync(base.Update_inside_structural_collection); + + public override Task Update_multiple_properties_inside_associates_and_on_entity_type() + => Assert.ThrowsAsync(base.Update_multiple_properties_inside_associates_and_on_entity_type); + + public override Task Update_multiple_projected_associates_via_anonymous_type() + => Assert.ThrowsAsync(base.Update_multiple_projected_associates_via_anonymous_type); + + public override async Task Update_property_inside_nested_associate() + { + await base.Update_property_inside_nested_associate(); + + AssertExecuteUpdateSql( + """ +@p='foo_updated' + +UPDATE "NestedAssociateType" AS n +SET "String" = @p +FROM "RootEntity" AS r +INNER JOIN "AssociateType" AS a ON r."RequiredAssociateId" = a."Id" +WHERE a."RequiredNestedAssociateId" = n."Id" +"""); + } + + public override async Task Update_associate_with_null_required_property() + { + await base.Update_associate_with_null_required_property(); + + AssertExecuteUpdateSql(); + } + + public override async Task Update_required_nested_associate_to_null() + { + await base.Update_required_nested_associate_to_null(); + + AssertExecuteUpdateSql(); + } + + public override Task Update_inside_primitive_collection() + => Assert.ThrowsAsync(base.Update_inside_primitive_collection); +} diff --git a/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedJson/OwnedJsonCollectionNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedJson/OwnedJsonCollectionNpgsqlTest.cs index 896cb38977..855fdbb008 100644 --- a/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedJson/OwnedJsonCollectionNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedJson/OwnedJsonCollectionNpgsqlTest.cs @@ -39,13 +39,7 @@ public override async Task Where() FROM "RootEntity" AS r WHERE ( SELECT count(*)::int - FROM ROWS FROM (jsonb_to_recordset(r."AssociateCollection") AS ( - "Id" integer, - "Int" integer, - "Ints" jsonb, - "Name" text, - "String" text - )) WITH ORDINALITY AS a + FROM ROWS FROM (jsonb_to_recordset(r."AssociateCollection") AS ("Int" integer)) WITH ORDINALITY AS a WHERE a."Int" <> 8) = 2 """); } @@ -62,10 +56,7 @@ public override async Task OrderBy_ElementAt() SELECT a."Int" FROM ROWS FROM (jsonb_to_recordset(r."AssociateCollection") AS ( "Id" integer, - "Int" integer, - "Ints" jsonb, - "Name" text, - "String" text + "Int" integer )) WITH ORDINALITY AS a ORDER BY a."Id" NULLS FIRST LIMIT 1 OFFSET 0) = 8 @@ -210,18 +201,12 @@ public override async Task GroupBy() SELECT r."Id", r."Name", r."AssociateCollection", r."OptionalAssociate", r."RequiredAssociate" FROM "RootEntity" AS r WHERE 16 IN ( - SELECT COALESCE(sum(a0."Int"), 0)::int - FROM ( - SELECT a."Id" AS "Id0", a."Int", a."Ints", a."Name", a."String", a."String" AS "Key" - FROM ROWS FROM (jsonb_to_recordset(r."AssociateCollection") AS ( - "Id" integer, - "Int" integer, - "Ints" jsonb, - "Name" text, - "String" text - )) WITH ORDINALITY AS a - ) AS a0 - GROUP BY a0."Key" + SELECT COALESCE(sum(a."Int"), 0)::int + FROM ROWS FROM (jsonb_to_recordset(r."AssociateCollection") AS ( + "Int" integer, + "String" text + )) WITH ORDINALITY AS a + GROUP BY a."String" ) """); } @@ -248,13 +233,7 @@ SELECT r."Name" FROM "RootEntity" AS r WHERE EXISTS ( SELECT 1 - FROM ROWS FROM (jsonb_to_recordset(r."AssociateCollection") AS ( - "Id" integer, - "Int" integer, - "Ints" jsonb, - "Name" text, - "String" text - )) WITH ORDINALITY AS a + FROM ROWS FROM (jsonb_to_recordset(r."AssociateCollection") AS ("Int" integer)) WITH ORDINALITY AS a WHERE a."Int" > 0) GROUP BY r."Name" ) AS r1 @@ -265,13 +244,7 @@ LEFT JOIN ( FROM "RootEntity" AS r0 WHERE EXISTS ( SELECT 1 - FROM ROWS FROM (jsonb_to_recordset(r0."AssociateCollection") AS ( - "Id" integer, - "Int" integer, - "Ints" jsonb, - "Name" text, - "String" text - )) WITH ORDINALITY AS a0 + FROM ROWS FROM (jsonb_to_recordset(r0."AssociateCollection") AS ("Int" integer)) WITH ORDINALITY AS a0 WHERE a0."Int" > 0) ) AS r2 WHERE r2.row <= 1 @@ -291,13 +264,7 @@ public override async Task Select_within_Select_within_Select_with_aggregates() SELECT ( SELECT COALESCE(sum(( SELECT max(n."Int") - FROM ROWS FROM (jsonb_to_recordset(a."NestedCollection") AS ( - "Id" integer, - "Int" integer, - "Ints" jsonb, - "Name" text, - "String" text - )) WITH ORDINALITY AS n)), 0)::int + FROM ROWS FROM (jsonb_to_recordset(a."NestedCollection") AS ("Int" integer)) WITH ORDINALITY AS n)), 0)::int FROM ROWS FROM (jsonb_to_recordset(r."AssociateCollection") AS ("NestedCollection" jsonb)) WITH ORDINALITY AS a) FROM "RootEntity" AS r """); diff --git a/test/EFCore.PG.FunctionalTests/Query/JsonQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/JsonQueryNpgsqlTest.cs index d8ef9dc4f6..5be988ccad 100644 --- a/test/EFCore.PG.FunctionalTests/Query/JsonQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/JsonQueryNpgsqlTest.cs @@ -932,10 +932,7 @@ public override async Task Json_collection_Where_ElementAt(bool async) WHERE ( SELECT o."OwnedReferenceLeaf" ->> 'SomethingSomething' FROM ROWS FROM (jsonb_to_recordset(j."OwnedReferenceRoot" -> 'OwnedCollectionBranch') AS ( - "Date" timestamp without time zone, "Enum" integer, - "Fraction" numeric(18,2), - "Id" integer, "OwnedReferenceLeaf" jsonb )) WITH ORDINALITY AS o WHERE o."Enum" = -3 @@ -973,18 +970,15 @@ public override async Task Json_collection_OrderByDescending_Skip_ElementAt(bool WHERE ( SELECT o0.c FROM ( - SELECT o."OwnedReferenceLeaf" ->> 'SomethingSomething' AS c, o."Date" AS c0 + SELECT o."OwnedReferenceLeaf" ->> 'SomethingSomething' AS c, o."Date" FROM ROWS FROM (jsonb_to_recordset(j."OwnedReferenceRoot" -> 'OwnedCollectionBranch') AS ( "Date" timestamp without time zone, - "Enum" integer, - "Fraction" numeric(18,2), - "Id" integer, "OwnedReferenceLeaf" jsonb )) WITH ORDINALITY AS o ORDER BY o."Date" DESC NULLS LAST OFFSET 1 ) AS o0 - ORDER BY o0.c0 DESC NULLS LAST + ORDER BY o0."Date" DESC NULLS LAST LIMIT 1 OFFSET 0) = 'e1_r_c1_r' """); } @@ -1075,7 +1069,6 @@ public override async Task Json_collection_in_projection_with_anonymous_projecti SELECT j."Id", o."Name", o."Number", o.ordinality FROM "JsonEntitiesBasic" AS j LEFT JOIN LATERAL ROWS FROM (jsonb_to_recordset(j."OwnedCollectionRoot") AS ( - "Id" integer, "Name" text, "Number" integer )) WITH ORDINALITY AS o ON TRUE @@ -1094,7 +1087,6 @@ public override async Task Json_collection_in_projection_with_composition_where_ LEFT JOIN LATERAL ( SELECT o."Name", o."Number", o.ordinality FROM ROWS FROM (jsonb_to_recordset(j."OwnedCollectionRoot") AS ( - "Id" integer, "Name" text, "Number" integer )) WITH ORDINALITY AS o @@ -1115,10 +1107,8 @@ public override async Task Json_collection_in_projection_with_composition_where_ LEFT JOIN LATERAL ( SELECT o."Names", o."Numbers", o.ordinality FROM ROWS FROM (jsonb_to_recordset(j."OwnedCollectionRoot") AS ( - "Id" integer, "Name" text, "Names" jsonb, - "Number" integer, "Numbers" jsonb )) WITH ORDINALITY AS o WHERE o."Name" = 'Foo' @@ -1189,17 +1179,16 @@ public override async Task Json_nested_collection_anonymous_projection_in_projec AssertSql( """ -SELECT j."Id", s.ordinality, s.c, s.c0, s.c1, s.c2, s.c3, s."Id", s.c4, s.ordinality0 +SELECT j."Id", s.ordinality, s."Date", s."Enum", s."Enums", s."Fraction", s.c, s."Id", s.c0, s.ordinality0 FROM "JsonEntitiesBasic" AS j LEFT JOIN LATERAL ( - SELECT o.ordinality, o0."Date" AS c, o0."Enum" AS c0, o0."Enums" AS c1, o0."Fraction" AS c2, o0."OwnedReferenceLeaf" AS c3, j."Id", o0."OwnedCollectionLeaf" AS c4, o0.ordinality AS ordinality0 + SELECT o.ordinality, o0."Date", o0."Enum", o0."Enums", o0."Fraction", o0."OwnedReferenceLeaf" AS c, j."Id", o0."OwnedCollectionLeaf" AS c0, o0.ordinality AS ordinality0 FROM ROWS FROM (jsonb_to_recordset(j."OwnedCollectionRoot") AS ("OwnedCollectionBranch" jsonb)) WITH ORDINALITY AS o LEFT JOIN LATERAL ROWS FROM (jsonb_to_recordset(o."OwnedCollectionBranch") AS ( "Date" timestamp without time zone, "Enum" integer, "Enums" jsonb, "Fraction" numeric(18,2), - "Id" integer, "OwnedCollectionLeaf" jsonb, "OwnedReferenceLeaf" jsonb )) WITH ORDINALITY AS o0 ON TRUE @@ -1217,7 +1206,7 @@ public override async Task Json_collection_skip_take_in_projection(bool async) SELECT j."Id", o0."Id", o0."Id0", o0."Name", o0."Names", o0."Number", o0."Numbers", o0.c, o0.c0, o0.ordinality FROM "JsonEntitiesBasic" AS j LEFT JOIN LATERAL ( - SELECT j."Id", o."Id" AS "Id0", o."Name", o."Names", o."Number", o."Numbers", o."OwnedCollectionBranch" AS c, o."OwnedReferenceBranch" AS c0, o.ordinality, o."Name" AS c1 + SELECT j."Id", o."Id" AS "Id0", o."Name", o."Names", o."Number", o."Numbers", o."OwnedCollectionBranch" AS c, o."OwnedReferenceBranch" AS c0, o.ordinality FROM ROWS FROM (jsonb_to_recordset(j."OwnedCollectionRoot") AS ( "Id" integer, "Name" text, @@ -1230,7 +1219,7 @@ FROM ROWS FROM (jsonb_to_recordset(j."OwnedCollectionRoot") AS ( ORDER BY o."Name" NULLS FIRST LIMIT 5 OFFSET 1 ) AS o0 ON TRUE -ORDER BY j."Id" NULLS FIRST, o0.c1 NULLS FIRST +ORDER BY j."Id" NULLS FIRST, o0."Name" NULLS FIRST """); } @@ -1240,12 +1229,11 @@ public override async Task Json_collection_skip_take_in_projection_project_into_ AssertSql( """ -SELECT j."Id", o0.c, o0.c0, o0.c1, o0.c2, o0.c3, o0."Id", o0.c4, o0.ordinality +SELECT j."Id", o0."Name", o0."Names", o0."Number", o0."Numbers", o0.c, o0."Id", o0.c0, o0.ordinality FROM "JsonEntitiesBasic" AS j LEFT JOIN LATERAL ( - SELECT o."Name" AS c, o."Names" AS c0, o."Number" AS c1, o."Numbers" AS c2, o."OwnedCollectionBranch" AS c3, j."Id", o."OwnedReferenceBranch" AS c4, o.ordinality + SELECT o."Name", o."Names", o."Number", o."Numbers", o."OwnedCollectionBranch" AS c, j."Id", o."OwnedReferenceBranch" AS c0, o.ordinality FROM ROWS FROM (jsonb_to_recordset(j."OwnedCollectionRoot") AS ( - "Id" integer, "Name" text, "Names" jsonb, "Number" integer, @@ -1256,7 +1244,7 @@ FROM ROWS FROM (jsonb_to_recordset(j."OwnedCollectionRoot") AS ( ORDER BY o."Name" NULLS FIRST LIMIT 5 OFFSET 1 ) AS o0 ON TRUE -ORDER BY j."Id" NULLS FIRST, o0.c NULLS FIRST +ORDER BY j."Id" NULLS FIRST, o0."Name" NULLS FIRST """); } @@ -1269,17 +1257,15 @@ public override async Task Json_collection_skip_take_in_projection_with_json_ref SELECT j."Id", o0.c, o0."Id", o0.ordinality FROM "JsonEntitiesBasic" AS j LEFT JOIN LATERAL ( - SELECT o."OwnedReferenceBranch" AS c, j."Id", o.ordinality, o."Name" AS c0 + SELECT o."OwnedReferenceBranch" AS c, j."Id", o.ordinality, o."Name" FROM ROWS FROM (jsonb_to_recordset(j."OwnedCollectionRoot") AS ( - "Id" integer, "Name" text, - "Number" integer, "OwnedReferenceBranch" jsonb )) WITH ORDINALITY AS o ORDER BY o."Name" NULLS FIRST LIMIT 5 OFFSET 1 ) AS o0 ON TRUE -ORDER BY j."Id" NULLS FIRST, o0.c0 NULLS FIRST +ORDER BY j."Id" NULLS FIRST, o0."Name" NULLS FIRST """); } @@ -2985,17 +2971,16 @@ public override async Task Json_nested_collection_anonymous_projection_of_primit AssertSql( """ -SELECT j."Id", s.ordinality, s.c, s.c0, s.c1, s.c2, s.ordinality0 +SELECT j."Id", s.ordinality, s."Date", s."Enum", s."Enums", s."Fraction", s.ordinality0 FROM "JsonEntitiesBasic" AS j LEFT JOIN LATERAL ( - SELECT o.ordinality, o0."Date" AS c, o0."Enum" AS c0, o0."Enums" AS c1, o0."Fraction" AS c2, o0.ordinality AS ordinality0 + SELECT o.ordinality, o0."Date", o0."Enum", o0."Enums", o0."Fraction", o0.ordinality AS ordinality0 FROM ROWS FROM (jsonb_to_recordset(j."OwnedCollectionRoot") AS ("OwnedCollectionBranch" jsonb)) WITH ORDINALITY AS o LEFT JOIN LATERAL ROWS FROM (jsonb_to_recordset(o."OwnedCollectionBranch") AS ( "Date" timestamp without time zone, "Enum" integer, "Enums" jsonb, - "Fraction" numeric(18,2), - "Id" integer + "Fraction" numeric(18,2) )) WITH ORDINALITY AS o0 ON TRUE ) AS s ON TRUE ORDER BY j."Id" NULLS FIRST, s.ordinality NULLS FIRST