Skip to content

Commit

Permalink
Fix to #28816 - Json: add support for Sqlite provider
Browse files Browse the repository at this point in the history
Adding support for Sqlite.

Limitation:
When accessing element of a JSON array we can only use constant values. Unlike Sql Server which supports parameters, columns or even arbitrary expressions.

Fixes #28816
  • Loading branch information
maumar committed Feb 17, 2023
1 parent def962e commit 2bf4e3d
Show file tree
Hide file tree
Showing 16 changed files with 3,616 additions and 31 deletions.
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,24 +48,28 @@ public override IEnumerable<IAnnotation> For(IRelationalModel model, bool design
/// </summary>
public override IEnumerable<IAnnotation> For(IColumn column, bool designTime)
{
// 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
var primaryKey = property.DeclaringEntityType.FindPrimaryKey();
if (primaryKey != null
&& primaryKey.Properties.Count == 1
&& primaryKey.Properties[0] == property
&& property.ValueGenerated == ValueGenerated.OnAdd
&& property.ClrType.UnwrapNullableType().IsInteger()
&& !HasConverter(property))
// JSON columns have no property mappings so all annotations that rely on property mappings should be skipped for them
if (column is not JsonColumn)
{
yield return new Annotation(SqliteAnnotationNames.Autoincrement, true);
}
// 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
var primaryKey = property.DeclaringEntityType.FindPrimaryKey();
if (primaryKey != null
&& primaryKey.Properties.Count == 1
&& primaryKey.Properties[0] == property
&& property.ValueGenerated == ValueGenerated.OnAdd
&& property.ClrType.UnwrapNullableType().IsInteger()
&& !HasConverter(property))
{
yield return new Annotation(SqliteAnnotationNames.Autoincrement, true);
}

var srid = property.GetSrid();
if (srid != null)
{
yield return new Annotation(SqliteAnnotationNames.Srid, srid);
var srid = property.GetSrid();
if (srid != null)
{
yield return new Annotation(SqliteAnnotationNames.Srid, srid);
}
}
}

Expand Down
8 changes: 8 additions & 0 deletions src/EFCore.Sqlite.Core/Properties/SqliteStrings.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/EFCore.Sqlite.Core/Properties/SqliteStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,9 @@
<data name="MigrationScriptGenerationNotSupported" xml:space="preserve">
<value>Generating idempotent scripts for migrations is not currently supported for SQLite. See http://go.microsoft.com/fwlink/?LinkId=723262 for more information and examples.</value>
</data>
<data name="NonConstantJsonArrayIndexNotSupported" xml:space="preserve">
<value>JSON array element can only be accessed using constant value for the array index. Non-constant value is being used in PATH for JSON column '{jsonColumnName}'.</value>
</data>
<data name="OrderByNotSupported" xml:space="preserve">
<value>SQLite does not support expressions of type '{type}' in ORDER BY clauses. Convert the values to a supported type, or use LINQ to Objects to order the results on the client side.</value>
</data>
Expand Down
66 changes: 66 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,70 @@ 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;
}

// TODO: need casts?
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("' + ");
Visit(pathSegment.ArrayIndex);
Sql.Append(" + '");
//throw new InvalidOperationException("TODO");
//Sql.Append("' + CAST(");
//Visit(pathSegment.ArrayIndex);
//Sql.Append(" AS ");
//Sql.Append(_typeMappingSource.GetMapping(typeof(string)).StoreType);
//Sql.Append(") + '");
}

Sql.Append("]");
}
}

Sql.Append("')");

//if (jsonScalarExpression.TypeMapping is not SqlServerJsonTypeMapping and not StringTypeMapping)
//{
// Sql.Append(" AS ");
// Sql.Append(jsonScalarExpression.TypeMapping!.StoreType);
// Sql.Append(")");
//}

return jsonScalarExpression;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ namespace Microsoft.EntityFrameworkCore.Sqlite.Query.Internal;
/// </summary>
public class SqliteQueryTranslationPostprocessor : RelationalQueryTranslationPostprocessor
{
private readonly ApplyValidatingVisitor _applyValidator = new();
private readonly ValidatingVisitor _validator = new();

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
Expand All @@ -39,12 +39,12 @@ public SqliteQueryTranslationPostprocessor(
public override Expression Process(Expression query)
{
var result = base.Process(query);
_applyValidator.Visit(result);
_validator.Visit(result);

return result;
}

private sealed class ApplyValidatingVisitor : ExpressionVisitor
private sealed class ValidatingVisitor : ExpressionVisitor
{
protected override Expression VisitExtension(Expression extensionExpression)
{
Expand All @@ -62,6 +62,14 @@ protected override Expression VisitExtension(Expression extensionExpression)
throw new InvalidOperationException(SqliteStrings.ApplyNotSupported);
}

if (extensionExpression is JsonScalarExpression jsonScalarExpression
&& jsonScalarExpression.Path.Any(x => x.ArrayIndex is not null and not SqlConstantExpression))
{
throw new InvalidOperationException(
SqliteStrings.NonConstantJsonArrayIndexNotSupported(
jsonScalarExpression.JsonColumn.Name));
}

return base.VisitExtension(extensionExpression);
}
}
Expand Down
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
55 changes: 55 additions & 0 deletions src/EFCore.Sqlite.Core/Update/Internal/SqliteUpdateSqlGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Text;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore.Sqlite.Internal;
using Microsoft.EntityFrameworkCore.Update;

namespace Microsoft.EntityFrameworkCore.Sqlite.Update.Internal;

Expand Down Expand Up @@ -143,6 +144,60 @@ protected override void AppendRowsAffectedWhereCondition(StringBuilder commandSt
public override string GenerateNextSequenceValueOperation(string name, string? schema)
=> throw new NotSupportedException(SqliteStrings.SequencesNotSupported);


/// <inheritdoc />
protected override void AppendUpdateColumnValue(
ISqlGenerationHelper updateSqlGeneratorHelper,
IColumnModification columnModification,
StringBuilder stringBuilder,
string name,
string? schema)
{
if (columnModification.JsonPath != null
&& columnModification.JsonPath != "$")
{

stringBuilder.Append("json_set(");
updateSqlGeneratorHelper.DelimitIdentifier(stringBuilder, columnModification.ColumnName);
stringBuilder.Append(", '");
stringBuilder.Append(columnModification.JsonPath);
stringBuilder.Append("', ");

if (columnModification.Property != null)
{
// HACK: sqlite converts true/false into it's native format 0/1
// which then is not a correct bool value from the perspective of JsonElement
// so we do the normal extraction of bool value from the parameter object
// and then convert it to JSON true/false again
if (columnModification.Property.ClrType == typeof(bool))
{
stringBuilder.Append("json(replace(replace(");
}

stringBuilder.Append("json_extract(");
base.AppendUpdateColumnValue(updateSqlGeneratorHelper, columnModification, stringBuilder, name, schema);
stringBuilder.Append(", '$[0]')");

if (columnModification.Property.ClrType == typeof(bool))
{
stringBuilder.Append(", '0', 'false'), '1', 'true'))");
}
}
else
{
stringBuilder.Append("json(");
base.AppendUpdateColumnValue(updateSqlGeneratorHelper, columnModification, stringBuilder, name, schema);
stringBuilder.Append(")");
}

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

private bool CanUseReturningClause(IReadOnlyModificationCommand command)
=> _isReturningClauseSupported && command.Table?.IsSqlReturningClauseUsed() == true;
}
Loading

0 comments on commit 2bf4e3d

Please sign in to comment.