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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.DS_Store
0.DS_Store
*.resources
*.suo
*.user
Expand All @@ -16,3 +16,4 @@ artifacts/
*.ide/
TestResult.xml
.dotnet
.vscode/
4 changes: 2 additions & 2 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project>
<PropertyGroup>
<EFCoreVersion>10.0.0-preview.7.25380.108</EFCoreVersion>
<MicrosoftExtensionsVersion>10.0.0-preview.7.25380.108</MicrosoftExtensionsVersion>
<EFCoreVersion>10.0.0-rc.1.25416.111</EFCoreVersion>
<MicrosoftExtensionsVersion>10.0.0-rc.1.25416.111</MicrosoftExtensionsVersion>
<NpgsqlVersion>9.0.3</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": "10.0.100-preview.5.25277.114",
"version": "10.0.100-preview.7.25380.108",
"rollForward": "latestMajor",
"allowPrerelease": true
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,16 @@ public NpgsqlParameterBasedSqlProcessor(
/// 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.
/// </summary>
public override Expression Process(Expression queryExpression, CacheSafeParameterFacade parametersFacade)
public override Expression Process(Expression queryExpression, ParametersCacheDecorator parametersDecorator)
{
queryExpression = base.Process(queryExpression, parametersFacade);
queryExpression = base.Process(queryExpression, parametersDecorator);

queryExpression = new NpgsqlDeleteConvertingExpressionVisitor().Process(queryExpression);

return queryExpression;
}

/// <inheritdoc />
protected override Expression ProcessSqlNullability(Expression selectExpression, CacheSafeParameterFacade parametersFacade)
=> new NpgsqlSqlNullabilityProcessor(Dependencies, Parameters).Process(selectExpression, parametersFacade);
protected override Expression ProcessSqlNullability(Expression selectExpression, ParametersCacheDecorator parametersDecorator)
=> new NpgsqlSqlNullabilityProcessor(Dependencies, Parameters).Process(selectExpression, parametersDecorator);
}
2 changes: 1 addition & 1 deletion src/EFCore.PG/Query/Internal/NpgsqlQuerySqlGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1069,7 +1069,7 @@ protected override Expression VisitJsonScalar(JsonScalarExpression jsonScalarExp
{
// 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 NpgsqlOwnedJsonTypeMapping:
case NpgsqlStructuralJsonTypeMapping:
GenerateJsonPath(returnsText: false);
break;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -199,14 +199,14 @@ protected override ShapedQueryExpression TransformJsonQueryToTable(JsonQueryExpr
};

var jsonTypeMapping = jsonQueryExpression.JsonColumn.TypeMapping!;
Check.DebugAssert(jsonTypeMapping is NpgsqlOwnedJsonTypeMapping, "JSON column has a non-JSON mapping");
Check.DebugAssert(jsonTypeMapping is NpgsqlStructuralJsonTypeMapping, "JSON column has a non-JSON mapping");

// We now add all of projected entity's the properties and navigations into the jsonb_to_recordset's AS clause, which defines the
// names and types of columns to come out of the JSON fragments.
var columnInfos = new List<PgTableValuedFunctionExpression.ColumnInfo>();

// We're only interested in properties which actually exist in the JSON, filter out uninteresting shadow keys
foreach (var property in GetAllPropertiesInHierarchy(jsonQueryExpression.EntityType))
foreach (var property in jsonQueryExpression.StructuralType.GetPropertiesInHierarchy())
{
if (property.GetJsonPropertyName() is string jsonPropertyName)
{
Expand All @@ -218,18 +218,37 @@ protected override ShapedQueryExpression TransformJsonQueryToTable(JsonQueryExpr
}
}

// Navigations represent nested JSON owned entities, which we also add to the AS clause, but with the JSON type.
foreach (var navigation in GetAllNavigationsInHierarchy(jsonQueryExpression.EntityType)
.Where(
n => n.ForeignKey.IsOwnership
&& n.TargetEntityType.IsMappedToJson()
&& n.ForeignKey.PrincipalToDependent == n))
switch (jsonQueryExpression.StructuralType)
{
var jsonNavigationName = navigation.TargetEntityType.GetJsonPropertyName();
Check.DebugAssert(jsonNavigationName is not null, $"No JSON property name for navigation {navigation.Name}");
case IEntityType entityType:
foreach (var navigation in entityType.GetNavigationsInHierarchy()
.Where(n => n.ForeignKey.IsOwnership
&& n.TargetEntityType.IsMappedToJson()
&& n.ForeignKey.PrincipalToDependent == n))
{
var jsonNavigationName = navigation.TargetEntityType.GetJsonPropertyName();
Check.DebugAssert(jsonNavigationName is not null, $"No JSON property name for navigation {navigation.Name}");

columnInfos.Add(
new PgTableValuedFunctionExpression.ColumnInfo { Name = jsonNavigationName, TypeMapping = jsonTypeMapping });
}

break;

case IComplexType complexType:
foreach (var complexProperty in complexType.GetComplexProperties())
{
var jsonPropertyName = complexProperty.ComplexType.GetJsonPropertyName();
Check.DebugAssert(jsonPropertyName is not null, $"No JSON property name for complex property {complexProperty.Name}");

columnInfos.Add(
new PgTableValuedFunctionExpression.ColumnInfo { Name = jsonNavigationName, TypeMapping = jsonTypeMapping });
columnInfos.Add(
new PgTableValuedFunctionExpression.ColumnInfo { Name = jsonPropertyName, TypeMapping = jsonTypeMapping });
}

break;

default:
throw new UnreachableException();
}

// json_to_recordset requires the nested JSON document - it does not accept a path within a containing JSON document (like SQL
Expand All @@ -254,21 +273,12 @@ protected override ShapedQueryExpression TransformJsonQueryToTable(JsonQueryExpr
return new ShapedQueryExpression(
selectExpression,
new RelationalStructuralTypeShaperExpression(
jsonQueryExpression.EntityType,
jsonQueryExpression.StructuralType,
new ProjectionBindingExpression(
selectExpression,
new ProjectionMember(),
typeof(ValueBuffer)),
false));

// TODO: Move these to IEntityType?
static IEnumerable<IProperty> GetAllPropertiesInHierarchy(IEntityType entityType)
=> entityType.GetAllBaseTypes().Concat(entityType.GetDerivedTypesInclusive())
.SelectMany(t => t.GetDeclaredProperties());

static IEnumerable<INavigation> GetAllNavigationsInHierarchy(IEntityType entityType)
=> entityType.GetAllBaseTypes().Concat(entityType.GetDerivedTypesInclusive())
.SelectMany(t => t.GetDeclaredNavigations());
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -737,5 +737,5 @@ private static bool MayContainNulls(SqlExpression arrayExpression)
// Note that we can check parameter values for null since we cache by the parameter nullability; but we cannot do the same for bool.
private bool IsNull(SqlExpression? expression)
=> expression is SqlConstantExpression { Value: null }
|| expression is SqlParameterExpression { Name: string parameterName } && ParametersFacade.IsParameterNull(parameterName);
|| expression is SqlParameterExpression { Name: string parameterName } && ParametersDecorator.IsNull(parameterName);
}
Original file line number Diff line number Diff line change
Expand Up @@ -300,16 +300,40 @@ when binaryExpression.Left.Type.UnwrapNullableType().FullName == "NodaTime.Local
// further JSON operations may need to be composed. However, when the value extracted is a JSON null, a non-NULL jsonb value is
// returned, and comparing that to relational NULL returns false.
// Pattern-match this and force the use of ->> by changing the mapping to be a scalar rather than an entity type.
case SqlUnaryExpression
case SqlBinaryExpression
{
OperatorType: ExpressionType.Equal or ExpressionType.NotEqual,
Operand: JsonScalarExpression { TypeMapping: NpgsqlOwnedJsonTypeMapping } operand
} unary:
Left: JsonScalarExpression { TypeMapping: NpgsqlStructuralJsonTypeMapping } operand,
Right: SqlConstantExpression { Value: null }
} binary:
{
return unary.Update(
return binary.Update(
new JsonScalarExpression(
operand.Json, operand.Path, operand.Type, _typeMappingSource.FindMapping("text"), operand.IsNullable),
binary.Right);
}
case SqlBinaryExpression
{
OperatorType: ExpressionType.Equal or ExpressionType.NotEqual,
Left: SqlConstantExpression { Value: null },
Right: JsonScalarExpression { TypeMapping: NpgsqlStructuralJsonTypeMapping } operand
} binary:
{
return binary.Update(
binary.Left,
new JsonScalarExpression(
operand.Json, operand.Path, operand.Type, _typeMappingSource.FindMapping("text"), operand.IsNullable));
}
// Unfortunately EF isn't consistent in its representation of X IS NULL in the SQL tree - sometimes it's a SqlUnaryExpression with Equals,
// sometimes it's an X = NULL SqlBinaryExpression that later gets transformed to SqlUnaryExpression, in SqlNullabilityProcessor. We recognize
// both of these here.
case SqlUnaryExpression
{
Operand: JsonScalarExpression { TypeMapping: NpgsqlStructuralJsonTypeMapping } operand
} unary:
return unary.Update(
new JsonScalarExpression(
operand.Json, operand.Path, operand.Type, _typeMappingSource.FindMapping("text"), operand.IsNullable));
}

return translation;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping;
/// <summary>
/// Supports the older Npgsql-specific JSON mapping, allowing mapping json/jsonb to text, to e.g.
/// <see cref="JsonElement" /> (weakly-typed mapping) or to arbitrary POCOs (but without them being modeled).
/// For the standard EF JSON support, which relies on owned entity modeling, see <see cref="NpgsqlOwnedJsonTypeMapping" />.
/// For the standard EF JSON support, which relies on owned entity modeling, see <see cref="NpgsqlStructuralJsonTypeMapping" />.
/// </summary>
public class NpgsqlJsonTypeMapping : NpgsqlTypeMapping
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping;

/// <summary>
/// Supports the standard EF JSON support, which relies on owned entity modeling.
/// Supports the standard EF JSON support, which relies on owned entity or complex type modeling.
/// See <see cref="NpgsqlJsonTypeMapping" /> for the older Npgsql-specific support, which allows mapping json/jsonb to text, to e.g.
/// <see cref="JsonElement" /> (weakly-typed mapping) or to arbitrary POCOs (but without them being modeled).
/// </summary>
public class NpgsqlOwnedJsonTypeMapping : JsonTypeMapping
public class NpgsqlStructuralJsonTypeMapping : JsonTypeMapping
{
/// <summary>
/// The database type used by Npgsql (<see cref="NpgsqlDbType.Json" /> or <see cref="NpgsqlDbType.Jsonb" />.
Expand All @@ -34,7 +34,7 @@ private static readonly ConstructorInfo MemoryStreamConstructor
/// 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.
/// </summary>
public NpgsqlOwnedJsonTypeMapping(string storeType)
public NpgsqlStructuralJsonTypeMapping(string storeType)
: base(storeType, typeof(JsonElement), dbType: null)
{
NpgsqlDbType = storeType switch
Expand Down Expand Up @@ -74,7 +74,7 @@ public override Expression CustomizeDataReaderExpression(Expression expression)
/// 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.
/// </summary>
protected NpgsqlOwnedJsonTypeMapping(RelationalTypeMappingParameters parameters, NpgsqlDbType npgsqlDbType)
protected NpgsqlStructuralJsonTypeMapping(RelationalTypeMappingParameters parameters, NpgsqlDbType npgsqlDbType)
: base(parameters)
{
NpgsqlDbType = npgsqlDbType;
Expand All @@ -91,7 +91,7 @@ protected override void ConfigureParameter(DbParameter parameter)
if (parameter is not NpgsqlParameter npgsqlParameter)
{
throw new InvalidOperationException(
$"Npgsql-specific type mapping {nameof(NpgsqlOwnedJsonTypeMapping)} being used with non-Npgsql parameter type {parameter.GetType().Name}");
$"Npgsql-specific type mapping {nameof(NpgsqlStructuralJsonTypeMapping)} being used with non-Npgsql parameter type {parameter.GetType().Name}");
}

base.ConfigureParameter(parameter);
Expand All @@ -114,7 +114,7 @@ protected virtual string EscapeSqlLiteral(string literal)
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
protected override string GenerateNonNullSqlLiteral(object value)
=> $"'{EscapeSqlLiteral(JsonSerializer.Serialize(value))}'";
=> $"'{EscapeSqlLiteral((string)value)}'";

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
Expand All @@ -123,5 +123,5 @@ protected override string GenerateNonNullSqlLiteral(object value)
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters)
=> new NpgsqlOwnedJsonTypeMapping(parameters, NpgsqlDbType);
=> new NpgsqlStructuralJsonTypeMapping(parameters, NpgsqlDbType);
}
4 changes: 2 additions & 2 deletions src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,8 @@ static NpgsqlTypeMappingSource()
private readonly NpgsqlStringTypeMapping _jsonpath = new("jsonpath", NpgsqlDbType.JsonPath);

// JSON mappings - EF owned entity support
private readonly NpgsqlOwnedJsonTypeMapping _jsonbOwned = new("jsonb");
private readonly NpgsqlOwnedJsonTypeMapping _jsonOwned = new("json");
private readonly NpgsqlStructuralJsonTypeMapping _jsonbOwned = new("jsonb");
private readonly NpgsqlStructuralJsonTypeMapping _jsonOwned = new("json");

// JSON mappings - older string/weakly-typed support
private readonly NpgsqlJsonTypeMapping _jsonbString = new("jsonb", typeof(string));
Expand Down
2 changes: 1 addition & 1 deletion src/EFCore.PG/Update/Internal/NpgsqlUpdateSqlGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ protected override void AppendUpdateColumnValue(
if (columnModification.JsonPath is not (null or "$"))
{
Check.DebugAssert(
columnModification.TypeMapping is NpgsqlOwnedJsonTypeMapping,
columnModification.TypeMapping is NpgsqlStructuralJsonTypeMapping,
"ColumnModification with JsonPath but non-NpgsqlOwnedJsonTypeMapping");

if (columnModification.TypeMapping.StoreType is "json")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,54 @@ public override async Task Update_collection_inside_complex_type(bool async)
""");
}

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());
Expand Down
19 changes: 7 additions & 12 deletions test/EFCore.PG.FunctionalTests/ComplexTypesTrackingNpgsqlTest.cs
Original file line number Diff line number Diff line change
@@ -1,23 +1,18 @@
namespace Microsoft.EntityFrameworkCore;

public class ComplexTypesTrackingNpgsqlTest : ComplexTypesTrackingTestBase<ComplexTypesTrackingNpgsqlTest.NpgsqlFixture>
public class ComplexTypesTrackingNpgsqlTest(ComplexTypesTrackingNpgsqlTest.NpgsqlFixture fixture, ITestOutputHelper testOutputHelper)
: ComplexTypesTrackingRelationalTestBase<ComplexTypesTrackingNpgsqlTest.NpgsqlFixture>(fixture, testOutputHelper)
{
public ComplexTypesTrackingNpgsqlTest(NpgsqlFixture fixture, ITestOutputHelper testOutputHelper)
: base(fixture)
{
fixture.TestSqlLoggerFactory.Clear();
fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper);
}

protected override void UseTransaction(DatabaseFacade facade, IDbContextTransaction transaction)
=> facade.UseTransaction(transaction.GetDbTransaction());

public class NpgsqlFixture : FixtureBase
// 'timestamp with time zone' literal cannot be generated for Unspecified DateTime: a UTC DateTime is required
public override Task Can_track_entity_with_complex_property_bag_collections(EntityState state, bool async)
=> Task.CompletedTask;

public class NpgsqlFixture : RelationalFixtureBase
{
protected override ITestStoreFactory TestStoreFactory
=> NpgsqlTestStoreFactory.Instance;

public TestSqlLoggerFactory TestSqlLoggerFactory
=> (TestSqlLoggerFactory)ListLoggerFactory;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3125,6 +3125,9 @@ public override async Task Create_table_with_complex_type_with_required_properti
"MyComplex_Prop" text,
"MyComplex_MyNestedComplex_Bar" timestamp with time zone,
"MyComplex_MyNestedComplex_Foo" integer,
"MyComplex_Nested_Bar" timestamp with time zone,
"MyComplex_Nested_Foo" integer,
"NestedCollection" jsonb,
CONSTRAINT "PK_Contacts" PRIMARY KEY ("Id")
);
""");
Expand Down
Loading