diff --git a/src/EFCore.PG.NTS/Storage/Internal/NpgsqlGeometryTypeMapping.cs b/src/EFCore.PG.NTS/Storage/Internal/NpgsqlGeometryTypeMapping.cs
index 857ebc65d..9467af77b 100644
--- a/src/EFCore.PG.NTS/Storage/Internal/NpgsqlGeometryTypeMapping.cs
+++ b/src/EFCore.PG.NTS/Storage/Internal/NpgsqlGeometryTypeMapping.cs
@@ -7,6 +7,23 @@
// ReSharper disable once CheckNamespace
namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.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 interface INpgsqlGeometryTypeMapping
+{
+ ///
+ /// 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.
+ ///
+ RelationalTypeMapping CloneWithElementTypeMapping(RelationalTypeMapping elementTypeMapping);
+}
+
///
/// 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
@@ -14,7 +31,8 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal;
/// doing so can result in application failures when updating to a new Entity Framework Core release.
///
[UsedImplicitly]
-public class NpgsqlGeometryTypeMapping : RelationalGeometryTypeMapping, INpgsqlTypeMapping
+public class NpgsqlGeometryTypeMapping : RelationalGeometryTypeMapping,
+ INpgsqlTypeMapping, INpgsqlGeometryTypeMapping
{
private readonly bool _isGeography;
@@ -49,6 +67,16 @@ protected NpgsqlGeometryTypeMapping(RelationalTypeMappingParameters parameters)
protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters)
=> new NpgsqlGeometryTypeMapping(parameters);
+ ///
+ /// 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.
+ ///
+ RelationalTypeMapping INpgsqlGeometryTypeMapping.CloneWithElementTypeMapping(RelationalTypeMapping elementTypeMapping)
+ => new NpgsqlGeometryTypeMapping(
+ Parameters.WithCoreParameters(Parameters.CoreParameters.WithElementTypeMapping(elementTypeMapping)));
+
///
/// 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
diff --git a/src/EFCore.PG.NTS/Storage/Internal/NpgsqlNetTopologySuiteTypeMappingSourcePlugin.cs b/src/EFCore.PG.NTS/Storage/Internal/NpgsqlNetTopologySuiteTypeMappingSourcePlugin.cs
index 09558b3e6..3955b16fd 100644
--- a/src/EFCore.PG.NTS/Storage/Internal/NpgsqlNetTopologySuiteTypeMappingSourcePlugin.cs
+++ b/src/EFCore.PG.NTS/Storage/Internal/NpgsqlNetTopologySuiteTypeMappingSourcePlugin.cs
@@ -57,9 +57,15 @@ public NpgsqlNetTopologySuiteTypeMappingSourcePlugin(INpgsqlNetTopologySuiteOpti
var storeTypeName = mappingInfo.StoreTypeName;
var isGeography = _options.IsGeographyDefault;
- if (clrType is not null && !typeof(Geometry).IsAssignableFrom(clrType))
+ if (clrType is not null)
{
- return null;
+ if (!clrType.IsAssignableTo(typeof(Geometry)))
+ {
+ return null;
+ }
+
+ // TODO: if store type is null, consider setting it based on the CLR type, i.e. create GEOMETRY(Point) instead of Geometry when
+ // the CLR property is NTS Point.
}
if (storeTypeName is not null)
@@ -69,18 +75,34 @@ public NpgsqlNetTopologySuiteTypeMappingSourcePlugin(INpgsqlNetTopologySuiteOpti
return null;
}
- if (clrType is null)
- {
- clrType = parsedSubtype;
- }
+ clrType ??= parsedSubtype;
+ }
+
+ storeTypeName ??= isGeography ? "geography" : "geometry";
+
+ Check.DebugAssert(clrType is not null, "clrType is not null");
+
+ var typeMapping = (RelationalTypeMapping)Activator.CreateInstance(
+ typeof(NpgsqlGeometryTypeMapping<>).MakeGenericType(clrType), storeTypeName, isGeography)!;
+
+ // TODO: Also restrict the element type mapping based on the user-specified store type?
+ var elementType = clrType == typeof(MultiPoint)
+ ? typeof(Point)
+ : clrType == typeof(MultiLineString)
+ ? typeof(LineString)
+ : clrType == typeof(MultiPolygon)
+ ? typeof(Polygon)
+ : clrType == typeof(GeometryCollection)
+ ? typeof(Geometry)
+ : null;
+
+ if (elementType is not null)
+ {
+ var elementTypeMapping = FindMapping(new() { ClrType = elementType })!;
+ typeMapping = ((INpgsqlGeometryTypeMapping)typeMapping).CloneWithElementTypeMapping(elementTypeMapping);
}
- return clrType is not null || storeTypeName is not null
- ? (RelationalTypeMapping)Activator.CreateInstance(
- typeof(NpgsqlGeometryTypeMapping<>).MakeGenericType(clrType ?? typeof(Geometry)),
- storeTypeName ?? (isGeography ? "geography" : "geometry"),
- isGeography)!
- : null;
+ return typeMapping;
}
///
diff --git a/src/EFCore.PG/Extensions/NpgsqlServiceCollectionExtensions.cs b/src/EFCore.PG/Extensions/NpgsqlServiceCollectionExtensions.cs
index 265e1ec46..a98389e99 100644
--- a/src/EFCore.PG/Extensions/NpgsqlServiceCollectionExtensions.cs
+++ b/src/EFCore.PG/Extensions/NpgsqlServiceCollectionExtensions.cs
@@ -1,4 +1,3 @@
-using System.Data.Common;
using Npgsql.EntityFrameworkCore.PostgreSQL.Diagnostics.Internal;
using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure;
using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal;
@@ -114,6 +113,7 @@ public static IServiceCollection AddEntityFrameworkNpgsql(this IServiceCollectio
.TryAdd()
.TryAdd()
.TryAdd()
+ .TryAdd()
.TryAdd()
.TryAdd()
.TryAdd()
@@ -130,4 +130,4 @@ public static IServiceCollection AddEntityFrameworkNpgsql(this IServiceCollectio
return serviceCollection;
}
-}
\ No newline at end of file
+}
diff --git a/src/EFCore.PG/Infrastructure/Internal/INpgsqlSingletonOptions.cs b/src/EFCore.PG/Infrastructure/Internal/INpgsqlSingletonOptions.cs
index aab2b1cff..7da9f06a4 100644
--- a/src/EFCore.PG/Infrastructure/Internal/INpgsqlSingletonOptions.cs
+++ b/src/EFCore.PG/Infrastructure/Internal/INpgsqlSingletonOptions.cs
@@ -13,9 +13,9 @@ public interface INpgsqlSingletonOptions : ISingletonOptions
Version PostgresVersion { get; }
///
- /// The backend version to target, but returns unless the user explicitly specified a version.
+ /// Whether the user has explicitly set the backend version to target.
///
- Version? PostgresVersionWithoutDefault { get; }
+ bool IsPostgresVersionSet { get; }
///
/// Whether to target Redshift.
@@ -41,4 +41,4 @@ public interface INpgsqlSingletonOptions : ISingletonOptions
/// The root service provider for the application, if available. />.
///
IServiceProvider? ApplicationServiceProvider { get; }
-}
\ No newline at end of file
+}
diff --git a/src/EFCore.PG/Infrastructure/Internal/NpgsqlOptionsExtension.cs b/src/EFCore.PG/Infrastructure/Internal/NpgsqlOptionsExtension.cs
index 171615a3f..65b9494a0 100644
--- a/src/EFCore.PG/Infrastructure/Internal/NpgsqlOptionsExtension.cs
+++ b/src/EFCore.PG/Infrastructure/Internal/NpgsqlOptionsExtension.cs
@@ -32,8 +32,8 @@ public virtual Version PostgresVersion
///
/// The backend version to target, but returns unless the user explicitly specified a version.
///
- public virtual Version? PostgresVersionWithoutDefault
- => _postgresVersion;
+ public virtual bool IsPostgresVersionSet
+ => _postgresVersion is not null;
///
/// The , or if a connection string or was used
diff --git a/src/EFCore.PG/Internal/NpgsqlSingletonOptions.cs b/src/EFCore.PG/Internal/NpgsqlSingletonOptions.cs
index 3cb8a4093..3b057b93b 100644
--- a/src/EFCore.PG/Internal/NpgsqlSingletonOptions.cs
+++ b/src/EFCore.PG/Internal/NpgsqlSingletonOptions.cs
@@ -21,7 +21,7 @@ public class NpgsqlSingletonOptions : INpgsqlSingletonOptions
/// 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 virtual Version? PostgresVersionWithoutDefault { get; private set; }
+ public virtual bool IsPostgresVersionSet { get; private set; }
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -79,7 +79,7 @@ public virtual void Initialize(IDbContextOptions options)
var coreOptions = options.FindExtension() ?? new();
PostgresVersion = npgsqlOptions.PostgresVersion;
- PostgresVersionWithoutDefault = npgsqlOptions.PostgresVersionWithoutDefault;
+ IsPostgresVersionSet = npgsqlOptions.IsPostgresVersionSet;
UseRedshift = npgsqlOptions.UseRedshift;
ReverseNullOrderingEnabled = npgsqlOptions.ReverseNullOrdering;
UserRangeDefinitions = npgsqlOptions.UserRangeDefinitions;
diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMemberTranslatorProvider.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMemberTranslatorProvider.cs
index 36ab7f7fa..56971ce73 100644
--- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMemberTranslatorProvider.cs
+++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMemberTranslatorProvider.cs
@@ -29,8 +29,8 @@ public NpgsqlMemberTranslatorProvider(
: base(dependencies)
{
var npgsqlOptions = contextOptions.FindExtension() ?? new();
- var supportsMultiranges = npgsqlOptions.PostgresVersionWithoutDefault is null
- || npgsqlOptions.PostgresVersionWithoutDefault.AtLeast(14);
+ var supportsMultiranges = !npgsqlOptions.IsPostgresVersionSet
+ || npgsqlOptions.IsPostgresVersionSet && npgsqlOptions.PostgresVersion.AtLeast(14);
var sqlExpressionFactory = (NpgsqlSqlExpressionFactory)dependencies.SqlExpressionFactory;
JsonPocoTranslator = new NpgsqlJsonPocoTranslator(typeMappingSource, sqlExpressionFactory, model);
diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMethodCallTranslatorProvider.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMethodCallTranslatorProvider.cs
index 885d4db45..f2de4aa1b 100644
--- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMethodCallTranslatorProvider.cs
+++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMethodCallTranslatorProvider.cs
@@ -32,8 +32,8 @@ public NpgsqlMethodCallTranslatorProvider(
: base(dependencies)
{
var npgsqlOptions = contextOptions.FindExtension() ?? new();
- var supportsMultiranges = npgsqlOptions.PostgresVersionWithoutDefault is null
- || npgsqlOptions.PostgresVersionWithoutDefault.AtLeast(14);
+ var supportsMultiranges = !npgsqlOptions.IsPostgresVersionSet
+ || npgsqlOptions.IsPostgresVersionSet && npgsqlOptions.PostgresVersion.AtLeast(14);
var sqlExpressionFactory = (NpgsqlSqlExpressionFactory)dependencies.SqlExpressionFactory;
var typeMappingSource = (NpgsqlTypeMappingSource)dependencies.RelationalTypeMappingSource;
diff --git a/src/EFCore.PG/Query/Expressions/Internal/PostgresArraySliceExpression.cs b/src/EFCore.PG/Query/Expressions/Internal/PostgresArraySliceExpression.cs
new file mode 100644
index 000000000..44d9a201b
--- /dev/null
+++ b/src/EFCore.PG/Query/Expressions/Internal/PostgresArraySliceExpression.cs
@@ -0,0 +1,98 @@
+namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal;
+
+///
+/// A SQL expression that represents a slicing into a PostgreSQL array (e.g. array[2:3]).
+///
+///
+/// .
+///
+public class PostgresArraySliceExpression : SqlExpression, IEquatable
+{
+ ///
+ /// The array being sliced.
+ ///
+ public virtual SqlExpression Array { get; }
+
+ ///
+ /// The lower bound of the slice.
+ ///
+ public virtual SqlExpression? LowerBound { get; }
+
+ ///
+ /// The upper bound of the slice.
+ ///
+ public virtual SqlExpression? UpperBound { get; }
+
+ ///
+ /// Creates a new instance of the class.
+ ///
+ /// The array tp slice into.
+ /// The lower bound of the slice.
+ /// The upper bound of the slice.
+ public PostgresArraySliceExpression(
+ SqlExpression array,
+ SqlExpression? lowerBound,
+ SqlExpression? upperBound)
+ : base(array.Type, array.TypeMapping)
+ {
+ Check.NotNull(array, nameof(array));
+
+ if (lowerBound is null && upperBound is null)
+ {
+ throw new ArgumentException("At least one of lowerBound or upperBound must be provided");
+ }
+
+ Array = array;
+ LowerBound = lowerBound;
+ UpperBound = upperBound;
+ }
+
+ ///
+ /// Creates a new expression that is like this one, but using the supplied children. If all of the children are the same, it will
+ /// return this expression.
+ ///
+ /// The property of the result.
+ /// The lower bound of the slice.
+ /// The upper bound of the slice.
+ /// This expression if no children changed, or an expression with the updated children.
+ public virtual PostgresArraySliceExpression Update(SqlExpression array, SqlExpression? lowerBound, SqlExpression? upperBound)
+ => array == Array && lowerBound == LowerBound && upperBound == UpperBound
+ ? this
+ : new PostgresArraySliceExpression(array, lowerBound, upperBound);
+
+ ///
+ protected override Expression VisitChildren(ExpressionVisitor visitor)
+ => Update(
+ (SqlExpression)visitor.Visit(Array),
+ (SqlExpression?)visitor.Visit(LowerBound),
+ (SqlExpression?)visitor.Visit(UpperBound));
+
+ ///
+ public virtual bool Equals(PostgresArraySliceExpression? other)
+ => ReferenceEquals(this, other)
+ || other is not null
+ && base.Equals(other)
+ && Array.Equals(other.Array)
+ && (LowerBound is null ? other.LowerBound is null : LowerBound.Equals(other.LowerBound))
+ && (UpperBound is null ? other.UpperBound is null : UpperBound.Equals(other.UpperBound));
+
+ ///
+ public override bool Equals(object? obj) => obj is PostgresArraySliceExpression e && Equals(e);
+
+ ///
+ public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), Array, LowerBound, UpperBound);
+
+ ///
+ protected override void Print(ExpressionPrinter expressionPrinter)
+ {
+ expressionPrinter.Visit(Array);
+ expressionPrinter.Append("[");
+ expressionPrinter.Visit(LowerBound);
+ expressionPrinter.Append(":");
+ expressionPrinter.Visit(UpperBound);
+ expressionPrinter.Append("]");
+ }
+
+ ///
+ public override string ToString() => $"{Array}[{LowerBound}:{UpperBound}]";
+}
diff --git a/src/EFCore.PG/Query/Expressions/Internal/PostgresBinaryExpression.cs b/src/EFCore.PG/Query/Expressions/Internal/PostgresBinaryExpression.cs
index a26093dfe..13ad583a8 100644
--- a/src/EFCore.PG/Query/Expressions/Internal/PostgresBinaryExpression.cs
+++ b/src/EFCore.PG/Query/Expressions/Internal/PostgresBinaryExpression.cs
@@ -123,7 +123,7 @@ protected override void Print(ExpressionPrinter expressionPrinter)
PostgresExpressionType.LTreeMatches
when Right.TypeMapping?.StoreType == "lquery" ||
Right.TypeMapping is NpgsqlArrayTypeMapping arrayMapping &&
- arrayMapping.ElementMapping.StoreType == "lquery"
+ arrayMapping.ElementTypeMapping.StoreType == "lquery"
=> "~",
PostgresExpressionType.LTreeMatches
when Right.TypeMapping?.StoreType == "ltxtquery"
diff --git a/src/EFCore.PG/Query/Expressions/Internal/PostgresUnnestExpression.cs b/src/EFCore.PG/Query/Expressions/Internal/PostgresUnnestExpression.cs
new file mode 100644
index 000000000..5a6fe9c0d
--- /dev/null
+++ b/src/EFCore.PG/Query/Expressions/Internal/PostgresUnnestExpression.cs
@@ -0,0 +1,113 @@
+namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal;
+
+///
+/// An expression that represents a PostgreSQL unnest function call in a SQL tree.
+///
+///
+///
+/// This expression is just a , 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.
+///
+///
+/// See unnest for more
+/// information and examples.
+///
+///
+/// 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 PostgresUnnestExpression : TableValuedFunctionExpression
+{
+ ///
+ /// The array to be un-nested into a table.
+ ///
+ ///
+ /// 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 virtual SqlExpression Array
+ => Arguments[0];
+
+ ///
+ /// The name of the column to be projected out from the unnest call.
+ ///
+ ///
+ /// 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 virtual string ColumnName { get; }
+
+ ///
+ /// 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 PostgresUnnestExpression(string alias, SqlExpression array, string columnName)
+ : base(alias, "unnest", schema: null, builtIn: true, new[] { array })
+ {
+ ColumnName = columnName;
+ }
+
+ ///
+ /// 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 override TableValuedFunctionExpression Update(IReadOnlyList arguments)
+ => arguments is [var singleArgument]
+ ? Update(singleArgument)
+ : throw new ArgumentException();
+
+ ///
+ /// Creates a new expression that is like this one, but using the supplied children. If all of the children are the same, it will
+ /// return this expression.
+ ///
+ /// The property of the result.
+ /// This expression if no children changed, or an expression with the updated children.
+ public virtual PostgresUnnestExpression Update(SqlExpression array)
+ => array != Array
+ ? new PostgresUnnestExpression(Alias, array, ColumnName)
+ : this;
+
+ ///
+ protected override void Print(ExpressionPrinter expressionPrinter)
+ {
+ expressionPrinter.Append(Name);
+ expressionPrinter.Append("(");
+ expressionPrinter.VisitCollection(Arguments);
+ expressionPrinter.Append(")");
+
+ PrintAnnotations(expressionPrinter);
+ expressionPrinter
+ .Append(" AS ")
+ .Append(Alias)
+ .Append("(")
+ .Append(ColumnName)
+ .Append(")");
+ }
+
+ ///
+ public override bool Equals(object? obj)
+ => obj != null
+ && (ReferenceEquals(this, obj)
+ || obj is PostgresUnnestExpression unnestExpression
+ && Equals(unnestExpression));
+
+ private bool Equals(PostgresUnnestExpression unnestExpression)
+ => base.Equals(unnestExpression) && ColumnName == unnestExpression.ColumnName;
+
+ ///
+ public override int GetHashCode()
+ => base.GetHashCode();
+}
diff --git a/src/EFCore.PG/Query/Internal/NpgsqlQueryRootProcessor.cs b/src/EFCore.PG/Query/Internal/NpgsqlQueryRootProcessor.cs
new file mode 100644
index 000000000..cc9cd7937
--- /dev/null
+++ b/src/EFCore.PG/Query/Internal/NpgsqlQueryRootProcessor.cs
@@ -0,0 +1,37 @@
+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 NpgsqlQueryRootProcessor : RelationalQueryRootProcessor
+{
+ ///
+ /// 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 NpgsqlQueryRootProcessor(
+ QueryTranslationPreprocessorDependencies dependencies,
+ RelationalQueryTranslationPreprocessorDependencies relationalDependencies,
+ QueryCompilationContext queryCompilationContext)
+ : base(dependencies, relationalDependencies, queryCompilationContext)
+ {
+ }
+
+ ///
+ /// Converts a to a , to be later translated to
+ /// PostgreSQL unnest over an array parameter.
+ ///
+ ///
+ /// 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 ShouldConvertToParameterQueryRoot(ParameterExpression parameterExpression)
+ => true;
+}
diff --git a/src/EFCore.PG/Query/Internal/NpgsqlQuerySqlGenerator.cs b/src/EFCore.PG/Query/Internal/NpgsqlQuerySqlGenerator.cs
index 5d9afc43c..321b16917 100644
--- a/src/EFCore.PG/Query/Internal/NpgsqlQuerySqlGenerator.cs
+++ b/src/EFCore.PG/Query/Internal/NpgsqlQuerySqlGenerator.cs
@@ -47,19 +47,21 @@ public NpgsqlQuerySqlGenerator(
protected override Expression VisitExtension(Expression extensionExpression)
=> extensionExpression switch
{
- PostgresAllExpression allExpression => VisitArrayAll(allExpression),
- PostgresAnyExpression anyExpression => VisitArrayAny(anyExpression),
- PostgresArrayIndexExpression arrayIndexExpression => VisitArrayIndex(arrayIndexExpression),
- PostgresBinaryExpression binaryExpression => VisitPostgresBinary(binaryExpression),
- PostgresDeleteExpression deleteExpression => VisitPostgresDelete(deleteExpression),
- PostgresFunctionExpression functionExpression => VisitPostgresFunction(functionExpression),
- PostgresILikeExpression iLikeExpression => VisitILike(iLikeExpression),
- PostgresJsonTraversalExpression jsonTraversalExpression => VisitJsonPathTraversal(jsonTraversalExpression),
- PostgresNewArrayExpression newArrayExpression => VisitPostgresNewArray(newArrayExpression),
- PostgresRegexMatchExpression regexMatchExpression => VisitRegexMatch(regexMatchExpression),
- PostgresRowValueExpression rowValueExpression => VisitRowValue(rowValueExpression),
- PostgresUnknownBinaryExpression unknownBinaryExpression => VisitUnknownBinary(unknownBinaryExpression),
- _ => base.VisitExtension(extensionExpression)
+ PostgresAllExpression e => VisitArrayAll(e),
+ PostgresAnyExpression e => VisitArrayAny(e),
+ PostgresArrayIndexExpression e => VisitArrayIndex(e),
+ PostgresArraySliceExpression e => VisitArraySlice(e),
+ PostgresBinaryExpression e => VisitPostgresBinary(e),
+ PostgresDeleteExpression e => VisitPostgresDelete(e),
+ PostgresFunctionExpression e => VisitPostgresFunction(e),
+ PostgresILikeExpression e => VisitILike(e),
+ PostgresJsonTraversalExpression e => VisitJsonPathTraversal(e),
+ PostgresNewArrayExpression e => VisitPostgresNewArray(e),
+ PostgresRegexMatchExpression e => VisitRegexMatch(e),
+ PostgresRowValueExpression e => VisitRowValue(e),
+ PostgresUnknownBinaryExpression e => VisitUnknownBinary(e),
+ PostgresUnnestExpression e => VisitUnnestExpression(e),
+ _ => base.VisitExtension(extensionExpression)
};
///
@@ -110,12 +112,14 @@ protected override string GetOperator(SqlBinaryExpression e)
=> e.OperatorType switch
{
// PostgreSQL has a special string concatenation operator: ||
- // We switch to it if the expression itself has type string, or if one of the sides has a
- // string type mapping. Same for full-text search's TsVector.
+ // We switch to it if the expression itself has type string, or if one of the sides has a string type mapping.
+ // Same for full-text search's TsVector, arrays.
ExpressionType.Add when
e.Type == typeof(string) || e.Left.TypeMapping?.ClrType == typeof(string) || e.Right.TypeMapping?.ClrType == typeof(string) ||
- e.Type == typeof(NpgsqlTsVector) || e.Left.TypeMapping?.ClrType == typeof(NpgsqlTsVector) || e.Right.TypeMapping?.ClrType == typeof(NpgsqlTsVector)
+ e.Type == typeof(NpgsqlTsVector) || e.Left.TypeMapping?.ClrType == typeof(NpgsqlTsVector) || e.Right.TypeMapping?.ClrType == typeof(NpgsqlTsVector) ||
+ e.Left.TypeMapping is NpgsqlArrayTypeMapping && e.Right.TypeMapping is NpgsqlArrayTypeMapping
=> " || ",
+
ExpressionType.And when e.Type == typeof(bool) => " AND ",
ExpressionType.Or when e.Type == typeof(bool) => " OR ",
_ => base.GetOperator(e)
@@ -497,7 +501,7 @@ binaryExpression.Left.TypeMapping is NpgsqlCidrTypeMapping
PostgresExpressionType.LTreeMatches
when binaryExpression.Right.TypeMapping.StoreType == "lquery" ||
binaryExpression.Right.TypeMapping is NpgsqlArrayTypeMapping arrayMapping &&
- arrayMapping.ElementMapping.StoreType == "lquery"
+ arrayMapping.ElementTypeMapping.StoreType == "lquery"
=> "~",
PostgresExpressionType.LTreeMatches
when binaryExpression.Right.TypeMapping.StoreType == "ltxtquery"
@@ -638,19 +642,139 @@ protected override void GenerateSetOperationOperand(SetOperationBase setOperatio
/// 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 VisitCollate(CollateExpression collateExpresion)
+ protected override Expression VisitCollate(CollateExpression collateExpression)
{
- Check.NotNull(collateExpresion, nameof(collateExpresion));
+ Check.NotNull(collateExpression, nameof(collateExpression));
- Visit(collateExpresion.Operand);
+ Visit(collateExpression.Operand);
+ // In PG, collation names are regular identifiers which need to be quoted for case-sensitivity.
Sql
.Append(" COLLATE ")
- .Append(_sqlGenerationHelper.DelimitIdentifier(collateExpresion.Collation));
+ .Append(_sqlGenerationHelper.DelimitIdentifier(collateExpression.Collation));
+
+ return collateExpression;
+ }
+
+ ///
+ /// 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 TryGenerateWithoutWrappingSelect(SelectExpression selectExpression)
+ // PostgreSQL supports VALUES as a top-level statement - and directly under set operations.
+ // However, when on the left side of a set operation, we need the column coming out of VALUES to be named, so we need the wrapping
+ // SELECT for that.
+ => selectExpression.Tables is not [ValuesExpression]
+ && base.TryGenerateWithoutWrappingSelect(selectExpression);
+
+ ///
+ /// 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 void GenerateSetOperation(SetOperationBase setOperation)
+ {
+ GenerateSetOperationOperand(setOperation, setOperation.Source1);
+
+ Sql
+ .AppendLine()
+ .Append(
+ setOperation switch
+ {
+ ExceptExpression => "EXCEPT",
+ IntersectExpression => "INTERSECT",
+ UnionExpression => "UNION",
+ _ => throw new InvalidOperationException(CoreStrings.UnknownEntity("SetOperationType"))
+ })
+ .AppendLine(setOperation.IsDistinct ? string.Empty : " ALL");
+
+ // For ValuesExpression, we can remove its wrapping SelectExpression but only if on the right side of a set operation, since on
+ // the left side we need the column name to be specified.
+ if (setOperation.Source2 is
+ {
+ Tables: [ValuesExpression valuesExpression],
+ Offset: null,
+ Limit: null,
+ IsDistinct: false,
+ Predicate: null,
+ Having: null,
+ Orderings.Count: 0,
+ GroupBy.Count: 0,
+ } rightSelectExpression
+ && rightSelectExpression.Projection.Count == valuesExpression.ColumnNames.Count
+ && rightSelectExpression.Projection.Select(
+ (pe, index) => pe.Expression is ColumnExpression column
+ && column.Name == valuesExpression.ColumnNames[index])
+ .All(e => e))
+ {
+ GenerateValues(valuesExpression);
+ }
+ else
+ {
+ GenerateSetOperationOperand(setOperation, setOperation.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 Expression VisitValues(ValuesExpression valuesExpression)
+ {
+ base.VisitValues(valuesExpression);
+
+ // PostgreSQL VALUES supports setting the projects column names: FROM (VALUES (1), (2)) AS v(foo)
+ Sql.Append("(");
+
+ for (var i = 0; i < valuesExpression.ColumnNames.Count; i++)
+ {
+ if (i > 0)
+ {
+ Sql.Append(", ");
+ }
+
+ Sql.Append(_sqlGenerationHelper.DelimitIdentifier(valuesExpression.ColumnNames[i]));
+ }
+
+ Sql.Append(")");
- return collateExpresion;
+ return valuesExpression;
}
+ ///
+ /// 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 void GenerateValues(ValuesExpression valuesExpression)
+ {
+ // PostgreSQL supports providing the names of columns projected out of VALUES: (VALUES (1, 3), (2, 4)) AS x(a, b).
+ // But since other databases sometimes don't, the default relational implementation is complex, involving a SELECT for the first row
+ // and a UNION All on the rest. Override to do the nice simple thing.
+ var rowValues = valuesExpression.RowValues;
+
+ Sql.Append("VALUES ");
+
+ for (var i = 0; i < rowValues.Count; i++)
+ {
+ // TODO: Do we want newlines here?
+ if (i > 0)
+ {
+ Sql.Append(", ");
+ }
+
+ Visit(valuesExpression.RowValues[i]);
+ }
+ }
+
+ #region PostgreSQL-specific expression types
+
///
/// 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
@@ -718,6 +842,20 @@ public virtual Expression VisitArrayIndex(PostgresArrayIndexExpression expressio
return expression;
}
+ ///
+ /// Produces SQL array slice expression (e.g. arr[1:2]).
+ ///
+ public virtual Expression VisitArraySlice(PostgresArraySliceExpression expression)
+ {
+ Visit(expression.Array);
+ Sql.Append("[");
+ Visit(expression.LowerBound);
+ Sql.Append(":");
+ Visit(expression.UpperBound);
+ Sql.Append("]");
+ return expression;
+ }
+
///
/// Visits the children of a .
///
@@ -891,6 +1029,27 @@ public virtual Expression VisitJsonPathTraversal(PostgresJsonTraversalExpression
return 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 virtual Expression VisitUnnestExpression(PostgresUnnestExpression unnestExpression)
+ {
+ // unnest docs: https://www.postgresql.org/docs/current/functions-array.html#ARRAY-FUNCTIONS-TABLE
+
+ // unnest is a regular table-valued function with a special AS foo(bar) at the end
+ base.VisitTableValuedFunction(unnestExpression);
+
+ Sql
+ .Append("(")
+ .Append(unnestExpression.ColumnName)
+ .Append(")");
+
+ return unnestExpression;
+ }
+
///
/// Visits the children of a .
///
@@ -1023,6 +1182,8 @@ public virtual Expression VisitPostgresFunction(PostgresFunctionExpression e)
return e;
}
+ #endregion PostgreSQL-specific expression types
+
///
/// 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
diff --git a/src/EFCore.PG/Query/Internal/NpgsqlQueryTranslationPreprocessor.cs b/src/EFCore.PG/Query/Internal/NpgsqlQueryTranslationPreprocessor.cs
new file mode 100644
index 000000000..4f274561d
--- /dev/null
+++ b/src/EFCore.PG/Query/Internal/NpgsqlQueryTranslationPreprocessor.cs
@@ -0,0 +1,27 @@
+namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.Internal;
+
+public class NpgsqlQueryTranslationPreprocessor : RelationalQueryTranslationPreprocessor
+{
+ ///
+ /// 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 NpgsqlQueryTranslationPreprocessor(
+ QueryTranslationPreprocessorDependencies dependencies,
+ RelationalQueryTranslationPreprocessorDependencies relationalDependencies,
+ QueryCompilationContext queryCompilationContext)
+ : base(dependencies, relationalDependencies, queryCompilationContext)
+ {
+ }
+
+ ///
+ /// 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 ProcessQueryRoots(Expression expression)
+ => new NpgsqlQueryRootProcessor(Dependencies, RelationalDependencies, QueryCompilationContext).Visit(expression);
+}
diff --git a/src/EFCore.PG/Query/Internal/NpgsqlQueryTranslationPreprocessorFactory.cs b/src/EFCore.PG/Query/Internal/NpgsqlQueryTranslationPreprocessorFactory.cs
new file mode 100644
index 000000000..502a6348a
--- /dev/null
+++ b/src/EFCore.PG/Query/Internal/NpgsqlQueryTranslationPreprocessorFactory.cs
@@ -0,0 +1,43 @@
+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 NpgsqlQueryTranslationPreprocessorFactory : IQueryTranslationPreprocessorFactory
+{
+ ///
+ /// 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 NpgsqlQueryTranslationPreprocessorFactory(
+ QueryTranslationPreprocessorDependencies dependencies,
+ RelationalQueryTranslationPreprocessorDependencies relationalDependencies)
+ {
+ Dependencies = dependencies;
+ RelationalDependencies = relationalDependencies;
+ }
+
+ ///
+ /// Dependencies for this service.
+ ///
+ protected virtual QueryTranslationPreprocessorDependencies Dependencies { get; }
+
+ ///
+ /// Relational provider-specific dependencies for this service.
+ ///
+ protected virtual RelationalQueryTranslationPreprocessorDependencies RelationalDependencies { get; }
+
+ ///
+ /// 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 QueryTranslationPreprocessor Create(QueryCompilationContext queryCompilationContext)
+ => new NpgsqlQueryTranslationPreprocessor(Dependencies, RelationalDependencies, queryCompilationContext);
+}
diff --git a/src/EFCore.PG/Query/Internal/NpgsqlQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.PG/Query/Internal/NpgsqlQueryableMethodTranslatingExpressionVisitor.cs
index b25007ac5..97f47c3df 100644
--- a/src/EFCore.PG/Query/Internal/NpgsqlQueryableMethodTranslatingExpressionVisitor.cs
+++ b/src/EFCore.PG/Query/Internal/NpgsqlQueryableMethodTranslatingExpressionVisitor.cs
@@ -1,4 +1,8 @@
using System.Diagnostics.CodeAnalysis;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal;
+using static Npgsql.EntityFrameworkCore.PostgreSQL.Utilities.Statics;
namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.Internal;
@@ -10,6 +14,22 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.Internal;
///
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) })!;
+
+ #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
@@ -22,6 +42,633 @@ public NpgsqlQueryableMethodTranslatingExpressionVisitor(
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);
+
+ protected override ShapedQueryExpression? TranslateAll(ShapedQueryExpression source, LambdaExpression predicate)
+ {
+ if (source.QueryExpression is SelectExpression
+ {
+ Tables: [(PostgresUnnestExpression or ValuesExpression { ColumnNames: ["_ord", "Value"] }) and var sourceTable],
+ 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:
+ {
+ //s.IntArray @> ARRAY[v.Value]
+ 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);
+ }
+
+ protected override ShapedQueryExpression? TranslateAny(ShapedQueryExpression source, LambdaExpression? predicate)
+ {
+ if (source.QueryExpression is SelectExpression
+ {
+ Tables: [(PostgresUnnestExpression or ValuesExpression { ColumnNames: ["_ord", "Value"] }) and var sourceTable],
+ GroupBy: [],
+ Having: null,
+ IsDistinct: false,
+ Limit: null,
+ Offset: null
+ })
+ {
+ // x.Array.Any() => 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));
+ }
+ }
+ }
+
+ // x.Array1.Intersect(x.Array2).Any() => x.Array1 && x.Array2
+ if (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);
+ }
+
+ protected override ShapedQueryExpression? TranslateCount(ShapedQueryExpression source, LambdaExpression? predicate)
+ {
+ // TODO: Does json_array_length pass through here? Most probably not, since it's not mapped with ElementTypeMapping...
+ // 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);
+ }
+
+ 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: []
+ } select1
+ && source2.QueryExpression is SelectExpression
+ {
+ Tables: [PostgresUnnestExpression { Array: var array2 }],
+ GroupBy: [],
+ Having: null,
+ IsDistinct: false,
+ Limit: null,
+ Offset: null,
+ Orderings: []
+ } select2)
+ {
+ // TODO: Allow peeking into projection mapping
+ var (clonedSelect1, clonedSelect2) = (select1.Clone(), select2.Clone());
+ clonedSelect1.ApplyProjection();
+ clonedSelect2.ApplyProjection();
+
+ Check.DebugAssert(clonedSelect1.Projection.Count == 1 && clonedSelect2.Projection.Count == 1,
+ "Multiple projections out of unnest");
+ var elementClrType = clonedSelect1.Projection[0].Expression.Type;
+ var typeMapping1 = clonedSelect1.Projection[0].Expression.TypeMapping;
+ var typeMapping2 = clonedSelect2.Projection[0].Expression.TypeMapping;
+
+ Check.DebugAssert(typeMapping1 is not null || typeMapping2 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 = typeMapping1 ?? typeMapping2;
+ var unnestExpression = new PostgresUnnestExpression(
+ unnestExpression1.Alias, _sqlExpressionFactory.Add(array1, array2), "value");
+ var selectExpression = new SelectExpression(unnestExpression, "value", elementClrType, 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)
+ {
+ // TODO: Does json_array_length pass through here? Most probably not, since it's not mapped with ElementTypeMapping...
+ // 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? 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
+ } selectExpression
+ && TranslateExpression(count) is { } translatedCount)
+ {
+ // Extract the column projected out of the source, and simplify the subquery to a simple JsonScalarExpression
+ var shaperExpression = source.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
+ && selectExpression.GetProjection(projectionBindingExpression) is SqlExpression projection)
+ {
+ selectExpression = new SelectExpression(
+ new PostgresUnnestExpression(
+ unnestExpression.Alias,
+ new PostgresArraySliceExpression(
+ array,
+ lowerBound: GenerateOneBasedIndexExpression(translatedCount),
+ upperBound: null),
+ "value"),
+ "value",
+ projection.Type,
+ projection.TypeMapping);
+
+ return source.Update(
+ selectExpression,
+ new ProjectionBindingExpression(selectExpression, new ProjectionMember(), projection.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
+ } selectExpression)
+ {
+ 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 cloned = selectExpression.Clone();
+ cloned.ApplyProjection();
+
+ return source.UpdateQueryExpression(
+ new SelectExpression(
+ new PostgresUnnestExpression(unnestExpression.Alias, sliceExpression, "value"),
+ "value",
+ cloned.Projection[0].Expression.Type,
+ cloned.Projection[0].Expression.TypeMapping));
+ }
+
+ return base.TranslateTake(source, count);
}
///
@@ -127,6 +774,63 @@ protected override bool IsValidSelectExpressionForExecuteDelete(
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];
+
+ ///
+ /// 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;
@@ -163,4 +867,56 @@ public bool ContainsReferenceToMainTable(TableExpressionBase tableExpression)
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);
+
+ return unnestExpression.Update(
+ _sqlExpressionFactory.ApplyTypeMapping(unnestExpression.Array, collectionTypeMapping));
+ }
+
+ default:
+ return base.VisitExtension(expression);
+ }
+ }
+ }
}
diff --git a/src/EFCore.PG/Query/Internal/NpgsqlSqlNullabilityProcessor.cs b/src/EFCore.PG/Query/Internal/NpgsqlSqlNullabilityProcessor.cs
index ffb4a7caa..b7c20e5c1 100644
--- a/src/EFCore.PG/Query/Internal/NpgsqlSqlNullabilityProcessor.cs
+++ b/src/EFCore.PG/Query/Internal/NpgsqlSqlNullabilityProcessor.cs
@@ -176,26 +176,17 @@ protected override SqlExpression VisitCustomSqlExpression(
out bool nullable)
=> sqlExpression switch
{
- PostgresAnyExpression postgresAnyExpression
- => VisitAny(postgresAnyExpression, allowOptimizedExpansion, out nullable),
- PostgresAllExpression postgresAllExpression
- => VisitAll(postgresAllExpression, allowOptimizedExpansion, out nullable),
- PostgresArrayIndexExpression arrayIndexExpression
- => VisitArrayIndex(arrayIndexExpression, allowOptimizedExpansion, out nullable),
- PostgresBinaryExpression binaryExpression
- => VisitPostgresBinary(binaryExpression, allowOptimizedExpansion, out nullable),
- PostgresILikeExpression ilikeExpression
- => VisitILike(ilikeExpression, allowOptimizedExpansion, out nullable),
- PostgresJsonTraversalExpression postgresJsonTraversalExpression
- => VisitJsonTraversal(postgresJsonTraversalExpression, allowOptimizedExpansion, out nullable),
- PostgresNewArrayExpression newArrayExpression
- => VisitNewArray(newArrayExpression, allowOptimizedExpansion, out nullable),
- PostgresRegexMatchExpression regexMatchExpression
- => VisitRegexMatch(regexMatchExpression, allowOptimizedExpansion, out nullable),
- PostgresRowValueExpression postgresRowValueExpression
- => VisitRowValueExpression(postgresRowValueExpression, allowOptimizedExpansion, out nullable),
- PostgresUnknownBinaryExpression postgresUnknownBinaryExpression
- => VisitUnknownBinary(postgresUnknownBinaryExpression, allowOptimizedExpansion, out nullable),
+ PostgresAnyExpression e => VisitAny(e, allowOptimizedExpansion, out nullable),
+ PostgresAllExpression e => VisitAll(e, allowOptimizedExpansion, out nullable),
+ PostgresArrayIndexExpression e => VisitArrayIndex(e, allowOptimizedExpansion, out nullable),
+ PostgresArraySliceExpression e => VisitArraySlice(e, allowOptimizedExpansion, out nullable),
+ PostgresBinaryExpression e => VisitPostgresBinary(e, allowOptimizedExpansion, out nullable),
+ PostgresILikeExpression e => VisitILike(e, allowOptimizedExpansion, out nullable),
+ PostgresJsonTraversalExpression e => VisitJsonTraversal(e, allowOptimizedExpansion, out nullable),
+ PostgresNewArrayExpression e => VisitNewArray(e, allowOptimizedExpansion, out nullable),
+ PostgresRegexMatchExpression e => VisitRegexMatch(e, allowOptimizedExpansion, out nullable),
+ PostgresRowValueExpression e => VisitRowValueExpression(e, allowOptimizedExpansion, out nullable),
+ PostgresUnknownBinaryExpression e => VisitUnknownBinary(e, allowOptimizedExpansion, out nullable),
// PostgresFunctionExpression is visited via the SqlFunctionExpression override below
@@ -321,6 +312,29 @@ protected virtual SqlExpression VisitArrayIndex(
return arrayIndexExpression.Update(array, index);
}
+ ///
+ /// 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 virtual SqlExpression VisitArraySlice(
+ PostgresArraySliceExpression arraySliceExpression, bool allowOptimizedExpansion, out bool nullable)
+ {
+ Check.NotNull(arraySliceExpression, nameof(arraySliceExpression));
+
+ var array = Visit(arraySliceExpression.Array, allowOptimizedExpansion, out var arrayNullable);
+ var lowerBound = Visit(arraySliceExpression.LowerBound, allowOptimizedExpansion, out var lowerBoundNullable);
+ var upperBound = Visit(arraySliceExpression.UpperBound, allowOptimizedExpansion, out var upperBoundNullable);
+
+ nullable = arrayNullable
+ || lowerBoundNullable
+ || upperBoundNullable
+ || ((NpgsqlArrayTypeMapping)arraySliceExpression.Array.TypeMapping!).IsElementNullable;
+
+ return arraySliceExpression.Update(array, lowerBound, upperBound);
+ }
+
///
/// 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
diff --git a/src/EFCore.PG/Query/Internal/NpgsqlSqlTranslatingExpressionVisitor.cs b/src/EFCore.PG/Query/Internal/NpgsqlSqlTranslatingExpressionVisitor.cs
index 44bd37b73..5787fb4f5 100644
--- a/src/EFCore.PG/Query/Internal/NpgsqlSqlTranslatingExpressionVisitor.cs
+++ b/src/EFCore.PG/Query/Internal/NpgsqlSqlTranslatingExpressionVisitor.cs
@@ -115,179 +115,6 @@ protected override Expression VisitUnary(UnaryExpression unaryExpression)
return base.VisitUnary(unaryExpression);
}
- ///
- /// 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 VisitMethodCall(MethodCallExpression methodCall)
- {
- if (methodCall.Arguments.Count > 0 &&
- methodCall.Arguments[0].Type.IsArrayOrGenericList() &&
- VisitArrayMethodCall(methodCall.Method, methodCall.Arguments) is { } visited)
- {
- return visited;
- }
-
- return base.VisitMethodCall(methodCall);
- }
-
- ///
- /// Identifies complex array-related constructs which cannot be translated in regular method translators, since
- /// they require accessing lambdas.
- ///
- private Expression? VisitArrayMethodCall(MethodInfo method, ReadOnlyCollection arguments)
- {
- var array = arguments[0];
- {
- if (method.IsClosedFormOf(EnumerableMethods.AnyWithPredicate) &&
- arguments[1] is LambdaExpression wherePredicate)
- {
- if (wherePredicate.Body is MethodCallExpression wherePredicateMethodCall)
- {
- var predicateMethod = wherePredicateMethodCall.Method;
- var predicateArguments = wherePredicateMethodCall.Arguments;
-
- // Pattern match: new[] { "a", "b", "c" }.Any(p => EF.Functions.Like(e.SomeText, p))
- // Translation: s.SomeText LIKE ANY (ARRAY['a','b','c'])
- // Note: we also handle the this equality instead of Like, see NpgsqlArrayMethodTranslator
- if ((predicateMethod == Like2MethodInfo || predicateMethod == ILike2MethodInfo) &&
- predicateArguments[2] == wherePredicate.Parameters[0])
- {
- return _sqlExpressionFactory.Any(
- (SqlExpression)Visit(predicateArguments[1]),
- (SqlExpression)Visit(array),
- wherePredicateMethodCall.Method == Like2MethodInfo
- ? PostgresAnyOperatorType.Like
- : PostgresAnyOperatorType.ILike);
- }
-
- // Pattern match: new[] { 4, 5 }.Any(p => e.SomeArray.Contains(p))
- // Translation: s.SomeArray && ARRAY[4, 5] (array overlap).
- if (predicateMethod.IsClosedFormOf(EnumerableMethods.Contains) &&
- predicateArguments[0].Type.IsArrayOrGenericList() &&
- predicateArguments[1] is ParameterExpression parameterExpression1 &&
- parameterExpression1 == wherePredicate.Parameters[0])
- {
- return _sqlExpressionFactory.Overlaps(
- (SqlExpression)Visit(arguments[0]),
- (SqlExpression)Visit(wherePredicateMethodCall.Arguments[0]));
- }
-
- // As above, but for Contains on List
- if (predicateMethod.DeclaringType?.IsGenericType == true &&
- predicateMethod.DeclaringType.GetGenericTypeDefinition() == typeof(List<>) &&
- predicateMethod.Name == nameof(List.Contains) &&
- predicateMethod.GetParameters().Length == 1 &&
- predicateArguments[0] is ParameterExpression parameterExpression2 &&
- parameterExpression2 == wherePredicate.Parameters[0])
- {
- return _sqlExpressionFactory.Overlaps(
- (SqlExpression)Visit(arguments[0]),
- (SqlExpression)Visit(wherePredicateMethodCall.Object!));
- }
- }
-
- // Pattern match for: array.Any(e => e == x) (and other equality patterns)
- // Transform this to Contains.
- if (TryMatchEquality(wherePredicate.Body, out var left, out var right) &&
- (left == wherePredicate.Parameters[0] || right == wherePredicate.Parameters[0]))
- {
- var item = left == wherePredicate.Parameters[0]
- ? right
- : right == wherePredicate.Parameters[0]
- ? left
- : null;
-
- return item is null
- ? null
- : Visit(Expression.Call(EnumerableMethods.Contains.MakeGenericMethod(method.GetGenericArguments()[0]), array,
- item));
- }
-
- static bool TryMatchEquality(
- Expression expression,
- [NotNullWhen(true)] out Expression? left,
- [NotNullWhen(true)] out Expression? right)
- {
- if (expression is BinaryExpression binary)
- {
- (left, right) = (binary.Left, binary.Right);
- return true;
- }
-
- if (expression is MethodCallExpression methodCall)
- {
- if (methodCall.Method == ObjectEquals)
- {
- (left, right) = (methodCall.Arguments[0], methodCall.Arguments[1]);
- return true;
- }
-
- if (methodCall.Method.Name == nameof(object.Equals) && methodCall.Arguments.Count == 1)
- {
- (left, right) = (methodCall.Object!, methodCall.Arguments[0]);
- return true;
- }
- }
-
- (left, right) = (null, null);
- return false;
- }
- }
- }
-
- {
- if (method.IsClosedFormOf(EnumerableMethods.All) &&
- arguments[1] is LambdaExpression wherePredicate &&
- wherePredicate.Body is MethodCallExpression wherePredicateMethodCall)
- {
- var predicateMethod = wherePredicateMethodCall.Method;
- var predicateArguments = wherePredicateMethodCall.Arguments;
-
- // 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'])
- if ((predicateMethod == Like2MethodInfo || predicateMethod == ILike2MethodInfo) &&
- predicateArguments[2] == wherePredicate.Parameters[0])
- {
- return _sqlExpressionFactory.All(
- (SqlExpression)Visit(predicateArguments[1]),
- (SqlExpression)Visit(arguments[0]),
- wherePredicateMethodCall.Method == Like2MethodInfo
- ? PostgresAllOperatorType.Like : PostgresAllOperatorType.ILike);
- }
-
- // Pattern match for: new[] { 4, 5 }.All(p => e.SomeArray.Contains(p)),
- // using array containment (<@)
- if (predicateMethod.IsClosedFormOf(EnumerableMethods.Contains) &&
- predicateArguments[0].Type.IsArrayOrGenericList() &&
- predicateArguments[1] is ParameterExpression parameterExpression &&
- parameterExpression == wherePredicate.Parameters[0])
- {
- return _sqlExpressionFactory.ContainedBy(
- (SqlExpression)Visit(arguments[0]),
- (SqlExpression)Visit(predicateArguments[0]));
- }
-
- // As above, but for Contains on List
- if (predicateMethod.DeclaringType?.IsGenericType == true &&
- predicateMethod.DeclaringType.GetGenericTypeDefinition() == typeof(List<>) &&
- predicateMethod.Name == nameof(List.Contains) &&
- predicateMethod.GetParameters().Length == 1 &&
- predicateArguments[0] is ParameterExpression parameterExpression2 &&
- parameterExpression2 == wherePredicate.Parameters[0])
- {
- return _sqlExpressionFactory.ContainedBy(
- (SqlExpression)Visit(arguments[0]),
- (SqlExpression)Visit(wherePredicateMethodCall.Object!));
- }
- }
- }
-
- return _ltreeTranslator.VisitArrayMethodCall(this, method, arguments);
- }
-
///
protected override Expression VisitNewArray(NewArrayExpression newArrayExpression)
{
diff --git a/src/EFCore.PG/Query/NpgsqlSqlExpressionFactory.cs b/src/EFCore.PG/Query/NpgsqlSqlExpressionFactory.cs
index f5717d339..4691cc64f 100644
--- a/src/EFCore.PG/Query/NpgsqlSqlExpressionFactory.cs
+++ b/src/EFCore.PG/Query/NpgsqlSqlExpressionFactory.cs
@@ -60,6 +60,7 @@ public virtual PostgresAllExpression All(
PostgresAllOperatorType operatorType)
=> (PostgresAllExpression)ApplyDefaultTypeMapping(new PostgresAllExpression(item, array, operatorType, null));
+
///
/// Creates a new , corresponding to the PostgreSQL-specific array subscripting operator.
///
@@ -381,6 +382,7 @@ public virtual PostgresFunctionExpression AggregateFunction(
PostgresAnyExpression e => ApplyTypeMappingOnAny(e),
PostgresAllExpression e => ApplyTypeMappingOnAll(e),
PostgresArrayIndexExpression e => ApplyTypeMappingOnArrayIndex(e, typeMapping),
+ PostgresArraySliceExpression e => ApplyTypeMappingOnArraySlice(e, typeMapping),
PostgresBinaryExpression e => ApplyTypeMappingOnPostgresBinary(e, typeMapping),
PostgresFunctionExpression e => e.ApplyTypeMapping(typeMapping),
PostgresILikeExpression e => ApplyTypeMappingOnILike(e),
@@ -605,7 +607,7 @@ private SqlExpression ApplyTypeMappingOnAll(PostgresAllExpression postgresAllExp
? unary.Operand.TypeMapping
: null)
// If we couldn't find a type mapping on the item, try inferring it from the array
- ?? arrayMapping?.ElementMapping
+ ?? arrayMapping?.ElementTypeMapping
?? _typeMappingSource.FindMapping(itemExpression.Type, Dependencies.Model);
if (itemMapping is null)
@@ -662,11 +664,32 @@ private SqlExpression ApplyTypeMappingOnArrayIndex(
postgresArrayIndexExpression.Type,
// If the array has a type mapping (i.e. column), prefer that just like we prefer column mappings in general
postgresArrayIndexExpression.Array.TypeMapping is NpgsqlArrayTypeMapping arrayMapping
- ? arrayMapping.ElementMapping
+ ? arrayMapping.ElementTypeMapping
: typeMapping
?? _typeMappingSource.FindMapping(postgresArrayIndexExpression.Type, Dependencies.Model));
}
+ private SqlExpression ApplyTypeMappingOnArraySlice(
+ PostgresArraySliceExpression postgresArraySliceExpression,
+ RelationalTypeMapping? typeMapping)
+ {
+ // If the slice operand has a type mapping, that bubbles up (slice is just a view over that). Otherwise apply the external type
+ // mapping down. The bounds are always ints and don't participate in any inference.
+ var lowerBound = postgresArraySliceExpression.LowerBound is null
+ ? null
+ : ApplyDefaultTypeMapping(postgresArraySliceExpression.LowerBound);
+ var upperBound = postgresArraySliceExpression.UpperBound is null
+ ? null
+ : ApplyDefaultTypeMapping(postgresArraySliceExpression.UpperBound);
+
+ var inferredTypeMapping = postgresArraySliceExpression.TypeMapping ?? typeMapping;
+
+ return new PostgresArraySliceExpression(
+ ApplyTypeMapping(postgresArraySliceExpression.Array, inferredTypeMapping),
+ lowerBound,
+ upperBound);
+ }
+
private SqlExpression ApplyTypeMappingOnILike(PostgresILikeExpression ilikeExpression)
{
var inferredTypeMapping = (ilikeExpression.EscapeChar is null
@@ -970,7 +993,7 @@ static bool IsTextualTypeMapping(RelationalTypeMapping mapping)
return postgresNewArrayExpression;
}
- elementTypeMapping = arrayTypeMapping.ElementMapping;
+ elementTypeMapping = arrayTypeMapping.ElementTypeMapping;
}
else
{
diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlArrayArrayTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlArrayArrayTypeMapping.cs
index b7008ee22..33d47da03 100644
--- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlArrayArrayTypeMapping.cs
+++ b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlArrayArrayTypeMapping.cs
@@ -20,31 +20,31 @@ public class NpgsqlArrayArrayTypeMapping : NpgsqlArrayTypeMapping
/// Creates the default array mapping (i.e. for the single-dimensional CLR array type)
///
/// The database type to map.
- /// The element type mapping.
- public NpgsqlArrayArrayTypeMapping(string storeType, RelationalTypeMapping elementMapping)
- : this(storeType, elementMapping, elementMapping.ClrType.MakeArrayType()) {}
+ /// The element type mapping.
+ public NpgsqlArrayArrayTypeMapping(string storeType, RelationalTypeMapping elementTypeMapping)
+ : this(storeType, elementTypeMapping, elementTypeMapping.ClrType.MakeArrayType()) {}
///
/// Creates the default array mapping (i.e. for the single-dimensional CLR array type)
///
/// The array type to map.
- /// The element type mapping.
- public NpgsqlArrayArrayTypeMapping(Type arrayType, RelationalTypeMapping elementMapping)
- : this(elementMapping.StoreType + "[]", elementMapping, arrayType) {}
+ /// The element type mapping.
+ public NpgsqlArrayArrayTypeMapping(Type arrayType, RelationalTypeMapping elementTypeMapping)
+ : this(elementTypeMapping.StoreType + "[]", elementTypeMapping, arrayType) {}
- private NpgsqlArrayArrayTypeMapping(string storeType, RelationalTypeMapping elementMapping, Type arrayType)
- : this(CreateParameters(storeType, elementMapping, arrayType), elementMapping)
+ private NpgsqlArrayArrayTypeMapping(string storeType, RelationalTypeMapping elementTypeMapping, Type arrayType)
+ : this(CreateParameters(storeType, elementTypeMapping, arrayType))
{
}
private static RelationalTypeMappingParameters CreateParameters(
string storeType,
- RelationalTypeMapping elementMapping,
+ RelationalTypeMapping elementTypeMapping,
Type arrayType)
{
ValueConverter? converter = null;
- if (elementMapping.Converter is { } elementConverter)
+ if (elementTypeMapping.Converter is { } elementConverter)
{
var isNullable = arrayType.GetElementType()!.IsNullableValueType();
@@ -64,7 +64,11 @@ private static RelationalTypeMappingParameters CreateParameters(
}
return new RelationalTypeMappingParameters(
- new CoreTypeMappingParameters(arrayType, converter, CreateComparer(elementMapping, arrayType)),
+ new CoreTypeMappingParameters(
+ arrayType,
+ converter,
+ CreateComparer(elementTypeMapping, arrayType),
+ elementTypeMapping: elementTypeMapping),
storeType);
}
@@ -74,13 +78,9 @@ private static RelationalTypeMappingParameters CreateParameters(
/// 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 NpgsqlArrayArrayTypeMapping(
- RelationalTypeMappingParameters parameters,
- RelationalTypeMapping elementMapping,
- bool? isElementNullable = null)
+ protected NpgsqlArrayArrayTypeMapping(RelationalTypeMappingParameters parameters, bool? isElementNullable = null)
: base(
parameters,
- elementMapping,
CalculateElementNullability(
// Note that the ClrType on elementMapping has been unwrapped for nullability, so we consult the array's CLR type instead
parameters.CoreParameters.ClrType.GetElementType()
@@ -100,7 +100,7 @@ protected NpgsqlArrayArrayTypeMapping(
/// doing so can result in application failures when updating to a new Entity Framework Core release.
///
public override NpgsqlArrayTypeMapping MakeNonNullable()
- => new NpgsqlArrayArrayTypeMapping(Parameters, ElementMapping, isElementNullable: false);
+ => new NpgsqlArrayArrayTypeMapping(Parameters, isElementNullable: false);
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -109,7 +109,7 @@ public override NpgsqlArrayTypeMapping MakeNonNullable()
/// doing so can result in application failures when updating to a new Entity Framework Core release.
///
protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters, RelationalTypeMapping elementMapping)
- => new NpgsqlArrayArrayTypeMapping(parameters, elementMapping);
+ => new NpgsqlArrayArrayTypeMapping(parameters.WithCoreParameters(parameters.CoreParameters.WithElementTypeMapping(elementMapping)));
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -135,7 +135,7 @@ public override NpgsqlArrayTypeMapping FlipArrayListClrType(Type newType)
var listElementType = newType.GetGenericArguments()[0];
return listElementType == elementType
- ? new NpgsqlArrayListTypeMapping(newType, ElementMapping)
+ ? new NpgsqlArrayListTypeMapping(newType, ElementTypeMapping)
: throw new ArgumentException(
"Mismatch in array element CLR types when converting a type mapping: " +
$"{listElementType} and {elementType.Name}");
diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlArrayListTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlArrayListTypeMapping.cs
index cc53387d3..17c699b3a 100644
--- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlArrayListTypeMapping.cs
+++ b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlArrayListTypeMapping.cs
@@ -18,31 +18,31 @@ public class NpgsqlArrayListTypeMapping : NpgsqlArrayTypeMapping
/// Creates the default list mapping.
///
/// The database type to map.
- /// The element type mapping.
- public NpgsqlArrayListTypeMapping(string storeType, RelationalTypeMapping elementMapping)
- : this(storeType, elementMapping, typeof(List<>).MakeGenericType(elementMapping.ClrType)) {}
+ /// The element type mapping.
+ public NpgsqlArrayListTypeMapping(string storeType, RelationalTypeMapping elementTypeMapping)
+ : this(storeType, elementTypeMapping, typeof(List<>).MakeGenericType(elementTypeMapping.ClrType)) {}
///
/// Creates the default list mapping.
///
/// The database type to map.
- /// The element type mapping.
- public NpgsqlArrayListTypeMapping(Type listType, RelationalTypeMapping elementMapping)
- : this(elementMapping.StoreType + "[]", elementMapping, listType) {}
+ /// The element type mapping.
+ public NpgsqlArrayListTypeMapping(Type listType, RelationalTypeMapping elementTypeMapping)
+ : this(elementTypeMapping.StoreType + "[]", elementTypeMapping, listType) {}
- private NpgsqlArrayListTypeMapping(string storeType, RelationalTypeMapping elementMapping, Type listType)
- : this(CreateParameters(storeType, elementMapping, listType), elementMapping)
+ private NpgsqlArrayListTypeMapping(string storeType, RelationalTypeMapping elementTypeMapping, Type listType)
+ : this(CreateParameters(storeType, elementTypeMapping, listType))
{
}
private static RelationalTypeMappingParameters CreateParameters(
string storeType,
- RelationalTypeMapping elementMapping,
+ RelationalTypeMapping elementTypeMapping,
Type listType)
{
ValueConverter? converter = null;
- if (elementMapping.Converter is { } elementConverter)
+ if (elementTypeMapping.Converter is { } elementConverter)
{
var isNullable = listType.TryGetElementType(out var elementType) && elementType.IsNullableValueType();
@@ -62,7 +62,11 @@ private static RelationalTypeMappingParameters CreateParameters(
}
return new RelationalTypeMappingParameters(
- new CoreTypeMappingParameters(listType, converter, CreateComparer(elementMapping, listType)),
+ new CoreTypeMappingParameters(
+ listType,
+ converter,
+ CreateComparer(elementTypeMapping, listType),
+ elementTypeMapping: elementTypeMapping),
storeType);
}
@@ -72,11 +76,9 @@ private static RelationalTypeMappingParameters CreateParameters(
/// 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 NpgsqlArrayListTypeMapping(
- RelationalTypeMappingParameters parameters, RelationalTypeMapping elementMapping, bool? isElementNullable = null)
+ protected NpgsqlArrayListTypeMapping(RelationalTypeMappingParameters parameters, bool? isElementNullable = null)
: base(
parameters,
- elementMapping,
CalculateElementNullability(
// Note that the ClrType on elementMapping has been unwrapped for nullability, so we consult the List's CLR type instead
parameters.CoreParameters.ClrType.GetGenericArguments()[0],
@@ -95,7 +97,7 @@ protected NpgsqlArrayListTypeMapping(
/// doing so can result in application failures when updating to a new Entity Framework Core release.
///
public override NpgsqlArrayTypeMapping MakeNonNullable()
- => new NpgsqlArrayListTypeMapping(Parameters, ElementMapping, isElementNullable: false);
+ => new NpgsqlArrayListTypeMapping(Parameters, isElementNullable: false);
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -104,7 +106,7 @@ public override NpgsqlArrayTypeMapping MakeNonNullable()
/// doing so can result in application failures when updating to a new Entity Framework Core release.
///
protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters, RelationalTypeMapping elementMapping)
- => new NpgsqlArrayListTypeMapping(parameters, elementMapping);
+ => new NpgsqlArrayListTypeMapping(parameters.WithCoreParameters(parameters.CoreParameters.WithElementTypeMapping(elementMapping)));
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -130,7 +132,7 @@ public override NpgsqlArrayTypeMapping FlipArrayListClrType(Type newType)
var arrayElementType = newType.GetElementType()!;
return arrayElementType == elementType
- ? new NpgsqlArrayArrayTypeMapping(newType, ElementMapping)
+ ? new NpgsqlArrayArrayTypeMapping(newType, ElementTypeMapping)
: throw new ArgumentException(
"Mismatch in list element CLR types when converting a type mapping: " +
$"{arrayElementType} and {elementType.Name}");
diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlArrayTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlArrayTypeMapping.cs
index 7e3b89fdf..9158e0b43 100644
--- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlArrayTypeMapping.cs
+++ b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlArrayTypeMapping.cs
@@ -13,18 +13,13 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping;
///
public abstract class NpgsqlArrayTypeMapping : RelationalTypeMapping
{
- ///
- /// The relational type mapping used to initialize the array mapping.
- ///
- public virtual RelationalTypeMapping ElementMapping { get; }
-
///
/// The database type used by Npgsql.
///
public virtual NpgsqlDbType? NpgsqlDbType { get; }
///
- /// Whether the array's element is nullable. This is required since and do not
+ /// Whether the array's element is nullable. This is required since and do not
/// contain nullable reference type information.
///
public virtual bool IsElementNullable { get; }
@@ -35,24 +30,38 @@ public abstract class NpgsqlArrayTypeMapping : RelationalTypeMapping
/// 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 NpgsqlArrayTypeMapping(
- RelationalTypeMappingParameters parameters, RelationalTypeMapping elementMapping, bool isElementNullable)
+ protected NpgsqlArrayTypeMapping(RelationalTypeMappingParameters parameters, bool isElementNullable)
: base(parameters)
{
- ElementMapping = elementMapping;
IsElementNullable = isElementNullable;
// If the element mapping has an NpgsqlDbType or DbType, set our own NpgsqlDbType as an array of that.
// Otherwise let the ADO.NET layer infer the PostgreSQL type. We can't always let it infer, otherwise
// when given a byte[] it will infer byte (but we want smallint[])
NpgsqlDbType = NpgsqlTypes.NpgsqlDbType.Array |
- (elementMapping is INpgsqlTypeMapping elementNpgsqlTypeMapping
+ (ElementTypeMapping is INpgsqlTypeMapping elementNpgsqlTypeMapping
? elementNpgsqlTypeMapping.NpgsqlDbType
- : elementMapping.DbType.HasValue
- ? new NpgsqlParameter { DbType = elementMapping.DbType.Value }.NpgsqlDbType
+ : ElementTypeMapping.DbType.HasValue
+ ? new NpgsqlParameter { DbType = ElementTypeMapping.DbType.Value }.NpgsqlDbType
: default(NpgsqlDbType?));
}
+ ///
+ /// The element's type mapping.
+ ///
+ public override RelationalTypeMapping ElementTypeMapping
+ {
+ get
+ {
+ var elementTypeMapping = base.ElementTypeMapping;
+ Check.DebugAssert(elementTypeMapping is not null,
+ "NpgsqlArrayTypeMapping without an element type mapping");
+ Check.DebugAssert(elementTypeMapping is RelationalTypeMapping,
+ "NpgsqlArrayTypeMapping with a non-relational element type mapping");
+ return (RelationalTypeMapping)elementTypeMapping;
+ }
+ }
+
///
/// Returns a copy of this type mapping with set to .
///
@@ -74,7 +83,7 @@ public override CoreTypeMapping Clone(ValueConverter? converter)
{
return Clone(
Parameters.WithComposedConverter(converter),
- (RelationalTypeMapping)ElementMapping.Clone(converter is INpgsqlArrayConverter arrayConverter
+ (RelationalTypeMapping)ElementTypeMapping.Clone(converter is INpgsqlArrayConverter arrayConverter
? arrayConverter.ElementConverter
: null));
}
@@ -99,7 +108,7 @@ public override CoreTypeMapping Clone(ValueConverter? converter)
///
protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters)
{
- var elementMapping = ElementMapping;
+ var elementMapping = ElementTypeMapping;
// Apply precision, scale and size to the element mapping, not to the array
if (parameters.Size is not null)
@@ -155,7 +164,7 @@ protected override string GenerateNonNullSqlLiteral(object value)
sb.Append("ARRAY[");
for (var i = 0; i < list.Count; i++)
{
- sb.Append(ElementMapping.GenerateProviderValueSqlLiteral(list[i]));
+ sb.Append(ElementTypeMapping.GenerateProviderValueSqlLiteral(list[i]));
if (i < list.Count - 1)
{
sb.Append(",");
@@ -163,7 +172,7 @@ protected override string GenerateNonNullSqlLiteral(object value)
}
sb.Append("]::");
- sb.Append(ElementMapping.StoreType);
+ sb.Append(ElementTypeMapping.StoreType);
sb.Append("[]");
return sb.ToString();
}
diff --git a/src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs b/src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs
index b0cbf226a..d1aa4aeeb 100644
--- a/src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs
+++ b/src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs
@@ -47,7 +47,7 @@ static NpgsqlTypeMappingSource()
/// doing so can result in application failures when updating to a new Entity Framework Core release.
///
protected virtual ConcurrentDictionary StoreTypeMappings { get; }
-
+
///
/// 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
@@ -214,7 +214,8 @@ public NpgsqlTypeMappingSource(
INpgsqlSingletonOptions options)
: base(dependencies, relationalDependencies)
{
- _supportsMultiranges = options.PostgresVersionWithoutDefault is null || options.PostgresVersionWithoutDefault.AtLeast(14);
+ _supportsMultiranges = !options.IsPostgresVersionSet
+ || options.IsPostgresVersionSet && options.PostgresVersion.AtLeast(14);
_sqlGenerationHelper = Check.NotNull(sqlGenerationHelper, nameof(sqlGenerationHelper));
@@ -684,7 +685,7 @@ protected virtual void SetupEnumMappings(ISqlGenerationHelper sqlGenerationHelpe
// If no mapping was found for the element, there's no mapping for the array.
// Also, arrays of arrays aren't supported (as opposed to multidimensional arrays) by PostgreSQL
- if (elementMapping is null || elementMapping is NpgsqlArrayTypeMapping)
+ if (elementMapping is null or NpgsqlArrayTypeMapping)
{
return null;
}
@@ -708,7 +709,7 @@ protected virtual void SetupEnumMappings(ISqlGenerationHelper sqlGenerationHelpe
// If no mapping was found for the element, there's no mapping for the array.
// Also, arrays of arrays aren't supported (as opposed to multidimensional arrays) by PostgreSQL
- if (elementMapping is null || elementMapping is NpgsqlArrayTypeMapping)
+ if (elementMapping is null or NpgsqlArrayTypeMapping)
{
return null;
}
@@ -824,11 +825,16 @@ protected virtual void SetupEnumMappings(ISqlGenerationHelper sqlGenerationHelpe
}
///
- /// Finds the mapping for a container given its CLR type and its containee's type mapping; this is currently used to infer type
- /// mappings for ranges and multiranges from their values.
+ /// Finds the mapping for a container given its CLR type and its containee's type mapping; this is used when inferring type mappings
+ /// for arrays and ranges/multiranges.
///
public virtual RelationalTypeMapping? FindContainerMapping(Type containerClrType, RelationalTypeMapping containeeTypeMapping)
{
+ if (containerClrType.TryGetElementType(out _))
+ {
+ return FindMapping(containerClrType, containeeTypeMapping.StoreType + "[]");
+ }
+
if (containerClrType.TryGetRangeSubtype(out var subtypeType))
{
return _rangeTypeMappings.TryGetValue(subtypeType, out var candidateMappings)
@@ -951,7 +957,7 @@ private static bool NameBasesUsesPrecision(ReadOnlySpan span)
// For arrays over reference types, the CLR type doesn't convey nullability (unlike with arrays over value types).
// We decode NRT annotations here to return the correct type mapping.
- if (mapping is NpgsqlArrayTypeMapping { ElementMapping.ClrType.IsValueType: false } arrayMapping
+ if (mapping is NpgsqlArrayTypeMapping { ElementTypeMapping.ClrType.IsValueType: false } arrayMapping
&& !property.IsShadowProperty())
{
var nullabilityInfo =
diff --git a/test/EFCore.PG.FunctionalTests/Query/ArrayArrayQueryTest.cs b/test/EFCore.PG.FunctionalTests/Query/ArrayArrayQueryTest.cs
index 475b29583..8f54f70fc 100644
--- a/test/EFCore.PG.FunctionalTests/Query/ArrayArrayQueryTest.cs
+++ b/test/EFCore.PG.FunctionalTests/Query/ArrayArrayQueryTest.cs
@@ -817,14 +817,17 @@ public override async Task All_Contains(bool async)
public override async Task Append(bool async)
{
- await base.Append(async);
-
- AssertSql(
-"""
-SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15"
-FROM "SomeEntities" AS s
-WHERE array_append(s."IntArray", 5) = ARRAY[3,4,5]::integer[]
-""");
+ // TODO: https://github.com/dotnet/efcore/issues/30669
+ await AssertTranslationFailed(() => base.Append(async));
+
+// await base.Append(async);
+//
+// AssertSql(
+// """
+// SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15"
+// FROM "SomeEntities" AS s
+// WHERE array_append(s."IntArray", 5) = ARRAY[3,4,5]::integer[]
+// """);
}
public override async Task Concat(bool async)
diff --git a/test/EFCore.PG.FunctionalTests/Query/ArrayListQueryTest.cs b/test/EFCore.PG.FunctionalTests/Query/ArrayListQueryTest.cs
index 0333ece2f..3f374d5a4 100644
--- a/test/EFCore.PG.FunctionalTests/Query/ArrayListQueryTest.cs
+++ b/test/EFCore.PG.FunctionalTests/Query/ArrayListQueryTest.cs
@@ -820,14 +820,17 @@ public override async Task All_Contains(bool async)
public override async Task Append(bool async)
{
- await base.Append(async);
-
- AssertSql(
-"""
-SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15"
-FROM "SomeEntities" AS s
-WHERE array_append(s."IntList", 5) = ARRAY[3,4,5]::integer[]
-""");
+ // TODO: https://github.com/dotnet/efcore/issues/30669
+ await AssertTranslationFailed(() => base.Append(async));
+
+// await base.Append(async);
+//
+// AssertSql(
+// """
+// SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15"
+// FROM "SomeEntities" AS s
+// WHERE array_append(s."IntList", 5) = ARRAY[3,4,5]::integer[]
+// """);
}
public override async Task Concat(bool async)
diff --git a/test/EFCore.PG.FunctionalTests/Query/ArrayQueryTest.cs b/test/EFCore.PG.FunctionalTests/Query/ArrayQueryTest.cs
index 1a5c9a62c..34fe9c503 100644
--- a/test/EFCore.PG.FunctionalTests/Query/ArrayQueryTest.cs
+++ b/test/EFCore.PG.FunctionalTests/Query/ArrayQueryTest.cs
@@ -13,7 +13,7 @@ public ArrayQueryTest(TFixture fixture, ITestOutputHelper testOutputHelper)
: base(fixture)
{
Fixture.TestSqlLoggerFactory.Clear();
- // Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper);
+ Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper);
}
#region Roundtrip
@@ -372,15 +372,15 @@ public virtual async Task All_Contains(bool async)
ss => ss.Set().Where(e => new[] { 5, 6 }.All(p => e.IntArray.Contains(p))),
entryCount: 1);
- [ConditionalFact]
- public virtual async Task Any_like_column()
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual async Task Any_like_column(bool async)
{
- using var ctx = CreateContext();
-
- await AssertTranslationFailed(
- () => ctx.SomeEntities
- .Where(e => e.StringArray.Any(p => EF.Functions.Like(p, "3")))
- .ToListAsync());
+ await AssertQuery(
+ async,
+ ss => ss.Set().Where(e => e.StringArray.Any(s => EF.Functions.Like(s, "3"))),
+ ss => ss.Set().Where(e => e.StringArray.Any(s => s.Contains("3"))),
+ entryCount: 1);
}
#endregion Any/All
diff --git a/test/EFCore.PG.FunctionalTests/Query/NonSharedPrimitiveCollectionsQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/NonSharedPrimitiveCollectionsQueryNpgsqlTest.cs
new file mode 100644
index 000000000..088c1dab1
--- /dev/null
+++ b/test/EFCore.PG.FunctionalTests/Query/NonSharedPrimitiveCollectionsQueryNpgsqlTest.cs
@@ -0,0 +1,63 @@
+using NetTopologySuite.Geometries;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Internal;
+using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities;
+
+namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query;
+
+public class NonSharedPrimitiveCollectionsQueryNpgsqlTest : NonSharedPrimitiveCollectionsQueryRelationalTestBase
+{
+ #region Support for specific element types
+
+ // Since we just use arrays for primitive collections, there's no need to test each and every element type; arrays are fully typed
+ // and don't need any special conversion/handling like in providers which use JSON.
+
+ // Npgsql maps DateTime to timestamp with time zone by default, which requires UTC timestamps.
+ public override Task Array_of_DateTime()
+ => TestArray(
+ new DateTime(2023, 1, 1, 12, 30, 0),
+ new DateTime(2023, 1, 2, 12, 30, 0),
+ mb => mb.Entity()
+ .Property(typeof(DateTime[]), "SomeArray")
+ .HasColumnType("timestamp without time zone[]"));
+
+ [ConditionalFact]
+ public virtual Task Array_of_DateTime_utc()
+ => TestArray(
+ new DateTime(2023, 1, 1, 12, 30, 0, DateTimeKind.Utc),
+ new DateTime(2023, 1, 2, 12, 30, 0, DateTimeKind.Utc));
+
+ // Npgsql only supports DateTimeOffset with Offset 0 (mapped to timestamp with time zone)
+ public override Task Array_of_DateTimeOffset()
+ => TestArray(
+ new DateTimeOffset(2023, 1, 1, 12, 30, 0, TimeSpan.Zero),
+ new DateTimeOffset(2023, 1, 2, 12, 30, 0, TimeSpan.Zero));
+
+ public override Task Array_of_geometry_is_not_supported()
+ {
+ throw new NotImplementedException();
+ }
+
+ // [ConditionalFact] // #30630
+ // public override async Task Array_of_geometry()
+ // {
+ // var exception = await Assert.ThrowsAsync(
+ // () => InitializeAsync(
+ // onConfiguring: options => options.UseNpgsql(o => o.UseNetTopologySuite()),
+ // addServices: s => s.AddEntityFrameworkNpgsqlNetTopologySuite(),
+ // onModelCreating: mb => mb.Entity().Property("Points")));
+ //
+ // Assert.Equal(CoreStrings.PropertyNotMapped("Point[]", "MyEntity", "Points"), exception.Message);
+ // }
+
+ #endregion Support for specific element types
+
+ public override async Task Column_collection_inside_json_owned_entity()
+ {
+ var exception = await Assert.ThrowsAsync(() => base.Column_collection_inside_json_owned_entity());
+
+ Assert.Equal(exception.Message, NpgsqlStrings.Ef7JsonMappingNotSupported);
+ }
+
+ protected override ITestStoreFactory TestStoreFactory
+ => NpgsqlTestStoreFactory.Instance;
+}
diff --git a/test/EFCore.PG.FunctionalTests/Query/NorthwindCompiledQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/NorthwindCompiledQueryNpgsqlTest.cs
index 7f3e7219e..8fa81bc7b 100644
--- a/test/EFCore.PG.FunctionalTests/Query/NorthwindCompiledQueryNpgsqlTest.cs
+++ b/test/EFCore.PG.FunctionalTests/Query/NorthwindCompiledQueryNpgsqlTest.cs
@@ -10,8 +10,4 @@ public NorthwindCompiledQueryNpgsqlTest(
Fixture.TestSqlLoggerFactory.Clear();
//Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper);
}
-
- // This one fails in a different way than what EF expects, since we do support array indexing,
- // but not over object arrays.
- public override void MakeBinary_does_not_throw_for_unsupported_operator() {}
-}
\ No newline at end of file
+}
diff --git a/test/EFCore.PG.FunctionalTests/Query/PrimitiveCollectionsQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/PrimitiveCollectionsQueryNpgsqlTest.cs
new file mode 100644
index 000000000..992584a4c
--- /dev/null
+++ b/test/EFCore.PG.FunctionalTests/Query/PrimitiveCollectionsQueryNpgsqlTest.cs
@@ -0,0 +1,805 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities;
+
+namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query;
+
+public class PrimitiveCollectionsQueryNpgsqlTest : PrimitiveCollectionsQueryTestBase<
+ PrimitiveCollectionsQueryNpgsqlTest.PrimitiveCollectionsQueryNpgsqlFixture>
+{
+ public PrimitiveCollectionsQueryNpgsqlTest(PrimitiveCollectionsQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper)
+ : base(fixture)
+ {
+ Fixture.TestSqlLoggerFactory.Clear();
+ Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper);
+ }
+
+ public override async Task Inline_collection_of_ints_Contains(bool async)
+ {
+ await base.Inline_collection_of_ints_Contains(async);
+
+ AssertSql(
+"""
+SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."String", p."Strings"
+FROM "PrimitiveCollectionsEntity" AS p
+WHERE p."Int" IN (10, 999)
+""");
+ }
+
+ public override async Task Inline_collection_of_nullable_ints_Contains(bool async)
+ {
+ await base.Inline_collection_of_nullable_ints_Contains(async);
+
+ AssertSql(
+"""
+SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."String", p."Strings"
+FROM "PrimitiveCollectionsEntity" AS p
+WHERE p."NullableInt" IN (10, 999)
+""");
+ }
+
+ public override async Task Inline_collection_of_nullable_ints_Contains_null(bool async)
+ {
+ await base.Inline_collection_of_nullable_ints_Contains_null(async);
+
+ AssertSql(
+"""
+SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."String", p."Strings"
+FROM "PrimitiveCollectionsEntity" AS p
+WHERE p."NullableInt" = 999 OR p."NullableInt" IS NULL
+""");
+ }
+
+ public override Task Inline_collection_Count_with_zero_values(bool async)
+ => AssertTranslationFailedWithDetails(
+ () => base.Inline_collection_Count_with_zero_values(async),
+ RelationalStrings.EmptyCollectionNotSupportedAsInlineQueryRoot);
+
+ public override async Task Inline_collection_Count_with_one_value(bool async)
+ {
+ await base.Inline_collection_Count_with_one_value(async);
+
+ AssertSql(
+"""
+SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."String", p."Strings"
+FROM "PrimitiveCollectionsEntity" AS p
+WHERE (
+ SELECT count(*)::int
+ FROM (VALUES (2::int)) AS v("Value")
+ WHERE v."Value" > p."Id") = 1
+""");
+ }
+
+ public override async Task Inline_collection_Count_with_two_values(bool async)
+ {
+ await base.Inline_collection_Count_with_two_values(async);
+
+ AssertSql(
+"""
+SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."String", p."Strings"
+FROM "PrimitiveCollectionsEntity" AS p
+WHERE (
+ SELECT count(*)::int
+ FROM (VALUES (2::int), (999)) AS v("Value")
+ WHERE v."Value" > p."Id") = 1
+""");
+ }
+
+ public override async Task Inline_collection_Count_with_three_values(bool async)
+ {
+ await base.Inline_collection_Count_with_three_values(async);
+
+ AssertSql(
+"""
+SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."String", p."Strings"
+FROM "PrimitiveCollectionsEntity" AS p
+WHERE (
+ SELECT count(*)::int
+ FROM (VALUES (2::int), (999), (1000)) AS v("Value")
+ WHERE v."Value" > p."Id") = 2
+""");
+ }
+
+ public override Task Inline_collection_Contains_with_zero_values(bool async)
+ => AssertTranslationFailedWithDetails(
+ () => base.Inline_collection_Contains_with_zero_values(async),
+ RelationalStrings.EmptyCollectionNotSupportedAsInlineQueryRoot);
+
+ public override async Task Inline_collection_Contains_with_one_value(bool async)
+ {
+ await base.Inline_collection_Contains_with_one_value(async);
+
+ AssertSql(
+"""
+SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."String", p."Strings"
+FROM "PrimitiveCollectionsEntity" AS p
+WHERE p."Id" = 2
+""");
+ }
+
+ public override async Task Inline_collection_Contains_with_two_values(bool async)
+ {
+ await base.Inline_collection_Contains_with_two_values(async);
+
+ AssertSql(
+"""
+SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."String", p."Strings"
+FROM "PrimitiveCollectionsEntity" AS p
+WHERE p."Id" IN (2, 999)
+""");
+ }
+
+ public override async Task Inline_collection_Contains_with_three_values(bool async)
+ {
+ await base.Inline_collection_Contains_with_three_values(async);
+
+ AssertSql(
+"""
+SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."String", p."Strings"
+FROM "PrimitiveCollectionsEntity" AS p
+WHERE p."Id" IN (2, 999, 1000)
+""");
+ }
+
+ public override async Task Inline_collection_Contains_with_all_parameters(bool async)
+ {
+ await base.Inline_collection_Contains_with_all_parameters(async);
+
+ // See #30732 for making this better
+
+ AssertSql(
+"""
+@__p_0={ '2', '999' } (DbType = Object)
+
+SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."String", p."Strings"
+FROM "PrimitiveCollectionsEntity" AS p
+WHERE p."Id" = ANY (@__p_0)
+""");
+ }
+
+ public override async Task Inline_collection_Contains_with_parameter_and_column_based_expression(bool async)
+ {
+ var i = 2;
+
+ await AssertQuery(
+ async,
+ ss => ss.Set().Where(c => new[] { i, c.Int }.Contains(c.Id)),
+ entryCount: 1);
+
+ AssertSql(
+"""
+@__i_0='2'
+
+SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."String", p."Strings"
+FROM "PrimitiveCollectionsEntity" AS p
+WHERE p."Id" = ANY (ARRAY[@__i_0,p."Int"]::integer[])
+""");
+ }
+
+ public override async Task Parameter_collection_Count(bool async)
+ {
+ await base.Parameter_collection_Count(async);
+
+ AssertSql(
+"""
+@__ids_0={ '2', '999' } (DbType = Object)
+
+SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."String", p."Strings"
+FROM "PrimitiveCollectionsEntity" AS p
+WHERE (
+ SELECT count(*)::int
+ FROM unnest(@__ids_0) AS i(value)
+ WHERE i.value > p."Id") = 1
+""");
+ }
+
+ public override async Task Parameter_collection_of_ints_Contains(bool async)
+ {
+ await base.Parameter_collection_of_ints_Contains(async);
+
+ AssertSql(
+"""
+@__ints_0={ '10', '999' } (DbType = Object)
+
+SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."String", p."Strings"
+FROM "PrimitiveCollectionsEntity" AS p
+WHERE p."Int" = ANY (@__ints_0)
+""");
+ }
+
+ public override async Task Parameter_collection_of_nullable_ints_Contains(bool async)
+ {
+ await base.Parameter_collection_of_nullable_ints_Contains(async);
+
+ AssertSql(
+"""
+@__nullableInts_0={ '10', '999' } (DbType = Object)
+
+SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."String", p."Strings"
+FROM "PrimitiveCollectionsEntity" AS p
+WHERE p."NullableInt" = ANY (@__nullableInts_0) OR (p."NullableInt" IS NULL AND array_position(@__nullableInts_0, NULL) IS NOT NULL)
+""");
+ }
+
+ public override async Task Parameter_collection_of_nullable_ints_Contains_null(bool async)
+ {
+ await base.Parameter_collection_of_nullable_ints_Contains_null(async);
+
+ AssertSql(
+"""
+@__nullableInts_0={ NULL, '999' } (DbType = Object)
+
+SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."String", p."Strings"
+FROM "PrimitiveCollectionsEntity" AS p
+WHERE p."NullableInt" = ANY (@__nullableInts_0) OR (p."NullableInt" IS NULL AND array_position(@__nullableInts_0, NULL) IS NOT NULL)
+""");
+ }
+
+ public override async Task Parameter_collection_of_strings_Contains(bool async)
+ {
+ await base.Parameter_collection_of_strings_Contains(async);
+
+ AssertSql(
+"""
+@__strings_0={ '10', '999' } (DbType = Object)
+
+SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."String", p."Strings"
+FROM "PrimitiveCollectionsEntity" AS p
+WHERE p."String" = ANY (@__strings_0) OR (p."String" IS NULL AND array_position(@__strings_0, NULL) IS NOT NULL)
+""");
+ }
+
+ public override async Task Parameter_collection_of_DateTimes_Contains(bool async)
+ {
+ await base.Parameter_collection_of_DateTimes_Contains(async);
+
+ AssertSql(
+"""
+@__dateTimes_0={ '2020-01-10T12:30:00.0000000Z', '9999-01-01T00:00:00.0000000Z' } (DbType = Object)
+
+SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."String", p."Strings"
+FROM "PrimitiveCollectionsEntity" AS p
+WHERE p."DateTime" = ANY (@__dateTimes_0)
+""");
+ }
+
+ public override async Task Parameter_collection_of_bools_Contains(bool async)
+ {
+ await base.Parameter_collection_of_bools_Contains(async);
+
+ AssertSql(
+"""
+@__bools_0={ 'True' } (DbType = Object)
+
+SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."String", p."Strings"
+FROM "PrimitiveCollectionsEntity" AS p
+WHERE p."Bool" = ANY (@__bools_0)
+""");
+ }
+
+ public override async Task Parameter_collection_of_enums_Contains(bool async)
+ {
+ await base.Parameter_collection_of_enums_Contains(async);
+
+ AssertSql(
+"""
+@__enums_0={ '0', '3' } (DbType = Object)
+
+SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."String", p."Strings"
+FROM "PrimitiveCollectionsEntity" AS p
+WHERE p."Enum" = ANY (@__enums_0)
+""");
+ }
+
+ public override async Task Parameter_collection_null_Contains(bool async)
+ {
+ await base.Parameter_collection_null_Contains(async);
+
+ AssertSql(
+"""
+SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."String", p."Strings"
+FROM "PrimitiveCollectionsEntity" AS p
+WHERE p."Int" = ANY (NULL)
+""");
+ }
+
+ public override async Task Column_collection_of_ints_Contains(bool async)
+ {
+ await base.Column_collection_of_ints_Contains(async);
+
+ AssertSql(
+"""
+SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."String", p."Strings"
+FROM "PrimitiveCollectionsEntity" AS p
+WHERE p."Ints" @> ARRAY[10]::integer[]
+""");
+ }
+
+ public override async Task Column_collection_of_nullable_ints_Contains(bool async)
+ {
+ await base.Column_collection_of_nullable_ints_Contains(async);
+
+ AssertSql(
+"""
+SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."String", p."Strings"
+FROM "PrimitiveCollectionsEntity" AS p
+WHERE p."NullableInts" @> ARRAY[10]::integer[]
+""");
+ }
+
+ public override async Task Column_collection_of_nullable_ints_Contains_null(bool async)
+ {
+ await base.Column_collection_of_nullable_ints_Contains_null(async);
+
+ AssertSql(
+"""
+SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."String", p."Strings"
+FROM "PrimitiveCollectionsEntity" AS p
+WHERE array_position(p."NullableInts", NULL) IS NOT NULL
+""");
+ }
+
+ public override async Task Column_collection_of_bools_Contains(bool async)
+ {
+ await base.Column_collection_of_bools_Contains(async);
+
+ AssertSql(
+"""
+SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."String", p."Strings"
+FROM "PrimitiveCollectionsEntity" AS p
+WHERE p."Bools" @> ARRAY[TRUE]::boolean[]
+""");
+ }
+
+ public override async Task Column_collection_Count_method(bool async)
+ {
+ await base.Column_collection_Count_method(async);
+
+ AssertSql(
+"""
+SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."String", p."Strings"
+FROM "PrimitiveCollectionsEntity" AS p
+WHERE cardinality(p."Ints") = 2
+""");
+ }
+
+ public override async Task Column_collection_Length(bool async)
+ {
+ await base.Column_collection_Length(async);
+
+ AssertSql(
+"""
+SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."String", p."Strings"
+FROM "PrimitiveCollectionsEntity" AS p
+WHERE cardinality(p."Ints") = 2
+""");
+ }
+
+ public override async Task Column_collection_index_int(bool async)
+ {
+ await base.Column_collection_index_int(async);
+
+ AssertSql(
+"""
+SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."String", p."Strings"
+FROM "PrimitiveCollectionsEntity" AS p
+WHERE p."Ints"[2] = 10
+""");
+ }
+
+ public override async Task Column_collection_index_string(bool async)
+ {
+ await base.Column_collection_index_string(async);
+
+ AssertSql(
+"""
+SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."String", p."Strings"
+FROM "PrimitiveCollectionsEntity" AS p
+WHERE p."Strings"[2] = '10'
+""");
+ }
+
+ public override async Task Column_collection_index_datetime(bool async)
+ {
+ await base.Column_collection_index_datetime(async);
+
+ AssertSql(
+"""
+SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."String", p."Strings"
+FROM "PrimitiveCollectionsEntity" AS p
+WHERE p."DateTimes"[2] = TIMESTAMPTZ '2020-01-10 12:30:00Z'
+""");
+ }
+
+ public override async Task Column_collection_index_beyond_end(bool async)
+ {
+ await base.Column_collection_index_beyond_end(async);
+
+ AssertSql(
+"""
+SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."String", p."Strings"
+FROM "PrimitiveCollectionsEntity" AS p
+WHERE p."Ints"[1000] = 10
+""");
+ }
+
+ public override async Task Inline_collection_index_Column(bool async)
+ {
+ await base.Inline_collection_index_Column(async);
+
+ AssertSql(
+"""
+SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."String", p."Strings"
+FROM "PrimitiveCollectionsEntity" AS p
+WHERE (
+ SELECT v."Value"
+ FROM (VALUES (0, 1::int), (1, 2), (2, 3)) AS v(_ord, "Value")
+ ORDER BY v._ord NULLS FIRST
+ LIMIT 1 OFFSET p."Int") = 1
+""");
+ }
+
+ public override async Task Parameter_collection_index_Column_equal_Column(bool async)
+ {
+ await base.Parameter_collection_index_Column_equal_Column(async);
+
+ AssertSql(
+"""
+@__ints_0={ '0', '2', '3' } (DbType = Object)
+
+SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."String", p."Strings"
+FROM "PrimitiveCollectionsEntity" AS p
+WHERE @__ints_0[p."Int" + 1] = p."Int"
+""");
+ }
+
+ public override async Task Parameter_collection_index_Column_equal_constant(bool async)
+ {
+ await base.Parameter_collection_index_Column_equal_constant(async);
+
+ AssertSql(
+"""
+@__ints_0={ '1', '2', '3' } (DbType = Object)
+
+SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."String", p."Strings"
+FROM "PrimitiveCollectionsEntity" AS p
+WHERE @__ints_0[p."Int" + 1] = 1
+""");
+ }
+
+ public override async Task Column_collection_ElementAt(bool async)
+ {
+ await base.Column_collection_ElementAt(async);
+
+ AssertSql(
+"""
+SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."String", p."Strings"
+FROM "PrimitiveCollectionsEntity" AS p
+WHERE p."Ints"[2] = 10
+""");
+ }
+
+ public override async Task Column_collection_Skip(bool async)
+ {
+ await base.Column_collection_Skip(async);
+
+ AssertSql(
+"""
+SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."String", p."Strings"
+FROM "PrimitiveCollectionsEntity" AS p
+WHERE cardinality(p."Ints"[2:]) = 2
+""");
+ }
+
+ public override async Task Column_collection_Take(bool async)
+ {
+ await base.Column_collection_Take(async);
+
+ AssertSql(
+"""
+SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."String", p."Strings"
+FROM "PrimitiveCollectionsEntity" AS p
+WHERE EXISTS (
+ SELECT 1
+ FROM unnest(p."Ints"[:2]) AS i(value)
+ WHERE i.value = 11)
+""");
+ }
+
+ public override async Task Column_collection_Skip_Take(bool async)
+ {
+ await base.Column_collection_Skip_Take(async);
+
+ AssertSql(
+"""
+SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."String", p."Strings"
+FROM "PrimitiveCollectionsEntity" AS p
+WHERE EXISTS (
+ SELECT 1
+ FROM unnest(p."Ints"[2:3]) AS i(value)
+ WHERE i.value = 11)
+""");
+ }
+
+ public override async Task Column_collection_Any(bool async)
+ {
+ await base.Column_collection_Any(async);
+
+ AssertSql(
+"""
+SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."String", p."Strings"
+FROM "PrimitiveCollectionsEntity" AS p
+WHERE cardinality(p."Ints") > 0
+""");
+ }
+
+ public override async Task Column_collection_projection_from_top_level(bool async)
+ {
+ await base.Column_collection_projection_from_top_level(async);
+
+ AssertSql(
+"""
+SELECT p."Ints"
+FROM "PrimitiveCollectionsEntity" AS p
+ORDER BY p."Id" NULLS FIRST
+""");
+ }
+
+ public override async Task Column_collection_and_parameter_collection_Join(bool async)
+ {
+ await base.Column_collection_and_parameter_collection_Join(async);
+
+ AssertSql(
+"""
+@__ints_0={ '11', '111' } (DbType = Object)
+
+SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."String", p."Strings"
+FROM "PrimitiveCollectionsEntity" AS p
+WHERE (
+ SELECT count(*)::int
+ FROM unnest(p."Ints") AS i(value)
+ INNER JOIN unnest(@__ints_0) AS i0(value) ON i.value = i0.value) = 2
+""");
+ }
+
+ public override async Task Parameter_collection_Concat_column_collection(bool async)
+ {
+ await base.Parameter_collection_Concat_column_collection(async);
+
+ AssertSql(
+"""
+@__ints_0={ '11', '111' } (DbType = Object)
+
+SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."String", p."Strings"
+FROM "PrimitiveCollectionsEntity" AS p
+WHERE cardinality(@__ints_0 || p."Ints") = 2
+""");
+ }
+
+ public override async Task Column_collection_Union_parameter_collection(bool async)
+ {
+ await base.Column_collection_Union_parameter_collection(async);
+
+ AssertSql(
+"""
+@__ints_0={ '11', '111' } (DbType = Object)
+
+SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."String", p."Strings"
+FROM "PrimitiveCollectionsEntity" AS p
+WHERE (
+ SELECT count(*)::int
+ FROM (
+ SELECT i.value
+ FROM unnest(p."Ints") AS i(value)
+ UNION
+ SELECT i0.value
+ FROM unnest(@__ints_0) AS i0(value)
+ ) AS t) = 2
+""");
+ }
+
+ public override async Task Column_collection_Intersect_inline_collection(bool async)
+ {
+ await base.Column_collection_Intersect_inline_collection(async);
+
+ AssertSql(
+"""
+SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."String", p."Strings"
+FROM "PrimitiveCollectionsEntity" AS p
+WHERE (
+ SELECT count(*)::int
+ FROM (
+ SELECT i.value
+ FROM unnest(p."Ints") AS i(value)
+ INTERSECT
+ VALUES (11::int), (111)
+ ) AS t) = 2
+""");
+ }
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual async Task Column_collection_Intersect_Parameter_collection_Any(bool async)
+ {
+ var ints = new[] { 11, 12 };
+
+ await AssertQuery(
+ async,
+ ss => ss.Set().Where(c => c.Ints.Intersect(ints).Any()),
+ entryCount: 1);
+
+ AssertSql(
+"""
+@__ints_0={ '11', '12' } (DbType = Object)
+
+SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."String", p."Strings"
+FROM "PrimitiveCollectionsEntity" AS p
+WHERE p."Ints" && @__ints_0
+""");
+ }
+
+ public override async Task Inline_collection_Except_column_collection(bool async)
+ {
+ await base.Inline_collection_Except_column_collection(async);
+
+ AssertSql(
+"""
+SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."String", p."Strings"
+FROM "PrimitiveCollectionsEntity" AS p
+WHERE (
+ SELECT count(*)::int
+ FROM (
+ SELECT v."Value"
+ FROM (VALUES (11::int), (111)) AS v("Value")
+ EXCEPT
+ SELECT i.value AS "Value"
+ FROM unnest(p."Ints") AS i(value)
+ ) AS t
+ WHERE t."Value" % 2 = 1) = 2
+""");
+ }
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual async Task Parameter_collection_Concat_Column_collection_Concat_parameter(bool async)
+ {
+ var ints1 = new[] { 11 };
+ var ints2 = new[] { 12 };
+
+ await AssertQuery(
+ async,
+ ss => ss.Set().Where(c => ints1.Concat(c.Ints).Concat(ints2).Count() == 4),
+ entryCount: 1);
+
+ AssertSql(
+"""
+@__ints1_0={ '11' } (DbType = Object)
+@__ints2_1={ '12' } (DbType = Object)
+
+SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."String", p."Strings"
+FROM "PrimitiveCollectionsEntity" AS p
+WHERE cardinality(@__ints1_0 || p."Ints" || @__ints2_1) = 4
+""");
+ }
+
+ public override async Task Column_collection_Concat_parameter_collection_equality_inline_collection_not_supported(bool async)
+ {
+ await base.Column_collection_Concat_parameter_collection_equality_inline_collection_not_supported(async);
+
+ AssertSql();
+ }
+
+ public override async Task Column_collection_equality_parameter_collection(bool async)
+ {
+ await base.Column_collection_equality_parameter_collection(async);
+
+ AssertSql(
+"""
+@__ints_0={ '1', '10' } (DbType = Object)
+
+SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."String", p."Strings"
+FROM "PrimitiveCollectionsEntity" AS p
+WHERE p."Ints" = @__ints_0
+""");
+ }
+
+ public override async Task Column_collection_equality_inline_collection(bool async)
+ {
+ await base.Column_collection_equality_inline_collection(async);
+
+ AssertSql(
+"""
+SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."String", p."Strings"
+FROM "PrimitiveCollectionsEntity" AS p
+WHERE p."Ints" = ARRAY[1,10]::integer[]
+""");
+ }
+
+ public override async Task Parameter_collection_in_subquery_Union_column_collection_as_compiled_query(bool async)
+ {
+ await base.Parameter_collection_in_subquery_Union_column_collection_as_compiled_query(async);
+
+ AssertSql(
+ """
+@__ints={ '10', '111' } (DbType = Object)
+
+SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."String", p."Strings"
+FROM "PrimitiveCollectionsEntity" AS p
+WHERE (
+ SELECT count(*)::int
+ FROM (
+ SELECT i.value
+ FROM unnest(@__ints[2:]) AS i(value)
+ UNION
+ SELECT i0.value
+ FROM unnest(p."Ints") AS i0(value)
+ ) AS t) = 3
+""");
+ }
+
+ public override void Parameter_collection_in_subquery_and_Convert_as_compiled_query()
+ {
+ base.Parameter_collection_in_subquery_and_Convert_as_compiled_query();
+
+ AssertSql();
+ }
+
+ public override async Task Parameter_collection_in_subquery_Count_as_compiled_query(bool async)
+ {
+ await base.Parameter_collection_in_subquery_Count_as_compiled_query(async);
+
+ AssertSql(
+"""
+@__ints={ '10', '111' } (DbType = Object)
+
+SELECT count(*)::int
+FROM "PrimitiveCollectionsEntity" AS p
+WHERE (
+ SELECT count(*)::int
+ FROM unnest(@__ints[2:]) AS i(value)
+ WHERE i.value > p."Id") = 1
+""");
+ }
+
+ public override async Task Column_collection_in_subquery_Union_parameter_collection(bool async)
+ {
+ await base.Column_collection_in_subquery_Union_parameter_collection(async);
+
+ AssertSql(
+"""
+@__ints_0={ '10', '111' } (DbType = Object)
+
+SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."String", p."Strings"
+FROM "PrimitiveCollectionsEntity" AS p
+WHERE (
+ SELECT count(*)::int
+ FROM (
+ SELECT i.value
+ FROM unnest(p."Ints"[2:]) AS i(value)
+ UNION
+ SELECT i0.value
+ FROM unnest(@__ints_0) AS i0(value)
+ ) AS t) = 3
+""");
+ }
+
+ [ConditionalFact]
+ public virtual void Check_all_tests_overridden()
+ => TestHelpers.AssertAllMethodsOverridden(GetType());
+
+ private void AssertSql(params string[] expected)
+ => Fixture.TestSqlLoggerFactory.AssertBaseline(expected);
+
+ private PrimitiveCollectionsContext CreateContext()
+ => Fixture.CreateContext();
+
+ public class PrimitiveCollectionsQueryNpgsqlFixture : PrimitiveCollectionsQueryFixtureBase
+ {
+ public TestSqlLoggerFactory TestSqlLoggerFactory
+ => (TestSqlLoggerFactory)ListLoggerFactory;
+
+ protected override ITestStoreFactory TestStoreFactory
+ => NpgsqlTestStoreFactory.Instance;
+ }
+}
diff --git a/test/EFCore.PG.FunctionalTests/Query/SpatialQueryNpgsqlGeometryTest.cs b/test/EFCore.PG.FunctionalTests/Query/SpatialQueryNpgsqlGeometryTest.cs
index ec773270d..31f3fbe61 100644
--- a/test/EFCore.PG.FunctionalTests/Query/SpatialQueryNpgsqlGeometryTest.cs
+++ b/test/EFCore.PG.FunctionalTests/Query/SpatialQueryNpgsqlGeometryTest.cs
@@ -735,6 +735,34 @@ public override async Task Z(bool async)
""");
}
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual async Task MultiString_Any(bool async)
+ {
+ var lineString = Fixture.GeometryFactory.CreateLineString(new[] { new Coordinate(1, 0), new Coordinate(1, 1) });
+
+ // Note the subtle difference between Contains any Any here: Contains resolves to Geometry.Contains, which checks whether a geometry
+ // is contained in another; this is different from .NET collection/enumerable Contains, which checks whether an item is in a
+ // collection.
+ await AssertQuery(
+ async,
+ ss => ss.Set().Where(e => e.MultiLineString.Any(ls => ls == lineString)),
+ ss => ss.Set().Where(e => e.MultiLineString != null && e.MultiLineString.Any(ls => GeometryComparer.Instance.Equals(ls, lineString))),
+ elementSorter: e => e.Id);
+
+ AssertSql(
+"""
+@__lineString_0='LINESTRING (1 0, 1 1)' (DbType = Object)
+
+SELECT m."Id", m."MultiLineString"
+FROM "MultiLineStringEntity" AS m
+WHERE EXISTS (
+ SELECT 1
+ FROM ST_Dump(m."MultiLineString") AS m0
+ WHERE m0.geom = @__lineString_0)
+""");
+ }
+
private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected);
public class SpatialQueryNpgsqlGeometryFixture : SpatialQueryNpgsqlFixture
diff --git a/test/EFCore.PG.Tests/Storage/NpgsqlTypeMappingSourceTest.cs b/test/EFCore.PG.Tests/Storage/NpgsqlTypeMappingSourceTest.cs
index a8af10d90..3406b7d56 100644
--- a/test/EFCore.PG.Tests/Storage/NpgsqlTypeMappingSourceTest.cs
+++ b/test/EFCore.PG.Tests/Storage/NpgsqlTypeMappingSourceTest.cs
@@ -68,7 +68,7 @@ public void Varchar32_Array()
Assert.Equal("varchar(32)[]", arrayMapping.StoreType);
Assert.Null(arrayMapping.Size);
- var elementMapping = arrayMapping.ElementMapping;
+ var elementMapping = arrayMapping.ElementTypeMapping;
Assert.Same(typeof(string), elementMapping.ClrType);
Assert.Equal("varchar(32)", elementMapping.StoreType);
Assert.Equal(32, elementMapping.Size);
@@ -90,7 +90,7 @@ public void Timestamp_without_time_zone_Array_5()
Assert.Same(typeof(DateTime[]), arrayMapping.ClrType);
Assert.Equal("timestamp(5) without time zone[]", arrayMapping.StoreType);
- var elementMapping = arrayMapping.ElementMapping;
+ var elementMapping = arrayMapping.ElementTypeMapping;
Assert.Same(typeof(DateTime), elementMapping.ClrType);
Assert.Equal("timestamp(5) without time zone", elementMapping.StoreType);
}
@@ -210,7 +210,7 @@ private void Array_over_type_mapping_with_value_converter(CoreTypeMapping mappin
Assert.Equal("ltree[]", arrayMapping.StoreType);
Assert.Same(expectedType, arrayMapping.ClrType);
- var elementMapping = arrayMapping.ElementMapping;
+ var elementMapping = arrayMapping.ElementTypeMapping;
Assert.NotNull(elementMapping);
Assert.Equal("ltree", elementMapping.StoreType);
Assert.Same(typeof(LTree), elementMapping.ClrType);