Skip to content

Commit

Permalink
Fix to #28816 - Json: add support for Sqlite provider (#30302)
Browse files Browse the repository at this point in the history
Adding support for Sqlite.
Also adding some more query and update tests for properties with value converters.

Fixes #28816
  • Loading branch information
maumar authored Feb 24, 2023
1 parent e2c1e16 commit 358b9ca
Show file tree
Hide file tree
Showing 29 changed files with 2,594 additions and 129 deletions.
12 changes: 11 additions & 1 deletion src/EFCore.Relational/Update/ModificationCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,7 @@ private List<IColumnModification> GenerateColumnModifications()

if (updateInfo.Property != null)
{
json = new JsonArray(JsonValue.Create(updateInfo.PropertyValue));
json = new JsonArray(GenerateJsonForSinglePropertyUpdate(updateInfo.Property, updateInfo.PropertyValue));
jsonPathString = jsonPathString + "." + updateInfo.Property.GetJsonPropertyName();
}
else
Expand Down Expand Up @@ -699,6 +699,16 @@ static JsonPartialUpdateInfo FindCommonJsonPartialUpdateInfo(
}
}

/// <summary>
/// Generates <see cref="JsonNode" /> representing the value to use for update in case a single property is being updated.
/// </summary>
/// <param name="property">Property to be updated.</param>
/// <param name="propertyValue">Value object that the property will be updated to.</param>
/// <returns><see cref="JsonNode" /> representing the value that the property will be updated to.</returns>
[EntityFrameworkInternal]
protected virtual JsonNode? GenerateJsonForSinglePropertyUpdate(IProperty property, object? propertyValue)
=> JsonValue.Create(propertyValue);

