using System.Collections.ObjectModel; using Npgsql.EntityFrameworkCore.PostgreSQL.Internal; using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions; using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal; using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Internal; using static Npgsql.EntityFrameworkCore.PostgreSQL.Utilities.Statics; namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Internal; public class NpgsqlLTreeTranslator : IMethodCallTranslator, IMemberTranslator { private readonly NpgsqlSqlExpressionFactory _sqlExpressionFactory; private readonly RelationalTypeMapping _boolTypeMapping; private readonly RelationalTypeMapping _ltreeTypeMapping; private readonly RelationalTypeMapping _ltreeArrayTypeMapping; private readonly RelationalTypeMapping _lqueryTypeMapping; private readonly RelationalTypeMapping _lqueryArrayTypeMapping; private readonly RelationalTypeMapping _ltxtqueryTypeMapping; private static readonly MethodInfo IsAncestorOf = typeof(LTree).GetRuntimeMethod(nameof(LTree.IsAncestorOf), new[] { typeof(LTree) })!; private static readonly MethodInfo IsDescendantOf = typeof(LTree).GetRuntimeMethod(nameof(LTree.IsDescendantOf), new[] { typeof(LTree) })!; private static readonly MethodInfo MatchesLQuery = typeof(LTree).GetRuntimeMethod(nameof(LTree.MatchesLQuery), new[] { typeof(string) })!; private static readonly MethodInfo MatchesLTxtQuery = typeof(LTree).GetRuntimeMethod(nameof(LTree.MatchesLTxtQuery), new[] { typeof(string) })!; public NpgsqlLTreeTranslator( IRelationalTypeMappingSource typeMappingSource, NpgsqlSqlExpressionFactory sqlExpressionFactory, IModel model) { _sqlExpressionFactory = sqlExpressionFactory; _boolTypeMapping = typeMappingSource.FindMapping(typeof(bool), model)!; _ltreeTypeMapping = typeMappingSource.FindMapping(typeof(LTree), model)!; _ltreeArrayTypeMapping = typeMappingSource.FindMapping(typeof(LTree[]), model)!; _lqueryTypeMapping = typeMappingSource.FindMapping("lquery")!; _lqueryArrayTypeMapping = typeMappingSource.FindMapping("lquery[]")!; _ltxtqueryTypeMapping = typeMappingSource.FindMapping("ltxtquery")!; } /// public virtual SqlExpression? Translate( SqlExpression? instance, MethodInfo method, IReadOnlyList arguments, IDiagnosticsLogger logger) { if (method.DeclaringType == typeof(LTree)) { return method.Name switch { nameof(LTree.IsAncestorOf) => new PostgresBinaryExpression( PostgresExpressionType.Contains, ApplyTypeMappingOrConvert(instance!, _ltreeTypeMapping), ApplyTypeMappingOrConvert(arguments[0], _ltreeTypeMapping), typeof(bool), _boolTypeMapping), nameof(LTree.IsDescendantOf) => new PostgresBinaryExpression( PostgresExpressionType.ContainedBy, ApplyTypeMappingOrConvert(instance!, _ltreeTypeMapping), ApplyTypeMappingOrConvert(arguments[0], _ltreeTypeMapping), typeof(bool), _boolTypeMapping), nameof(LTree.MatchesLQuery) => new PostgresBinaryExpression( PostgresExpressionType.LTreeMatches, ApplyTypeMappingOrConvert(instance!, _ltreeTypeMapping), ApplyTypeMappingOrConvert(arguments[0], _lqueryTypeMapping), typeof(bool), _boolTypeMapping), nameof(LTree.MatchesLTxtQuery) => new PostgresBinaryExpression( PostgresExpressionType.LTreeMatches, ApplyTypeMappingOrConvert(instance!, _ltreeTypeMapping), ApplyTypeMappingOrConvert(arguments[0], _ltxtqueryTypeMapping), typeof(bool), _boolTypeMapping), nameof(LTree.Subtree) => _sqlExpressionFactory.Function( "subltree", new[] { instance!, arguments[0], arguments[1] }, nullable: true, TrueArrays[3], typeof(LTree), _ltreeTypeMapping), nameof(LTree.Subpath) => _sqlExpressionFactory.Function( "subpath", arguments.Count == 2 ? new[] { instance!, arguments[0], arguments[1] } : new[] { instance!, arguments[0] }, nullable: true, arguments.Count == 2 ? TrueArrays[3] : TrueArrays[2], typeof(LTree), _ltreeTypeMapping), nameof(LTree.Index) => _sqlExpressionFactory.Function( "index", arguments.Count == 2 ? new[] { instance!, arguments[0], arguments[1] } : new[] { instance!, arguments[0] }, nullable: true, arguments.Count == 2 ? TrueArrays[3] : TrueArrays[2], typeof(int)), nameof(LTree.LongestCommonAncestor) => _sqlExpressionFactory.Function( "lca", new[] { arguments[0] }, nullable: true, TrueArrays[1], typeof(LTree), _ltreeTypeMapping), _ => null }; } return null; } public virtual SqlExpression? Translate( SqlExpression? instance, MemberInfo member, Type returnType, IDiagnosticsLogger logger) => member.DeclaringType == typeof(LTree) && member.Name == nameof(LTree.NLevel) ? _sqlExpressionFactory.Function( "nlevel", new[] { instance! }, nullable: true, TrueArrays[1], typeof(int)) : null; /// /// Called directly from to translate LTree array-related constructs which /// cannot be translated in regular method translators, since they require accessing lambdas. /// public virtual Expression? VisitArrayMethodCall( NpgsqlSqlTranslatingExpressionVisitor sqlTranslatingExpressionVisitor, MethodInfo method, ReadOnlyCollection arguments) { var array = arguments[0]; { if (method.IsClosedFormOf(EnumerableMethods.AnyWithPredicate) && arguments[1] is LambdaExpression wherePredicate && wherePredicate.Body is MethodCallExpression wherePredicateMethodCall) { var predicateMethod = wherePredicateMethodCall.Method; var predicateInstance = wherePredicateMethodCall.Object!; var predicateArguments = wherePredicateMethodCall.Arguments; // Pattern match: new[] { "q1", "q2" }.Any(q => e.SomeLTree.MatchesLQuery(q)) // Translation: s.SomeLTree ? ARRAY['q1','q2'] if (predicateMethod == MatchesLQuery && predicateArguments[0] == wherePredicate.Parameters[0]) { return new PostgresBinaryExpression( PostgresExpressionType.LTreeMatchesAny, ApplyTypeMappingOrConvert(Visit(predicateInstance), _ltreeTypeMapping), _sqlExpressionFactory.ApplyTypeMapping(Visit(array), _lqueryArrayTypeMapping), typeof(bool), _boolTypeMapping); } // Pattern match: new[] { "t1", "t2" }.Any(t => t.IsAncestorOf(e.SomeLTree)) // Translation: ARRAY['t1','t2'] @> s.SomeLTree if (predicateMethod == IsAncestorOf && predicateInstance == wherePredicate.Parameters[0]) { return new PostgresBinaryExpression( PostgresExpressionType.Contains, _sqlExpressionFactory.ApplyTypeMapping(Visit(array), _ltreeArrayTypeMapping), ApplyTypeMappingOrConvert(Visit(predicateArguments[0]), _ltreeTypeMapping), typeof(bool), _boolTypeMapping); } // Pattern match: new[] { "t1", "t2" }.Any(t => t.IsDescendantOf(e.SomeLTree)) // Translation: s.SomeLTree <@ ARRAY['t1','t2'] if (predicateMethod == IsDescendantOf && predicateInstance == wherePredicate.Parameters[0]) { return new PostgresBinaryExpression( PostgresExpressionType.ContainedBy, _sqlExpressionFactory.ApplyTypeMapping(Visit(array), _ltreeArrayTypeMapping), ApplyTypeMappingOrConvert(Visit(predicateArguments[0]), _ltreeTypeMapping), typeof(bool), _boolTypeMapping); } // Pattern match: new[] { "t1", "t2" }.Any(t => t.MatchesLQuery(lquery)) // Translation: ARRAY['t1','t2'] ~ lquery if (predicateMethod == MatchesLQuery && predicateInstance == wherePredicate.Parameters[0]) { return new PostgresBinaryExpression( PostgresExpressionType.LTreeMatches, _sqlExpressionFactory.ApplyTypeMapping(Visit(array), _ltreeArrayTypeMapping), ApplyTypeMappingOrConvert(Visit(predicateArguments[0]), _lqueryTypeMapping), typeof(bool), _boolTypeMapping); } // Pattern match: new[] { "t1", "t2" }.Any(t => t.MatchesLTxtQuery(ltxtquery)) // Translation: ARRAY['t1','t2'] @ ltxtquery if (predicateMethod == MatchesLTxtQuery && predicateInstance == wherePredicate.Parameters[0]) { return new PostgresBinaryExpression( PostgresExpressionType.LTreeMatches, _sqlExpressionFactory.ApplyTypeMapping(Visit(array), _ltreeArrayTypeMapping), ApplyTypeMappingOrConvert(Visit(predicateArguments[0]), _ltxtqueryTypeMapping), typeof(bool), _boolTypeMapping); } // Any within Any (i.e. intersection) if (predicateMethod.IsClosedFormOf(EnumerableMethods.AnyWithPredicate) && predicateArguments[1] is LambdaExpression nestedWherePredicate && nestedWherePredicate.Body is MethodCallExpression nestedWherePredicateMethodCall) { var nestedPredicateMethod = nestedWherePredicateMethodCall.Method; var nestedPredicateInstance = nestedWherePredicateMethodCall.Object; var nestedPredicateArguments = nestedWherePredicateMethodCall.Arguments; // Pattern match: new[] { "t1", "t2" }.Any(t => lqueries.Any(q => t.MatchesLQuery(q))) // Translation: ARRAY['t1','t2'] ~ ARRAY['q1', 'q2'] if (nestedPredicateMethod == MatchesLQuery && nestedPredicateInstance == wherePredicate.Parameters[0] && nestedPredicateArguments[0] == nestedWherePredicate.Parameters[0]) { return new PostgresBinaryExpression( PostgresExpressionType.LTreeMatchesAny, _sqlExpressionFactory.ApplyTypeMapping(Visit(array), _ltreeArrayTypeMapping), _sqlExpressionFactory.ApplyTypeMapping(Visit(predicateArguments[0]), _lqueryArrayTypeMapping), typeof(bool), _boolTypeMapping); } } } } { if (method.IsClosedFormOf(EnumerableMethods.FirstOrDefaultWithPredicate) && arguments[1] is LambdaExpression wherePredicate && wherePredicate.Body is MethodCallExpression wherePredicateMethodCall) { var predicateMethod = wherePredicateMethodCall.Method; var predicateInstance = wherePredicateMethodCall.Object; var predicateArguments = wherePredicateMethodCall.Arguments; // Pattern match: new[] { "t1", "t2" }.FirstOrDefault(t => t.IsAncestorOf(e.SomeLTree)) // Translation: ARRAY['t1','t2'] ?@> e.SomeLTree if (predicateMethod == IsAncestorOf && predicateInstance == wherePredicate.Parameters[0]) { return new PostgresBinaryExpression( PostgresExpressionType.LTreeFirstAncestor, _sqlExpressionFactory.ApplyTypeMapping(Visit(array), _ltreeArrayTypeMapping), ApplyTypeMappingOrConvert(Visit(predicateArguments[0]), _ltreeTypeMapping), typeof(LTree), _ltreeTypeMapping); } // Pattern match: new[] { "t1", "t2" }.FirstOrDefault(t => t.IsDescendant(e.SomeLTree)) // Translation: ARRAY['t1','t2'] ?<@ e.SomeLTree if (predicateMethod == IsDescendantOf && predicateInstance == wherePredicate.Parameters[0]) { return new PostgresBinaryExpression( PostgresExpressionType.LTreeFirstDescendent, _sqlExpressionFactory.ApplyTypeMapping(Visit(array), _ltreeArrayTypeMapping), ApplyTypeMappingOrConvert(Visit(predicateArguments[0]), _ltreeTypeMapping), typeof(LTree), _ltreeTypeMapping); } // Pattern match: new[] { "t1", "t2" }.FirstOrDefault(t => t.MatchesLQuery(lquery)) // Translation: ARRAY['t1','t2'] ?~ e.lquery if (predicateMethod == MatchesLQuery && predicateInstance == wherePredicate.Parameters[0]) { return new PostgresBinaryExpression( PostgresExpressionType.LTreeFirstMatches, _sqlExpressionFactory.ApplyTypeMapping(Visit(array), _ltreeArrayTypeMapping), ApplyTypeMappingOrConvert(Visit(predicateArguments[0]), _lqueryTypeMapping), typeof(LTree), _ltreeTypeMapping); } // Pattern match: new[] { "t1", "t2" }.FirstOrDefault(t => t.MatchesLQuery(ltxtquery)) // Translation: ARRAY['t1','t2'] ?@ e.ltxtquery if (predicateMethod == MatchesLTxtQuery && predicateInstance == wherePredicate.Parameters[0]) { return new PostgresBinaryExpression( PostgresExpressionType.LTreeFirstMatches, _sqlExpressionFactory.ApplyTypeMapping(Visit(array), _ltreeArrayTypeMapping), ApplyTypeMappingOrConvert(Visit(predicateArguments[0]), _ltxtqueryTypeMapping), typeof(string), _ltreeTypeMapping); } } } return null; SqlExpression Visit(Expression expression) => (SqlExpression)sqlTranslatingExpressionVisitor.Visit(expression); } // Applying e.g. the LQuery type mapping on a function operator is a bit tricky. // If it's a constant, we can just apply the mapping: the constant will get rendered as an untyped string literal, and PG will // coerce it as the function parameter. // If it's a parameter, we can also just apply the mapping (which causes NpgsqlDbType to be set to LQuery). // For anything else, we may need an explicit cast to LQuery, e.g. a plain text column or a concatenation between strings; // apply the default type mapping and then apply an additional Convert node if the resulting mapping isn't what we need. private SqlExpression ApplyTypeMappingOrConvert(SqlExpression sqlExpression, RelationalTypeMapping typeMapping) => sqlExpression is SqlConstantExpression or SqlParameterExpression ? _sqlExpressionFactory.ApplyTypeMapping(sqlExpression, typeMapping) : _sqlExpressionFactory.ApplyDefaultTypeMapping(sqlExpression) is var expressionWithDefaultTypeMapping && expressionWithDefaultTypeMapping.TypeMapping!.StoreType == typeMapping.StoreType ? expressionWithDefaultTypeMapping : _sqlExpressionFactory.Convert(expressionWithDefaultTypeMapping, typeMapping.ClrType, typeMapping); }