Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Issue #981 enabled mapping arbitrary user POCOs to PG JON. This adds support for mapping the System.Text.Json DOM types JsonDocument and JsonElement, which are more relevant for unstructured scenarios. Traversal is implemented as well as some of the PG JSON operators. Closes #1002
- Loading branch information
Showing
18 changed files
with
1,023 additions
and
84 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
59 changes: 59 additions & 0 deletions
59
src/EFCore.PG/Extensions/NpgsqlJsonDbFunctionsExtensions.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
using System; | ||
using System.Diagnostics.CodeAnalysis; | ||
using System.Runtime.CompilerServices; | ||
using System.Text.Json; | ||
using Microsoft.EntityFrameworkCore; | ||
|
||
namespace Npgsql.EntityFrameworkCore.PostgreSQL.Extensions | ||
{ | ||
/// <summary> | ||
/// Provides methods for <see cref="JsonElement"/> supporting translation to PostgreSQL JSON operators and functions. | ||
/// </summary> | ||
public static class NpgsqlJsonDbFunctionsExtensions | ||
{ | ||
/// <summary> | ||
/// Checks if <paramref name="left"/> contains <paramref name="right"/> as top-level entries. | ||
/// </summary> | ||
public static bool JsonContains(this DbFunctions _, JsonElement left, JsonElement right) | ||
=> throw ClientEvaluationNotSupportedException(); | ||
|
||
/// <summary> | ||
/// Checks if <paramref name="left"/> contains <paramref name="right"/> as top-level entries. | ||
/// </summary> | ||
public static bool JsonContains(this DbFunctions _, JsonElement left, string right) | ||
=> throw ClientEvaluationNotSupportedException(); | ||
|
||
/// <summary> | ||
/// Checks if <paramref name="left"/> is contained in <paramref name="right"/> as top-level entries. | ||
/// </summary> | ||
public static bool JsonContained(this DbFunctions _, JsonElement left, JsonElement right) | ||
=> throw ClientEvaluationNotSupportedException(); | ||
|
||
/// <summary> | ||
/// Checks if <paramref name="left"/> is contained in <paramref name="right"/> as top-level entries. | ||
/// </summary> | ||
public static bool JsonContained(this DbFunctions _, string left, JsonElement right) | ||
=> throw ClientEvaluationNotSupportedException(); | ||
|
||
/// <summary> | ||
/// Checks if <paramref name="key"/> exists as a top-level key within <paramref name="element"/>. | ||
/// </summary> | ||
public static bool JsonExists(this DbFunctions _, JsonElement element, string key) | ||
=> throw ClientEvaluationNotSupportedException(); | ||
|
||
/// <summary> | ||
/// Checks if any of the given <paramref name="keys"/> exist as top-level keys within <paramref name="element"/>. | ||
/// </summary> | ||
public static bool JsonExistAny(this DbFunctions _, JsonElement element, string[] keys) | ||
=> throw ClientEvaluationNotSupportedException(); | ||
|
||
/// <summary> | ||
/// Checks if all of the given <paramref name="keys"/> exist as top-level keys within <paramref name="element"/>. | ||
/// </summary> | ||
public static bool JsonExistAll(this DbFunctions _, JsonElement element, string[] keys) | ||
=> throw ClientEvaluationNotSupportedException(); | ||
|
||
static NotSupportedException ClientEvaluationNotSupportedException([CallerMemberName] string method = default) | ||
=> new NotSupportedException($"{method} is only intended for use via SQL translation as part of an EF Core LINQ query."); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
192 changes: 192 additions & 0 deletions
192
src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlJsonDomTranslator.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,192 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Reflection; | ||
using System.Text.Json; | ||
using Microsoft.EntityFrameworkCore.Query; | ||
using Microsoft.EntityFrameworkCore.Query.SqlExpressions; | ||
using Microsoft.EntityFrameworkCore.Storage; | ||
using Npgsql.EntityFrameworkCore.PostgreSQL.Extensions; | ||
using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal; | ||
using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Internal; | ||
using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; | ||
|
||
namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Internal | ||
{ | ||
public class NpgsqlJsonDomTranslator : IMemberTranslator, IMethodCallTranslator | ||
{ | ||
static readonly MemberInfo RootElement = typeof(JsonDocument).GetProperty(nameof(JsonDocument.RootElement)); | ||
static readonly MethodInfo GetProperty = typeof(JsonElement).GetRuntimeMethod(nameof(JsonElement.GetProperty), new[] { typeof(string) }); | ||
static readonly MethodInfo GetArrayLength = typeof(JsonElement).GetRuntimeMethod(nameof(JsonElement.GetArrayLength), Type.EmptyTypes); | ||
|
||
static readonly MethodInfo ArrayIndexer = typeof(JsonElement).GetProperties() | ||
.Single(p => p.GetIndexParameters().Length == 1 && p.GetIndexParameters()[0].ParameterType == typeof(int)) | ||
.GetMethod; | ||
|
||
static readonly string[] GetMethods = | ||
{ | ||
nameof(JsonElement.GetBoolean), | ||
nameof(JsonElement.GetDateTime), | ||
nameof(JsonElement.GetDateTimeOffset), | ||
nameof(JsonElement.GetDecimal), | ||
nameof(JsonElement.GetDouble), | ||
nameof(JsonElement.GetGuid), | ||
nameof(JsonElement.GetInt16), | ||
nameof(JsonElement.GetInt32), | ||
nameof(JsonElement.GetInt64), | ||
nameof(JsonElement.GetSingle), | ||
nameof(JsonElement.GetString) | ||
}; | ||
|
||
readonly NpgsqlSqlExpressionFactory _sqlExpressionFactory; | ||
readonly RelationalTypeMapping _stringTypeMapping; | ||
readonly RelationalTypeMapping _boolTypeMapping; | ||
readonly RelationalTypeMapping _jsonbTypeMapping; | ||
|
||
public NpgsqlJsonDomTranslator(NpgsqlSqlExpressionFactory sqlExpressionFactory, IRelationalTypeMappingSource typeMappingSource) | ||
{ | ||
_sqlExpressionFactory = sqlExpressionFactory; | ||
_stringTypeMapping = typeMappingSource.FindMapping(typeof(string)); | ||
_boolTypeMapping = typeMappingSource.FindMapping(typeof(bool)); | ||
_jsonbTypeMapping = typeMappingSource.FindMapping("jsonb"); | ||
} | ||
|
||
public SqlExpression Translate(SqlExpression instance, MemberInfo member, Type returnType) | ||
{ | ||
if (member.DeclaringType != typeof(JsonDocument)) | ||
return null; | ||
|
||
if (member == RootElement && | ||
instance is ColumnExpression column && | ||
column.TypeMapping is NpgsqlJsonTypeMapping) | ||
{ | ||
// Simply get rid of the RootElement member access | ||
return column; | ||
} | ||
|
||
return null; | ||
} | ||
|
||
public SqlExpression Translate(SqlExpression instance, MethodInfo method, IReadOnlyList<SqlExpression> arguments) | ||
=> TranslateJsonElementMethod(instance, method, arguments) ?? | ||
TranslateDbFunctionsMethod(method, arguments); | ||
|
||
public SqlExpression TranslateJsonElementMethod(SqlExpression instance, MethodInfo method, IReadOnlyList<SqlExpression> arguments) | ||
{ | ||
if (method.DeclaringType != typeof(JsonElement) || | ||
!(instance.TypeMapping is NpgsqlJsonTypeMapping mapping)) | ||
{ | ||
return null; | ||
} | ||
|
||
if (method == GetProperty || method == ArrayIndexer) | ||
{ | ||
// The first time we see a JSON traversal it's on a column - create a JsonTraversalExpression. | ||
// Traversals on top of that get appended into the same expression. | ||
return instance is ColumnExpression columnExpression | ||
? _sqlExpressionFactory.JsonTraversal(columnExpression, arguments, false, typeof(string), mapping) | ||
: instance is JsonTraversalExpression prevPathTraversal | ||
? prevPathTraversal.Append(_sqlExpressionFactory.ApplyDefaultTypeMapping(arguments[0])) | ||
: null; | ||
} | ||
|
||
if (GetMethods.Contains(method.Name) && | ||
arguments.Count == 0 && | ||
instance is JsonTraversalExpression traversal) | ||
{ | ||
var traversalToText = new JsonTraversalExpression( | ||
traversal.Expression, | ||
traversal.Path, | ||
returnsText: true, | ||
typeof(string), | ||
_stringTypeMapping); | ||
|
||
return method.Name == nameof(JsonElement.GetString) | ||
? traversalToText | ||
: ConvertFromText(traversalToText, method.ReturnType); | ||
} | ||
|
||
if (method == GetArrayLength) | ||
{ | ||
return _sqlExpressionFactory.Function( | ||
mapping.IsJsonb ? "jsonb_array_length" : "json_array_length", | ||
new[] { instance }, typeof(int)); | ||
} | ||
|
||
if (method.Name.StartsWith("TryGet") && arguments.Count == 0) | ||
throw new InvalidOperationException($"The TryGet* methods on {nameof(JsonElement)} aren't translated yet, use Get* instead.'"); | ||
|
||
return null; | ||
} | ||
|
||
public SqlExpression TranslateDbFunctionsMethod(MethodInfo method, IReadOnlyList<SqlExpression> arguments) | ||
{ | ||
if (arguments.Any(a => a.TypeMapping is NpgsqlJsonTypeMapping jsonMapping && !jsonMapping.IsJsonb)) | ||
throw new InvalidOperationException("JSON methods on EF.Functions only support the jsonb type, not json."); | ||
|
||
return method.Name switch | ||
{ | ||
nameof(NpgsqlJsonDbFunctionsExtensions.JsonContains) => new SqlCustomBinaryExpression( | ||
_sqlExpressionFactory.ApplyDefaultTypeMapping(arguments[1]), | ||
_sqlExpressionFactory.ApplyDefaultTypeMapping(arguments[2]), | ||
"@>", | ||
typeof(bool), | ||
_boolTypeMapping), | ||
|
||
nameof(NpgsqlJsonDbFunctionsExtensions.JsonContained) => new SqlCustomBinaryExpression( | ||
_sqlExpressionFactory.ApplyDefaultTypeMapping(arguments[1]), | ||
_sqlExpressionFactory.ApplyDefaultTypeMapping(arguments[2]), | ||
"<@", | ||
typeof(bool), | ||
_boolTypeMapping), | ||
|
||
nameof(NpgsqlJsonDbFunctionsExtensions.JsonExists) => new SqlCustomBinaryExpression( | ||
_sqlExpressionFactory.ApplyDefaultTypeMapping(arguments[1]), | ||
_sqlExpressionFactory.ApplyDefaultTypeMapping(arguments[2]), | ||
"?", | ||
typeof(bool), | ||
_boolTypeMapping), | ||
|
||
nameof(NpgsqlJsonDbFunctionsExtensions.JsonExistAny) => new SqlCustomBinaryExpression( | ||
_sqlExpressionFactory.ApplyDefaultTypeMapping(arguments[1]), | ||
_sqlExpressionFactory.ApplyDefaultTypeMapping(arguments[2]), | ||
"?|", | ||
typeof(bool), | ||
_boolTypeMapping), | ||
|
||
nameof(NpgsqlJsonDbFunctionsExtensions.JsonExistAll) => new SqlCustomBinaryExpression( | ||
_sqlExpressionFactory.ApplyDefaultTypeMapping(arguments[1]), | ||
_sqlExpressionFactory.ApplyDefaultTypeMapping(arguments[2]), | ||
"?&", | ||
typeof(bool), | ||
_boolTypeMapping), | ||
|
||
_ => null | ||
}; | ||
} | ||
|
||
// The PostgreSQL traversal operator always returns text, so we need to convert to int, bool, etc. | ||
SqlExpression ConvertFromText(SqlExpression expression, Type returnType) | ||
{ | ||
switch (Type.GetTypeCode(returnType)) | ||
{ | ||
case TypeCode.Boolean: | ||
case TypeCode.Byte: | ||
case TypeCode.DateTime: | ||
case TypeCode.Decimal: | ||
case TypeCode.Double: | ||
case TypeCode.Int16: | ||
case TypeCode.Int32: | ||
case TypeCode.Int64: | ||
case TypeCode.SByte: | ||
case TypeCode.Single: | ||
case TypeCode.UInt16: | ||
case TypeCode.UInt32: | ||
case TypeCode.UInt64: | ||
return _sqlExpressionFactory.Convert(expression, returnType, _sqlExpressionFactory.FindMapping(returnType)); | ||
default: | ||
return expression; | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.