Skip to content

Commit

Permalink
Implement JSON_VALUE translations
Browse files Browse the repository at this point in the history
Part of dotnet#29306
  • Loading branch information
joelmandell authored and roji committed Dec 19, 2024
1 parent 6418d63 commit 354a9df
Show file tree
Hide file tree
Showing 10 changed files with 361 additions and 1 deletion.
34 changes: 34 additions & 0 deletions src/EFCore.SqlServer/Extensions/SqlServerDbFunctionsExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2452,4 +2452,38 @@ public static long PatIndex(
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(VariancePopulation)));

#endregion Population variance

/// <summary>
/// Extracts a scalar value from a JSON string.
/// Corresponds to SQL Server's <c>JSON_VALUE(@expression, @path)</c>.
/// </summary>
/// <remarks>
/// See <see href="https://aka.ms/efcore-docs-database-functions">Database functions</see>, and
/// <see href="https://aka.ms/efcore-docs-sqlserver">Accessing SQL Server and SQL Azure databases with EF Core</see>
/// for more information and examples.
/// </remarks>
/// <param name="_">The <see cref="DbFunctions" /> instance.</param>
/// <param name="expression">An expression. Typically the name of a variable or a column that contains JSON text.</param>
/// <param name="path">A JSON path that specifies the property to extract.</param>
/// <returns>A single text value.</returns>
/// <seealso href="https://learn.microsoft.com/sql/t-sql/functions/json-value-transact-sql">SQL Server documentation for <c>JSON_VALUE</c>.</seealso>
public static string JsonValue(this DbFunctions _, object expression, string path)
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(SqlServerDbFunctionsExtensions.JsonValue)));

/// <summary>
/// Extracts an object or an array from a JSON string.
/// Corresponds to SQL Server's <c>JSON_QUERY(@expression, @path)</c>.
/// </summary>
/// <remarks>
/// See <see href="https://aka.ms/efcore-docs-database-functions">Database functions</see>, and
/// <see href="https://aka.ms/efcore-docs-sqlserver">Accessing SQL Server and SQL Azure databases with EF Core</see>
/// for more information and examples.
/// </remarks>
/// <param name="_">The <see cref="DbFunctions" /> instance.</param>
/// <param name="expression">An expression. Typically the name of a variable or a column that contains JSON text.</param>
/// <param name="path">A JSON path that specifies the object or the array to extract.</param>
/// <returns>Returns a JSON fragment of type nvarchar(max).</returns>
/// <seealso href="https://learn.microsoft.com/sv-se/sql/t-sql/functions/json-query-transact-sql">SQL Server documentation for <c>JSON_QUERY</c>.</seealso>
public static string JsonQuery(this DbFunctions _, object expression, string path)
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(SqlServerDbFunctionsExtensions.JsonQuery)));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// 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.Query.SqlExpressions;

namespace Microsoft.EntityFrameworkCore.SqlServer.Query.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 SqlServerJsonFunctionsTranslator : IMethodCallTranslator
{
private static readonly bool[] _propagatedNulls = new bool[] { true, true };

private static readonly Dictionary<MethodInfo, string> _methodInfoJsonFunctions
= new()
{
{
typeof(SqlServerDbFunctionsExtensions).GetRuntimeMethod(nameof(SqlServerDbFunctionsExtensions.JsonValue), new[] { typeof(DbFunctions), typeof(object), typeof(string) })!,
"JSON_VALUE"
},
{
typeof(SqlServerDbFunctionsExtensions).GetRuntimeMethod(nameof(SqlServerDbFunctionsExtensions.JsonQuery), new[] { typeof(DbFunctions), typeof(object), typeof(string) })!,
"JSON_QUERY"
},
};

private readonly ISqlExpressionFactory _sqlExpressionFactory;


/// <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 SqlServerJsonFunctionsTranslator(
ISqlExpressionFactory sqlExpressionFactory)
{
_sqlExpressionFactory = sqlExpressionFactory;
}

