Skip to content

Commit

Permalink
Experimental support for the Azure SQL json type
Browse files Browse the repository at this point in the history
Fixes #28452
Fixes #32150

Remaining work:

- Test reverse engineering from an existing database
- Output a warning when the native JSON type is used
- Replace the ToJson overload with HasColumnType()
- Move the type mapping visitation to another visitor

Known issues:

- Various issues communicated with the SQL team--see TODO:SQLJSON
- Testing is disabled until we have an appropriate server and driver to test against
  • Loading branch information
ajcvickers committed Aug 11, 2024
1 parent ab30db3 commit dd28a35
Show file tree
Hide file tree
Showing 27 changed files with 9,305 additions and 778 deletions.
1 change: 1 addition & 0 deletions NuGet.config
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
<configuration>
<packageSources>
<clear />
<add key="localnuget" value="C:\tempnuget" />
<!--Begin: Package sources managed by Dependency Flow automation. Do not edit the sources below.-->
<!-- Begin: Package sources from dotnet-runtime -->
<!-- End: Package sources from dotnet-runtime -->
Expand Down
40 changes: 40 additions & 0 deletions src/EFCore.Relational/Extensions/RelationalEntityTypeExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1610,6 +1610,46 @@ public static void SetContainerColumnName(this IMutableEntityType entityType, st
? columnName
: (entityType.FindOwnership()?.PrincipalEntityType.GetContainerColumnName());

/// <summary>
/// Sets the column type to use for the container column to which the entity type is mapped.
/// </summary>
/// <param name="entityType">The entity type.</param>
/// <param name="columnType">The database column type.</param>
public static void SetContainerColumnType(this IMutableEntityType entityType, string? columnType)
=> entityType.SetOrRemoveAnnotation(RelationalAnnotationNames.ContainerColumnType, columnType);

/// <summary>
/// Sets the column type to use for the container column to which the entity type is mapped.
/// </summary>
/// <param name="entityType">The entity type.</param>
/// <param name="columnType">The database column type.</param>
/// <param name="fromDataAnnotation">Indicates whether the configuration was specified using a data annotation.</param>
/// <returns>The configured value.</returns>
public static string? SetContainerColumnType(
this IConventionEntityType entityType,
string? columnType,
bool fromDataAnnotation = false)
=> (string?)entityType.SetAnnotation(RelationalAnnotationNames.ContainerColumnType, columnType, fromDataAnnotation)?.Value;

/// <summary>
/// Gets the <see cref="ConfigurationSource" /> for the container column type.
/// </summary>
/// <param name="entityType">The entity type.</param>
/// <returns>The <see cref="ConfigurationSource" />.</returns>
public static ConfigurationSource? GetContainerColumnTypeConfigurationSource(this IConventionEntityType entityType)
=> entityType.FindAnnotation(RelationalAnnotationNames.ContainerColumnType)
?.GetConfigurationSource();

/// <summary>
/// Gets the column type to use for the container column to which the entity type is mapped.
/// </summary>
/// <param name="entityType">The entity type.</param>
/// <returns>The database column type.</returns>
public static string? GetContainerColumnType(this IReadOnlyEntityType entityType)
=> entityType.FindAnnotation(RelationalAnnotationNames.ContainerColumnType)?.Value is string columnType
? columnType
: (entityType.FindOwnership()?.PrincipalEntityType.GetContainerColumnType());

/// <summary>
/// Sets the type mapping for the container column to which the entity type is mapped.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,7 @@ public static class RelationalOwnedNavigationBuilderExtensions
/// <param name="builder">The builder for the owned navigation being configured.</param>
/// <returns>The same builder instance so that multiple calls can be chained.</returns>
public static OwnedNavigationBuilder ToJson(this OwnedNavigationBuilder builder)
{
var navigationName = builder.Metadata.GetNavigation(pointsToPrincipal: false)!.Name;
builder.ToJson(navigationName);

return builder;
}
=> builder.ToJson(builder.Metadata.GetNavigation(pointsToPrincipal: false)!.Name);

/// <summary>
/// Configures a relationship where this entity type and the entities that it owns are mapped to a JSON column in the database.
Expand All @@ -45,12 +40,7 @@ public static OwnedNavigationBuilder<TOwnerEntity, TDependentEntity> ToJson<TOwn
this OwnedNavigationBuilder<TOwnerEntity, TDependentEntity> builder)
where TOwnerEntity : class
where TDependentEntity : class
{
var navigationName = builder.Metadata.GetNavigation(pointsToPrincipal: false)!.Name;
builder.ToJson(navigationName);

return builder;
}
=> (OwnedNavigationBuilder<TOwnerEntity, TDependentEntity>)((OwnedNavigationBuilder)builder).ToJson();