private JsonNode? CreateJson(object? navigationValue, IUpdateEntry parentEntry, IEntityType entityType, int? ordinal, bool isCollection)
{
if (navigationValue == null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,7 @@ protected override Expression VisitJsonScalar(JsonScalarExpression jsonScalarExp

Visit(jsonScalarExpression.JsonColumn);

Sql.Append(",'");
Sql.Append(", '");
foreach (var pathSegment in jsonScalarExpression.Path)
{
if (pathSegment.PropertyName != null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ namespace Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal;
/// </summary>
public class SqlServerJsonTypeMapping : JsonTypeMapping
{
private static readonly MethodInfo _getStringMethod
private static readonly MethodInfo GetStringMethod
= typeof(DbDataReader).GetRuntimeMethod(nameof(DbDataReader.GetString), new[] { typeof(int) })!;

private static readonly MethodInfo _jsonDocumentParseMethod
private static readonly MethodInfo JsonDocumentParseMethod
= typeof(JsonDocument).GetRuntimeMethod(nameof(JsonDocument.Parse), new[] { typeof(string), typeof(JsonDocumentOptions) })!;

private static readonly MemberInfo _jsonDocumentRootElementMember
private static readonly MemberInfo JsonDocumentRootElementMember
= typeof(JsonDocument).GetRuntimeProperty(nameof(JsonDocument.RootElement))!;

/// <summary>
Expand All @@ -40,7 +40,7 @@ public SqlServerJsonTypeMapping(string storeType)
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public override MethodInfo GetDataReaderMethod()
=> _getStringMethod;
=> GetStringMethod;

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
Expand All @@ -51,10 +51,10 @@ public override MethodInfo GetDataReaderMethod()
public override Expression CustomizeDataReaderExpression(Expression expression)
=> Expression.MakeMemberAccess(
Expression.Call(
_jsonDocumentParseMethod,
JsonDocumentParseMethod,
expression,
Expression.Default(typeof(JsonDocumentOptions))),
_jsonDocumentRootElementMember);
JsonDocumentRootElementMember);

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,7 @@ protected override void AppendUpdateColumnValue(
string name,
string? schema)
{
if (columnModification.JsonPath != null
&& columnModification.JsonPath != "$")
if (columnModification.JsonPath is not (null or "$"))
{
stringBuilder.Append("JSON_MODIFY(");
updateSqlGeneratorHelper.DelimitIdentifier(stringBuilder, columnModification.ColumnName);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ public static IServiceCollection AddEntityFrameworkSqlite(this IServiceCollectio
.TryAdd<IModelValidator, SqliteModelValidator>()
.TryAdd<IProviderConventionSetBuilder, SqliteConventionSetBuilder>()
.TryAdd<IModificationCommandBatchFactory, SqliteModificationCommandBatchFactory>()
.TryAdd<IModificationCommandFactory, SqliteModificationCommandFactory>()
.TryAdd<IRelationalConnection>(p => p.GetRequiredService<ISqliteRelationalConnection>())
.TryAdd<IMigrationsSqlGenerator, SqliteMigrationsSqlGenerator>()
.TryAdd<IRelationalDatabaseCreator, SqliteDatabaseCreator>()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.EntityFrameworkCore.Metadata.Internal;
using Microsoft.EntityFrameworkCore.Sqlite.Storage.Internal;

namespace Microsoft.EntityFrameworkCore.Sqlite.Metadata.Internal;
Expand Down Expand Up @@ -47,6 +48,12 @@ public override IEnumerable<IAnnotation> For(IRelationalModel model, bool design
/// </summary>
public override IEnumerable<IAnnotation> For(IColumn column, bool designTime)
{
// JSON columns have no property mappings so all annotations that rely on property mappings should be skipped for them
if (column is JsonColumn)
{
yield break;
}

// Model validation ensures that these facets are the same on all mapped properties
var property = column.PropertyMappings.First().Property;
// Only return auto increment for integer single column primary key
Expand Down
62 changes: 62 additions & 0 deletions src/EFCore.Sqlite.Core/Query/Internal/SqliteQuerySqlGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -117,4 +117,66 @@ private Expression VisitRegexp(RegexpExpression regexpExpression)

return regexpExpression;
}

/// <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>
protected override Expression VisitJsonScalar(JsonScalarExpression jsonScalarExpression)
{
if (jsonScalarExpression.Path.Count == 1
&& jsonScalarExpression.Path[0].ToString() == "$")
{
Visit(jsonScalarExpression.JsonColumn);

return jsonScalarExpression;
}

Sql.Append("json_extract(");

Visit(jsonScalarExpression.JsonColumn);

Sql.Append(", '");
foreach (var pathSegment in jsonScalarExpression.Path)
{
if (pathSegment.PropertyName != null)
{
Sql.Append((pathSegment.PropertyName == "$" ? "" : ".") + pathSegment.PropertyName);
}

if (pathSegment.ArrayIndex != null)
{
Sql.Append("[");

if (pathSegment.ArrayIndex is SqlConstantExpression)
{
Visit(pathSegment.ArrayIndex);
}
else
{
Sql.Append("' || ");
if (pathSegment.ArrayIndex is SqlParameterExpression)
{
Visit(pathSegment.ArrayIndex);
}
else
{
Sql.Append("(");
Visit(pathSegment.ArrayIndex);
Sql.Append(")");
}

Sql.Append(" || '");
}

Sql.Append("]");
}
}

Sql.Append("')");

return jsonScalarExpression;
}
}
95 changes: 95 additions & 0 deletions src/EFCore.Sqlite.Core/Storage/Internal/SqliteJsonTypeMapping.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Data;
using System.Text.Json;

namespace Microsoft.EntityFrameworkCore.Sqlite.Storage.Internal;

/// <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 class SqliteJsonTypeMapping : JsonTypeMapping
{
private static readonly MethodInfo GetStringMethod
= typeof(DbDataReader).GetRuntimeMethod(nameof(DbDataReader.GetString), new[] { typeof(int) })!;

private static readonly MethodInfo JsonDocumentParseMethod
= typeof(JsonDocument).GetRuntimeMethod(nameof(JsonDocument.Parse), new[] { typeof(string), typeof(JsonDocumentOptions) })!;

private static readonly MemberInfo JsonDocumentRootElementMember
= typeof(JsonDocument).GetRuntimeProperty(nameof(JsonDocument.RootElement))!;

/// <summary>
/// Initializes a new instance of the <see cref="SqliteJsonTypeMapping" /> class.
/// </summary>
/// <param name="storeType">The name of the database type.</param>
public SqliteJsonTypeMapping(string storeType)
: base(storeType, typeof(JsonElement), System.Data.DbType.String)
{
}

/// <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>
protected SqliteJsonTypeMapping(RelationalTypeMappingParameters parameters)
: base(parameters)
{
}

/// <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 override MethodInfo GetDataReaderMethod()
=> GetStringMethod;

/// <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 override Expression CustomizeDataReaderExpression(Expression expression)
=> Expression.MakeMemberAccess(
Expression.Call(
JsonDocumentParseMethod,
expression,
Expression.Default(typeof(JsonDocumentOptions))),
JsonDocumentRootElementMember);

/// <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>
protected override string GenerateNonNullSqlLiteral(object value)
=> $"'{EscapeSqlLiteral(JsonSerializer.Serialize(value))}'";

/// <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>
protected virtual string EscapeSqlLiteral(string literal)
=> literal.Replace("'", "''");

/// <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>
protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters)
=> new SqliteJsonTypeMapping(parameters);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Text.Json;

namespace Microsoft.EntityFrameworkCore.Sqlite.Storage.Internal;

/// <summary>
Expand Down Expand Up @@ -80,7 +82,8 @@ private static readonly HashSet<string> SpatialiteTypes
{ typeof(decimal), new SqliteDecimalTypeMapping(TextTypeName) },
{ typeof(double), Real },
{ typeof(float), new FloatTypeMapping(RealTypeName) },
{ typeof(Guid), new SqliteGuidTypeMapping(TextTypeName) }
{ typeof(Guid), new SqliteGuidTypeMapping(TextTypeName) },
{ typeof(JsonElement), new SqliteJsonTypeMapping(TextTypeName) }
};

private readonly Dictionary<string, RelationalTypeMapping> _storeTypeMappings = new(StringComparer.OrdinalIgnoreCase)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Text.Json.Nodes;

namespace Microsoft.EntityFrameworkCore.Sqlite.Update.Internal;

/// <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 class SqliteModificationCommand : ModificationCommand
{
/// <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 SqliteModificationCommand(in ModificationCommandParameters modificationCommandParameters)
: base(modificationCommandParameters)
{
}

/// <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 SqliteModificationCommand(in NonTrackedModificationCommandParameters modificationCommandParameters)
: base(modificationCommandParameters)
{
}

/// <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>
protected override JsonNode? GenerateJsonForSinglePropertyUpdate(IProperty property, object? propertyValue)
{
if (propertyValue is bool boolPropertyValue
&& (property.GetTypeMapping().Converter?.ProviderClrType ?? property.ClrType).UnwrapNullableType() == typeof(bool))
{
// Sqlite converts true/false into native 0/1 when using json_extract
// so we convert those values to strings so that they stay as true/false
// which is what we want to store in json object in the end
var modifiedPropertyValue = boolPropertyValue
? "true"
: "false";

#pragma warning disable EF1001 // Internal EF Core API usage.
return base.GenerateJsonForSinglePropertyUpdate(property, modifiedPropertyValue);
#pragma warning restore EF1001 // Internal EF Core API usage.
}

#pragma warning disable EF1001 // Internal EF Core API usage.
return base.GenerateJsonForSinglePropertyUpdate(property, propertyValue);
#pragma warning restore EF1001 // Internal EF Core API usage.
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.EntityFrameworkCore.Sqlite.Update.Internal;

/// <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 class SqliteModificationCommandFactory : IModificationCommandFactory
{
/// <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 IModificationCommand CreateModificationCommand(
in ModificationCommandParameters modificationCommandParameters)
=> new SqliteModificationCommand(modificationCommandParameters);

/// <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 INonTrackedModificationCommand CreateNonTrackedModificationCommand(
in NonTrackedModificationCommandParameters modificationCommandParameters)
=> new SqliteModificationCommand(modificationCommandParameters);
}
Loading

0 comments on commit 358b9ca

Please sign in to comment.