/// <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 SqlExpression? Translate(
SqlExpression? instance,
MethodInfo method,
IReadOnlyList<SqlExpression> arguments,
IDiagnosticsLogger<DbLoggerCategory.Query> logger) => _methodInfoJsonFunctions.TryGetValue(method, out var function) ?
_sqlExpressionFactory.Function(function,new List<SqlExpression> { arguments[1], arguments[2] }, nullable: true,argumentsPropagateNullability: _propagatedNulls,typeof(string)) : null;
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ public SqlServerMethodCallTranslatorProvider(
new SqlServerFullTextSearchFunctionsTranslator(sqlExpressionFactory),
new SqlServerIsDateFunctionTranslator(sqlExpressionFactory),
new SqlServerIsNumericFunctionTranslator(sqlExpressionFactory),
new SqlServerJsonFunctionsTranslator(sqlExpressionFactory),
new SqlServerMathTranslator(sqlExpressionFactory),
new SqlServerNewGuidTranslator(sqlExpressionFactory),
new SqlServerObjectToStringTranslator(sqlExpressionFactory, typeMappingSource),
Expand Down
30 changes: 30 additions & 0 deletions src/EFCore.Sqlite.Core/Extensions/SqliteDbFunctionsExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,36 @@ public static class SqliteDbFunctionsExtensions
public static bool Glob(this DbFunctions _, string matchExpression, string pattern)
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Glob)));

/// <summary>
/// Maps to the SQLite <c>json_extract</c> function, which extracts and returns one or more values from well-formed JSON.
/// </summary>
/// <remarks>
/// See <see href="https://aka.ms/efcore-docs-database-functions">Database functions</see>, and
/// <see href="https://aka.ms/efcore-docs-sqlite">Accessing SQLite databases with EF Core</see> for more information and examples.
/// </remarks>
/// <param name="_">The <see cref="DbFunctions" /> instance.</param>
/// <param name="expression">The json</param>
/// <param name="paths">The paths</param>
/// <returns>One or more values from the well-formed JSON.</returns>
/// <seealso href="https://www.sqlite.org/json1.html#the_json_extract_function">SQLite documentation for <c>json_extract</c>.</seealso>
public static string JsonExtract(this DbFunctions _, object expression, params string[] paths)
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(JsonExtract)));

/// <summary>
/// Maps to the SQLite <c>json_extract</c> function, which extracts and returns one or more values from well-formed JSON.
/// </summary>
/// <remarks>
/// See <see href="https://aka.ms/efcore-docs-database-functions">Database functions</see>, and
/// <see href="https://aka.ms/efcore-docs-sqlite">Accessing SQLite databases with EF Core</see> for more information and examples.
/// </remarks>
/// <param name="_">The <see cref="DbFunctions" /> instance.</param>
/// <param name="expression">The json</param>
/// <param name="path">The paths</param>
/// <returns>INTEGER or REAL for a JSON numeric value, an INTEGER zero for a JSON false value, an INTEGER one for a JSON true value, the dequoted text for a JSON string value, and a text representation for JSON object and array values</returns>
/// <seealso href="https://www.sqlite.org/json1.html#the_json_extract_function">SQLite documentation for <c>json_extract</c>.</seealso>
public static object JsonExtract(this DbFunctions _, object expression, string path)
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(JsonExtract)));

/// <summary>
/// Maps to the SQLite <c>hex</c> function which returns a hexadecimal string representing the specified value.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// 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.Query.SqlExpressions;

namespace Microsoft.EntityFrameworkCore.Sqlite.Query.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 SqliteJsonFunctionsTranslator : IMethodCallTranslator
{
private static readonly Dictionary<MethodInfo, string> _methodInfoJsonFunctions
= new()
{
{
typeof(SqliteDbFunctionsExtensions).GetRuntimeMethod(nameof(SqliteDbFunctionsExtensions.JsonExtract), new[] { typeof(DbFunctions), typeof(object), typeof(string[]) })!,
"JsonExtract_Paths"
},
{
typeof(SqliteDbFunctionsExtensions).GetRuntimeMethod(nameof(SqliteDbFunctionsExtensions.JsonExtract), new[] { typeof(DbFunctions), typeof(object), typeof(string) })!,
"JsonExtract_Path"
},
};

private readonly ISqlExpressionFactory _sqlExpressionFactory;

/// <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 SqliteJsonFunctionsTranslator(
ISqlExpressionFactory sqlExpressionFactory)
{
_sqlExpressionFactory = sqlExpressionFactory;
}

