diff --git a/src/EFCore.PG/Extensions/NpgsqlArrayExtensions.cs b/src/EFCore.PG/Extensions/NpgsqlArrayExtensions.cs
new file mode 100644
index 000000000..c07a4663f
--- /dev/null
+++ b/src/EFCore.PG/Extensions/NpgsqlArrayExtensions.cs
@@ -0,0 +1,666 @@
+using System;
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+using JetBrains.Annotations;
+
+// ReSharper disable UnusedParameter.Global
+// ReSharper disable once CheckNamespace
+namespace Microsoft.EntityFrameworkCore
+{
+ ///
+ /// Provides extension methods for supporting PostgreSQL translation.
+ ///
+ public static class NpgsqlArrayExtensions
+ {
+ #region @>
+
+ ///
+ /// Tests whether the first collection contains the second collection.
+ ///
+ /// The DbFunctions instance.
+ /// The first collection.
+ /// The second collection.
+ /// The type of the elements in the array.
+ ///
+ /// True if the first collection contains the second collection; otherwise, false.
+ ///
+ ///
+ /// This method is only intended for use via SQL translation as part of an EF Core LINQ query.
+ ///
+ public static bool Contains(
+ [CanBeNull] this DbFunctions _,
+ [CanBeNull] [ItemCanBeNull] T[] first,
+ [CanBeNull] [ItemCanBeNull] T[] second)
+ => throw ClientEvaluationNotSupportedException();
+
+ ///
+ /// Tests whether the first collection contains the second collection.
+ ///
+ /// The DbFunctions instance.
+ /// The first collection.
+ /// The second collection.
+ /// The type of the elements in the array.
+ ///
+ /// True if the first collection contains the second collection; otherwise, false.
+ ///
+ ///
+ /// This method is only intended for use via SQL translation as part of an EF Core LINQ query.
+ ///
+ public static bool Contains(
+ [CanBeNull] this DbFunctions _,
+ [CanBeNull] [ItemCanBeNull] T[] first,
+ [CanBeNull] [ItemCanBeNull] List second)
+ => throw ClientEvaluationNotSupportedException();
+
+ ///
+ /// Tests whether the first collection contains the second collection.
+ ///
+ /// The DbFunctions instance.
+ /// The first collection.
+ /// The second collection.
+ /// The type of the elements in the array.
+ ///
+ /// True if the first collection contains the second collection; otherwise, false.
+ ///
+ ///
+ /// This method is only intended for use via SQL translation as part of an EF Core LINQ query.
+ ///
+ public static bool Contains(
+ [CanBeNull] this DbFunctions _,
+ [CanBeNull] [ItemCanBeNull] List first,
+ [CanBeNull] [ItemCanBeNull] T[] second)
+ => throw ClientEvaluationNotSupportedException();
+
+ ///
+ /// Tests whether the first collection contains the second collection.
+ ///
+ /// The DbFunctions instance.
+ /// The first collection.
+ /// The second collection.
+ /// The type of the elements in the array.
+ ///
+ /// True if the first collection contains the second collection; otherwise, false.
+ ///
+ ///
+ /// This method is only intended for use via SQL translation as part of an EF Core LINQ query.
+ ///
+ public static bool Contains(
+ [CanBeNull] this DbFunctions _,
+ [CanBeNull] [ItemCanBeNull] List first,
+ [CanBeNull] [ItemCanBeNull] List second)
+ => throw ClientEvaluationNotSupportedException();
+
+ #endregion
+
+ #region <@
+
+ ///
+ /// Tests whether the first collection is contained by the second collection.
+ ///
+ /// The DbFunctions instance.
+ /// The first collection.
+ /// The second collection.
+ /// The type of the elements in the array.
+ ///
+ /// True if the first collection is contained by the second collection; otherwise, false.
+ ///
+ ///
+ /// This method is only intended for use via SQL translation as part of an EF Core LINQ query.
+ ///
+ public static bool ContainedBy(
+ [CanBeNull] this DbFunctions _,
+ [CanBeNull] [ItemCanBeNull] T[] first,
+ [CanBeNull] [ItemCanBeNull] T[] second)
+ => throw ClientEvaluationNotSupportedException();
+
+ ///
+ /// Tests whether the first collection is contained by the second collection.
+ ///
+ /// The DbFunctions instance.
+ /// The first collection.
+ /// The second collection.
+ /// The type of the elements in the array.
+ ///
+ /// True if the first collection is contained by the second collection; otherwise, false.
+ ///
+ ///
+ /// This method is only intended for use via SQL translation as part of an EF Core LINQ query.
+ ///
+ public static bool ContainedBy(
+ [CanBeNull] this DbFunctions _,
+ [CanBeNull] [ItemCanBeNull] T[] first,
+ [CanBeNull] [ItemCanBeNull] List second)
+ => throw ClientEvaluationNotSupportedException();
+
+ ///
+ /// Tests whether the first collection is contained by the second collection.
+ ///
+ /// The DbFunctions instance.
+ /// The first collection.
+ /// The second collection.
+ /// The type of the elements in the array.
+ ///
+ /// True if the first collection is contained by the second collection; otherwise, false.
+ ///
+ ///
+ /// This method is only intended for use via SQL translation as part of an EF Core LINQ query.
+ ///
+ public static bool ContainedBy(
+ [CanBeNull] this DbFunctions _,
+ [CanBeNull] [ItemCanBeNull] List first,
+ [CanBeNull] [ItemCanBeNull] T[] second)
+ => throw ClientEvaluationNotSupportedException();
+
+ ///
+ /// Tests whether the first collection is contained by the second collection.
+ ///
+ /// The DbFunctions instance.
+ /// The first collection.
+ /// The second collection.
+ /// The type of the elements in the array.
+ ///
+ /// True if the first collection is contained by the second collection; otherwise, false.
+ ///
+ ///
+ /// This method is only intended for use via SQL translation as part of an EF Core LINQ query.
+ ///
+ public static bool ContainedBy(
+ [CanBeNull] this DbFunctions _,
+ [CanBeNull] [ItemCanBeNull] List first,
+ [CanBeNull] [ItemCanBeNull] List second)
+ => throw ClientEvaluationNotSupportedException();
+
+ #endregion
+
+ #region &&
+
+ ///
+ /// Tests whether the first collection overlaps the second collection.
+ ///
+ /// The DbFunctions instance.
+ /// The first collection.
+ /// The second collection.
+ /// The type of the elements in the array.
+ ///
+ /// True if the first collection overlaps the second collection; otherwise, false.
+ ///
+ ///
+ /// This method is only intended for use via SQL translation as part of an EF Core LINQ query.
+ ///
+ public static bool Overlaps(
+ [CanBeNull] this DbFunctions _,
+ [CanBeNull] [ItemCanBeNull] T[] first,
+ [CanBeNull] [ItemCanBeNull] T[] second)
+ => throw ClientEvaluationNotSupportedException();
+
+ ///
+ /// Tests whether the first collection overlaps the second collection.
+ ///
+ /// The DbFunctions instance.
+ /// The first collection.
+ /// The second collection.
+ /// The type of the elements in the array.
+ ///
+ /// True if the first collection overlaps the second collection; otherwise, false.
+ ///
+ ///
+ /// This method is only intended for use via SQL translation as part of an EF Core LINQ query.
+ ///
+ public static bool Overlaps(
+ [CanBeNull] this DbFunctions _,
+ [CanBeNull] [ItemCanBeNull] T[] first,
+ [CanBeNull] [ItemCanBeNull] List second)
+ => throw ClientEvaluationNotSupportedException();
+
+ ///
+ /// Tests whether the first collection overlaps the second collection.
+ ///
+ /// The DbFunctions instance.
+ /// The first collection.
+ /// The second collection.
+ /// The type of the elements in the array.
+ ///
+ /// True if the first collection overlaps the second collection; otherwise, false.
+ ///
+ ///
+ /// This method is only intended for use via SQL translation as part of an EF Core LINQ query.
+ ///
+ public static bool Overlaps(
+ [CanBeNull] this DbFunctions _,
+ [CanBeNull] [ItemCanBeNull] List first,
+ [CanBeNull] [ItemCanBeNull] T[] second)
+ => throw ClientEvaluationNotSupportedException();
+
+ ///
+ /// Tests whether the first collection overlaps with the second collection.
+ ///
+ /// The DbFunctions instance.
+ /// The first collection.
+ /// The second collection.
+ /// The type of the elements in the array.
+ ///
+ /// True if the first collection overlaps the second collection; otherwise, false.
+ ///
+ ///
+ /// This method is only intended for use via SQL translation as part of an EF Core LINQ query.
+ ///
+ public static bool Overlaps(
+ [CanBeNull] this DbFunctions _,
+ [CanBeNull] [ItemCanBeNull] List first,
+ [CanBeNull] [ItemCanBeNull] List second)
+ => throw ClientEvaluationNotSupportedException();
+
+ #endregion
+
+ #region array_fill
+
+ ///
+ /// Initializes an array with the given
+ ///
+ /// The DbFunctions instance.
+ /// The value with which to initialize each element.
+ /// The dimensions of the array.
+ /// The type of the elements in the array.
+ ///
+ /// An array initialized with the given and .
+ ///
+ ///
+ /// This method is only intended for use via SQL translation as part of an EF Core LINQ query.
+ ///
+ [NotNull]
+ public static T[] ArrayFill(
+ [CanBeNull] this DbFunctions _,
+ [CanBeNull] T value,
+ [CanBeNull] [ItemNotNull] T[] dimensions)
+ => throw ClientEvaluationNotSupportedException();
+
+ ///
+ /// Initializes an array with the given
+ ///
+ /// The DbFunctions instance.
+ /// The value with which to initialize each element.
+ /// The dimensions of the array.
+ /// The type of the elements in the array.
+ ///
+ /// An array initialized with the given and .
+ ///
+ ///
+ /// This method is only intended for use via SQL translation as part of an EF Core LINQ query.
+ ///
+ [NotNull]
+ public static List ListFill(
+ [CanBeNull] this DbFunctions _,
+ [CanBeNull] T value,
+ [CanBeNull] [ItemNotNull] T[] dimensions)
+ => throw ClientEvaluationNotSupportedException();
+
+ #endregion
+
+ #region array_dims
+
+ ///
+ /// Returns a text representation of the array dimensions.
+ ///
+ /// The DbFunctions instance.
+ /// The array.
+ /// The type of the elements in the array.
+ ///
+ /// A text representation of the array dimensions.
+ ///
+ ///
+ /// This method is only intended for use via SQL translation as part of an EF Core LINQ query.
+ ///
+ [NotNull]
+ public static string ArrayDimensions(
+ [CanBeNull] this DbFunctions _,
+ [CanBeNull] T[] array)
+ => throw ClientEvaluationNotSupportedException();
+
+ ///
+ /// Returns a text representation of the list dimensions.
+ ///
+ /// The DbFunctions instance.
+ /// The list.
+ /// The type of the elements in the list.
+ ///
+ /// A text representation of the list dimensions.
+ ///
+ ///
+ /// This method is only intended for use via SQL translation as part of an EF Core LINQ query.
+ ///
+ [NotNull]
+ public static string ArrayDimensions(
+ [CanBeNull] this DbFunctions _,
+ [CanBeNull] List list)
+ => throw ClientEvaluationNotSupportedException();
+
+ #endregion
+
+ #region array_positions
+
+ ///
+ /// Finds the positions at which the appears in the .
+ ///
+ /// The DbFunctions instance.
+ /// The array to search.
+ /// The value to locate.
+ /// The type of the elements in the array.
+ ///
+ /// The positions at which the appears in the .
+ ///
+ ///
+ /// This method is only intended for use via SQL translation as part of an EF Core LINQ query.
+ ///
+ [NotNull]
+ public static int[] ArrayPositions(
+ [CanBeNull] this DbFunctions _,
+ [CanBeNull] [ItemCanBeNull] T[] array,
+ [CanBeNull] T value)
+ => throw ClientEvaluationNotSupportedException();
+
+ ///
+ /// Finds the positions at which the appears in the .
+ ///
+ /// The DbFunctions instance.
+ /// The list to search.
+ /// The value to locate.
+ /// The type of the elements in the list.
+ ///
+ /// The positions at which the appears in the .
+ ///
+ ///
+ /// This method is only intended for use via SQL translation as part of an EF Core LINQ query.
+ ///
+ [NotNull]
+ public static int[] ArrayPositions(
+ [CanBeNull] this DbFunctions _,
+ [CanBeNull] [ItemCanBeNull] List list,
+ [CanBeNull] T value)
+ => throw ClientEvaluationNotSupportedException();
+
+ #endregion
+
+ #region array_remove
+
+ ///
+ /// Removes all elements equal to the given value from the array.
+ ///
+ /// The DbFunctions instance.
+ /// The array to search.
+ /// The value to locate.
+ /// The type of the elements in the array.
+ ///
+ /// An array where all elements are not equal to the given value.
+ ///
+ ///
+ /// This method is only intended for use via SQL translation as part of an EF Core LINQ query.
+ ///
+ [CanBeNull]
+ public static T[] ArrayRemove(
+ [CanBeNull] this DbFunctions _,
+ [CanBeNull] [ItemCanBeNull] T[] array,
+ [CanBeNull] T value)
+ => throw ClientEvaluationNotSupportedException();
+
+ ///
+ /// Removes all elements equal to the given value from the .
+ ///
+ /// The DbFunctions instance.
+ /// The list to search.
+ /// The value to locate.
+ /// The type of the elements in the list.
+ ///
+ /// A where all elements are not equal to the given value.
+ ///
+ ///
+ /// This method is only intended for use via SQL translation as part of an EF Core LINQ query.
+ ///
+ [CanBeNull]
+ public static List ArrayRemove(
+ [CanBeNull] this DbFunctions _,
+ [CanBeNull] [ItemCanBeNull] List list,
+ [CanBeNull] T value)
+ => throw ClientEvaluationNotSupportedException();
+
+ #endregion
+
+ #region array_replace
+
+ ///
+ /// Replaces each element equal to the given value with a new value.
+ ///
+ /// The DbFunctions instance.
+ /// The array to search.
+ /// The value to replace.
+ /// The new value.
+ /// The type of the elements in the array.
+ ///
+ /// An array where each element equal to the given value is replaced with a new value.
+ ///
+ ///
+ /// This method is only intended for use via SQL translation as part of an EF Core LINQ query.
+ ///
+ [CanBeNull]
+ public static T[] ArrayReplace(
+ [CanBeNull] this DbFunctions _,
+ [CanBeNull] [ItemCanBeNull] T[] array,
+ [CanBeNull] T current,
+ [CanBeNull] T replacement)
+ => throw ClientEvaluationNotSupportedException();
+
+ ///
+ /// Replaces each element equal to the given value with a new value.
+ ///
+ /// The DbFunctions instance.
+ /// The list to search.
+ /// The value to replace.
+ /// The new value.
+ /// The type of the elements in the list.
+ ///
+ /// An where each element equal to the given value is replaced with a new value.
+ ///
+ ///
+ /// This method is only intended for use via SQL translation as part of an EF Core LINQ query.
+ ///
+ [CanBeNull]
+ public static List ArrayReplace(
+ [CanBeNull] this DbFunctions _,
+ [CanBeNull] [ItemCanBeNull] List list,
+ [CanBeNull] T current,
+ [CanBeNull] T replacement)
+ => throw ClientEvaluationNotSupportedException();
+
+ #endregion
+
+ #region array_to_string
+
+ ///
+ /// Concatenates elements using the supplied delimiter.
+ ///
+ /// The DbFunctions instance.
+ /// The list to convert to a string in which to locate the value.
+ /// The value used to delimit the elements.
+ /// The type of the elements of .
+ ///
+ /// The string concatenation of the elements with the supplied delimiter.
+ ///
+ ///
+ /// This method is only intended for use via SQL translation as part of an EF Core LINQ query.
+ ///
+ [CanBeNull]
+ public static string ArrayToString(
+ [CanBeNull] this DbFunctions _,
+ [CanBeNull] [ItemCanBeNull] T[] array,
+ [CanBeNull] string delimiter)
+ => throw ClientEvaluationNotSupportedException();
+
+ ///
+ /// Concatenates elements using the supplied delimiter.
+ ///
+ /// The DbFunctions instance.
+ /// The list to convert to a string in which to locate the value.
+ /// The value used to delimit the elements.
+ /// The type of the elements of .
+ ///
+ /// The string concatenation of the elements with the supplied delimiter.
+ ///
+ ///
+ /// This method is only intended for use via SQL translation as part of an EF Core LINQ query.
+ ///
+ [CanBeNull]
+ public static string ArrayToString(
+ [CanBeNull] this DbFunctions _,
+ [CanBeNull] [ItemCanBeNull] List list,
+ [CanBeNull] string delimiter)
+ => throw ClientEvaluationNotSupportedException();
+
+ ///
+ /// Concatenates elements using the supplied delimiter and the string representation for null elements.
+ ///
+ /// The DbFunctions instance.
+ /// The list to convert to a string in which to locate the value.
+ /// The value used to delimit the elements.
+ /// The value used to represent a null value.
+ /// The type of the elements of .
+ ///
+ /// The string concatenation of the elements with the supplied delimiter and null string.
+ ///
+ ///
+ /// This method is only intended for use via SQL translation as part of an EF Core LINQ query.
+ ///
+ [CanBeNull]
+ public static string ArrayToString(
+ [CanBeNull] this DbFunctions _,
+ [CanBeNull] [ItemCanBeNull] T[] array,
+ [CanBeNull] string delimiter,
+ [CanBeNull] string nullString)
+ => throw ClientEvaluationNotSupportedException();
+
+ ///
+ /// Concatenates elements using the supplied delimiter and the string representation for null elements.
+ ///
+ /// The DbFunctions instance.
+ /// The list to convert to a string in which to locate the value.
+ /// The value used to delimit the elements.
+ /// The value used to represent a null value.
+ /// The type of the elements of .
+ ///
+ /// The string concatenation of the elements with the supplied delimiter and null string.
+ ///
+ ///
+ /// This method is only intended for use via SQL translation as part of an EF Core LINQ query.
+ ///
+ [CanBeNull]
+ public static string ArrayToString(
+ [CanBeNull] this DbFunctions _,
+ [CanBeNull] [ItemCanBeNull] List list,
+ [CanBeNull] string delimiter,
+ [CanBeNull] string nullString)
+ => throw ClientEvaluationNotSupportedException();
+
+ #endregion
+
+ #region string_to_array
+
+ ///
+ /// Converts the input string into an array using the given delimiter.
+ ///
+ /// The DbFunctions instance.
+ /// The input string of delimited values.
+ /// The value that delimits the elements.
+ /// The type of the elements in the resulting array.
+ ///
+ /// The array resulting from splitting the input string based on the supplied delimiter.
+ ///
+ ///
+ /// This method is only intended for use via SQL translation as part of an EF Core LINQ query.
+ ///
+ [CanBeNull]
+ public static T[] StringToArray(
+ [CanBeNull] this DbFunctions _,
+ [CanBeNull] string input,
+ [CanBeNull] string delimiter)
+ => throw ClientEvaluationNotSupportedException();
+
+ ///
+ /// Converts the input string into an array using the given delimiter and null string.
+ ///
+ /// The DbFunctions instance.
+ /// The input string of delimited values.
+ /// The value that delimits the elements.
+ /// The value that represents a null value.
+ /// The type of the elements in the resulting array.
+ ///
+ /// The array resulting from splitting the input string based on the supplied delimiter and null string.
+ ///
+ ///
+ /// This method is only intended for use via SQL translation as part of an EF Core LINQ query.
+ ///
+ [CanBeNull]
+ public static T[] StringToArray(
+ [CanBeNull] this DbFunctions _,
+ [CanBeNull] string input,
+ [CanBeNull] string delimiter,
+ [CanBeNull] string nullString)
+ => throw ClientEvaluationNotSupportedException();
+
+ ///
+ /// Converts the input string into a using the given delimiter.
+ ///
+ /// The DbFunctions instance.
+ /// The input string of delimited values.
+ /// The value that delimits the elements.
+ /// The type of the elements in the resulting array.
+ ///
+ /// The list resulting from splitting the input string based on the supplied delimiter.
+ ///
+ ///
+ /// This method is only intended for use via SQL translation as part of an EF Core LINQ query.
+ ///
+ [CanBeNull]
+ public static List StringToList(
+ [CanBeNull] this DbFunctions _,
+ [CanBeNull] string input,
+ [CanBeNull] string delimiter)
+ => throw ClientEvaluationNotSupportedException();
+
+ ///
+ /// Converts the input string into a using the given delimiter and null string.
+ ///
+ /// The DbFunctions instance.
+ /// The input string of delimited values.
+ /// The value that delimits the elements.
+ /// The value that represents a null value.
+ /// The type of the elements in the resulting array.
+ ///
+ /// The list resulting from splitting the input string based on the supplied delimiter and null string.
+ ///
+ ///
+ /// This method is only intended for use via SQL translation as part of an EF Core LINQ query.
+ ///
+ [CanBeNull]
+ public static List StringToList(
+ [CanBeNull] this DbFunctions _,
+ [CanBeNull] string input,
+ [CanBeNull] string delimiter,
+ [CanBeNull] string nullString)
+ => throw ClientEvaluationNotSupportedException();
+
+ #endregion
+
+ #region Utilities
+
+ ///
+ /// Helper method to throw a with the name of the throwing method.
+ ///
+ /// The method that throws the exception.
+ ///
+ /// A .
+ ///
+ [NotNull]
+ static NotSupportedException ClientEvaluationNotSupportedException([CallerMemberName] string method = default)
+ => new NotSupportedException($"{method} is only intended for use via SQL translation as part of an EF Core LINQ query.");
+
+ #endregion
+ }
+}
diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayFragmentTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayFragmentTranslator.cs
new file mode 100644
index 000000000..a004b809c
--- /dev/null
+++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayFragmentTranslator.cs
@@ -0,0 +1,232 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq.Expressions;
+using System.Reflection;
+using JetBrains.Annotations;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Query.ExpressionTranslators;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal;
+using Remotion.Linq;
+using Remotion.Linq.Clauses;
+using Remotion.Linq.Clauses.Expressions;
+using Remotion.Linq.Clauses.ResultOperators;
+
+namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Internal
+{
+ ///
+ /// Provides translation services for array fragments.
+ ///
+ public class NpgsqlArrayFragmentTranslator : IExpressionFragmentTranslator
+ {
+ #region MethodInfoFields
+
+ ///
+ /// The for .
+ ///
+ [NotNull] static readonly MethodInfo Like2MethodInfo =
+ typeof(DbFunctionsExtensions)
+ .GetRuntimeMethod(nameof(DbFunctionsExtensions.Like), new[] { typeof(DbFunctions), typeof(string), typeof(string) });
+
+ ///
+ /// The for .
+ ///
+ [NotNull] static readonly MethodInfo Like3MethodInfo =
+ typeof(DbFunctionsExtensions)
+ .GetRuntimeMethod(nameof(DbFunctionsExtensions.Like), new[] { typeof(DbFunctions), typeof(string), typeof(string), typeof(string) });
+
+ // ReSharper disable once InconsistentNaming
+ ///
+ /// The for .
+ ///
+ [NotNull] static readonly MethodInfo ILike2MethodInfo =
+ typeof(NpgsqlDbFunctionsExtensions)
+ .GetRuntimeMethod(nameof(NpgsqlDbFunctionsExtensions.ILike), new[] { typeof(DbFunctions), typeof(string), typeof(string) });
+
+ // ReSharper disable once InconsistentNaming
+ ///
+ /// The for .
+ ///
+ [NotNull] static readonly MethodInfo ILike3MethodInfo =
+ typeof(NpgsqlDbFunctionsExtensions)
+ .GetRuntimeMethod(nameof(NpgsqlDbFunctionsExtensions.ILike), new[] { typeof(DbFunctions), typeof(string), typeof(string), typeof(string) });
+
+ #endregion
+
+ ///
+ [CanBeNull]
+ public Expression Translate(Expression expression)
+ {
+ if (!(expression is SubQueryExpression subQuery))
+ return null;
+
+ var model = subQuery.QueryModel;
+
+ if (!IsArrayOrList(model.MainFromClause.FromExpression.Type))
+ return null;
+
+ return
+ AllResult(model) ??
+ AnyResult(model) ??
+ ConcatResult(model) ??
+ CountResult(model);
+ }
+
+ #region SubQueries
+
+ ///
+ /// Visits an array-based ALL expression.
+ ///
+ /// The query model to visit.
+ ///
+ /// An expression or null.
+ ///
+ [CanBeNull]
+ static Expression AllResult([NotNull] QueryModel model)
+ {
+ Expression array = model.MainFromClause.FromExpression;
+
+ // TODO: when is there more than one result operator?
+ // Only handle singular result operators.
+ if (model.ResultOperators.Count == 1 && model.ResultOperators[0] is AllResultOperator all)
+ return ConstructArrayLike(array, all.Predicate, ArrayComparisonType.ALL);
+
+ return null;
+ }
+
+ ///
+ /// Visits an array-based ANY expression.
+ ///
+ /// The query model to visit.
+ ///
+ /// An expression or null.
+ ///
+ [CanBeNull]
+ static Expression AnyResult([NotNull] QueryModel model)
+ {
+ Expression array = model.MainFromClause.FromExpression;
+
+ // TODO: when is there more than one result operator?
+ // Only handle singular result operators.
+ if (model.ResultOperators.Count != 1 || !(model.ResultOperators[0] is AnyResultOperator _))
+ return null;
+
+ if (model.BodyClauses.Count == 1 && model.BodyClauses[0] is WhereClause where)
+ return ConstructArrayLike(array, where.Predicate, ArrayComparisonType.ANY);
+
+ return null;
+ }
+
+ ///
+ /// Visits an array-based concatenation expression: {array|value} || {array|value}.
+ ///
+ /// The query model to visit.
+ ///
+ /// An expression or null.
+ ///
+ [CanBeNull]
+ static Expression ConcatResult([NotNull] QueryModel model)
+ {
+ if (model.BodyClauses.Count != 0)
+ return null;
+
+ if (model.ResultOperators.Count != 1)
+ return null;
+
+ if (!(model.ResultOperators[0] is ConcatResultOperator concat))
+ return null;
+
+ Expression from = model.MainFromClause.FromExpression;
+
+ Expression other = concat.Source2;
+
+ if (!IsArrayOrList(other.Type))
+ return null;
+
+ return new CustomBinaryExpression(from, other, "||", from.Type);
+ }
+
+ ///
+ /// Visits an array-based count expression: {array}.Length, {list}.Count, {array|list}.Count(), {array|list}.Count({predicate}).
+ ///
+ /// The query model to visit.
+ ///
+ /// An expression or null.
+ ///
+ [CanBeNull]
+ static Expression CountResult([NotNull] QueryModel model)
+ {
+ // TODO: handle count operation with predicate.
+ if (model.BodyClauses.Count != 0)
+ return null;
+
+ if (model.ResultOperators.Count != 1)
+ return null;
+
+ if (!(model.ResultOperators[0] is CountResultOperator _))
+ return null;
+
+ Expression from = model.MainFromClause.FromExpression;
+
+ return
+ from.Type.IsArray
+ ? Expression.MakeMemberAccess(from, from.Type.GetRuntimeProperty(nameof(Array.Length)))
+ : Expression.MakeMemberAccess(from, from.Type.GetRuntimeProperty(nameof(IList.Count)));
+ }
+
+ ///
+ /// Visits an array-based comparison for an LIKE or ILIKE expression: {operand} {LIKE|ILIKE} {ANY|ALL} ({array}).
+ ///
+ /// The array expression.
+ /// The method call expression.
+ /// The array comparison type.
+ ///
+ /// An expression or null.
+ ///
+ [CanBeNull]
+ static Expression ConstructArrayLike([NotNull] Expression array, [CanBeNull] Expression predicate, ArrayComparisonType comparisonType)
+ {
+ if (!(predicate is MethodCallExpression call))
+ return null;
+
+ if (call.Arguments.Count < 2)
+ return null;
+
+ Expression operand = call.Arguments[1];
+ Expression collection = array;
+
+ switch (call.Method)
+ {
+ case MethodInfo m when m == Like2MethodInfo:
+ return new ArrayAnyAllExpression(comparisonType, "LIKE", operand, collection);
+
+ case MethodInfo m when m == Like3MethodInfo:
+ return new ArrayAnyAllExpression(comparisonType, "LIKE", operand, collection);
+
+ case MethodInfo m when m == ILike2MethodInfo:
+ return new ArrayAnyAllExpression(comparisonType, "ILIKE", operand, collection);
+
+ case MethodInfo m when m == ILike3MethodInfo:
+ return new ArrayAnyAllExpression(comparisonType, "ILIKE", operand, collection);
+
+ default:
+ return null;
+ }
+ }
+
+ #endregion
+
+ #region Helpers
+
+ ///
+ /// Tests if the type is an array or a .
+ ///
+ /// The type to test.
+ ///
+ /// True if is an array or a ; otherwise, false.
+ ///
+ static bool IsArrayOrList([NotNull] Type type) => type.IsArray || type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>);
+
+ #endregion
+ }
+}
diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayMemberTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayMemberTranslator.cs
new file mode 100644
index 000000000..8ab2f55d1
--- /dev/null
+++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayMemberTranslator.cs
@@ -0,0 +1,112 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq.Expressions;
+using JetBrains.Annotations;
+using Microsoft.EntityFrameworkCore.Query.Expressions;
+using Microsoft.EntityFrameworkCore.Query.ExpressionTranslators;
+
+namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Internal
+{
+ ///
+ /// Provides translation services for PostgreSQL array operators mapped to generic array members.
+ ///
+ ///
+ /// See: https://www.postgresql.org/docs/current/static/functions-array.html
+ ///
+ public class NpgsqlArrayMemberTranslator : IMemberTranslator
+ {
+ ///
+ /// The backend version to target.
+ ///
+ [CanBeNull] readonly Version _postgresVersion;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The backend version to target.
+ public NpgsqlArrayMemberTranslator([CanBeNull] Version postgresVersion) => _postgresVersion = postgresVersion;
+
+ ///
+ public Expression Translate(MemberExpression e)
+ {
+ if (!VersionAtLeast(9, 4))
+ return null;
+
+ return ArrayInstanceHandler(e) ??
+ ListInstanceHandler(e);
+ }
+
+ #region Handlers
+
+ [CanBeNull]
+ static Expression ArrayInstanceHandler([NotNull] MemberExpression e)
+ {
+ var instance = e.Expression;
+
+ if (instance == null || !instance.Type.IsArray || instance.Type.GetArrayRank() != 1)
+ return null;
+
+ switch (e.Member.Name)
+ {
+ case nameof(Array.Length):
+ return Expression.Coalesce(
+ new SqlFunctionExpression(
+ "array_length",
+ typeof(int?),
+ new[] { instance, Expression.Constant(1) }),
+ Expression.Constant(0));
+
+ case nameof(Array.Rank):
+ return Expression.Coalesce(
+ new SqlFunctionExpression(
+ "array_ndims",
+ typeof(int?),
+ new[] { instance }),
+ Expression.Constant(1));
+
+ default:
+ return null;
+ }
+ }
+
+ [CanBeNull]
+ static Expression ListInstanceHandler([NotNull] MemberExpression e)
+ {
+ var instance = e.Expression;
+
+ if (instance is null || !instance.Type.IsGenericType || instance.Type.GetGenericTypeDefinition() != typeof(List<>))
+ return null;
+
+ switch (e.Member.Name)
+ {
+ case nameof(IList.Count):
+ return Expression.Coalesce(
+ new SqlFunctionExpression(
+ "array_length",
+ typeof(int?),
+ new Expression[] { instance, Expression.Constant(1) }),
+ Expression.Constant(0));
+
+ default:
+ return null;
+ }
+ }
+
+ #endregion
+
+ #region Helpers
+
+ ///
+ /// True if is null, greater than, or equal to the specified version.
+ ///
+ /// The major version.
+ /// The minor version.
+ ///
+ /// True if is null, greater than, or equal to the specified version.
+ ///
+ bool VersionAtLeast(int major, int minor) => _postgresVersion is null || new Version(major, minor) <= _postgresVersion;
+
+ #endregion
+ }
+}
diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayMethodCallTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayMethodCallTranslator.cs
new file mode 100644
index 000000000..2018f35c6
--- /dev/null
+++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayMethodCallTranslator.cs
@@ -0,0 +1,294 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Reflection;
+using JetBrains.Annotations;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Query.Expressions;
+using Microsoft.EntityFrameworkCore.Query.ExpressionTranslators;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal;
+
+namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Internal
+{
+ ///
+ /// Provides translation services for PostgreSQL array operators mapped to methods declared on
+ /// , , , and .
+ ///
+ ///
+ /// See: https://www.postgresql.org/docs/current/static/functions-array.html
+ ///
+ public class NpgsqlArrayMethodCallTranslator : IMethodCallTranslator
+ {
+ ///
+ /// The backend version to target.
+ ///
+ [CanBeNull] readonly Version _postgresVersion;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The backend version to target.
+ public NpgsqlArrayMethodCallTranslator([CanBeNull] Version postgresVersion) => _postgresVersion = postgresVersion;
+
+ ///
+ [CanBeNull]
+ public Expression Translate(MethodCallExpression e)
+ {
+ if (!VersionAtLeast(9, 4))
+ return null;
+
+ var declaringType = e.Method.DeclaringType;
+
+ if (declaringType != null &&
+ declaringType != typeof(Enumerable) &&
+ declaringType != typeof(Array) &&
+ declaringType != typeof(NpgsqlArrayExtensions) &&
+ !declaringType.IsArray &&
+ !IsList(declaringType))
+ return null;
+
+ return EnumerableHandler(e) ??
+ NpgsqlArrayExtensionsHandler(e) ??
+ ArrayStaticHandler(e) ??
+ ArrayInstanceHandler(e) ??
+ ListInstanceHandler(e);
+ }
+
+ #region Handlers
+
+ [CanBeNull]
+ Expression EnumerableHandler([NotNull] MethodCallExpression e)
+ {
+ if (e.Method.DeclaringType != typeof(Enumerable))
+ return null;
+
+ var type = e.Arguments[0].Type;
+
+ if (!type.IsArray && !IsList(type))
+ return null;
+
+ switch (e.Method.Name)
+ {
+ case nameof(Enumerable.Count):
+ return Expression.Coalesce(
+ new SqlFunctionExpression(
+ "array_length",
+ typeof(int?),
+ new[] { e.Arguments[0], Expression.Constant(1) }),
+ Expression.Constant(0));
+
+ case nameof(Enumerable.ElementAt) when e.Arguments[0].Type.IsArray:
+ return MakeArrayIndex(e.Arguments[0], e.Arguments[1]);
+
+ case nameof(Enumerable.ElementAt):
+ return MakeListIndex(e.Arguments[0], e.Arguments[1]);
+
+ case nameof(Enumerable.Append):
+ return new CustomBinaryExpression(e.Arguments[0], e.Arguments[1], "||", e.Arguments[0].Type);
+
+ case nameof(Enumerable.Prepend):
+ return new CustomBinaryExpression(e.Arguments[1], e.Arguments[0], "||", e.Arguments[0].Type);
+
+ case nameof(Enumerable.SequenceEqual):
+ return Expression.MakeBinary(ExpressionType.Equal, e.Arguments[0], e.Arguments[1]);
+
+ default:
+ return null;
+ }
+ }
+
+ [CanBeNull]
+ Expression NpgsqlArrayExtensionsHandler([NotNull] MethodCallExpression e)
+ {
+ if (e.Method.DeclaringType != typeof(NpgsqlArrayExtensions))
+ return null;
+
+ switch (e.Method.Name)
+ {
+ case nameof(NpgsqlArrayExtensions.Contains):
+ return new CustomBinaryExpression(e.Arguments[1], e.Arguments[2], "@>", typeof(bool));
+
+ case nameof(NpgsqlArrayExtensions.ContainedBy):
+ return new CustomBinaryExpression(e.Arguments[1], e.Arguments[2], "<@", typeof(bool));
+
+ case nameof(NpgsqlArrayExtensions.Overlaps):
+ return new CustomBinaryExpression(e.Arguments[1], e.Arguments[2], "&&", typeof(bool));
+
+ case nameof(NpgsqlArrayExtensions.ArrayFill):
+ return new SqlFunctionExpression("array_fill", e.Method.ReturnType, e.Arguments.Skip(1));
+
+ case nameof(NpgsqlArrayExtensions.ListFill):
+ return new SqlFunctionExpression("array_fill", e.Method.ReturnType, e.Arguments.Skip(1));
+
+ case nameof(NpgsqlArrayExtensions.ArrayDimensions):
+ return new SqlFunctionExpression("array_dims", e.Method.ReturnType, e.Arguments.Skip(1).Take(1));
+
+ case nameof(NpgsqlArrayExtensions.ArrayPositions) when VersionAtLeast(9, 5):
+ return new SqlFunctionExpression("array_positions", e.Method.ReturnType, e.Arguments.Skip(1));
+
+ case nameof(NpgsqlArrayExtensions.ArrayRemove):
+ return new SqlFunctionExpression("array_remove", e.Method.ReturnType, e.Arguments.Skip(1).Take(2));
+
+ case nameof(NpgsqlArrayExtensions.ArrayReplace):
+ return new SqlFunctionExpression("array_replace", e.Method.ReturnType, e.Arguments.Skip(1).Take(3));
+
+ case nameof(NpgsqlArrayExtensions.ArrayToString):
+ return new SqlFunctionExpression("array_to_string", e.Method.ReturnType, e.Arguments.Skip(1).Take(3));
+
+ case nameof(NpgsqlArrayExtensions.StringToArray):
+ return new SqlFunctionExpression("string_to_array", e.Method.ReturnType, e.Arguments.Skip(1).Take(3));
+
+ case nameof(NpgsqlArrayExtensions.StringToList):
+ return new SqlFunctionExpression("string_to_array", e.Method.ReturnType, e.Arguments.Skip(1).Take(3));
+
+ default:
+ return null;
+ }
+ }
+
+ [CanBeNull]
+ Expression ArrayStaticHandler([NotNull] MethodCallExpression e)
+ {
+ if (!e.Method.IsStatic || e.Method.DeclaringType != typeof(Array))
+ return null;
+
+ switch (e.Method.Name)
+ {
+ case nameof(Array.IndexOf) when VersionAtLeast(9, 5) &&
+ e.Arguments[0].Type.GetArrayRank() == 1 &&
+ e.Method.GetParameters().Length <= 3:
+ return Expression.Subtract(
+ Expression.Coalesce(
+ new SqlFunctionExpression(
+ "array_position",
+ typeof(int?),
+ e.Arguments.Take(3)),
+ Expression.Constant(0)),
+ Expression.Constant(1));
+
+ default:
+ return null;
+ }
+ }
+
+ [CanBeNull]
+ Expression ArrayInstanceHandler([NotNull] MethodCallExpression e)
+ {
+ var instance = e.Object;
+
+ if (instance == null || !instance.Type.IsArray)
+ return null;
+
+ switch (e.Method.Name)
+ {
+ case nameof(Array.GetLength):
+ return Expression.Coalesce(
+ new SqlFunctionExpression(
+ "array_length",
+ typeof(int?),
+ new[] { instance, GenerateOneBasedIndexExpression(e.Arguments[0]) }),
+ Expression.Constant(0));
+
+ case nameof(Array.GetLowerBound):
+ return Expression.Subtract(
+ Expression.Coalesce(
+ new SqlFunctionExpression(
+ "array_lower",
+ typeof(int?),
+ new[] { instance, GenerateOneBasedIndexExpression(e.Arguments[0]) }),
+ Expression.Constant(0)),
+ Expression.Constant(1));
+
+ case nameof(Array.GetUpperBound):
+ return Expression.Subtract(
+ Expression.Coalesce(
+ new SqlFunctionExpression(
+ "array_upper",
+ typeof(int?),
+ new[] { instance, GenerateOneBasedIndexExpression(e.Arguments[0]) }),
+ Expression.Constant(0)),
+ Expression.Constant(1));
+
+ default:
+ return null;
+ }
+ }
+
+ [CanBeNull]
+ Expression ListInstanceHandler([NotNull] MethodCallExpression e)
+ {
+ var instance = e.Object;
+
+ if (instance == null || !IsList(instance.Type))
+ return null;
+
+ switch (e.Method.Name)
+ {
+ case "get_Item":
+ return MakeListIndex(instance, e.Arguments[0]);
+
+ case nameof(IList.IndexOf) when VersionAtLeast(9, 5):
+ return Expression.Subtract(
+ Expression.Coalesce(
+ new SqlFunctionExpression(
+ "array_position",
+ typeof(int?),
+ new[] { instance, e.Arguments[0] }),
+ Expression.Constant(0)),
+ Expression.Constant(1));
+
+ default:
+ return null;
+ }
+ }
+
+ #endregion
+
+ #region Helpers
+
+ ///
+ /// True if is null, greater than, or equal to the specified version.
+ ///
+ /// The major version.
+ /// The minor version.
+ ///
+ /// True if is null, greater than, or equal to the specified version.
+ ///
+ bool VersionAtLeast(int major, int minor) => _postgresVersion == null || new Version(major, minor) <= _postgresVersion;
+
+ [NotNull]
+ static Expression MakeListIndex([NotNull] Expression instance, [NotNull] Expression index)
+ => Expression.MakeIndex(
+ instance,
+ instance.Type
+ .GetRuntimeProperties()
+ .Where(x => x.Name == instance.Type.GetCustomAttribute()?.MemberName)
+ .Select(x => (Indexer: x, Parameters: x.GetGetMethod().GetParameters()))
+ .Where(x => x.Parameters.Length == 1)
+ .SingleOrDefault(x => x.Parameters.Single().ParameterType == index.Type)
+ .Indexer,
+ new[] { index });
+
+ [NotNull]
+ static Expression MakeArrayIndex([NotNull] Expression instance, [NotNull] Expression index)
+ => instance.Type.GetArrayRank() == 1
+ ? (Expression)Expression.ArrayIndex(instance, index)
+ : Expression.ArrayAccess(instance, index);
+
+ ///
+ /// PostgreSQL array indexing is 1-based. If the index happens to be a constant,
+ /// just increment it. Otherwise, append a +1 in the SQL.
+ ///
+ [NotNull]
+ static Expression GenerateOneBasedIndexExpression([NotNull] Expression expression)
+ => expression is ConstantExpression constantExpression
+ ? Expression.Constant(Convert.ToInt32(constantExpression.Value) + 1)
+ : (Expression)Expression.Add(expression, Expression.Constant(1));
+
+ static bool IsList([NotNull] Type type) => type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>);
+
+ #endregion
+ }
+}
diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArraySequenceEqualTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArraySequenceEqualTranslator.cs
deleted file mode 100644
index ecbac4b63..000000000
--- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArraySequenceEqualTranslator.cs
+++ /dev/null
@@ -1,38 +0,0 @@
-using System.Linq;
-using System.Linq.Expressions;
-using System.Reflection;
-using JetBrains.Annotations;
-using Microsoft.EntityFrameworkCore.Query.ExpressionTranslators;
-
-namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Internal
-{
- ///
- /// Translates Enumerable.SequenceEqual on arrays into PostgreSQL array equality operations.
- ///
- ///
- /// https://www.postgresql.org/docs/current/static/functions-array.html
- ///
- public class NpgsqlArraySequenceEqualTranslator : IMethodCallTranslator
- {
- static readonly MethodInfo SequenceEqualMethodInfo = typeof(Enumerable).GetTypeInfo().GetDeclaredMethods(nameof(Enumerable.SequenceEqual)).Single(m =>
- m.IsGenericMethodDefinition &&
- m.GetParameters().Length == 2
- );
-
- [CanBeNull]
- public Expression Translate(MethodCallExpression methodCallExpression)
- {
- var method = methodCallExpression.Method;
- if (method.IsGenericMethod &&
- ReferenceEquals(method.GetGenericMethodDefinition(), SequenceEqualMethodInfo) &&
- methodCallExpression.Arguments.All(a => a.Type.IsArray))
- {
- return Expression.MakeBinary(ExpressionType.Equal,
- methodCallExpression.Arguments[0],
- methodCallExpression.Arguments[1]);
- }
-
- return null;
- }
- }
-}
diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlCompositeExpressionFragmentTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlCompositeExpressionFragmentTranslator.cs
index 6aa928ccf..3b738a4b8 100644
--- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlCompositeExpressionFragmentTranslator.cs
+++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlCompositeExpressionFragmentTranslator.cs
@@ -13,7 +13,9 @@ public class NpgsqlCompositeExpressionFragmentTranslator : RelationalCompositeEx
/// The default expression fragment translators registered by the Npgsql provider.
///
[NotNull] [ItemNotNull] static readonly IExpressionFragmentTranslator[] ExpressionFragmentTranslators =
- {};
+ {
+ new NpgsqlArrayFragmentTranslator()
+ };
///
public NpgsqlCompositeExpressionFragmentTranslator(
diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlCompositeMemberTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlCompositeMemberTranslator.cs
index 3accfdc90..17a1204bc 100644
--- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlCompositeMemberTranslator.cs
+++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlCompositeMemberTranslator.cs
@@ -26,8 +26,15 @@ public NpgsqlCompositeMemberTranslator(
[NotNull] INpgsqlOptions npgsqlOptions)
: base(dependencies)
{
- // ReSharper disable once VirtualMemberCallInConstructor
+ var versionDependentTranslators = new IMemberTranslator[]
+ {
+ new NpgsqlArrayMemberTranslator(npgsqlOptions.PostgresVersion)
+ };
+
+ // ReSharper disable VirtualMemberCallInConstructor
AddTranslators(MemberTranslators);
+ AddTranslators(versionDependentTranslators);
+ // ReSharper restore VirtualMemberCallInConstructor
}
///
diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlCompositeMethodCallTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlCompositeMethodCallTranslator.cs
index 9d55d6d2e..e915f5b76 100644
--- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlCompositeMethodCallTranslator.cs
+++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlCompositeMethodCallTranslator.cs
@@ -15,7 +15,6 @@ public class NpgsqlCompositeMethodCallTranslator : RelationalCompositeMethodCall
///
[NotNull] [ItemNotNull] static readonly IMethodCallTranslator[] MethodCallTranslators =
{
- new NpgsqlArraySequenceEqualTranslator(),
new NpgsqlConvertTranslator(),
new NpgsqlGuidTranslator(),
new NpgsqlLikeTranslator(),
@@ -37,15 +36,15 @@ public NpgsqlCompositeMethodCallTranslator(
{
var versionDependentTranslators = new IMethodCallTranslator[]
{
+ new NpgsqlArrayMethodCallTranslator(npgsqlOptions.PostgresVersion),
new NpgsqlDateAddTranslator(npgsqlOptions.PostgresVersion),
new NpgsqlMathTranslator(npgsqlOptions.PostgresVersion)
};
- // ReSharper disable once DoNotCallOverridableMethodsInConstructor
+ // ReSharper disable VirtualMemberCallInConstructor
AddTranslators(MethodCallTranslators);
-
- // ReSharper disable once DoNotCallOverridableMethodsInConstructor
AddTranslators(versionDependentTranslators);
+ // ReSharper restore VirtualMemberCallInConstructor
}
///
diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlStringTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlStringTranslator.cs
index d0e4e76c7..db24aca5a 100644
--- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlStringTranslator.cs
+++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlStringTranslator.cs
@@ -25,6 +25,7 @@ public class NpgsqlStringTranslator : IMethodCallTranslator
[NotNull] static readonly MethodInfo Contains = typeof(string).GetRuntimeMethod(nameof(string.Contains), new[] { typeof(string) });
[NotNull] static readonly MethodInfo EndsWith = typeof(string).GetRuntimeMethod(nameof(string.EndsWith), new[] { typeof(string) });
[NotNull] static readonly MethodInfo StartsWith = typeof(string).GetRuntimeMethod(nameof(string.StartsWith), new[] { typeof(string) });
+ [NotNull] static readonly MethodInfo GetIndexer = typeof(string).GetRuntimeMethod("get_Chars", new[] { typeof(int) });
[NotNull] static readonly MethodInfo IndexOfString = typeof(string).GetRuntimeMethod(nameof(string.IndexOf), new[] { typeof(string) });
[NotNull] static readonly MethodInfo IndexOfChar = typeof(string).GetRuntimeMethod(nameof(string.IndexOf), new[] { typeof(char) });
[NotNull] static readonly MethodInfo IsNullOrWhiteSpace = typeof(string).GetRuntimeMethod(nameof(string.IsNullOrWhiteSpace), new[] { typeof(string) });
@@ -48,6 +49,12 @@ public class NpgsqlStringTranslator : IMethodCallTranslator
#endregion
+ #region PropertyInfo
+
+ [NotNull] static readonly PropertyInfo Indexer = typeof(string).GetRuntimeProperty("Chars");
+
+ #endregion
+
///
[CanBeNull]
public virtual Expression Translate(MethodCallExpression e)
@@ -58,6 +65,7 @@ public virtual Expression Translate(MethodCallExpression e)
return TranslateContains(e) ??
TranslateEndsWith(e) ??
TranslateStartsWith(e) ??
+ TranslateIndexer(e) ??
TranslateIndexOf(e) ??
TranslateIsNullOrWhiteSpace(e) ??
TranslatePadLeft(e) ??
@@ -147,6 +155,19 @@ static Expression TranslateStartsWith([NotNull] MethodCallExpression e)
#endregion
+ #region Indexer
+
+ [CanBeNull]
+ static Expression TranslateIndexer([NotNull] MethodCallExpression e)
+ {
+ if (e.Method != GetIndexer || e.Object == null)
+ return null;
+
+ return Expression.MakeIndex(e.Object, Indexer, e.Arguments.Take(1));
+ }
+
+ #endregion
+
#region IndexOf
[CanBeNull]
diff --git a/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlSqlTranslatingExpressionVisitor.cs b/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlSqlTranslatingExpressionVisitor.cs
index fe8d4530b..e849b85a8 100644
--- a/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlSqlTranslatingExpressionVisitor.cs
+++ b/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlSqlTranslatingExpressionVisitor.cs
@@ -1,14 +1,12 @@
-using System.Linq;
+using System;
+using System.Collections.Generic;
using System.Linq.Expressions;
-using System.Reflection;
using JetBrains.Annotations;
-using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Query;
using Microsoft.EntityFrameworkCore.Query.Expressions;
using Microsoft.EntityFrameworkCore.Query.ExpressionVisitors;
using Microsoft.EntityFrameworkCore.Query.ExpressionVisitors.Internal;
using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal;
-using Remotion.Linq.Clauses;
using Remotion.Linq.Clauses.Expressions;
using Remotion.Linq.Clauses.ResultOperators;
@@ -20,39 +18,15 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionVisitors
public class NpgsqlSqlTranslatingExpressionVisitor : SqlTranslatingExpressionVisitor
{
///
- /// The for .
+ /// The current query model visitor.
///
- [NotNull] static readonly MethodInfo Like2MethodInfo =
- typeof(DbFunctionsExtensions)
- .GetRuntimeMethod(nameof(DbFunctionsExtensions.Like), new[] { typeof(DbFunctions), typeof(string), typeof(string) });
-
- ///
- /// The for .
- ///
- [NotNull] static readonly MethodInfo Like3MethodInfo =
- typeof(DbFunctionsExtensions)
- .GetRuntimeMethod(nameof(DbFunctionsExtensions.Like), new[] { typeof(DbFunctions), typeof(string), typeof(string), typeof(string) });
-
- // ReSharper disable once InconsistentNaming
- ///
- /// The for .
- ///
- [NotNull] static readonly MethodInfo ILike2MethodInfo =
- typeof(NpgsqlDbFunctionsExtensions)
- .GetRuntimeMethod(nameof(NpgsqlDbFunctionsExtensions.ILike), new[] { typeof(DbFunctions), typeof(string), typeof(string) });
-
- // ReSharper disable once InconsistentNaming
- ///
- /// The for .
- ///
- [NotNull] static readonly MethodInfo ILike3MethodInfo =
- typeof(NpgsqlDbFunctionsExtensions)
- .GetRuntimeMethod(nameof(NpgsqlDbFunctionsExtensions.ILike), new[] { typeof(DbFunctions), typeof(string), typeof(string), typeof(string) });
+ [NotNull] readonly RelationalQueryModelVisitor _queryModelVisitor;
///
- /// The query model visitor.
+ /// The current query compilation context.
///
- [NotNull] readonly RelationalQueryModelVisitor _queryModelVisitor;
+ [NotNull]
+ RelationalQueryCompilationContext QueryCompilationContext => _queryModelVisitor.QueryCompilationContext;
///
public NpgsqlSqlTranslatingExpressionVisitor(
@@ -64,32 +38,40 @@ public NpgsqlSqlTranslatingExpressionVisitor(
: base(dependencies, queryModelVisitor, targetSelectExpression, topLevelPredicate, inProjection)
=> _queryModelVisitor = queryModelVisitor;
- ///
- protected override Expression VisitSubQuery(SubQueryExpression expression)
- => base.VisitSubQuery(expression) ?? VisitLikeAnyAll(expression) ?? VisitEqualsAny(expression);
+ #region Overrides
///
- protected override Expression VisitBinary(BinaryExpression expression)
+ protected override Expression VisitBinary(BinaryExpression e)
{
- if (expression.NodeType == ExpressionType.ArrayIndex)
+ switch (e.NodeType)
{
- var properties = MemberAccessBindingExpressionVisitor.GetPropertyPath(
- expression.Left, _queryModelVisitor.QueryCompilationContext, out _);
- if (properties.Count == 0)
- return base.VisitBinary(expression);
- var lastPropertyType = properties[properties.Count - 1].ClrType;
- if (lastPropertyType.IsArray && lastPropertyType.GetArrayRank() == 1)
- {
- var left = Visit(expression.Left);
- var right = Visit(expression.Right);
-
- return left != null && right != null
- ? Expression.MakeBinary(ExpressionType.ArrayIndex, left, right)
- : base.VisitBinary(expression);
- }
- }
+ case ExpressionType.ArrayIndex:
+ if (MemberAccessBindingExpressionVisitor.GetPropertyPath(e.Left, QueryCompilationContext, out _).Count == 0)
+ goto default;
+
+ var leftArrayIndex = Visit(e.Left);
+ var rightArrayIndex = Visit(e.Right);
+
+ if (leftArrayIndex == null || rightArrayIndex == null)
+ goto default;
- return base.VisitBinary(expression);
+ return Expression.ArrayIndex(leftArrayIndex, rightArrayIndex);
+
+ case ExpressionType.Index:
+ if (MemberAccessBindingExpressionVisitor.GetPropertyPath(e.Left, QueryCompilationContext, out _).Count == 0)
+ goto default;
+
+ var leftArrayAccess = Visit(e.Left);
+ var rightArrayAccess = Visit(e.Right);
+
+ if (leftArrayAccess == null || rightArrayAccess == null)
+ goto default;
+
+ return Expression.ArrayAccess(leftArrayAccess, rightArrayAccess);
+
+ default:
+ return base.VisitBinary(e);
+ }
}
///
@@ -100,105 +82,117 @@ protected override Expression VisitBinary(BinaryExpression expression)
/// An '= ANY' expression or null.
///
[CanBeNull]
- protected virtual Expression VisitEqualsAny([NotNull] SubQueryExpression expression)
- {
- var subQueryModel = expression.QueryModel;
- var fromExpression = subQueryModel.MainFromClause.FromExpression;
+ protected override Expression VisitSubQuery(SubQueryExpression expression)
+ => base.VisitSubQuery(expression) ?? VisitArraySubQuery(expression);
- var properties = MemberAccessBindingExpressionVisitor.GetPropertyPath(
- fromExpression, _queryModelVisitor.QueryCompilationContext, out _);
+ ///
+ protected override Expression VisitUnary(UnaryExpression expression)
+ {
+ // Character literals are represented as integer literals by the expression tree.
+ // So `myString[0] == 'T'` looks like `((int)myString[0]) == 84`.
+ // Since `substr` returns `text`, not `char` or `int`, wrap it in `ascii`.
+ if (expression.NodeType == ExpressionType.Convert &&
+ Visit(expression.Operand) is IndexExpression index &&
+ index.Object.Type == typeof(string))
+ return new SqlFunctionExpression("ascii", typeof(int), new[] { index });
+
+ return base.VisitUnary(expression);
+ }
- if (properties.Count == 0)
- return null;
- var lastPropertyType = properties[properties.Count - 1].ClrType;
- if (lastPropertyType.IsArray && lastPropertyType.GetArrayRank() == 1 && subQueryModel.ResultOperators.Count > 0)
+ ///
+ ///
+ /// https://github.com/aspnet/EntityFrameworkCore/blob/release/2.2/src/EFCore.Relational/Query/ExpressionVisitors/SqlTranslatingExpressionVisitor.cs#L1077-L1144
+ ///
+ protected override Expression VisitExtension(Expression expression)
+ {
+ switch (expression)
+ {
+ case ArrayAnyAllExpression e:
{
- var from = Visit(fromExpression);
+ var operand = Visit(e.Operand);
+ var array = Visit(e.Array);
- if (from == null)
+ if (operand == null || array == null)
return null;
- switch (subQueryModel.ResultOperators.First())
- {
- // Translate someArray.Length
- case CountResultOperator _:
- return Expression.ArrayLength(from);
+ return
+ operand != e.Operand || array != e.Array
+ ? new ArrayAnyAllExpression(e.ArrayComparisonType, e.Operator, operand, array)
+ : e;
+ }
+
+ case CustomBinaryExpression e:
+ {
+ var left = Visit(e.Left);
+ var right = Visit(e.Right);
+
+ if (left == null || right == null)
+ return null;
- // Translate someArray.Contains(someValue)
- case ContainsResultOperator contains when Visit(contains.Item) is Expression containsItem:
- return new ArrayAnyAllExpression(ArrayComparisonType.ANY, "=", containsItem, from);
- }
+ return
+ left != e.Left || right != e.Right
+ ? new CustomBinaryExpression(left, right, e.Operator, e.Type)
+ : e;
}
- return null;
+ default:
+ return base.VisitExtension(expression);
+ }
}
+ #endregion
+
+ #region ArraySubQueries
+
///
- /// Visits a and attempts to translate a LIKE/ILIKE ANY/ALL expression.
+ /// Visits an array-based subquery.
///
- /// The expression to visit.
+ /// The subquery expression.
///
- /// A 'LIKE ANY', 'LIKE ALL', 'ILIKE ANY', or 'ILIKE ALL' expression or null.
+ /// An expression or null.
///
[CanBeNull]
- protected virtual Expression VisitLikeAnyAll([NotNull] SubQueryExpression expression)
+ protected virtual Expression VisitArraySubQuery([NotNull] SubQueryExpression expression)
{
- var queryModel = expression.QueryModel;
- var results = queryModel.ResultOperators;
- var body = queryModel.BodyClauses;
+ var model = expression.QueryModel;
+ var from = model.MainFromClause.FromExpression;
+ var results = model.ResultOperators;
+
+ // Only handle types mapped to PostgreSQL arrays.
+ if (!IsArrayOrList(from.Type))
+ return null;
+ // Only handle subqueries when the from expression is visitable.
+ if (!(Visit(from) is Expression array))
+ return null;
+
+ // Only handle singular result operators.
if (results.Count != 1)
return null;
- ArrayComparisonType comparisonType;
- MethodCallExpression call;
switch (results[0])
{
- case AnyResultOperator _:
- comparisonType = ArrayComparisonType.ANY;
- call =
- body.Count == 1 &&
- body[0] is WhereClause whereClause &&
- whereClause.Predicate is MethodCallExpression methocCall
- ? methocCall
- : null;
- break;
-
- case AllResultOperator allResult:
- comparisonType = ArrayComparisonType.ALL;
- call = allResult.Predicate as MethodCallExpression;
- break;
+ case ContainsResultOperator contains:
+ return new ArrayAnyAllExpression(ArrayComparisonType.ANY, "=", Visit(contains.Item) ?? contains.Item, array);
default:
return null;
}
+ }
- if (call is null)
- return null;
-
- if (!(Visit(queryModel.MainFromClause.FromExpression) is Expression patterns))
- return null;
-
- if (!(Visit(call.Arguments[1]) is Expression match))
- return null;
-
- switch (call.Method)
- {
- case MethodInfo m when m == Like2MethodInfo:
- return new ArrayAnyAllExpression(comparisonType, "LIKE", match, patterns);
-
- case MethodInfo m when m == Like3MethodInfo:
- return new ArrayAnyAllExpression(comparisonType, "LIKE", match, patterns);
+ #endregion
- case MethodInfo m when m == ILike2MethodInfo:
- return new ArrayAnyAllExpression(comparisonType, "ILIKE", match, patterns);
+ #region Helpers
- case MethodInfo m when m == ILike3MethodInfo:
- return new ArrayAnyAllExpression(comparisonType, "ILIKE", match, patterns);
+ ///
+ /// Tests if the type is an array or a .
+ ///
+ /// The type to test.
+ ///
+ /// True if is an array or a ; otherwise, false.
+ ///
+ static bool IsArrayOrList([NotNull] Type type) => type.IsArray || type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>);
- default:
- return null;
- }
- }
+ #endregion
}
}
diff --git a/src/EFCore.PG/Query/Sql/Internal/NpgsqlQuerySqlGenerator.cs b/src/EFCore.PG/Query/Sql/Internal/NpgsqlQuerySqlGenerator.cs
index 1da319ef9..cc7bf8501 100644
--- a/src/EFCore.PG/Query/Sql/Internal/NpgsqlQuerySqlGenerator.cs
+++ b/src/EFCore.PG/Query/Sql/Internal/NpgsqlQuerySqlGenerator.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.Diagnostics;
using System.Linq.Expressions;
using System.Text.RegularExpressions;
@@ -202,14 +203,48 @@ protected override Expression VisitUnary(UnaryExpression expression)
return base.VisitUnary(expression);
}
+ ///
+ protected override Expression VisitIndex(IndexExpression e)
+ {
+ // text cannot be subscripted.
+ if (e.Object.Type == typeof(string))
+ {
+ return VisitSqlFunction(
+ new SqlFunctionExpression(
+ "substr",
+ typeof(char),
+ new[] { e.Object, GenerateOneBasedIndexExpression(e.Arguments[0]), Expression.Constant(1) }));
+ }
+
+ if (!IsArrayOrList(e.Object.Type))
+ return null;
+
+ Visit(e.Object);
+ for (int i = 0; i < e.Arguments.Count; i++)
+ {
+ Sql.Append('[');
+ Visit(GenerateOneBasedIndexExpression(e.Arguments[i]));
+ Sql.Append(']');
+ }
+
+ return e;
+ }
+
+ ///
+ /// Visits the children of an node.
+ ///
+ /// The expression.
+ ///
+ /// An .
+ ///
[NotNull]
protected virtual Expression VisitArrayIndex([NotNull] BinaryExpression expression)
{
Debug.Assert(expression.NodeType == ExpressionType.ArrayIndex);
+ // bytea cannot be subscripted, but there's get_byte.
if (expression.Left.Type == typeof(byte[]))
{
- // bytea cannot be subscripted, but there's get_byte.
return VisitSqlFunction(
new SqlFunctionExpression(
"get_byte",
@@ -217,16 +252,6 @@ protected virtual Expression VisitArrayIndex([NotNull] BinaryExpression expressi
new[] { expression.Left, expression.Right }));
}
- if (expression.Left.Type == typeof(string))
- {
- // text cannot be subscripted, use substr. PostgreSQL substr() is 1-based.
- return VisitSqlFunction(
- new SqlFunctionExpression(
- "substr",
- typeof(char),
- new[] { expression.Left, expression.Right, Expression.Constant(1) }));
- }
-
// Regular array from here
Visit(expression.Left);
Sql.Append('[');
@@ -480,5 +505,18 @@ public virtual Expression VisitPgFunction([NotNull] PgFunctionExpression express
}
#endregion
+
+ #region Helpers
+
+ ///
+ /// Tests if the type is an array or a .
+ ///
+ /// The type to test.
+ ///
+ /// True if is an array or a ; otherwise, false.
+ ///
+ static bool IsArrayOrList([NotNull] Type type) => type.IsArray || type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>);
+
+ #endregion
}
}
diff --git a/test/EFCore.PG.FunctionalTests/Query/ArrayQueryTest.cs b/test/EFCore.PG.FunctionalTests/Query/ArrayQueryTest.cs
index 41863ba5b..f8f4e654a 100644
--- a/test/EFCore.PG.FunctionalTests/Query/ArrayQueryTest.cs
+++ b/test/EFCore.PG.FunctionalTests/Query/ArrayQueryTest.cs
@@ -17,7 +17,18 @@ public class ArrayQueryTest : IClassFixture
#region Roundtrip
[Fact]
- public void Roundtrip()
+ public void Array_Roundtrip()
+ {
+ using (var ctx = Fixture.CreateContext())
+ {
+ var x = ctx.SomeEntities.Single(e => e.Id == 1);
+ Assert.Equal(new[] { 3, 4 }, x.SomeArray);
+ Assert.Equal(new List { 3, 4 }, x.SomeList);
+ }
+ }
+
+ [Fact]
+ public void List_Roundtrip()
{
using (var ctx = Fixture.CreateContext())
{
@@ -32,42 +43,139 @@ public void Roundtrip()
#region Indexers
[Fact]
- public void Index_with_constant()
+ public void Array_Index_with_constant()
{
using (var ctx = Fixture.CreateContext())
{
- var actual = ctx.SomeEntities.Where(e => e.SomeArray[0] == 3).ToList();
- Assert.Equal(1, actual.Count);
- AssertContainsInSql(@"WHERE (e.""SomeArray""[1]) = 3");
+ var _ = ctx.SomeEntities.Where(e => e.SomeArray[0] == 3).ToList();
+ AssertContainsInSql("WHERE (e.\"SomeArray\"[1]) = 3");
+ }
+ }
+
+ [Fact]
+ public void List_Index_with_constant()
+ {
+ using (var ctx = Fixture.CreateContext())
+ {
+ var _ = ctx.SomeEntities.Where(e => e.SomeList[0] == 3).ToList();
+ AssertContainsInSql("WHERE e.\"SomeList\"[1] = 3");
+ }
+ }
+
+ [Fact]
+ public void Array_Index_bytea_with_constant()
+ {
+ using (var ctx = Fixture.CreateContext())
+ {
+ var _ = ctx.SomeEntities.Where(e => e.SomeBytea[0] == 3).ToList();
+ AssertContainsInSql("WHERE (get_byte(e.\"SomeBytea\", 0)) = 3");
+ }
+ }
+
+ [Fact]
+ public void Array_ElementAt_with_constant()
+ {
+ using (var ctx = Fixture.CreateContext())
+ {
+ var _ = ctx.SomeEntities.Where(e => e.SomeArray.ElementAt(0) == 3).ToList();
+ AssertContainsInSql("WHERE (e.\"SomeArray\"[1]) = 3");
}
}
[Fact]
- public void Index_with_non_constant()
+ public void List_ElementAt_with_constant()
+ {
+ using (var ctx = Fixture.CreateContext())
+ {
+ var _ = ctx.SomeEntities.Where(e => e.SomeList.ElementAt(0) == 3).ToList();
+ AssertContainsInSql("WHERE e.\"SomeList\"[1] = 3");
+ }
+ }
+
+ [Fact]
+ public void Array_ElementAt_bytea_with_constant()
+ {
+ using (var ctx = Fixture.CreateContext())
+ {
+ var _ = ctx.SomeEntities.Where(e => e.SomeBytea.ElementAt(0) == 3).ToList();
+ AssertContainsInSql("WHERE (get_byte(e.\"SomeBytea\", 0)) = 3");
+ }
+ }
+
+ [Fact]
+ public void Array_Index_with_non_constant()
{
using (var ctx = Fixture.CreateContext())
{
// ReSharper disable once ConvertToConstant.Local
var x = 0;
- var actual = ctx.SomeEntities.Where(e => e.SomeArray[x] == 3).ToList();
- Assert.Equal(1, actual.Count);
- AssertContainsInSql(@"WHERE (e.""SomeArray""[@__x_0 + 1]) = 3");
+ var _ = ctx.SomeEntities.Where(e => e.SomeArray[x] == 3).ToList();
+ AssertContainsInSql("WHERE (e.\"SomeArray\"[@__x_0 + 1]) = 3");
}
}
[Fact]
- public void Index_bytea_with_constant()
+ public void List_Index_with_non_constant()
{
using (var ctx = Fixture.CreateContext())
{
- var actual = ctx.SomeEntities.Where(e => e.SomeBytea[0] == 3).ToList();
- Assert.Equal(1, actual.Count);
- AssertContainsInSql(@"WHERE (get_byte(e.""SomeBytea"", 0)) = 3");
+ // ReSharper disable once ConvertToConstant.Local
+ var x = 0;
+ var _ = ctx.SomeEntities.Where(e => e.SomeList[x] == 3).ToList();
+ AssertContainsInSql("WHERE e.\"SomeList\"[@__x_0 + 1] = 3");
+ }
+ }
+
+ [Fact]
+ public void Array_Index_bytea_with_non_constant()
+ {
+ using (var ctx = Fixture.CreateContext())
+ {
+ // ReSharper disable once ConvertToConstant.Local
+ var x = 0;
+ var _ = ctx.SomeEntities.Where(e => e.SomeBytea[x] == 3).ToList();
+ AssertContainsInSql("WHERE (get_byte(e.\"SomeBytea\", @__x_0)) = 3");
+ }
+ }
+
+ [Fact]
+ public void Array_ElementAt_with_non_constant()
+ {
+ using (var ctx = Fixture.CreateContext())
+ {
+ // ReSharper disable once ConvertToConstant.Local
+ var x = 0;
+ var _ = ctx.SomeEntities.Where(e => e.SomeArray.ElementAt(x) == 3).ToList();
+ AssertContainsInSql("WHERE (e.\"SomeArray\"[@__x_0 + 1]) = 3");
+ }
+ }
+
+ [Fact]
+ public void List_ElementAt_with_non_constant()
+ {
+ using (var ctx = Fixture.CreateContext())
+ {
+ // ReSharper disable once ConvertToConstant.Local
+ var x = 0;
+ var _ = ctx.SomeEntities.Where(e => e.SomeList.ElementAt(x) == 3).ToList();
+ AssertContainsInSql("WHERE e.\"SomeList\"[@__x_0 + 1] = 3");
+ }
+ }
+
+ [Fact]
+ public void Array_ElementAt_bytea_with_non_constant()
+ {
+ using (var ctx = Fixture.CreateContext())
+ {
+ // ReSharper disable once ConvertToConstant.Local
+ var x = 0;
+ var _ = ctx.SomeEntities.Where(e => e.SomeBytea.ElementAt(x) == 3).ToList();
+ AssertContainsInSql("WHERE (get_byte(e.\"SomeBytea\", @__x_0)) = 3");
}
}
[Fact]
- public void Index_multidimensional()
+ public void Array_Index_multidimensional()
{
using (var ctx = Fixture.CreateContext())
{
@@ -82,153 +190,752 @@ public void Index_multidimensional()
#region Equality
[Fact]
- public void SequenceEqual_with_parameter()
+ public void Array_Equal_with_parameter()
{
using (var ctx = Fixture.CreateContext())
{
- var arr = new[] { 3, 4 };
- var x = ctx.SomeEntities.Single(e => e.SomeArray.SequenceEqual(arr));
- Assert.Equal(new[] { 3, 4 }, x.SomeArray);
- AssertContainsInSql(@"WHERE e.""SomeArray"" = @");
+ var array = new[] { 3, 4 };
+ var _ = ctx.SomeEntities.Where(e => e.SomeArray.Equals(array)).ToList();
+ AssertContainsInSql("WHERE e.\"SomeArray\" = @__array_0");
}
}
[Fact]
- public void SequenceEqual_with_array_literal()
+ public void List_Equal_with_parameter()
{
using (var ctx = Fixture.CreateContext())
{
- var x = ctx.SomeEntities.Single(e => e.SomeArray.SequenceEqual(new[] { 3, 4 }));
- Assert.Equal(new[] { 3, 4 }, x.SomeArray);
- AssertContainsInSql(@"WHERE e.""SomeArray"" = ARRAY[3,4]::integer");
+ var list = new List { 3, 4 };
+ var _ = ctx.SomeEntities.Where(e => e.SomeList.Equals(list)).ToList();
+ AssertContainsInSql("WHERE e.\"SomeList\" = @__list_0");
+ }
+ }
+
+ [Fact]
+ public void Array_SequenceEqual_with_parameter()
+ {
+ using (var ctx = Fixture.CreateContext())
+ {
+ var array = new[] { 3, 4 };
+ var _ = ctx.SomeEntities.Where(e => e.SomeArray.Equals(array)).ToList();
+ AssertContainsInSql("WHERE e.\"SomeArray\" = @__array_0");
+ }
+ }
+
+ [Fact]
+ public void List_SequenceEqual_with_parameter()
+ {
+ using (var ctx = Fixture.CreateContext())
+ {
+ var list = new List { 3, 4 };
+ var _ = ctx.SomeEntities.Where(e => e.SomeList.SequenceEqual(list)).ToList();
+ AssertContainsInSql("WHERE e.\"SomeList\" = @__list_0");
+ }
+ }
+
+ [Fact]
+ public void Array_SequenceEqual_with_literal()
+ {
+ using (var ctx = Fixture.CreateContext())
+ {
+ var _ = ctx.SomeEntities.Where(e => e.SomeArray.SequenceEqual(new[] { 3, 4 })).ToList();
+ AssertContainsInSql("WHERE e.\"SomeArray\" = ARRAY[3,4]::integer");
+ }
+ }
+
+ [Fact]
+ public void List_SequenceEqual_with_literal()
+ {
+ using (var ctx = Fixture.CreateContext())
+ {
+ var _ = ctx.SomeEntities.Where(e => e.SomeList.SequenceEqual(new List { 3, 4 })).ToList();
+ AssertContainsInSql("WHERE e.\"SomeList\" = ARRAY[3,4]");
}
}
#endregion
- #region Containment
+ #region value = ANY (array)
[Fact]
- public void Contains_with_literal()
+ public void Array_Contains_with_literal()
{
using (var ctx = Fixture.CreateContext())
{
- var x = ctx.SomeEntities.Single(e => e.SomeArray.Contains(3));
- Assert.Equal(new[] { 3, 4 }, x.SomeArray);
- AssertContainsInSql(@"WHERE 3 = ANY (e.""SomeArray"")");
+ var _ = ctx.SomeEntities.Where(e => e.SomeArray.Contains(3)).ToList();
+ AssertContainsInSql("WHERE 3 = ANY (e.\"SomeArray\")");
+ }
+ }
+
+ [Fact]
+ public void List_Contains_with_literal()
+ {
+ using (var ctx = Fixture.CreateContext())
+ {
+ var _ = ctx.SomeEntities.Where(e => e.SomeList.Contains(3)).ToList();
+ AssertContainsInSql("WHERE 3 = ANY (e.\"SomeList\")");
}
}
[Fact]
- public void Contains_with_parameter()
+ public void Array_Contains_with_parameter()
{
using (var ctx = Fixture.CreateContext())
{
// ReSharper disable once ConvertToConstant.Local
var p = 3;
- var x = ctx.SomeEntities.Single(e => e.SomeArray.Contains(p));
- Assert.Equal(new[] { 3, 4 }, x.SomeArray);
- AssertContainsInSql(@"WHERE @__p_0 = ANY (e.""SomeArray"")");
+ var _ = ctx.SomeEntities.Where(e => e.SomeArray.Contains(p)).ToList();
+ AssertContainsInSql("WHERE @__p_0 = ANY (e.\"SomeArray\")");
}
}
[Fact]
- public void Contains_with_column()
+ public void List_Contains_with_parameter()
{
using (var ctx = Fixture.CreateContext())
{
- var x = ctx.SomeEntities.Single(e => e.SomeArray.Contains(e.Id + 2));
- Assert.Equal(new[] { 3, 4 }, x.SomeArray);
- AssertContainsInSql(@"WHERE e.""Id"" + 2 = ANY (e.""SomeArray"")");
+ // ReSharper disable once ConvertToConstant.Local
+ var p = 3;
+ var _ = ctx.SomeEntities.Where(e => e.SomeList.Contains(p)).ToList();
+ AssertContainsInSql("WHERE @__p_0 = ANY (e.\"SomeList\")");
+ }
+ }
+
+ [Fact]
+ public void Array_Contains_with_column()
+ {
+ using (var ctx = Fixture.CreateContext())
+ {
+ var _ = ctx.SomeEntities.Where(e => e.SomeArray.Contains(e.Id + 2)).ToList();
+ AssertContainsInSql("WHERE e.\"Id\" + 2 = ANY (e.\"SomeArray\")");
+ }
+ }
+
+ [Fact]
+ public void List_Contains_with_column()
+ {
+ using (var ctx = Fixture.CreateContext())
+ {
+ var _ = ctx.SomeEntities.Where(e => e.SomeList.Contains(e.Id + 2)).ToList();
+ AssertContainsInSql("WHERE e.\"Id\" + 2 = ANY (e.\"SomeList\")");
}
}
#endregion
- #region Length
+ #region @>
[Fact]
- public void Length()
+ public void Array_Contains_Array()
{
using (var ctx = Fixture.CreateContext())
{
- var x = ctx.SomeEntities.Single(e => e.SomeArray.Length == 2);
- Assert.Equal(new[] { 3, 4 }, x.SomeArray);
- AssertContainsInSql(@"WHERE array_length(e.""SomeArray"", 1) = 2");
+ var _ = ctx.SomeEntities.Where(x => EF.Functions.Contains(x.SomeArray, x.SomeArray)).ToList();
+ AssertContainsInSql("WHERE (x.\"SomeArray\" @> x.\"SomeArray\") = TRUE");
+ }
+ }
+
+ [Fact]
+ public void Array_Contains_List()
+ {
+ using (var ctx = Fixture.CreateContext())
+ {
+ var _ = ctx.SomeEntities.Where(x => EF.Functions.Contains(x.SomeArray, x.SomeList)).ToList();
+ AssertContainsInSql("WHERE (x.\"SomeArray\" @> x.\"SomeList\") = TRUE");
+ }
+ }
+
+ [Fact]
+ public void List_Contains_Array()
+ {
+ using (var ctx = Fixture.CreateContext())
+ {
+ var _ = ctx.SomeEntities.Where(x => EF.Functions.Contains(x.SomeList, x.SomeArray)).ToList();
+ AssertContainsInSql("WHERE (x.\"SomeList\" @> x.\"SomeArray\") = TRUE");
+ }
+ }
+
+ [Fact]
+ public void List_Contains_List()
+ {
+ using (var ctx = Fixture.CreateContext())
+ {
+ var _ = ctx.SomeEntities.Where(x => EF.Functions.Contains(x.SomeList, x.SomeList)).ToList();
+ AssertContainsInSql("WHERE (x.\"SomeList\" @> x.\"SomeList\") = TRUE");
+ }
+ }
+
+ #endregion
+
+ #region <@
+
+ [Fact]
+ public void Array_ContainedBy_Array()
+ {
+ using (var ctx = Fixture.CreateContext())
+ {
+ var _ = ctx.SomeEntities.Where(x => EF.Functions.ContainedBy(x.SomeArray, x.SomeArray)).ToList();
+ AssertContainsInSql("WHERE (x.\"SomeArray\" <@ x.\"SomeArray\") = TRUE");
+ }
+ }
+
+ [Fact]
+ public void Array_ContainedBy_List()
+ {
+ using (var ctx = Fixture.CreateContext())
+ {
+ var _ = ctx.SomeEntities.Where(x => EF.Functions.ContainedBy(x.SomeArray, x.SomeList)).ToList();
+ AssertContainsInSql("WHERE (x.\"SomeArray\" <@ x.\"SomeList\") = TRUE");
+ }
+ }
+
+ [Fact]
+ public void List_ContainedBy_Array()
+ {
+ using (var ctx = Fixture.CreateContext())
+ {
+ var _ = ctx.SomeEntities.Where(x => EF.Functions.ContainedBy(x.SomeList, x.SomeArray)).ToList();
+ AssertContainsInSql("WHERE (x.\"SomeList\" <@ x.\"SomeArray\") = TRUE");
+ }
+ }
+
+ [Fact]
+ public void List_ContainedBy_List()
+ {
+ using (var ctx = Fixture.CreateContext())
+ {
+ var _ = ctx.SomeEntities.Where(x => EF.Functions.ContainedBy(x.SomeList, x.SomeList)).ToList();
+ AssertContainsInSql("WHERE (x.\"SomeList\" <@ x.\"SomeList\") = TRUE");
+ }
+ }
+
+ #endregion
+
+ #region &&
+
+ [Fact]
+ public void Array_Overlaps_Array()
+ {
+ using (var ctx = Fixture.CreateContext())
+ {
+ var _ = ctx.SomeEntities.Where(x => EF.Functions.Overlaps(x.SomeArray, x.SomeArray)).ToList();
+ AssertContainsInSql("WHERE (x.\"SomeArray\" && x.\"SomeArray\") = TRUE");
+ }
+ }
+
+ [Fact]
+ public void Array_Overlaps_List()
+ {
+ using (var ctx = Fixture.CreateContext())
+ {
+ var _ = ctx.SomeEntities.Where(x => EF.Functions.Overlaps(x.SomeArray, x.SomeList)).ToList();
+ AssertContainsInSql("WHERE (x.\"SomeArray\" && x.\"SomeList\") = TRUE");
+ }
+ }
+
+ [Fact]
+ public void List_Overlaps_Array()
+ {
+ using (var ctx = Fixture.CreateContext())
+ {
+ var _ = ctx.SomeEntities.Where(x => EF.Functions.Overlaps(x.SomeList, x.SomeArray)).ToList();
+ AssertContainsInSql("WHERE (x.\"SomeList\" && x.\"SomeArray\") = TRUE");
+ }
+ }
+
+ [Fact]
+ public void List_Overlaps_List()
+ {
+ using (var ctx = Fixture.CreateContext())
+ {
+ var _ = ctx.SomeEntities.Where(x => EF.Functions.Overlaps(x.SomeList, x.SomeList)).ToList();
+ AssertContainsInSql("WHERE (x.\"SomeList\" && x.\"SomeList\") = TRUE");
+ }
+ }
+
+ #endregion
+
+ #region ||
+
+ [Fact]
+ public void Array_Concat_with_array_column()
+ {
+ using (var ctx = Fixture.CreateContext())
+ {
+ var _ = ctx.SomeEntities.Select(e => e.SomeArray.Concat(e.SomeArray)).ToList();
+ AssertContainsInSql("SELECT (e.\"SomeArray\" || e.\"SomeArray\")");
+ }
+ }
+
+ [Fact]
+ public void List_Concat_with_list_column()
+ {
+ using (var ctx = Fixture.CreateContext())
+ {
+ var _ = ctx.SomeEntities.Select(e => e.SomeList.Concat(e.SomeList)).ToList();
+ AssertContainsInSql("SELECT (e.\"SomeList\" || e.\"SomeList\")");
+ }
+ }
+
+ [Fact]
+ public void Array_Concat_with_list_column()
+ {
+ using (var ctx = Fixture.CreateContext())
+ {
+ var _ = ctx.SomeEntities.Select(e => e.SomeArray.Concat(e.SomeList)).ToList();
+ AssertContainsInSql("SELECT (e.\"SomeArray\" || e.\"SomeList\")");
+ }
+ }
+
+ [Fact]
+ public void List_Concat_with_array_column()
+ {
+ using (var ctx = Fixture.CreateContext())
+ {
+ var _ = ctx.SomeEntities.Select(e => e.SomeList.Concat(e.SomeArray)).ToList();
+ AssertContainsInSql("SELECT (e.\"SomeList\" || e.\"SomeArray\")");
+ }
+ }
+
+// .NET 4.6.1 doesn't include the Enumerable.Append and Enumerable.Prepend functions...
+#if !NET461
+ [Fact]
+
+ public void Array_Append_constant()
+ {
+ using (var ctx = Fixture.CreateContext())
+ {
+ var _ = ctx.SomeEntities.Select(e => e.SomeArray.Append(0)).ToList();
+ AssertContainsInSql("SELECT (e.\"SomeArray\" || 0)");
+ }
+ }
+
+ [Fact]
+
+ public void List_Append_constant()
+ {
+ using (var ctx = Fixture.CreateContext())
+ {
+ var _ = ctx.SomeEntities.Select(e => e.SomeList.Append(0)).ToList();
+ AssertContainsInSql("SELECT (e.\"SomeList\" || 0)");
+ }
+ }
+
+ [Fact]
+
+ public void Array_Prepend_constant()
+ {
+ using (var ctx = Fixture.CreateContext())
+ {
+ var _ = ctx.SomeEntities.Select(e => e.SomeArray.Prepend(0)).ToList();
+ AssertContainsInSql("SELECT (0 || e.\"SomeArray\")");
+ }
+ }
+
+ [Fact]
+
+ public void List_Prepend_constant()
+ {
+ using (var ctx = Fixture.CreateContext())
+ {
+ var _ = ctx.SomeEntities.Select(e => e.SomeList.Prepend(0)).ToList();
+ AssertContainsInSql("SELECT (0 || e.\"SomeList\")");
+ }
+ }
+
+#endif
+
+ #endregion
+
+ #region array_fill
+
+ [Fact]
+ public void Array_ArrayFill_constant()
+ {
+ using (var ctx = Fixture.CreateContext())
+ {
+ var _ = ctx.SomeEntities.Select(e => EF.Functions.ArrayFill(e.Id, new[] { 2 })).ToList();
+ AssertContainsInSql("SELECT array_fill(e.\"Id\", ARRAY[2]::integer[])");
+ }
+ }
+
+ [Fact]
+ public void List_ListFill_constant()
+ {
+ using (var ctx = Fixture.CreateContext())
+ {
+ var _ = ctx.SomeEntities.Select(e => EF.Functions.ListFill(e.Id, new[] { 3 })).ToList();
+ AssertContainsInSql("SELECT array_fill(e.\"Id\", ARRAY[3]::integer[])");
+ }
+ }
+
+ #endregion
+
+ #region array_dims
+
+ [Fact]
+ public void Array_ArrayDimensions_column()
+ {
+ using (var ctx = Fixture.CreateContext())
+ {
+ var _ = ctx.SomeEntities.Select(e => EF.Functions.ArrayDimensions(e.SomeArray)).ToList();
+ AssertContainsInSql("SELECT array_dims(e.\"SomeArray\")");
+ }
+ }
+
+ [Fact]
+ public void List_ArrayDimensions_column()
+ {
+ using (var ctx = Fixture.CreateContext())
+ {
+ var _ = ctx.SomeEntities.Select(e => EF.Functions.ArrayDimensions(e.SomeList)).ToList();
+ AssertContainsInSql("SELECT array_dims(e.\"SomeList\")");
+ }
+ }
+
+ #endregion
+
+ #region array_length
+
+ [Fact]
+ public void Array_Length()
+ {
+ using (var ctx = Fixture.CreateContext())
+ {
+ var _ = ctx.SomeEntities.Where(e => e.SomeArray.Length == 2).ToList();
+ AssertContainsInSql("WHERE COALESCE(array_length(e.\"SomeArray\", 1), 0) = 2");
+ }
+ }
+
+ [Fact]
+ public void List_Length()
+ {
+ using (var ctx = Fixture.CreateContext())
+ {
+ var _ = ctx.SomeEntities.Where(e => e.SomeList.Count == 0).ToList();
+ AssertContainsInSql("WHERE COALESCE(array_length(e.\"SomeList\", 1), 0) = 0");
+ }
+ }
+
+ [Fact]
+ public void Array_GetLength()
+ {
+ using (var ctx = Fixture.CreateContext())
+ {
+ var _ = ctx.SomeEntities.Single(e => e.SomeArray.GetLength(0) == 2);
+ AssertContainsInSql("WHERE COALESCE(array_length(e.\"SomeArray\", 1), 0) = 2");
+ }
+ }
+
+ [Fact]
+ public void Array_Count()
+ {
+ using (var ctx = Fixture.CreateContext())
+ {
+ // ReSharper disable once UseCollectionCountProperty
+ var _ = ctx.SomeEntities.Where(e => e.SomeArray.Count() == 1).ToArray();
+ AssertContainsInSql("WHERE COALESCE(array_length(e.\"SomeArray\", 1), 0) = 1");
+ }
+ }
+
+ [Fact]
+ public void List_Count()
+ {
+ using (var ctx = Fixture.CreateContext())
+ {
+ // ReSharper disable once UseCollectionCountProperty
+ var _ = ctx.SomeEntities.Where(e => e.SomeList.Count() == 1).ToArray();
+ AssertContainsInSql("WHERE COALESCE(array_length(e.\"SomeList\", 1), 0) = 1");
}
}
[Fact(Skip = "https://github.com/aspnet/EntityFramework/issues/9242")]
- public void Length_on_EF_Property()
+ public void Array_Length_on_EF_Property()
{
using (var ctx = Fixture.CreateContext())
{
// TODO: This fails
var x = ctx.SomeEntities.Single(e => EF.Property(e, nameof(SomeArrayEntity.SomeArray)).Length == 2);
Assert.Equal(new[] { 3, 4 }, x.SomeArray);
- AssertContainsInSql(@"WHERE array_length(e.""SomeArray"", 1) = 2");
+ AssertContainsInSql("WHERE array_length(e.\"SomeArray\", 1) = 2");
+ }
+ }
+
+ [Fact(Skip = "https://github.com/aspnet/EntityFramework/issues/9242")]
+ public void List_Length_on_EF_Property()
+ {
+ using (var ctx = Fixture.CreateContext())
+ {
+ // TODO: This fails
+ var x = ctx.SomeEntities.Single(e => EF.Property>(e, nameof(SomeArrayEntity.SomeList)).Count == 2);
+ Assert.Equal(new List { 3, 4 }, x.SomeList);
+ AssertContainsInSql("WHERE array_length(e.\"SomeList\", 1) = 2");
}
}
[Fact]
- public void Length_on_literal_not_translated()
+ public void Array_Length_on_literal_not_translated()
{
using (var ctx = Fixture.CreateContext())
{
var _ = ctx.SomeEntities.Where(e => new[] { 1, 2, 3 }.Length == e.Id).ToList();
+ AssertContainsInSql("WHERE 3 = e.\"Id\"");
+ AssertDoesNotContainInSql("array_length");
+ AssertDoesNotContainInSql("cardinality");
+ }
+ }
+
+ [Fact]
+ public void List_Length_on_literal_not_translated()
+ {
+ using (var ctx = Fixture.CreateContext())
+ {
+ var _ = ctx.SomeEntities.Where(e => new List { 1, 2, 3 }.Count == e.Id).ToList();
+ AssertContainsInSql("WHERE @__Count_0 = e.\"Id\"");
+ AssertDoesNotContainInSql("array_length");
+ AssertDoesNotContainInSql("cardinality");
+ }
+ }
+
+ [Fact]
+ public void Array_GetLength_on_literal_not_translated()
+ {
+ using (var ctx = Fixture.CreateContext())
+ {
+ var _ = ctx.SomeEntities.Where(e => new[] { 1, 2, 3 }.GetLength(0) == e.Id).ToList();
+ AssertContainsInSql("WHERE @__GetLength_0 = e.\"Id\"");
AssertDoesNotContainInSql("array_length");
+ AssertDoesNotContainInSql("cardinality");
+ }
+ }
+
+ #endregion
+
+ #region array_lower
+
+ [Fact]
+ public void Array_ArrayLower()
+ {
+ using (var ctx = Fixture.CreateContext())
+ {
+ var _ = ctx.SomeEntities.Where(e => e.SomeArray.GetLowerBound(0) == 2).ToList();
+ AssertContainsInSql("WHERE (COALESCE(array_lower(e.\"SomeArray\", 1), 0) - 1) = 2");
+ }
+ }
+
+ #endregion
+
+ #region array_ndims
+
+ [Fact]
+ public void Array_Rank()
+ {
+ using (var ctx = Fixture.CreateContext())
+ {
+ var _ = ctx.SomeEntities.Where(e => e.SomeArray.Rank == 2).ToList();
+ AssertContainsInSql("WHERE COALESCE(array_ndims(e.\"SomeArray\"), 1) = 2");
+ }
+ }
+
+ #endregion
+
+ #region array_position
+
+ [Theory]
+ [InlineData(9, 5)]
+ public void Array_IndexOf_constant(int major, int minor)
+ {
+ using (var ctx = Fixture.CreateContext(new Version(major, minor)))
+ {
+ var _ = ctx.SomeEntities.Select(e => Array.IndexOf(e.SomeArray, 0)).ToList();
+ AssertContainsInSql("SELECT COALESCE(array_position(e.\"SomeArray\", 0), 0) - 1");
+ }
+ }
+
+ [Theory]
+ [InlineData(9, 5)]
+ public void List_IndexOf_constant(int major, int minor)
+ {
+ using (var ctx = Fixture.CreateContext(new Version(major, minor)))
+ {
+ var _ = ctx.SomeEntities.Select(e => e.SomeList.IndexOf(0)).ToList();
+ AssertContainsInSql("SELECT COALESCE(array_position(e.\"SomeList\", 0), 0) - 1");
+ }
+ }
+
+ #endregion
+
+ #region array_positions
+
+ [Theory]
+ [InlineData(9, 5)]
+ public void Array_ArrayPositions_column(int major, int minor)
+ {
+ using (var ctx = Fixture.CreateContext(new Version(major, minor)))
+ {
+ var _ = ctx.SomeEntities.Select(e => EF.Functions.ArrayPositions(e.SomeArray, 0)).ToList();
+ AssertContainsInSql("SELECT array_positions(e.\"SomeArray\", 0)");
+ }
+ }
+
+ [Theory]
+ [InlineData(9, 5)]
+ public void List_ListPositions_column(int major, int minor)
+ {
+ using (var ctx = Fixture.CreateContext(new Version(major, minor)))
+ {
+ var _ = ctx.SomeEntities.Select(e => EF.Functions.ArrayPositions(e.SomeList, 0)).ToList();
+ AssertContainsInSql("SELECT array_positions(e.\"SomeList\", 0)");
+ }
+ }
+
+ #endregion
+
+ #region array_remove
+
+ [Fact]
+ public void Array_ArrayRemove_column()
+ {
+ using (var ctx = Fixture.CreateContext())
+ {
+ var _ = ctx.SomeEntities.Select(e => EF.Functions.ArrayRemove(e.SomeArray, 0)).ToList();
+ AssertContainsInSql("SELECT array_remove(e.\"SomeArray\", 0)");
+ }
+ }
+
+ [Fact]
+ public void List_ArrayRemove_column()
+ {
+ using (var ctx = Fixture.CreateContext())
+ {
+ var _ = ctx.SomeEntities.Select(e => EF.Functions.ArrayRemove(e.SomeList, 0)).ToList();
+ AssertContainsInSql("SELECT array_remove(e.\"SomeList\", 0)");
+ }
+ }
+
+ #endregion
+
+ #region array_replace
+
+ [Fact]
+ public void Array_ArrayReplace_column()
+ {
+ using (var ctx = Fixture.CreateContext())
+ {
+ var _ = ctx.SomeEntities.Select(e => EF.Functions.ArrayReplace(e.SomeArray, 0, 1)).ToList();
+ AssertContainsInSql("SELECT array_replace(e.\"SomeArray\", 0, 1)");
+ }
+ }
+
+ [Fact]
+ public void List_ArrayReplace_column()
+ {
+ using (var ctx = Fixture.CreateContext())
+ {
+ var _ = ctx.SomeEntities.Select(e => EF.Functions.ArrayReplace(e.SomeList, 0, 1)).ToList();
+ AssertContainsInSql("SELECT array_replace(e.\"SomeList\", 0, 1)");
+ }
+ }
+
+ #endregion
+
+ #region array_upper
+
+ [Fact]
+ public void Array_ArrayUpper()
+ {
+ using (var ctx = Fixture.CreateContext())
+ {
+ var _ = ctx.SomeEntities.Where(e => e.SomeArray.GetUpperBound(0) == 2).ToList();
+ AssertContainsInSql("WHERE (COALESCE(array_upper(e.\"SomeArray\", 1), 0) - 1) = 2");
}
}
#endregion
- #region AnyAll
+ #region array_to_string
[Fact]
- public void Array_like_any_when_match_expression_is_column()
+ public void Array_ArrayToString()
{
using (var ctx = Fixture.CreateContext())
{
- var patterns = new[] { "a", "b", "c" };
+ var _ = ctx.SomeEntities.Select(e => EF.Functions.ArrayToString(e.SomeArray, "*")).ToList();
+ AssertContainsInSql("SELECT array_to_string(e.\"SomeArray\", '*')");
+ }
+ }
- var anon =
- ctx.SomeEntities
- .Select(
- x => new
- {
- Array = x.SomeArray,
- List = x.SomeList,
- Text = x.SomeText
- });
+ [Fact]
+ public void List_ArrayToString()
+ {
+ using (var ctx = Fixture.CreateContext())
+ {
+ var _ = ctx.SomeEntities.Select(e => EF.Functions.ArrayToString(e.SomeList, "*")).ToList();
+ AssertContainsInSql("SELECT array_to_string(e.\"SomeList\", '*')");
+ }
+ }
- var _ = anon.Where(x => patterns.Any(p => EF.Functions.Like(x.Text, p))).ToList();
+ [Fact]
+ public void Array_ArrayToString_with_null()
+ {
+ using (var ctx = Fixture.CreateContext())
+ {
+ var _ = ctx.SomeEntities.Select(e => EF.Functions.ArrayToString(e.SomeArray, "*", ";")).ToList();
+ AssertContainsInSql("SELECT array_to_string(e.\"SomeArray\", '*', ';')");
+ }
+ }
- AssertContainsInSql("x.\"SomeText\" LIKE ANY (@__patterns_0) = TRUE");
+ [Fact]
+ public void List_ArrayToString_with_null()
+ {
+ using (var ctx = Fixture.CreateContext())
+ {
+ var _ = ctx.SomeEntities.Select(e => EF.Functions.ArrayToString(e.SomeList, "*", ";")).ToList();
+ AssertContainsInSql("SELECT array_to_string(e.\"SomeList\", '*', ';')");
}
}
+ #endregion
+
+ #region string_to_array
+
[Fact]
- public void Array_like_any_not_translated_when_match_expression_is_qsre()
+ public void Array_StringToArray()
{
using (var ctx = Fixture.CreateContext())
{
- var matches = new[] { "a", "b", "c" };
+ var _ = ctx.SomeEntities.Where(e => EF.Functions.StringToArray(e.SomeText, "*") != null).ToList();
+ AssertContainsInSql("WHERE string_to_array(e.\"SomeText\", '*') IS NOT NULL");
+ }
+ }
- var anon =
- ctx.SomeEntities
- .Select(
- x => new
- {
- Array = x.SomeArray,
- List = x.SomeList,
- Text = x.SomeText
- });
+ [Fact]
+ public void List_StringToList()
+ {
+ using (var ctx = Fixture.CreateContext())
+ {
+ var _ = ctx.SomeEntities.Where(e => EF.Functions.StringToList(e.SomeText, "*") != null).ToList();
+ AssertContainsInSql("WHERE string_to_array(e.\"SomeText\", '*') IS NOT NULL");
+ }
+ }
- var _ = anon.Where(x => matches.Any(m => EF.Functions.Like(m, x.Text))).ToList();
+ [Fact]
+ public void Array_StringToArray_with_null_string()
+ {
+ using (var ctx = Fixture.CreateContext())
+ {
+ var _ = ctx.SomeEntities.Where(e => EF.Functions.StringToArray(e.SomeText, "*", ";") != null).ToList();
+ AssertContainsInSql("WHERE string_to_array(e.\"SomeText\", '*', ';') IS NOT NULL");
+ }
+ }
- AssertDoesNotContainInSql("LIKE");
- AssertDoesNotContainInSql("ANY");
- AssertDoesNotContainInSql("@__matches_0");
+ [Fact]
+ public void List_StringToList_with_null_string()
+ {
+ using (var ctx = Fixture.CreateContext())
+ {
+ var _ = ctx.SomeEntities.Where(e => EF.Functions.StringToList(e.SomeText, "*", ";") != null).ToList();
+ AssertContainsInSql("WHERE string_to_array(e.\"SomeText\", '*', ';') IS NOT NULL");
}
}
diff --git a/test/EFCore.PG.FunctionalTests/Query/StringQueryTest.cs b/test/EFCore.PG.FunctionalTests/Query/StringQueryTest.cs
new file mode 100644
index 000000000..13feae84e
--- /dev/null
+++ b/test/EFCore.PG.FunctionalTests/Query/StringQueryTest.cs
@@ -0,0 +1,72 @@
+using System.Linq;
+using Microsoft.EntityFrameworkCore.TestUtilities;
+using Xunit;
+
+namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query
+{
+ public class StringQueryTest : IClassFixture>
+ {
+ #region Indexer
+
+ [Fact]
+ public void String_Index_text_with_constant_char_as_int()
+ {
+ using (var ctx = Fixture.CreateContext())
+ {
+ var _ = ctx.Customers.Where(e => e.Address[0] == 'T').ToList();
+ AssertContainsInSql("WHERE ascii(substr(e.\"Address\", 1, 1)) = 84");
+ }
+ }
+
+ [Fact]
+ public void String_Index_text_with_constant_string()
+ {
+ using (var ctx = Fixture.CreateContext())
+ {
+ var _ = ctx.Customers.Where(e => e.Address[0].ToString() == "T").ToList();
+ AssertContainsInSql("WHERE CAST(substr(e.\"Address\", 1, 1) AS text) = 'T'");
+ }
+ }
+
+ [Fact]
+ public void String_Index_text_with_non_constant_char_as_int()
+ {
+ using (var ctx = Fixture.CreateContext())
+ {
+ // ReSharper disable once ConvertToConstant.Local
+ var x = 0;
+ var _ = ctx.Customers.Where(e => e.Address[x] == 'T').ToList();
+ AssertContainsInSql("WHERE ascii(substr(e.\"Address\", @__x_0 + 1, 1)) = 84");
+ }
+ }
+
+ [Fact]
+ public void String_Index_text_with_non_constant_string()
+ {
+ using (var ctx = Fixture.CreateContext())
+ {
+ // ReSharper disable once ConvertToConstant.Local
+ var x = 0;
+ var _ = ctx.Customers.Where(e => e.Address[x].ToString() == "T").ToList();
+ AssertContainsInSql("WHERE CAST(substr(e.\"Address\", @__x_0 + 1, 1) AS text) = 'T'");
+ }
+ }
+
+ #endregion
+
+ #region Support
+
+ NorthwindQueryNpgsqlFixture Fixture { get; }
+
+ public StringQueryTest(NorthwindQueryNpgsqlFixture fixture)
+ {
+ Fixture = fixture;
+ Fixture.TestSqlLoggerFactory.Clear();
+ }
+
+ void AssertContainsInSql(string expected)
+ => Assert.Contains(expected, Fixture.TestSqlLoggerFactory.Sql);
+
+ #endregion
+ }
+}