Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<Project>
<PropertyGroup>
<EFCoreVersion>11.0.0-preview.4.26203.108</EFCoreVersion>
<MicrosoftExtensionsVersion>11.0.0-preview.4.26203.108</MicrosoftExtensionsVersion>
<MicrosoftExtensionsConfigurationVersion>11.0.0-preview.4.26203.108</MicrosoftExtensionsConfigurationVersion>
<EFCoreVersion>11.0.0-preview.4.26210.110</EFCoreVersion>
<MicrosoftExtensionsVersion>11.0.0-preview.4.26210.110</MicrosoftExtensionsVersion>
<MicrosoftExtensionsConfigurationVersion>11.0.0-preview.4.26210.110</MicrosoftExtensionsConfigurationVersion>
<NpgsqlVersion>10.0.0</NpgsqlVersion>
</PropertyGroup>

Expand Down
2 changes: 1 addition & 1 deletion global.json
Original file line number Diff line number Diff line change
@@ -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
}
Expand Down
41 changes: 15 additions & 26 deletions src/EFCore.PG/Update/Internal/NpgsqlUpdateSqlGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;
}

Expand All @@ -197,10 +190,6 @@ protected override void AppendUpdateColumnValue(

stringBuilder.Append(")");
}
else
{
base.AppendUpdateColumnValue(updateSqlGeneratorHelper, columnModification, stringBuilder, name, schema);
}
}

private FieldInfo? _columnModificationValueField;
Expand Down
Original file line number Diff line number Diff line change
@@ -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<NavigationsNpgsqlFixture>(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<PostgresException>(base.Delete_entity_with_associations);

public override Task Delete_required_associate()
=> Assert.ThrowsAsync<PostgresException>(base.Delete_required_associate);

public override Task Delete_optional_associate()
=> Assert.ThrowsAsync<PostgresException>(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<EqualException>(base.Update_property_inside_associate);

public override Task Update_property_inside_associate_with_special_chars()
=> Assert.ThrowsAsync<EqualException>(base.Update_property_inside_associate_with_special_chars);

public override Task Update_property_on_projected_associate()
=> Assert.ThrowsAsync<EqualException>(base.Update_property_on_projected_associate);

public override Task Update_property_on_projected_associate_with_OrderBy_Skip()
=> Assert.ThrowsAsync<EqualException>(base.Update_property_on_projected_associate_with_OrderBy_Skip);

public override Task Update_multiple_properties_inside_same_associate()
=> Assert.ThrowsAsync<EqualException>(base.Update_multiple_properties_inside_same_associate);

public override Task Update_primitive_collection_to_constant()
=> Assert.ThrowsAsync<EqualException>(base.Update_primitive_collection_to_constant);

public override Task Update_primitive_collection_to_parameter()
=> Assert.ThrowsAsync<EqualException>(base.Update_primitive_collection_to_parameter);

// Translation not yet supported for navigation-mapped associations
public override Task Update_associate_to_parameter()
=> Assert.ThrowsAsync<InvalidOperationException>(base.Update_associate_to_parameter);

public override Task Update_associate_to_inline()
=> Assert.ThrowsAsync<InvalidOperationException>(base.Update_associate_to_inline);

public override Task Update_associate_to_inline_with_lambda()
=> Assert.ThrowsAsync<InvalidOperationException>(base.Update_associate_to_inline_with_lambda);

public override Task Update_associate_to_another_associate()
=> Assert.ThrowsAsync<InvalidOperationException>(base.Update_associate_to_another_associate);

public override Task Update_associate_to_null()
=> Assert.ThrowsAsync<InvalidOperationException>(base.Update_associate_to_null);

public override Task Update_associate_to_null_with_lambda()
=> Assert.ThrowsAsync<InvalidOperationException>(base.Update_associate_to_null_with_lambda);

public override Task Update_associate_to_null_parameter()
=> Assert.ThrowsAsync<InvalidOperationException>(base.Update_associate_to_null_parameter);

public override Task Update_nested_associate_to_parameter()
=> Assert.ThrowsAsync<InvalidOperationException>(base.Update_nested_associate_to_parameter);

public override Task Update_nested_associate_to_inline_with_lambda()
=> Assert.ThrowsAsync<InvalidOperationException>(base.Update_nested_associate_to_inline_with_lambda);

public override Task Update_nested_associate_to_another_nested_associate()
=> Assert.ThrowsAsync<InvalidOperationException>(base.Update_nested_associate_to_another_nested_associate);

public override Task Update_nested_collection_to_parameter()
=> Assert.ThrowsAsync<InvalidOperationException>(base.Update_nested_collection_to_parameter);

public override Task Update_nested_collection_to_inline_with_lambda()
=> Assert.ThrowsAsync<InvalidOperationException>(base.Update_nested_collection_to_inline_with_lambda);

public override Task Update_nested_collection_to_another_nested_collection()
=> Assert.ThrowsAsync<InvalidOperationException>(base.Update_nested_collection_to_another_nested_collection);

public override Task Update_collection_to_parameter()
=> Assert.ThrowsAsync<InvalidOperationException>(base.Update_collection_to_parameter);

public override Task Update_collection_referencing_the_original_collection()
=> Assert.ThrowsAsync<InvalidOperationException>(base.Update_collection_referencing_the_original_collection);

public override Task Update_primitive_collection_to_another_collection()
=> Assert.ThrowsAsync<InvalidOperationException>(base.Update_primitive_collection_to_another_collection);

public override Task Update_inside_structural_collection()
=> Assert.ThrowsAsync<InvalidOperationException>(base.Update_inside_structural_collection);

public override Task Update_multiple_properties_inside_associates_and_on_entity_type()
=> Assert.ThrowsAsync<InvalidOperationException>(base.Update_multiple_properties_inside_associates_and_on_entity_type);

public override Task Update_multiple_projected_associates_via_anonymous_type()
=> Assert.ThrowsAsync<InvalidOperationException>(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<InvalidOperationException>(base.Update_inside_primitive_collection);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
""");
}
Expand All @@ -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
Expand Down Expand Up @@ -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"
)
""");
}
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
""");
Expand Down
Loading
Loading