From 6f21661f170e13982b6bd49e915179c4d2398289 Mon Sep 17 00:00:00 2001 From: Austin Drenski Date: Sat, 29 Sep 2018 21:33:21 -0400 Subject: [PATCH 1/4] Map array and list operators --- .../Extensions/NpgsqlArrayExtensions.cs | 768 ++++++++++++ .../Internal/NpgsqlArrayFragmentTranslator.cs | 232 ++++ .../Internal/NpgsqlArrayMemberTranslator.cs | 140 +++ .../NpgsqlArrayMethodCallTranslator.cs | 408 +++++++ .../NpgsqlArraySequenceEqualTranslator.cs | 38 - ...qlCompositeExpressionFragmentTranslator.cs | 4 +- .../NpgsqlCompositeMemberTranslator.cs | 10 +- .../NpgsqlCompositeMethodCallTranslator.cs | 2 +- .../NpgsqlSqlTranslatingExpressionVisitor.cs | 221 ++-- .../Sql/Internal/NpgsqlQuerySqlGenerator.cs | 43 +- .../Query/ArrayQueryTest.cs | 1080 +++++++++++++++-- 11 files changed, 2688 insertions(+), 258 deletions(-) create mode 100644 src/EFCore.PG/Extensions/NpgsqlArrayExtensions.cs create mode 100644 src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayFragmentTranslator.cs create mode 100644 src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayMemberTranslator.cs create mode 100644 src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayMethodCallTranslator.cs delete mode 100644 src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArraySequenceEqualTranslator.cs diff --git a/src/EFCore.PG/Extensions/NpgsqlArrayExtensions.cs b/src/EFCore.PG/Extensions/NpgsqlArrayExtensions.cs new file mode 100644 index 000000000..28a7dd8ea --- /dev/null +++ b/src/EFCore.PG/Extensions/NpgsqlArrayExtensions.cs @@ -0,0 +1,768 @@ +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 T[,] MatrixFill( + [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 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 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] 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] 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..a931e0fb4 --- /dev/null +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayMemberTranslator.cs @@ -0,0 +1,140 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Query.Expressions; +using Microsoft.EntityFrameworkCore.Query.ExpressionTranslators; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Internal +{ + /// + /// 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 expression) + => ArrayInstanceHandler(expression) ?? ListInstanceHandler(expression); + + #region Handlers + + /// + /// Translates instance members defined on . + /// + /// The expression to translate. + /// + /// A translated expression or null if no translation is supported. + /// + [CanBeNull] + Expression ArrayInstanceHandler([NotNull] MemberExpression expression) + { + var instance = expression.Expression; + + if (instance is null || !instance.Type.IsArray) + return null; + + switch (expression.Member.Name) + { + case nameof(Array.Length) when VersionAtLeast(9, 4): + return new SqlFunctionExpression("cardinality", typeof(int), new[] { instance }); + + case nameof(Array.Length) when VersionAtLeast(8, 4) && instance.Type.GetArrayRank() == 1: + return + Expression.Coalesce( + new SqlFunctionExpression( + "array_length", + typeof(int?), + new[] { instance, Expression.Constant(1) }), + Expression.Constant(0)); + + case nameof(Array.Length) when VersionAtLeast(8, 4): + return + Enumerable.Range(1, instance.Type.GetArrayRank()) + .Select(x => + Expression.Coalesce( + new SqlFunctionExpression( + "array_length", + typeof(int?), + new[] { instance, Expression.Constant(x) }), + Expression.Constant(0))) + .Cast() + .Aggregate(Expression.Multiply); + + case nameof(Array.Rank) when VersionAtLeast(8, 4): + return + Expression.Coalesce( + new SqlFunctionExpression( + "array_ndims", + typeof(int?), + new[] { instance }), + Expression.Constant(1)); + + default: + return null; + } + } + + /// + /// Translates instance members defined on . + /// + /// The expression to translate. + /// + /// A translated expression or null if no translation is supported. + /// + [CanBeNull] + Expression ListInstanceHandler([NotNull] MemberExpression expression) + { + var instance = expression.Expression; + + if (instance is null || !instance.Type.IsGenericType || instance.Type.GetGenericTypeDefinition() != typeof(List<>)) + return null; + + switch (expression.Member.Name) + { + case nameof(IList.Count) when VersionAtLeast(8, 4): + return + Expression.Coalesce( + new SqlFunctionExpression( + "array_length", + typeof(int?), + new Expression[] { instance, Expression.Constant(1) }), + Expression.Constant(0)); + + default: + return null; + } + } + + #endregion + + #region Helpers + + /// + /// 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..fb4f8cbbe --- /dev/null +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayMethodCallTranslator.cs @@ -0,0 +1,408 @@ +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 expression) + => EnumerableHandler(expression) ?? + NpgsqlArrayExtensionsHandler(expression) ?? + ArrayStaticHandler(expression) ?? + ArrayInstanceHandler(expression) ?? + StringInstanceHandler(expression) ?? + ListInstanceHandler(expression); + + #region Handlers + + /// + /// Translates methods defined on . + /// + /// The expression to translate. + /// + /// A translated expression or null if no translation is supported. + /// + [CanBeNull] + Expression EnumerableHandler([NotNull] MethodCallExpression expression) + { + if (expression.Method.DeclaringType != typeof(Enumerable)) + return null; + + var type = expression.Arguments[0].Type; + + if (!type.IsArray && type != typeof(string) && (!type.IsGenericType || type.GetGenericTypeDefinition() != typeof(List<>))) + return null; + + if (!VersionAtLeast(7, 4)) + return null; + + switch (expression.Method.Name) + { + case nameof(Enumerable.Count) when type == typeof(string): + return + Expression.Coalesce( + new SqlFunctionExpression( + "character_length", + typeof(int?), + new[] { expression.Arguments[0] }), + Expression.Constant(0)); + + case nameof(Enumerable.Count): + return VersionAtLeast(8, 4) + ? Expression.Coalesce( + new SqlFunctionExpression( + "array_length", + typeof(int?), + new[] { expression.Arguments[0], Expression.Constant(1) }), + Expression.Constant(0)) + : null; + + case nameof(Enumerable.ElementAt): + return MakeIndex(expression.Arguments[0], expression.Arguments[1]); + + case nameof(Enumerable.Append): + return new CustomBinaryExpression(expression.Arguments[0], expression.Arguments[1], "||", expression.Arguments[0].Type); + + case nameof(Enumerable.Prepend): + return new CustomBinaryExpression(expression.Arguments[1], expression.Arguments[0], "||", expression.Arguments[0].Type); + + case nameof(Enumerable.SequenceEqual): + return Expression.MakeBinary(ExpressionType.Equal, expression.Arguments[0], expression.Arguments[1]); + + default: + return null; + } + } + + /// + /// Translates methods defined on . + /// + /// The expression to translate. + /// + /// A translated expression or null if no translation is supported. + /// + /// + /// This handler throws on unsupported versions since + /// does not support client-side evaluation. + /// + [CanBeNull] + Expression NpgsqlArrayExtensionsHandler([NotNull] MethodCallExpression expression) + { + if (expression.Method.DeclaringType != typeof(NpgsqlArrayExtensions)) + return null; + + switch (expression.Method.Name) + { + case nameof(NpgsqlArrayExtensions.Contains): + return VersionAtLeast(8, 2) + ? new CustomBinaryExpression(expression.Arguments[1], expression.Arguments[2], "@>", typeof(bool)) + : throw VersionNotSupportedException(nameof(NpgsqlArrayExtensions.Contains), 8, 2); + + case nameof(NpgsqlArrayExtensions.ContainedBy): + return VersionAtLeast(8, 2) + ? new CustomBinaryExpression(expression.Arguments[1], expression.Arguments[2], "<@", typeof(bool)) + : throw VersionNotSupportedException(nameof(NpgsqlArrayExtensions.ContainedBy), 8, 2); + + case nameof(NpgsqlArrayExtensions.Overlaps): + return VersionAtLeast(8, 2) + ? new CustomBinaryExpression(expression.Arguments[1], expression.Arguments[2], "&&", typeof(bool)) + : throw VersionNotSupportedException(nameof(NpgsqlArrayExtensions.Overlaps), 8, 2); + + case nameof(NpgsqlArrayExtensions.ArrayFill): + return VersionAtLeast(8, 4) + ? new SqlFunctionExpression("array_fill", expression.Method.ReturnType, expression.Arguments.Skip(1)) + : throw VersionNotSupportedException(nameof(NpgsqlArrayExtensions.ArrayFill), 8, 4); + + case nameof(NpgsqlArrayExtensions.ListFill): + return VersionAtLeast(8, 4) + ? new SqlFunctionExpression("array_fill", expression.Method.ReturnType, expression.Arguments.Skip(1)) + : throw VersionNotSupportedException(nameof(NpgsqlArrayExtensions.ListFill), 8, 4); + + case nameof(NpgsqlArrayExtensions.MatrixFill): + return VersionAtLeast(8, 4) + ? new SqlFunctionExpression("array_fill", expression.Method.ReturnType, expression.Arguments.Skip(1)) + : throw VersionNotSupportedException(nameof(NpgsqlArrayExtensions.MatrixFill), 8, 4); + + case nameof(NpgsqlArrayExtensions.ArrayDimensions): + return VersionAtLeast(7, 4) + ? new SqlFunctionExpression("array_dims", expression.Method.ReturnType, expression.Arguments.Skip(1).Take(1)) + : throw VersionNotSupportedException(nameof(NpgsqlArrayExtensions.ArrayDimensions), 7, 4); + + case nameof(NpgsqlArrayExtensions.ArrayPositions): + return VersionAtLeast(9, 5) + ? new SqlFunctionExpression("array_positions", expression.Method.ReturnType, expression.Arguments.Skip(1)) + : throw VersionNotSupportedException(nameof(NpgsqlArrayExtensions.ArrayPositions), 9, 5); + + case nameof(NpgsqlArrayExtensions.ArrayRemove): + return VersionAtLeast(9, 3) + ? new SqlFunctionExpression("array_remove", expression.Method.ReturnType, expression.Arguments.Skip(1).Take(2)) + : throw VersionNotSupportedException(nameof(NpgsqlArrayExtensions.ArrayRemove), 9, 3); + + case nameof(NpgsqlArrayExtensions.ArrayReplace): + return VersionAtLeast(9, 3) + ? new SqlFunctionExpression("array_replace", expression.Method.ReturnType, expression.Arguments.Skip(1).Take(3)) + : throw VersionNotSupportedException(nameof(NpgsqlArrayExtensions.ArrayReplace), 9, 3); + + case nameof(NpgsqlArrayExtensions.ArrayToString): + return VersionAtLeast(9, 1) + ? new SqlFunctionExpression("array_to_string", expression.Method.ReturnType, expression.Arguments.Skip(1).Take(3)) + : VersionAtLeast(7, 4) + ? new SqlFunctionExpression("array_to_string", expression.Method.ReturnType, expression.Arguments.Skip(1).Take(2)) + : throw VersionNotSupportedException(nameof(NpgsqlArrayExtensions.ArrayToString), 7, 4); + + case nameof(NpgsqlArrayExtensions.StringToArray): + return VersionAtLeast(9, 1) + ? new SqlFunctionExpression("string_to_array", expression.Method.ReturnType, expression.Arguments.Skip(1).Take(3)) + : VersionAtLeast(7, 4) + ? new SqlFunctionExpression("string_to_array", expression.Method.ReturnType, expression.Arguments.Skip(1).Take(2)) + : throw VersionNotSupportedException(nameof(NpgsqlArrayExtensions.StringToArray), 7, 4); + + case nameof(NpgsqlArrayExtensions.StringToList): + return VersionAtLeast(9, 1) + ? new SqlFunctionExpression("string_to_array", expression.Method.ReturnType, expression.Arguments.Skip(1).Take(3)) + : VersionAtLeast(7, 4) + ? new SqlFunctionExpression("string_to_array", expression.Method.ReturnType, expression.Arguments.Skip(1).Take(2)) + : throw VersionNotSupportedException(nameof(NpgsqlArrayExtensions.StringToList), 7, 4); + + default: + return null; + } + + NotSupportedException VersionNotSupportedException(string name, int major, int minor) + => new NotSupportedException($"{nameof(NpgsqlArrayExtensions)}.{name} is not supported before PostgreSQL {major}.{minor}."); + } + + /// + /// Translates static methods defined on . + /// + /// The expression to translate. + /// + /// A translated expression or null if no translation is supported. + /// + [CanBeNull] + Expression ArrayStaticHandler([NotNull] MethodCallExpression expression) + { + if (!expression.Method.IsStatic || expression.Method.DeclaringType != typeof(Array)) + return null; + + switch (expression.Method.Name) + { + case nameof(Array.IndexOf) when VersionAtLeast(9, 5) && + expression.Arguments[0].Type.GetArrayRank() == 1 && + expression.Method.GetParameters().Length <= 3: + return + Expression.Subtract( + Expression.Coalesce( + new SqlFunctionExpression( + "array_position", + typeof(int?), + expression.Arguments.Take(3)), + Expression.Constant(0)), + Expression.Constant(1)); + + default: + return null; + } + } + + /// + /// Translates instance methods defined on . + /// + /// The expression to translate. + /// + /// A translated expression or null if no translation is supported. + /// + [CanBeNull] + Expression ArrayInstanceHandler([NotNull] MethodCallExpression expression) + { + var instance = expression.Object; + + if (instance == null || !instance.Type.IsArray) + return null; + + switch (expression.Method.Name) + { + case nameof(Array.GetLength) when VersionAtLeast(8, 4): + return + Expression.Coalesce( + new SqlFunctionExpression( + "array_length", + typeof(int?), + new[] { instance, GenerateOneBasedIndexExpression(expression.Arguments[0]) }), + Expression.Constant(0)); + + case nameof(Array.GetLowerBound) when VersionAtLeast(7, 4): + return + Expression.Subtract( + Expression.Coalesce( + new SqlFunctionExpression( + "array_lower", + typeof(int?), + new[] { instance, GenerateOneBasedIndexExpression(expression.Arguments[0]) }), + Expression.Constant(0)), + Expression.Constant(1)); + + case nameof(Array.GetUpperBound) when VersionAtLeast(7, 4): + return + Expression.Subtract( + Expression.Coalesce( + new SqlFunctionExpression( + "array_upper", + typeof(int?), + new[] { instance, GenerateOneBasedIndexExpression(expression.Arguments[0]) }), + Expression.Constant(0)), + Expression.Constant(1)); + + default: + return null; + } + } + + /// + /// Translates instance methods defined on . + /// + /// The expression to translate. + /// + /// A translated expression or null if no translation is supported. + /// + [CanBeNull] + Expression ListInstanceHandler([NotNull] MethodCallExpression expression) + { + var instance = expression.Object; + + if (instance == null || !instance.Type.IsGenericType || instance.Type.GetGenericTypeDefinition() != typeof(List<>)) + return null; + + switch (expression.Method.Name) + { + case "get_Item": + return MakeIndex(instance, expression.Arguments[0]); + + case nameof(IList.IndexOf) when VersionAtLeast(9, 5): + return + Expression.Subtract( + Expression.Coalesce( + new SqlFunctionExpression( + "array_position", + typeof(int?), + new[] { instance, expression.Arguments[0] }), + Expression.Constant(0)), + Expression.Constant(1)); + + default: + return null; + } + } + + /// + /// Translates instance methods defined on . + /// + /// The expression to translate. + /// + /// A translated expression or null if no translation is supported. + /// + [CanBeNull] + static Expression StringInstanceHandler([NotNull] MethodCallExpression expression) + { + var instance = expression.Object; + + if (instance == null || instance.Type != typeof(string)) + return null; + + switch (expression.Method.Name) + { + case "get_Chars": + return MakeIndex(instance, expression.Arguments[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; + + /// + /// Creates an expression of type or + /// from the and the . + /// + /// The instance to index. + /// The index value. + /// + /// An or . + /// + [NotNull] + static Expression MakeIndex([NotNull] Expression instance, [NotNull] Expression index) + { + if (instance.Type.IsArray) + { + return instance.Type.GetArrayRank() == 1 + ? (Expression)Expression.ArrayIndex(instance, index) + : Expression.ArrayAccess(instance, index); + } + + return 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 }); + } + + /// + /// 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)); + + #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..d4cee16c5 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlCompositeMemberTranslator.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlCompositeMemberTranslator.cs @@ -26,8 +26,16 @@ public NpgsqlCompositeMemberTranslator( [NotNull] INpgsqlOptions npgsqlOptions) : base(dependencies) { - // ReSharper disable once VirtualMemberCallInConstructor + var versionDependentTranslators = new IMemberTranslator[] + { + new NpgsqlArrayMemberTranslator(npgsqlOptions.PostgresVersion) + }; + + // ReSharper disable once DoNotCallOverridableMethodsInConstructor AddTranslators(MemberTranslators); + + // ReSharper disable once DoNotCallOverridableMethodsInConstructor + AddTranslators(versionDependentTranslators); } /// diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlCompositeMethodCallTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlCompositeMethodCallTranslator.cs index 9d55d6d2e..ba3729126 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,6 +36,7 @@ public NpgsqlCompositeMethodCallTranslator( { var versionDependentTranslators = new IMethodCallTranslator[] { + new NpgsqlArrayMethodCallTranslator(npgsqlOptions.PostgresVersion), new NpgsqlDateAddTranslator(npgsqlOptions.PostgresVersion), new NpgsqlMathTranslator(npgsqlOptions.PostgresVersion) }; diff --git a/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlSqlTranslatingExpressionVisitor.cs b/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlSqlTranslatingExpressionVisitor.cs index fe8d4530b..e7c5e4826 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 Context => _queryModelVisitor.QueryCompilationContext; /// public NpgsqlSqlTranslatingExpressionVisitor( @@ -64,29 +38,24 @@ 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) { - if (expression.NodeType == ExpressionType.ArrayIndex) + var left = Visit(expression.Left); + var right = Visit(expression.Right); + + if (left == null || right == null) + return base.VisitBinary(expression); + + if (MemberAccessBindingExpressionVisitor.GetPropertyPath(expression.Left, Context, out _).Count != 0) { - 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); - } + if (expression.NodeType == ExpressionType.ArrayIndex) + return Expression.ArrayIndex(left, right); + + if (expression.NodeType == ExpressionType.Index) + return Expression.ArrayAccess(left, right); } return base.VisitBinary(expression); @@ -100,105 +69,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..3937d14ad 100644 --- a/src/EFCore.PG/Query/Sql/Internal/NpgsqlQuerySqlGenerator.cs +++ b/src/EFCore.PG/Query/Sql/Internal/NpgsqlQuerySqlGenerator.cs @@ -202,14 +202,45 @@ protected override Expression VisitUnary(UnaryExpression expression) return base.VisitUnary(expression); } + /// + protected override Expression VisitIndex(IndexExpression expression) + { + // text cannot be subscripted. + if (expression.Object.Type == typeof(string)) + { + return VisitSqlFunction( + new SqlFunctionExpression( + "substr", + typeof(char), + new[] { expression.Object, GenerateOneBasedIndexExpression(expression.Arguments[0]), Expression.Constant(1) })); + } + + Visit(expression.Object); + for (int i = 0; i < expression.Arguments.Count; i++) + { + Sql.Append('['); + Visit(GenerateOneBasedIndexExpression(expression.Arguments[i])); + Sql.Append(']'); + } + + return expression; + } + + /// + /// 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 +248,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('['); diff --git a/test/EFCore.PG.FunctionalTests/Query/ArrayQueryTest.cs b/test/EFCore.PG.FunctionalTests/Query/ArrayQueryTest.cs index 41863ba5b..b9ad8cc3c 100644 --- a/test/EFCore.PG.FunctionalTests/Query/ArrayQueryTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/ArrayQueryTest.cs @@ -12,12 +12,25 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query { public class ArrayQueryTest : IClassFixture { + // TODO: add multidimensional tests throughout + #region ArrayTests #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 +45,227 @@ 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 String_Index_text_with_constant_char_as_int() + { + using (var ctx = Fixture.CreateContext()) + { + var _ = ctx.SomeEntities.Where(e => e.SomeText[0] == 'T').ToList(); + AssertContainsInSql("WHERE ascii(substr(e.\"SomeText\", 1, 1)) = 84"); + } + } + + [Fact] + public void String_Index_text_with_constant_string() + { + using (var ctx = Fixture.CreateContext()) + { + var _ = ctx.SomeEntities.Where(e => e.SomeText[0].ToString() == "T").ToList(); + AssertContainsInSql("WHERE CAST(substr(e.\"SomeText\", 1, 1) AS text) = 'T'"); + } + } + + [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 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 String_ElementAt_text_with_constant_char_as_int() + { + using (var ctx = Fixture.CreateContext()) + { + var _ = ctx.SomeEntities.Where(e => e.SomeText.ElementAt(0) == 'T').ToList(); + AssertContainsInSql("WHERE ascii(substr(e.\"SomeText\", 1, 1)) = 84"); + } + } + + [Fact] + public void String_ElementAt_text_with_constant_string() + { + using (var ctx = Fixture.CreateContext()) + { + var _ = ctx.SomeEntities.Where(e => e.SomeText.ElementAt(0).ToString() == "T").ToList(); + AssertContainsInSql("WHERE CAST(substr(e.\"SomeText\", 1, 1) AS text) = 'T'"); } } [Fact] - public void Index_with_non_constant() + 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 String_Index_text_with_non_constant_char_as_int() + { + using (var ctx = Fixture.CreateContext()) + { + // ReSharper disable once ConvertToConstant.Local + var x = 0; + var _ = ctx.SomeEntities.Where(e => e.SomeText[x] == 'T').ToList(); + AssertContainsInSql("WHERE ascii(substr(e.\"SomeText\", @__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.SomeEntities.Where(e => e.SomeText[x].ToString() == "T").ToList(); + AssertContainsInSql("WHERE CAST(substr(e.\"SomeText\", @__x_0 + 1, 1) AS text) = 'T'"); + } + } + + [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 String_ElementAt_text_with_non_constant_char_as_int() + { + using (var ctx = Fixture.CreateContext()) + { + // ReSharper disable once ConvertToConstant.Local + var x = 0; + var _ = ctx.SomeEntities.Where(e => e.SomeText.ElementAt(x) == 'T').ToList(); + AssertContainsInSql("WHERE ascii(substr(e.\"SomeText\", @__x_0 + 1, 1)) = 84"); + } + } + + [Fact] + public void String_ElementAt_text_with_non_constant_sting() + { + using (var ctx = Fixture.CreateContext()) + { + // ReSharper disable once ConvertToConstant.Local + var x = 0; + var _ = ctx.SomeEntities.Where(e => e.SomeText.ElementAt(x).ToString() == "T").ToList(); + AssertContainsInSql("WHERE CAST(substr(e.\"SomeText\", @__x_0 + 1, 1) AS text) = 'T'"); } } [Fact] - public void Index_multidimensional() + public void Array_Index_multidimensional() { using (var ctx = Fixture.CreateContext()) { @@ -82,153 +280,863 @@ 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"); } } - #endregion - - #region Containment + [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 Contains_with_literal() + public void List_SequenceEqual_with_parameter() { 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 list = new List { 3, 4 }; + var _ = ctx.SomeEntities.Where(e => e.SomeList.SequenceEqual(list)).ToList(); + AssertContainsInSql("WHERE e.\"SomeList\" = @__list_0"); } } [Fact] - public void Contains_with_parameter() + public void Array_SequenceEqual_with_literal() { 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.SequenceEqual(new[] { 3, 4 })).ToList(); + AssertContainsInSql("WHERE e.\"SomeArray\" = ARRAY[3,4]::integer"); } } [Fact] - public void Contains_with_column() + public void List_SequenceEqual_with_literal() { 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"")"); + var _ = ctx.SomeEntities.Where(e => e.SomeList.SequenceEqual(new List { 3, 4 })).ToList(); + AssertContainsInSql("WHERE e.\"SomeList\" = ARRAY[3,4]"); } } #endregion - #region Length + #region value = ANY (array) [Fact] - public void Length() + public void Array_Contains_with_literal() { 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(e => e.SomeArray.Contains(3)).ToList(); + AssertContainsInSql("WHERE 3 = ANY (e.\"SomeArray\")"); } } - [Fact(Skip = "https://github.com/aspnet/EntityFramework/issues/9242")] - public void Length_on_EF_Property() + [Fact] + public void List_Contains_with_literal() { 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"); + var _ = ctx.SomeEntities.Where(e => e.SomeList.Contains(3)).ToList(); + AssertContainsInSql("WHERE 3 = ANY (e.\"SomeList\")"); } } [Fact] - public void Length_on_literal_not_translated() + public void Array_Contains_with_parameter() { using (var ctx = Fixture.CreateContext()) { - var _ = ctx.SomeEntities.Where(e => new[] { 1, 2, 3 }.Length == e.Id).ToList(); - AssertDoesNotContainInSql("array_length"); + // ReSharper disable once ConvertToConstant.Local + var p = 3; + var _ = ctx.SomeEntities.Where(e => e.SomeArray.Contains(p)).ToList(); + AssertContainsInSql("WHERE @__p_0 = ANY (e.\"SomeArray\")"); } } - #endregion + [Fact] + public void List_Contains_with_parameter() + { + using (var ctx = Fixture.CreateContext()) + { + // 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\")"); + } + } - #region AnyAll + [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 Array_like_any_when_match_expression_is_column() + public void List_Contains_with_column() { using (var ctx = Fixture.CreateContext()) { - var patterns = new[] { "a", "b", "c" }; + var _ = ctx.SomeEntities.Where(e => e.SomeList.Contains(e.Id + 2)).ToList(); + AssertContainsInSql("WHERE e.\"Id\" + 2 = ANY (e.\"SomeList\")"); + } + } + + #endregion + + #region @> + + [Theory] + [InlineData(8, 2)] + public void Array_Contains_Array(int major, int minor) + { + using (var ctx = Fixture.CreateContext(new Version(major, minor))) + { + var _ = ctx.SomeEntities.Where(x => EF.Functions.Contains(x.SomeArray, x.SomeArray)).ToList(); + AssertContainsInSql("WHERE (x.\"SomeArray\" @> x.\"SomeArray\") = TRUE"); + } + } + + [Theory] + [InlineData(8, 2)] + public void Array_Contains_List(int major, int minor) + { + using (var ctx = Fixture.CreateContext(new Version(major, minor))) + { + var _ = ctx.SomeEntities.Where(x => EF.Functions.Contains(x.SomeArray, x.SomeList)).ToList(); + AssertContainsInSql("WHERE (x.\"SomeArray\" @> x.\"SomeList\") = TRUE"); + } + } + + [Theory] + [InlineData(8, 2)] + public void List_Contains_Array(int major, int minor) + { + using (var ctx = Fixture.CreateContext(new Version(major, minor))) + { + var _ = ctx.SomeEntities.Where(x => EF.Functions.Contains(x.SomeList, x.SomeArray)).ToList(); + AssertContainsInSql("WHERE (x.\"SomeList\" @> x.\"SomeArray\") = TRUE"); + } + } + + [Theory] + [InlineData(8, 2)] + public void List_Contains_List(int major, int minor) + { + using (var ctx = Fixture.CreateContext(new Version(major, minor))) + { + var _ = ctx.SomeEntities.Where(x => EF.Functions.Contains(x.SomeList, x.SomeList)).ToList(); + AssertContainsInSql("WHERE (x.\"SomeList\" @> x.\"SomeList\") = TRUE"); + } + } - var anon = - ctx.SomeEntities - .Select( - x => new - { - Array = x.SomeArray, - List = x.SomeList, - Text = x.SomeText - }); + #endregion - var _ = anon.Where(x => patterns.Any(p => EF.Functions.Like(x.Text, p))).ToList(); + #region <@ - AssertContainsInSql("x.\"SomeText\" LIKE ANY (@__patterns_0) = TRUE"); + [Theory] + [InlineData(8, 2)] + public void Array_ContainedBy_Array(int major, int minor) + { + using (var ctx = Fixture.CreateContext(new Version(major, minor))) + { + var _ = ctx.SomeEntities.Where(x => EF.Functions.ContainedBy(x.SomeArray, x.SomeArray)).ToList(); + AssertContainsInSql("WHERE (x.\"SomeArray\" <@ x.\"SomeArray\") = TRUE"); } } - [Fact] - public void Array_like_any_not_translated_when_match_expression_is_qsre() + [Theory] + [InlineData(8, 2)] + public void Array_ContainedBy_List(int major, int minor) { - using (var ctx = Fixture.CreateContext()) + using (var ctx = Fixture.CreateContext(new Version(major, minor))) + { + var _ = ctx.SomeEntities.Where(x => EF.Functions.ContainedBy(x.SomeArray, x.SomeList)).ToList(); + AssertContainsInSql("WHERE (x.\"SomeArray\" <@ x.\"SomeList\") = TRUE"); + } + } + + [Theory] + [InlineData(8, 2)] + public void List_ContainedBy_Array(int major, int minor) + { + using (var ctx = Fixture.CreateContext(new Version(major, minor))) + { + var _ = ctx.SomeEntities.Where(x => EF.Functions.ContainedBy(x.SomeList, x.SomeArray)).ToList(); + AssertContainsInSql("WHERE (x.\"SomeList\" <@ x.\"SomeArray\") = TRUE"); + } + } + + [Theory] + [InlineData(8, 2)] + public void List_ContainedBy_List(int major, int minor) + { + using (var ctx = Fixture.CreateContext(new Version(major, minor))) + { + var _ = ctx.SomeEntities.Where(x => EF.Functions.ContainedBy(x.SomeList, x.SomeList)).ToList(); + AssertContainsInSql("WHERE (x.\"SomeList\" <@ x.\"SomeList\") = TRUE"); + } + } + + #endregion + + #region && + + [Theory] + [InlineData(8, 2)] + public void Array_Overlaps_Array(int major, int minor) + { + using (var ctx = Fixture.CreateContext(new Version(major, minor))) + { + var _ = ctx.SomeEntities.Where(x => EF.Functions.Overlaps(x.SomeArray, x.SomeArray)).ToList(); + AssertContainsInSql("WHERE (x.\"SomeArray\" && x.\"SomeArray\") = TRUE"); + } + } + + [Theory] + [InlineData(8, 2)] + public void Array_Overlaps_List(int major, int minor) + { + using (var ctx = Fixture.CreateContext(new Version(major, minor))) + { + var _ = ctx.SomeEntities.Where(x => EF.Functions.Overlaps(x.SomeArray, x.SomeList)).ToList(); + AssertContainsInSql("WHERE (x.\"SomeArray\" && x.\"SomeList\") = TRUE"); + } + } + + [Theory] + [InlineData(8, 2)] + public void List_Overlaps_Array(int major, int minor) + { + using (var ctx = Fixture.CreateContext(new Version(major, minor))) { - var matches = new[] { "a", "b", "c" }; + var _ = ctx.SomeEntities.Where(x => EF.Functions.Overlaps(x.SomeList, x.SomeArray)).ToList(); + AssertContainsInSql("WHERE (x.\"SomeList\" && x.\"SomeArray\") = TRUE"); + } + } - var anon = - ctx.SomeEntities - .Select( - x => new - { - Array = x.SomeArray, - List = x.SomeList, - Text = x.SomeText - }); + [Theory] + [InlineData(8, 2)] + public void List_Overlaps_List(int major, int minor) + { + using (var ctx = Fixture.CreateContext(new Version(major, minor))) + { + var _ = ctx.SomeEntities.Where(x => EF.Functions.Overlaps(x.SomeList, x.SomeList)).ToList(); + AssertContainsInSql("WHERE (x.\"SomeList\" && x.\"SomeList\") = TRUE"); + } + } - var _ = anon.Where(x => matches.Any(m => EF.Functions.Like(m, x.Text))).ToList(); + #endregion - AssertDoesNotContainInSql("LIKE"); - AssertDoesNotContainInSql("ANY"); - AssertDoesNotContainInSql("@__matches_0"); + #region || + + [Theory] + [InlineData(7, 4)] + public void Array_Concat_with_array_column(int major, int minor) + { + using (var ctx = Fixture.CreateContext(new Version(major, minor))) + { + var _ = ctx.SomeEntities.Select(e => e.SomeArray.Concat(e.SomeArray)).ToList(); + AssertContainsInSql("SELECT (e.\"SomeArray\" || e.\"SomeArray\")"); + } + } + + [Theory] + [InlineData(7, 4)] + public void List_Concat_with_list_column(int major, int minor) + { + using (var ctx = Fixture.CreateContext(new Version(major, minor))) + { + var _ = ctx.SomeEntities.Select(e => e.SomeList.Concat(e.SomeList)).ToList(); + AssertContainsInSql("SELECT (e.\"SomeList\" || e.\"SomeList\")"); + } + } + + [Theory] + [InlineData(7, 4)] + public void Array_Concat_with_list_column(int major, int minor) + { + using (var ctx = Fixture.CreateContext(new Version(major, minor))) + { + var _ = ctx.SomeEntities.Select(e => e.SomeArray.Concat(e.SomeList)).ToList(); + AssertContainsInSql("SELECT (e.\"SomeArray\" || e.\"SomeList\")"); + } + } + + [Theory] + [InlineData(7, 4)] + public void List_Concat_with_array_column(int major, int minor) + { + using (var ctx = Fixture.CreateContext(new Version(major, minor))) + { + 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 + [Theory] + [InlineData(7, 4)] + public void Array_Append_constant(int major, int minor) + { + using (var ctx = Fixture.CreateContext(new Version(major, minor))) + { + var _ = ctx.SomeEntities.Select(e => e.SomeArray.Append(0)).ToList(); + AssertContainsInSql("SELECT (e.\"SomeArray\" || 0)"); + } + } + + [Theory] + [InlineData(7, 4)] + public void List_Append_constant(int major, int minor) + { + using (var ctx = Fixture.CreateContext(new Version(major, minor))) + { + var _ = ctx.SomeEntities.Select(e => e.SomeList.Append(0)).ToList(); + AssertContainsInSql("SELECT (e.\"SomeList\" || 0)"); + } + } + + [Theory] + [InlineData(7, 4)] + public void Array_Prepend_constant(int major, int minor) + { + using (var ctx = Fixture.CreateContext(new Version(major, minor))) + { + var _ = ctx.SomeEntities.Select(e => e.SomeArray.Prepend(0)).ToList(); + AssertContainsInSql("SELECT (0 || e.\"SomeArray\")"); + } + } + + [Theory] + [InlineData(7, 4)] + public void List_Prepend_constant(int major, int minor) + { + using (var ctx = Fixture.CreateContext(new Version(major, minor))) + { + var _ = ctx.SomeEntities.Select(e => e.SomeList.Prepend(0)).ToList(); + AssertContainsInSql("SELECT (0 || e.\"SomeList\")"); + } + } + +#endif + + #endregion + + #region array_fill + + [Theory] + [InlineData(8, 4)] + public void Array_ArrayFill_constant(int major, int minor) + { + using (var ctx = Fixture.CreateContext(new Version(major, minor))) + { + var _ = ctx.SomeEntities.Select(e => EF.Functions.ArrayFill(e.Id, new[] { 2 })).ToList(); + AssertContainsInSql("SELECT array_fill(e.\"Id\", ARRAY[2]::integer[])"); + } + } + + [Theory] + [InlineData(8, 4)] + public void Matrix_ArrayFill_constant(int major, int minor) + { + using (var ctx = Fixture.CreateContext(new Version(major, minor))) + { + var _ = ctx.SomeEntities.Select(e => EF.Functions.MatrixFill(e.Id, new[] { 1, 2 })).ToList(); + AssertContainsInSql("SELECT array_fill(e.\"Id\", ARRAY[1,2]::integer[])"); + } + } + + [Theory] + [InlineData(8, 4)] + public void List_ListFill_constant(int major, int minor) + { + using (var ctx = Fixture.CreateContext(new Version(major, minor))) + { + 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 Matrix_ArrayDimensions_column() + { + using (var ctx = Fixture.CreateContext()) + { + var _ = ctx.SomeEntities.Select(e => EF.Functions.ArrayDimensions(e.SomeMatrix)).ToList(); + AssertContainsInSql("SELECT array_dims(e.\"SomeMatrix\")"); + } + } + + [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, cardinality + + [Theory] + [InlineData(8, 4, "COALESCE(array_length(e.\"SomeArray\", 1), 0)")] + [InlineData(9, 4, "cardinality(e.\"SomeArray\")")] + public void Array_Length(int major, int minor, string sql) + { + using (var ctx = Fixture.CreateContext(new Version(major, minor))) + { + var _ = ctx.SomeEntities.Where(e => e.SomeArray.Length == 2).ToList(); + AssertContainsInSql($"WHERE {sql} = 2"); + } + } + + [Theory] + [InlineData(8, 4)] + public void List_Length(int major, int minor) + { + using (var ctx = Fixture.CreateContext(new Version(major, minor))) + { + var _ = ctx.SomeEntities.Where(e => e.SomeList.Count == 0).ToList(); + AssertContainsInSql("WHERE COALESCE(array_length(e.\"SomeList\", 1), 0) = 0"); + } + } + + [Theory] + [InlineData(8, 4, "(COALESCE(array_length(e.\"SomeMatrix\", 1), 0) * COALESCE(array_length(e.\"SomeMatrix\", 2), 0))")] + [InlineData(9, 4, "cardinality(e.\"SomeMatrix\")")] + public void Matrix_Length(int major, int minor, string sql) + { + using (var ctx = Fixture.CreateContext(new Version(major, minor))) + { + var _ = ctx.SomeEntities.Where(e => e.SomeMatrix.Length == 2).ToList(); + AssertContainsInSql($"WHERE {sql} = 2"); + } + } + + [Theory] + [InlineData(8, 4)] + public void Array_GetLength(int major, int minor) + { + using (var ctx = Fixture.CreateContext(new Version(major, minor))) + { + var _ = ctx.SomeEntities.Single(e => e.SomeArray.GetLength(0) == 2); + AssertContainsInSql("WHERE COALESCE(array_length(e.\"SomeArray\", 1), 0) = 2"); + } + } + + [Theory] + [InlineData(8, 4, "COALESCE(array_length(e.\"SomeArray\", 1), 0)")] + [InlineData(9, 4, "cardinality(e.\"SomeArray\")")] + public void Array_Count(int major, int minor, string sql) + { + using (var ctx = Fixture.CreateContext(new Version(major, minor))) + { + // ReSharper disable once UseCollectionCountProperty + var _ = ctx.SomeEntities.Where(e => e.SomeArray.Count() == 1).ToArray(); + AssertContainsInSql($"WHERE {sql} = 1"); + } + } + + [Theory] + [InlineData(8, 4)] + public void List_Count(int major, int minor) + { + using (var ctx = Fixture.CreateContext(new Version(major, minor))) + { + // 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 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"); + } + } + + [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 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 + + [Theory] + [InlineData(7, 4)] + public void Array_ArrayLower(int major, int minor) + { + using (var ctx = Fixture.CreateContext(new Version(major, minor))) + { + 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 + + [Theory] + [InlineData(8, 4)] + public void Array_Rank(int major, int minor) + { + using (var ctx = Fixture.CreateContext(new Version(major, minor))) + { + 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 + + [Theory] + [InlineData(9, 3)] + public void Array_ArrayRemove_column(int major, int minor) + { + using (var ctx = Fixture.CreateContext(new Version(major, minor))) + { + var _ = ctx.SomeEntities.Select(e => EF.Functions.ArrayRemove(e.SomeArray, 0)).ToList(); + AssertContainsInSql("SELECT array_remove(e.\"SomeArray\", 0)"); + } + } + + [Theory] + [InlineData(9, 3)] + public void List_ArrayRemove_column(int major, int minor) + { + using (var ctx = Fixture.CreateContext(new Version(major, minor))) + { + var _ = ctx.SomeEntities.Select(e => EF.Functions.ArrayRemove(e.SomeList, 0)).ToList(); + AssertContainsInSql("SELECT array_remove(e.\"SomeList\", 0)"); + } + } + + #endregion + + #region array_replace + + [Theory] + [InlineData(9, 3)] + public void Array_ArrayReplace_column(int major, int minor) + { + using (var ctx = Fixture.CreateContext(new Version(major, minor))) + { + var _ = ctx.SomeEntities.Select(e => EF.Functions.ArrayReplace(e.SomeArray, 0, 1)).ToList(); + AssertContainsInSql("SELECT array_replace(e.\"SomeArray\", 0, 1)"); + } + } + + [Theory] + [InlineData(9, 3)] + public void Matrix_ArrayReplace_column(int major, int minor) + { + using (var ctx = Fixture.CreateContext(new Version(major, minor))) + { + var _ = ctx.SomeEntities.Select(e => EF.Functions.ArrayReplace(e.SomeMatrix, 0, 1)).ToList(); + AssertContainsInSql("SELECT array_replace(e.\"SomeMatrix\", 0, 1)"); + } + } + + [Theory] + [InlineData(9, 3)] + public void List_ArrayReplace_column(int major, int minor) + { + using (var ctx = Fixture.CreateContext(new Version(major, minor))) + { + 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 + + [Theory] + [InlineData(7, 4)] + public void Array_ArrayUpper(int major, int minor) + { + using (var ctx = Fixture.CreateContext(new Version(major, minor))) + { + var _ = ctx.SomeEntities.Where(e => e.SomeArray.GetUpperBound(0) == 2).ToList(); + AssertContainsInSql("WHERE (COALESCE(array_upper(e.\"SomeArray\", 1), 0) - 1) = 2"); + } + } + + #endregion + + #region array_to_string + + [Theory] + [InlineData(7, 4)] + [InlineData(9, 1)] + public void Array_ArrayToString(int major, int minor) + { + using (var ctx = Fixture.CreateContext(new Version(major, minor))) + { + var _ = ctx.SomeEntities.Select(e => EF.Functions.ArrayToString(e.SomeArray, "*")).ToList(); + AssertContainsInSql("SELECT array_to_string(e.\"SomeArray\", '*')"); + } + } + + [Theory] + [InlineData(7, 4)] + [InlineData(9, 1)] + public void Matrix_ArrayToString(int major, int minor) + { + using (var ctx = Fixture.CreateContext(new Version(major, minor))) + { + var _ = ctx.SomeEntities.Select(e => EF.Functions.ArrayToString(e.SomeMatrix, "*")).ToList(); + AssertContainsInSql("SELECT array_to_string(e.\"SomeMatrix\", '*')"); + } + } + + [Theory] + [InlineData(7, 4)] + [InlineData(9, 1)] + public void List_ArrayToString(int major, int minor) + { + using (var ctx = Fixture.CreateContext(new Version(major, minor))) + { + var _ = ctx.SomeEntities.Select(e => EF.Functions.ArrayToString(e.SomeList, "*")).ToList(); + AssertContainsInSql("SELECT array_to_string(e.\"SomeList\", '*')"); + } + } + + [Theory] + [InlineData(9, 1)] + public void Array_ArrayToString_with_null(int major, int minor) + { + using (var ctx = Fixture.CreateContext(new Version(major, minor))) + { + var _ = ctx.SomeEntities.Select(e => EF.Functions.ArrayToString(e.SomeArray, "*", ";")).ToList(); + AssertContainsInSql("SELECT array_to_string(e.\"SomeArray\", '*', ';')"); + } + } + + [Theory] + [InlineData(9, 1)] + public void Matrix_ArrayToString_with_null(int major, int minor) + { + using (var ctx = Fixture.CreateContext(new Version(major, minor))) + { + var _ = ctx.SomeEntities.Select(e => EF.Functions.ArrayToString(e.SomeMatrix, "*", ";")).ToList(); + AssertContainsInSql("SELECT array_to_string(e.\"SomeMatrix\", '*', ';')"); + } + } + + [Theory] + [InlineData(9, 1)] + public void List_ArrayToString_with_null(int major, int minor) + { + using (var ctx = Fixture.CreateContext(new Version(major, minor))) + { + var _ = ctx.SomeEntities.Select(e => EF.Functions.ArrayToString(e.SomeList, "*", ";")).ToList(); + AssertContainsInSql("SELECT array_to_string(e.\"SomeList\", '*', ';')"); + } + } + + #endregion + + #region string_to_array + + [Theory] + [InlineData(7, 4)] + [InlineData(9, 1)] + public void Array_StringToArray(int major, int minor) + { + using (var ctx = Fixture.CreateContext(new Version(major, minor))) + { + var _ = ctx.SomeEntities.Where(e => EF.Functions.StringToArray(e.SomeText, "*") != null).ToList(); + AssertContainsInSql("WHERE string_to_array(e.\"SomeText\", '*') IS NOT NULL"); + } + } + + [Theory] + [InlineData(7, 4)] + [InlineData(9, 1)] + public void List_StringToList(int major, int minor) + { + using (var ctx = Fixture.CreateContext(new Version(major, minor))) + { + var _ = ctx.SomeEntities.Where(e => EF.Functions.StringToList(e.SomeText, "*") != null).ToList(); + AssertContainsInSql("WHERE string_to_array(e.\"SomeText\", '*') IS NOT NULL"); + } + } + + [Theory] + [InlineData(9, 1)] + public void Array_StringToArray_with_null_string(int major, int minor) + { + using (var ctx = Fixture.CreateContext(new Version(major, minor))) + { + var _ = ctx.SomeEntities.Where(e => EF.Functions.StringToArray(e.SomeText, "*", ";") != null).ToList(); + AssertContainsInSql("WHERE string_to_array(e.\"SomeText\", '*', ';') IS NOT NULL"); + } + } + + [Theory] + [InlineData(9, 1)] + public void List_StringToList_with_null_string(int major, int minor) + { + using (var ctx = Fixture.CreateContext(new Version(major, minor))) + { + var _ = ctx.SomeEntities.Where(e => EF.Functions.StringToList(e.SomeText, "*", ";") != null).ToList(); + AssertContainsInSql("WHERE string_to_array(e.\"SomeText\", '*', ';') IS NOT NULL"); } } From 75293ec42201cb0b6475a17a85c304372a9b7bd8 Mon Sep 17 00:00:00 2001 From: Austin Drenski Date: Tue, 11 Dec 2018 21:24:27 -0500 Subject: [PATCH 2/4] Address review (partial) TODO: - Investigate `Array.Length` as member vs node. - Decide on whether to defer to client eval for pre-9.4 versions. - Recheck/revert logic in `NpgsqlSqlTranslatingExpressionVisitor` to make sure the query pipeline is modified as minimally as possible. --- .../Extensions/NpgsqlArrayExtensions.cs | 102 ----- .../Internal/NpgsqlArrayMemberTranslator.cs | 89 ++--- .../NpgsqlArrayMethodCallTranslator.cs | 378 +++++++----------- .../NpgsqlCompositeMemberTranslator.cs | 5 +- .../NpgsqlCompositeMethodCallTranslator.cs | 5 +- .../Internal/NpgsqlStringTranslator.cs | 21 + .../NpgsqlSqlTranslatingExpressionVisitor.cs | 4 +- .../Sql/Internal/NpgsqlQuerySqlGenerator.cs | 31 +- .../Query/ArrayQueryTest.cs | 344 ++++------------ .../Query/StringQueryTest.cs | 72 ++++ 10 files changed, 368 insertions(+), 683 deletions(-) create mode 100644 test/EFCore.PG.FunctionalTests/Query/StringQueryTest.cs diff --git a/src/EFCore.PG/Extensions/NpgsqlArrayExtensions.cs b/src/EFCore.PG/Extensions/NpgsqlArrayExtensions.cs index 28a7dd8ea..c07a4663f 100644 --- a/src/EFCore.PG/Extensions/NpgsqlArrayExtensions.cs +++ b/src/EFCore.PG/Extensions/NpgsqlArrayExtensions.cs @@ -274,26 +274,6 @@ public static T[] ArrayFill( [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 T[,] MatrixFill( - [CanBeNull] this DbFunctions _, - [CanBeNull] T value, - [CanBeNull] [ItemNotNull] T[] dimensions) - => throw ClientEvaluationNotSupportedException(); - /// /// Initializes an array with the given /// @@ -336,24 +316,6 @@ public static string ArrayDimensions( [CanBeNull] T[] array) => throw ClientEvaluationNotSupportedException(); - /// - /// 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. /// @@ -486,28 +448,6 @@ public static T[] ArrayReplace( [CanBeNull] T replacement) => throw ClientEvaluationNotSupportedException(); - /// - /// 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. /// @@ -554,26 +494,6 @@ public static string ArrayToString( [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] T[,] array, - [CanBeNull] string delimiter) - => throw ClientEvaluationNotSupportedException(); - /// /// Concatenates elements using the supplied delimiter. /// @@ -616,28 +536,6 @@ public static string ArrayToString( [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] T[,] array, - [CanBeNull] string delimiter, - [CanBeNull] string nullString) - => throw ClientEvaluationNotSupportedException(); - /// /// Concatenates elements using the supplied delimiter and the string representation for null elements. /// diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayMemberTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayMemberTranslator.cs index a931e0fb4..aab9a6c0a 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayMemberTranslator.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayMemberTranslator.cs @@ -1,7 +1,6 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Linq; using System.Linq.Expressions; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Query.Expressions; @@ -29,92 +28,60 @@ public class NpgsqlArrayMemberTranslator : IMemberTranslator public NpgsqlArrayMemberTranslator([CanBeNull] Version postgresVersion) => _postgresVersion = postgresVersion; /// - public Expression Translate(MemberExpression expression) - => ArrayInstanceHandler(expression) ?? ListInstanceHandler(expression); + public Expression Translate(MemberExpression e) + => ArrayInstanceHandler(e) ?? + ListInstanceHandler(e); #region Handlers - /// - /// Translates instance members defined on . - /// - /// The expression to translate. - /// - /// A translated expression or null if no translation is supported. - /// [CanBeNull] - Expression ArrayInstanceHandler([NotNull] MemberExpression expression) + Expression ArrayInstanceHandler([NotNull] MemberExpression e) { - var instance = expression.Expression; + var instance = e.Expression; - if (instance is null || !instance.Type.IsArray) + if (instance == null || !instance.Type.IsArray || instance.Type.GetArrayRank() != 1) return null; - switch (expression.Member.Name) + switch (e.Member.Name) { - case nameof(Array.Length) when VersionAtLeast(9, 4): - return new SqlFunctionExpression("cardinality", typeof(int), new[] { instance }); - - case nameof(Array.Length) when VersionAtLeast(8, 4) && instance.Type.GetArrayRank() == 1: - return - Expression.Coalesce( - new SqlFunctionExpression( - "array_length", - typeof(int?), - new[] { instance, Expression.Constant(1) }), - Expression.Constant(0)); - case nameof(Array.Length) when VersionAtLeast(8, 4): - return - Enumerable.Range(1, instance.Type.GetArrayRank()) - .Select(x => - Expression.Coalesce( - new SqlFunctionExpression( - "array_length", - typeof(int?), - new[] { instance, Expression.Constant(x) }), - Expression.Constant(0))) - .Cast() - .Aggregate(Expression.Multiply); + return Expression.Coalesce( + new SqlFunctionExpression( + "array_length", + typeof(int?), + new[] { instance, Expression.Constant(1) }), + Expression.Constant(0)); case nameof(Array.Rank) when VersionAtLeast(8, 4): - return - Expression.Coalesce( - new SqlFunctionExpression( - "array_ndims", - typeof(int?), - new[] { instance }), - Expression.Constant(1)); + return Expression.Coalesce( + new SqlFunctionExpression( + "array_ndims", + typeof(int?), + new[] { instance }), + Expression.Constant(1)); default: return null; } } - /// - /// Translates instance members defined on . - /// - /// The expression to translate. - /// - /// A translated expression or null if no translation is supported. - /// [CanBeNull] - Expression ListInstanceHandler([NotNull] MemberExpression expression) + Expression ListInstanceHandler([NotNull] MemberExpression e) { - var instance = expression.Expression; + var instance = e.Expression; if (instance is null || !instance.Type.IsGenericType || instance.Type.GetGenericTypeDefinition() != typeof(List<>)) return null; - switch (expression.Member.Name) + switch (e.Member.Name) { case nameof(IList.Count) when VersionAtLeast(8, 4): - return - Expression.Coalesce( - new SqlFunctionExpression( - "array_length", - typeof(int?), - new Expression[] { instance, Expression.Constant(1) }), - Expression.Constant(0)); + return Expression.Coalesce( + new SqlFunctionExpression( + "array_length", + typeof(int?), + new Expression[] { instance, Expression.Constant(1) }), + Expression.Constant(0)); default: return null; diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayMethodCallTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayMethodCallTranslator.cs index fb4f8cbbe..aeeea6d35 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayMethodCallTranslator.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayMethodCallTranslator.cs @@ -34,314 +34,216 @@ public class NpgsqlArrayMethodCallTranslator : IMethodCallTranslator /// [CanBeNull] - public Expression Translate(MethodCallExpression expression) - => EnumerableHandler(expression) ?? - NpgsqlArrayExtensionsHandler(expression) ?? - ArrayStaticHandler(expression) ?? - ArrayInstanceHandler(expression) ?? - StringInstanceHandler(expression) ?? - ListInstanceHandler(expression); + public Expression Translate(MethodCallExpression e) + { + 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 - /// - /// Translates methods defined on . - /// - /// The expression to translate. - /// - /// A translated expression or null if no translation is supported. - /// [CanBeNull] - Expression EnumerableHandler([NotNull] MethodCallExpression expression) + Expression EnumerableHandler([NotNull] MethodCallExpression e) { - if (expression.Method.DeclaringType != typeof(Enumerable)) + if (e.Method.DeclaringType != typeof(Enumerable)) return null; - var type = expression.Arguments[0].Type; - - if (!type.IsArray && type != typeof(string) && (!type.IsGenericType || type.GetGenericTypeDefinition() != typeof(List<>))) - return null; + var type = e.Arguments[0].Type; - if (!VersionAtLeast(7, 4)) + if (!type.IsArray && !IsList(type)) return null; - switch (expression.Method.Name) + switch (e.Method.Name) { - case nameof(Enumerable.Count) when type == typeof(string): - return - Expression.Coalesce( - new SqlFunctionExpression( - "character_length", - typeof(int?), - new[] { expression.Arguments[0] }), - Expression.Constant(0)); + case nameof(Enumerable.Count) when VersionAtLeast(8, 4): + return Expression.Coalesce( + new SqlFunctionExpression( + "array_length", + typeof(int?), + new[] { e.Arguments[0], Expression.Constant(1) }), + Expression.Constant(0)); - case nameof(Enumerable.Count): - return VersionAtLeast(8, 4) - ? Expression.Coalesce( - new SqlFunctionExpression( - "array_length", - typeof(int?), - new[] { expression.Arguments[0], Expression.Constant(1) }), - Expression.Constant(0)) - : null; + case nameof(Enumerable.ElementAt) when e.Arguments[0].Type.IsArray: + return MakeArrayIndex(e.Arguments[0], e.Arguments[1]); case nameof(Enumerable.ElementAt): - return MakeIndex(expression.Arguments[0], expression.Arguments[1]); + return MakeListIndex(e.Arguments[0], e.Arguments[1]); case nameof(Enumerable.Append): - return new CustomBinaryExpression(expression.Arguments[0], expression.Arguments[1], "||", expression.Arguments[0].Type); + return new CustomBinaryExpression(e.Arguments[0], e.Arguments[1], "||", e.Arguments[0].Type); case nameof(Enumerable.Prepend): - return new CustomBinaryExpression(expression.Arguments[1], expression.Arguments[0], "||", expression.Arguments[0].Type); + return new CustomBinaryExpression(e.Arguments[1], e.Arguments[0], "||", e.Arguments[0].Type); case nameof(Enumerable.SequenceEqual): - return Expression.MakeBinary(ExpressionType.Equal, expression.Arguments[0], expression.Arguments[1]); + return Expression.MakeBinary(ExpressionType.Equal, e.Arguments[0], e.Arguments[1]); default: return null; } } - /// - /// Translates methods defined on . - /// - /// The expression to translate. - /// - /// A translated expression or null if no translation is supported. - /// - /// - /// This handler throws on unsupported versions since - /// does not support client-side evaluation. - /// [CanBeNull] - Expression NpgsqlArrayExtensionsHandler([NotNull] MethodCallExpression expression) + Expression NpgsqlArrayExtensionsHandler([NotNull] MethodCallExpression e) { - if (expression.Method.DeclaringType != typeof(NpgsqlArrayExtensions)) + if (e.Method.DeclaringType != typeof(NpgsqlArrayExtensions)) return null; - switch (expression.Method.Name) + switch (e.Method.Name) { - case nameof(NpgsqlArrayExtensions.Contains): - return VersionAtLeast(8, 2) - ? new CustomBinaryExpression(expression.Arguments[1], expression.Arguments[2], "@>", typeof(bool)) - : throw VersionNotSupportedException(nameof(NpgsqlArrayExtensions.Contains), 8, 2); - - case nameof(NpgsqlArrayExtensions.ContainedBy): - return VersionAtLeast(8, 2) - ? new CustomBinaryExpression(expression.Arguments[1], expression.Arguments[2], "<@", typeof(bool)) - : throw VersionNotSupportedException(nameof(NpgsqlArrayExtensions.ContainedBy), 8, 2); - - case nameof(NpgsqlArrayExtensions.Overlaps): - return VersionAtLeast(8, 2) - ? new CustomBinaryExpression(expression.Arguments[1], expression.Arguments[2], "&&", typeof(bool)) - : throw VersionNotSupportedException(nameof(NpgsqlArrayExtensions.Overlaps), 8, 2); - - case nameof(NpgsqlArrayExtensions.ArrayFill): - return VersionAtLeast(8, 4) - ? new SqlFunctionExpression("array_fill", expression.Method.ReturnType, expression.Arguments.Skip(1)) - : throw VersionNotSupportedException(nameof(NpgsqlArrayExtensions.ArrayFill), 8, 4); - - case nameof(NpgsqlArrayExtensions.ListFill): - return VersionAtLeast(8, 4) - ? new SqlFunctionExpression("array_fill", expression.Method.ReturnType, expression.Arguments.Skip(1)) - : throw VersionNotSupportedException(nameof(NpgsqlArrayExtensions.ListFill), 8, 4); - - case nameof(NpgsqlArrayExtensions.MatrixFill): - return VersionAtLeast(8, 4) - ? new SqlFunctionExpression("array_fill", expression.Method.ReturnType, expression.Arguments.Skip(1)) - : throw VersionNotSupportedException(nameof(NpgsqlArrayExtensions.MatrixFill), 8, 4); + case nameof(NpgsqlArrayExtensions.Contains) when VersionAtLeast(8, 2): + return new CustomBinaryExpression(e.Arguments[1], e.Arguments[2], "@>", typeof(bool)); + + case nameof(NpgsqlArrayExtensions.ContainedBy) when VersionAtLeast(8, 2): + return new CustomBinaryExpression(e.Arguments[1], e.Arguments[2], "<@", typeof(bool)); + + case nameof(NpgsqlArrayExtensions.Overlaps) when VersionAtLeast(8, 2): + return new CustomBinaryExpression(e.Arguments[1], e.Arguments[2], "&&", typeof(bool)); + + case nameof(NpgsqlArrayExtensions.ArrayFill) when VersionAtLeast(8, 4): + return new SqlFunctionExpression("array_fill", e.Method.ReturnType, e.Arguments.Skip(1)); + + case nameof(NpgsqlArrayExtensions.ListFill) when VersionAtLeast(8, 4): + return new SqlFunctionExpression("array_fill", e.Method.ReturnType, e.Arguments.Skip(1)); case nameof(NpgsqlArrayExtensions.ArrayDimensions): - return VersionAtLeast(7, 4) - ? new SqlFunctionExpression("array_dims", expression.Method.ReturnType, expression.Arguments.Skip(1).Take(1)) - : throw VersionNotSupportedException(nameof(NpgsqlArrayExtensions.ArrayDimensions), 7, 4); + return new SqlFunctionExpression("array_dims", e.Method.ReturnType, e.Arguments.Skip(1).Take(1)); - case nameof(NpgsqlArrayExtensions.ArrayPositions): - return VersionAtLeast(9, 5) - ? new SqlFunctionExpression("array_positions", expression.Method.ReturnType, expression.Arguments.Skip(1)) - : throw VersionNotSupportedException(nameof(NpgsqlArrayExtensions.ArrayPositions), 9, 5); + 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 VersionAtLeast(9, 3) - ? new SqlFunctionExpression("array_remove", expression.Method.ReturnType, expression.Arguments.Skip(1).Take(2)) - : throw VersionNotSupportedException(nameof(NpgsqlArrayExtensions.ArrayRemove), 9, 3); + case nameof(NpgsqlArrayExtensions.ArrayRemove) when VersionAtLeast(9, 3): + return new SqlFunctionExpression("array_remove", e.Method.ReturnType, e.Arguments.Skip(1).Take(2)); - case nameof(NpgsqlArrayExtensions.ArrayReplace): - return VersionAtLeast(9, 3) - ? new SqlFunctionExpression("array_replace", expression.Method.ReturnType, expression.Arguments.Skip(1).Take(3)) - : throw VersionNotSupportedException(nameof(NpgsqlArrayExtensions.ArrayReplace), 9, 3); + case nameof(NpgsqlArrayExtensions.ArrayReplace) when VersionAtLeast(9, 3): + return new SqlFunctionExpression("array_replace", e.Method.ReturnType, e.Arguments.Skip(1).Take(3)); + + case nameof(NpgsqlArrayExtensions.ArrayToString) when VersionAtLeast(9, 1): + return new SqlFunctionExpression("array_to_string", e.Method.ReturnType, e.Arguments.Skip(1).Take(3)); case nameof(NpgsqlArrayExtensions.ArrayToString): - return VersionAtLeast(9, 1) - ? new SqlFunctionExpression("array_to_string", expression.Method.ReturnType, expression.Arguments.Skip(1).Take(3)) - : VersionAtLeast(7, 4) - ? new SqlFunctionExpression("array_to_string", expression.Method.ReturnType, expression.Arguments.Skip(1).Take(2)) - : throw VersionNotSupportedException(nameof(NpgsqlArrayExtensions.ArrayToString), 7, 4); + return new SqlFunctionExpression("array_to_string", e.Method.ReturnType, e.Arguments.Skip(1).Take(2)); + + case nameof(NpgsqlArrayExtensions.StringToArray) when VersionAtLeast(9, 1): + return new SqlFunctionExpression("string_to_array", e.Method.ReturnType, e.Arguments.Skip(1).Take(3)); case nameof(NpgsqlArrayExtensions.StringToArray): - return VersionAtLeast(9, 1) - ? new SqlFunctionExpression("string_to_array", expression.Method.ReturnType, expression.Arguments.Skip(1).Take(3)) - : VersionAtLeast(7, 4) - ? new SqlFunctionExpression("string_to_array", expression.Method.ReturnType, expression.Arguments.Skip(1).Take(2)) - : throw VersionNotSupportedException(nameof(NpgsqlArrayExtensions.StringToArray), 7, 4); + return new SqlFunctionExpression("string_to_array", e.Method.ReturnType, e.Arguments.Skip(1).Take(2)); + + case nameof(NpgsqlArrayExtensions.StringToList) when VersionAtLeast(9, 1): + return new SqlFunctionExpression("string_to_array", e.Method.ReturnType, e.Arguments.Skip(1).Take(3)); case nameof(NpgsqlArrayExtensions.StringToList): - return VersionAtLeast(9, 1) - ? new SqlFunctionExpression("string_to_array", expression.Method.ReturnType, expression.Arguments.Skip(1).Take(3)) - : VersionAtLeast(7, 4) - ? new SqlFunctionExpression("string_to_array", expression.Method.ReturnType, expression.Arguments.Skip(1).Take(2)) - : throw VersionNotSupportedException(nameof(NpgsqlArrayExtensions.StringToList), 7, 4); + return new SqlFunctionExpression("string_to_array", e.Method.ReturnType, e.Arguments.Skip(1).Take(2)); default: return null; } - - NotSupportedException VersionNotSupportedException(string name, int major, int minor) - => new NotSupportedException($"{nameof(NpgsqlArrayExtensions)}.{name} is not supported before PostgreSQL {major}.{minor}."); } - /// - /// Translates static methods defined on . - /// - /// The expression to translate. - /// - /// A translated expression or null if no translation is supported. - /// [CanBeNull] - Expression ArrayStaticHandler([NotNull] MethodCallExpression expression) + Expression ArrayStaticHandler([NotNull] MethodCallExpression e) { - if (!expression.Method.IsStatic || expression.Method.DeclaringType != typeof(Array)) + if (!e.Method.IsStatic || e.Method.DeclaringType != typeof(Array)) return null; - switch (expression.Method.Name) + switch (e.Method.Name) { case nameof(Array.IndexOf) when VersionAtLeast(9, 5) && - expression.Arguments[0].Type.GetArrayRank() == 1 && - expression.Method.GetParameters().Length <= 3: - return - Expression.Subtract( - Expression.Coalesce( - new SqlFunctionExpression( - "array_position", - typeof(int?), - expression.Arguments.Take(3)), - Expression.Constant(0)), - Expression.Constant(1)); + 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; } } - /// - /// Translates instance methods defined on . - /// - /// The expression to translate. - /// - /// A translated expression or null if no translation is supported. - /// [CanBeNull] - Expression ArrayInstanceHandler([NotNull] MethodCallExpression expression) + Expression ArrayInstanceHandler([NotNull] MethodCallExpression e) { - var instance = expression.Object; + var instance = e.Object; if (instance == null || !instance.Type.IsArray) return null; - switch (expression.Method.Name) + switch (e.Method.Name) { case nameof(Array.GetLength) when VersionAtLeast(8, 4): - return + 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_length", + "array_lower", typeof(int?), - new[] { instance, GenerateOneBasedIndexExpression(expression.Arguments[0]) }), - Expression.Constant(0)); - - case nameof(Array.GetLowerBound) when VersionAtLeast(7, 4): - return - Expression.Subtract( - Expression.Coalesce( - new SqlFunctionExpression( - "array_lower", - typeof(int?), - new[] { instance, GenerateOneBasedIndexExpression(expression.Arguments[0]) }), - Expression.Constant(0)), - Expression.Constant(1)); - - case nameof(Array.GetUpperBound) when VersionAtLeast(7, 4): - return - Expression.Subtract( - Expression.Coalesce( - new SqlFunctionExpression( - "array_upper", - typeof(int?), - new[] { instance, GenerateOneBasedIndexExpression(expression.Arguments[0]) }), - Expression.Constant(0)), - Expression.Constant(1)); + 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; } } - /// - /// Translates instance methods defined on . - /// - /// The expression to translate. - /// - /// A translated expression or null if no translation is supported. - /// [CanBeNull] - Expression ListInstanceHandler([NotNull] MethodCallExpression expression) + Expression ListInstanceHandler([NotNull] MethodCallExpression e) { - var instance = expression.Object; + var instance = e.Object; - if (instance == null || !instance.Type.IsGenericType || instance.Type.GetGenericTypeDefinition() != typeof(List<>)) + if (instance == null || !IsList(instance.Type)) return null; - switch (expression.Method.Name) + switch (e.Method.Name) { case "get_Item": - return MakeIndex(instance, expression.Arguments[0]); + 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, expression.Arguments[0] }), - Expression.Constant(0)), - Expression.Constant(1)); - - default: - return null; - } - } - - /// - /// Translates instance methods defined on . - /// - /// The expression to translate. - /// - /// A translated expression or null if no translation is supported. - /// - [CanBeNull] - static Expression StringInstanceHandler([NotNull] MethodCallExpression expression) - { - var instance = expression.Object; - - if (instance == null || instance.Type != typeof(string)) - return null; - - switch (expression.Method.Name) - { - case "get_Chars": - return MakeIndex(instance, expression.Arguments[0]); + 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; @@ -360,28 +262,11 @@ static Expression StringInstanceHandler([NotNull] MethodCallExpression expressio /// /// 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; + bool VersionAtLeast(int major, int minor) => _postgresVersion == null || new Version(major, minor) <= _postgresVersion; - /// - /// Creates an expression of type or - /// from the and the . - /// - /// The instance to index. - /// The index value. - /// - /// An or . - /// [NotNull] - static Expression MakeIndex([NotNull] Expression instance, [NotNull] Expression index) - { - if (instance.Type.IsArray) - { - return instance.Type.GetArrayRank() == 1 - ? (Expression)Expression.ArrayIndex(instance, index) - : Expression.ArrayAccess(instance, index); - } - - return Expression.MakeIndex( + static Expression MakeListIndex([NotNull] Expression instance, [NotNull] Expression index) + => Expression.MakeIndex( instance, instance.Type .GetRuntimeProperties() @@ -391,7 +276,12 @@ static Expression MakeIndex([NotNull] Expression instance, [NotNull] Expression .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, @@ -403,6 +293,8 @@ static Expression GenerateOneBasedIndexExpression([NotNull] Expression expressio ? 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/NpgsqlCompositeMemberTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlCompositeMemberTranslator.cs index d4cee16c5..17a1204bc 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlCompositeMemberTranslator.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlCompositeMemberTranslator.cs @@ -31,11 +31,10 @@ public NpgsqlCompositeMemberTranslator( new NpgsqlArrayMemberTranslator(npgsqlOptions.PostgresVersion) }; - // ReSharper disable once DoNotCallOverridableMethodsInConstructor + // ReSharper disable VirtualMemberCallInConstructor AddTranslators(MemberTranslators); - - // ReSharper disable once DoNotCallOverridableMethodsInConstructor 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 ba3729126..e915f5b76 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlCompositeMethodCallTranslator.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlCompositeMethodCallTranslator.cs @@ -41,11 +41,10 @@ public NpgsqlCompositeMethodCallTranslator( 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 e7c5e4826..238747f0d 100644 --- a/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlSqlTranslatingExpressionVisitor.cs @@ -26,7 +26,7 @@ public class NpgsqlSqlTranslatingExpressionVisitor : SqlTranslatingExpressionVis /// The current query compilation context. /// [NotNull] - RelationalQueryCompilationContext Context => _queryModelVisitor.QueryCompilationContext; + RelationalQueryCompilationContext QueryCompilationContext => _queryModelVisitor.QueryCompilationContext; /// public NpgsqlSqlTranslatingExpressionVisitor( @@ -49,7 +49,7 @@ protected override Expression VisitBinary(BinaryExpression expression) if (left == null || right == null) return base.VisitBinary(expression); - if (MemberAccessBindingExpressionVisitor.GetPropertyPath(expression.Left, Context, out _).Count != 0) + if (MemberAccessBindingExpressionVisitor.GetPropertyPath(expression.Left, QueryCompilationContext, out _).Count != 0) { if (expression.NodeType == ExpressionType.ArrayIndex) return Expression.ArrayIndex(left, right); diff --git a/src/EFCore.PG/Query/Sql/Internal/NpgsqlQuerySqlGenerator.cs b/src/EFCore.PG/Query/Sql/Internal/NpgsqlQuerySqlGenerator.cs index 3937d14ad..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; @@ -203,27 +204,30 @@ protected override Expression VisitUnary(UnaryExpression expression) } /// - protected override Expression VisitIndex(IndexExpression expression) + protected override Expression VisitIndex(IndexExpression e) { // text cannot be subscripted. - if (expression.Object.Type == typeof(string)) + if (e.Object.Type == typeof(string)) { return VisitSqlFunction( new SqlFunctionExpression( "substr", typeof(char), - new[] { expression.Object, GenerateOneBasedIndexExpression(expression.Arguments[0]), Expression.Constant(1) })); + new[] { e.Object, GenerateOneBasedIndexExpression(e.Arguments[0]), Expression.Constant(1) })); } - Visit(expression.Object); - for (int i = 0; i < expression.Arguments.Count; i++) + if (!IsArrayOrList(e.Object.Type)) + return null; + + Visit(e.Object); + for (int i = 0; i < e.Arguments.Count; i++) { Sql.Append('['); - Visit(GenerateOneBasedIndexExpression(expression.Arguments[i])); + Visit(GenerateOneBasedIndexExpression(e.Arguments[i])); Sql.Append(']'); } - return expression; + return e; } /// @@ -501,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 b9ad8cc3c..678eab818 100644 --- a/test/EFCore.PG.FunctionalTests/Query/ArrayQueryTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/ArrayQueryTest.cs @@ -12,8 +12,6 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query { public class ArrayQueryTest : IClassFixture { - // TODO: add multidimensional tests throughout - #region ArrayTests #region Roundtrip @@ -74,26 +72,6 @@ public void Array_Index_bytea_with_constant() } } - [Fact] - public void String_Index_text_with_constant_char_as_int() - { - using (var ctx = Fixture.CreateContext()) - { - var _ = ctx.SomeEntities.Where(e => e.SomeText[0] == 'T').ToList(); - AssertContainsInSql("WHERE ascii(substr(e.\"SomeText\", 1, 1)) = 84"); - } - } - - [Fact] - public void String_Index_text_with_constant_string() - { - using (var ctx = Fixture.CreateContext()) - { - var _ = ctx.SomeEntities.Where(e => e.SomeText[0].ToString() == "T").ToList(); - AssertContainsInSql("WHERE CAST(substr(e.\"SomeText\", 1, 1) AS text) = 'T'"); - } - } - [Fact] public void Array_ElementAt_with_constant() { @@ -124,26 +102,6 @@ public void Array_ElementAt_bytea_with_constant() } } - [Fact] - public void String_ElementAt_text_with_constant_char_as_int() - { - using (var ctx = Fixture.CreateContext()) - { - var _ = ctx.SomeEntities.Where(e => e.SomeText.ElementAt(0) == 'T').ToList(); - AssertContainsInSql("WHERE ascii(substr(e.\"SomeText\", 1, 1)) = 84"); - } - } - - [Fact] - public void String_ElementAt_text_with_constant_string() - { - using (var ctx = Fixture.CreateContext()) - { - var _ = ctx.SomeEntities.Where(e => e.SomeText.ElementAt(0).ToString() == "T").ToList(); - AssertContainsInSql("WHERE CAST(substr(e.\"SomeText\", 1, 1) AS text) = 'T'"); - } - } - [Fact] public void Array_Index_with_non_constant() { @@ -180,30 +138,6 @@ public void Array_Index_bytea_with_non_constant() } } - [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.SomeEntities.Where(e => e.SomeText[x] == 'T').ToList(); - AssertContainsInSql("WHERE ascii(substr(e.\"SomeText\", @__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.SomeEntities.Where(e => e.SomeText[x].ToString() == "T").ToList(); - AssertContainsInSql("WHERE CAST(substr(e.\"SomeText\", @__x_0 + 1, 1) AS text) = 'T'"); - } - } - [Fact] public void Array_ElementAt_with_non_constant() { @@ -240,30 +174,6 @@ public void Array_ElementAt_bytea_with_non_constant() } } - [Fact] - public void String_ElementAt_text_with_non_constant_char_as_int() - { - using (var ctx = Fixture.CreateContext()) - { - // ReSharper disable once ConvertToConstant.Local - var x = 0; - var _ = ctx.SomeEntities.Where(e => e.SomeText.ElementAt(x) == 'T').ToList(); - AssertContainsInSql("WHERE ascii(substr(e.\"SomeText\", @__x_0 + 1, 1)) = 84"); - } - } - - [Fact] - public void String_ElementAt_text_with_non_constant_sting() - { - using (var ctx = Fixture.CreateContext()) - { - // ReSharper disable once ConvertToConstant.Local - var x = 0; - var _ = ctx.SomeEntities.Where(e => e.SomeText.ElementAt(x).ToString() == "T").ToList(); - AssertContainsInSql("WHERE CAST(substr(e.\"SomeText\", @__x_0 + 1, 1) AS text) = 'T'"); - } - } - [Fact] public void Array_Index_multidimensional() { @@ -415,44 +325,40 @@ public void List_Contains_with_column() #region @> - [Theory] - [InlineData(8, 2)] - public void Array_Contains_Array(int major, int minor) + [Fact] + public void Array_Contains_Array() { - using (var ctx = Fixture.CreateContext(new Version(major, minor))) + using (var ctx = Fixture.CreateContext()) { var _ = ctx.SomeEntities.Where(x => EF.Functions.Contains(x.SomeArray, x.SomeArray)).ToList(); AssertContainsInSql("WHERE (x.\"SomeArray\" @> x.\"SomeArray\") = TRUE"); } } - [Theory] - [InlineData(8, 2)] - public void Array_Contains_List(int major, int minor) + [Fact] + public void Array_Contains_List() { - using (var ctx = Fixture.CreateContext(new Version(major, minor))) + 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"); } } - [Theory] - [InlineData(8, 2)] - public void List_Contains_Array(int major, int minor) + [Fact] + public void List_Contains_Array() { - using (var ctx = Fixture.CreateContext(new Version(major, minor))) + 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"); } } - [Theory] - [InlineData(8, 2)] - public void List_Contains_List(int major, int minor) + [Fact] + public void List_Contains_List() { - using (var ctx = Fixture.CreateContext(new Version(major, minor))) + 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"); @@ -463,44 +369,40 @@ public void List_Contains_List(int major, int minor) #region <@ - [Theory] - [InlineData(8, 2)] - public void Array_ContainedBy_Array(int major, int minor) + [Fact] + public void Array_ContainedBy_Array() { - using (var ctx = Fixture.CreateContext(new Version(major, minor))) + 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"); } } - [Theory] - [InlineData(8, 2)] - public void Array_ContainedBy_List(int major, int minor) + [Fact] + public void Array_ContainedBy_List() { - using (var ctx = Fixture.CreateContext(new Version(major, minor))) + 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"); } } - [Theory] - [InlineData(8, 2)] - public void List_ContainedBy_Array(int major, int minor) + [Fact] + public void List_ContainedBy_Array() { - using (var ctx = Fixture.CreateContext(new Version(major, minor))) + 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"); } } - [Theory] - [InlineData(8, 2)] - public void List_ContainedBy_List(int major, int minor) + [Fact] + public void List_ContainedBy_List() { - using (var ctx = Fixture.CreateContext(new Version(major, minor))) + 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"); @@ -511,44 +413,40 @@ public void List_ContainedBy_List(int major, int minor) #region && - [Theory] - [InlineData(8, 2)] - public void Array_Overlaps_Array(int major, int minor) + [Fact] + public void Array_Overlaps_Array() { - using (var ctx = Fixture.CreateContext(new Version(major, minor))) + 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"); } } - [Theory] - [InlineData(8, 2)] - public void Array_Overlaps_List(int major, int minor) + [Fact] + public void Array_Overlaps_List() { - using (var ctx = Fixture.CreateContext(new Version(major, minor))) + 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"); } } - [Theory] - [InlineData(8, 2)] - public void List_Overlaps_Array(int major, int minor) + [Fact] + public void List_Overlaps_Array() { - using (var ctx = Fixture.CreateContext(new Version(major, minor))) + 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"); } } - [Theory] - [InlineData(8, 2)] - public void List_Overlaps_List(int major, int minor) + [Fact] + public void List_Overlaps_List() { - using (var ctx = Fixture.CreateContext(new Version(major, minor))) + 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"); @@ -559,44 +457,40 @@ public void List_Overlaps_List(int major, int minor) #region || - [Theory] - [InlineData(7, 4)] - public void Array_Concat_with_array_column(int major, int minor) + [Fact] + public void Array_Concat_with_array_column() { - using (var ctx = Fixture.CreateContext(new Version(major, minor))) + using (var ctx = Fixture.CreateContext()) { var _ = ctx.SomeEntities.Select(e => e.SomeArray.Concat(e.SomeArray)).ToList(); AssertContainsInSql("SELECT (e.\"SomeArray\" || e.\"SomeArray\")"); } } - [Theory] - [InlineData(7, 4)] - public void List_Concat_with_list_column(int major, int minor) + [Fact] + public void List_Concat_with_list_column() { - using (var ctx = Fixture.CreateContext(new Version(major, minor))) + using (var ctx = Fixture.CreateContext()) { var _ = ctx.SomeEntities.Select(e => e.SomeList.Concat(e.SomeList)).ToList(); AssertContainsInSql("SELECT (e.\"SomeList\" || e.\"SomeList\")"); } } - [Theory] - [InlineData(7, 4)] - public void Array_Concat_with_list_column(int major, int minor) + [Fact] + public void Array_Concat_with_list_column() { - using (var ctx = Fixture.CreateContext(new Version(major, minor))) + using (var ctx = Fixture.CreateContext()) { var _ = ctx.SomeEntities.Select(e => e.SomeArray.Concat(e.SomeList)).ToList(); AssertContainsInSql("SELECT (e.\"SomeArray\" || e.\"SomeList\")"); } } - [Theory] - [InlineData(7, 4)] - public void List_Concat_with_array_column(int major, int minor) + [Fact] + public void List_Concat_with_array_column() { - using (var ctx = Fixture.CreateContext(new Version(major, minor))) + using (var ctx = Fixture.CreateContext()) { var _ = ctx.SomeEntities.Select(e => e.SomeList.Concat(e.SomeArray)).ToList(); AssertContainsInSql("SELECT (e.\"SomeList\" || e.\"SomeArray\")"); @@ -605,44 +499,44 @@ public void List_Concat_with_array_column(int major, int minor) // .NET 4.6.1 doesn't include the Enumerable.Append and Enumerable.Prepend functions... #if !NET461 - [Theory] - [InlineData(7, 4)] - public void Array_Append_constant(int major, int minor) + [Fact] + + public void Array_Append_constant() { - using (var ctx = Fixture.CreateContext(new Version(major, minor))) + using (var ctx = Fixture.CreateContext()) { var _ = ctx.SomeEntities.Select(e => e.SomeArray.Append(0)).ToList(); AssertContainsInSql("SELECT (e.\"SomeArray\" || 0)"); } } - [Theory] - [InlineData(7, 4)] - public void List_Append_constant(int major, int minor) + [Fact] + + public void List_Append_constant() { - using (var ctx = Fixture.CreateContext(new Version(major, minor))) + using (var ctx = Fixture.CreateContext()) { var _ = ctx.SomeEntities.Select(e => e.SomeList.Append(0)).ToList(); AssertContainsInSql("SELECT (e.\"SomeList\" || 0)"); } } - [Theory] - [InlineData(7, 4)] - public void Array_Prepend_constant(int major, int minor) + [Fact] + + public void Array_Prepend_constant() { - using (var ctx = Fixture.CreateContext(new Version(major, minor))) + using (var ctx = Fixture.CreateContext()) { var _ = ctx.SomeEntities.Select(e => e.SomeArray.Prepend(0)).ToList(); AssertContainsInSql("SELECT (0 || e.\"SomeArray\")"); } } - [Theory] - [InlineData(7, 4)] - public void List_Prepend_constant(int major, int minor) + [Fact] + + public void List_Prepend_constant() { - using (var ctx = Fixture.CreateContext(new Version(major, minor))) + using (var ctx = Fixture.CreateContext()) { var _ = ctx.SomeEntities.Select(e => e.SomeList.Prepend(0)).ToList(); AssertContainsInSql("SELECT (0 || e.\"SomeList\")"); @@ -666,17 +560,6 @@ public void Array_ArrayFill_constant(int major, int minor) } } - [Theory] - [InlineData(8, 4)] - public void Matrix_ArrayFill_constant(int major, int minor) - { - using (var ctx = Fixture.CreateContext(new Version(major, minor))) - { - var _ = ctx.SomeEntities.Select(e => EF.Functions.MatrixFill(e.Id, new[] { 1, 2 })).ToList(); - AssertContainsInSql("SELECT array_fill(e.\"Id\", ARRAY[1,2]::integer[])"); - } - } - [Theory] [InlineData(8, 4)] public void List_ListFill_constant(int major, int minor) @@ -702,16 +585,6 @@ public void Array_ArrayDimensions_column() } } - [Fact] - public void Matrix_ArrayDimensions_column() - { - using (var ctx = Fixture.CreateContext()) - { - var _ = ctx.SomeEntities.Select(e => EF.Functions.ArrayDimensions(e.SomeMatrix)).ToList(); - AssertContainsInSql("SELECT array_dims(e.\"SomeMatrix\")"); - } - } - [Fact] public void List_ArrayDimensions_column() { @@ -724,72 +597,53 @@ public void List_ArrayDimensions_column() #endregion - #region array_length, cardinality + #region array_length - [Theory] - [InlineData(8, 4, "COALESCE(array_length(e.\"SomeArray\", 1), 0)")] - [InlineData(9, 4, "cardinality(e.\"SomeArray\")")] - public void Array_Length(int major, int minor, string sql) + [Fact] + public void Array_Length() { - using (var ctx = Fixture.CreateContext(new Version(major, minor))) + using (var ctx = Fixture.CreateContext()) { var _ = ctx.SomeEntities.Where(e => e.SomeArray.Length == 2).ToList(); - AssertContainsInSql($"WHERE {sql} = 2"); + AssertContainsInSql("WHERE COALESCE(array_length(e.\"SomeArray\", 1), 0) = 2"); } } - [Theory] - [InlineData(8, 4)] - public void List_Length(int major, int minor) + [Fact] + public void List_Length() { - using (var ctx = Fixture.CreateContext(new Version(major, minor))) + 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"); } } - [Theory] - [InlineData(8, 4, "(COALESCE(array_length(e.\"SomeMatrix\", 1), 0) * COALESCE(array_length(e.\"SomeMatrix\", 2), 0))")] - [InlineData(9, 4, "cardinality(e.\"SomeMatrix\")")] - public void Matrix_Length(int major, int minor, string sql) - { - using (var ctx = Fixture.CreateContext(new Version(major, minor))) - { - var _ = ctx.SomeEntities.Where(e => e.SomeMatrix.Length == 2).ToList(); - AssertContainsInSql($"WHERE {sql} = 2"); - } - } - - [Theory] - [InlineData(8, 4)] - public void Array_GetLength(int major, int minor) + [Fact] + public void Array_GetLength() { - using (var ctx = Fixture.CreateContext(new Version(major, minor))) + 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"); } } - [Theory] - [InlineData(8, 4, "COALESCE(array_length(e.\"SomeArray\", 1), 0)")] - [InlineData(9, 4, "cardinality(e.\"SomeArray\")")] - public void Array_Count(int major, int minor, string sql) + [Fact] + public void Array_Count() { - using (var ctx = Fixture.CreateContext(new Version(major, minor))) + using (var ctx = Fixture.CreateContext()) { // ReSharper disable once UseCollectionCountProperty var _ = ctx.SomeEntities.Where(e => e.SomeArray.Count() == 1).ToArray(); - AssertContainsInSql($"WHERE {sql} = 1"); + AssertContainsInSql("WHERE COALESCE(array_length(e.\"SomeArray\", 1), 0) = 1"); } } - [Theory] - [InlineData(8, 4)] - public void List_Count(int major, int minor) + [Fact] + public void List_Count() { - using (var ctx = Fixture.CreateContext(new Version(major, minor))) + using (var ctx = Fixture.CreateContext()) { // ReSharper disable once UseCollectionCountProperty var _ = ctx.SomeEntities.Where(e => e.SomeList.Count() == 1).ToArray(); @@ -980,17 +834,6 @@ public void Array_ArrayReplace_column(int major, int minor) } } - [Theory] - [InlineData(9, 3)] - public void Matrix_ArrayReplace_column(int major, int minor) - { - using (var ctx = Fixture.CreateContext(new Version(major, minor))) - { - var _ = ctx.SomeEntities.Select(e => EF.Functions.ArrayReplace(e.SomeMatrix, 0, 1)).ToList(); - AssertContainsInSql("SELECT array_replace(e.\"SomeMatrix\", 0, 1)"); - } - } - [Theory] [InlineData(9, 3)] public void List_ArrayReplace_column(int major, int minor) @@ -1033,18 +876,6 @@ public void Array_ArrayToString(int major, int minor) } } - [Theory] - [InlineData(7, 4)] - [InlineData(9, 1)] - public void Matrix_ArrayToString(int major, int minor) - { - using (var ctx = Fixture.CreateContext(new Version(major, minor))) - { - var _ = ctx.SomeEntities.Select(e => EF.Functions.ArrayToString(e.SomeMatrix, "*")).ToList(); - AssertContainsInSql("SELECT array_to_string(e.\"SomeMatrix\", '*')"); - } - } - [Theory] [InlineData(7, 4)] [InlineData(9, 1)] @@ -1068,17 +899,6 @@ public void Array_ArrayToString_with_null(int major, int minor) } } - [Theory] - [InlineData(9, 1)] - public void Matrix_ArrayToString_with_null(int major, int minor) - { - using (var ctx = Fixture.CreateContext(new Version(major, minor))) - { - var _ = ctx.SomeEntities.Select(e => EF.Functions.ArrayToString(e.SomeMatrix, "*", ";")).ToList(); - AssertContainsInSql("SELECT array_to_string(e.\"SomeMatrix\", '*', ';')"); - } - } - [Theory] [InlineData(9, 1)] public void List_ArrayToString_with_null(int major, int minor) 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 + } +} From 2ea34f16566ffe69a4d5fef51cb9eba5c208eb5a Mon Sep 17 00:00:00 2001 From: Austin Drenski Date: Wed, 12 Dec 2018 00:48:49 -0500 Subject: [PATCH 3/4] Require in-support version for array translations --- .../Internal/NpgsqlArrayMemberTranslator.cs | 19 ++- .../NpgsqlArrayMethodCallTranslator.cs | 34 ++--- .../Query/ArrayQueryTest.cs | 123 ++++++++---------- 3 files changed, 77 insertions(+), 99 deletions(-) diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayMemberTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayMemberTranslator.cs index aab9a6c0a..8ab2f55d1 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayMemberTranslator.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayMemberTranslator.cs @@ -29,13 +29,18 @@ public class NpgsqlArrayMemberTranslator : IMemberTranslator /// public Expression Translate(MemberExpression e) - => ArrayInstanceHandler(e) ?? - ListInstanceHandler(e); + { + if (!VersionAtLeast(9, 4)) + return null; + + return ArrayInstanceHandler(e) ?? + ListInstanceHandler(e); + } #region Handlers [CanBeNull] - Expression ArrayInstanceHandler([NotNull] MemberExpression e) + static Expression ArrayInstanceHandler([NotNull] MemberExpression e) { var instance = e.Expression; @@ -44,7 +49,7 @@ Expression ArrayInstanceHandler([NotNull] MemberExpression e) switch (e.Member.Name) { - case nameof(Array.Length) when VersionAtLeast(8, 4): + case nameof(Array.Length): return Expression.Coalesce( new SqlFunctionExpression( "array_length", @@ -52,7 +57,7 @@ Expression ArrayInstanceHandler([NotNull] MemberExpression e) new[] { instance, Expression.Constant(1) }), Expression.Constant(0)); - case nameof(Array.Rank) when VersionAtLeast(8, 4): + case nameof(Array.Rank): return Expression.Coalesce( new SqlFunctionExpression( "array_ndims", @@ -66,7 +71,7 @@ Expression ArrayInstanceHandler([NotNull] MemberExpression e) } [CanBeNull] - Expression ListInstanceHandler([NotNull] MemberExpression e) + static Expression ListInstanceHandler([NotNull] MemberExpression e) { var instance = e.Expression; @@ -75,7 +80,7 @@ Expression ListInstanceHandler([NotNull] MemberExpression e) switch (e.Member.Name) { - case nameof(IList.Count) when VersionAtLeast(8, 4): + case nameof(IList.Count): return Expression.Coalesce( new SqlFunctionExpression( "array_length", diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayMethodCallTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayMethodCallTranslator.cs index aeeea6d35..2018f35c6 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayMethodCallTranslator.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayMethodCallTranslator.cs @@ -36,6 +36,9 @@ public class NpgsqlArrayMethodCallTranslator : IMethodCallTranslator [CanBeNull] public Expression Translate(MethodCallExpression e) { + if (!VersionAtLeast(9, 4)) + return null; + var declaringType = e.Method.DeclaringType; if (declaringType != null && @@ -68,7 +71,7 @@ Expression EnumerableHandler([NotNull] MethodCallExpression e) switch (e.Method.Name) { - case nameof(Enumerable.Count) when VersionAtLeast(8, 4): + case nameof(Enumerable.Count): return Expression.Coalesce( new SqlFunctionExpression( "array_length", @@ -104,19 +107,19 @@ Expression NpgsqlArrayExtensionsHandler([NotNull] MethodCallExpression e) switch (e.Method.Name) { - case nameof(NpgsqlArrayExtensions.Contains) when VersionAtLeast(8, 2): + case nameof(NpgsqlArrayExtensions.Contains): return new CustomBinaryExpression(e.Arguments[1], e.Arguments[2], "@>", typeof(bool)); - case nameof(NpgsqlArrayExtensions.ContainedBy) when VersionAtLeast(8, 2): + case nameof(NpgsqlArrayExtensions.ContainedBy): return new CustomBinaryExpression(e.Arguments[1], e.Arguments[2], "<@", typeof(bool)); - case nameof(NpgsqlArrayExtensions.Overlaps) when VersionAtLeast(8, 2): + case nameof(NpgsqlArrayExtensions.Overlaps): return new CustomBinaryExpression(e.Arguments[1], e.Arguments[2], "&&", typeof(bool)); - case nameof(NpgsqlArrayExtensions.ArrayFill) when VersionAtLeast(8, 4): + case nameof(NpgsqlArrayExtensions.ArrayFill): return new SqlFunctionExpression("array_fill", e.Method.ReturnType, e.Arguments.Skip(1)); - case nameof(NpgsqlArrayExtensions.ListFill) when VersionAtLeast(8, 4): + case nameof(NpgsqlArrayExtensions.ListFill): return new SqlFunctionExpression("array_fill", e.Method.ReturnType, e.Arguments.Skip(1)); case nameof(NpgsqlArrayExtensions.ArrayDimensions): @@ -125,29 +128,20 @@ Expression NpgsqlArrayExtensionsHandler([NotNull] MethodCallExpression e) case nameof(NpgsqlArrayExtensions.ArrayPositions) when VersionAtLeast(9, 5): return new SqlFunctionExpression("array_positions", e.Method.ReturnType, e.Arguments.Skip(1)); - case nameof(NpgsqlArrayExtensions.ArrayRemove) when VersionAtLeast(9, 3): + case nameof(NpgsqlArrayExtensions.ArrayRemove): return new SqlFunctionExpression("array_remove", e.Method.ReturnType, e.Arguments.Skip(1).Take(2)); - case nameof(NpgsqlArrayExtensions.ArrayReplace) when VersionAtLeast(9, 3): + case nameof(NpgsqlArrayExtensions.ArrayReplace): return new SqlFunctionExpression("array_replace", e.Method.ReturnType, e.Arguments.Skip(1).Take(3)); - case nameof(NpgsqlArrayExtensions.ArrayToString) when VersionAtLeast(9, 1): - return new SqlFunctionExpression("array_to_string", 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(2)); - - case nameof(NpgsqlArrayExtensions.StringToArray) when VersionAtLeast(9, 1): - return new SqlFunctionExpression("string_to_array", e.Method.ReturnType, e.Arguments.Skip(1).Take(3)); + 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(2)); - - case nameof(NpgsqlArrayExtensions.StringToList) when VersionAtLeast(9, 1): 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(2)); + return new SqlFunctionExpression("string_to_array", e.Method.ReturnType, e.Arguments.Skip(1).Take(3)); default: return null; @@ -189,7 +183,7 @@ Expression ArrayInstanceHandler([NotNull] MethodCallExpression e) switch (e.Method.Name) { - case nameof(Array.GetLength) when VersionAtLeast(8, 4): + case nameof(Array.GetLength): return Expression.Coalesce( new SqlFunctionExpression( "array_length", diff --git a/test/EFCore.PG.FunctionalTests/Query/ArrayQueryTest.cs b/test/EFCore.PG.FunctionalTests/Query/ArrayQueryTest.cs index 678eab818..f8f4e654a 100644 --- a/test/EFCore.PG.FunctionalTests/Query/ArrayQueryTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/ArrayQueryTest.cs @@ -549,22 +549,20 @@ public void List_Prepend_constant() #region array_fill - [Theory] - [InlineData(8, 4)] - public void Array_ArrayFill_constant(int major, int minor) + [Fact] + public void Array_ArrayFill_constant() { - using (var ctx = Fixture.CreateContext(new Version(major, minor))) + 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[])"); } } - [Theory] - [InlineData(8, 4)] - public void List_ListFill_constant(int major, int minor) + [Fact] + public void List_ListFill_constant() { - using (var ctx = Fixture.CreateContext(new Version(major, minor))) + 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[])"); @@ -715,11 +713,10 @@ public void Array_GetLength_on_literal_not_translated() #region array_lower - [Theory] - [InlineData(7, 4)] - public void Array_ArrayLower(int major, int minor) + [Fact] + public void Array_ArrayLower() { - using (var ctx = Fixture.CreateContext(new Version(major, minor))) + 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"); @@ -730,11 +727,10 @@ public void Array_ArrayLower(int major, int minor) #region array_ndims - [Theory] - [InlineData(8, 4)] - public void Array_Rank(int major, int minor) + [Fact] + public void Array_Rank() { - using (var ctx = Fixture.CreateContext(new Version(major, minor))) + using (var ctx = Fixture.CreateContext()) { var _ = ctx.SomeEntities.Where(e => e.SomeArray.Rank == 2).ToList(); AssertContainsInSql("WHERE COALESCE(array_ndims(e.\"SomeArray\"), 1) = 2"); @@ -797,22 +793,20 @@ public void List_ListPositions_column(int major, int minor) #region array_remove - [Theory] - [InlineData(9, 3)] - public void Array_ArrayRemove_column(int major, int minor) + [Fact] + public void Array_ArrayRemove_column() { - using (var ctx = Fixture.CreateContext(new Version(major, minor))) + using (var ctx = Fixture.CreateContext()) { var _ = ctx.SomeEntities.Select(e => EF.Functions.ArrayRemove(e.SomeArray, 0)).ToList(); AssertContainsInSql("SELECT array_remove(e.\"SomeArray\", 0)"); } } - [Theory] - [InlineData(9, 3)] - public void List_ArrayRemove_column(int major, int minor) + [Fact] + public void List_ArrayRemove_column() { - using (var ctx = Fixture.CreateContext(new Version(major, minor))) + using (var ctx = Fixture.CreateContext()) { var _ = ctx.SomeEntities.Select(e => EF.Functions.ArrayRemove(e.SomeList, 0)).ToList(); AssertContainsInSql("SELECT array_remove(e.\"SomeList\", 0)"); @@ -823,22 +817,20 @@ public void List_ArrayRemove_column(int major, int minor) #region array_replace - [Theory] - [InlineData(9, 3)] - public void Array_ArrayReplace_column(int major, int minor) + [Fact] + public void Array_ArrayReplace_column() { - using (var ctx = Fixture.CreateContext(new Version(major, minor))) + 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)"); } } - [Theory] - [InlineData(9, 3)] - public void List_ArrayReplace_column(int major, int minor) + [Fact] + public void List_ArrayReplace_column() { - using (var ctx = Fixture.CreateContext(new Version(major, minor))) + 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)"); @@ -849,11 +841,10 @@ public void List_ArrayReplace_column(int major, int minor) #region array_upper - [Theory] - [InlineData(7, 4)] - public void Array_ArrayUpper(int major, int minor) + [Fact] + public void Array_ArrayUpper() { - using (var ctx = Fixture.CreateContext(new Version(major, minor))) + 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"); @@ -864,46 +855,40 @@ public void Array_ArrayUpper(int major, int minor) #region array_to_string - [Theory] - [InlineData(7, 4)] - [InlineData(9, 1)] - public void Array_ArrayToString(int major, int minor) + [Fact] + public void Array_ArrayToString() { - using (var ctx = Fixture.CreateContext(new Version(major, minor))) + using (var ctx = Fixture.CreateContext()) { var _ = ctx.SomeEntities.Select(e => EF.Functions.ArrayToString(e.SomeArray, "*")).ToList(); AssertContainsInSql("SELECT array_to_string(e.\"SomeArray\", '*')"); } } - [Theory] - [InlineData(7, 4)] - [InlineData(9, 1)] - public void List_ArrayToString(int major, int minor) + [Fact] + public void List_ArrayToString() { - using (var ctx = Fixture.CreateContext(new Version(major, minor))) + using (var ctx = Fixture.CreateContext()) { var _ = ctx.SomeEntities.Select(e => EF.Functions.ArrayToString(e.SomeList, "*")).ToList(); AssertContainsInSql("SELECT array_to_string(e.\"SomeList\", '*')"); } } - [Theory] - [InlineData(9, 1)] - public void Array_ArrayToString_with_null(int major, int minor) + [Fact] + public void Array_ArrayToString_with_null() { - using (var ctx = Fixture.CreateContext(new Version(major, minor))) + using (var ctx = Fixture.CreateContext()) { var _ = ctx.SomeEntities.Select(e => EF.Functions.ArrayToString(e.SomeArray, "*", ";")).ToList(); AssertContainsInSql("SELECT array_to_string(e.\"SomeArray\", '*', ';')"); } } - [Theory] - [InlineData(9, 1)] - public void List_ArrayToString_with_null(int major, int minor) + [Fact] + public void List_ArrayToString_with_null() { - using (var ctx = Fixture.CreateContext(new Version(major, minor))) + using (var ctx = Fixture.CreateContext()) { var _ = ctx.SomeEntities.Select(e => EF.Functions.ArrayToString(e.SomeList, "*", ";")).ToList(); AssertContainsInSql("SELECT array_to_string(e.\"SomeList\", '*', ';')"); @@ -914,46 +899,40 @@ public void List_ArrayToString_with_null(int major, int minor) #region string_to_array - [Theory] - [InlineData(7, 4)] - [InlineData(9, 1)] - public void Array_StringToArray(int major, int minor) + [Fact] + public void Array_StringToArray() { - using (var ctx = Fixture.CreateContext(new Version(major, minor))) + 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"); } } - [Theory] - [InlineData(7, 4)] - [InlineData(9, 1)] - public void List_StringToList(int major, int minor) + [Fact] + public void List_StringToList() { - using (var ctx = Fixture.CreateContext(new Version(major, minor))) + 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"); } } - [Theory] - [InlineData(9, 1)] - public void Array_StringToArray_with_null_string(int major, int minor) + [Fact] + public void Array_StringToArray_with_null_string() { - using (var ctx = Fixture.CreateContext(new Version(major, minor))) + 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"); } } - [Theory] - [InlineData(9, 1)] - public void List_StringToList_with_null_string(int major, int minor) + [Fact] + public void List_StringToList_with_null_string() { - using (var ctx = Fixture.CreateContext(new Version(major, minor))) + 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"); From 97f4ae581a923101140b6e130fcca5b91bcf9de7 Mon Sep 17 00:00:00 2001 From: Austin Drenski Date: Wed, 12 Dec 2018 01:58:39 -0500 Subject: [PATCH 4/4] Rework VisitBinary to limit sub-visits --- .../NpgsqlSqlTranslatingExpressionVisitor.cs | 39 ++++++++++++------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlSqlTranslatingExpressionVisitor.cs b/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlSqlTranslatingExpressionVisitor.cs index 238747f0d..e849b85a8 100644 --- a/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlSqlTranslatingExpressionVisitor.cs @@ -41,24 +41,37 @@ public NpgsqlSqlTranslatingExpressionVisitor( #region Overrides /// - protected override Expression VisitBinary(BinaryExpression expression) + protected override Expression VisitBinary(BinaryExpression e) { - var left = Visit(expression.Left); - var right = Visit(expression.Right); + switch (e.NodeType) + { + case ExpressionType.ArrayIndex: + if (MemberAccessBindingExpressionVisitor.GetPropertyPath(e.Left, QueryCompilationContext, out _).Count == 0) + goto default; - if (left == null || right == null) - return base.VisitBinary(expression); + var leftArrayIndex = Visit(e.Left); + var rightArrayIndex = Visit(e.Right); - if (MemberAccessBindingExpressionVisitor.GetPropertyPath(expression.Left, QueryCompilationContext, out _).Count != 0) - { - if (expression.NodeType == ExpressionType.ArrayIndex) - return Expression.ArrayIndex(left, right); + if (leftArrayIndex == null || rightArrayIndex == null) + goto default; - if (expression.NodeType == ExpressionType.Index) - return Expression.ArrayAccess(left, right); - } + 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); - return base.VisitBinary(expression); + if (leftArrayAccess == null || rightArrayAccess == null) + goto default; + + return Expression.ArrayAccess(leftArrayAccess, rightArrayAccess); + + default: + return base.VisitBinary(e); + } } ///