/// <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 SqlExpression? Translate(
SqlExpression? instance,
MethodInfo method,
IReadOnlyList<SqlExpression> arguments,
IDiagnosticsLogger<DbLoggerCategory.Query> logger)
{
if(_methodInfoJsonFunctions.TryGetValue(method,out _))
{
var expression = arguments[1];
var pathArg = arguments[2];
var functionArguments = new List<SqlExpression> { expression };
var pathValue = ((SqlConstantExpression)pathArg)?.Value;

//If return type is of string it means JSON_EXTRACT will return text which is a well-formed JSON array holding the various values.
if (method.ReturnParameter?.ParameterType == typeof(string))
{
if (pathValue?.GetType() == typeof(string[]))
{
var constantValues = pathValue is not null ? (string[])pathValue : Array.Empty<string>();

foreach (var path in constantValues)
{
functionArguments.Add(_sqlExpressionFactory.Constant(path));
}
}
}

//If return type is of object it means JSON_EXTRACT will return either null, INTEGER or REAL for a JSON numeric value, an INTEGER zero for a JSON false value, an INTEGER one for a JSON true value.
//the dequoted text for a JSON string value, and a text representation for JSON object and array values.
if (method.ReturnParameter?.ParameterType == typeof(object))
functionArguments.Add(_sqlExpressionFactory.Constant(pathValue));

return _sqlExpressionFactory.Function(
"JSON_EXTRACT",
functionArguments,
nullable: true,
argumentsPropagateNullability: functionArguments.Select(_ => true).ToList(),
typeof(string));
}

return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public SqliteMethodCallTranslatorProvider(RelationalMethodCallTranslatorProvider
new SqliteDateTimeMethodTranslator(sqlExpressionFactory),
new SqliteGlobMethodTranslator(sqlExpressionFactory),
new SqliteHexMethodTranslator(sqlExpressionFactory),
new SqliteJsonFunctionsTranslator(sqlExpressionFactory),
new SqliteMathTranslator(sqlExpressionFactory),
new SqliteObjectToStringTranslator(sqlExpressionFactory),
new SqliteRandomTranslator(sqlExpressionFactory),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ public class JsonQueryContext(DbContextOptions options) : DbContext(options)
{
public DbSet<EntityBasic> EntitiesBasic { get; set; }
public DbSet<JsonEntityBasic> JsonEntitiesBasic { get; set; }
public DbSet<JsonEntityBasicString> JsonEntitiesBasicString { get; set; }

public DbSet<JsonEntityBasicForReference> JsonEntitiesBasicForReference { get; set; }
public DbSet<JsonEntityBasicForCollection> JsonEntitiesBasicForCollection { get; set; }
public DbSet<JsonEntityCustomNaming> JsonEntitiesCustomNaming { get; set; }
Expand All @@ -20,6 +22,7 @@ public class JsonQueryContext(DbContextOptions options) : DbContext(options)
public static Task SeedAsync(JsonQueryContext context)
{
var jsonEntitiesBasic = JsonQueryData.CreateJsonEntitiesBasic();
var jsonEntitiesBasicStrings = JsonQueryData.CreateJsonEntitiesBasicString();
var entitiesBasic = JsonQueryData.CreateEntitiesBasic();
var jsonEntitiesBasicForReference = JsonQueryData.CreateJsonEntitiesBasicForReference();
var jsonEntitiesBasicForCollection = JsonQueryData.CreateJsonEntitiesBasicForCollection();
Expand All @@ -32,6 +35,7 @@ public static Task SeedAsync(JsonQueryContext context)
var jsonEntitiesConverters = JsonQueryData.CreateJsonEntitiesConverters();

context.JsonEntitiesBasic.AddRange(jsonEntitiesBasic);
context.JsonEntitiesBasicString.AddRange(jsonEntitiesBasicStrings);
context.EntitiesBasic.AddRange(entitiesBasic);
context.JsonEntitiesBasicForReference.AddRange(jsonEntitiesBasicForReference);
context.JsonEntitiesBasicForCollection.AddRange(jsonEntitiesBasicForCollection);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public class JsonQueryData : ISetSource
public JsonQueryData()
{
JsonEntitiesBasic = CreateJsonEntitiesBasic();
JsonEntitiesBasicStrings = CreateJsonEntitiesBasicString();
EntitiesBasic = CreateEntitiesBasic();
JsonEntitiesBasicForReference = CreateJsonEntitiesBasicForReference();
JsonEntitiesBasicForCollection = CreateJsonEntitiesBasicForCollection();
Expand All @@ -26,6 +27,8 @@ public JsonQueryData()

public IReadOnlyList<EntityBasic> EntitiesBasic { get; }
public IReadOnlyList<JsonEntityBasic> JsonEntitiesBasic { get; }
public IReadOnlyList<JsonEntityBasicString> JsonEntitiesBasicStrings { get; }

public IReadOnlyList<JsonEntityBasicForReference> JsonEntitiesBasicForReference { get; }
public IReadOnlyList<JsonEntityBasicForCollection> JsonEntitiesBasicForCollection { get; }
public IReadOnlyList<JsonEntityCustomNaming> JsonEntitiesCustomNaming { get; set; }
Expand All @@ -34,6 +37,16 @@ public JsonQueryData()
public IReadOnlyList<JsonEntityAllTypes> JsonEntitiesAllTypes { get; set; }
public IReadOnlyList<JsonEntityConverters> JsonEntitiesConverters { get; set; }


public static IReadOnlyList<JsonEntityBasicString> CreateJsonEntitiesBasicString()
{
var entity = new JsonEntityBasicString();
entity.Name = "Testing SQL Json-functions.";
entity.OwnedReferenceRoot = "{\"Name\":\"e1_r\",\"Number\":10,\"OwnedCollectionBranch\":[{\"Date\":\"2101-01-01T00:00:00\",\"Enum\":\"Two\",\"Fraction\":10.1,\"NullableEnum\":\"One\",\"OwnedCollectionLeaf\":[{\"SomethingSomething\":\"e1_r_c1_c1\"},{\"SomethingSomething\":\"e1_r_c1_c2\"}],\"OwnedReferenceLeaf\":{\"SomethingSomething\":\"e1_r_c1_r\"}},{\"Date\":\"2102-01-01T00:00:00\",\"Enum\":\"Three\",\"Fraction\":10.2,\"NullableEnum\":\"Two\",\"OwnedCollectionLeaf\":[{\"SomethingSomething\":\"e1_r_c2_c1\"},{\"SomethingSomething\":\"e1_r_c2_c2\"}],\"OwnedReferenceLeaf\":{\"SomethingSomething\":\"e1_r_c2_r\"}}],\"OwnedReferenceBranch\":{\"Date\":\"2100-01-01T00:00:00\",\"Enum\":\"One\",\"Fraction\":10.0,\"NullableEnum\":null,\"OwnedCollectionLeaf\":[{\"SomethingSomething\":\"e1_r_r_c1\"},{\"SomethingSomething\":\"e1_r_r_c2\"}],\"OwnedReferenceLeaf\":{\"SomethingSomething\":\"e1_r_r_r\"}}}";

return new List<JsonEntityBasicString>() { entity };
}

public static IReadOnlyList<JsonEntityBasic> CreateJsonEntitiesBasic()
{
//-------------------------------------------------------------------------------------------
Expand Down Expand Up @@ -1483,6 +1496,11 @@ public IQueryable<TEntity> Set<TEntity>()
return (IQueryable<TEntity>)JsonEntitiesBasic.AsQueryable();
}

if (typeof(TEntity) == typeof(JsonEntityBasicString))
{
return (IQueryable<TEntity>)JsonEntitiesBasicStrings.AsQueryable();
}

if (typeof(TEntity) == typeof(JsonEntityCustomNaming))
{
return (IQueryable<TEntity>)JsonEntitiesCustomNaming.AsQueryable();
Expand Down
Loading

0 comments on commit 354a9df

Please sign in to comment.