/// <summary>
/// Configures a relationship where this entity type and the entities that it owns are mapped to a JSON column in the database.
Expand All @@ -68,11 +58,7 @@ public static OwnedNavigationBuilder<TOwnerEntity, TDependentEntity> ToJson<TOwn
string? jsonColumnName)
where TOwnerEntity : class
where TDependentEntity : class
{
builder.OwnedEntityType.SetContainerColumnName(jsonColumnName);

return builder;
}
=> builder.ToJson(jsonColumnName, null);

/// <summary>
/// Configures a relationship where this entity type and the entities that it owns are mapped to a JSON column in the database.
Expand All @@ -88,8 +74,47 @@ public static OwnedNavigationBuilder<TOwnerEntity, TDependentEntity> ToJson<TOwn
public static OwnedNavigationBuilder ToJson(
this OwnedNavigationBuilder builder,
string? jsonColumnName)
=> builder.ToJson(jsonColumnName, null);

/// <summary>
/// Configures a relationship where this entity type and the entities that it owns are mapped to a JSON column in the database.
/// </summary>
/// <remarks>
/// This method should only be specified for the outer-most owned entity in the given ownership structure.
/// All entities owned by this will be automatically mapped to the same JSON column.
/// The ownerships must still be explicitly defined.
/// </remarks>
/// <param name="builder">The builder for the owned navigation being configured.</param>
/// <param name="jsonColumnName">JSON column name to use.</param>
/// <param name="jsonColumnType">The database type for the JSON column, or <see langword="null"/> to use the database default.</param>
/// <returns>The same builder instance so that multiple calls can be chained.</returns>
public static OwnedNavigationBuilder<TOwnerEntity, TDependentEntity> ToJson<TOwnerEntity, TDependentEntity>(
this OwnedNavigationBuilder<TOwnerEntity, TDependentEntity> builder,
string? jsonColumnName,
string? jsonColumnType)
where TOwnerEntity : class
where TDependentEntity : class
=> (OwnedNavigationBuilder<TOwnerEntity, TDependentEntity>)((OwnedNavigationBuilder)builder).ToJson(jsonColumnName, jsonColumnType);

/// <summary>
/// Configures a relationship where this entity type and the entities that it owns are mapped to a JSON column in the database.
/// </summary>
/// <remarks>
/// This method should only be specified for the outer-most owned entity in the given ownership structure.
/// All entities owned by this will be automatically mapped to the same JSON column.
/// The ownerships must still be explicitly defined.
/// </remarks>
/// <param name="builder">The builder for the owned navigation being configured.</param>
/// <param name="jsonColumnName">JSON column name to use.</param>
/// <param name="jsonColumnType">The database type for the JSON column, or <see langword="null"/> to use the database default.</param>
/// <returns>The same builder instance so that multiple calls can be chained.</returns>
public static OwnedNavigationBuilder ToJson(
this OwnedNavigationBuilder builder,
string? jsonColumnName,
string? jsonColumnType)
{
builder.OwnedEntityType.SetContainerColumnName(jsonColumnName);
builder.OwnedEntityType.SetContainerColumnType(jsonColumnType);

return builder;
}
Expand Down
11 changes: 11 additions & 0 deletions src/EFCore.Relational/Extensions/RelationalTypeBaseExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,17 @@ public static bool IsMappedToJson(this IReadOnlyTypeBase typeBase)
? entityType.GetContainerColumnName()
: ((IReadOnlyComplexType)typeBase).GetContainerColumnName();


/// <summary>
/// Gets the column type to use for the container column to which the type is mapped.
/// </summary>
/// <param name="typeBase">The type.</param>
/// <returns>The database column type.</returns>
public static string? GetContainerColumnType(this IReadOnlyTypeBase typeBase)
=> typeBase is IReadOnlyEntityType entityType
? entityType.GetContainerColumnType()
: null;

