diff --git a/EFCore.PG.slnx b/EFCore.PG.slnx
index 8166ab441..795733aef 100644
--- a/EFCore.PG.slnx
+++ b/EFCore.PG.slnx
@@ -22,4 +22,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/global.json b/global.json
deleted file mode 100644
index 5cb25519f..000000000
--- a/global.json
+++ /dev/null
@@ -1,7 +0,0 @@
-{
- "sdk": {
- "version": "10.0.100-preview.7.25380.108",
- "rollForward": "latestMajor",
- "allowPrerelease": true
- }
-}
diff --git a/global.json b/global.json
new file mode 120000
index 000000000..298c39bb6
--- /dev/null
+++ b/global.json
@@ -0,0 +1 @@
+../efcore/global.json
\ No newline at end of file
diff --git a/src/Directory.Build.props b/src/Directory.Build.props
index b5e6b77b5..4816d1a1d 100644
--- a/src/Directory.Build.props
+++ b/src/Directory.Build.props
@@ -8,7 +8,10 @@
nullablePublicOnly
+
diff --git a/src/EFCore.PG/EFCore.PG.csproj b/src/EFCore.PG/EFCore.PG.csproj
index f6dd61b6c..4bceab1ea 100644
--- a/src/EFCore.PG/EFCore.PG.csproj
+++ b/src/EFCore.PG/EFCore.PG.csproj
@@ -22,6 +22,12 @@
+
+
+
+
+
+
diff --git a/src/EFCore.PG/Query/Internal/NpgsqlQuerySqlGenerator.cs b/src/EFCore.PG/Query/Internal/NpgsqlQuerySqlGenerator.cs
index 4a7fed392..7081d0a41 100644
--- a/src/EFCore.PG/Query/Internal/NpgsqlQuerySqlGenerator.cs
+++ b/src/EFCore.PG/Query/Internal/NpgsqlQuerySqlGenerator.cs
@@ -159,6 +159,23 @@ protected override void GenerateTop(SelectExpression selectExpression)
// No TOP() in PostgreSQL, see GenerateLimitOffset
}
+ ///
+ /// Generates SQL for a constant.
+ ///
+ /// The for which to generate SQL.
+ protected override Expression VisitSqlConstant(SqlConstantExpression sqlConstantExpression)
+ {
+ // Certain JSON functions (e.g. jsonb_set()) accept a JSONPATH argument - this is (currently) flown here as a
+ // SqlConstantExpression over IReadOnlyList. Render that to a string here.
+ if (sqlConstantExpression is { Value: IReadOnlyList path })
+ {
+ GenerateJsonPath(ConvertJsonPathSegments(path));
+ return sqlConstantExpression;
+ }
+
+ return base.VisitSqlConstant(sqlConstantExpression);
+ }
+
///
/// 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
@@ -1058,31 +1075,33 @@ protected virtual Expression VisitILike(PgILikeExpression likeExpression, bool n
protected override Expression VisitJsonScalar(JsonScalarExpression jsonScalarExpression)
{
// TODO: Stop producing empty JsonScalarExpressions, #30768
- var path = jsonScalarExpression.Path;
- if (path.Count == 0)
+ var segmentsPath = jsonScalarExpression.Path;
+ if (segmentsPath.Count == 0)
{
Visit(jsonScalarExpression.Json);
return jsonScalarExpression;
}
+ var path = ConvertJsonPathSegments(segmentsPath);
+
switch (jsonScalarExpression.TypeMapping)
{
// This case is for when a nested JSON entity is being accessed. We want the json/jsonb fragment in this case (not text),
// so we can perform further JSON operations on it.
case NpgsqlStructuralJsonTypeMapping:
- GenerateJsonPath(returnsText: false);
+ GenerateJsonPath(jsonScalarExpression.Json, returnsText: false, path);
break;
// No need to cast the output when we expect a string anyway
case StringTypeMapping:
- GenerateJsonPath(returnsText: true);
+ GenerateJsonPath(jsonScalarExpression.Json, returnsText: true, path);
break;
// bytea requires special handling, since we encode the binary data as base64 inside the JSON, but that requires a special
// conversion function to be extracted out to a PG bytea.
case NpgsqlByteArrayTypeMapping:
Sql.Append("decode(");
- GenerateJsonPath(returnsText: true);
+ GenerateJsonPath(jsonScalarExpression.Json, returnsText: true, path);
Sql.Append(", 'base64')");
break;
@@ -1092,13 +1111,13 @@ protected override Expression VisitJsonScalar(JsonScalarExpression jsonScalarExp
case NpgsqlArrayTypeMapping arrayMapping:
Sql.Append("(ARRAY(SELECT CAST(element AS ").Append(arrayMapping.ElementTypeMapping.StoreType)
.Append(") FROM jsonb_array_elements_text(");
- GenerateJsonPath(returnsText: false);
+ GenerateJsonPath(jsonScalarExpression.Json, returnsText: false, path);
Sql.Append(") WITH ORDINALITY AS t(element) ORDER BY ordinality))");
break;
default:
Sql.Append("CAST(");
- GenerateJsonPath(returnsText: true);
+ GenerateJsonPath(jsonScalarExpression.Json, returnsText: true, path);
Sql.Append(" AS ");
Sql.Append(jsonScalarExpression.TypeMapping!.StoreType);
Sql.Append(")");
@@ -1106,19 +1125,6 @@ protected override Expression VisitJsonScalar(JsonScalarExpression jsonScalarExp
}
return jsonScalarExpression;
-
- void GenerateJsonPath(bool returnsText)
- => this.GenerateJsonPath(
- jsonScalarExpression.Json,
- returnsText: returnsText,
- jsonScalarExpression.Path.Select(
- s => s switch
- {
- { PropertyName: string propertyName }
- => new SqlConstantExpression(propertyName, _textTypeMapping ??= _typeMappingSource.FindMapping(typeof(string))),
- { ArrayIndex: SqlExpression arrayIndex } => arrayIndex,
- _ => throw new UnreachableException()
- }).ToList());
}
///
@@ -1148,6 +1154,11 @@ private void GenerateJsonPath(SqlExpression expression, bool returnsText, IReadO
// Multiple path components
Sql.Append(returnsText ? " #>> " : " #> ");
+ GenerateJsonPath(path);
+ }
+
+ private void GenerateJsonPath(IReadOnlyList path)
+ {
// Use simplified array literal syntax if all path components are constants for cleaner SQL
if (path.All(p => p is SqlConstantExpression { Value: var pathSegment }
&& (pathSegment is not string s || s.All(char.IsAsciiLetterOrDigit))))
@@ -1173,6 +1184,23 @@ private void GenerateJsonPath(SqlExpression expression, bool returnsText, IReadO
}
}
+ ///
+ /// Converts the standard EF to an
+ /// (the EF built-in and don't support non-constant
+ /// property names, but we do via the Npgsql-specific JSON DOM support).
+ ///
+ private IReadOnlyList ConvertJsonPathSegments(IReadOnlyList path)
+ => path
+ .Select(
+ s => s switch
+ {
+ { PropertyName: string propertyName }
+ => new SqlConstantExpression(propertyName, _textTypeMapping ??= _typeMappingSource.FindMapping(typeof(string))),
+ { ArrayIndex: SqlExpression arrayIndex } => arrayIndex,
+ _ => throw new UnreachableException()
+ })
+ .ToList();
+
///
/// 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
diff --git a/src/EFCore.PG/Query/Internal/NpgsqlQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.PG/Query/Internal/NpgsqlQueryableMethodTranslatingExpressionVisitor.cs
index b8d9fc2a6..47ddb2038 100644
--- a/src/EFCore.PG/Query/Internal/NpgsqlQueryableMethodTranslatingExpressionVisitor.cs
+++ b/src/EFCore.PG/Query/Internal/NpgsqlQueryableMethodTranslatingExpressionVisitor.cs
@@ -1046,6 +1046,8 @@ protected override bool IsNaturallyOrdered(SelectExpression selectExpression)
[{ Expression: ColumnExpression { Name: "ordinality", TableAlias: var orderingTableAlias } }]
&& orderingTableAlias == unnest.Alias);
+ #region ExecuteUpdate
+
///
/// 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
@@ -1100,6 +1102,119 @@ protected override bool IsValidSelectExpressionForExecuteUpdate(
return true;
}
+ ///
+ /// 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.
+ ///
+ protected override bool TrySerializeScalarToJson(
+ JsonScalarExpression target,
+ SqlExpression value,
+ [NotNullWhen(true)] out SqlExpression? jsonValue)
+ {
+ var jsonTypeMapping = ((ColumnExpression)target.Json).TypeMapping!;
+
+ if (
+ // The base implementation doesn't handle serializing arbitrary SQL expressions to JSON, since that's
+ // database-specific. In PostgreSQL we simply do this by wrapping any expression in to_jsonb().
+ !base.TrySerializeScalarToJson(target, value, out jsonValue)
+ // In addition, for string, numeric and bool, the base implementation simply returns the value as-is, since most databases allow
+ // passing these native types directly to their JSON partial update function. In PostgreSQL, jsonb_set() always requires jsonb,
+ // so we wrap those expression with to_jsonb() as well.
+ || jsonValue.TypeMapping?.StoreType is not "jsonb" and not "json")
+ {
+ switch (value.TypeMapping!.StoreType)
+ {
+ case "jsonb" or "json":
+ jsonValue = value;
+ return true;
+
+ case "bytea":
+ value = _sqlExpressionFactory.Function(
+ "encode",
+ [value, _sqlExpressionFactory.Constant("base64")],
+ nullable: true,
+ argumentsPropagateNullability: [true, true],
+ typeof(string),
+ _typeMappingSource.FindMapping(typeof(string))!
+ );
+ break;
+ }
+
+ jsonValue = _sqlExpressionFactory.Function(
+ jsonTypeMapping.StoreType switch
+ {
+ "jsonb" => "to_jsonb",
+ "json" => "to_json",
+ _ => throw new UnreachableException()
+ },
+ // Make sure PG interprets constant values correctly by adding explicit typing based on the target property's type mapping.
+ // Note that we can only be here for scalar properties, for structural types we always already get a jsonb/json value
+ // and don't need to add to_jsonb/to_json.
+ [value is SqlConstantExpression ? _sqlExpressionFactory.Convert(value, target.Type, target.TypeMapping) : value],
+ nullable: true,
+ argumentsPropagateNullability: [true],
+ typeof(string),
+ jsonTypeMapping);
+ }
+
+ return true;
+ }
+
+ ///
+ /// 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.
+ ///
+ protected override SqlExpression? GenerateJsonPartialUpdateSetter(
+ Expression target,
+ SqlExpression value,
+ ref SqlExpression? existingSetterValue)
+ {
+ var (jsonColumn, path) = target switch
+ {
+ JsonScalarExpression j => ((ColumnExpression)j.Json, j.Path),
+ JsonQueryExpression j => (j.JsonColumn, j.Path),
+
+ _ => throw new UnreachableException(),
+ };
+
+ var jsonSet = _sqlExpressionFactory.Function(
+ jsonColumn.TypeMapping?.StoreType switch
+ {
+ "jsonb" => "jsonb_set",
+ "json" => "json_set",
+ _ => throw new UnreachableException()
+ },
+ arguments:
+ [
+ existingSetterValue ?? jsonColumn,
+ // Hack: Rendering of JSONPATH strings happens in value generation. We can have a special expression for modify to hold the
+ // IReadOnlyList (just like Json{Scalar,Query}Expression), but instead we do the slight hack of packaging it
+ // as a constant argument; it will be unpacked and handled in SQL generation.
+ _sqlExpressionFactory.Constant(path, RelationalTypeMapping.NullMapping),
+ value
+ ],
+ nullable: true,
+ argumentsPropagateNullability: [true, true, true],
+ typeof(string),
+ jsonColumn.TypeMapping);
+
+ if (existingSetterValue is null)
+ {
+ return jsonSet;
+ }
+ else
+ {
+ existingSetterValue = jsonSet;
+ return null;
+ }
+ }
+
+ #endregion ExecuteUpdate
+
///
/// 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
diff --git a/test/EFCore.PG.FunctionalTests/BulkUpdates/ComplexTypeBulkUpdatesNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/BulkUpdates/ComplexTypeBulkUpdatesNpgsqlTest.cs
deleted file mode 100644
index 9e9c4b732..000000000
--- a/test/EFCore.PG.FunctionalTests/BulkUpdates/ComplexTypeBulkUpdatesNpgsqlTest.cs
+++ /dev/null
@@ -1,304 +0,0 @@
-namespace Microsoft.EntityFrameworkCore.BulkUpdates;
-
-public class ComplexTypeBulkUpdatesNpgsqlTest(
- ComplexTypeBulkUpdatesNpgsqlTest.ComplexTypeBulkUpdatesNpgsqlFixture fixture,
- ITestOutputHelper testOutputHelper)
- : ComplexTypeBulkUpdatesRelationalTestBase(fixture, testOutputHelper)
-{
- public override async Task Delete_entity_type_with_complex_type(bool async)
- {
- await base.Delete_entity_type_with_complex_type(async);
-
- AssertSql(
- """
-DELETE FROM "Customer" AS c
-WHERE c."Name" = 'Monty Elias'
-""");
- }
-
- public override async Task Delete_complex_type(bool async)
- {
- await base.Delete_complex_type(async);
-
- AssertSql();
- }
-
- public override async Task Update_property_inside_complex_type(bool async)
- {
- await base.Update_property_inside_complex_type(async);
-
- AssertExecuteUpdateSql(
- """
-@p='12345'
-
-UPDATE "Customer" AS c
-SET "ShippingAddress_ZipCode" = @p
-WHERE c."ShippingAddress_ZipCode" = 7728
-""");
- }
-
- public override async Task Update_property_inside_nested_complex_type(bool async)
- {
- await base.Update_property_inside_nested_complex_type(async);
-
- AssertExecuteUpdateSql(
- """
-@p='United States Modified'
-
-UPDATE "Customer" AS c
-SET "ShippingAddress_Country_FullName" = @p
-WHERE c."ShippingAddress_Country_Code" = 'US'
-""");
- }
-
- public override async Task Update_multiple_properties_inside_multiple_complex_types_and_on_entity_type(bool async)
- {
- await base.Update_multiple_properties_inside_multiple_complex_types_and_on_entity_type(async);
-
- AssertExecuteUpdateSql(
- """
-@p='54321'
-
-UPDATE "Customer" AS c
-SET "Name" = c."Name" || 'Modified',
- "ShippingAddress_ZipCode" = c."BillingAddress_ZipCode",
- "BillingAddress_ZipCode" = @p
-WHERE c."ShippingAddress_ZipCode" = 7728
-""");
- }
-
- public override async Task Update_projected_complex_type(bool async)
- {
- await base.Update_projected_complex_type(async);
-
- AssertExecuteUpdateSql(
- """
-@p='12345'
-
-UPDATE "Customer" AS c
-SET "ShippingAddress_ZipCode" = @p
-""");
- }
-
- public override async Task Update_multiple_projected_complex_types_via_anonymous_type(bool async)
- {
- await base.Update_multiple_projected_complex_types_via_anonymous_type(async);
-
- AssertExecuteUpdateSql(
- """
-@p='54321'
-
-UPDATE "Customer" AS c
-SET "ShippingAddress_ZipCode" = c."BillingAddress_ZipCode",
- "BillingAddress_ZipCode" = @p
-""");
- }
-
- public override async Task Update_projected_complex_type_via_OrderBy_Skip(bool async)
- {
- await base.Update_projected_complex_type_via_OrderBy_Skip(async);
-
- AssertExecuteUpdateSql();
- }
-
- public override async Task Update_complex_type_to_parameter(bool async)
- {
- await base.Update_complex_type_to_parameter(async);
-
- AssertExecuteUpdateSql(
- """
-@complex_type_p_AddressLine1='New AddressLine1'
-@complex_type_p_AddressLine2='New AddressLine2'
-@complex_type_p_Tags={ 'new_tag1'
-'new_tag2' } (DbType = Object)
-@complex_type_p_ZipCode='99999' (Nullable = true)
-@complex_type_p_Code='FR'
-@complex_type_p_FullName='France'
-
-UPDATE "Customer" AS c
-SET "ShippingAddress_AddressLine1" = @complex_type_p_AddressLine1,
- "ShippingAddress_AddressLine2" = @complex_type_p_AddressLine2,
- "ShippingAddress_Tags" = @complex_type_p_Tags,
- "ShippingAddress_ZipCode" = @complex_type_p_ZipCode,
- "ShippingAddress_Country_Code" = @complex_type_p_Code,
- "ShippingAddress_Country_FullName" = @complex_type_p_FullName
-""");
- }
-
- public override async Task Update_nested_complex_type_to_parameter(bool async)
- {
- await base.Update_nested_complex_type_to_parameter(async);
-
- AssertExecuteUpdateSql(
- """
-@complex_type_p_Code='FR'
-@complex_type_p_FullName='France'
-
-UPDATE "Customer" AS c
-SET "ShippingAddress_Country_Code" = @complex_type_p_Code,
- "ShippingAddress_Country_FullName" = @complex_type_p_FullName
-""");
- }
-
- public override async Task Update_complex_type_to_another_database_complex_type(bool async)
- {
- await base.Update_complex_type_to_another_database_complex_type(async);
-
- AssertExecuteUpdateSql(
- """
-UPDATE "Customer" AS c
-SET "ShippingAddress_AddressLine1" = c."BillingAddress_AddressLine1",
- "ShippingAddress_AddressLine2" = c."BillingAddress_AddressLine2",
- "ShippingAddress_Tags" = c."BillingAddress_Tags",
- "ShippingAddress_ZipCode" = c."BillingAddress_ZipCode",
- "ShippingAddress_Country_Code" = c."ShippingAddress_Country_Code",
- "ShippingAddress_Country_FullName" = c."ShippingAddress_Country_FullName"
-""");
- }
-
- public override async Task Update_complex_type_to_inline_without_lambda(bool async)
- {
- await base.Update_complex_type_to_inline_without_lambda(async);
-
- AssertExecuteUpdateSql(
- """
-@complex_type_p_AddressLine1='New AddressLine1'
-@complex_type_p_AddressLine2='New AddressLine2'
-@complex_type_p_Tags={ 'new_tag1'
-'new_tag2' } (DbType = Object)
-@complex_type_p_ZipCode='99999' (Nullable = true)
-@complex_type_p_Code='FR'
-@complex_type_p_FullName='France'
-
-UPDATE "Customer" AS c
-SET "ShippingAddress_AddressLine1" = @complex_type_p_AddressLine1,
- "ShippingAddress_AddressLine2" = @complex_type_p_AddressLine2,
- "ShippingAddress_Tags" = @complex_type_p_Tags,
- "ShippingAddress_ZipCode" = @complex_type_p_ZipCode,
- "ShippingAddress_Country_Code" = @complex_type_p_Code,
- "ShippingAddress_Country_FullName" = @complex_type_p_FullName
-""");
- }
-
- public override async Task Update_complex_type_to_inline_with_lambda(bool async)
- {
- await base.Update_complex_type_to_inline_with_lambda(async);
-
- AssertExecuteUpdateSql(
- """
-UPDATE "Customer" AS c
-SET "ShippingAddress_AddressLine1" = 'New AddressLine1',
- "ShippingAddress_AddressLine2" = 'New AddressLine2',
- "ShippingAddress_Tags" = ARRAY['new_tag1','new_tag2']::text[],
- "ShippingAddress_ZipCode" = 99999,
- "ShippingAddress_Country_Code" = 'FR',
- "ShippingAddress_Country_FullName" = 'France'
-""");
- }
-
- public override async Task Update_complex_type_to_another_database_complex_type_with_subquery(bool async)
- {
- await base.Update_complex_type_to_another_database_complex_type_with_subquery(async);
-
- AssertExecuteUpdateSql(
- """
-@p='1'
-
-UPDATE "Customer" AS c0
-SET "ShippingAddress_AddressLine1" = c1."BillingAddress_AddressLine1",
- "ShippingAddress_AddressLine2" = c1."BillingAddress_AddressLine2",
- "ShippingAddress_Tags" = c1."BillingAddress_Tags",
- "ShippingAddress_ZipCode" = c1."BillingAddress_ZipCode",
- "ShippingAddress_Country_Code" = c1."ShippingAddress_Country_Code",
- "ShippingAddress_Country_FullName" = c1."ShippingAddress_Country_FullName"
-FROM (
- SELECT c."Id", c."BillingAddress_AddressLine1", c."BillingAddress_AddressLine2", c."BillingAddress_Tags", c."BillingAddress_ZipCode", c."ShippingAddress_Country_Code", c."ShippingAddress_Country_FullName"
- FROM "Customer" AS c
- ORDER BY c."Id" NULLS FIRST
- OFFSET @p
-) AS c1
-WHERE c0."Id" = c1."Id"
-""");
- }
-
- public override async Task Update_collection_inside_complex_type(bool async)
- {
- await base.Update_collection_inside_complex_type(async);
-
- AssertExecuteUpdateSql(
- """
-@p={ 'new_tag1'
-'new_tag2' } (DbType = Object)
-
-UPDATE "Customer" AS c
-SET "ShippingAddress_Tags" = @p
-""");
- }
-
- public override async Task Update_complex_type_to_null(bool async)
- {
- await base.Update_complex_type_to_null(async);
-
- AssertExecuteUpdateSql(
- """
-UPDATE "Customer" AS c
-SET "OptionalAddress_AddressLine1" = NULL,
- "OptionalAddress_AddressLine2" = NULL,
- "OptionalAddress_Tags" = NULL,
- "OptionalAddress_ZipCode" = NULL,
- "OptionalAddress_Country_Code" = NULL,
- "OptionalAddress_Country_FullName" = NULL
-""");
- }
-
- public override async Task Update_complex_type_to_null_lambda(bool async)
- {
- await base.Update_complex_type_to_null_lambda(async);
-
- AssertExecuteUpdateSql(
- """
-UPDATE "Customer" AS c
-SET "OptionalAddress_AddressLine1" = NULL,
- "OptionalAddress_AddressLine2" = NULL,
- "OptionalAddress_Tags" = NULL,
- "OptionalAddress_ZipCode" = NULL,
- "OptionalAddress_Country_Code" = NULL,
- "OptionalAddress_Country_FullName" = NULL
-""");
- }
-
- public override async Task Update_complex_type_to_null_parameter(bool async)
- {
- await base.Update_complex_type_to_null_parameter(async);
-
- AssertExecuteUpdateSql(
- """
-UPDATE "Customer" AS c
-SET "OptionalAddress_AddressLine1" = NULL,
- "OptionalAddress_AddressLine2" = NULL,
- "OptionalAddress_Tags" = NULL,
- "OptionalAddress_ZipCode" = NULL,
- "OptionalAddress_Country_Code" = NULL,
- "OptionalAddress_Country_FullName" = NULL
-""");
- }
-
- [ConditionalFact]
- public virtual void Check_all_tests_overridden()
- => TestHelpers.AssertAllMethodsOverridden(GetType());
-
- private void AssertExecuteUpdateSql(params string[] expected)
- => Fixture.TestSqlLoggerFactory.AssertBaseline(expected, forUpdate: true);
-
- private void AssertSql(params string[] expected)
- => Fixture.TestSqlLoggerFactory.AssertBaseline(expected);
-
- protected void ClearLog()
- => Fixture.TestSqlLoggerFactory.Clear();
-
- public class ComplexTypeBulkUpdatesNpgsqlFixture : ComplexTypeBulkUpdatesRelationalFixtureBase
- {
- protected override ITestStoreFactory TestStoreFactory
- => NpgsqlTestStoreFactory.Instance;
- }
-}
diff --git a/test/EFCore.PG.FunctionalTests/EFCore.PG.FunctionalTests.csproj b/test/EFCore.PG.FunctionalTests/EFCore.PG.FunctionalTests.csproj
index 8050bc586..e56ed6fa2 100644
--- a/test/EFCore.PG.FunctionalTests/EFCore.PG.FunctionalTests.csproj
+++ b/test/EFCore.PG.FunctionalTests/EFCore.PG.FunctionalTests.csproj
@@ -4,18 +4,25 @@
Npgsql.EntityFrameworkCore.PostgreSQL.FunctionalTests
Microsoft.EntityFrameworkCore
true
+ $(NoWarn);NU1903;CS8618
+
+
+
+
+
diff --git a/test/EFCore.PG.FunctionalTests/Query/Associations/ComplexJson/ComplexJsonBulkUpdateNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/Associations/ComplexJson/ComplexJsonBulkUpdateNpgsqlTest.cs
new file mode 100644
index 000000000..014b5f5c0
--- /dev/null
+++ b/test/EFCore.PG.FunctionalTests/Query/Associations/ComplexJson/ComplexJsonBulkUpdateNpgsqlTest.cs
@@ -0,0 +1,335 @@
+namespace Microsoft.EntityFrameworkCore.Query.Associations.ComplexJson;
+
+public class ComplexJsonBulkUpdateNpgsqlTest(
+ ComplexJsonNpgsqlFixture fixture,
+ ITestOutputHelper testOutputHelper)
+ : ComplexJsonBulkUpdateRelationalTestBase(fixture, testOutputHelper)
+{
+ #region Delete
+
+ public override async Task Delete_entity_with_associations()
+ {
+ await base.Delete_entity_with_associations();
+
+ AssertSql(
+ """
+@deletableEntity_Name='?'
+
+DELETE FROM "RootEntity" AS r
+WHERE r."Name" = @deletableEntity_Name
+""");
+ }
+
+ public override async Task Delete_required_association()
+ {
+ await base.Delete_required_association();
+
+ AssertSql();
+ }
+
+ public override async Task Delete_optional_association()
+ {
+ await base.Delete_optional_association();
+
+ AssertSql();
+ }
+
+ #endregion Delete
+
+ #region Update properties
+
+ public override async Task Update_property_inside_association()
+ {
+ await base.Update_property_inside_association();
+
+ AssertExecuteUpdateSql(
+ """
+@p='?'
+
+UPDATE "RootEntity" AS r
+SET "RequiredRelated" = jsonb_set(r."RequiredRelated", '{String}', to_jsonb(@p))
+""");
+ }
+
+ public override async Task Update_property_inside_association_with_special_chars()
+ {
+ await base.Update_property_inside_association_with_special_chars();
+
+ AssertExecuteUpdateSql(
+ """
+UPDATE "RootEntity" AS r
+SET "RequiredRelated" = jsonb_set(r."RequiredRelated", '{String}', to_jsonb('{ Some other/JSON:like text though it [isn''t]: ממש ממש לאéèéè }'::text))
+WHERE (r."RequiredRelated" ->> 'String') = '{ this may/look:like JSON but it [isn''t]: ממש ממש לאéèéè }'
+""");
+ }
+
+ public override async Task Update_property_inside_nested()
+ {
+ await base.Update_property_inside_nested();
+
+ AssertExecuteUpdateSql(
+ """
+@p='?'
+
+UPDATE "RootEntity" AS r
+SET "RequiredRelated" = jsonb_set(r."RequiredRelated", '{RequiredNested,String}', to_jsonb(@p))
+""");
+ }
+
+ public override async Task Update_property_on_projected_association()
+ {
+ await base.Update_property_on_projected_association();
+
+ AssertExecuteUpdateSql(
+ """
+@p='?'
+
+UPDATE "RootEntity" AS r
+SET "RequiredRelated" = jsonb_set(r."RequiredRelated", '{String}', to_jsonb(@p))
+""");
+ }
+
+ public override async Task Update_property_on_projected_association_with_OrderBy_Skip()
+ {
+ await base.Update_property_on_projected_association_with_OrderBy_Skip();
+
+ AssertExecuteUpdateSql();
+ }
+
+ #endregion Update properties
+
+ #region Update association
+
+ public override async Task Update_association_to_parameter()
+ {
+ await base.Update_association_to_parameter();
+
+ AssertExecuteUpdateSql(
+ """
+@complex_type_p='?' (DbType = Object)
+
+UPDATE "RootEntity" AS r
+SET "RequiredRelated" = @complex_type_p
+""");
+ }
+
+ public override async Task Update_nested_association_to_parameter()
+ {
+ await base.Update_nested_association_to_parameter();
+
+ AssertExecuteUpdateSql(
+ """
+@complex_type_p='?' (DbType = Object)
+
+UPDATE "RootEntity" AS r
+SET "RequiredRelated" = jsonb_set(r."RequiredRelated", '{RequiredNested}', @complex_type_p)
+""");
+ }
+
+ public override async Task Update_association_to_another_association()
+ {
+ await base.Update_association_to_another_association();
+
+ AssertExecuteUpdateSql(
+ """
+UPDATE "RootEntity" AS r
+SET "OptionalRelated" = r."RequiredRelated"
+""");
+ }
+
+ public override async Task Update_nested_association_to_another_nested_association()
+ {
+ await base.Update_nested_association_to_another_nested_association();
+
+ AssertExecuteUpdateSql(
+ """
+UPDATE "RootEntity" AS r
+SET "RequiredRelated" = jsonb_set(r."RequiredRelated", '{OptionalNested}', r."RequiredRelated" -> 'RequiredNested')
+""");
+ }
+
+ public override async Task Update_association_to_inline()
+ {
+ await base.Update_association_to_inline();
+
+ AssertExecuteUpdateSql(
+ """
+@complex_type_p='?' (DbType = Object)
+
+UPDATE "RootEntity" AS r
+SET "RequiredRelated" = @complex_type_p
+""");
+ }
+
+ public override async Task Update_association_to_inline_with_lambda()
+ {
+ await base.Update_association_to_inline_with_lambda();
+
+ AssertExecuteUpdateSql(
+ """
+UPDATE "RootEntity" AS r
+SET "RequiredRelated" = '{"Id":0,"Int":70,"Name":"Updated related name","String":"Updated related string","NestedCollection":[],"OptionalNested":null,"RequiredNested":{"Id":0,"Int":80,"Name":"Updated nested name","String":"Updated nested string"}}'
+""");
+ }
+
+ public override async Task Update_nested_association_to_inline_with_lambda()
+ {
+ await base.Update_nested_association_to_inline_with_lambda();
+
+ AssertExecuteUpdateSql(
+ """
+UPDATE "RootEntity" AS r
+SET "RequiredRelated" = jsonb_set(r."RequiredRelated", '{RequiredNested}', '{"Id":0,"Int":80,"Name":"Updated nested name","String":"Updated nested string"}')
+""");
+ }
+
+ public override async Task Update_association_to_null()
+ {
+ await base.Update_association_to_null();
+
+ AssertExecuteUpdateSql(
+ """
+UPDATE "RootEntity" AS r
+SET "OptionalRelated" = NULL
+""");
+ }
+
+ public override async Task Update_association_to_null_with_lambda()
+ {
+ await base.Update_association_to_null_with_lambda();
+
+ AssertExecuteUpdateSql(
+ """
+UPDATE "RootEntity" AS r
+SET "OptionalRelated" = NULL
+""");
+ }
+
+ public override async Task Update_association_to_null_parameter()
+ {
+ await base.Update_association_to_null_parameter();
+
+ AssertExecuteUpdateSql(
+ """
+UPDATE "RootEntity" AS r
+SET "OptionalRelated" = NULL
+""");
+ }
+
+ #endregion Update association
+
+ #region Update collection
+
+ public override async Task Update_collection_to_parameter()
+ {
+ await base.Update_collection_to_parameter();
+
+ AssertExecuteUpdateSql(
+ """
+@complex_type_p='?' (DbType = Object)
+
+UPDATE "RootEntity" AS r
+SET "RelatedCollection" = @complex_type_p
+""");
+ }
+
+ public override async Task Update_nested_collection_to_parameter()
+ {
+ await base.Update_nested_collection_to_parameter();
+
+ AssertExecuteUpdateSql(
+ """
+@complex_type_p='?' (DbType = Object)
+
+UPDATE "RootEntity" AS r
+SET "RequiredRelated" = jsonb_set(r."RequiredRelated", '{NestedCollection}', @complex_type_p)
+""");
+ }
+
+ public override async Task Update_nested_collection_to_inline_with_lambda()
+ {
+ await base.Update_nested_collection_to_inline_with_lambda();
+
+ AssertExecuteUpdateSql(
+ """
+UPDATE "RootEntity" AS r
+SET "RequiredRelated" = jsonb_set(r."RequiredRelated", '{NestedCollection}', '[{"Id":0,"Int":80,"Name":"Updated nested name1","String":"Updated nested string1"},{"Id":0,"Int":81,"Name":"Updated nested name2","String":"Updated nested string2"}]')
+""");
+ }
+
+ public override async Task Update_nested_collection_to_another_nested_collection()
+ {
+ await base.Update_nested_collection_to_another_nested_collection();
+
+ AssertExecuteUpdateSql(
+ """
+UPDATE "RootEntity" AS r
+SET "RequiredRelated" = jsonb_set(r."RequiredRelated", '{NestedCollection}', r."OptionalRelated" -> 'NestedCollection')
+WHERE (r."OptionalRelated") IS NOT NULL
+""");
+ }
+
+ public override async Task Update_collection_referencing_the_original_collection()
+ {
+ await base.Update_collection_referencing_the_original_collection();
+
+ AssertExecuteUpdateSql();
+ }
+
+ #endregion Update collection
+
+ #region Multiple updates
+
+ public override async Task Update_multiple_properties_inside_same_association()
+ {
+ await base.Update_multiple_properties_inside_same_association();
+
+ // Note that since two properties within the same JSON column are updated, SQL Server 2025 modify
+ // is not used (it only supports modifying a single property)
+ AssertExecuteUpdateSql(
+ """
+@p='?'
+@p0='?' (DbType = Int32)
+
+UPDATE "RootEntity" AS r
+SET "RequiredRelated" = jsonb_set(jsonb_set(r."RequiredRelated", '{String}', to_jsonb(@p)), '{Int}', to_jsonb(@p0))
+""");
+ }
+
+ public override async Task Update_multiple_properties_inside_associations_and_on_entity_type()
+ {
+ await base.Update_multiple_properties_inside_associations_and_on_entity_type();
+
+ AssertExecuteUpdateSql(
+ """
+@p='?'
+
+UPDATE "RootEntity" AS r
+SET "Name" = r."Name" || 'Modified',
+ "RequiredRelated" = jsonb_set(r."RequiredRelated", '{String}', to_jsonb(r."OptionalRelated" ->> 'String')),
+ "OptionalRelated" = jsonb_set(r."OptionalRelated", '{RequiredNested,String}', to_jsonb(@p))
+WHERE (r."OptionalRelated") IS NOT NULL
+""");
+ }
+
+ public override async Task Update_multiple_projected_associations_via_anonymous_type()
+ {
+ await base.Update_multiple_projected_associations_via_anonymous_type();
+
+ AssertExecuteUpdateSql(
+ """
+@p='?'
+
+UPDATE "RootEntity" AS r
+SET "RequiredRelated" = jsonb_set(r."RequiredRelated", '{String}', to_jsonb(r."OptionalRelated" ->> 'String')),
+ "OptionalRelated" = jsonb_set(r."OptionalRelated", '{String}', to_jsonb(@p))
+WHERE (r."OptionalRelated") IS NOT NULL
+""");
+ }
+
+ #endregion Multiple updates
+
+ [ConditionalFact]
+ public virtual void Check_all_tests_overridden()
+ => TestHelpers.AssertAllMethodsOverridden(GetType());
+}
diff --git a/test/EFCore.PG.FunctionalTests/Query/Associations/ComplexTableSplitting/ComplexTableSplittingBulkUpdateNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/Associations/ComplexTableSplitting/ComplexTableSplittingBulkUpdateNpgsqlTest.cs
new file mode 100644
index 000000000..26d801d95
--- /dev/null
+++ b/test/EFCore.PG.FunctionalTests/Query/Associations/ComplexTableSplitting/ComplexTableSplittingBulkUpdateNpgsqlTest.cs
@@ -0,0 +1,415 @@
+
+namespace Microsoft.EntityFrameworkCore.Query.Associations.ComplexTableSplitting;
+
+public class ComplexTableSplittingBulkUpdateNpgsqlTest(
+ ComplexTableSplittingNpgsqlFixture fixture,
+ ITestOutputHelper testOutputHelper)
+ : ComplexTableSplittingBulkUpdateRelationalTestBase(fixture, testOutputHelper)
+{
+ #region Delete
+
+ public override async Task Delete_entity_with_associations()
+ {
+ await base.Delete_entity_with_associations();
+
+ AssertSql(
+ """
+@deletableEntity_Name='?'
+
+DELETE FROM "RootEntity" AS r
+WHERE r."Name" = @deletableEntity_Name
+""");
+ }
+
+ public override async Task Delete_required_association()
+ {
+ await base.Delete_required_association();
+
+ AssertSql();
+ }
+
+ public override async Task Delete_optional_association()
+ {
+ await base.Delete_optional_association();
+
+ AssertSql();
+ }
+
+ #endregion Delete
+
+ #region Update properties
+
+ public override async Task Update_property_inside_association()
+ {
+ await base.Update_property_inside_association();
+
+ AssertExecuteUpdateSql(
+ """
+@p='?'
+
+UPDATE "RootEntity" AS r
+SET "RequiredRelated_String" = @p
+""");
+ }
+
+ public override async Task Update_property_inside_association_with_special_chars()
+ {
+ await base.Update_property_inside_association_with_special_chars();
+
+ AssertExecuteUpdateSql(
+ """
+UPDATE "RootEntity" AS r
+SET "RequiredRelated_String" = '{ Some other/JSON:like text though it [isn''t]: ממש ממש לאéèéè }'
+WHERE r."RequiredRelated_String" = '{ this may/look:like JSON but it [isn''t]: ממש ממש לאéèéè }'
+""");
+ }
+
+ public override async Task Update_property_inside_nested()
+ {
+ await base.Update_property_inside_nested();
+
+ AssertExecuteUpdateSql(
+ """
+@p='?'
+
+UPDATE "RootEntity" AS r
+SET "RequiredRelated_RequiredNested_String" = @p
+""");
+ }
+
+ public override async Task Update_property_on_projected_association()
+ {
+ await base.Update_property_on_projected_association();
+
+ AssertExecuteUpdateSql(
+ """
+@p='?'
+
+UPDATE "RootEntity" AS r
+SET "RequiredRelated_String" = @p
+""");
+ }
+
+ public override async Task Update_property_on_projected_association_with_OrderBy_Skip()
+ {
+ await base.Update_property_on_projected_association_with_OrderBy_Skip();
+
+ AssertExecuteUpdateSql();
+ }
+
+ #endregion Update properties
+
+ #region Update association
+
+ public override async Task Update_association_to_parameter()
+ {
+ await base.Update_association_to_parameter();
+
+ AssertExecuteUpdateSql(
+ """
+@complex_type_p_Id='?' (DbType = Int32)
+@complex_type_p_Int='?' (DbType = Int32)
+@complex_type_p_Name='?'
+@complex_type_p_String='?'
+
+UPDATE "RootEntity" AS r
+SET "RequiredRelated_Id" = @complex_type_p_Id,
+ "RequiredRelated_Int" = @complex_type_p_Int,
+ "RequiredRelated_Name" = @complex_type_p_Name,
+ "RequiredRelated_String" = @complex_type_p_String,
+ "RequiredRelated_OptionalNested_Id" = @complex_type_p_Id,
+ "RequiredRelated_OptionalNested_Int" = @complex_type_p_Int,
+ "RequiredRelated_OptionalNested_Name" = @complex_type_p_Name,
+ "RequiredRelated_OptionalNested_String" = @complex_type_p_String,
+ "RequiredRelated_RequiredNested_Id" = @complex_type_p_Id,
+ "RequiredRelated_RequiredNested_Int" = @complex_type_p_Int,
+ "RequiredRelated_RequiredNested_Name" = @complex_type_p_Name,
+ "RequiredRelated_RequiredNested_String" = @complex_type_p_String
+""");
+ }
+
+ public override async Task Update_nested_association_to_parameter()
+ {
+ await base.Update_nested_association_to_parameter();
+
+ AssertExecuteUpdateSql(
+ """
+@complex_type_p_Id='?' (DbType = Int32)
+@complex_type_p_Int='?' (DbType = Int32)
+@complex_type_p_Name='?'
+@complex_type_p_String='?'
+
+UPDATE "RootEntity" AS r
+SET "RequiredRelated_RequiredNested_Id" = @complex_type_p_Id,
+ "RequiredRelated_RequiredNested_Int" = @complex_type_p_Int,
+ "RequiredRelated_RequiredNested_Name" = @complex_type_p_Name,
+ "RequiredRelated_RequiredNested_String" = @complex_type_p_String
+""");
+ }
+
+ public override async Task Update_association_to_another_association()
+ {
+ await base.Update_association_to_another_association();
+
+ AssertExecuteUpdateSql(
+ """
+UPDATE "RootEntity" AS r
+SET "OptionalRelated_Id" = r."RequiredRelated_Id",
+ "OptionalRelated_Int" = r."RequiredRelated_Int",
+ "OptionalRelated_Name" = r."RequiredRelated_Name",
+ "OptionalRelated_String" = r."RequiredRelated_String",
+ "OptionalRelated_OptionalNested_Id" = r."OptionalRelated_OptionalNested_Id",
+ "OptionalRelated_OptionalNested_Int" = r."OptionalRelated_OptionalNested_Int",
+ "OptionalRelated_OptionalNested_Name" = r."OptionalRelated_OptionalNested_Name",
+ "OptionalRelated_OptionalNested_String" = r."OptionalRelated_OptionalNested_String",
+ "OptionalRelated_RequiredNested_Id" = r."OptionalRelated_RequiredNested_Id",
+ "OptionalRelated_RequiredNested_Int" = r."OptionalRelated_RequiredNested_Int",
+ "OptionalRelated_RequiredNested_Name" = r."OptionalRelated_RequiredNested_Name",
+ "OptionalRelated_RequiredNested_String" = r."OptionalRelated_RequiredNested_String"
+""");
+ }
+
+ public override async Task Update_nested_association_to_another_nested_association()
+ {
+ await base.Update_nested_association_to_another_nested_association();
+
+ AssertExecuteUpdateSql(
+ """
+UPDATE "RootEntity" AS r
+SET "RequiredRelated_OptionalNested_Id" = r."RequiredRelated_RequiredNested_Id",
+ "RequiredRelated_OptionalNested_Int" = r."RequiredRelated_RequiredNested_Int",
+ "RequiredRelated_OptionalNested_Name" = r."RequiredRelated_RequiredNested_Name",
+ "RequiredRelated_OptionalNested_String" = r."RequiredRelated_RequiredNested_String"
+""");
+ }
+
+ public override async Task Update_association_to_inline()
+ {
+ await base.Update_association_to_inline();
+
+ AssertExecuteUpdateSql(
+ """
+@complex_type_p_Id='?' (DbType = Int32)
+@complex_type_p_Int='?' (DbType = Int32)
+@complex_type_p_Name='?'
+@complex_type_p_String='?'
+
+UPDATE "RootEntity" AS r
+SET "RequiredRelated_Id" = @complex_type_p_Id,
+ "RequiredRelated_Int" = @complex_type_p_Int,
+ "RequiredRelated_Name" = @complex_type_p_Name,
+ "RequiredRelated_String" = @complex_type_p_String,
+ "RequiredRelated_OptionalNested_Id" = @complex_type_p_Id,
+ "RequiredRelated_OptionalNested_Int" = @complex_type_p_Int,
+ "RequiredRelated_OptionalNested_Name" = @complex_type_p_Name,
+ "RequiredRelated_OptionalNested_String" = @complex_type_p_String,
+ "RequiredRelated_RequiredNested_Id" = @complex_type_p_Id,
+ "RequiredRelated_RequiredNested_Int" = @complex_type_p_Int,
+ "RequiredRelated_RequiredNested_Name" = @complex_type_p_Name,
+ "RequiredRelated_RequiredNested_String" = @complex_type_p_String
+""");
+ }
+
+ public override async Task Update_association_to_inline_with_lambda()
+ {
+ await base.Update_association_to_inline_with_lambda();
+
+ AssertExecuteUpdateSql(
+ """
+UPDATE "RootEntity" AS r
+SET "RequiredRelated_Id" = 0,
+ "RequiredRelated_Int" = 70,
+ "RequiredRelated_Name" = 'Updated related name',
+ "RequiredRelated_String" = 'Updated related string',
+ "RequiredRelated_OptionalNested_Id" = NULL,
+ "RequiredRelated_OptionalNested_Int" = NULL,
+ "RequiredRelated_OptionalNested_Name" = NULL,
+ "RequiredRelated_OptionalNested_String" = NULL,
+ "RequiredRelated_RequiredNested_Id" = 0,
+ "RequiredRelated_RequiredNested_Int" = 80,
+ "RequiredRelated_RequiredNested_Name" = 'Updated nested name',
+ "RequiredRelated_RequiredNested_String" = 'Updated nested string'
+""");
+ }
+
+ public override async Task Update_nested_association_to_inline_with_lambda()
+ {
+ await base.Update_nested_association_to_inline_with_lambda();
+
+ AssertExecuteUpdateSql(
+ """
+UPDATE "RootEntity" AS r
+SET "RequiredRelated_RequiredNested_Id" = 0,
+ "RequiredRelated_RequiredNested_Int" = 80,
+ "RequiredRelated_RequiredNested_Name" = 'Updated nested name',
+ "RequiredRelated_RequiredNested_String" = 'Updated nested string'
+""");
+ }
+
+ public override async Task Update_association_to_null()
+ {
+ await base.Update_association_to_null();
+
+ AssertExecuteUpdateSql(
+ """
+UPDATE "RootEntity" AS r
+SET "OptionalRelated_Id" = NULL,
+ "OptionalRelated_Int" = NULL,
+ "OptionalRelated_Name" = NULL,
+ "OptionalRelated_String" = NULL,
+ "OptionalRelated_OptionalNested_Id" = NULL,
+ "OptionalRelated_OptionalNested_Int" = NULL,
+ "OptionalRelated_OptionalNested_Name" = NULL,
+ "OptionalRelated_OptionalNested_String" = NULL,
+ "OptionalRelated_RequiredNested_Id" = NULL,
+ "OptionalRelated_RequiredNested_Int" = NULL,
+ "OptionalRelated_RequiredNested_Name" = NULL,
+ "OptionalRelated_RequiredNested_String" = NULL
+""");
+ }
+
+ public override async Task Update_association_to_null_with_lambda()
+ {
+ await base.Update_association_to_null_with_lambda();
+
+ AssertExecuteUpdateSql(
+ """
+UPDATE "RootEntity" AS r
+SET "OptionalRelated_Id" = NULL,
+ "OptionalRelated_Int" = NULL,
+ "OptionalRelated_Name" = NULL,
+ "OptionalRelated_String" = NULL,
+ "OptionalRelated_OptionalNested_Id" = NULL,
+ "OptionalRelated_OptionalNested_Int" = NULL,
+ "OptionalRelated_OptionalNested_Name" = NULL,
+ "OptionalRelated_OptionalNested_String" = NULL,
+ "OptionalRelated_RequiredNested_Id" = NULL,
+ "OptionalRelated_RequiredNested_Int" = NULL,
+ "OptionalRelated_RequiredNested_Name" = NULL,
+ "OptionalRelated_RequiredNested_String" = NULL
+""");
+ }
+
+ public override async Task Update_association_to_null_parameter()
+ {
+ await base.Update_association_to_null_parameter();
+
+ AssertExecuteUpdateSql(
+ """
+UPDATE "RootEntity" AS r
+SET "OptionalRelated_Id" = NULL,
+ "OptionalRelated_Int" = NULL,
+ "OptionalRelated_Name" = NULL,
+ "OptionalRelated_String" = NULL,
+ "OptionalRelated_OptionalNested_Id" = NULL,
+ "OptionalRelated_OptionalNested_Int" = NULL,
+ "OptionalRelated_OptionalNested_Name" = NULL,
+ "OptionalRelated_OptionalNested_String" = NULL,
+ "OptionalRelated_RequiredNested_Id" = NULL,
+ "OptionalRelated_RequiredNested_Int" = NULL,
+ "OptionalRelated_RequiredNested_Name" = NULL,
+ "OptionalRelated_RequiredNested_String" = NULL
+""");
+ }
+
+ #endregion Update association
+
+ #region Update collection
+
+ public override async Task Update_collection_to_parameter()
+ {
+ await base.Update_collection_to_parameter();
+
+ AssertExecuteUpdateSql(
+);
+ }
+
+ public override async Task Update_nested_collection_to_parameter()
+ {
+ await base.Update_nested_collection_to_parameter();
+
+ AssertExecuteUpdateSql(
+);
+ }
+
+ public override async Task Update_nested_collection_to_inline_with_lambda()
+ {
+ await base.Update_nested_collection_to_inline_with_lambda();
+
+ AssertExecuteUpdateSql(
+);
+ }
+
+ public override async Task Update_nested_collection_to_another_nested_collection()
+ {
+ await base.Update_nested_collection_to_another_nested_collection();
+
+ AssertExecuteUpdateSql(
+);
+ }
+
+ public override async Task Update_collection_referencing_the_original_collection()
+ {
+ await base.Update_collection_referencing_the_original_collection();
+
+ AssertExecuteUpdateSql();
+ }
+
+ #endregion Update collection
+
+ #region Multiple updates
+
+ public override async Task Update_multiple_properties_inside_same_association()
+ {
+ await base.Update_multiple_properties_inside_same_association();
+
+ // Note that since two properties within the same JSON column are updated, SQL Server 2025 modify
+ // is not used (it only supports modifying a single property)
+ AssertExecuteUpdateSql(
+ """
+@p='?'
+@p0='?' (DbType = Int32)
+
+UPDATE "RootEntity" AS r
+SET "RequiredRelated_String" = @p,
+ "RequiredRelated_Int" = @p0
+""");
+ }
+
+ public override async Task Update_multiple_properties_inside_associations_and_on_entity_type()
+ {
+ await base.Update_multiple_properties_inside_associations_and_on_entity_type();
+
+ AssertExecuteUpdateSql(
+ """
+@p='?'
+
+UPDATE "RootEntity" AS r
+SET "Name" = r."Name" || 'Modified',
+ "RequiredRelated_String" = r."OptionalRelated_String",
+ "OptionalRelated_RequiredNested_String" = @p
+WHERE r."OptionalRelated_Id" IS NOT NULL
+""");
+ }
+
+ public override async Task Update_multiple_projected_associations_via_anonymous_type()
+ {
+ await base.Update_multiple_projected_associations_via_anonymous_type();
+
+ AssertExecuteUpdateSql(
+ """
+@p='?'
+
+UPDATE "RootEntity" AS r
+SET "RequiredRelated_String" = r."OptionalRelated_String",
+ "OptionalRelated_String" = @p
+WHERE r."OptionalRelated_Id" IS NOT NULL
+""");
+ }
+
+ #endregion Multiple updates
+
+ [ConditionalFact]
+ public virtual void Check_all_tests_overridden()
+ => TestHelpers.AssertAllMethodsOverridden(GetType());
+}
diff --git a/test/EFCore.PG.FunctionalTests/StoreTypeNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/StoreTypeNpgsqlTest.cs
new file mode 100644
index 000000000..b5686259a
--- /dev/null
+++ b/test/EFCore.PG.FunctionalTests/StoreTypeNpgsqlTest.cs
@@ -0,0 +1,41 @@
+namespace Npgsql.EntityFrameworkCore.PostgreSQL;
+
+public class StoreTypeNpgsqlTest : StoreTypeRelationalTestBase
+{
+ public override Task DateTime_Unspecified()
+ => TestType(
+ new DateTime(2020, 1, 5, 12, 30, 45, DateTimeKind.Unspecified),
+ new DateTime(2022, 5, 3, 0, 0, 0, DateTimeKind.Unspecified),
+ onModelCreating: mb => mb.Entity>(b =>
+ {
+ // The PG provider maps DateTime properties to 'timestamp with time zone' by default, which requires
+ // Kind=Utc. Map to 'timestamp without time zone'.
+ b.Property(e => e.Value).HasColumnType("timestamp without time zone");
+ b.Property(e => e.OtherValue).HasColumnType("timestamp without time zone");
+ b.ComplexProperty(e => e.Container).Property(c => c.Value).HasColumnType("timestamp without time zone");
+ b.ComplexProperty(e => e.Container).Property(c => c.OtherValue).HasColumnType("timestamp without time zone");
+ }));
+
+ public override Task DateTime_Local()
+ => TestType(
+ new DateTime(2020, 1, 5, 12, 30, 45, DateTimeKind.Local),
+ new DateTime(2022, 5, 3, 0, 0, 0, DateTimeKind.Local),
+ onModelCreating: mb => mb.Entity>(b =>
+ {
+ // The PG provider maps DateTime properties to 'timestamp with time zone' by default, which requires
+ // Kind=Utc. Map to 'timestamp without time zone'.
+ b.Property(e => e.Value).HasColumnType("timestamp without time zone");
+ b.Property(e => e.OtherValue).HasColumnType("timestamp without time zone");
+ b.ComplexProperty(e => e.Container).Property(c => c.Value).HasColumnType("timestamp without time zone");
+ b.ComplexProperty(e => e.Container).Property(c => c.OtherValue).HasColumnType("timestamp without time zone");
+ }));
+
+ // PostgreSQL does not support persisting the offset, and so the provider accepts only DateTimeOffsets
+ // with offset zero.
+ public override Task DateTimeOffset()
+ => TestType(
+ new DateTimeOffset(2020, 1, 5, 12, 30, 45, TimeSpan.FromHours(0)),
+ new DateTimeOffset(2021, 1, 5, 12, 30, 45, TimeSpan.FromHours(0)));
+
+ protected override ITestStoreFactory TestStoreFactory => NpgsqlTestStoreFactory.Instance;
+}
diff --git a/test/EFCore.PG.Tests/EFCore.PG.Tests.csproj b/test/EFCore.PG.Tests/EFCore.PG.Tests.csproj
index 201184440..d194b1664 100644
--- a/test/EFCore.PG.Tests/EFCore.PG.Tests.csproj
+++ b/test/EFCore.PG.Tests/EFCore.PG.Tests.csproj
@@ -4,19 +4,28 @@
Npgsql.EntityFrameworkCore.PostgreSQL.Tests
Npgsql.EntityFrameworkCore.PostgreSQL
disable
+ $(NoWarn);NU1903
+
+
+
+
+
+
+