using System.Diagnostics.CodeAnalysis; using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions; using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal; using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; using static Npgsql.EntityFrameworkCore.PostgreSQL.Utilities.Statics; namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.Internal; /// /// 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. /// public class NpgsqlQueryableMethodTranslatingExpressionVisitor : RelationalQueryableMethodTranslatingExpressionVisitor { private readonly NpgsqlTypeMappingSource _typeMappingSource; private readonly NpgsqlSqlExpressionFactory _sqlExpressionFactory; #region MethodInfos private static readonly MethodInfo Like2MethodInfo = typeof(DbFunctionsExtensions).GetRuntimeMethod( nameof(DbFunctionsExtensions.Like), new[] { typeof(DbFunctions), typeof(string), typeof(string) })!; // ReSharper disable once InconsistentNaming private static readonly MethodInfo ILike2MethodInfo = typeof(NpgsqlDbFunctionsExtensions).GetRuntimeMethod( nameof(NpgsqlDbFunctionsExtensions.ILike), new[] { typeof(DbFunctions), typeof(string), typeof(string) })!; private static readonly MethodInfo MatchesLQuery = typeof(LTree).GetRuntimeMethod(nameof(LTree.MatchesLQuery), new[] { typeof(string) })!; #endregion /// /// 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. /// public NpgsqlQueryableMethodTranslatingExpressionVisitor( QueryableMethodTranslatingExpressionVisitorDependencies dependencies, RelationalQueryableMethodTranslatingExpressionVisitorDependencies relationalDependencies, QueryCompilationContext queryCompilationContext) : base(dependencies, relationalDependencies, queryCompilationContext) { _typeMappingSource = (NpgsqlTypeMappingSource)relationalDependencies.TypeMappingSource; _sqlExpressionFactory = (NpgsqlSqlExpressionFactory)relationalDependencies.SqlExpressionFactory; } /// /// 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. /// protected NpgsqlQueryableMethodTranslatingExpressionVisitor(NpgsqlQueryableMethodTranslatingExpressionVisitor parentVisitor) : base(parentVisitor) { _typeMappingSource = parentVisitor._typeMappingSource; _sqlExpressionFactory = parentVisitor._sqlExpressionFactory; } /// /// 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. /// protected override QueryableMethodTranslatingExpressionVisitor CreateSubqueryVisitor() => new NpgsqlQueryableMethodTranslatingExpressionVisitor(this); /// /// 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. /// protected override ShapedQueryExpression TranslateCollection( SqlExpression sqlExpression, RelationalTypeMapping? elementTypeMapping, string tableAlias) { var elementClrType = sqlExpression.Type.GetSequenceType(); // We support two kinds of primitive collections: the standard one with PostgreSQL arrays (where we use the unnest function), and // a special case for geometry collections, where we use SelectExpression selectExpression; // TODO: Parameters have no type mapping. We can check whether the expression type is one of the NTS geometry collection types, // though in a perfect world we'd actually infer this. In other words, when the type mapping of the element is inferred further on, // we'd replace the unnest expression with ST_Dump. We could even have a special expression type which means "indeterminate, must be // inferred". if (sqlExpression.TypeMapping is { StoreTypeNameBase: "geometry" or "geography" }) { selectExpression = new SelectExpression( new TableValuedFunctionExpression(tableAlias, "ST_Dump", new[] { sqlExpression }), "geom", elementClrType, elementTypeMapping, isColumnNullable: false); } else { // Note that for unnest we have a special expression type extending TableValuedFunctionExpression, adding the ability to provide // an explicit column name for its output (SELECT * FROM unnest(array) AS f(foo)). // This is necessary since when the column name isn't explicitly specified, it is automatically identical to the table alias // (f above); since the table alias may get uniquified by EF, this would break queries. // TODO: When we have metadata to determine if the element is nullable, pass that here to SelectExpression // Note also that with PostgreSQL unnest, the output ordering is guaranteed to be the same as the input array, so we don't need // to add ordering like in most other providers (https://www.postgresql.org/docs/current/functions-array.html) // We also don't need to apply any casts or typing, since PG arrays are fully typed (unlike e.g. a JSON string). selectExpression = new SelectExpression( new PostgresUnnestExpression(tableAlias, sqlExpression, "value"), "value", elementClrType, elementTypeMapping, isColumnNullable: null); } Expression shaperExpression = new ProjectionBindingExpression( selectExpression, new ProjectionMember(), elementClrType.MakeNullable()); if (elementClrType != shaperExpression.Type) { Check.DebugAssert( elementClrType.MakeNullable() == shaperExpression.Type, "expression.Type must be nullable of targetType"); shaperExpression = Expression.Convert(shaperExpression, elementClrType); } return new ShapedQueryExpression(selectExpression, shaperExpression); } /// /// 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. /// protected override Expression ApplyInferredTypeMappings( Expression expression, IReadOnlyDictionary<(TableExpressionBase, string), RelationalTypeMapping> inferredTypeMappings) => new NpgsqlInferredTypeMappingApplier(_typeMappingSource, _sqlExpressionFactory, inferredTypeMappings).Visit(expression); /// /// 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. /// protected override ShapedQueryExpression? TranslateAll(ShapedQueryExpression source, LambdaExpression predicate) { if (source.QueryExpression is SelectExpression { Tables: [(PostgresUnnestExpression or ValuesExpression { ColumnNames: ["_ord", "Value"] }) and var sourceTable], Predicate: null, GroupBy: [], Having: null, IsDistinct: false, Limit: null, Offset: null } && TranslateLambdaExpression(source, predicate) is { } translatedPredicate) { switch (translatedPredicate) { // Pattern match for: new[] { "a", "b", "c" }.All(p => EF.Functions.Like(e.SomeText, p)), // which we translate to WHERE s.""SomeText"" LIKE ALL (ARRAY['a','b','c']) case LikeExpression { Match: var match, Pattern: ColumnExpression pattern, EscapeChar: SqlConstantExpression { Value: "" } } when pattern.Table == sourceTable: { return BuildSimplifiedShapedQuery( source, _sqlExpressionFactory.All(match, GetArray(sourceTable), PostgresAllOperatorType.Like)); } // Pattern match for: new[] { "a", "b", "c" }.All(p => EF.Functions.Like(e.SomeText, p)), // which we translate to WHERE s.""SomeText"" LIKE ALL (ARRAY['a','b','c']) case PostgresILikeExpression { Match: var match, Pattern: ColumnExpression pattern, EscapeChar: SqlConstantExpression { Value: "" } } when pattern.Table == sourceTable: { return BuildSimplifiedShapedQuery( source, _sqlExpressionFactory.All(match, GetArray(sourceTable), PostgresAllOperatorType.ILike)); } // Pattern match for: e.SomeArray.All(p => ints.Contains(p)) over non-column, // using array containment (<@) case PostgresAnyExpression { Item: ColumnExpression sourceColumn, Array: var otherArray } when sourceColumn.Table == sourceTable: { return BuildSimplifiedShapedQuery(source, _sqlExpressionFactory.ContainedBy(GetArray(sourceTable), otherArray)); } // Pattern match for: new[] { 4, 5 }.All(p => e.SomeArray.Contains(p)) over column, // using array containment (<@) case PostgresBinaryExpression { OperatorType: PostgresExpressionType.Contains, Left: var otherArray, Right: PostgresNewArrayExpression { Expressions: [ColumnExpression sourceColumn] } } when sourceColumn.Table == sourceTable: { return BuildSimplifiedShapedQuery(source, _sqlExpressionFactory.ContainedBy(GetArray(sourceTable), otherArray)); } } } return base.TranslateAll(source, predicate); } /// /// 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. /// protected override ShapedQueryExpression? TranslateAny(ShapedQueryExpression source, LambdaExpression? predicate) { if (source.QueryExpression is SelectExpression { Tables: [(PostgresUnnestExpression or ValuesExpression { ColumnNames: ["_ord", "Value"] }) and var sourceTable], Predicate: null, GroupBy: [], Having: null, IsDistinct: false, Limit: null, Offset: null }) { // Pattern match: x.Array.Any() // Translation: cardinality(x.array) > 0 instead of EXISTS (SELECT 1 FROM FROM unnest(x.Array)) if (predicate is null) { return BuildSimplifiedShapedQuery( source, _sqlExpressionFactory.GreaterThan( _sqlExpressionFactory.Function( "cardinality", new[] { GetArray(sourceTable) }, nullable: true, argumentsPropagateNullability: TrueArrays[1], typeof(int)), _sqlExpressionFactory.Constant(0))); } var translatedPredicate = TranslateLambdaExpression(source, predicate); if (translatedPredicate is null) { return null; } // Simplify Contains / array.Any(i => i == x) // Note that most other simplifications here convert ValuesExpression to unnest over array constructor, but we avoid doing that // here, since the relational translation for ValuesExpression is better. if (sourceTable is PostgresUnnestExpression && translatedPredicate is SqlBinaryExpression { OperatorType: ExpressionType.Equal, Left: var left, Right: var right }) { var item = left is ColumnExpression leftColumn && ReferenceEquals(leftColumn.Table, sourceTable) ? right : right is ColumnExpression rightColumn && ReferenceEquals(rightColumn.Table, sourceTable) ? left : null; if (item is not null) { var array = GetArray(sourceTable); // When the array is a column, we translate Contains to array @> ARRAY[item]. GIN indexes on array are used, but null // semantics is impossible without preventing index use. switch (array) { case ColumnExpression: if (item is SqlConstantExpression { Value: null }) { // We special-case null constant item and use array_position instead, since it does // nulls correctly (but doesn't use indexes) // TODO: once lambda-based caching is implemented, move this to NpgsqlSqlNullabilityProcessor // (https://github.com/dotnet/efcore/issues/17598) and do for parameters as well. return BuildSimplifiedShapedQuery( source, _sqlExpressionFactory.IsNotNull( _sqlExpressionFactory.Function( "array_position", new[] { array, item }, nullable: true, argumentsPropagateNullability: FalseArrays[2], typeof(int)))); } return BuildSimplifiedShapedQuery( source, _sqlExpressionFactory.Contains( array, _sqlExpressionFactory.NewArrayOrConstant(new[] { item }, array.Type))); // Don't do anything PG-specific for constant arrays since the general EF Core mechanism is fine // for that case: item IN (1, 2, 3). // After https://github.com/aspnet/EntityFrameworkCore/issues/16375 is done we may not need the // check any more. case SqlConstantExpression: return null; // Similar to ParameterExpression below, but when a bare subquery is present inside ANY(), PostgreSQL just compares // against each of its resulting rows (just like IN). To "extract" the array result of the scalar subquery, we need // to add an explicit cast (see #1803). case ScalarSubqueryExpression subqueryExpression: return BuildSimplifiedShapedQuery( source, _sqlExpressionFactory.Any( item, _sqlExpressionFactory.Convert( subqueryExpression, subqueryExpression.Type, subqueryExpression.TypeMapping), PostgresAnyOperatorType.Equal)); // For ParameterExpression, and for all other cases - e.g. array returned from some function - // translate to e.SomeText = ANY (@p). This is superior to the general solution which will expand // parameters to constants, since non-PG SQL does not support arrays. // Note that this will allow indexes on the item to be used. default: return BuildSimplifiedShapedQuery( source, _sqlExpressionFactory.Any(item, array, PostgresAnyOperatorType.Equal)); } } } switch (translatedPredicate) { // Pattern match: new[] { "a", "b", "c" }.Any(p => EF.Functions.Like(e.SomeText, p)) // Translation: s.SomeText LIKE ANY (ARRAY['a','b','c']) case LikeExpression { Match: var match, Pattern: ColumnExpression pattern, EscapeChar: SqlConstantExpression { Value: "" } } when pattern.Table == sourceTable: { return BuildSimplifiedShapedQuery( source, _sqlExpressionFactory.Any(match, GetArray(sourceTable), PostgresAnyOperatorType.Like)); } // Pattern match: new[] { "a", "b", "c" }.Any(p => EF.Functions.Like(e.SomeText, p)) // Translation: s.SomeText LIKE ANY (ARRAY['a','b','c']) case PostgresILikeExpression { Match: var match, Pattern: ColumnExpression pattern, EscapeChar: SqlConstantExpression { Value: "" } } when pattern.Table == sourceTable: { return BuildSimplifiedShapedQuery( source, _sqlExpressionFactory.Any(match, GetArray(sourceTable), PostgresAnyOperatorType.ILike)); } // Array overlap over non-column // Pattern match: e.SomeArray.Any(p => ints.Contains(p)) // Translation: @ints && s.SomeArray case PostgresAnyExpression { Item: ColumnExpression sourceColumn, Array: var otherArray } when sourceColumn.Table == sourceTable: { return BuildSimplifiedShapedQuery(source, _sqlExpressionFactory.Overlaps(GetArray(sourceTable), otherArray)); } // Array overlap over column // Pattern match: new[] { 4, 5 }.Any(p => e.SomeArray.Contains(p)) // Translation: s.SomeArray && ARRAY[4, 5] case PostgresBinaryExpression { OperatorType: PostgresExpressionType.Contains, Left: var otherArray, Right: PostgresNewArrayExpression { Expressions: [ColumnExpression sourceColumn] } } when sourceColumn.Table == sourceTable: { return BuildSimplifiedShapedQuery(source, _sqlExpressionFactory.Overlaps(GetArray(sourceTable), otherArray)); } #region LTree translations // Pattern match: new[] { "q1", "q2" }.Any(q => e.SomeLTree.MatchesLQuery(q)) // Translation: s.SomeLTree ? ARRAY['q1','q2'] case PostgresBinaryExpression { OperatorType: PostgresExpressionType.LTreeMatches, Left: var ltree, Right: SqlUnaryExpression { OperatorType: ExpressionType.Convert, Operand: ColumnExpression lqueryColumn } } when lqueryColumn.Table == sourceTable: { return BuildSimplifiedShapedQuery( source, new PostgresBinaryExpression( PostgresExpressionType.LTreeMatchesAny, ltree, _sqlExpressionFactory.ApplyTypeMapping(GetArray(sourceTable), _typeMappingSource.FindMapping("lquery[]")), typeof(bool), typeMapping: _typeMappingSource.FindMapping(typeof(bool)))); } // Pattern match: new[] { "t1", "t2" }.Any(t => t.IsAncestorOf(e.SomeLTree)) // Translation: ARRAY['t1','t2'] @> s.SomeLTree // Pattern match: new[] { "t1", "t2" }.Any(t => t.IsDescendantOf(e.SomeLTree)) // Translation: ARRAY['t1','t2'] <@ s.SomeLTree case PostgresBinaryExpression { OperatorType: (PostgresExpressionType.Contains or PostgresExpressionType.ContainedBy) and var operatorType, Left: ColumnExpression ltreeColumn, // Contains/ContainedBy can happen for non-LTree types too, so check that Right: { TypeMapping: NpgsqlLTreeTypeMapping } ltree } when ltreeColumn.Table == sourceTable: { return BuildSimplifiedShapedQuery( source, new PostgresBinaryExpression( operatorType, _sqlExpressionFactory.ApplyDefaultTypeMapping(GetArray(sourceTable)), ltree, typeof(bool), typeMapping: _typeMappingSource.FindMapping(typeof(bool)))); } // Pattern match: new[] { "t1", "t2" }.Any(t => t.MatchesLQuery(lquery)) // Translation: ARRAY['t1','t2'] ~ lquery // Pattern match: new[] { "t1", "t2" }.Any(t => t.MatchesLTxtQuery(ltxtquery)) // Translation: ARRAY['t1','t2'] @ ltxtquery case PostgresBinaryExpression { OperatorType: PostgresExpressionType.LTreeMatches, Left: ColumnExpression ltreeColumn, Right: var lquery } when ltreeColumn.Table == sourceTable: { return BuildSimplifiedShapedQuery( source, new PostgresBinaryExpression( PostgresExpressionType.LTreeMatches, _sqlExpressionFactory.ApplyDefaultTypeMapping(GetArray(sourceTable)), lquery, typeof(bool), typeMapping: _typeMappingSource.FindMapping(typeof(bool)))); } // Any within Any (i.e. intersection) // Pattern match: ltrees.Any(t => lqueries.Any(q => t.MatchesLQuery(q))) // Translate: ltrees ? lqueries case PostgresBinaryExpression { OperatorType: PostgresExpressionType.LTreeMatchesAny, Left: ColumnExpression ltreeColumn, Right: var lqueries } when ltreeColumn.Table == sourceTable: { return BuildSimplifiedShapedQuery( source, new PostgresBinaryExpression( PostgresExpressionType.LTreeMatchesAny, _sqlExpressionFactory.ApplyDefaultTypeMapping(GetArray(sourceTable)), lqueries, typeof(bool), typeMapping: _typeMappingSource.FindMapping(typeof(bool)))); } #endregion LTree translations } } // Pattern match: x.Array1.Intersect(x.Array2).Any() // Translation: x.Array1 && x.Array2 if (predicate is null && source.QueryExpression is SelectExpression { Tables: [IntersectExpression { Source1: { Tables: [PostgresUnnestExpression { Array: var array1 }], GroupBy: [], Having: null, IsDistinct: false, Limit: null, Offset: null }, Source2: { Tables: [PostgresUnnestExpression { Array: var array2 }], GroupBy: [], Having: null, IsDistinct: false, Limit: null, Offset: null } }], GroupBy: [], Having: null, IsDistinct: false, Limit: null, Offset: null }) { return BuildSimplifiedShapedQuery(source, _sqlExpressionFactory.Overlaps(array1, array2)); } return base.TranslateAny(source, predicate); } /// /// 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. /// protected override ShapedQueryExpression? TranslateCount(ShapedQueryExpression source, LambdaExpression? predicate) { // Simplify x.Array.Count() => cardinality(x.Array) instead of SELECT COUNT(*) FROM unnest(x.Array) if (predicate is null && source.QueryExpression is SelectExpression { Tables: [PostgresUnnestExpression { Array: var array }], GroupBy: [], Having: null, IsDistinct: false, Limit: null, Offset: null }) { var translation = _sqlExpressionFactory.Function( "cardinality", new[] { array }, nullable: true, argumentsPropagateNullability: TrueArrays[1], typeof(int)); return source.Update( _sqlExpressionFactory.Select(translation), Expression.Convert( new ProjectionBindingExpression(source.QueryExpression, new ProjectionMember(), typeof(int?)), typeof(int))); } return base.TranslateCount(source, predicate); } /// /// 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. /// protected override ShapedQueryExpression? TranslateConcat(ShapedQueryExpression source1, ShapedQueryExpression source2) { // Simplify x.Array.Concat(y.Array) => x.Array || y.Array instead of: // SELECT u.value FROM unnest(x.Array) UNION ALL SELECT u.value FROM unnest(y.Array) if (source1.QueryExpression is SelectExpression { Tables: [PostgresUnnestExpression { Array: var array1 } unnestExpression1], GroupBy: [], Having: null, IsDistinct: false, Limit: null, Offset: null, Orderings: [] } && source2.QueryExpression is SelectExpression { Tables: [PostgresUnnestExpression { Array: var array2 }], GroupBy: [], Having: null, IsDistinct: false, Limit: null, Offset: null, Orderings: [] } && TryGetProjectedColumn(source1, out var projectedColumn1) && TryGetProjectedColumn(source2, out var projectedColumn2)) { Check.DebugAssert(projectedColumn1.Type == projectedColumn2.Type, "projectedColumn1.Type == projectedColumn2.Type"); Check.DebugAssert(projectedColumn1.TypeMapping is not null || projectedColumn2.TypeMapping is not null, "Concat with no type mapping on either side (operation should be client-evaluated over parameters/constants"); // TODO: Conflicting type mappings from both sides? var inferredTypeMapping = projectedColumn1.TypeMapping ?? projectedColumn2.TypeMapping; var unnestExpression = new PostgresUnnestExpression( unnestExpression1.Alias, _sqlExpressionFactory.Add(array1, array2), "value"); var selectExpression = new SelectExpression(unnestExpression, "value", projectedColumn1.Type, inferredTypeMapping); return source1.UpdateQueryExpression(selectExpression); } return base.TranslateConcat(source1, source2); } /// /// 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. /// protected override ShapedQueryExpression? TranslateElementAtOrDefault( ShapedQueryExpression source, Expression index, bool returnDefault) { // Simplify x.Array[1] => x.Array[1] (using the PG array subscript operator) instead of a subquery with LIMIT/OFFSET if (!returnDefault && source.QueryExpression is SelectExpression { Tables: [PostgresUnnestExpression { Array: var array }], GroupBy: [], Having: null, IsDistinct: false, Orderings: [], Limit: null, Offset: null }) { var translatedIndex = TranslateExpression(index); if (translatedIndex == null) { return base.TranslateElementAtOrDefault(source, index, returnDefault); } // Index on array - but PostgreSQL arrays are 1-based, so adjust the index. var translation = _sqlExpressionFactory.ArrayIndex(array, GenerateOneBasedIndexExpression(translatedIndex)); return source.Update(_sqlExpressionFactory.Select(translation), source.ShaperExpression); } return base.TranslateElementAtOrDefault(source, index, returnDefault); } /// /// 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. /// protected override ShapedQueryExpression? TranslateFirstOrDefault( ShapedQueryExpression source, LambdaExpression? predicate, Type returnType, bool returnDefault) { // Some LTree translations (see LTreeQueryTest) // Note that preprocessing normalizes FirstOrDefault(predicate) to Where(predicate).FirstOrDefault(), so the source's // select expression should already contain our predicate. if (source.QueryExpression is SelectExpression { Tables: [(PostgresUnnestExpression or ValuesExpression { ColumnNames: ["_ord", "Value"] }) and var sourceTable], Predicate: var translatedPredicate, GroupBy: [], Having: null, IsDistinct: false, Limit: null, Offset: null, Orderings: [] } && translatedPredicate is null ^ predicate is null) { if (translatedPredicate is null) { translatedPredicate = TranslateLambdaExpression(source, predicate!); if (translatedPredicate is null) { return null; } } switch (translatedPredicate) { // Pattern match: new[] { "t1", "t2" }.FirstOrDefault(t => t.IsAncestorOf(e.SomeLTree)) // Translation: ARRAY['t1','t2'] ?@> e.SomeLTree // Pattern match: new[] { "t1", "t2" }.FirstOrDefault(t => t.IsDescendant(e.SomeLTree)) // Translation: ARRAY['t1','t2'] ?<@ e.SomeLTree case PostgresBinaryExpression { OperatorType: (PostgresExpressionType.Contains or PostgresExpressionType.ContainedBy) and var operatorType, Left: ColumnExpression ltreeColumn, // Contains/ContainedBy can happen for non-LTree types too, so check that Right: { TypeMapping: NpgsqlLTreeTypeMapping } ltree } when ltreeColumn.Table == sourceTable: { return BuildSimplifiedShapedQuery( source, new PostgresBinaryExpression( operatorType == PostgresExpressionType.Contains ? PostgresExpressionType.LTreeFirstAncestor : PostgresExpressionType.LTreeFirstDescendent, _sqlExpressionFactory.ApplyDefaultTypeMapping(GetArray(sourceTable)), ltree, typeof(LTree), _typeMappingSource.FindMapping(typeof(LTree)))); } // Pattern match: new[] { "t1", "t2" }.FirstOrDefault(t => t.MatchesLQuery(lquery)) // Translation: ARRAY['t1','t2'] ?~ e.lquery // Pattern match: new[] { "t1", "t2" }.FirstOrDefault(t => t.MatchesLQuery(ltxtquery)) // Translation: ARRAY['t1','t2'] ?@ e.ltxtquery case PostgresBinaryExpression { OperatorType: PostgresExpressionType.LTreeMatches, Left: ColumnExpression ltreeColumn, Right: var lquery } when ltreeColumn.Table == sourceTable: { return BuildSimplifiedShapedQuery( source, new PostgresBinaryExpression( PostgresExpressionType.LTreeFirstMatches, _sqlExpressionFactory.ApplyDefaultTypeMapping(GetArray(sourceTable)), lquery, typeof(LTree), _typeMappingSource.FindMapping(typeof(LTree)))); } } } return base.TranslateFirstOrDefault(source, predicate, returnType, returnDefault); } /// /// 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. /// protected override ShapedQueryExpression? TranslateSkip(ShapedQueryExpression source, Expression count) { // Translate Skip over array to the PostgreSQL slice operator (array.Skip(2) -> array[3,]) if (source.QueryExpression is SelectExpression { Tables: [PostgresUnnestExpression { Array: var array } unnestExpression], GroupBy: [], Having: null, IsDistinct: false, Orderings: [], Limit: null, Offset: null } && TryGetProjectedColumn(source, out var projectedColumn) && TranslateExpression(count) is { } translatedCount) { var selectExpression = new SelectExpression( new PostgresUnnestExpression( unnestExpression.Alias, new PostgresArraySliceExpression( array, lowerBound: GenerateOneBasedIndexExpression(translatedCount), upperBound: null), "value"), "value", projectedColumn.Type, projectedColumn.TypeMapping); return source.Update( selectExpression, new ProjectionBindingExpression(selectExpression, new ProjectionMember(), projectedColumn.Type)); } return base.TranslateSkip(source, count); } /// /// 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. /// protected override ShapedQueryExpression? TranslateTake(ShapedQueryExpression source, Expression count) { // Translate Take over array to the PostgreSQL slice operator (array.Take(2) -> array[,2]) if (source.QueryExpression is SelectExpression { Tables: [PostgresUnnestExpression { Array: var array } unnestExpression], GroupBy: [], Having: null, IsDistinct: false, Orderings: [], Limit: null, Offset: null } && TryGetProjectedColumn(source, out var projectedColumn)) { var translatedCount = TranslateExpression(count); if (translatedCount == null) { return base.TranslateTake(source, count); } PostgresArraySliceExpression sliceExpression; // If Skip has been called before, an array slice expression is already there; try to integrate this Take into it. // Note that we need to take the Skip (lower bound) into account for the Take (upper bound), since the slice upper bound // operates on the original array (Skip hasn't yet taken place). if (array is PostgresArraySliceExpression existingSliceExpression) { if (existingSliceExpression is { LowerBound: SqlConstantExpression { Value: int lowerBoundValue } lowerBound, UpperBound: null }) { sliceExpression = existingSliceExpression.Update( existingSliceExpression.Array, existingSliceExpression.LowerBound, translatedCount is SqlConstantExpression { Value: int takeCount } ? _sqlExpressionFactory.Constant(lowerBoundValue + takeCount - 1, lowerBound.TypeMapping) : _sqlExpressionFactory.Subtract( _sqlExpressionFactory.Add(lowerBound, translatedCount), _sqlExpressionFactory.Constant(1, lowerBound.TypeMapping))); } else { // For any other case, we allow relational to translate with normal querying. For non-constant lower bounds, we could // duplicate them into the upper bound, but that could cause expensive double evaluation. return base.TranslateTake(source, count); } } else { sliceExpression = new PostgresArraySliceExpression(array, lowerBound: null, upperBound: translatedCount); } var selectExpression = new SelectExpression( new PostgresUnnestExpression(unnestExpression.Alias, sliceExpression, "value"), "value", projectedColumn.Type, projectedColumn.TypeMapping); return source.Update( selectExpression, new ProjectionBindingExpression(selectExpression, new ProjectionMember(), projectedColumn.Type)); } return base.TranslateTake(source, count); } /// /// 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. /// protected override bool IsValidSelectExpressionForExecuteUpdate( SelectExpression selectExpression, EntityShaperExpression entityShaperExpression, [NotNullWhen(true)] out TableExpression? tableExpression) { if (!base.IsValidSelectExpressionForExecuteUpdate(selectExpression, entityShaperExpression, out tableExpression)) { return false; } // PostgreSQL doesn't support referencing the main update table from anywhere except for the UPDATE WHERE clause. // This specifically makes it impossible to have joins which reference the main table in their predicate (ON ...). // Because of this, we detect all such inner joins and lift their predicates to the main WHERE clause (where a reference to the // main table is allowed) - see NpgsqlQuerySqlGenerator.VisitUpdate. // For any other type of join which contains a reference to the main table, we return false to trigger a subquery pushdown instead. OuterReferenceFindingExpressionVisitor? visitor = null; for (var i = 0; i < selectExpression.Tables.Count; i++) { var table = selectExpression.Tables[i]; if (ReferenceEquals(table, tableExpression)) { continue; } visitor ??= new OuterReferenceFindingExpressionVisitor(tableExpression); // For inner joins, if the predicate contains a reference to the main table, NpgsqlQuerySqlGenerator will lift the predicate // to the WHERE clause; so we only need to check the inner join's table (i.e. subquery) for such a reference. // Cross join and cross/outer apply (lateral joins) don't have predicates, so just check the entire join for a reference to // the main table, and switch to subquery syntax if one is found. // Left join does have a predicate, but it isn't possible to lift it to the main WHERE clause; so also check the entire // join. if (table is InnerJoinExpression innerJoin) { table = innerJoin.Table; } if (visitor.ContainsReferenceToMainTable(table)) { return false; } } return true; } /// /// 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. /// protected override bool IsValidSelectExpressionForExecuteDelete( SelectExpression selectExpression, EntityShaperExpression entityShaperExpression, [NotNullWhen(true)] out TableExpression? tableExpression) { // The default relational behavior is to allow only single-table expressions, and the only permitted feature is a predicate. // Here we extend this to also inner joins to tables, which we generate via the PostgreSQL-specific USING construct. if (selectExpression.Offset == null && selectExpression.Limit == null // If entity type has primary key then Distinct is no-op && (!selectExpression.IsDistinct || entityShaperExpression.EntityType.FindPrimaryKey() != null) && selectExpression.GroupBy.Count == 0 && selectExpression.Having == null && selectExpression.Orderings.Count == 0) { TableExpressionBase? table = null; if (selectExpression.Tables.Count == 1) { table = selectExpression.Tables[0]; } else if (selectExpression.Tables.All(t => t is TableExpression or InnerJoinExpression)) { var projectionBindingExpression = (ProjectionBindingExpression)entityShaperExpression.ValueBufferExpression; var entityProjectionExpression = (EntityProjectionExpression)selectExpression.GetProjection(projectionBindingExpression); var column = entityProjectionExpression.BindProperty(entityShaperExpression.EntityType.GetProperties().First()); table = column.Table; if (table is JoinExpressionBase joinExpressionBase) { table = joinExpressionBase.Table; } } if (table is TableExpression te) { tableExpression = te; return true; } } tableExpression = null; return false; } // PostgreSQL unnest is guaranteed to return output rows in the same order as its input array, // https://www.postgresql.org/docs/current/functions-array.html. /// protected override bool IsOrdered(SelectExpression selectExpression) => base.IsOrdered(selectExpression) || selectExpression.Tables is [PostgresUnnestExpression]; private bool TryGetProjectedColumn( ShapedQueryExpression shapedQueryExpression, [NotNullWhen(true)] out ColumnExpression? projectedColumn) { var shaperExpression = shapedQueryExpression.ShaperExpression; if (shaperExpression is UnaryExpression { NodeType: ExpressionType.Convert } unaryExpression && unaryExpression.Operand.Type.IsNullableType() && unaryExpression.Operand.Type.UnwrapNullableType() == unaryExpression.Type) { shaperExpression = unaryExpression.Operand; } if (shaperExpression is ProjectionBindingExpression projectionBindingExpression && shapedQueryExpression.QueryExpression is SelectExpression selectExpression && selectExpression.GetProjection(projectionBindingExpression) is ColumnExpression c) { projectedColumn = c; return true; } projectedColumn = null; return false; } /// /// PostgreSQL array indexing is 1-based. If the index happens to be a constant, just increment it. Otherwise, append a +1 in the /// SQL. /// private SqlExpression GenerateOneBasedIndexExpression(SqlExpression expression) => expression is SqlConstantExpression constant ? _sqlExpressionFactory.Constant(Convert.ToInt32(constant.Value) + 1, constant.TypeMapping) : _sqlExpressionFactory.Add(expression, _sqlExpressionFactory.Constant(1)); private ShapedQueryExpression BuildSimplifiedShapedQuery(ShapedQueryExpression source, SqlExpression translation) => source.Update( _sqlExpressionFactory.Select(translation), Expression.Convert( new ProjectionBindingExpression(translation, new ProjectionMember(), typeof(bool?)), typeof(bool))); /// /// Extracts the out of . /// If a is given, converts its literal values into a . /// private SqlExpression GetArray(TableExpressionBase tableExpression) { Check.DebugAssert( tableExpression is PostgresUnnestExpression or ValuesExpression { ColumnNames: ["_ord", "Value"] }, "Bad tableExpression"); switch (tableExpression) { case PostgresUnnestExpression unnest: return unnest.Array; case ValuesExpression valuesExpression: { // The source table was a constant collection, so translated by default to ValuesExpression. Convert it to an unnest over // an array constructor. var elements = new SqlExpression[valuesExpression.RowValues.Count]; for (var i = 0; i < elements.Length; i++) { // Skip the first column (_ord) and copy the second (Value) elements[i] = valuesExpression.RowValues[i].Values[1]; } return new PostgresNewArrayExpression( elements, valuesExpression.RowValues[0].Values[1].Type.MakeArrayType(), typeMapping: null); } default: throw new ArgumentException(nameof(tableExpression)); } } private sealed class OuterReferenceFindingExpressionVisitor : ExpressionVisitor { private readonly TableExpression _mainTable; private bool _containsReference; public OuterReferenceFindingExpressionVisitor(TableExpression mainTable) => _mainTable = mainTable; public bool ContainsReferenceToMainTable(TableExpressionBase tableExpression) { _containsReference = false; Visit(tableExpression); return _containsReference; } [return: NotNullIfNotNull("expression")] public override Expression? Visit(Expression? expression) { if (_containsReference) { return expression; } if (expression is ColumnExpression columnExpression && columnExpression.Table == _mainTable) { _containsReference = true; return expression; } return base.Visit(expression); } } /// /// 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. /// protected class NpgsqlInferredTypeMappingApplier : RelationalInferredTypeMappingApplier { private readonly NpgsqlTypeMappingSource _typeMappingSource; private readonly NpgsqlSqlExpressionFactory _sqlExpressionFactory; /// /// 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. /// public NpgsqlInferredTypeMappingApplier( NpgsqlTypeMappingSource typeMappingSource, NpgsqlSqlExpressionFactory sqlExpressionFactory, IReadOnlyDictionary<(TableExpressionBase, string), RelationalTypeMapping> inferredTypeMappings) : base(inferredTypeMappings) { _typeMappingSource = typeMappingSource; _sqlExpressionFactory = sqlExpressionFactory; } /// /// 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. /// protected override Expression VisitExtension(Expression expression) { switch (expression) { case PostgresUnnestExpression unnestExpression when InferredTypeMappings.TryGetValue( (unnestExpression, unnestExpression.ColumnName), out var elementTypeMapping): { var collectionTypeMapping = _typeMappingSource.FindContainerMapping(unnestExpression.Array.Type, elementTypeMapping); if (collectionTypeMapping is null) { throw new InvalidOperationException(RelationalStrings.NullTypeMappingInSqlTree(expression.Print())); } return unnestExpression.Update( _sqlExpressionFactory.ApplyTypeMapping(unnestExpression.Array, collectionTypeMapping)); } default: return base.VisitExtension(expression); } } } }