/// <summary>
/// Gets the value of JSON property name used for the given entity mapped to a JSON column.
/// </summary>
Expand Down
23 changes: 14 additions & 9 deletions src/EFCore.Relational/Metadata/Internal/RelationalModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -286,12 +286,14 @@ private static void AddDefaultMappings(
includesDerivedTypes: entityType.GetDirectlyDerivedTypes().Any()
? !isTpc && mappedType == entityType
: null);

var containerColumnName = mappedType.GetContainerColumnName();
var containerColumnType = mappedType.GetContainerColumnType();
if (!string.IsNullOrEmpty(containerColumnName))
{
CreateContainerColumn(
defaultTable, containerColumnName, mappedType, relationalTypeMappingSource,
static (c, t, m) => new JsonColumnBase(c, m.StoreType, t, m));
defaultTable, containerColumnName, containerColumnType, mappedType, relationalTypeMappingSource,
static (colName, colType, table, mapping) => new JsonColumnBase(colName, colType ?? mapping.StoreType, table, mapping));
}
else
{
Expand Down Expand Up @@ -492,11 +494,12 @@ private static void CreateTableMapping(
};

var containerColumnName = mappedType.GetContainerColumnName();
var containerColumnType = mappedType.GetContainerColumnType();
if (!string.IsNullOrEmpty(containerColumnName))
{
CreateContainerColumn(
table, containerColumnName, (IEntityType)mappedType, relationalTypeMappingSource,
static (c, t, m) => new JsonColumn(c, m.StoreType, (Table)t, m));
table, containerColumnName, containerColumnType, (IEntityType)mappedType, relationalTypeMappingSource,
static (colName, colType, table, mapping) => new JsonColumn(colName, colType ?? mapping.StoreType, (Table)table, mapping));
}
else
{
Expand Down Expand Up @@ -567,18 +570,19 @@ private static void CreateTableMapping(
private static void CreateContainerColumn<TColumnMappingBase>(
TableBase tableBase,
string containerColumnName,
string? containerColumnType,
IEntityType mappedType,
IRelationalTypeMappingSource relationalTypeMappingSource,
Func<string, TableBase, RelationalTypeMapping, ColumnBase<TColumnMappingBase>> createColumn)
Func<string, string?, TableBase, RelationalTypeMapping, ColumnBase<TColumnMappingBase>> createColumn)
where TColumnMappingBase : class, IColumnMappingBase
{
var ownership = mappedType.GetForeignKeys().Single(fk => fk.IsOwnership);
if (!ownership.PrincipalEntityType.IsMappedToJson())
{
Check.DebugAssert(tableBase.FindColumn(containerColumnName) == null, $"Table does not have column '{containerColumnName}'.");

var jsonColumnTypeMapping = relationalTypeMappingSource.FindMapping(typeof(JsonElement), mappedType.Model)!;
var jsonColumn = createColumn(containerColumnName, tableBase, jsonColumnTypeMapping);
var jsonColumnTypeMapping = relationalTypeMappingSource.FindMapping(typeof(JsonElement), storeTypeName: containerColumnType)!;
var jsonColumn = createColumn(containerColumnName, containerColumnType, tableBase, jsonColumnTypeMapping);
tableBase.Columns.Add(containerColumnName, jsonColumn);
jsonColumn.IsNullable = !ownership.IsRequiredDependent || !ownership.IsUnique;

Expand Down Expand Up @@ -684,11 +688,12 @@ private static void CreateViewMapping(
};

var containerColumnName = mappedType.GetContainerColumnName();
var containerColumnType = mappedType.GetContainerColumnType();
if (!string.IsNullOrEmpty(containerColumnName))
{
CreateContainerColumn(
view, containerColumnName, mappedType, relationalTypeMappingSource,
static (c, t, m) => new JsonViewColumn(c, m.StoreType, (View)t, m));
view, containerColumnName, containerColumnType, mappedType, relationalTypeMappingSource,
static (colName, colType, table, mapping) => new JsonViewColumn(colName, colType ?? mapping.StoreType, (View)table, mapping));
}
else
{
Expand Down
5 changes: 5 additions & 0 deletions src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,11 @@ public static class RelationalAnnotationNames
/// </summary>
public const string ContainerColumnName = Prefix + "ContainerColumnName";

/// <summary>
/// The column type for the container column to which the object is mapped.
/// </summary>
public const string ContainerColumnType = Prefix + nameof(ContainerColumnType);

/// <summary>
/// The name for the annotation specifying container column type mapping.
/// </summary>
Expand Down
3 changes: 2 additions & 1 deletion src/EFCore.SqlServer/EFCore.SqlServer.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@

<ItemGroup>
<Compile Include="..\Shared\*.cs" />
<Compile Remove="Storage\Internal\SqlServerJsonTypeMapping.cs" />
</ItemGroup>

<ItemGroup>
Expand All @@ -49,7 +50,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.1" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.0.0-dev" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -321,4 +321,14 @@ public readonly record struct ColumnInfo(
RelationalTypeMapping TypeMapping,
IReadOnlyList<PathSegment>? Path = null,
bool AsJson = false);

/// <summary>
/// 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.
/// </summary>
public virtual SqlServerOpenJsonExpression Update(SqlExpression sqlExpression)
=> new(Alias, sqlExpression, Path, ColumnInfos);

}
Original file line number Diff line number Diff line change
Expand Up @@ -475,7 +475,7 @@ protected override Expression VisitJsonScalar(JsonScalarExpression jsonScalarExp
return jsonScalarExpression;
}

if (jsonScalarExpression.TypeMapping is SqlServerJsonTypeMapping
if (jsonScalarExpression.TypeMapping is SqlServerJsonElementTypeMapping
|| jsonScalarExpression.TypeMapping?.ElementTypeMapping is not null)
{
Sql.Append("JSON_QUERY(");
Expand All @@ -494,7 +494,7 @@ protected override Expression VisitJsonScalar(JsonScalarExpression jsonScalarExp
GenerateJsonPath(jsonScalarExpression.Path);
Sql.Append(")");

if (jsonScalarExpression.TypeMapping is not SqlServerJsonTypeMapping and not StringTypeMapping)
if (jsonScalarExpression.TypeMapping is not SqlServerJsonElementTypeMapping and not StringTypeMapping)
{
Sql.Append(" AS ");
Sql.Append(jsonScalarExpression.TypeMapping!.StoreType);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,7 @@ protected override Expression VisitExtension(Expression expression)
=> expression switch
{
SqlServerOpenJsonExpression openJsonExpression
when TryGetInferredTypeMapping(openJsonExpression.Alias, "value", out var typeMapping)
=> ApplyTypeMappingsOnOpenJsonExpression(openJsonExpression, new[] { typeMapping }),
=> ApplyTypeMappingsOnOpenJsonExpression(openJsonExpression),

_ => base.VisitExtension(expression)
};
Expand All @@ -55,12 +54,21 @@ when TryGetInferredTypeMapping(openJsonExpression.Alias, "value", out var typeMa
/// 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 virtual SqlServerOpenJsonExpression ApplyTypeMappingsOnOpenJsonExpression(
SqlServerOpenJsonExpression openJsonExpression,
IReadOnlyList<RelationalTypeMapping> typeMappings)
protected virtual SqlServerOpenJsonExpression ApplyTypeMappingsOnOpenJsonExpression(SqlServerOpenJsonExpression openJsonExpression)
{
Check.DebugAssert(typeMappings.Count == 1, "typeMappings.Count == 1");
var elementTypeMapping = typeMappings[0];
if (openJsonExpression is { JsonExpression.TypeMapping: SqlServerStringTypeMapping { StoreType: "json" } } or
{ JsonExpression.TypeMapping: SqlServerJsonElementTypeMapping { StoreType: "json" } })
{
openJsonExpression = openJsonExpression.Update(
new SqlUnaryExpression(
ExpressionType.Convert, (SqlExpression)Visit(openJsonExpression.JsonExpression), typeof(string),
_typeMappingSource.FindMapping(typeof(string))!));
}

if (!TryGetInferredTypeMapping(openJsonExpression.Alias, "value", out var elementTypeMapping))
{
return openJsonExpression;
}

// Constant queryables are translated to VALUES, no need for JSON.
// Column queryables have their type mapping from the model, so we don't ever need to apply an inferred mapping on them.
Expand Down
Loading

0 comments on commit dd28a35

Please sign in to comment.