-
Notifications
You must be signed in to change notification settings - Fork 225
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
7952315
commit 6f21661
Showing
11 changed files
with
2,688 additions
and
258 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
232 changes: 232 additions & 0 deletions
232
src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayFragmentTranslator.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,232 @@ | ||
using System; | ||
using System.Collections; | ||
using System.Collections.Generic; | ||
using System.Linq.Expressions; | ||
using System.Reflection; | ||
using JetBrains.Annotations; | ||
using Microsoft.EntityFrameworkCore; | ||
using Microsoft.EntityFrameworkCore.Query.ExpressionTranslators; | ||
using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal; | ||
using Remotion.Linq; | ||
using Remotion.Linq.Clauses; | ||
using Remotion.Linq.Clauses.Expressions; | ||
using Remotion.Linq.Clauses.ResultOperators; | ||
|
||
namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Internal | ||
{ | ||
/// <summary> | ||
/// Provides translation services for array fragments. | ||
/// </summary> | ||
public class NpgsqlArrayFragmentTranslator : IExpressionFragmentTranslator | ||
{ | ||
#region MethodInfoFields | ||
|
||
/// <summary> | ||
/// The <see cref="MethodInfo"/> for <see cref="DbFunctionsExtensions.Like(DbFunctions,string,string)"/>. | ||
/// </summary> | ||
[NotNull] static readonly MethodInfo Like2MethodInfo = | ||
typeof(DbFunctionsExtensions) | ||
.GetRuntimeMethod(nameof(DbFunctionsExtensions.Like), new[] { typeof(DbFunctions), typeof(string), typeof(string) }); | ||
|
||
/// <summary> | ||
/// The <see cref="MethodInfo"/> for <see cref="DbFunctionsExtensions.Like(DbFunctions,string,string, string)"/>. | ||
/// </summary> | ||
[NotNull] static readonly MethodInfo Like3MethodInfo = | ||
typeof(DbFunctionsExtensions) | ||
.GetRuntimeMethod(nameof(DbFunctionsExtensions.Like), new[] { typeof(DbFunctions), typeof(string), typeof(string), typeof(string) }); | ||
|
||
// ReSharper disable once InconsistentNaming | ||
/// <summary> | ||
/// The <see cref="MethodInfo"/> for <see cref="NpgsqlDbFunctionsExtensions.ILike(DbFunctions,string,string)"/>. | ||
/// </summary> | ||
[NotNull] static readonly MethodInfo ILike2MethodInfo = | ||
typeof(NpgsqlDbFunctionsExtensions) | ||
.GetRuntimeMethod(nameof(NpgsqlDbFunctionsExtensions.ILike), new[] { typeof(DbFunctions), typeof(string), typeof(string) }); | ||
|
||
// ReSharper disable once InconsistentNaming | ||
/// <summary> | ||
/// The <see cref="MethodInfo"/> for <see cref="NpgsqlDbFunctionsExtensions.ILike(DbFunctions,string,string,string)"/>. | ||
/// </summary> | ||
[NotNull] static readonly MethodInfo ILike3MethodInfo = | ||
typeof(NpgsqlDbFunctionsExtensions) | ||
.GetRuntimeMethod(nameof(NpgsqlDbFunctionsExtensions.ILike), new[] { typeof(DbFunctions), typeof(string), typeof(string), typeof(string) }); | ||
|
||
#endregion | ||
|
||
/// <inheritdoc /> | ||
[CanBeNull] | ||
public Expression Translate(Expression expression) | ||
{ | ||
if (!(expression is SubQueryExpression subQuery)) | ||
return null; | ||
|
||
var model = subQuery.QueryModel; | ||
|
||
if (!IsArrayOrList(model.MainFromClause.FromExpression.Type)) | ||
return null; | ||
|
||
return | ||
AllResult(model) ?? | ||
AnyResult(model) ?? | ||
ConcatResult(model) ?? | ||
CountResult(model); | ||
} | ||
|
||
#region SubQueries | ||
|
||
/// <summary> | ||
/// Visits an array-based ALL expression. | ||
/// </summary> | ||
/// <param name="model">The query model to visit.</param> | ||
/// <returns> | ||
/// An expression or null. | ||
/// </returns> | ||
[CanBeNull] | ||
static Expression AllResult([NotNull] QueryModel model) | ||
{ | ||
Expression array = model.MainFromClause.FromExpression; | ||
|
||
// TODO: when is there more than one result operator? | ||
// Only handle singular result operators. | ||
if (model.ResultOperators.Count == 1 && model.ResultOperators[0] is AllResultOperator all) | ||
return ConstructArrayLike(array, all.Predicate, ArrayComparisonType.ALL); | ||
|
||
return null; | ||
} | ||
|
||
/// <summary> | ||
/// Visits an array-based ANY expression. | ||
/// </summary> | ||
/// <param name="model">The query model to visit.</param> | ||
/// <returns> | ||
/// An expression or null. | ||
/// </returns> | ||
[CanBeNull] | ||
static Expression AnyResult([NotNull] QueryModel model) | ||
{ | ||
Expression array = model.MainFromClause.FromExpression; | ||
|
||
// TODO: when is there more than one result operator? | ||
// Only handle singular result operators. | ||
if (model.ResultOperators.Count != 1 || !(model.ResultOperators[0] is AnyResultOperator _)) | ||
return null; | ||
|
||
if (model.BodyClauses.Count == 1 && model.BodyClauses[0] is WhereClause where) | ||
return ConstructArrayLike(array, where.Predicate, ArrayComparisonType.ANY); | ||
|
||
return null; | ||
} | ||
|
||
/// <summary> | ||
/// Visits an array-based concatenation expression: {array|value} || {array|value}. | ||
/// </summary> | ||
/// <param name="model">The query model to visit.</param> | ||
/// <returns> | ||
/// An expression or null. | ||
/// </returns> | ||
[CanBeNull] | ||
static Expression ConcatResult([NotNull] QueryModel model) | ||
{ | ||
if (model.BodyClauses.Count != 0) | ||
return null; | ||
|
||
if (model.ResultOperators.Count != 1) | ||
return null; | ||
|
||
if (!(model.ResultOperators[0] is ConcatResultOperator concat)) | ||
return null; | ||
|
||
Expression from = model.MainFromClause.FromExpression; | ||
|
||
Expression other = concat.Source2; | ||
|
||
if (!IsArrayOrList(other.Type)) | ||
return null; | ||
|
||
return new CustomBinaryExpression(from, other, "||", from.Type); | ||
} | ||
|
||
/// <summary> | ||
/// Visits an array-based count expression: {array}.Length, {list}.Count, {array|list}.Count(), {array|list}.Count({predicate}). | ||
/// </summary> | ||
/// <param name="model">The query model to visit.</param> | ||
/// <returns> | ||
/// An expression or null. | ||
/// </returns> | ||
[CanBeNull] | ||
static Expression CountResult([NotNull] QueryModel model) | ||
{ | ||
// TODO: handle count operation with predicate. | ||
if (model.BodyClauses.Count != 0) | ||
return null; | ||
|
||
if (model.ResultOperators.Count != 1) | ||
return null; | ||
|
||
if (!(model.ResultOperators[0] is CountResultOperator _)) | ||
return null; | ||
|
||
Expression from = model.MainFromClause.FromExpression; | ||
|
||
return | ||
from.Type.IsArray | ||
? Expression.MakeMemberAccess(from, from.Type.GetRuntimeProperty(nameof(Array.Length))) | ||
: Expression.MakeMemberAccess(from, from.Type.GetRuntimeProperty(nameof(IList.Count))); | ||
} | ||
|
||
/// <summary> | ||
/// Visits an array-based comparison for an LIKE or ILIKE expression: {operand} {LIKE|ILIKE} {ANY|ALL} ({array}). | ||
/// </summary> | ||
/// <param name="array">The array expression.</param> | ||
/// <param name="predicate">The method call expression.</param> | ||
/// <param name="comparisonType">The array comparison type.</param> | ||
/// <returns> | ||
/// An expression or null. | ||
/// </returns> | ||
[CanBeNull] | ||
static Expression ConstructArrayLike([NotNull] Expression array, [CanBeNull] Expression predicate, ArrayComparisonType comparisonType) | ||
{ | ||
if (!(predicate is MethodCallExpression call)) | ||
return null; | ||
|
||
if (call.Arguments.Count < 2) | ||
return null; | ||
|
||
Expression operand = call.Arguments[1]; | ||
Expression collection = array; | ||
|
||
switch (call.Method) | ||
{ | ||
case MethodInfo m when m == Like2MethodInfo: | ||
return new ArrayAnyAllExpression(comparisonType, "LIKE", operand, collection); | ||
|
||
case MethodInfo m when m == Like3MethodInfo: | ||
return new ArrayAnyAllExpression(comparisonType, "LIKE", operand, collection); | ||
|
||
case MethodInfo m when m == ILike2MethodInfo: | ||
return new ArrayAnyAllExpression(comparisonType, "ILIKE", operand, collection); | ||
|
||
case MethodInfo m when m == ILike3MethodInfo: | ||
return new ArrayAnyAllExpression(comparisonType, "ILIKE", operand, collection); | ||
|
||
default: | ||
return null; | ||
} | ||
} | ||
|
||
#endregion | ||
|
||
#region Helpers | ||
|
||
/// <summary> | ||
/// Tests if the type is an array or a <see cref="List{T}"/>. | ||
/// </summary> | ||
/// <param name="type">The type to test.</param> | ||
/// <returns> | ||
/// True if <paramref name="type"/> is an array or a <see cref="List{T}"/>; otherwise, false. | ||
/// </returns> | ||
static bool IsArrayOrList([NotNull] Type type) => type.IsArray || type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>); | ||
|
||
#endregion | ||
} | ||
} |
140 changes: 140 additions & 0 deletions
140
src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayMemberTranslator.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,140 @@ | ||
using System; | ||
using System.Collections; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Linq.Expressions; | ||
using JetBrains.Annotations; | ||
using Microsoft.EntityFrameworkCore.Query.Expressions; | ||
using Microsoft.EntityFrameworkCore.Query.ExpressionTranslators; | ||
|
||
namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Internal | ||
{ | ||
/// <summary> | ||
/// Provides translation services for PostgreSQL array operators mapped to generic array members. | ||
/// </summary> | ||
/// <remarks> | ||
/// See: https://www.postgresql.org/docs/current/static/functions-array.html | ||
/// </remarks> | ||
public class NpgsqlArrayMemberTranslator : IMemberTranslator | ||
{ | ||
/// <summary> | ||
/// The backend version to target. | ||
/// </summary> | ||
[CanBeNull] readonly Version _postgresVersion; | ||
|
||
/// <summary> | ||
/// Initializes a new instance of the <see cref="NpgsqlArrayMemberTranslator"/> class. | ||
/// </summary> | ||
/// <param name="postgresVersion">The backend version to target.</param> | ||
public NpgsqlArrayMemberTranslator([CanBeNull] Version postgresVersion) => _postgresVersion = postgresVersion; | ||
|
||
/// <inheritdoc /> | ||
public Expression Translate(MemberExpression expression) | ||
=> ArrayInstanceHandler(expression) ?? ListInstanceHandler(expression); | ||
|
||
#region Handlers | ||
|
||
/// <summary> | ||
/// Translates instance members defined on <see cref="Array"/>. | ||
/// </summary> | ||
/// <param name="expression">The expression to translate.</param> | ||
/// <returns> | ||
/// A translated expression or null if no translation is supported. | ||
/// </returns> | ||
[CanBeNull] | ||
Expression ArrayInstanceHandler([NotNull] MemberExpression expression) | ||
{ | ||
var instance = expression.Expression; | ||
|
||
if (instance is null || !instance.Type.IsArray) | ||
return null; | ||
|
||
switch (expression.Member.Name) | ||
{ | ||
case nameof(Array.Length) when VersionAtLeast(9, 4): | ||
return new SqlFunctionExpression("cardinality", typeof(int), new[] { instance }); | ||
|
||
case nameof(Array.Length) when VersionAtLeast(8, 4) && instance.Type.GetArrayRank() == 1: | ||
return | ||
Expression.Coalesce( | ||
new SqlFunctionExpression( | ||
"array_length", | ||
typeof(int?), | ||
new[] { instance, Expression.Constant(1) }), | ||
Expression.Constant(0)); | ||
|
||
case nameof(Array.Length) when VersionAtLeast(8, 4): | ||
return | ||
Enumerable.Range(1, instance.Type.GetArrayRank()) | ||
.Select(x => | ||
Expression.Coalesce( | ||
new SqlFunctionExpression( | ||
"array_length", | ||
typeof(int?), | ||
new[] { instance, Expression.Constant(x) }), | ||
Expression.Constant(0))) | ||
.Cast<Expression>() | ||
.Aggregate(Expression.Multiply); | ||
|
||
case nameof(Array.Rank) when VersionAtLeast(8, 4): | ||
return | ||
Expression.Coalesce( | ||
new SqlFunctionExpression( | ||
"array_ndims", | ||
typeof(int?), | ||
new[] { instance }), | ||
Expression.Constant(1)); | ||
|
||
default: | ||
return null; | ||
} | ||
} | ||
|
||
/// <summary> | ||
/// Translates instance members defined on <see cref="List{T}"/>. | ||
/// </summary> | ||
/// <param name="expression">The expression to translate.</param> | ||
/// <returns> | ||
/// A translated expression or null if no translation is supported. | ||
/// </returns> | ||
[CanBeNull] | ||
Expression ListInstanceHandler([NotNull] MemberExpression expression) | ||
{ | ||
var instance = expression.Expression; | ||
|
||
if (instance is null || !instance.Type.IsGenericType || instance.Type.GetGenericTypeDefinition() != typeof(List<>)) | ||
return null; | ||
|
||
switch (expression.Member.Name) | ||
{ | ||
case nameof(IList.Count) when VersionAtLeast(8, 4): | ||
return | ||
Expression.Coalesce( | ||
new SqlFunctionExpression( | ||
"array_length", | ||
typeof(int?), | ||
new Expression[] { instance, Expression.Constant(1) }), | ||
Expression.Constant(0)); | ||
|
||
default: | ||
return null; | ||
} | ||
} | ||
|
||
#endregion | ||
|
||
#region Helpers | ||
|
||
/// <summary> | ||
/// True if <see cref="_postgresVersion"/> is null, greater than, or equal to the specified version. | ||
/// </summary> | ||
/// <param name="major">The major version.</param> | ||
/// <param name="minor">The minor version.</param> | ||
/// <returns> | ||
/// True if <see cref="_postgresVersion"/> is null, greater than, or equal to the specified version. | ||
/// </returns> | ||
bool VersionAtLeast(int major, int minor) => _postgresVersion is null || new Version(major, minor) <= _postgresVersion; | ||
|
||
#endregion | ||
} | ||
} |
Oops, something went wrong.