From cebb691f4e4489509809d235f05e27b7d4635e26 Mon Sep 17 00:00:00 2001 From: Austin Drenski Date: Sun, 27 May 2018 15:27:37 -0400 Subject: [PATCH 01/16] Map IList operations to PostgreSQL arrays - First goal: feature parity with mapped `T[]` operations. - 10 of 10 unit tests passing: - Passes - Roundtrip - SequenceEqual - Length/Count - Contains/Any - Indexers - Issues: - Blocked by #430 - There should be a better system for these types of mappings. - Along the lines of `IMethodCallTranslator`. --- .../NpgsqlArraySequenceEqualTranslator.cs | 26 +- .../NpgsqlCompositeMethodCallTranslator.cs | 3 +- .../Internal/NpgsqlListTranslator.cs | 92 +++++ .../NpgsqlSqlTranslatingExpressionVisitor.cs | 17 +- .../ExplicitStoreTypeCastExpression.cs | 2 + .../Sql/Internal/NpgsqlQuerySqlGenerator.cs | 78 ++-- .../Internal/Mapping/NpgsqlListTypeMapping.cs | 49 ++- .../Query/ArrayQueryTest.cs | 343 ++++++++++++++---- 8 files changed, 459 insertions(+), 151 deletions(-) create mode 100644 src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlListTranslator.cs diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArraySequenceEqualTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArraySequenceEqualTranslator.cs index 3fb959ff5..863ac10d1 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArraySequenceEqualTranslator.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArraySequenceEqualTranslator.cs @@ -1,4 +1,5 @@ #region License + // The PostgreSQL License // // Copyright (C) 2016 The Npgsql Development Team @@ -19,8 +20,10 @@ // AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS // ON AN "AS IS" BASIS, AND THE NPGSQL DEVELOPMENT TEAM HAS NO OBLIGATIONS // TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + #endregion +using System.Collections; using System.Linq; using System.Linq.Expressions; using System.Reflection; @@ -37,10 +40,13 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Inte /// public class NpgsqlArraySequenceEqualTranslator : IMethodCallTranslator { - static readonly MethodInfo SequenceEqualMethodInfo = typeof(Enumerable).GetTypeInfo().GetDeclaredMethods(nameof(Enumerable.SequenceEqual)).Single(m => - m.IsGenericMethodDefinition && - m.GetParameters().Length == 2 - ); + 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) @@ -48,12 +54,12 @@ 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]); - } + methodCallExpression.Arguments.All(a => a.Type.IsArray || typeof(IList).IsAssignableFrom(a.Type))) + return + Expression.MakeBinary( + ExpressionType.Equal, + methodCallExpression.Arguments[0], + methodCallExpression.Arguments[1]); return null; } diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlCompositeMethodCallTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlCompositeMethodCallTranslator.cs index 131ac6988..03d0e6e74 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlCompositeMethodCallTranslator.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlCompositeMethodCallTranslator.cs @@ -60,7 +60,8 @@ public class NpgsqlCompositeMethodCallTranslator : RelationalCompositeMethodCall new NpgsqlRegexIsMatchTranslator(), new NpgsqlFullTextSearchMethodTranslator(), new NpgsqlRangeTranslator(), - new NpgsqlNetworkTranslator() + new NpgsqlNetworkTranslator(), + new NpgsqlListTranslator() }; /// diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlListTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlListTranslator.cs new file mode 100644 index 000000000..d0797311e --- /dev/null +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlListTranslator.cs @@ -0,0 +1,92 @@ +#region License + +// The PostgreSQL License +// +// Copyright (C) 2016 The Npgsql Development Team +// +// Permission to use, copy, modify, and distribute this software and its +// documentation for any purpose, without fee, and without a written +// agreement is hereby granted, provided that the above copyright notice +// and this paragraph and the following two paragraphs appear in all copies. +// +// IN NO EVENT SHALL THE NPGSQL DEVELOPMENT TEAM BE LIABLE TO ANY PARTY +// FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, +// INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS +// DOCUMENTATION, EVEN IF THE NPGSQL DEVELOPMENT TEAM HAS BEEN ADVISED OF +// THE POSSIBILITY OF SUCH DAMAGE. +// +// THE NPGSQL DEVELOPMENT TEAM SPECIFICALLY DISCLAIMS ANY WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS +// ON AN "AS IS" BASIS, AND THE NPGSQL DEVELOPMENT TEAM HAS NO OBLIGATIONS +// TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + +#endregion + +using System.Collections; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Reflection; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Query.ExpressionTranslators; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Internal +{ + /// + /// Provides translation services for methods as PostgreSQL array operators. + /// + /// + /// See: https://www.postgresql.org/docs/current/static/functions-array.html + /// + public class NpgsqlListTranslator : IMethodCallTranslator + { + /// + [CanBeNull] + public Expression Translate(MethodCallExpression expression) + { + if (!typeof(IList).IsAssignableFrom(expression.Method.DeclaringType)) + return null; + + switch (expression.Method.Name) + { + case "get_Item" when expression.Object is Expression instance: + return Expression.MakeIndex(instance, instance.Type.GetRuntimeProperty("Item"), expression.Arguments); +// case nameof(NpgsqlListExtensions.Contains): +// return new CustomBinaryExpression(expression.Arguments[0], expression.Arguments[1], "@>", typeof(bool)); +// +// case nameof(NpgsqlListExtensions.ContainedBy): +// return new CustomBinaryExpression(expression.Arguments[0], expression.Arguments[1], "<@", typeof(bool)); +// +// case nameof(NpgsqlListExtensions.Overlaps): +// return new CustomBinaryExpression(expression.Arguments[0], expression.Arguments[1], "&&", typeof(bool)); +// +// case nameof(NpgsqlListExtensions.IsStrictlyLeftOf): +// return new CustomBinaryExpression(expression.Arguments[0], expression.Arguments[1], "<<", typeof(bool)); +// +// case nameof(NpgsqlListExtensions.IsStrictlyRightOf): +// return new CustomBinaryExpression(expression.Arguments[0], expression.Arguments[1], ">>", typeof(bool)); +// +// case nameof(NpgsqlListExtensions.DoesNotExtendRightOf): +// return new CustomBinaryExpression(expression.Arguments[0], expression.Arguments[1], "&<", typeof(bool)); +// +// case nameof(NpgsqlListExtensions.DoesNotExtendLeftOf): +// return new CustomBinaryExpression(expression.Arguments[0], expression.Arguments[1], "&>", typeof(bool)); +// +// case nameof(NpgsqlListExtensions.IsAdjacentTo): +// return new CustomBinaryExpression(expression.Arguments[0], expression.Arguments[1], "-|-", typeof(bool)); +// +// case nameof(NpgsqlListExtensions.Union): +// return new CustomBinaryExpression(expression.Arguments[0], expression.Arguments[1], "+", expression.Arguments[0].Type); +// +// case nameof(NpgsqlListExtensions.Intersect): +// return new CustomBinaryExpression(expression.Arguments[0], expression.Arguments[1], "*", expression.Arguments[0].Type); +// +// case nameof(NpgsqlListExtensions.Except): +// return new CustomBinaryExpression(expression.Arguments[0], expression.Arguments[1], "-", expression.Arguments[0].Type); + + default: + return null; + } + } + } +} diff --git a/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlSqlTranslatingExpressionVisitor.cs b/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlSqlTranslatingExpressionVisitor.cs index c50263d19..71003b5e9 100644 --- a/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlSqlTranslatingExpressionVisitor.cs @@ -23,6 +23,7 @@ #endregion +using System.Collections; using System.Linq; using System.Linq.Expressions; using System.Reflection; @@ -39,6 +40,9 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionVisitors { + /// + /// The default relational LINQ translating expression visitor for Npgsql. + /// public class NpgsqlSqlTranslatingExpressionVisitor : SqlTranslatingExpressionVisitor { /// @@ -87,6 +91,7 @@ public NpgsqlSqlTranslatingExpressionVisitor( => _queryModelVisitor = queryModelVisitor; /// + [CanBeNull] protected override Expression VisitSubQuery(SubQueryExpression expression) => base.VisitSubQuery(expression) ?? VisitLikeAnyAll(expression) ?? VisitEqualsAny(expression); @@ -133,19 +138,16 @@ protected virtual Expression VisitEqualsAny([NotNull] SubQueryExpression express if (properties.Count == 0) return null; var lastPropertyType = properties[properties.Count - 1].ClrType; - if (lastPropertyType.IsArray && lastPropertyType.GetArrayRank() == 1 && subQueryModel.ResultOperators.Count > 0) + if (typeof(IList).IsAssignableFrom(lastPropertyType) && subQueryModel.ResultOperators.Count > 0) { // Translate someArray.Length if (subQueryModel.ResultOperators.First() is CountResultOperator) - return Expression.ArrayLength(Visit(fromExpression)); + return new SqlFunctionExpression("array_length", typeof(int), new[] { Visit(fromExpression), Expression.Constant(1) }); // Translate someArray.Contains(someValue) if (subQueryModel.ResultOperators.First() is ContainsResultOperator contains) - { - var containsItem = Visit(contains.Item); - if (containsItem != null) - return new ArrayAnyAllExpression(ArrayComparisonType.ANY, "=", containsItem, Visit(fromExpression)); - } + if (Visit(contains.Item) is Expression containsItem && Visit(fromExpression) is Expression source) + return new ArrayAnyAllExpression(ArrayComparisonType.ANY, "=", containsItem, source); } return null; @@ -214,7 +216,6 @@ whereClause.Predicate is MethodCallExpression methocCall default: return null; } - // ReSharper restore AssignNullToNotNullAttribute } } } diff --git a/src/EFCore.PG/Query/Expressions/Internal/ExplicitStoreTypeCastExpression.cs b/src/EFCore.PG/Query/Expressions/Internal/ExplicitStoreTypeCastExpression.cs index 68e2352bb..7191f2c1c 100644 --- a/src/EFCore.PG/Query/Expressions/Internal/ExplicitStoreTypeCastExpression.cs +++ b/src/EFCore.PG/Query/Expressions/Internal/ExplicitStoreTypeCastExpression.cs @@ -1,4 +1,5 @@ #region License + // The PostgreSQL License // // Copyright (C) 2016 The Npgsql Development Team @@ -19,6 +20,7 @@ // AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS // ON AN "AS IS" BASIS, AND THE NPGSQL DEVELOPMENT TEAM HAS NO OBLIGATIONS // TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + #endregion using System; diff --git a/src/EFCore.PG/Query/Sql/Internal/NpgsqlQuerySqlGenerator.cs b/src/EFCore.PG/Query/Sql/Internal/NpgsqlQuerySqlGenerator.cs index f9728bfe2..eb14f2fc9 100644 --- a/src/EFCore.PG/Query/Sql/Internal/NpgsqlQuerySqlGenerator.cs +++ b/src/EFCore.PG/Query/Sql/Internal/NpgsqlQuerySqlGenerator.cs @@ -39,26 +39,30 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.Sql.Internal { public class NpgsqlQuerySqlGenerator : DefaultQuerySqlGenerator { + // ReSharper disable once NotAccessedField.Local readonly bool _reverseNullOrderingEnabled; + /// protected override string TypedTrueLiteral { get; } = "TRUE::bool"; + /// protected override string TypedFalseLiteral { get; } = "FALSE::bool"; + /// public NpgsqlQuerySqlGenerator( [NotNull] QuerySqlGeneratorDependencies dependencies, [NotNull] SelectExpression selectExpression, bool reverseNullOrderingEnabled) : base(dependencies, selectExpression) - { - _reverseNullOrderingEnabled = reverseNullOrderingEnabled; - } + => _reverseNullOrderingEnabled = reverseNullOrderingEnabled; + /// protected override void GenerateTop(SelectExpression selectExpression) { // No TOP() in PostgreSQL, see GenerateLimitOffset } + /// protected override void GenerateLimitOffset(SelectExpression selectExpression) { Check.NotNull(selectExpression, nameof(selectExpression)); @@ -81,6 +85,7 @@ protected override void GenerateLimitOffset(SelectExpression selectExpression) } } + /// public override Expression VisitSqlFunction(SqlFunctionExpression sqlFunctionExpression) { var expr = base.VisitSqlFunction(sqlFunctionExpression); @@ -108,6 +113,7 @@ public override Expression VisitSqlFunction(SqlFunctionExpression sqlFunctionExp return expr; } + /// protected override Expression VisitBinary(BinaryExpression expression) { switch (expression.NodeType) @@ -126,19 +132,23 @@ protected override Expression VisitBinary(BinaryExpression expression) return exp; } - break; + goto default; } case ExpressionType.ArrayIndex: VisitArrayIndex(expression); return expression; - } - return base.VisitBinary(expression); + default: + return base.VisitBinary(expression); + } } + /// protected override Expression VisitUnary(UnaryExpression expression) { + // TODO: I don't think this is called any longer. + // Handled by NpgsqlSqlTranslatingExpressionVisitor.VisitSubQuery. if (expression.NodeType == ExpressionType.ArrayLength) { VisitSqlFunction(new SqlFunctionExpression("array_length", typeof(int), new[] { expression.Operand, Expression.Constant(1) })); @@ -177,12 +187,29 @@ protected virtual void VisitArrayIndex([NotNull] BinaryExpression expression) Sql.Append(']'); } + /// + protected override Expression VisitIndex(IndexExpression expression) + { + // TODO: does this need wrapped? the array indexer is wrapped, but not by our code? + Sql.Append('('); + Visit(expression.Object); + for (int i = 0; i < expression.Arguments.Count; i++) + { + Sql.Append('['); + Visit(GenerateOneBasedIndexExpression(expression.Arguments[i])); + Sql.Append(']'); + } + + Sql.Append(')'); + + return expression; + } + /// /// Produces expressions like: 1 = ANY ('{0,1,2}') or 'cat' LIKE ANY ('{a%,b%,c%}'). /// public Expression VisitArrayAnyAll(ArrayAnyAllExpression arrayAnyAllExpression) { - Visit(arrayAnyAllExpression.Operand); Sql.Append(' '); Sql.Append(arrayAnyAllExpression.Operator); Sql.Append(' '); @@ -209,7 +236,6 @@ public Expression VisitRegexMatch([NotNull] RegexMatchExpression regexMatchExpre { Check.NotNull(regexMatchExpression, nameof(regexMatchExpression)); var options = regexMatchExpression.Options; - Visit(regexMatchExpression.Match); Sql.Append(" ~ "); @@ -223,33 +249,27 @@ public Expression VisitRegexMatch([NotNull] RegexMatchExpression regexMatchExpre Sql.Append("('(?"); if (options.HasFlag(RegexOptions.IgnoreCase)) Sql.Append('i'); - if (options.HasFlag(RegexOptions.Multiline)) Sql.Append('n'); else if (!options.HasFlag(RegexOptions.Singleline)) + // In .NET's default mode, . doesn't match newlines but PostgreSQL it does. Sql.Append('p'); - if (options.HasFlag(RegexOptions.IgnorePatternWhitespace)) Sql.Append('x'); - Sql.Append(")' || "); Visit(regexMatchExpression.Pattern); Sql.Append(')'); - return regexMatchExpression; } public Expression VisitAtTimeZone([NotNull] AtTimeZoneExpression atTimeZoneExpression) { Check.NotNull(atTimeZoneExpression, nameof(atTimeZoneExpression)); - Visit(atTimeZoneExpression.TimestampExpression); - Sql.Append(" AT TIME ZONE '"); Sql.Append(atTimeZoneExpression.TimeZone); Sql.Append('\''); - return atTimeZoneExpression; } @@ -259,13 +279,10 @@ public virtual Expression VisitILike(ILikeExpression iLikeExpression) //var parentTypeMapping = _typeMapping; //_typeMapping = InferTypeMappingFromColumn(iLikeExpression.Match) ?? parentTypeMapping; - Visit(iLikeExpression.Match); - Sql.Append(" ILIKE "); Visit(iLikeExpression.Pattern); - if (iLikeExpression.EscapeChar != null) { Sql.Append(" ESCAPE "); @@ -273,7 +290,6 @@ public virtual Expression VisitILike(ILikeExpression iLikeExpression) } //_typeMapping = parentTypeMapping; - return iLikeExpression; } @@ -283,18 +299,16 @@ public Expression VisitExplicitStoreTypeCast([NotNull] ExplicitStoreTypeCastExpr //var parentTypeMapping = _typeMapping; //_typeMapping = InferTypeMappingFromColumn(castExpression.Operand); - Visit(castExpression.Operand); - Sql.Append(" AS ") .Append(castExpression.StoreType) .Append(")"); //_typeMapping = parentTypeMapping; - return castExpression; } + /// protected override string GenerateOperator(Expression expression) { switch (expression.NodeType) @@ -303,26 +317,23 @@ protected override string GenerateOperator(Expression expression) if (expression.Type == typeof(string)) return " || "; goto default; - case ExpressionType.And: if (expression.Type == typeof(bool)) return " AND "; goto default; - case ExpressionType.Or: if (expression.Type == typeof(bool)) return " OR "; goto default; - default: return base.GenerateOperator(expression); } } + /// protected override void GenerateOrdering(Ordering ordering) { base.GenerateOrdering(ordering); - if (_reverseNullOrderingEnabled) Sql.Append( ordering.OrderingDirection == OrderingDirection.Asc @@ -333,7 +344,6 @@ protected override void GenerateOrdering(Ordering ordering) public virtual Expression VisitCustomBinary(CustomBinaryExpression expression) { Check.NotNull(expression, nameof(expression)); - Sql.Append('('); Visit(expression.Left); Sql.Append(' '); @@ -341,14 +351,12 @@ public virtual Expression VisitCustomBinary(CustomBinaryExpression expression) Sql.Append(' '); Visit(expression.Right); Sql.Append(')'); - return expression; } public virtual Expression VisitCustomUnary(CustomUnaryExpression expression) { Check.NotNull(expression, nameof(expression)); - if (expression.Postfix) { Visit(expression.Operand); @@ -366,22 +374,18 @@ public virtual Expression VisitCustomUnary(CustomUnaryExpression expression) public virtual Expression VisitPgFunction(PgFunctionExpression e) { //var parentTypeMapping = _typeMapping; - //_typeMapping = null; var wroteSchema = false; - if (e.Instance != null) { Visit(e.Instance); - Sql.Append("."); } else if (!string.IsNullOrWhiteSpace(e.Schema)) { Sql.Append(SqlGenerator.DelimitIdentifier(e.Schema)) .Append("."); - wroteSchema = true; } @@ -389,31 +393,25 @@ public virtual Expression VisitPgFunction(PgFunctionExpression e) wroteSchema ? SqlGenerator.DelimitIdentifier(e.FunctionName) : e.FunctionName); - Sql.Append("("); //_typeMapping = null; - GenerateList(e.PositionalArguments); - bool hasArguments = e.PositionalArguments.Count > 0 && e.NamedArguments.Count > 0; - foreach (var kv in e.NamedArguments) { if (hasArguments) Sql.Append(", "); else hasArguments = true; - Sql.Append(kv.Key) .Append(" => "); - Visit(kv.Value); } Sql.Append(")"); - //_typeMapping = parentTypeMapping; + //_typeMapping = parentTypeMapping; return e; } } diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlListTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlListTypeMapping.cs index 1f024448f..866bdeaec 100644 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlListTypeMapping.cs +++ b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlListTypeMapping.cs @@ -1,4 +1,5 @@ #region License + // The PostgreSQL License // // Copyright (C) 2016 The Npgsql Development Team @@ -19,9 +20,11 @@ // AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS // ON AN "AS IS" BASIS, AND THE NPGSQL DEVELOPMENT TEAM HAS NO OBLIGATIONS // TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + #endregion using System; +using System.Collections; using System.Text; using Microsoft.EntityFrameworkCore.Storage; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; @@ -36,49 +39,55 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping /// public class NpgsqlListTypeMapping : RelationalTypeMapping { + /// + /// The CLR type of the list items. + /// public RelationalTypeMapping ElementMapping { get; } /// - /// Creates the default array mapping (i.e. for the single-dimensional CLR array type) + /// Creates the default list mapping. /// public NpgsqlListTypeMapping(RelationalTypeMapping elementMapping, Type listType) - : this(elementMapping.StoreType + "[]", elementMapping, listType) - {} + : this(elementMapping.StoreType + "[]", elementMapping, listType) {} + /// NpgsqlListTypeMapping(string storeType, RelationalTypeMapping elementMapping, Type listType) - : base(new RelationalTypeMappingParameters( - new CoreTypeMappingParameters(listType, null, CreateComparer(elementMapping, listType)), storeType - )) - { - ElementMapping = elementMapping; - } + : base( + new RelationalTypeMappingParameters( + new CoreTypeMappingParameters(listType, null, CreateComparer(elementMapping, listType)), storeType)) + => ElementMapping = elementMapping; + /// protected NpgsqlListTypeMapping(RelationalTypeMappingParameters parameters, RelationalTypeMapping elementMapping) - : base(parameters) {} + : base(parameters) + => ElementMapping = elementMapping; + /// public override RelationalTypeMapping Clone(string storeType, int? size) => new NpgsqlListTypeMapping(StoreType, ElementMapping, ClrType); + /// public override CoreTypeMapping Clone(ValueConverter converter) => new NpgsqlListTypeMapping(Parameters.WithComposedConverter(converter), ElementMapping); + /// protected override string GenerateNonNullSqlLiteral(object value) { - // TODO: Duplicated from NpgsqlArrayTypeMapping - var arr = (Array)value; + var list = (IList)value; - if (arr.Rank != 1) + if (list.GetType().GenericTypeArguments[0] != ElementMapping.ClrType) throw new NotSupportedException("Multidimensional array literals aren't supported"); var sb = new StringBuilder(); sb.Append("ARRAY["); - for (var i = 0; i < arr.Length; i++) + for (var i = 0; i < list.Count; i++) { - sb.Append(ElementMapping.GenerateSqlLiteral(arr.GetValue(i))); - if (i < arr.Length - 1) - sb.Append(","); + if (i > 0) + sb.Append(','); + sb.Append(ElementMapping.GenerateSqlLiteral(list[i])); } - sb.Append("]"); + + sb.Append(']'); return sb.ToString(); } @@ -148,7 +157,7 @@ static List Snapshot(List source, ValueComparer elementComp class SingleDimComparerWithIEquatable : ValueComparer> where TElem : IEquatable { - public SingleDimComparerWithIEquatable(): base( + public SingleDimComparerWithIEquatable() : base( (a, b) => Compare(a, b), o => o.GetHashCode(), // TODO: Need to get hash code of elements... source => DoSnapshot(source)) {} @@ -171,6 +180,7 @@ static bool Compare(List a, List b) continue; return false; } + if (!elem1.Equals(elem2)) return false; } @@ -215,6 +225,7 @@ static bool Compare(List a, List b) continue; return false; } + if (!elem1.Equals(elem2)) return false; } diff --git a/test/EFCore.PG.FunctionalTests/Query/ArrayQueryTest.cs b/test/EFCore.PG.FunctionalTests/Query/ArrayQueryTest.cs index 2412ae223..ce5aecd79 100644 --- a/test/EFCore.PG.FunctionalTests/Query/ArrayQueryTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/ArrayQueryTest.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; using System.Linq; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.TestUtilities; @@ -10,10 +11,23 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query { - public class ArrayQueryTest : IClassFixture + public class ArrayQueryTest : IClassFixture { + #region ArrayTests + + [Fact] + public void Array_Roundtrip() + { + using (var ctx = 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 Roundtrip() + public void List_Roundtrip() { using (var ctx = CreateContext()) { @@ -24,7 +38,7 @@ public void Roundtrip() } [Fact] - public void Index_with_constant() + public void Array_Index_with_constant() { using (var ctx = CreateContext()) { @@ -35,10 +49,22 @@ public void Index_with_constant() } [Fact] - public void Index_with_non_constant() + public void List_Index_with_constant() { using (var ctx = CreateContext()) { + var actual = ctx.SomeEntities.Where(e => e.SomeList[0] == 3).ToList(); + Assert.Equal(1, actual.Count); + AssertContainsInSql(@"WHERE (e.""SomeList""[1]) = 3"); + } + } + + [Fact] + public void Array_Index_with_non_constant() + { + using (var ctx = 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); @@ -47,7 +73,20 @@ public void Index_with_non_constant() } [Fact] - public void Index_bytea_with_constant() + public void List_Index_with_non_constant() + { + using (var ctx = CreateContext()) + { + // ReSharper disable once ConvertToConstant.Local + var x = 0; + var actual = ctx.SomeEntities.Where(e => e.SomeList[x] == 3).ToList(); + Assert.Equal(1, actual.Count); + AssertContainsInSql(@"WHERE (e.""SomeList""[@__x_0 + 1]) = 3"); + } + } + + [Fact] + public void Array_Index_bytea_with_constant() { using (var ctx = CreateContext()) { @@ -58,30 +97,64 @@ public void Index_bytea_with_constant() } [Fact] - public void Index_multidimensional() + public void Array_Index_multidimensional() + { + using (var ctx = CreateContext()) + { + // Operations on multidimensional arrays aren't mapped to SQL yet + var actual = ctx.SomeEntities.Where(e => e.SomeMatrix[0, 0] == 5).ToList(); + Assert.Equal(1, actual.Count); + } + } + + [Fact(Skip = "Not yet supported")] + public void Array_Index_jagged() + { + using (var ctx = CreateContext()) + { + // Operations on multidimensional arrays aren't mapped to SQL yet + var actual = ctx.SomeEntities.Where(e => e.SomeArrayOfArrays[0][0] == 5).ToList(); + Assert.Equal(1, actual.Count); + } + } + + [Fact(Skip = "Not yet supported")] + public void List_Index_jagged() { using (var ctx = CreateContext()) { // Operations on multidimensional arrays aren't mapped to SQL yet - var actual = ctx.SomeEntities.Where(e => e.SomeMatrix[0,0] == 5).ToList(); + var actual = ctx.SomeEntities.Where(e => e.SomeListOfLists[0][0] == 5).ToList(); Assert.Equal(1, actual.Count); } } [Fact] - public void SequenceEqual_with_parameter() + public void Array_SequenceEqual_with_parameter() { using (var ctx = 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 x = ctx.SomeEntities.Single(e => e.SomeArray.SequenceEqual(array)); + Assert.Equal(array, x.SomeArray); + AssertContainsInSql(@"WHERE e.""SomeArray"" = @__array_0"); } } [Fact] - public void SequenceEqual_with_array_literal() + public void List_SequenceEqual_with_parameter() + { + using (var ctx = CreateContext()) + { + var list = new List { 3, 4 }; + var x = ctx.SomeEntities.Single(e => e.SomeList.SequenceEqual(list)); + Assert.Equal(list, x.SomeList); + AssertContainsInSql(@"WHERE e.""SomeList"" = @__list_0"); + } + } + + [Fact] + public void Array_SequenceEqual_with_literal() { using (var ctx = CreateContext()) { @@ -92,7 +165,18 @@ public void SequenceEqual_with_array_literal() } [Fact] - public void Contains_with_literal() + public void List_SequenceEqual_with_literal() + { + using (var ctx = CreateContext()) + { + var x = ctx.SomeEntities.Single(e => e.SomeList.SequenceEqual(new List { 3, 4 })); + Assert.Equal(new List { 3, 4 }, x.SomeList); + AssertContainsInSql(@"WHERE e.""SomeList"" = ARRAY[3,4]"); + } + } + + [Fact] + public void Array_Contains_with_literal() { using (var ctx = CreateContext()) { @@ -103,10 +187,22 @@ public void Contains_with_literal() } [Fact] - public void Contains_with_parameter() + public void List_Contains_with_literal() { using (var ctx = CreateContext()) { + var x = ctx.SomeEntities.Single(e => e.SomeList.Contains(3)); + Assert.Equal(new[] { 3, 4 }, x.SomeList); + AssertContainsInSql(@"WHERE 3 = ANY (e.""SomeList"")"); + } + } + + [Fact] + public void Array_Contains_with_parameter() + { + using (var ctx = 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); @@ -115,7 +211,20 @@ public void Contains_with_parameter() } [Fact] - public void Contains_with_column() + public void List_Contains_with_parameter() + { + using (var ctx = CreateContext()) + { + // ReSharper disable once ConvertToConstant.Local + var p = 3; + var x = ctx.SomeEntities.Single(e => e.SomeList.Contains(p)); + Assert.Equal(new[] { 3, 4 }, x.SomeList); + AssertContainsInSql(@"WHERE @__p_0 = ANY (e.""SomeList"")"); + } + } + + [Fact] + public void Array_Contains_with_column() { using (var ctx = CreateContext()) { @@ -126,7 +235,18 @@ public void Contains_with_column() } [Fact] - public void Length() + public void List_Contains_with_column() + { + using (var ctx = CreateContext()) + { + var x = ctx.SomeEntities.Single(e => e.SomeList.Contains(e.Id + 2)); + Assert.Equal(new List { 3, 4 }, x.SomeList); + AssertContainsInSql(@"WHERE e.""Id"" + 2 = ANY (e.""SomeList"")"); + } + } + + [Fact] + public void Array_Length() { using (var ctx = CreateContext()) { @@ -136,8 +256,18 @@ public void Length() } } - [Fact(Skip="https://github.com/aspnet/EntityFramework/issues/9242")] - public void Length_on_EF_Property() + [Fact] + public void List_Length() + { + using (var ctx = CreateContext()) + { + var _ = ctx.SomeEntities.Where(e => e.SomeList.Count > 0).ToArray(); + AssertContainsInSql(@"WHERE array_length(e.""SomeList"", 1) > 0"); + } + } + + [Fact(Skip = "https://github.com/aspnet/EntityFramework/issues/9242")] + public void Array_Length_on_EF_Property() { using (var ctx = CreateContext()) { @@ -148,99 +278,166 @@ public void Length_on_EF_Property() } } + [Fact(Skip = "https://github.com/aspnet/EntityFramework/issues/9242")] + public void List_Length_on_EF_Property() + { + using (var ctx = CreateContext()) + { + // TODO: This fails + var x = ctx.SomeEntities.Single(e => EF.Property>(e, nameof(SomeArrayEntity.SomeList)).Count == 2); + Assert.Equal(new List { 3, 4 }, x.SomeList); + AssertContainsInSql(@"WHERE array_length(e.""SomeList"", 1) = 2"); + } + } + [Fact] - public void Length_on_literal_not_translated() + public void Array_Length_on_literal_not_translated() { using (var ctx = CreateContext()) { - var x = ctx.SomeEntities.Where(e => new[] { 1, 2, 3 }.Length == e.Id).ToList(); + var _ = ctx.SomeEntities.Where(e => new[] { 1, 2, 3 }.Length == e.Id).ToList(); + AssertContainsInSql(@"WHERE 3 = e.""Id"""); AssertDoesNotContainInSql("array_length"); } } + [Fact] + public void List_Length_on_literal_not_translated() + { + using (var ctx = CreateContext()) + { + var _ = ctx.SomeEntities.Where(e => new List { 1, 2, 3 }.Count == e.Id).ToList(); + AssertContainsInSql(@"WHERE @__Count_0 = e.""Id"""); + AssertDoesNotContainInSql("array_length"); + } + } + + #endregion + #region Support + /// + /// Provides resources for unit tests. + /// ArrayFixture Fixture { get; } + /// + /// Initializes resources for unit tests. + /// + /// The fixture of resources for testing. public ArrayQueryTest(ArrayFixture fixture) { Fixture = fixture; Fixture.TestSqlLoggerFactory.Clear(); } + /// + /// Creates a new . + /// + /// + /// An for testing. + /// ArrayContext CreateContext() => Fixture.CreateContext(); + /// + /// Asserts that the SQL fragment appears in the logs. + /// + /// The SQL statement or fragment to search for in the logs. void AssertContainsInSql(string expected) => Assert.Contains(expected, Fixture.TestSqlLoggerFactory.Sql); + /// + /// Asserts that the SQL fragment does not appear in the logs. + /// + /// The SQL statement or fragment to search for in the logs. void AssertDoesNotContainInSql(string expected) => Assert.DoesNotContain(expected, Fixture.TestSqlLoggerFactory.Sql); #endregion Support - } - public class ArrayContext : DbContext - { - public DbSet SomeEntities { get; set; } - public ArrayContext(DbContextOptions options) : base(options) {} - protected override void OnModelCreating(ModelBuilder builder) - { + #region Fixtures + /// + /// Represents a database suitable for testing operations with PostgreSQL arrays. + /// + public class ArrayContext : DbContext + { + public DbSet SomeEntities { get; set; } + public ArrayContext(DbContextOptions options) : base(options) {} + protected override void OnModelCreating(ModelBuilder builder) {} } - } - public class SomeArrayEntity - { - public int Id { get; set; } - public int[] SomeArray { get; set; } - public int[,] SomeMatrix { get; set; } - public List SomeList { get; set; } - public byte[] SomeBytea { get; set; } - public string SomeText { get; set; } - } + /// + /// Represents an entity suitable for testing operations with PostgreSQL arrays. + /// + public class SomeArrayEntity + { + public int Id { get; set; } + public int[] SomeArray { get; set; } + public List SomeList { get; set; } + public int[,] SomeMatrix { get; set; } - public class ArrayFixture : IDisposable - { - readonly DbContextOptions _options; - public TestSqlLoggerFactory TestSqlLoggerFactory { get; } = new TestSqlLoggerFactory(); + [NotMapped] + public int[][] SomeArrayOfArrays { get; set; } - public ArrayFixture() + [NotMapped] + public List> SomeListOfLists { get; set; } + + public byte[] SomeBytea { get; set; } + } + + /// + /// Represents a fixture suitable for testing operations with PostgreSQL arrays. + /// + public class ArrayFixture : IDisposable { - _testStore = NpgsqlTestStore.CreateScratch(); - _options = new DbContextOptionsBuilder() - .UseNpgsql(_testStore.ConnectionString, b => b.ApplyConfiguration()) - .UseInternalServiceProvider( - new ServiceCollection() - .AddEntityFrameworkNpgsql() - .AddSingleton(TestSqlLoggerFactory) - .BuildServiceProvider()) - .Options; + readonly DbContextOptions _options; + public TestSqlLoggerFactory TestSqlLoggerFactory { get; } = new TestSqlLoggerFactory(); - using (var ctx = CreateContext()) + public ArrayFixture() { - ctx.Database.EnsureCreated(); - ctx.SomeEntities.Add(new SomeArrayEntity - { - Id=1, - SomeArray = new[] { 3, 4 }, - SomeBytea = new byte[] { 3, 4 }, - SomeMatrix = new[,] { { 5, 6 }, { 7, 8 } }, - SomeList = new List { 3, 4 } - }); - ctx.SomeEntities.Add(new SomeArrayEntity + _testStore = NpgsqlTestStore.CreateScratch(); + _options = new DbContextOptionsBuilder() + .UseNpgsql(_testStore.ConnectionString, b => b.ApplyConfiguration()) + .UseInternalServiceProvider( + new ServiceCollection() + .AddEntityFrameworkNpgsql() + .AddSingleton(TestSqlLoggerFactory) + .BuildServiceProvider()) + .Options; + + using (var ctx = CreateContext()) { - Id=2, - SomeArray = new[] { 5, 6, 7 }, - SomeBytea = new byte[] { 5, 6, 7 }, - SomeMatrix = new[,] { { 10, 11 }, { 12, 13 } }, - SomeList = new List { 3, 4 } - }); - ctx.SaveChanges(); + ctx.Database.EnsureCreated(); + ctx.SomeEntities.Add(new SomeArrayEntity + { + Id = 1, + SomeArray = new[] { 3, 4 }, + SomeBytea = new byte[] { 3, 4 }, + SomeList = new List { 3, 4 }, + SomeMatrix = new[,] { { 5, 6 }, { 7, 8 } }, + SomeArrayOfArrays = new[] { new[] { 5, 6 }, new[] { 7, 8 } }, + SomeListOfLists = new List> { new List { 5, 6 }, new List { 7, 8 } }, + }); + ctx.SomeEntities.Add(new SomeArrayEntity + { + Id = 2, + SomeArray = new[] { 5, 6, 7 }, + SomeBytea = new byte[] { 5, 6, 7 }, + SomeList = new List { 5, 6, 7 }, + SomeMatrix = new[,] { { 10, 11 }, { 12, 13 } }, + SomeArrayOfArrays = new[] { new[] { 10, 11 }, new[] { 12, 13 } }, + SomeListOfLists = new List> { new List { 10, 11 }, new List { 12, 13 } } + }); + ctx.SaveChanges(); + } } + + readonly NpgsqlTestStore _testStore; + public ArrayContext CreateContext() => new ArrayContext(_options); + public void Dispose() => _testStore.Dispose(); } - readonly NpgsqlTestStore _testStore; - public ArrayContext CreateContext() => new ArrayContext(_options); - public void Dispose() => _testStore.Dispose(); + #endregion } } From 508e72a092d9bbacb7a4f2fca9aeb6b8396d6129 Mon Sep 17 00:00:00 2001 From: Austin Drenski Date: Mon, 28 May 2018 21:52:00 -0400 Subject: [PATCH 02/16] Maps additional array (`IList`) operators | C# expression | SQL generated by Npgsql | |----------------------------------|-------------------------| | `Array.Length` | `array_length(1, 1)` | `IList.Count` | `array_length(a, 1)` | `Enumerable.Count(a)` | `array_length(a, 1)` | `Array[0]` | `a[1]` | `IList[0]` | `a[1]` | `Enumerable.ElementAt(a, 0)` | `a[1]` | `Enumerable.Append(a, b)` | `a || b` | `Enumerable.Concat(a, b)` | `a || b` | `Enumerable.Prepend(a, b)` | `b || a` | `Object.Equals(a, b)` | `a = b` | `Enumerable.SequenceEqual(a, b)` | `a = b` | `a.ToString()` | `array_to_string(a, ',')` | `Enumerable.IndexOf(a, 0)` | `COALESCE(array_position(a, 1), -1)` | `Enumerable.Contains(a, b)` | `b = ANY (a)` | C# expression | SQL generated by Npgsql | |----------------------------|-------------------------| | `a.All(x => b.Contains(x)` | `b <@ a` | `a.Any(x => b.Contains(x)` | `a && b` - Relational operators could also be supported via extensions on `EF.Functions`. - https://www.postgresql.org/docs/current/static/functions-array.html#ARRAY-OPERATORS-TABLE --- .../NpgsqlArraySequenceEqualTranslator.cs | 67 ------ .../NpgsqlCompositeMethodCallTranslator.cs | 1 - .../Internal/NpgsqlListTranslator.cs | 79 +++--- .../NpgsqlSqlTranslatingExpressionVisitor.cs | 5 + .../Query/ArrayQueryTest.cs | 224 +++++++++++++++++- 5 files changed, 272 insertions(+), 104 deletions(-) delete mode 100644 src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArraySequenceEqualTranslator.cs 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 863ac10d1..000000000 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArraySequenceEqualTranslator.cs +++ /dev/null @@ -1,67 +0,0 @@ -#region License - -// The PostgreSQL License -// -// Copyright (C) 2016 The Npgsql Development Team -// -// Permission to use, copy, modify, and distribute this software and its -// documentation for any purpose, without fee, and without a written -// agreement is hereby granted, provided that the above copyright notice -// and this paragraph and the following two paragraphs appear in all copies. -// -// IN NO EVENT SHALL THE NPGSQL DEVELOPMENT TEAM BE LIABLE TO ANY PARTY -// FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, -// INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS -// DOCUMENTATION, EVEN IF THE NPGSQL DEVELOPMENT TEAM HAS BEEN ADVISED OF -// THE POSSIBILITY OF SUCH DAMAGE. -// -// THE NPGSQL DEVELOPMENT TEAM SPECIFICALLY DISCLAIMS ANY WARRANTIES, -// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY -// AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS -// ON AN "AS IS" BASIS, AND THE NPGSQL DEVELOPMENT TEAM HAS NO OBLIGATIONS -// TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. - -#endregion - -using System.Collections; -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 || typeof(IList).IsAssignableFrom(a.Type))) - return - Expression.MakeBinary( - ExpressionType.Equal, - methodCallExpression.Arguments[0], - methodCallExpression.Arguments[1]); - - return null; - } - } -} diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlCompositeMethodCallTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlCompositeMethodCallTranslator.cs index 03d0e6e74..cc41023dd 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlCompositeMethodCallTranslator.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlCompositeMethodCallTranslator.cs @@ -40,7 +40,6 @@ public class NpgsqlCompositeMethodCallTranslator : RelationalCompositeMethodCall /// [NotNull] [ItemNotNull] static readonly IMethodCallTranslator[] MethodCallTranslators = { - new NpgsqlArraySequenceEqualTranslator(), new NpgsqlConvertTranslator(), new NpgsqlStringSubstringTranslator(), new NpgsqlLikeTranslator(), diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlListTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlListTranslator.cs index d0797311e..953241d9e 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlListTranslator.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlListTranslator.cs @@ -25,10 +25,13 @@ using System.Collections; using System.Collections.Generic; +using System.Linq; using System.Linq.Expressions; using System.Reflection; using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Query.Expressions; using Microsoft.EntityFrameworkCore.Query.ExpressionTranslators; +using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal; namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Internal { @@ -44,45 +47,55 @@ public class NpgsqlListTranslator : IMethodCallTranslator [CanBeNull] public Expression Translate(MethodCallExpression expression) { - if (!typeof(IList).IsAssignableFrom(expression.Method.DeclaringType)) + if (expression.Object != null && !typeof(IList).IsAssignableFrom(expression.Object.Type)) return null; + if (expression.Object == null && expression.Arguments.Count > 0 && !typeof(IList).IsAssignableFrom(expression.Arguments[0].Type)) + return null; + + // TODO: use #430 to map @> to source.All(x => other.Contains(x)); + // TODO: use #430 to map && to soucre.Any(x => other.Contains(x)); switch (expression.Method.Name) { + // TODO: Currently handled in NpgsqlSqlTranslatingExpressionVisitor. + case nameof(Enumerable.Append): + case nameof(Enumerable.Concat): + return new CustomBinaryExpression(expression.Arguments[0], expression.Arguments[1], "||", expression.Arguments[0].Type); + + // TODO: Currently handled in NpgsqlSqlTranslatingExpressionVisitor. + case nameof(Enumerable.Count): + return new SqlFunctionExpression("array_length", typeof(int), new[] { expression.Arguments[0], Expression.Constant(1) }); + case "get_Item" when expression.Object is Expression instance: return Expression.MakeIndex(instance, instance.Type.GetRuntimeProperty("Item"), expression.Arguments); -// case nameof(NpgsqlListExtensions.Contains): -// return new CustomBinaryExpression(expression.Arguments[0], expression.Arguments[1], "@>", typeof(bool)); -// -// case nameof(NpgsqlListExtensions.ContainedBy): -// return new CustomBinaryExpression(expression.Arguments[0], expression.Arguments[1], "<@", typeof(bool)); -// -// case nameof(NpgsqlListExtensions.Overlaps): -// return new CustomBinaryExpression(expression.Arguments[0], expression.Arguments[1], "&&", typeof(bool)); -// -// case nameof(NpgsqlListExtensions.IsStrictlyLeftOf): -// return new CustomBinaryExpression(expression.Arguments[0], expression.Arguments[1], "<<", typeof(bool)); -// -// case nameof(NpgsqlListExtensions.IsStrictlyRightOf): -// return new CustomBinaryExpression(expression.Arguments[0], expression.Arguments[1], ">>", typeof(bool)); -// -// case nameof(NpgsqlListExtensions.DoesNotExtendRightOf): -// return new CustomBinaryExpression(expression.Arguments[0], expression.Arguments[1], "&<", typeof(bool)); -// -// case nameof(NpgsqlListExtensions.DoesNotExtendLeftOf): -// return new CustomBinaryExpression(expression.Arguments[0], expression.Arguments[1], "&>", typeof(bool)); -// -// case nameof(NpgsqlListExtensions.IsAdjacentTo): -// return new CustomBinaryExpression(expression.Arguments[0], expression.Arguments[1], "-|-", typeof(bool)); -// -// case nameof(NpgsqlListExtensions.Union): -// return new CustomBinaryExpression(expression.Arguments[0], expression.Arguments[1], "+", expression.Arguments[0].Type); -// -// case nameof(NpgsqlListExtensions.Intersect): -// return new CustomBinaryExpression(expression.Arguments[0], expression.Arguments[1], "*", expression.Arguments[0].Type); -// -// case nameof(NpgsqlListExtensions.Except): -// return new CustomBinaryExpression(expression.Arguments[0], expression.Arguments[1], "-", expression.Arguments[0].Type); + + case nameof(Enumerable.ElementAt): + return Expression.MakeIndex(expression.Arguments[0], expression.Arguments[0].Type.GetRuntimeProperty("Item"), new[] { expression.Arguments[1] }); + + 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]); + + case nameof(ToString): + return new SqlFunctionExpression("array_to_string", typeof(string), new[] { expression.Object, Expression.Constant(",") }); + + case nameof(IList.IndexOf): + return + new SqlFunctionExpression( + "COALESCE", + typeof(int), + new Expression[] + { + new SqlFunctionExpression( + "array_position", + typeof(int), + expression.Object is null + ? (IEnumerable)expression.Arguments + : new[] { expression.Object, expression.Arguments[0] }), + Expression.Constant(-1) + }); default: return null; diff --git a/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlSqlTranslatingExpressionVisitor.cs b/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlSqlTranslatingExpressionVisitor.cs index 71003b5e9..e46433c66 100644 --- a/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlSqlTranslatingExpressionVisitor.cs @@ -140,6 +140,11 @@ protected virtual Expression VisitEqualsAny([NotNull] SubQueryExpression express var lastPropertyType = properties[properties.Count - 1].ClrType; if (typeof(IList).IsAssignableFrom(lastPropertyType) && subQueryModel.ResultOperators.Count > 0) { + if (subQueryModel.ResultOperators.First() is ConcatResultOperator concatResultOperator && + Visit(fromExpression) is Expression first && + Visit(concatResultOperator.Source2) is Expression second) + return new CustomBinaryExpression(first, second, "||", first.Type); + // Translate someArray.Length if (subQueryModel.ResultOperators.First() is CountResultOperator) return new SqlFunctionExpression("array_length", typeof(int), new[] { Visit(fromExpression), Expression.Constant(1) }); diff --git a/test/EFCore.PG.FunctionalTests/Query/ArrayQueryTest.cs b/test/EFCore.PG.FunctionalTests/Query/ArrayQueryTest.cs index ce5aecd79..03a26fbe4 100644 --- a/test/EFCore.PG.FunctionalTests/Query/ArrayQueryTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/ArrayQueryTest.cs @@ -3,6 +3,7 @@ using System.ComponentModel.DataAnnotations.Schema; using System.Linq; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Internal; using Microsoft.EntityFrameworkCore.TestUtilities; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -59,6 +60,28 @@ public void List_Index_with_constant() } } + [Fact] + public void Array_ElementAt_with_constant() + { + using (var ctx = CreateContext()) + { + var actual = ctx.SomeEntities.Where(e => e.SomeArray.ElementAt(0) == 3).ToList(); + Assert.Equal(1, actual.Count); + AssertContainsInSql(@"WHERE (e.""SomeArray""[1]) = 3"); + } + } + + [Fact] + public void List_ElementAt_with_constant() + { + using (var ctx = CreateContext()) + { + var actual = ctx.SomeEntities.Where(e => e.SomeList.ElementAt(0) == 3).ToList(); + Assert.Equal(1, actual.Count); + AssertContainsInSql(@"WHERE (e.""SomeList""[1]) = 3"); + } + } + [Fact] public void Array_Index_with_non_constant() { @@ -85,6 +108,32 @@ public void List_Index_with_non_constant() } } + [Fact] + public void Array_ElementAt_with_non_constant() + { + using (var ctx = CreateContext()) + { + // ReSharper disable once ConvertToConstant.Local + var x = 0; + var actual = ctx.SomeEntities.Where(e => e.SomeArray.ElementAt(x) == 3).ToList(); + Assert.Equal(1, actual.Count); + AssertContainsInSql(@"WHERE (e.""SomeArray""[@__x_0 + 1]) = 3"); + } + } + + [Fact] + public void List_IndexElementAt_with_non_constant() + { + using (var ctx = CreateContext()) + { + // ReSharper disable once ConvertToConstant.Local + var x = 0; + var actual = ctx.SomeEntities.Where(e => e.SomeList.ElementAt(x) == 3).ToList(); + Assert.Equal(1, actual.Count); + AssertContainsInSql(@"WHERE (e.""SomeList""[@__x_0 + 1]) = 3"); + } + } + [Fact] public void Array_Index_bytea_with_constant() { @@ -129,13 +178,37 @@ public void List_Index_jagged() } } + [Fact] + public void Array_Equal_with_parameter() + { + using (var ctx = CreateContext()) + { + var array = new[] { 3, 4 }; + var x = ctx.SomeEntities.Single(e => e.SomeArray.Equals(array)); + Assert.Equal(array, x.SomeArray); + AssertContainsInSql(@"WHERE e.""SomeArray"" = @__array_0"); + } + } + + [Fact] + public void List_Equal_with_parameter() + { + using (var ctx = CreateContext()) + { + var list = new List { 3, 4 }; + var x = ctx.SomeEntities.Single(e => e.SomeList.Equals(list)); + Assert.Equal(list, x.SomeList); + AssertContainsInSql(@"WHERE e.""SomeList"" = @__list_0"); + } + } + [Fact] public void Array_SequenceEqual_with_parameter() { using (var ctx = CreateContext()) { var array = new[] { 3, 4 }; - var x = ctx.SomeEntities.Single(e => e.SomeArray.SequenceEqual(array)); + var x = ctx.SomeEntities.Single(e => e.SomeArray.Equals(array)); Assert.Equal(array, x.SomeArray); AssertContainsInSql(@"WHERE e.""SomeArray"" = @__array_0"); } @@ -261,8 +334,30 @@ public void List_Length() { using (var ctx = CreateContext()) { - var _ = ctx.SomeEntities.Where(e => e.SomeList.Count > 0).ToArray(); - AssertContainsInSql(@"WHERE array_length(e.""SomeList"", 1) > 0"); + var _ = ctx.SomeEntities.Where(e => e.SomeList.Count == 0).ToArray(); + AssertContainsInSql(@"WHERE array_length(e.""SomeList"", 1) = 0"); + } + } + + [Fact] + public void Array_Count() + { + using (var ctx = CreateContext()) + { + // ReSharper disable once UseCollectionCountProperty + var _ = ctx.SomeEntities.Where(e => e.SomeArray.Count() == 1).ToArray(); + AssertContainsInSql(@"WHERE array_length(e.""SomeArray"", 1) = 1"); + } + } + + [Fact] + public void List_Count() + { + using (var ctx = CreateContext()) + { + // ReSharper disable once UseCollectionCountProperty + var _ = ctx.SomeEntities.Where(e => e.SomeList.Count() == 1).ToArray(); + AssertContainsInSql(@"WHERE array_length(e.""SomeList"", 1) = 1"); } } @@ -312,6 +407,129 @@ public void List_Length_on_literal_not_translated() } } + [Fact] + public void Array_Concat_with_array_column() + { + using (var ctx = CreateContext()) + { + var _ = ctx.SomeEntities.Select(e => e.SomeArray.Concat(e.SomeArray)).ToList(); + AssertContainsInSql(@"SELECT (e.""SomeArray"" || e.""SomeArray"")"); + } + } + + [Fact] + public void List_Concat_with_list_column() + { + using (var ctx = CreateContext()) + { + var _ = ctx.SomeEntities.Select(e => e.SomeList.Concat(e.SomeList)).ToList(); + AssertContainsInSql(@"SELECT (e.""SomeList"" || e.""SomeList"")"); + } + } + + [Fact] + public void Array_Concat_with_list_column() + { + using (var ctx = CreateContext()) + { + var _ = ctx.SomeEntities.Select(e => e.SomeArray.Concat(e.SomeList)).ToList(); + AssertContainsInSql(@"SELECT (e.""SomeArray"" || e.""SomeList"")"); + } + } + + [Fact] + public void List_Concat_with_array_column() + { + using (var ctx = CreateContext()) + { + var _ = ctx.SomeEntities.Select(e => e.SomeList.Concat(e.SomeArray)).ToList(); + AssertContainsInSql(@"SELECT (e.""SomeList"" || e.""SomeArray"")"); + } + } + + [Fact] + public void Array_IndexOf_constant() + { + using (var ctx = CreateContext()) + { + var _ = ctx.SomeEntities.Select(e => e.SomeArray.IndexOf(0)).ToList(); + AssertContainsInSql(@"SELECT COALESCE(array_position(e.""SomeArray"", 0), -1)"); + } + } + + [Fact] + public void List_IndexOf_constant() + { + using (var ctx = CreateContext()) + { + var _ = ctx.SomeEntities.Select(e => e.SomeList.IndexOf(0)).ToList(); + AssertContainsInSql(@"SELECT COALESCE(array_position(e.""SomeList"", 0), -1)"); + } + } + + [Fact] + public void Array_ToString() + { + using (var ctx = CreateContext()) + { + var _ = ctx.SomeEntities.Select(e => e.SomeArray.ToString()).ToList(); + AssertContainsInSql(@"SELECT array_to_string(e.""SomeArray"", ',')"); + } + } + + [Fact] + public void List_ToString() + { + using (var ctx = CreateContext()) + { + var _ = ctx.SomeEntities.Select(e => e.SomeList.ToString()).ToList(); + AssertContainsInSql(@"SELECT array_to_string(e.""SomeList"", ',')"); + } + } + +#if NETCOREAPP2_1 + [Fact] + public void Array_Append_constant() + { + using (var ctx = CreateContext()) + { + var _ = ctx.SomeEntities.Select(e => e.SomeArray.Append(0)).ToList(); + AssertContainsInSql(@"SELECT (e.""SomeArray"" || 0)"); + } + } + + [Fact] + public void List_Append_constant() + { + using (var ctx = CreateContext()) + { + var _ = ctx.SomeEntities.Select(e => e.SomeList.Append(0)).ToList(); + AssertContainsInSql(@"SELECT (e.""SomeList"" || 0)"); + } + } + + [Fact] + public void Array_Prepend_constant() + { + using (var ctx = CreateContext()) + { + var _ = ctx.SomeEntities.Select(e => e.SomeArray.Prepend(0)).ToList(); + AssertContainsInSql(@"SELECT (0 || e.""SomeArray"")"); + } + } + + [Fact] + public void List_Prepend_constant() + { + using (var ctx = CreateContext()) + { + var _ = ctx.SomeEntities.Select(e => e.SomeList.Prepend(0)).ToList(); + AssertContainsInSql(@"SELECT (0 || e.""SomeList"")"); + } + } + +#endif + #endregion #region Support From 1f40ca9dbaef70707601256c71e6590248e53701 Mon Sep 17 00:00:00 2001 From: Austin Drenski Date: Tue, 29 May 2018 12:47:03 -0400 Subject: [PATCH 03/16] Refactors method identification for translation - Improves how we verify that: - an array or list is supported - the function for an array or list is supported. - Renames `NpgsqlListTranslator` to `NpgsqlArrayTranslator` - Adds `NpgsqlArrayExtensions` --- .../Extensions/NpgsqlArrayExtensions.cs | 92 +++++++++ .../Internal/NpgsqlArrayTranslator.cs | 191 ++++++++++++++++++ .../NpgsqlCompositeMethodCallTranslator.cs | 2 +- .../Internal/NpgsqlListTranslator.cs | 105 ---------- .../NpgsqlSqlTranslatingExpressionVisitor.cs | 22 +- .../Sql/Internal/NpgsqlQuerySqlGenerator.cs | 2 - .../Query/ArrayQueryTest.cs | 31 ++- 7 files changed, 329 insertions(+), 116 deletions(-) create mode 100644 src/EFCore.PG/Extensions/NpgsqlArrayExtensions.cs create mode 100644 src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayTranslator.cs delete mode 100644 src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlListTranslator.cs diff --git a/src/EFCore.PG/Extensions/NpgsqlArrayExtensions.cs b/src/EFCore.PG/Extensions/NpgsqlArrayExtensions.cs new file mode 100644 index 000000000..405431cec --- /dev/null +++ b/src/EFCore.PG/Extensions/NpgsqlArrayExtensions.cs @@ -0,0 +1,92 @@ +#region License + +// The PostgreSQL License +// +// Copyright (C) 2016 The Npgsql Development Team +// +// Permission to use, copy, modify, and distribute this software and its +// documentation for any purpose, without fee, and without a written +// agreement is hereby granted, provided that the above copyright notice +// and this paragraph and the following two paragraphs appear in all copies. +// +// IN NO EVENT SHALL THE NPGSQL DEVELOPMENT TEAM BE LIABLE TO ANY PARTY +// FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, +// INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS +// DOCUMENTATION, EVEN IF THE NPGSQL DEVELOPMENT TEAM HAS BEEN ADVISED OF +// THE POSSIBILITY OF SUCH DAMAGE. +// +// THE NPGSQL DEVELOPMENT TEAM SPECIFICALLY DISCLAIMS ANY WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS +// ON AN "AS IS" BASIS, AND THE NPGSQL DEVELOPMENT TEAM HAS NO OBLIGATIONS +// TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + +#endregion + +using System; +using System.Collections.Generic; +using JetBrains.Annotations; + +// ReSharper disable once CheckNamespace +namespace Microsoft.EntityFrameworkCore +{ + /// + /// Provides extension methods for supporting PostgreSQL translation. + /// + public static class NpgsqlArrayExtensions + { + /// + /// Determines whether a range contains a specified value. + /// + /// The DbFunctions instance. + /// The list to conver to a string in which to locate the value. + /// The value used to delimit the elements. + /// The type of the elements of . + /// + /// true if the range contains the specified value; otherwise, false. + /// + public static string ArrayToString([CanBeNull] this DbFunctions _, [NotNull] T[] array, [CanBeNull] string delimiter) + => throw new NotSupportedException(); + + /// + /// Determines whether a range contains a specified value. + /// + /// The DbFunctions instance. + /// The list to conver 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 . + /// + /// true if the range contains the specified value; otherwise, false. + /// + public static string ArrayToString([CanBeNull] this DbFunctions _, [NotNull] T[] array, [CanBeNull] string delimiter, [CanBeNull] string nullString) + => throw new NotSupportedException(); + + /// + /// Determines whether a range contains a specified value. + /// + /// The DbFunctions instance. + /// The list to conver to a string in which to locate the value. + /// The value used to delimit the elements. + /// The type of the elements of . + /// + /// true if the range contains the specified value; otherwise, false. + /// + public static string ArrayToString([CanBeNull] this DbFunctions _, [NotNull] List list, [CanBeNull] string delimiter) + => throw new NotSupportedException(); + + /// + /// Determines whether a range contains a specified value. + /// + /// The DbFunctions instance. + /// The list to conver 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 . + /// + /// true if the range contains the specified value; otherwise, false. + /// + public static string ArrayToString([CanBeNull] this DbFunctions _, [NotNull] List list, [CanBeNull] string delimiter, [CanBeNull] string nullString) + => throw new NotSupportedException(); + } +} diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayTranslator.cs new file mode 100644 index 000000000..4656523e3 --- /dev/null +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayTranslator.cs @@ -0,0 +1,191 @@ +#region License + +// The PostgreSQL License +// +// Copyright (C) 2016 The Npgsql Development Team +// +// Permission to use, copy, modify, and distribute this software and its +// documentation for any purpose, without fee, and without a written +// agreement is hereby granted, provided that the above copyright notice +// and this paragraph and the following two paragraphs appear in all copies. +// +// IN NO EVENT SHALL THE NPGSQL DEVELOPMENT TEAM BE LIABLE TO ANY PARTY +// FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, +// INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS +// DOCUMENTATION, EVEN IF THE NPGSQL DEVELOPMENT TEAM HAS BEEN ADVISED OF +// THE POSSIBILITY OF SUCH DAMAGE. +// +// THE NPGSQL DEVELOPMENT TEAM SPECIFICALLY DISCLAIMS ANY WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS +// ON AN "AS IS" BASIS, AND THE NPGSQL DEVELOPMENT TEAM HAS NO OBLIGATIONS +// TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + +#endregion + +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 NpgsqlArrayTranslator : IMethodCallTranslator + { + /// + [CanBeNull] + public Expression Translate(MethodCallExpression expression) + { + if (!IsTypeSupported(expression)) + return null; + + if (!IsMethodSupported(expression.Method)) + return null; + + // TODO: use #430 to map @> to source.All(x => other.Contains(x)); + // TODO: use #430 to map && to soucre.Any(x => other.Contains(x)); + + switch (expression.Method.Name) + { + #region Instance + + case "get_Item" when expression.Object is Expression instance: + return Expression.MakeIndex(instance, instance.Type.GetRuntimeProperty("Item"), expression.Arguments); + + #endregion + + #region Enumerable + + case nameof(Enumerable.ElementAt): + return Expression.MakeIndex(expression.Arguments[0], expression.Arguments[0].Type.GetRuntimeProperty("Item"), new[] { expression.Arguments[1] }); + + // TODO: Currently handled in NpgsqlSqlTranslatingExpressionVisitor. + case nameof(Enumerable.Append): + case nameof(Enumerable.Concat) when IsArrayOrList(expression.Arguments[1].Type): + return new CustomBinaryExpression(expression.Arguments[0], expression.Arguments[1], "||", expression.Arguments[0].Type); + + case nameof(Enumerable.Count): + return new SqlFunctionExpression("array_length", typeof(int), new[] { expression.Arguments[0], Expression.Constant(1) }); + + 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]); + + #endregion + + #region NpgsqlArrayExtensions + + case nameof(NpgsqlArrayExtensions.ArrayToString): + return new SqlFunctionExpression("array_to_string", typeof(string), expression.Arguments.Skip(1)); + + #endregion + + #region ArrayStatic + + case nameof(Array.IndexOf) when expression.Method.DeclaringType == typeof(Array): + return + new SqlFunctionExpression( + "COALESCE", + typeof(int), + new Expression[] + { + new SqlFunctionExpression("array_position", typeof(int), expression.Arguments), + Expression.Constant(-1) + }); + + #endregion + + #region ListInstance + + case nameof(IList.IndexOf) when IsArrayOrList(expression.Method.DeclaringType): + return + new SqlFunctionExpression( + "COALESCE", + typeof(int), + new Expression[] + { + new SqlFunctionExpression("array_position", typeof(int), new[] { expression.Object, expression.Arguments[0] }), + Expression.Constant(-1) + }); + + #endregion + + default: + return null; + } + } + + #region Helpers + + /// + /// Tests if the instance or argument types are supported. + /// + /// + /// The to test. + /// + /// + /// True if the instance or argument types are supported; otherwise, false. + /// + static bool IsTypeSupported([NotNull] MethodCallExpression expression) + { + if (expression.Object is Expression instance) + return IsArrayOrList(instance.Type); + + if (expression.Object is null) + { + if (expression.Arguments.Count == 0) + return false; + + if (expression.Arguments.Count > 1 && + expression.Arguments[0].Type == typeof(DbFunctions)) + return IsArrayOrList(expression.Arguments[1].Type); + + return IsArrayOrList(expression.Arguments[0].Type); + } + + return false; + } + + /// + /// 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(Type type) + => type.IsArray || type.IsGenericType && typeof(List<>) == type.GetGenericTypeDefinition(); + + /// + /// Tests if the method is declared on an array, a , or . + /// + /// + /// The method to test. + /// + /// + /// True if is declared on an array, a , or ; otherwise, false. + /// + static bool IsMethodSupported([NotNull] MethodInfo method) + => method.DeclaringType is Type t && (IsArrayOrList(t) || t == typeof(Array) || t == typeof(Enumerable) || t == typeof(NpgsqlArrayExtensions)); + + #endregion + } +} diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlCompositeMethodCallTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlCompositeMethodCallTranslator.cs index cc41023dd..44d96dd9b 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlCompositeMethodCallTranslator.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlCompositeMethodCallTranslator.cs @@ -60,7 +60,7 @@ public class NpgsqlCompositeMethodCallTranslator : RelationalCompositeMethodCall new NpgsqlFullTextSearchMethodTranslator(), new NpgsqlRangeTranslator(), new NpgsqlNetworkTranslator(), - new NpgsqlListTranslator() + new NpgsqlArrayTranslator() }; /// diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlListTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlListTranslator.cs deleted file mode 100644 index 953241d9e..000000000 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlListTranslator.cs +++ /dev/null @@ -1,105 +0,0 @@ -#region License - -// The PostgreSQL License -// -// Copyright (C) 2016 The Npgsql Development Team -// -// Permission to use, copy, modify, and distribute this software and its -// documentation for any purpose, without fee, and without a written -// agreement is hereby granted, provided that the above copyright notice -// and this paragraph and the following two paragraphs appear in all copies. -// -// IN NO EVENT SHALL THE NPGSQL DEVELOPMENT TEAM BE LIABLE TO ANY PARTY -// FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, -// INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS -// DOCUMENTATION, EVEN IF THE NPGSQL DEVELOPMENT TEAM HAS BEEN ADVISED OF -// THE POSSIBILITY OF SUCH DAMAGE. -// -// THE NPGSQL DEVELOPMENT TEAM SPECIFICALLY DISCLAIMS ANY WARRANTIES, -// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY -// AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS -// ON AN "AS IS" BASIS, AND THE NPGSQL DEVELOPMENT TEAM HAS NO OBLIGATIONS -// TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. - -#endregion - -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Reflection; -using JetBrains.Annotations; -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 methods as PostgreSQL array operators. - /// - /// - /// See: https://www.postgresql.org/docs/current/static/functions-array.html - /// - public class NpgsqlListTranslator : IMethodCallTranslator - { - /// - [CanBeNull] - public Expression Translate(MethodCallExpression expression) - { - if (expression.Object != null && !typeof(IList).IsAssignableFrom(expression.Object.Type)) - return null; - if (expression.Object == null && expression.Arguments.Count > 0 && !typeof(IList).IsAssignableFrom(expression.Arguments[0].Type)) - return null; - - // TODO: use #430 to map @> to source.All(x => other.Contains(x)); - // TODO: use #430 to map && to soucre.Any(x => other.Contains(x)); - - switch (expression.Method.Name) - { - // TODO: Currently handled in NpgsqlSqlTranslatingExpressionVisitor. - case nameof(Enumerable.Append): - case nameof(Enumerable.Concat): - return new CustomBinaryExpression(expression.Arguments[0], expression.Arguments[1], "||", expression.Arguments[0].Type); - - // TODO: Currently handled in NpgsqlSqlTranslatingExpressionVisitor. - case nameof(Enumerable.Count): - return new SqlFunctionExpression("array_length", typeof(int), new[] { expression.Arguments[0], Expression.Constant(1) }); - - case "get_Item" when expression.Object is Expression instance: - return Expression.MakeIndex(instance, instance.Type.GetRuntimeProperty("Item"), expression.Arguments); - - case nameof(Enumerable.ElementAt): - return Expression.MakeIndex(expression.Arguments[0], expression.Arguments[0].Type.GetRuntimeProperty("Item"), new[] { expression.Arguments[1] }); - - 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]); - - case nameof(ToString): - return new SqlFunctionExpression("array_to_string", typeof(string), new[] { expression.Object, Expression.Constant(",") }); - - case nameof(IList.IndexOf): - return - new SqlFunctionExpression( - "COALESCE", - typeof(int), - new Expression[] - { - new SqlFunctionExpression( - "array_position", - typeof(int), - expression.Object is null - ? (IEnumerable)expression.Arguments - : new[] { expression.Object, expression.Arguments[0] }), - Expression.Constant(-1) - }); - - default: - return null; - } - } - } -} diff --git a/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlSqlTranslatingExpressionVisitor.cs b/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlSqlTranslatingExpressionVisitor.cs index e46433c66..9dc95ffc5 100644 --- a/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlSqlTranslatingExpressionVisitor.cs @@ -23,7 +23,8 @@ #endregion -using System.Collections; +using System; +using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Reflection; @@ -138,7 +139,7 @@ protected virtual Expression VisitEqualsAny([NotNull] SubQueryExpression express if (properties.Count == 0) return null; var lastPropertyType = properties[properties.Count - 1].ClrType; - if (typeof(IList).IsAssignableFrom(lastPropertyType) && subQueryModel.ResultOperators.Count > 0) + if (IsArrayOrList(lastPropertyType) && subQueryModel.ResultOperators.Count > 0) { if (subQueryModel.ResultOperators.First() is ConcatResultOperator concatResultOperator && Visit(fromExpression) is Expression first && @@ -147,7 +148,12 @@ protected virtual Expression VisitEqualsAny([NotNull] SubQueryExpression express // Translate someArray.Length if (subQueryModel.ResultOperators.First() is CountResultOperator) + { + if (lastPropertyType.IsArray) + return Expression.ArrayLength(Visit(fromExpression)); + return new SqlFunctionExpression("array_length", typeof(int), new[] { Visit(fromExpression), Expression.Constant(1) }); + } // Translate someArray.Contains(someValue) if (subQueryModel.ResultOperators.First() is ContainsResultOperator contains) @@ -222,5 +228,17 @@ whereClause.Predicate is MethodCallExpression methocCall return null; } } + + /// + /// 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 && typeof(List<>) == type.GetGenericTypeDefinition(); } } diff --git a/src/EFCore.PG/Query/Sql/Internal/NpgsqlQuerySqlGenerator.cs b/src/EFCore.PG/Query/Sql/Internal/NpgsqlQuerySqlGenerator.cs index eb14f2fc9..735518f4b 100644 --- a/src/EFCore.PG/Query/Sql/Internal/NpgsqlQuerySqlGenerator.cs +++ b/src/EFCore.PG/Query/Sql/Internal/NpgsqlQuerySqlGenerator.cs @@ -147,8 +147,6 @@ protected override Expression VisitBinary(BinaryExpression expression) /// protected override Expression VisitUnary(UnaryExpression expression) { - // TODO: I don't think this is called any longer. - // Handled by NpgsqlSqlTranslatingExpressionVisitor.VisitSubQuery. if (expression.NodeType == ExpressionType.ArrayLength) { VisitSqlFunction(new SqlFunctionExpression("array_length", typeof(int), new[] { expression.Operand, Expression.Constant(1) })); diff --git a/test/EFCore.PG.FunctionalTests/Query/ArrayQueryTest.cs b/test/EFCore.PG.FunctionalTests/Query/ArrayQueryTest.cs index 03a26fbe4..52391cbd5 100644 --- a/test/EFCore.PG.FunctionalTests/Query/ArrayQueryTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/ArrayQueryTest.cs @@ -3,7 +3,6 @@ using System.ComponentModel.DataAnnotations.Schema; using System.Linq; using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Internal; using Microsoft.EntityFrameworkCore.TestUtilities; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -452,7 +451,7 @@ public void Array_IndexOf_constant() { using (var ctx = CreateContext()) { - var _ = ctx.SomeEntities.Select(e => e.SomeArray.IndexOf(0)).ToList(); + var _ = ctx.SomeEntities.Select(e => Array.IndexOf(e.SomeArray, 0)).ToList(); AssertContainsInSql(@"SELECT COALESCE(array_position(e.""SomeArray"", 0), -1)"); } } @@ -468,25 +467,45 @@ public void List_IndexOf_constant() } [Fact] - public void Array_ToString() + public void Array_ArrayToString() { using (var ctx = CreateContext()) { - var _ = ctx.SomeEntities.Select(e => e.SomeArray.ToString()).ToList(); + var _ = ctx.SomeEntities.Select(e => EF.Functions.ArrayToString(e.SomeArray, ",")).ToList(); AssertContainsInSql(@"SELECT array_to_string(e.""SomeArray"", ',')"); } } [Fact] - public void List_ToString() + public void List_ArrayToString() { using (var ctx = CreateContext()) { - var _ = ctx.SomeEntities.Select(e => e.SomeList.ToString()).ToList(); + var _ = ctx.SomeEntities.Select(e => EF.Functions.ArrayToString(e.SomeList, ",")).ToList(); AssertContainsInSql(@"SELECT array_to_string(e.""SomeList"", ',')"); } } + [Fact] + public void Array_ArrayToString_with_null() + { + using (var ctx = CreateContext()) + { + var _ = ctx.SomeEntities.Select(e => EF.Functions.ArrayToString(e.SomeArray, ",", "*")).ToList(); + AssertContainsInSql(@"SELECT array_to_string(e.""SomeArray"", ',', '*')"); + } + } + + [Fact] + public void List_ArrayToString_with_null() + { + using (var ctx = CreateContext()) + { + var _ = ctx.SomeEntities.Select(e => EF.Functions.ArrayToString(e.SomeList, ",", "*")).ToList(); + AssertContainsInSql(@"SELECT array_to_string(e.""SomeList"", ',', '*')"); + } + } + #if NETCOREAPP2_1 [Fact] public void Array_Append_constant() From 7479c189a8ec14581d2769574e5af0ecadc74327 Mon Sep 17 00:00:00 2001 From: Austin Drenski Date: Tue, 29 May 2018 13:24:20 -0400 Subject: [PATCH 04/16] Updating array docs --- doc/mapping/array.md | 46 +++++++++++++++++++++++++++++--------------- 1 file changed, 31 insertions(+), 15 deletions(-) diff --git a/doc/mapping/array.md b/doc/mapping/array.md index 11aeca56c..b76be46eb 100644 --- a/doc/mapping/array.md +++ b/doc/mapping/array.md @@ -7,29 +7,45 @@ PostgreSQL has the unique feature of supporting [*array data types*](https://www # Mapping arrays -Simply define a regular .NET array or `List<>` property, and the provider +Npgsql maps PostgreSQL arrays to generic `T[]` and `List` types: ```c# public class Post { public int Id { get; set; } - public string Name { get; set; } - public string[] Tags { get; set; } - public List AlternativeTags { get; set; } + public int[] SomeArray { get; set; } + public List SomeList { get; set; } } ``` -The provider will create `text[]` columns for the above two properties, and will properly detect changes in them - if you load an array and change one of its elements, calling `SaveChanges()` will automatically update the row in the database accordingly. +The provider will create `int[]` columns for the above two properties, and will properly detect changes in them—if you load an array and change one of its elements, calling `SaveChanges()` will automatically update the row in the database accordingly. # Operation translation -The provider can also translate CLR array operations to the corresponding SQL operation; this allows you to efficiently work with arrays by evaluating operations in the database and avoids pulling all the data. The following table lists the range operations that currently get translated. If you run into a missing operation, please open an issue. - -Note that operation translation on `List<>` is limited at this time, but will be improved in the future. It's recommended to use an array for now. - -| C# expression | SQL generated by Npgsql | -|------------------------------------------------------------|-------------------------| -| `.Where(c => c.SomeArray[1] = "foo")` | [`WHERE "c"."SomeArray"[1] = 'foo'`](https://www.postgresql.org/docs/current/static/arrays.html#ARRAYS-ACCESSING) -| `.Where(c => c.SomeArray.SequenceEqual(new[] { 1, 2, 3 })` | [`WHERE "c"."SomeArray" = ARRAY[1, 2, 3])`](https://www.postgresql.org/docs/current/static/arrays.html) -| `.Where(c => c.SomeArray.Contains(3))` | [`WHERE 3 = ANY("c"."SomeArray")`](https://www.postgresql.org/docs/current/static/functions-comparisons.html#AEN21104) -| `.Where(c => c.SomeArray.Length == 3)` | [`WHERE array_length("c"."SomeArray, 1) = 3`](https://www.postgresql.org/docs/current/static/functions-array.html#ARRAY-FUNCTIONS-TABLE) +The provider can also translate CLR array operations to the corresponding SQL operation; this allows you to efficiently work with arrays by evaluating operations in the database and avoids pulling all the data. The following table lists the array operations that currently get translated. If you run into a missing operation, please open an issue. + +| C# expression | SQL generated by Npgsql | +|------------------------------------------------------------------|-------------------------| +| `.Where(c => c.SomeArray[0] == "foo")` | [`WHERE "c"."SomeArray"[1] = 'foo'`](https://www.postgresql.org/docs/current/static/arrays.html#ARRAYS-ACCESSING) +| `.Where(c => c.SomeList[0] == "foo")` | [`WHERE "c"."SomeList"[1] = 'foo'`](https://www.postgresql.org/docs/current/static/arrays.html#ARRAYS-ACCESSING) +| `.Where(c => c.SomeArray.ElementAt(0) == "foo")` | [`WHERE "c"."SomeArray"[1] = 'foo'`](https://www.postgresql.org/docs/current/static/arrays.html#ARRAYS-ACCESSING) +| `.Where(c => c.SomeList.ElementAt(0) == "foo")` | [`WHERE "c"."SomeList"[1] = 'foo'`](https://www.postgresql.org/docs/current/static/arrays.html#ARRAYS-ACCESSING) +| `.Where(x => x.SomeArray.Length == 1)` | [`WHERE array_length(x."SomeArray", 1) = 1`](https://www.postgresql.org/docs/current/static/functions-array.html#ARRAY-FUNCTIONS-TABLE) +| `.Where(x => x.SomeList.Count == 1)` | [`WHERE array_length(x."SomeList", 1) = 1`](https://www.postgresql.org/docs/current/static/functions-array.html#ARRAY-FUNCTIONS-TABLE) +| `.Where(x => x.SomeArray.Count() == 1)` | [`WHERE array_length(x."SomeArray", 1) = 1`](https://www.postgresql.org/docs/current/static/functions-array.html#ARRAY-FUNCTIONS-TABLE) +| `.Where(x => x.SomeList.Count() == 1)` | [`WHERE array_length(x."SomeList", 1) = 1`](https://www.postgresql.org/docs/current/static/functions-array.html#ARRAY-FUNCTIONS-TABLE) +| `.Where(x => x.SomeArray == x.SomeList)` | [`WHERE x."SomeArray" = x."SomeList"`](https://www.postgresql.org/docs/current/static/functions-array.html#ARRAY-FUNCTIONS-TABLE) +| `.Where(x => x.SomeArray.Equals(x.SomeList))` | [`WHERE x."SomeArray" = x."SomeList"`](https://www.postgresql.org/docs/current/static/functions-array.html#ARRAY-FUNCTIONS-TABLE) +| `.Where(x => x.SomeArray.SequenceEquals(x.SomeList))` | [`WHERE x."SomeArray" = x."SomeList"`](https://www.postgresql.org/docs/current/static/functions-array.html#ARRAY-FUNCTIONS-TABLE) +| `.Where(x => x.SomeArray.Contains(0))` | [`WHERE 0 = ANY (x."SomeArray")`](https://www.postgresql.org/docs/current/static/functions-array.html#ARRAY-FUNCTIONS-TABLE) +| `.Where(x => x.SomeList.Contains(0))` | [`WHERE 0 = ANY (x."SomeList")`](https://www.postgresql.org/docs/current/static/functions-array.html#ARRAY-FUNCTIONS-TABLE) +| `.Select(x => x.SomeArray.Append(0))` | [`SELECT x."SomeArray" \|\| 0`](https://www.postgresql.org/docs/current/static/functions-array.html#ARRAY-FUNCTIONS-TABLE) +| `.Select(x => x.SomeList.Append(0))` | [`SELECT x."SomeList" \|\| 0`](https://www.postgresql.org/docs/current/static/functions-array.html#ARRAY-FUNCTIONS-TABLE) +| `.Select(x => x.SomeArray.Prepend(0))` | [`SELECT 0 \|\| x."SomeArray"`](https://www.postgresql.org/docs/current/static/functions-array.html#ARRAY-FUNCTIONS-TABLE) +| `.Select(x => x.SomeList.Prepend(0))` | [`SELECT 0 \|\| x."SomeList"`](https://www.postgresql.org/docs/current/static/functions-array.html#ARRAY-FUNCTIONS-TABLE) +| `.Select(x => x.SomeArray.Concat(x.SomeList))` | [`SELECT x."SomeArray" \|\| x."SomeList"`](https://www.postgresql.org/docs/current/static/functions-array.html#ARRAY-FUNCTIONS-TABLE) +| `.Select(x => x.SomeList.Concat(x.SomeArray))` | [`SELECT x."SomeList" \|\| x."SomeArray"`](https://www.postgresql.org/docs/current/static/functions-array.html#ARRAY-FUNCTIONS-TABLE) +| `.Select(x => EF.Functions.ArrayToString(x.SomeArray, ","))` | [`SELECT array_to_string(x."SomeArray", ',')`](https://www.postgresql.org/docs/current/static/functions-array.html#ARRAY-FUNCTIONS-TABLE) +| `.Select(x => EF.Functions.ArrayToString(x.SomeList, ",", "*"))` | [`SELECT array_to_string(x."SomeList", ',', '*')`](https://www.postgresql.org/docs/current/static/functions-array.html#ARRAY-FUNCTIONS-TABLE) +| `.Select(x => Array.IndexOf(x.SomeArray, 0))` | [`SELECT COALESCE(array_position(x."SomeArray", 1), -1)`](https://www.postgresql.org/docs/current/static/functions-array.html#ARRAY-FUNCTIONS-TABLE) +| `.Select(x => x.SomeList.IndexOf(0))` | [`SELECT COALESCE(array_position(x."SomeList", 1), -1)`](https://www.postgresql.org/docs/current/static/functions-array.html#ARRAY-FUNCTIONS-TABLE) \ No newline at end of file From 2d8c560d43864414ea0dade816d0610da91cd970 Mon Sep 17 00:00:00 2001 From: Austin Drenski Date: Tue, 29 May 2018 16:39:21 -0400 Subject: [PATCH 05/16] Adding an extension mapping for string_to_array --- .../Extensions/NpgsqlArrayExtensions.cs | 72 +++++++++++++------ .../Internal/NpgsqlArrayTranslator.cs | 61 ++++++++-------- .../Utilities/ClientEvaluationNotSupported.cs | 48 +++++++++++++ .../Query/ArrayQueryTest.cs | 28 ++++++++ 4 files changed, 156 insertions(+), 53 deletions(-) create mode 100644 src/EFCore.PG/Utilities/ClientEvaluationNotSupported.cs diff --git a/src/EFCore.PG/Extensions/NpgsqlArrayExtensions.cs b/src/EFCore.PG/Extensions/NpgsqlArrayExtensions.cs index 405431cec..04f3409fd 100644 --- a/src/EFCore.PG/Extensions/NpgsqlArrayExtensions.cs +++ b/src/EFCore.PG/Extensions/NpgsqlArrayExtensions.cs @@ -23,9 +23,9 @@ #endregion -using System; using System.Collections.Generic; using JetBrains.Annotations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Utilities; // ReSharper disable once CheckNamespace namespace Microsoft.EntityFrameworkCore @@ -36,57 +36,85 @@ namespace Microsoft.EntityFrameworkCore public static class NpgsqlArrayExtensions { /// - /// Determines whether a range contains a specified value. + /// Concatenates elements using the supplied delimiter. /// /// The DbFunctions instance. - /// The list to conver to a string in which to locate the value. + /// 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 . /// - /// true if the range contains the specified value; otherwise, false. + /// The string concatenation of the elements with the supplied delimiter. /// public static string ArrayToString([CanBeNull] this DbFunctions _, [NotNull] T[] array, [CanBeNull] string delimiter) - => throw new NotSupportedException(); + => throw new ClientEvaluationNotSupportedException(); /// - /// Determines whether a range contains a specified value. + /// Concatenates elements using the supplied delimiter. /// /// The DbFunctions instance. - /// The list to conver to a string in which to locate the value. + /// 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 type of the elements of . /// - /// true if the range contains the specified value; otherwise, false. + /// The string concatenation of the elements with the supplied delimiter. /// - public static string ArrayToString([CanBeNull] this DbFunctions _, [NotNull] T[] array, [CanBeNull] string delimiter, [CanBeNull] string nullString) - => throw new NotSupportedException(); + public static string ArrayToString([CanBeNull] this DbFunctions _, [NotNull] List list, [CanBeNull] string delimiter) + => throw new ClientEvaluationNotSupportedException(); /// - /// Determines whether a range contains a specified value. + /// Concatenates elements using the supplied delimiter and the string representation for null elements. /// /// The DbFunctions instance. - /// The list to conver to a string in which to locate the value. + /// 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 value used to represent a null value. + /// The type of the elements of . /// - /// true if the range contains the specified value; otherwise, false. + /// The string concatenation of the elements with the supplied delimiter and null string. /// - public static string ArrayToString([CanBeNull] this DbFunctions _, [NotNull] List list, [CanBeNull] string delimiter) - => throw new NotSupportedException(); + public static string ArrayToString([CanBeNull] this DbFunctions _, [NotNull] T[] array, [CanBeNull] string delimiter, [CanBeNull] string nullString) + => throw new ClientEvaluationNotSupportedException(); /// - /// Determines whether a range contains a specified value. + /// Concatenates elements using the supplied delimiter and the string representation for null elements. /// /// The DbFunctions instance. - /// The list to conver to a string in which to locate the value. + /// 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 . /// - /// true if the range contains the specified value; otherwise, false. + /// The string concatenation of the elements with the supplied delimiter and null string. /// public static string ArrayToString([CanBeNull] this DbFunctions _, [NotNull] List list, [CanBeNull] string delimiter, [CanBeNull] string nullString) - => throw new NotSupportedException(); + => throw new ClientEvaluationNotSupportedException(); + + /// + /// Converts the input string into an array using the supplied delimiter and the string representation for null elements. + /// + /// 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. + /// + public static T[] StringToArray([CanBeNull] this DbFunctions _, [NotNull] string input, [CanBeNull] string delimiter, [CanBeNull] string nullString) + => throw new ClientEvaluationNotSupportedException(); + + /// + /// Converts the input string into a using the supplied delimiter and the string representation for null elements. + /// + /// 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. + /// + public static List StringToList([CanBeNull] this DbFunctions _, [NotNull] string input, [CanBeNull] string delimiter, [CanBeNull] string nullString) + => throw new ClientEvaluationNotSupportedException(); } } diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayTranslator.cs index 4656523e3..3a6847079 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayTranslator.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayTranslator.cs @@ -53,22 +53,12 @@ public Expression Translate(MethodCallExpression expression) if (!IsTypeSupported(expression)) return null; - if (!IsMethodSupported(expression.Method)) - return null; - // TODO: use #430 to map @> to source.All(x => other.Contains(x)); // TODO: use #430 to map && to soucre.Any(x => other.Contains(x)); switch (expression.Method.Name) { - #region Instance - - case "get_Item" when expression.Object is Expression instance: - return Expression.MakeIndex(instance, instance.Type.GetRuntimeProperty("Item"), expression.Arguments); - - #endregion - - #region Enumerable + #region EnumerableStaticMethods case nameof(Enumerable.ElementAt): return Expression.MakeIndex(expression.Arguments[0], expression.Arguments[0].Type.GetRuntimeProperty("Item"), new[] { expression.Arguments[1] }); @@ -94,9 +84,15 @@ public Expression Translate(MethodCallExpression expression) case nameof(NpgsqlArrayExtensions.ArrayToString): return new SqlFunctionExpression("array_to_string", typeof(string), expression.Arguments.Skip(1)); + case nameof(NpgsqlArrayExtensions.StringToArray): + return new SqlFunctionExpression("string_to_array", expression.Method.ReturnType, expression.Arguments.Skip(1)); + + case nameof(NpgsqlArrayExtensions.StringToList): + return new SqlFunctionExpression("string_to_array", expression.Method.ReturnType, expression.Arguments.Skip(1)); + #endregion - #region ArrayStatic + #region ArrayStaticMethods case nameof(Array.IndexOf) when expression.Method.DeclaringType == typeof(Array): return @@ -111,7 +107,10 @@ public Expression Translate(MethodCallExpression expression) #endregion - #region ListInstance + #region ListInstanceMethods + + case "get_Item" when expression.Object is Expression instance: + return Expression.MakeIndex(instance, instance.Type.GetRuntimeProperty("Item"), expression.Arguments); case nameof(IList.IndexOf) when IsArrayOrList(expression.Method.DeclaringType): return @@ -144,21 +143,34 @@ public Expression Translate(MethodCallExpression expression) /// static bool IsTypeSupported([NotNull] MethodCallExpression expression) { + Type declaringType = expression.Method.DeclaringType; + + // Methods declared here are always translated. + if (declaringType == typeof(NpgsqlArrayExtensions)) + return true; + + // Methods not declared here are never translated. + if (!IsArrayOrList(declaringType) && + declaringType != typeof(Array) && + declaringType != typeof(Enumerable)) + return false; + + // Instance methods are only translated for T[] and List. if (expression.Object is Expression instance) return IsArrayOrList(instance.Type); + // Extension methods may only be translated when a parameter is T[] or List if (expression.Object is null) { + // Static method with no parameters? Skip. if (expression.Arguments.Count == 0) return false; - if (expression.Arguments.Count > 1 && - expression.Arguments[0].Type == typeof(DbFunctions)) - return IsArrayOrList(expression.Arguments[1].Type); - + // Is this an extension method on T[] or List? return IsArrayOrList(expression.Arguments[0].Type); } + // Something else? Skip. return false; } @@ -171,20 +183,7 @@ static bool IsTypeSupported([NotNull] MethodCallExpression expression) /// /// True if is an array or a ; otherwise, false. /// - static bool IsArrayOrList(Type type) - => type.IsArray || type.IsGenericType && typeof(List<>) == type.GetGenericTypeDefinition(); - - /// - /// Tests if the method is declared on an array, a , or . - /// - /// - /// The method to test. - /// - /// - /// True if is declared on an array, a , or ; otherwise, false. - /// - static bool IsMethodSupported([NotNull] MethodInfo method) - => method.DeclaringType is Type t && (IsArrayOrList(t) || t == typeof(Array) || t == typeof(Enumerable) || t == typeof(NpgsqlArrayExtensions)); + static bool IsArrayOrList(Type type) => type.IsArray || type.IsGenericType && typeof(List<>) == type.GetGenericTypeDefinition(); #endregion } diff --git a/src/EFCore.PG/Utilities/ClientEvaluationNotSupported.cs b/src/EFCore.PG/Utilities/ClientEvaluationNotSupported.cs new file mode 100644 index 000000000..8fce1fdbb --- /dev/null +++ b/src/EFCore.PG/Utilities/ClientEvaluationNotSupported.cs @@ -0,0 +1,48 @@ +#region License + +// The PostgreSQL License +// +// Copyright (C) 2016 The Npgsql Development Team +// +// Permission to use, copy, modify, and distribute this software and its +// documentation for any purpose, without fee, and without a written +// agreement is hereby granted, provided that the above copyright notice +// and this paragraph and the following two paragraphs appear in all copies. +// +// IN NO EVENT SHALL THE NPGSQL DEVELOPMENT TEAM BE LIABLE TO ANY PARTY +// FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, +// INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS +// DOCUMENTATION, EVEN IF THE NPGSQL DEVELOPMENT TEAM HAS BEEN ADVISED OF +// THE POSSIBILITY OF SUCH DAMAGE. +// +// THE NPGSQL DEVELOPMENT TEAM SPECIFICALLY DISCLAIMS ANY WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS +// ON AN "AS IS" BASIS, AND THE NPGSQL DEVELOPMENT TEAM HAS NO OBLIGATIONS +// TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + +#endregion + +using System; +using System.Runtime.CompilerServices; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Utilities +{ + /// + /// The exception that is thrown when a method intended for SQL translation is evaluated by the client. + /// + public class ClientEvaluationNotSupportedException : NotSupportedException + { + readonly string _callerMemberName; + + /// + public override string Message + => $"{_callerMemberName} is only intended for use via SQL translation as part of an EF Core LINQ query."; + + /// + public ClientEvaluationNotSupportedException([CallerMemberName] string method = default) + { + _callerMemberName = method; + } + } +} diff --git a/test/EFCore.PG.FunctionalTests/Query/ArrayQueryTest.cs b/test/EFCore.PG.FunctionalTests/Query/ArrayQueryTest.cs index 52391cbd5..d423bdbbe 100644 --- a/test/EFCore.PG.FunctionalTests/Query/ArrayQueryTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/ArrayQueryTest.cs @@ -506,6 +506,34 @@ public void List_ArrayToString_with_null() } } + [Fact] + public void Array_StringToArray() + { + using (var ctx = CreateContext()) + { + var _ = + ctx.SomeEntities + .Select(e => EF.Functions.ArrayToString(e.SomeArray, ",", "*")) + .Select(e => EF.Functions.StringToArray(e, ",", "*")).ToList(); + + AssertContainsInSql(@"SELECT string_to_array(array_to_string(e.""SomeArray"", ',', '*'), ',', '*')"); + } + } + + [Fact] + public void List_StringToList() + { + using (var ctx = CreateContext()) + { + var _ = + ctx.SomeEntities + .Select(e => EF.Functions.ArrayToString(e.SomeList, ",", "*")) + .Select(e => EF.Functions.StringToList(e, ",", "*")).ToList(); + + AssertContainsInSql(@"SELECT string_to_array(array_to_string(e.""SomeList"", ',', '*'), ',', '*')"); + } + } + #if NETCOREAPP2_1 [Fact] public void Array_Append_constant() From 79b966fb92a1206b86b16573452ae0a7dc4a0e42 Mon Sep 17 00:00:00 2001 From: Austin Drenski Date: Tue, 29 May 2018 17:12:29 -0400 Subject: [PATCH 06/16] Rebase + fixing mistakes during rebase --- .../Expressions/Internal/ExplicitStoreTypeCastExpression.cs | 2 -- src/EFCore.PG/Query/Sql/Internal/NpgsqlQuerySqlGenerator.cs | 3 +-- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/EFCore.PG/Query/Expressions/Internal/ExplicitStoreTypeCastExpression.cs b/src/EFCore.PG/Query/Expressions/Internal/ExplicitStoreTypeCastExpression.cs index 7191f2c1c..68e2352bb 100644 --- a/src/EFCore.PG/Query/Expressions/Internal/ExplicitStoreTypeCastExpression.cs +++ b/src/EFCore.PG/Query/Expressions/Internal/ExplicitStoreTypeCastExpression.cs @@ -1,5 +1,4 @@ #region License - // The PostgreSQL License // // Copyright (C) 2016 The Npgsql Development Team @@ -20,7 +19,6 @@ // AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS // ON AN "AS IS" BASIS, AND THE NPGSQL DEVELOPMENT TEAM HAS NO OBLIGATIONS // TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. - #endregion using System; diff --git a/src/EFCore.PG/Query/Sql/Internal/NpgsqlQuerySqlGenerator.cs b/src/EFCore.PG/Query/Sql/Internal/NpgsqlQuerySqlGenerator.cs index 735518f4b..417b3d899 100644 --- a/src/EFCore.PG/Query/Sql/Internal/NpgsqlQuerySqlGenerator.cs +++ b/src/EFCore.PG/Query/Sql/Internal/NpgsqlQuerySqlGenerator.cs @@ -197,9 +197,7 @@ protected override Expression VisitIndex(IndexExpression expression) Visit(GenerateOneBasedIndexExpression(expression.Arguments[i])); Sql.Append(']'); } - Sql.Append(')'); - return expression; } @@ -208,6 +206,7 @@ protected override Expression VisitIndex(IndexExpression expression) /// public Expression VisitArrayAnyAll(ArrayAnyAllExpression arrayAnyAllExpression) { + Visit(arrayAnyAllExpression.Operand); Sql.Append(' '); Sql.Append(arrayAnyAllExpression.Operator); Sql.Append(' '); From 9f081a94add3c3f9de914f61c35321a445e7a1dc Mon Sep 17 00:00:00 2001 From: Austin Drenski Date: Tue, 29 May 2018 23:52:28 -0400 Subject: [PATCH 07/16] Adds array contains (@>) and overlaps (&&) support - This support is fragile: - Tests are passing...but more complex tests are needed. Contains (@>): - src.Where(x => x.SomeList.All(y => x.SomeArray.Contains(y))) - WHERE x."SomeArray" @> x."SomeList" Overlaps (&&): - src.Where(x => x.SomeArray.Any(y => x.SomeList.Contains(y))) - WHERE x."SomeArray" && x."SomeList" --- .../Extensions/NpgsqlArrayExtensions.cs | 18 +++ .../Internal/NpgsqlArrayTranslator.cs | 3 - .../NpgsqlSqlTranslatingExpressionVisitor.cs | 134 ++++++++++++------ .../Internal/ArrayAnyAllExpression.cs | 5 + .../Query/ArrayQueryTest.cs | 40 ++++++ 5 files changed, 156 insertions(+), 44 deletions(-) diff --git a/src/EFCore.PG/Extensions/NpgsqlArrayExtensions.cs b/src/EFCore.PG/Extensions/NpgsqlArrayExtensions.cs index 04f3409fd..afbe423c6 100644 --- a/src/EFCore.PG/Extensions/NpgsqlArrayExtensions.cs +++ b/src/EFCore.PG/Extensions/NpgsqlArrayExtensions.cs @@ -45,6 +45,9 @@ public static class NpgsqlArrayExtensions /// /// 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. + /// public static string ArrayToString([CanBeNull] this DbFunctions _, [NotNull] T[] array, [CanBeNull] string delimiter) => throw new ClientEvaluationNotSupportedException(); @@ -58,6 +61,9 @@ public static string ArrayToString([CanBeNull] this DbFunctions _, [NotNull] /// /// 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. + /// public static string ArrayToString([CanBeNull] this DbFunctions _, [NotNull] List list, [CanBeNull] string delimiter) => throw new ClientEvaluationNotSupportedException(); @@ -72,6 +78,9 @@ public static string ArrayToString([CanBeNull] this DbFunctions _, [NotNull] /// /// 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. + /// public static string ArrayToString([CanBeNull] this DbFunctions _, [NotNull] T[] array, [CanBeNull] string delimiter, [CanBeNull] string nullString) => throw new ClientEvaluationNotSupportedException(); @@ -86,6 +95,9 @@ public static string ArrayToString([CanBeNull] this DbFunctions _, [NotNull] /// /// 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. + /// public static string ArrayToString([CanBeNull] this DbFunctions _, [NotNull] List list, [CanBeNull] string delimiter, [CanBeNull] string nullString) => throw new ClientEvaluationNotSupportedException(); @@ -100,6 +112,9 @@ public static string ArrayToString([CanBeNull] this DbFunctions _, [NotNull] /// /// 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. + /// public static T[] StringToArray([CanBeNull] this DbFunctions _, [NotNull] string input, [CanBeNull] string delimiter, [CanBeNull] string nullString) => throw new ClientEvaluationNotSupportedException(); @@ -114,6 +129,9 @@ public static T[] StringToArray([CanBeNull] this DbFunctions _, [NotNull] str /// /// 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. + /// public static List StringToList([CanBeNull] this DbFunctions _, [NotNull] string input, [CanBeNull] string delimiter, [CanBeNull] string nullString) => throw new ClientEvaluationNotSupportedException(); } diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayTranslator.cs index 3a6847079..be6a585fc 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayTranslator.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayTranslator.cs @@ -53,9 +53,6 @@ public Expression Translate(MethodCallExpression expression) if (!IsTypeSupported(expression)) return null; - // TODO: use #430 to map @> to source.All(x => other.Contains(x)); - // TODO: use #430 to map && to soucre.Any(x => other.Contains(x)); - switch (expression.Method.Name) { #region EnumerableStaticMethods diff --git a/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlSqlTranslatingExpressionVisitor.cs b/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlSqlTranslatingExpressionVisitor.cs index 9dc95ffc5..b18a13a5f 100644 --- a/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlSqlTranslatingExpressionVisitor.cs @@ -25,7 +25,6 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Linq.Expressions; using System.Reflection; using JetBrains.Annotations; @@ -91,10 +90,14 @@ public NpgsqlSqlTranslatingExpressionVisitor( : base(dependencies, queryModelVisitor, targetSelectExpression, topLevelPredicate, inProjection) => _queryModelVisitor = queryModelVisitor; + // TODO: This should be refactored along the lines of NpgsqlCompositeMethodCallTranslator. /// [CanBeNull] protected override Expression VisitSubQuery(SubQueryExpression expression) - => base.VisitSubQuery(expression) ?? VisitLikeAnyAll(expression) ?? VisitEqualsAny(expression); + => base.VisitSubQuery(expression) ?? + VisitConcatContainsCount(expression) ?? + VisitAnyAllLike(expression) ?? + VisitAnyAllContains(expression); /// protected override Expression VisitBinary(BinaryExpression expression) @@ -121,64 +124,71 @@ protected override Expression VisitBinary(BinaryExpression expression) } /// - /// Visits a and attempts to translate a '= ANY' expression. + /// Visits an array-based to translate a + /// , + /// , or + /// . /// /// The expression to visit. /// - /// An '= ANY' expression or null. + /// An expression or null. /// [CanBeNull] - protected virtual Expression VisitEqualsAny([NotNull] SubQueryExpression expression) + protected virtual Expression VisitConcatContainsCount([NotNull] SubQueryExpression expression) { - var subQueryModel = expression.QueryModel; - var fromExpression = subQueryModel.MainFromClause.FromExpression; + var queryModel = expression.QueryModel; + var from = queryModel.MainFromClause.FromExpression; + var results = queryModel.ResultOperators; - var properties = MemberAccessBindingExpressionVisitor.GetPropertyPath( - fromExpression, _queryModelVisitor.QueryCompilationContext, out _); + if (!IsArrayOrList(from.Type) || results.Count != 1) + return null; - if (properties.Count == 0) + // BUG: This keeps a few unit tests from failing. + // - SimpleQueryNpgsqlTest.Contains_with_local_anonymous_type_array_closure + // - SimpleQueryNpgsqlTest.Contains_with_local_tuple_array_closure + // - SimpleQueryNpgsqlTest.Where_navigation_contains + if (from is ParameterExpression) return null; - var lastPropertyType = properties[properties.Count - 1].ClrType; - if (IsArrayOrList(lastPropertyType) && subQueryModel.ResultOperators.Count > 0) + + var array = Visit(from) ?? from; + + switch (results[0]) { - if (subQueryModel.ResultOperators.First() is ConcatResultOperator concatResultOperator && - Visit(fromExpression) is Expression first && - Visit(concatResultOperator.Source2) is Expression second) - return new CustomBinaryExpression(first, second, "||", first.Type); + case ConcatResultOperator concat when IsArrayOrList(concat.Source2.Type): + return new CustomBinaryExpression(array, Visit(concat.Source2) ?? concat.Source2, "||", array.Type); - // Translate someArray.Length - if (subQueryModel.ResultOperators.First() is CountResultOperator) - { - if (lastPropertyType.IsArray) - return Expression.ArrayLength(Visit(fromExpression)); + case ContainsResultOperator contains: + return new ArrayAnyAllExpression(ArrayComparisonType.ANY, "=", Visit(contains.Item) ?? contains.Item, array); - return new SqlFunctionExpression("array_length", typeof(int), new[] { Visit(fromExpression), Expression.Constant(1) }); - } + case CountResultOperator _ when array.Type.IsArray: + return Expression.ArrayLength(array); - // Translate someArray.Contains(someValue) - if (subQueryModel.ResultOperators.First() is ContainsResultOperator contains) - if (Visit(contains.Item) is Expression containsItem && Visit(fromExpression) is Expression source) - return new ArrayAnyAllExpression(ArrayComparisonType.ANY, "=", containsItem, source); - } + case CountResultOperator _: + return new SqlFunctionExpression("array_length", typeof(int), new[] { array, Expression.Constant(1) }); - return null; + default: + return null; + } } /// - /// Visits a and attempts to translate a LIKE/ILIKE ANY/ALL expression. + /// Visits an array-based to translate a + /// or + /// when the relevant predicate is a LIKE or ILIKE expression. /// /// The expression to visit. /// - /// 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 VisitAnyAllLike([NotNull] SubQueryExpression expression) { var queryModel = expression.QueryModel; - var results = queryModel.ResultOperators; var body = queryModel.BodyClauses; + var from = queryModel.MainFromClause.FromExpression; + var results = queryModel.ResultOperators; - if (results.Count != 1) + if (!IsArrayOrList(from.Type) || results.Count != 1) return null; ArrayComparisonType comparisonType; @@ -207,22 +217,65 @@ whereClause.Predicate is MethodCallExpression methocCall if (call is null) return null; - var source = queryModel.MainFromClause.FromExpression; + var operand = Visit(call.Arguments[1]) ?? call.Arguments[1]; + var array = Visit(from) ?? from; // ReSharper disable AssignNullToNotNullAttribute switch (call.Method) { case MethodInfo m when m == Like2MethodInfo: - return new ArrayAnyAllExpression(comparisonType, "LIKE", Visit(call.Arguments[1]), Visit(source)); + return new ArrayAnyAllExpression(comparisonType, "LIKE", operand, array); case MethodInfo m when m == Like3MethodInfo: - return new ArrayAnyAllExpression(comparisonType, "LIKE", Visit(call.Arguments[1]), Visit(source)); + return new ArrayAnyAllExpression(comparisonType, "LIKE", operand, array); case MethodInfo m when m == ILike2MethodInfo: - return new ArrayAnyAllExpression(comparisonType, "ILIKE", Visit(call.Arguments[1]), Visit(source)); + return new ArrayAnyAllExpression(comparisonType, "ILIKE", operand, array); case MethodInfo m when m == ILike3MethodInfo: - return new ArrayAnyAllExpression(comparisonType, "ILIKE", Visit(call.Arguments[1]), Visit(source)); + return new ArrayAnyAllExpression(comparisonType, "ILIKE", operand, array); + + default: + return null; + } + } + + /// + /// Visits an array-based to translate a + /// or + /// when the relevant predicate is a + /// for which the relevant predicate is a . + /// + /// The expression to visit. + /// + /// An expression or null. + /// + [CanBeNull] + protected virtual Expression VisitAnyAllContains([NotNull] SubQueryExpression expression) + { + var queryModel = expression.QueryModel; + var body = queryModel.BodyClauses; + var from = queryModel.MainFromClause.FromExpression; + var results = queryModel.ResultOperators; + + if (!IsArrayOrList(from.Type) || results.Count != 1) + return null; + + var array = Visit(from) ?? from; + + switch (results[0]) + { + case AnyResultOperator _ + when body.Count == 1 && + body[0] is WhereClause where && + Visit(where.Predicate) is ArrayAnyAllExpression a && + a.IsAnyEquals: + return new CustomBinaryExpression(array, Visit(a.Array) ?? a.Array, "&&", typeof(bool)); + + case AllResultOperator all + when Visit(all.Predicate) is ArrayAnyAllExpression a && + a.IsAnyEquals: + return new CustomBinaryExpression(Visit(a.Array) ?? a.Array, array, "@>", typeof(bool)); default: return null; @@ -238,7 +291,6 @@ whereClause.Predicate is MethodCallExpression methocCall /// /// True if is an array or a ; otherwise, false. /// - static bool IsArrayOrList([NotNull] Type type) - => type.IsArray || type.IsGenericType && typeof(List<>) == type.GetGenericTypeDefinition(); + static bool IsArrayOrList([NotNull] Type type) => type.IsArray || type.IsGenericType && typeof(List<>) == type.GetGenericTypeDefinition(); } } diff --git a/src/EFCore.PG/Query/Expressions/Internal/ArrayAnyAllExpression.cs b/src/EFCore.PG/Query/Expressions/Internal/ArrayAnyAllExpression.cs index 04054d3a4..62a79cd28 100644 --- a/src/EFCore.PG/Query/Expressions/Internal/ArrayAnyAllExpression.cs +++ b/src/EFCore.PG/Query/Expressions/Internal/ArrayAnyAllExpression.cs @@ -68,6 +68,11 @@ public class ArrayAnyAllExpression : Expression, IEquatable public virtual ArrayComparisonType ArrayComparisonType { get; } + /// + /// True if this instance represents: {operand} = ANY ({array})". + /// + public bool IsAnyEquals => ArrayComparisonType is ArrayComparisonType.ANY && Operator is "="; + /// /// Constructs a . /// diff --git a/test/EFCore.PG.FunctionalTests/Query/ArrayQueryTest.cs b/test/EFCore.PG.FunctionalTests/Query/ArrayQueryTest.cs index d423bdbbe..045b0bbf0 100644 --- a/test/EFCore.PG.FunctionalTests/Query/ArrayQueryTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/ArrayQueryTest.cs @@ -534,6 +534,46 @@ public void List_StringToList() } } + [Fact] + public void Array_All_Contains_List() + { + using (var ctx = CreateContext()) + { + var _ = ctx.SomeEntities.Where(x => x.SomeList.All(y => x.SomeArray.Contains(y))).ToList(); + AssertContainsInSql(@"WHERE (x.""SomeArray"" @> x.""SomeList"") = TRUE"); + } + } + + [Fact] + public void List_All_Contains_Array() + { + using (var ctx = CreateContext()) + { + var _ = ctx.SomeEntities.Where(x => x.SomeArray.All(y => x.SomeList.Contains(y))).ToList(); + AssertContainsInSql(@"WHERE (x.""SomeList"" @> x.""SomeArray"") = TRUE"); + } + } + + [Fact] + public void Array_Any_Contains_List() + { + using (var ctx = CreateContext()) + { + var _ = ctx.SomeEntities.Where(x => x.SomeArray.Any(y => x.SomeList.Contains(y))).ToList(); + AssertContainsInSql(@"WHERE (x.""SomeArray"" && x.""SomeList"") = TRUE"); + } + } + + [Fact] + public void List_Any_Contains_Array() + { + using (var ctx = CreateContext()) + { + var _ = ctx.SomeEntities.Where(x => x.SomeList.Any(y => x.SomeArray.Contains(y))).ToList(); + AssertContainsInSql(@"WHERE (x.""SomeList"" && x.""SomeArray"") = TRUE"); + } + } + #if NETCOREAPP2_1 [Fact] public void Array_Append_constant() From ab75e97c5d7581f9d9f84a22447fea030eb40aaa Mon Sep 17 00:00:00 2001 From: Austin Drenski Date: Wed, 30 May 2018 11:09:50 -0400 Subject: [PATCH 08/16] Refactors subquery handling for complex patterns This refactors `NpgsqlSqlTranslatingExpressionVisitor` to use `VisitSubQuery` to delegate to `VisitArraySubQuery` which itself delegates to more specialized virtual methods based on the subquery's result operator. The goal is to allow the subquery visits to be called recursively to handle more complex patterns. For example, translating the array operator `<@` involves identifying a subquery nested inside of a subquery. Rather than duplicating identification logic, we want to use the standard visitor approach and send a nested subquery back up to `Visit`. Another consideration is the ability to compose result operators within an `Any` or `All` result. These result operators are almost exclusively designed to have a nested condition or subquery. --- .../NpgsqlSqlTranslatingExpressionVisitor.cs | 218 ++++++++++-------- .../Internal/ArrayAnyAllExpression.cs | 4 +- .../Query/ArrayQueryTest.cs | 4 +- 3 files changed, 128 insertions(+), 98 deletions(-) diff --git a/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlSqlTranslatingExpressionVisitor.cs b/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlSqlTranslatingExpressionVisitor.cs index b18a13a5f..ee7d0c184 100644 --- a/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlSqlTranslatingExpressionVisitor.cs @@ -25,6 +25,7 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq.Expressions; using System.Reflection; using JetBrains.Annotations; @@ -45,6 +46,8 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionVisitors /// public class NpgsqlSqlTranslatingExpressionVisitor : SqlTranslatingExpressionVisitor { + #region MethodInfoFields + /// /// The for . /// @@ -75,6 +78,8 @@ public class NpgsqlSqlTranslatingExpressionVisitor : SqlTranslatingExpressionVis typeof(NpgsqlDbFunctionsExtensions) .GetRuntimeMethod(nameof(NpgsqlDbFunctionsExtensions.ILike), new[] { typeof(DbFunctions), typeof(string), typeof(string), typeof(string) }); + #endregion + /// /// The query model visitor. /// @@ -90,14 +95,12 @@ public NpgsqlSqlTranslatingExpressionVisitor( : base(dependencies, queryModelVisitor, targetSelectExpression, topLevelPredicate, inProjection) => _queryModelVisitor = queryModelVisitor; - // TODO: This should be refactored along the lines of NpgsqlCompositeMethodCallTranslator. + #region Overrides + /// [CanBeNull] protected override Expression VisitSubQuery(SubQueryExpression expression) - => base.VisitSubQuery(expression) ?? - VisitConcatContainsCount(expression) ?? - VisitAnyAllLike(expression) ?? - VisitAnyAllContains(expression); + => base.VisitSubQuery(expression) ?? VisitArraySubQuery(expression); /// protected override Expression VisitBinary(BinaryExpression expression) @@ -111,60 +114,65 @@ protected override Expression VisitBinary(BinaryExpression expression) var lastPropertyType = properties[properties.Count - 1].ClrType; if (lastPropertyType.IsArray && lastPropertyType.GetArrayRank() == 1) { - var left = Visit(expression.Left); - var right = Visit(expression.Right); + var left = Visit(expression.Left) ?? expression.Left; + var right = Visit(expression.Right) ?? expression.Right; - return left != null && right != null - ? Expression.MakeBinary(ExpressionType.ArrayIndex, left, right) - : null; + return Expression.MakeBinary(ExpressionType.ArrayIndex, left, right); } } return base.VisitBinary(expression); } + #endregion + + #region ArraySubQueries + /// - /// Visits an array-based to translate a - /// , - /// , or - /// . + /// Visits an array-based subquery. /// - /// The expression to visit. + /// The subquery expression. /// /// An expression or null. /// [CanBeNull] - protected virtual Expression VisitConcatContainsCount([NotNull] SubQueryExpression expression) + protected virtual Expression VisitArraySubQuery([NotNull] SubQueryExpression expression) { var queryModel = expression.QueryModel; var from = queryModel.MainFromClause.FromExpression; + var body = queryModel.BodyClauses; var results = queryModel.ResultOperators; - if (!IsArrayOrList(from.Type) || results.Count != 1) + // TODO: what causes the from expression to not be visitable? + // Only handle subqueries when the from expression is visitable. + if (!(Visit(from) is Expression array)) return null; - // BUG: This keeps a few unit tests from failing. - // - SimpleQueryNpgsqlTest.Contains_with_local_anonymous_type_array_closure - // - SimpleQueryNpgsqlTest.Contains_with_local_tuple_array_closure - // - SimpleQueryNpgsqlTest.Where_navigation_contains - if (from is ParameterExpression) + // Only handle types mapped to PostgreSQL arrays. + if (!IsArrayOrList(array.Type)) return null; - var array = Visit(from) ?? from; + // TODO: when is there more than one result operator? + // Only handle singular result operators. + if (results.Count != 1) + return null; switch (results[0]) { - case ConcatResultOperator concat when IsArrayOrList(concat.Source2.Type): - return new CustomBinaryExpression(array, Visit(concat.Source2) ?? concat.Source2, "||", array.Type); + case AnyResultOperator _: + return VisitArrayAny(array, body); + + case AllResultOperator allResultOperator: + return VisitArrayAll(array, allResultOperator); + + case ConcatResultOperator concatResultOperator: + return VisitArrayConcat(array, concatResultOperator); case ContainsResultOperator contains: return new ArrayAnyAllExpression(ArrayComparisonType.ANY, "=", Visit(contains.Item) ?? contains.Item, array); - case CountResultOperator _ when array.Type.IsArray: - return Expression.ArrayLength(array); - - case CountResultOperator _: - return new SqlFunctionExpression("array_length", typeof(int), new[] { array, Expression.Constant(1) }); + case CountResultOperator countResultOperator: + return VisitArrayCount(array, countResultOperator); default: return null; @@ -172,55 +180,93 @@ protected virtual Expression VisitConcatContainsCount([NotNull] SubQueryExpressi } /// - /// Visits an array-based to translate a - /// or - /// when the relevant predicate is a LIKE or ILIKE expression. + /// Visits an array-based ANY comparison: {operand} {operator} ANY ({array}). /// - /// The expression to visit. + /// The array expression. + /// The body clauses. /// /// An expression or null. /// [CanBeNull] - protected virtual Expression VisitAnyAllLike([NotNull] SubQueryExpression expression) + protected virtual Expression VisitArrayAny(Expression array, [NotNull] ObservableCollection body) { - var queryModel = expression.QueryModel; - var body = queryModel.BodyClauses; - var from = queryModel.MainFromClause.FromExpression; - var results = queryModel.ResultOperators; + var predicate = + body.Count == 1 && + body[0] is WhereClause whereClause + ? whereClause.Predicate + : null; - if (!IsArrayOrList(from.Type) || results.Count != 1) + if (predicate is null) 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; + return + VisitArrayLike(array, predicate, ArrayComparisonType.ANY) ?? + VisitArrayContains(array, predicate, ArrayComparisonType.ANY); + } - default: - return null; - } + /// + /// Visits an array-based ALL comparison: {operand} {operator} ALL ({array}). + /// + /// The array expression. + /// The result operator. + /// + /// An expression or null. + /// + [CanBeNull] + protected virtual Expression VisitArrayAll([NotNull] Expression array, [NotNull] AllResultOperator allResultOperator) + => VisitArrayLike(array, allResultOperator.Predicate, ArrayComparisonType.ALL) ?? + VisitArrayContains(array, allResultOperator.Predicate, ArrayComparisonType.ALL); + + /// + /// Visits an array-based concatenation expression: {array|value} || {array|value}. + /// + /// The source expression. + /// The result operator. + /// + /// An expression or null. + /// + [CanBeNull] + protected virtual Expression VisitArrayConcat([NotNull] Expression array, [NotNull] ConcatResultOperator concatResultOperator) + { + var other = Visit(concatResultOperator.Source2) ?? concatResultOperator.Source2; + return IsArrayOrList(other.Type) ? new CustomBinaryExpression(array, other, "||", array.Type) : null; + } - if (call is null) + /// + /// Visits an array-based count expression: {array}.Length, {list}.Count, {array|list}.Count(), {array|list}.Count({predicate}). + /// + /// The source expression. + /// The result operator. + /// + /// An expression or null. + /// + [CanBeNull] + protected virtual Expression VisitArrayCount([NotNull] Expression array, [NotNull] CountResultOperator countResultOperator) + { + // TODO: handle count operation with predicate. + + return array.Type.IsArray + ? (Expression)Expression.ArrayLength(array) + : new SqlFunctionExpression("array_length", typeof(int), new[] { array, Expression.Constant(1) }); + } + + /// + /// 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] + protected virtual Expression VisitArrayLike([NotNull] Expression array, [NotNull] Expression predicate, ArrayComparisonType comparisonType) + { + if (!(predicate is MethodCallExpression call)) return null; var operand = Visit(call.Arguments[1]) ?? call.Arguments[1]; - var array = Visit(from) ?? from; - // ReSharper disable AssignNullToNotNullAttribute switch (call.Method) { case MethodInfo m when m == Like2MethodInfo: @@ -241,47 +287,29 @@ whereClause.Predicate is MethodCallExpression methocCall } /// - /// Visits an array-based to translate a - /// or - /// when the relevant predicate is a - /// for which the relevant predicate is a . + /// Visits an array-based comparison for a containment expression: {operand} = {ANY|ALL} ({array}). /// - /// The expression to visit. + /// The array expression. + /// The method call expression. + /// The array comparison type. /// /// An expression or null. /// [CanBeNull] - protected virtual Expression VisitAnyAllContains([NotNull] SubQueryExpression expression) + protected virtual Expression VisitArrayContains([NotNull] Expression array, [NotNull] Expression predicate, ArrayComparisonType comparisonType) { - var queryModel = expression.QueryModel; - var body = queryModel.BodyClauses; - var from = queryModel.MainFromClause.FromExpression; - var results = queryModel.ResultOperators; - - if (!IsArrayOrList(from.Type) || results.Count != 1) + if (!(Visit(predicate) is ArrayAnyAllExpression expression) || !expression.IsContainsExpression) return null; - var array = Visit(from) ?? from; + var inner = Visit(expression.Array) ?? expression.Array; - switch (results[0]) - { - case AnyResultOperator _ - when body.Count == 1 && - body[0] is WhereClause where && - Visit(where.Predicate) is ArrayAnyAllExpression a && - a.IsAnyEquals: - return new CustomBinaryExpression(array, Visit(a.Array) ?? a.Array, "&&", typeof(bool)); - - case AllResultOperator all - when Visit(all.Predicate) is ArrayAnyAllExpression a && - a.IsAnyEquals: - return new CustomBinaryExpression(Visit(a.Array) ?? a.Array, array, "@>", typeof(bool)); - - default: - return null; - } + return new CustomBinaryExpression(array, inner, comparisonType == ArrayComparisonType.ALL ? "<@" : "&&", typeof(bool)); } + #endregion + + #region Helpers + /// /// Tests if the type is an array or a . /// @@ -292,5 +320,7 @@ when Visit(all.Predicate) is ArrayAnyAllExpression a && /// True if is an array or a ; otherwise, false. /// static bool IsArrayOrList([NotNull] Type type) => type.IsArray || type.IsGenericType && typeof(List<>) == type.GetGenericTypeDefinition(); + + #endregion } } diff --git a/src/EFCore.PG/Query/Expressions/Internal/ArrayAnyAllExpression.cs b/src/EFCore.PG/Query/Expressions/Internal/ArrayAnyAllExpression.cs index 62a79cd28..a617b4fdd 100644 --- a/src/EFCore.PG/Query/Expressions/Internal/ArrayAnyAllExpression.cs +++ b/src/EFCore.PG/Query/Expressions/Internal/ArrayAnyAllExpression.cs @@ -71,7 +71,7 @@ public class ArrayAnyAllExpression : Expression, IEquatable /// True if this instance represents: {operand} = ANY ({array})". /// - public bool IsAnyEquals => ArrayComparisonType is ArrayComparisonType.ANY && Operator is "="; + public bool IsContainsExpression => ArrayComparisonType is ArrayComparisonType.ANY && Operator is "="; /// /// Constructs a . @@ -101,7 +101,7 @@ public ArrayAnyAllExpression( protected override Expression Accept(ExpressionVisitor visitor) => visitor is NpgsqlQuerySqlGenerator npsgqlGenerator ? npsgqlGenerator.VisitArrayAnyAll(this) - : base.Accept(visitor); + : base.Accept(visitor) ?? this; /// protected override Expression VisitChildren(ExpressionVisitor visitor) diff --git a/test/EFCore.PG.FunctionalTests/Query/ArrayQueryTest.cs b/test/EFCore.PG.FunctionalTests/Query/ArrayQueryTest.cs index 045b0bbf0..3f3288b04 100644 --- a/test/EFCore.PG.FunctionalTests/Query/ArrayQueryTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/ArrayQueryTest.cs @@ -540,7 +540,7 @@ public void Array_All_Contains_List() using (var ctx = CreateContext()) { var _ = ctx.SomeEntities.Where(x => x.SomeList.All(y => x.SomeArray.Contains(y))).ToList(); - AssertContainsInSql(@"WHERE (x.""SomeArray"" @> x.""SomeList"") = TRUE"); + AssertContainsInSql(@"WHERE (x.""SomeList"" <@ x.""SomeArray"") = TRUE"); } } @@ -550,7 +550,7 @@ public void List_All_Contains_Array() using (var ctx = CreateContext()) { var _ = ctx.SomeEntities.Where(x => x.SomeArray.All(y => x.SomeList.Contains(y))).ToList(); - AssertContainsInSql(@"WHERE (x.""SomeList"" @> x.""SomeArray"") = TRUE"); + AssertContainsInSql(@"WHERE (x.""SomeArray"" <@ x.""SomeList"") = TRUE"); } } From 40be68a49ba1755d6a82004cb32d676b8869c14f Mon Sep 17 00:00:00 2001 From: Austin Drenski Date: Thu, 31 May 2018 00:52:00 -0400 Subject: [PATCH 09/16] Begins work on a composite fragment translator The plan is to use an expression fragment translator to offload some of the work currently done in `NpgsqlSqlTranslatingExpressionVisitor` and make complex pattern translations more approachable. Specifically, this makes it tractable to handle `Array.Exists` and `List.Exists` for PostgreSQL array columns. This commit includes new tests in various stages: - Passing: - List_Exists_equals_with_literal_constant - List_Exists_less_than_with_literal_constant - Failing (simple): - List_Exists_less_than_with_literal_constant - Failing (complicated): - Array_Exists_equals_with_literal_constant - Array_Exists_equals_with_column_list_element - Array_Exists_less_than_with_literal_constant The simple failure is related to sign inversion when switching from C# to SQL patterns (e.g. 'y < 1' becomes '1 = ANY (...)`. The complicated failures are due to missing rewrite support for `Array.Exists` and arrays generally. There is a built-in `ExistsToAnyRewritingExpressionVisitor` which handles lists, but it will need to be extended to support the same rewrite for arrays. Solving the above will provide exist translations for basic query shapes. But this support is fragile at best until the `NpgsqlArrayExpressionFragmentTranslator` class has a more thorough treatment for array identification. Currently, we just look for a `QuerySourceReferenceExpression` and assume its a `MainFromClause`. This could be handled better to allow for literal arrays, and possibly other set returning functions that PostgreSQL would consider valid in an array expression. --- .../NpgsqlServiceCollectionExtensions.cs | 2 +- .../Internal/NpgsqlArrayFragmentTranslator.cs | 91 +++++++++++++++++++ ...qlCompositeExpressionFragmentTranslator.cs | 36 ++++++++ .../NpgsqlSqlTranslatingExpressionVisitor.cs | 10 +- .../Internal/ArrayAnyAllExpression.cs | 24 ++++- .../Query/ArrayQueryTest.cs | 60 ++++++++++++ 6 files changed, 212 insertions(+), 11 deletions(-) create mode 100644 src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayFragmentTranslator.cs create mode 100644 src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlCompositeExpressionFragmentTranslator.cs diff --git a/src/EFCore.PG/Extensions/NpgsqlServiceCollectionExtensions.cs b/src/EFCore.PG/Extensions/NpgsqlServiceCollectionExtensions.cs index ee76be5df..b5bb74743 100644 --- a/src/EFCore.PG/Extensions/NpgsqlServiceCollectionExtensions.cs +++ b/src/EFCore.PG/Extensions/NpgsqlServiceCollectionExtensions.cs @@ -28,7 +28,6 @@ using Microsoft.EntityFrameworkCore.Query; using Microsoft.EntityFrameworkCore.Query.ExpressionTranslators; using Microsoft.EntityFrameworkCore.Query.ExpressionVisitors; -using Microsoft.EntityFrameworkCore.Query.Internal; using Microsoft.EntityFrameworkCore.Query.Sql; using Microsoft.EntityFrameworkCore.Storage; using Microsoft.EntityFrameworkCore.Update; @@ -106,6 +105,7 @@ public static IServiceCollection AddEntityFrameworkNpgsql([NotNull] this IServic .TryAdd() .TryAdd() .TryAdd() + .TryAdd() .TryAdd() .TryAdd() .TryAdd(p => p.GetService()) 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..9b06bb2bd --- /dev/null +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayFragmentTranslator.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Query.ExpressionTranslators; +using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal; +using Remotion.Linq.Clauses; +using Remotion.Linq.Clauses.Expressions; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Internal +{ + /// + /// Provides translation services for + /// and + /// as PostgreSQL array operations. + /// + public class NpgsqlArrayFragmentTranslator : IExpressionFragmentTranslator + { + /// + [CanBeNull] + public Expression Translate(Expression expression) + { + if (!(expression is SubQueryExpression subQuery)) + return null; + + if (subQuery.QueryModel.BodyClauses.Count != 1) + return null; + + if (!(subQuery.QueryModel.BodyClauses[0] is WhereClause where)) + return null; + + if (!(where.Predicate is BinaryExpression b)) + return null; + + var qsre = b.Left as QuerySourceReferenceExpression ?? b.Right as QuerySourceReferenceExpression; + if (qsre is null) + return null; + + var operand = b.Left is QuerySourceReferenceExpression ? b.Right : b.Left; + + if (qsre.ReferencedQuerySource is MainFromClause mfc && + mfc.FromExpression is Expression from && + IsArrayOrList(from.Type)) + { + // ReSharper disable once SwitchStatementMissingSomeCases + switch (b.NodeType) + { + case ExpressionType.Equal: + return new ArrayAnyAllExpression(ArrayComparisonType.ANY, "=", operand, from); + + case ExpressionType.NotEqual: + return new ArrayAnyAllExpression(ArrayComparisonType.ANY, "<>", operand, from); + + // TODO: the direction of the lt/lte/gt/gte operators depends on where the array is (left/right). + + case ExpressionType.LessThan: + return new ArrayAnyAllExpression(ArrayComparisonType.ANY, "<", operand, from); + + case ExpressionType.LessThanOrEqual: + return new ArrayAnyAllExpression(ArrayComparisonType.ANY, "<=", operand, from); + + case ExpressionType.GreaterThan: + return new ArrayAnyAllExpression(ArrayComparisonType.ANY, ">", operand, from); + + case ExpressionType.GreaterThanOrEqual: + return new ArrayAnyAllExpression(ArrayComparisonType.ANY, ">=", operand, from); + + default: + return null; + } + } + + return null; + } + + #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 && typeof(List<>) == type.GetGenericTypeDefinition(); + + #endregion + } +} diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlCompositeExpressionFragmentTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlCompositeExpressionFragmentTranslator.cs new file mode 100644 index 000000000..16438ed6a --- /dev/null +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlCompositeExpressionFragmentTranslator.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Query.ExpressionTranslators; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Internal +{ + /// + /// A composite expression fragment translator that dispatches to multiple specialized translators specific to Npgsql. + /// + public class NpgsqlCompositeExpressionFragmentTranslator : RelationalCompositeExpressionFragmentTranslator + { + /// + /// The default expression fragment translators registered by the Npgsql provider. + /// + static readonly IExpressionFragmentTranslator[] ExpressionFragmentTranslators = + { + new NpgsqlArrayFragmentTranslator() + }; + + /// + public NpgsqlCompositeExpressionFragmentTranslator( + [NotNull] RelationalCompositeExpressionFragmentTranslatorDependencies dependencies) + : base(dependencies) + { + // ReSharper disable once DoNotCallOverridableMethodsInConstructor + AddTranslators(ExpressionFragmentTranslators); + } + + /// + /// Adds additional dispatches to the translators list. + /// + /// The translators. + public new virtual void AddTranslators([NotNull] IEnumerable translators) + => base.AddTranslators(translators); + } +} diff --git a/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlSqlTranslatingExpressionVisitor.cs b/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlSqlTranslatingExpressionVisitor.cs index ee7d0c184..d1afdf08f 100644 --- a/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlSqlTranslatingExpressionVisitor.cs @@ -97,11 +97,6 @@ public NpgsqlSqlTranslatingExpressionVisitor( #region Overrides - /// - [CanBeNull] - protected override Expression VisitSubQuery(SubQueryExpression expression) - => base.VisitSubQuery(expression) ?? VisitArraySubQuery(expression); - /// protected override Expression VisitBinary(BinaryExpression expression) { @@ -124,6 +119,11 @@ protected override Expression VisitBinary(BinaryExpression expression) return base.VisitBinary(expression); } + /// + [CanBeNull] + protected override Expression VisitSubQuery(SubQueryExpression expression) + => base.VisitSubQuery(expression) ?? VisitArraySubQuery(expression); + #endregion #region ArraySubQueries diff --git a/src/EFCore.PG/Query/Expressions/Internal/ArrayAnyAllExpression.cs b/src/EFCore.PG/Query/Expressions/Internal/ArrayAnyAllExpression.cs index a617b4fdd..ffd926fba 100644 --- a/src/EFCore.PG/Query/Expressions/Internal/ArrayAnyAllExpression.cs +++ b/src/EFCore.PG/Query/Expressions/Internal/ArrayAnyAllExpression.cs @@ -26,6 +26,7 @@ using System; using System.Linq.Expressions; using JetBrains.Annotations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionVisitors; using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Sql.Internal; using Npgsql.EntityFrameworkCore.PostgreSQL.Utilities; @@ -43,24 +44,27 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal public class ArrayAnyAllExpression : Expression, IEquatable { /// - public override ExpressionType NodeType { get; } = ExpressionType.Extension; + public override ExpressionType NodeType => ExpressionType.Extension; /// - public override Type Type { get; } = typeof(bool); + public override Type Type => typeof(bool); /// /// The value to test against the . /// + [NotNull] public virtual Expression Operand { get; } /// /// The array of values or patterns to test for the . /// + [NotNull] public virtual Expression Array { get; } /// /// The operator. /// + [NotNull] public virtual string Operator { get; } /// @@ -99,9 +103,19 @@ public ArrayAnyAllExpression( /// protected override Expression Accept(ExpressionVisitor visitor) - => visitor is NpgsqlQuerySqlGenerator npsgqlGenerator - ? npsgqlGenerator.VisitArrayAnyAll(this) - : base.Accept(visitor) ?? this; + { + switch (visitor) + { + case NpgsqlQuerySqlGenerator npsgqlGenerator: + return npsgqlGenerator.VisitArrayAnyAll(this); + + case NpgsqlSqlTranslatingExpressionVisitor npgsqlVisitor: + return VisitChildren(npgsqlVisitor); + + default: + return base.Accept(visitor) ?? this; + } + } /// protected override Expression VisitChildren(ExpressionVisitor visitor) diff --git a/test/EFCore.PG.FunctionalTests/Query/ArrayQueryTest.cs b/test/EFCore.PG.FunctionalTests/Query/ArrayQueryTest.cs index 3f3288b04..9c41e7dd0 100644 --- a/test/EFCore.PG.FunctionalTests/Query/ArrayQueryTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/ArrayQueryTest.cs @@ -574,6 +574,66 @@ public void List_Any_Contains_Array() } } + [Fact] + public void Array_Exists_equals_with_literal_constant() + { + using (var ctx = CreateContext()) + { + var _ = ctx.SomeEntities.Where(x => Array.Exists(x.SomeArray, y => y == 1)).ToList(); + AssertContainsInSql(@"WHERE 1 = ANY (x.""SomeArray"")"); + } + } + + [Fact] + public void List_Exists_equals_with_literal_constant() + { + using (var ctx = CreateContext()) + { + var _ = ctx.SomeEntities.Where(x => x.SomeList.Exists(y => y == 1)).ToList(); + AssertContainsInSql(@"WHERE 1 = ANY (x.""SomeList"")"); + } + } + + [Fact] + public void Array_Exists_less_than_with_literal_constant() + { + using (var ctx = CreateContext()) + { + var _ = ctx.SomeEntities.Where(x => Array.Exists(x.SomeArray, y => y < 1)).ToList(); + AssertContainsInSql(@"WHERE 1 > ANY (x.""SomeArray"")"); + } + } + + [Fact] + public void List_Exists_less_than_with_literal_constant() + { + using (var ctx = CreateContext()) + { + var _ = ctx.SomeEntities.Where(x => x.SomeList.Exists(y => y < 1)).ToList(); + AssertContainsInSql(@"WHERE 1 > ANY (x.""SomeList"")"); + } + } + + [Fact] + public void Array_Exists_equals_with_column_list_element() + { + using (var ctx = CreateContext()) + { + var _ = ctx.SomeEntities.Where(x => Array.Exists(x.SomeArray, y => y == x.SomeList[0])).ToList(); + AssertContainsInSql(@"WHERE x.""SomeList""[1] = ANY (x.""SomeArray"")"); + } + } + + [Fact] + public void List_Exists_equals_with_column_array_element() + { + using (var ctx = CreateContext()) + { + var _ = ctx.SomeEntities.Where(x => x.SomeList.Exists(y => y == x.SomeArray[0])).ToList(); + AssertContainsInSql(@"WHERE x.""SomeArray""[1] = ANY (x.""SomeList"")"); + } + } + #if NETCOREAPP2_1 [Fact] public void Array_Append_constant() From 3bba9b96beefd0141f734e6f892d7d1f12bb0186 Mon Sep 17 00:00:00 2001 From: Austin Drenski Date: Thu, 31 May 2018 01:11:56 -0400 Subject: [PATCH 10/16] Updates the array docs - Organizing the table. - Outlining sections to explain the pattern translations. --- doc/mapping/array.md | 43 +++++++++++++++++++++++++++++++------------ 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/doc/mapping/array.md b/doc/mapping/array.md index b76be46eb..ed152e430 100644 --- a/doc/mapping/array.md +++ b/doc/mapping/array.md @@ -13,16 +13,18 @@ Npgsql maps PostgreSQL arrays to generic `T[]` and `List` types: public class Post { public int Id { get; set; } - public int[] SomeArray { get; set; } - public List SomeList { get; set; } + public string[] SomeArray { get; set; } + public List SomeList { get; set; } } ``` -The provider will create `int[]` columns for the above two properties, and will properly detect changes in them—if you load an array and change one of its elements, calling `SaveChanges()` will automatically update the row in the database accordingly. +The provider will create `text[]` columns for the above two properties, and will properly detect changes in them—if you load an array and change one of its elements, calling `SaveChanges()` will automatically update the row in the database accordingly. # Operation translation -The provider can also translate CLR array operations to the corresponding SQL operation; this allows you to efficiently work with arrays by evaluating operations in the database and avoids pulling all the data. The following table lists the array operations that currently get translated. If you run into a missing operation, please open an issue. +The provider translates many operations on `T[]` and `List` to corresponding SQL operations. This allows arrays to be worked with efficiently by evaluating operations in the database. + +The following table lists the operations that are currently translated. If you run into a missing operation, please open an issue. | C# expression | SQL generated by Npgsql | |------------------------------------------------------------------|-------------------------| @@ -37,15 +39,32 @@ The provider can also translate CLR array operations to the corresponding SQL op | `.Where(x => x.SomeArray == x.SomeList)` | [`WHERE x."SomeArray" = x."SomeList"`](https://www.postgresql.org/docs/current/static/functions-array.html#ARRAY-FUNCTIONS-TABLE) | `.Where(x => x.SomeArray.Equals(x.SomeList))` | [`WHERE x."SomeArray" = x."SomeList"`](https://www.postgresql.org/docs/current/static/functions-array.html#ARRAY-FUNCTIONS-TABLE) | `.Where(x => x.SomeArray.SequenceEquals(x.SomeList))` | [`WHERE x."SomeArray" = x."SomeList"`](https://www.postgresql.org/docs/current/static/functions-array.html#ARRAY-FUNCTIONS-TABLE) -| `.Where(x => x.SomeArray.Contains(0))` | [`WHERE 0 = ANY (x."SomeArray")`](https://www.postgresql.org/docs/current/static/functions-array.html#ARRAY-FUNCTIONS-TABLE) -| `.Where(x => x.SomeList.Contains(0))` | [`WHERE 0 = ANY (x."SomeList")`](https://www.postgresql.org/docs/current/static/functions-array.html#ARRAY-FUNCTIONS-TABLE) -| `.Select(x => x.SomeArray.Append(0))` | [`SELECT x."SomeArray" \|\| 0`](https://www.postgresql.org/docs/current/static/functions-array.html#ARRAY-FUNCTIONS-TABLE) -| `.Select(x => x.SomeList.Append(0))` | [`SELECT x."SomeList" \|\| 0`](https://www.postgresql.org/docs/current/static/functions-array.html#ARRAY-FUNCTIONS-TABLE) -| `.Select(x => x.SomeArray.Prepend(0))` | [`SELECT 0 \|\| x."SomeArray"`](https://www.postgresql.org/docs/current/static/functions-array.html#ARRAY-FUNCTIONS-TABLE) -| `.Select(x => x.SomeList.Prepend(0))` | [`SELECT 0 \|\| x."SomeList"`](https://www.postgresql.org/docs/current/static/functions-array.html#ARRAY-FUNCTIONS-TABLE) +| `.Where(x => x.SomeArray.Contains("foo"))` | [`WHERE 'foo' = ANY (x."SomeArray")`](https://www.postgresql.org/docs/current/static/functions-array.html#ARRAY-FUNCTIONS-TABLE) +| `.Where(x => x.SomeList.Contains("foo"))` | [`WHERE 'foo' = ANY (x."SomeList")`](https://www.postgresql.org/docs/current/static/functions-array.html#ARRAY-FUNCTIONS-TABLE) +| `.Select(x => x.SomeArray.Append("foo"))` | [`SELECT x."SomeArray" \|\| 'foo'`](https://www.postgresql.org/docs/current/static/functions-array.html#ARRAY-FUNCTIONS-TABLE) +| `.Select(x => x.SomeList.Append("foo"))` | [`SELECT x."SomeList" \|\| 'foo'`](https://www.postgresql.org/docs/current/static/functions-array.html#ARRAY-FUNCTIONS-TABLE) +| `.Select(x => x.SomeArray.Prepend("foo"))` | [`SELECT 'foo' \|\| x."SomeArray"`](https://www.postgresql.org/docs/current/static/functions-array.html#ARRAY-FUNCTIONS-TABLE) +| `.Select(x => x.SomeList.Prepend("foo"))` | [`SELECT 'foo' \|\| x."SomeList"`](https://www.postgresql.org/docs/current/static/functions-array.html#ARRAY-FUNCTIONS-TABLE) | `.Select(x => x.SomeArray.Concat(x.SomeList))` | [`SELECT x."SomeArray" \|\| x."SomeList"`](https://www.postgresql.org/docs/current/static/functions-array.html#ARRAY-FUNCTIONS-TABLE) | `.Select(x => x.SomeList.Concat(x.SomeArray))` | [`SELECT x."SomeList" \|\| x."SomeArray"`](https://www.postgresql.org/docs/current/static/functions-array.html#ARRAY-FUNCTIONS-TABLE) | `.Select(x => EF.Functions.ArrayToString(x.SomeArray, ","))` | [`SELECT array_to_string(x."SomeArray", ',')`](https://www.postgresql.org/docs/current/static/functions-array.html#ARRAY-FUNCTIONS-TABLE) | `.Select(x => EF.Functions.ArrayToString(x.SomeList, ",", "*"))` | [`SELECT array_to_string(x."SomeList", ',', '*')`](https://www.postgresql.org/docs/current/static/functions-array.html#ARRAY-FUNCTIONS-TABLE) -| `.Select(x => Array.IndexOf(x.SomeArray, 0))` | [`SELECT COALESCE(array_position(x."SomeArray", 1), -1)`](https://www.postgresql.org/docs/current/static/functions-array.html#ARRAY-FUNCTIONS-TABLE) -| `.Select(x => x.SomeList.IndexOf(0))` | [`SELECT COALESCE(array_position(x."SomeList", 1), -1)`](https://www.postgresql.org/docs/current/static/functions-array.html#ARRAY-FUNCTIONS-TABLE) \ No newline at end of file +| `.Select(x => Array.IndexOf(x.SomeArray, "foo"))` | [`SELECT COALESCE(array_position(x."SomeArray", 'foo'), -1)`](https://www.postgresql.org/docs/current/static/functions-array.html#ARRAY-FUNCTIONS-TABLE) +| `.Select(x => x.SomeList.IndexOf("foo"))` | [`SELECT COALESCE(array_position(x."SomeList", 'foo'), -1)`](https://www.postgresql.org/docs/current/static/functions-array.html#ARRAY-FUNCTIONS-TABLE) + +# Pattern translation + +The provider has special translations for certain patterns of operations. These pattern-based translations are more susceptible to client-evaluation than standard translations. + +The following sections describe the patterns that are currently translated. If you find that one of these patterns is being evaluated on the client, please open an issue. + + +## LIKE ANY + +## LIKE ALL + +## EXISTS + +## @> + +## && \ No newline at end of file From 9a97564128b1dfe21d80926eba6a2fc946ce04b8 Mon Sep 17 00:00:00 2001 From: Austin Drenski Date: Thu, 31 May 2018 11:31:32 -0400 Subject: [PATCH 11/16] Improves identification for exists fragments --- .../Internal/NpgsqlArrayFragmentTranslator.cs | 171 ++++++++-- .../Internal/NpgsqlArrayTranslator.cs | 8 +- ...qlCompositeExpressionFragmentTranslator.cs | 27 +- .../NpgsqlSqlTranslatingExpressionVisitor.cs | 7 +- .../Internal/ArrayAnyAllExpression.cs | 13 +- .../Sql/Internal/NpgsqlQuerySqlGenerator.cs | 3 - .../Query/ArrayQueryTest.cs | 295 +++++++++++++----- 7 files changed, 389 insertions(+), 135 deletions(-) diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayFragmentTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayFragmentTranslator.cs index 9b06bb2bd..3e2e4953b 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayFragmentTranslator.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayFragmentTranslator.cs @@ -1,4 +1,29 @@ -using System; +#region License + +// The PostgreSQL License +// +// Copyright (C) 2016 The Npgsql Development Team +// +// Permission to use, copy, modify, and distribute this software and its +// documentation for any purpose, without fee, and without a written +// agreement is hereby granted, provided that the above copyright notice +// and this paragraph and the following two paragraphs appear in all copies. +// +// IN NO EVENT SHALL THE NPGSQL DEVELOPMENT TEAM BE LIABLE TO ANY PARTY +// FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, +// INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS +// DOCUMENTATION, EVEN IF THE NPGSQL DEVELOPMENT TEAM HAS BEEN ADVISED OF +// THE POSSIBILITY OF SUCH DAMAGE. +// +// THE NPGSQL DEVELOPMENT TEAM SPECIFICALLY DISCLAIMS ANY WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS +// ON AN "AS IS" BASIS, AND THE NPGSQL DEVELOPMENT TEAM HAS NO OBLIGATIONS +// TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + +#endregion + +using System; using System.Collections.Generic; using System.Linq.Expressions; using JetBrains.Annotations; @@ -6,6 +31,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal; using Remotion.Linq.Clauses; using Remotion.Linq.Clauses.Expressions; +using Remotion.Linq.Clauses.ResultOperators; namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Internal { @@ -23,6 +49,9 @@ public Expression Translate(Expression expression) if (!(expression is SubQueryExpression subQuery)) return null; + if (ContainsResult(subQuery) is Expression contains) + return contains; + if (subQuery.QueryModel.BodyClauses.Count != 1) return null; @@ -32,55 +61,135 @@ public Expression Translate(Expression expression) if (!(where.Predicate is BinaryExpression b)) return null; - var qsre = b.Left as QuerySourceReferenceExpression ?? b.Right as QuerySourceReferenceExpression; - if (qsre is null) + if (!TryFindArray(b, out Expression from, out ArrayPosition position) || from is null) return null; - var operand = b.Left is QuerySourceReferenceExpression ? b.Right : b.Left; + var operand = position is ArrayPosition.Left ? b.Right : b.Left; - if (qsre.ReferencedQuerySource is MainFromClause mfc && - mfc.FromExpression is Expression from && - IsArrayOrList(from.Type)) - { - // ReSharper disable once SwitchStatementMissingSomeCases - switch (b.NodeType) - { - case ExpressionType.Equal: - return new ArrayAnyAllExpression(ArrayComparisonType.ANY, "=", operand, from); + // In PostgreSQL, the array is on the right. Flip the sign if needed. + bool flip = position is ArrayPosition.Left; - case ExpressionType.NotEqual: - return new ArrayAnyAllExpression(ArrayComparisonType.ANY, "<>", operand, from); + // ReSharper disable once SwitchStatementMissingSomeCases + switch (b.NodeType) + { + case ExpressionType.Equal: + return new ArrayAnyAllExpression(ArrayComparisonType.ANY, "=", operand, from); - // TODO: the direction of the lt/lte/gt/gte operators depends on where the array is (left/right). + case ExpressionType.NotEqual: + return new ArrayAnyAllExpression(ArrayComparisonType.ANY, "<>", operand, from); - case ExpressionType.LessThan: - return new ArrayAnyAllExpression(ArrayComparisonType.ANY, "<", operand, from); + case ExpressionType.LessThan: + return new ArrayAnyAllExpression(ArrayComparisonType.ANY, flip ? ">" : "<", operand, from); - case ExpressionType.LessThanOrEqual: - return new ArrayAnyAllExpression(ArrayComparisonType.ANY, "<=", operand, from); + case ExpressionType.LessThanOrEqual: + return new ArrayAnyAllExpression(ArrayComparisonType.ANY, flip ? ">=" : "<=", operand, from); - case ExpressionType.GreaterThan: - return new ArrayAnyAllExpression(ArrayComparisonType.ANY, ">", operand, from); + case ExpressionType.GreaterThan: + return new ArrayAnyAllExpression(ArrayComparisonType.ANY, flip ? "<" : ">", operand, from); - case ExpressionType.GreaterThanOrEqual: - return new ArrayAnyAllExpression(ArrayComparisonType.ANY, ">=", operand, from); + case ExpressionType.GreaterThanOrEqual: + return new ArrayAnyAllExpression(ArrayComparisonType.ANY, flip ? "<=" : ">=", operand, from); - default: - return null; - } + default: + return null; } + } + + #region SubQueries + + [CanBeNull] + static Expression ContainsResult(SubQueryExpression expression) + { + var model = expression.QueryModel; + if (model.BodyClauses.Count != 0) + return null; + + if (model.ResultOperators.Count != 1) + return null; + + if (!(model.ResultOperators[0] is ContainsResultOperator contains)) + return null; - return null; + return new ArrayAnyAllExpression(ArrayComparisonType.ANY, "=", contains.Item, model.MainFromClause.FromExpression); } + #endregion + #region Helpers + /// + /// Try to return the array expression and its position in the . + /// + /// The expression to visit. + /// The array expression, if found. + /// The postion of the array. + /// + /// True if the array was found; otherwise, false. + /// + static bool TryFindArray([NotNull] BinaryExpression binaryExpression, [CanBeNull] out Expression array, out ArrayPosition position) + { + if (TryFindArray(binaryExpression.Left, out array)) + { + position = ArrayPosition.Left; + return true; + } + + if (TryFindArray(binaryExpression.Right, out array)) + { + position = ArrayPosition.Right; + return true; + } + + position = ArrayPosition.None; + return false; + } + + /// + /// Try to return the array expression. + /// + /// The expression to visit. + /// The array expression, if found. + /// + /// True if the array was found; otherwise, false. + /// + static bool TryFindArray([NotNull] Expression expression, [CanBeNull] out Expression array) + { + switch (expression) + { + // Is one side a qsre pointing to an array? + case QuerySourceReferenceExpression qsre + when qsre.ReferencedQuerySource is MainFromClause mfc && + mfc.FromExpression is Expression from && + IsArrayOrList(from.Type): + array = from; + return true; + + // Is the expression a parameter array? + case ParameterExpression param + when IsArrayOrList(param.Type): + array = param; + return true; + + default: + array = null; + return false; + } + } + + /// + /// Describes the position of an array in a . + /// + private enum ArrayPosition + { + None, + Left, + Right + } + /// /// Tests if the type is an array or a . /// - /// - /// The type to test. - /// + /// The type to test. /// /// True if is an array or a ; otherwise, false. /// diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayTranslator.cs index be6a585fc..f7f1a8ea9 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayTranslator.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayTranslator.cs @@ -132,9 +132,7 @@ public Expression Translate(MethodCallExpression expression) /// /// Tests if the instance or argument types are supported. /// - /// - /// The to test. - /// + /// The to test. /// /// True if the instance or argument types are supported; otherwise, false. /// @@ -174,9 +172,7 @@ static bool IsTypeSupported([NotNull] MethodCallExpression expression) /// /// Tests if the type is an array or a . /// - /// - /// The type to test. - /// + /// The type to test. /// /// True if is an array or a ; otherwise, false. /// diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlCompositeExpressionFragmentTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlCompositeExpressionFragmentTranslator.cs index 16438ed6a..42fc81838 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlCompositeExpressionFragmentTranslator.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlCompositeExpressionFragmentTranslator.cs @@ -1,4 +1,29 @@ -using System.Collections.Generic; +#region License + +// The PostgreSQL License +// +// Copyright (C) 2016 The Npgsql Development Team +// +// Permission to use, copy, modify, and distribute this software and its +// documentation for any purpose, without fee, and without a written +// agreement is hereby granted, provided that the above copyright notice +// and this paragraph and the following two paragraphs appear in all copies. +// +// IN NO EVENT SHALL THE NPGSQL DEVELOPMENT TEAM BE LIABLE TO ANY PARTY +// FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, +// INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS +// DOCUMENTATION, EVEN IF THE NPGSQL DEVELOPMENT TEAM HAS BEEN ADVISED OF +// THE POSSIBILITY OF SUCH DAMAGE. +// +// THE NPGSQL DEVELOPMENT TEAM SPECIFICALLY DISCLAIMS ANY WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS +// ON AN "AS IS" BASIS, AND THE NPGSQL DEVELOPMENT TEAM HAS NO OBLIGATIONS +// TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + +#endregion + +using System.Collections.Generic; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Query.ExpressionTranslators; diff --git a/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlSqlTranslatingExpressionVisitor.cs b/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlSqlTranslatingExpressionVisitor.cs index d1afdf08f..5a38b2704 100644 --- a/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlSqlTranslatingExpressionVisitor.cs @@ -168,9 +168,6 @@ protected virtual Expression VisitArraySubQuery([NotNull] SubQueryExpression exp case ConcatResultOperator concatResultOperator: return VisitArrayConcat(array, concatResultOperator); - case ContainsResultOperator contains: - return new ArrayAnyAllExpression(ArrayComparisonType.ANY, "=", Visit(contains.Item) ?? contains.Item, array); - case CountResultOperator countResultOperator: return VisitArrayCount(array, countResultOperator); @@ -313,9 +310,7 @@ protected virtual Expression VisitArrayContains([NotNull] Expression array, [Not /// /// Tests if the type is an array or a . /// - /// - /// The type to test. - /// + /// The type to test. /// /// True if is an array or a ; otherwise, false. /// diff --git a/src/EFCore.PG/Query/Expressions/Internal/ArrayAnyAllExpression.cs b/src/EFCore.PG/Query/Expressions/Internal/ArrayAnyAllExpression.cs index ffd926fba..3bdac9c92 100644 --- a/src/EFCore.PG/Query/Expressions/Internal/ArrayAnyAllExpression.cs +++ b/src/EFCore.PG/Query/Expressions/Internal/ArrayAnyAllExpression.cs @@ -120,16 +120,13 @@ protected override Expression Accept(ExpressionVisitor visitor) /// protected override Expression VisitChildren(ExpressionVisitor visitor) { - if (!(visitor.Visit(Operand) is Expression operand)) - throw new ArgumentException($"The {nameof(operand)} of a {nameof(ArrayAnyAllExpression)} cannot be null."); - - if (!(visitor.Visit(Array) is Expression collection)) - throw new ArgumentException($"The {nameof(collection)} of a {nameof(ArrayAnyAllExpression)} cannot be null."); + var operand = visitor.Visit(Operand) ?? Operand; + var array = visitor.Visit(Array) ?? Array; return - operand == Operand && collection == Array - ? this - : new ArrayAnyAllExpression(ArrayComparisonType, Operator, operand, collection); + operand != Operand || array != Array + ? new ArrayAnyAllExpression(ArrayComparisonType, Operator, operand, array) + : this; } /// diff --git a/src/EFCore.PG/Query/Sql/Internal/NpgsqlQuerySqlGenerator.cs b/src/EFCore.PG/Query/Sql/Internal/NpgsqlQuerySqlGenerator.cs index 417b3d899..c30a5f5ee 100644 --- a/src/EFCore.PG/Query/Sql/Internal/NpgsqlQuerySqlGenerator.cs +++ b/src/EFCore.PG/Query/Sql/Internal/NpgsqlQuerySqlGenerator.cs @@ -188,8 +188,6 @@ protected virtual void VisitArrayIndex([NotNull] BinaryExpression expression) /// protected override Expression VisitIndex(IndexExpression expression) { - // TODO: does this need wrapped? the array indexer is wrapped, but not by our code? - Sql.Append('('); Visit(expression.Object); for (int i = 0; i < expression.Arguments.Count; i++) { @@ -197,7 +195,6 @@ protected override Expression VisitIndex(IndexExpression expression) Visit(GenerateOneBasedIndexExpression(expression.Arguments[i])); Sql.Append(']'); } - Sql.Append(')'); return expression; } diff --git a/test/EFCore.PG.FunctionalTests/Query/ArrayQueryTest.cs b/test/EFCore.PG.FunctionalTests/Query/ArrayQueryTest.cs index 9c41e7dd0..8135ad0d7 100644 --- a/test/EFCore.PG.FunctionalTests/Query/ArrayQueryTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/ArrayQueryTest.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.ComponentModel.DataAnnotations.Schema; using System.Linq; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.TestUtilities; @@ -15,6 +14,8 @@ public class ArrayQueryTest : IClassFixture { #region ArrayTests + #region Roundtrip + [Fact] public void Array_Roundtrip() { @@ -37,6 +38,10 @@ public void List_Roundtrip() } } + #endregion + + #region Indexers + [Fact] public void Array_Index_with_constant() { @@ -55,7 +60,7 @@ public void List_Index_with_constant() { var actual = ctx.SomeEntities.Where(e => e.SomeList[0] == 3).ToList(); Assert.Equal(1, actual.Count); - AssertContainsInSql(@"WHERE (e.""SomeList""[1]) = 3"); + AssertContainsInSql(@"WHERE e.""SomeList""[1] = 3"); } } @@ -66,7 +71,7 @@ public void Array_ElementAt_with_constant() { var actual = ctx.SomeEntities.Where(e => e.SomeArray.ElementAt(0) == 3).ToList(); Assert.Equal(1, actual.Count); - AssertContainsInSql(@"WHERE (e.""SomeArray""[1]) = 3"); + AssertContainsInSql(@"WHERE e.""SomeArray""[1] = 3"); } } @@ -77,7 +82,7 @@ public void List_ElementAt_with_constant() { var actual = ctx.SomeEntities.Where(e => e.SomeList.ElementAt(0) == 3).ToList(); Assert.Equal(1, actual.Count); - AssertContainsInSql(@"WHERE (e.""SomeList""[1]) = 3"); + AssertContainsInSql(@"WHERE e.""SomeList""[1] = 3"); } } @@ -103,7 +108,7 @@ public void List_Index_with_non_constant() var x = 0; var actual = ctx.SomeEntities.Where(e => e.SomeList[x] == 3).ToList(); Assert.Equal(1, actual.Count); - AssertContainsInSql(@"WHERE (e.""SomeList""[@__x_0 + 1]) = 3"); + AssertContainsInSql(@"WHERE e.""SomeList""[@__x_0 + 1] = 3"); } } @@ -116,7 +121,7 @@ public void Array_ElementAt_with_non_constant() var x = 0; var actual = ctx.SomeEntities.Where(e => e.SomeArray.ElementAt(x) == 3).ToList(); Assert.Equal(1, actual.Count); - AssertContainsInSql(@"WHERE (e.""SomeArray""[@__x_0 + 1]) = 3"); + AssertContainsInSql(@"WHERE e.""SomeArray""[@__x_0 + 1] = 3"); } } @@ -129,7 +134,7 @@ public void List_IndexElementAt_with_non_constant() var x = 0; var actual = ctx.SomeEntities.Where(e => e.SomeList.ElementAt(x) == 3).ToList(); Assert.Equal(1, actual.Count); - AssertContainsInSql(@"WHERE (e.""SomeList""[@__x_0 + 1]) = 3"); + AssertContainsInSql(@"WHERE e.""SomeList""[@__x_0 + 1] = 3"); } } @@ -155,27 +160,9 @@ public void Array_Index_multidimensional() } } - [Fact(Skip = "Not yet supported")] - public void Array_Index_jagged() - { - using (var ctx = CreateContext()) - { - // Operations on multidimensional arrays aren't mapped to SQL yet - var actual = ctx.SomeEntities.Where(e => e.SomeArrayOfArrays[0][0] == 5).ToList(); - Assert.Equal(1, actual.Count); - } - } + #endregion - [Fact(Skip = "Not yet supported")] - public void List_Index_jagged() - { - using (var ctx = CreateContext()) - { - // Operations on multidimensional arrays aren't mapped to SQL yet - var actual = ctx.SomeEntities.Where(e => e.SomeListOfLists[0][0] == 5).ToList(); - Assert.Equal(1, actual.Count); - } - } + #region Equality [Fact] public void Array_Equal_with_parameter() @@ -247,6 +234,10 @@ public void List_SequenceEqual_with_literal() } } + #endregion + + #region Containment + [Fact] public void Array_Contains_with_literal() { @@ -317,6 +308,50 @@ public void List_Contains_with_column() } } + [Fact] + public void Array_All_Contains_List() + { + using (var ctx = CreateContext()) + { + var _ = ctx.SomeEntities.Where(x => x.SomeList.All(y => x.SomeArray.Contains(y))).ToList(); + AssertContainsInSql(@"WHERE (x.""SomeList"" <@ x.""SomeArray"") = TRUE"); + } + } + + [Fact] + public void List_All_Contains_Array() + { + using (var ctx = CreateContext()) + { + var _ = ctx.SomeEntities.Where(x => x.SomeArray.All(y => x.SomeList.Contains(y))).ToList(); + AssertContainsInSql(@"WHERE (x.""SomeArray"" <@ x.""SomeList"") = TRUE"); + } + } + + [Fact] + public void Array_Any_Contains_List() + { + using (var ctx = CreateContext()) + { + var _ = ctx.SomeEntities.Where(x => x.SomeArray.Any(y => x.SomeList.Contains(y))).ToList(); + AssertContainsInSql(@"WHERE (x.""SomeArray"" && x.""SomeList"") = TRUE"); + } + } + + [Fact] + public void List_Any_Contains_Array() + { + using (var ctx = CreateContext()) + { + var _ = ctx.SomeEntities.Where(x => x.SomeList.Any(y => x.SomeArray.Contains(y))).ToList(); + AssertContainsInSql(@"WHERE (x.""SomeList"" && x.""SomeArray"") = TRUE"); + } + } + + #endregion + + #region Count + [Fact] public void Array_Length() { @@ -406,6 +441,10 @@ public void List_Length_on_literal_not_translated() } } + #endregion + + #region Concatenation + [Fact] public void Array_Concat_with_array_column() { @@ -446,6 +485,54 @@ public void List_Concat_with_array_column() } } +// .NET 4.6.1 doesn't include the Enumerable.Append and Enumerable.Prepend functions... +#if !NET461 + [Fact] + public void Array_Append_constant() + { + using (var ctx = CreateContext()) + { + var _ = ctx.SomeEntities.Select(e => e.SomeArray.Append(0)).ToList(); + AssertContainsInSql(@"SELECT (e.""SomeArray"" || 0)"); + } + } + + [Fact] + public void List_Append_constant() + { + using (var ctx = CreateContext()) + { + var _ = ctx.SomeEntities.Select(e => e.SomeList.Append(0)).ToList(); + AssertContainsInSql(@"SELECT (e.""SomeList"" || 0)"); + } + } + + [Fact] + public void Array_Prepend_constant() + { + using (var ctx = CreateContext()) + { + var _ = ctx.SomeEntities.Select(e => e.SomeArray.Prepend(0)).ToList(); + AssertContainsInSql(@"SELECT (0 || e.""SomeArray"")"); + } + } + + [Fact] + public void List_Prepend_constant() + { + using (var ctx = CreateContext()) + { + var _ = ctx.SomeEntities.Select(e => e.SomeList.Prepend(0)).ToList(); + AssertContainsInSql(@"SELECT (0 || e.""SomeList"")"); + } + } + +#endif + + #endregion + + #region IndexOf + [Fact] public void Array_IndexOf_constant() { @@ -466,6 +553,10 @@ public void List_IndexOf_constant() } } + #endregion + + #region StringConversion + [Fact] public void Array_ArrayToString() { @@ -534,63 +625,119 @@ public void List_StringToList() } } + #endregion + + #region Exists + [Fact] - public void Array_All_Contains_List() + public void Array_Exists_equals_with_literal_constant() { using (var ctx = CreateContext()) { - var _ = ctx.SomeEntities.Where(x => x.SomeList.All(y => x.SomeArray.Contains(y))).ToList(); - AssertContainsInSql(@"WHERE (x.""SomeList"" <@ x.""SomeArray"") = TRUE"); + var _ = ctx.SomeEntities.Where(x => Array.Exists(x.SomeArray, y => y == 1)).ToList(); + AssertContainsInSql(@"WHERE 1 = ANY (x.""SomeArray"") = TRUE"); } } [Fact] - public void List_All_Contains_Array() + public void List_Exists_equals_with_literal_constant() { using (var ctx = CreateContext()) { - var _ = ctx.SomeEntities.Where(x => x.SomeArray.All(y => x.SomeList.Contains(y))).ToList(); - AssertContainsInSql(@"WHERE (x.""SomeArray"" <@ x.""SomeList"") = TRUE"); + var _ = ctx.SomeEntities.Where(x => x.SomeList.Exists(y => y == 1)).ToList(); + AssertContainsInSql(@"WHERE 1 = ANY (x.""SomeList"") = TRUE"); } } [Fact] - public void Array_Any_Contains_List() + public void Array_Exists_not_equal_with_literal_constant() { using (var ctx = CreateContext()) { - var _ = ctx.SomeEntities.Where(x => x.SomeArray.Any(y => x.SomeList.Contains(y))).ToList(); - AssertContainsInSql(@"WHERE (x.""SomeArray"" && x.""SomeList"") = TRUE"); + var _ = ctx.SomeEntities.Where(x => Array.Exists(x.SomeArray, y => y != 1)).ToList(); + AssertContainsInSql(@"WHERE 1 <> ANY (x.""SomeArray"") = TRUE"); } } [Fact] - public void List_Any_Contains_Array() + public void List_Exists_not_equal_with_literal_constant() { using (var ctx = CreateContext()) { - var _ = ctx.SomeEntities.Where(x => x.SomeList.Any(y => x.SomeArray.Contains(y))).ToList(); - AssertContainsInSql(@"WHERE (x.""SomeList"" && x.""SomeArray"") = TRUE"); + var _ = ctx.SomeEntities.Where(x => x.SomeList.Exists(y => y != 1)).ToList(); + AssertContainsInSql(@"WHERE 1 <> ANY (x.""SomeList"") = TRUE"); } } [Fact] - public void Array_Exists_equals_with_literal_constant() + public void Array_Exists_equals_with_parameter_array_and_column_array_element() { using (var ctx = CreateContext()) { - var _ = ctx.SomeEntities.Where(x => Array.Exists(x.SomeArray, y => y == 1)).ToList(); - AssertContainsInSql(@"WHERE 1 = ANY (x.""SomeArray"")"); + var array = new[] { 0, 1, 2 }; + var _ = ctx.SomeEntities.Where(x => Array.Exists(array, y => y == x.SomeArray[0])).ToList(); + AssertContainsInSql(@"WHERE x.""SomeArray""[1] = ANY (@__array_0) = TRUE"); } } [Fact] - public void List_Exists_equals_with_literal_constant() + public void List_Exists_equals_with_parameter_array_and_column_list_element() { using (var ctx = CreateContext()) { - var _ = ctx.SomeEntities.Where(x => x.SomeList.Exists(y => y == 1)).ToList(); - AssertContainsInSql(@"WHERE 1 = ANY (x.""SomeList"")"); + var list = new List { 0, 1, 2 }; + var _ = ctx.SomeEntities.Where(x => list.Exists(y => y == x.SomeList[0])).ToList(); + + // What we don't want: + AssertDoesNotContainInSql(@"WHERE (x.""SomeList""[1]) IN (0, 1, 2)"); + // What we do want: + AssertContainsInSql(@"WHERE x.""SomeList""[1] = ANY (@__list_0) = TRUE"); + } + } + + [Fact] + public void Array_Exists_equals_with_parameter_array_and_column_array_element_flipped() + { + using (var ctx = CreateContext()) + { + var array = new[] { 0, 1, 2 }; + var _ = ctx.SomeEntities.Where(x => Array.Exists(array, y => x.SomeArray[0] == y)).ToList(); + AssertContainsInSql(@"WHERE x.""SomeArray""[1] = ANY (@__array_0) = TRUE"); + } + } + + [Fact] + public void List_Exists_equals_with_parameter_array_and_column_list_element_flipped() + { + using (var ctx = CreateContext()) + { + var list = new List { 0, 1, 2 }; + var _ = ctx.SomeEntities.Where(x => list.Exists(y => x.SomeList[0] == y)).ToList(); + + // What we don't want: + AssertDoesNotContainInSql(@"WHERE (x.""SomeList""[1]) IN (0, 1, 2)"); + // What we do want: + AssertContainsInSql(@"WHERE x.""SomeList""[1] = ANY (@__list_0) = TRUE"); + } + } + + [Fact] + public void Array_Exists_equals_with_literal_constant_flipped() + { + using (var ctx = CreateContext()) + { + var _ = ctx.SomeEntities.Where(x => Array.Exists(x.SomeArray, y => 1 == y)).ToList(); + AssertContainsInSql(@"WHERE 1 = ANY (x.""SomeArray"") = TRUE"); + } + } + + [Fact] + public void List_Exists_equals_with_literal_constant_flipped() + { + using (var ctx = CreateContext()) + { + var _ = ctx.SomeEntities.Where(x => x.SomeList.Exists(y => 1 == y)).ToList(); + AssertContainsInSql(@"WHERE 1 = ANY (x.""SomeList"") = TRUE"); } } @@ -600,7 +747,7 @@ public void Array_Exists_less_than_with_literal_constant() using (var ctx = CreateContext()) { var _ = ctx.SomeEntities.Where(x => Array.Exists(x.SomeArray, y => y < 1)).ToList(); - AssertContainsInSql(@"WHERE 1 > ANY (x.""SomeArray"")"); + AssertContainsInSql(@"WHERE 1 > ANY (x.""SomeArray"") = TRUE"); } } @@ -610,72 +757,71 @@ public void List_Exists_less_than_with_literal_constant() using (var ctx = CreateContext()) { var _ = ctx.SomeEntities.Where(x => x.SomeList.Exists(y => y < 1)).ToList(); - AssertContainsInSql(@"WHERE 1 > ANY (x.""SomeList"")"); + AssertContainsInSql(@"WHERE 1 > ANY (x.""SomeList"") = TRUE"); } } [Fact] - public void Array_Exists_equals_with_column_list_element() + public void Array_Exists_less_than_with_literal_constant_flipped() { using (var ctx = CreateContext()) { - var _ = ctx.SomeEntities.Where(x => Array.Exists(x.SomeArray, y => y == x.SomeList[0])).ToList(); - AssertContainsInSql(@"WHERE x.""SomeList""[1] = ANY (x.""SomeArray"")"); + var _ = ctx.SomeEntities.Where(x => Array.Exists(x.SomeArray, y => 1 > y)).ToList(); + AssertContainsInSql(@"WHERE 1 > ANY (x.""SomeArray"") = TRUE"); } } [Fact] - public void List_Exists_equals_with_column_array_element() + public void List_Exists_less_than_with_literal_constant_flipped() { using (var ctx = CreateContext()) { - var _ = ctx.SomeEntities.Where(x => x.SomeList.Exists(y => y == x.SomeArray[0])).ToList(); - AssertContainsInSql(@"WHERE x.""SomeArray""[1] = ANY (x.""SomeList"")"); + var _ = ctx.SomeEntities.Where(x => x.SomeList.Exists(y => 1 > y)).ToList(); + AssertContainsInSql(@"WHERE 1 > ANY (x.""SomeList"") = TRUE"); } } -#if NETCOREAPP2_1 [Fact] - public void Array_Append_constant() + public void Array_Exists_equals_with_column_list_element() { using (var ctx = CreateContext()) { - var _ = ctx.SomeEntities.Select(e => e.SomeArray.Append(0)).ToList(); - AssertContainsInSql(@"SELECT (e.""SomeArray"" || 0)"); + var _ = ctx.SomeEntities.Where(x => Array.Exists(x.SomeArray, y => y == x.SomeList[0])).ToList(); + AssertContainsInSql(@"WHERE x.""SomeList""[1] = ANY (x.""SomeArray"") = TRUE"); } } [Fact] - public void List_Append_constant() + public void List_Exists_equals_with_column_array_element() { using (var ctx = CreateContext()) { - var _ = ctx.SomeEntities.Select(e => e.SomeList.Append(0)).ToList(); - AssertContainsInSql(@"SELECT (e.""SomeList"" || 0)"); + var _ = ctx.SomeEntities.Where(x => x.SomeList.Exists(y => y == x.SomeArray[0])).ToList(); + AssertContainsInSql(@"WHERE x.""SomeArray""[1] = ANY (x.""SomeList"") = TRUE"); } } [Fact] - public void Array_Prepend_constant() + public void Array_Exists_equals_with_column_list_element_flipped() { using (var ctx = CreateContext()) { - var _ = ctx.SomeEntities.Select(e => e.SomeArray.Prepend(0)).ToList(); - AssertContainsInSql(@"SELECT (0 || e.""SomeArray"")"); + var _ = ctx.SomeEntities.Where(x => Array.Exists(x.SomeArray, y => x.SomeList[0] == y)).ToList(); + AssertContainsInSql(@"WHERE x.""SomeList""[1] = ANY (x.""SomeArray"") = TRUE"); } } [Fact] - public void List_Prepend_constant() + public void List_Exists_equals_with_column_array_element_flipped() { using (var ctx = CreateContext()) { - var _ = ctx.SomeEntities.Select(e => e.SomeList.Prepend(0)).ToList(); - AssertContainsInSql(@"SELECT (0 || e.""SomeList"")"); + var _ = ctx.SomeEntities.Where(x => x.SomeList.Exists(y => x.SomeArray[0] == y)).ToList(); + AssertContainsInSql(@"WHERE x.""SomeArray""[1] = ANY (x.""SomeList"") = TRUE"); } } -#endif + #endregion #endregion @@ -741,13 +887,6 @@ public class SomeArrayEntity public int[] SomeArray { get; set; } public List SomeList { get; set; } public int[,] SomeMatrix { get; set; } - - [NotMapped] - public int[][] SomeArrayOfArrays { get; set; } - - [NotMapped] - public List> SomeListOfLists { get; set; } - public byte[] SomeBytea { get; set; } } @@ -780,9 +919,7 @@ public ArrayFixture() SomeArray = new[] { 3, 4 }, SomeBytea = new byte[] { 3, 4 }, SomeList = new List { 3, 4 }, - SomeMatrix = new[,] { { 5, 6 }, { 7, 8 } }, - SomeArrayOfArrays = new[] { new[] { 5, 6 }, new[] { 7, 8 } }, - SomeListOfLists = new List> { new List { 5, 6 }, new List { 7, 8 } }, + SomeMatrix = new[,] { { 5, 6 }, { 7, 8 } } }); ctx.SomeEntities.Add(new SomeArrayEntity { @@ -790,9 +927,7 @@ public ArrayFixture() SomeArray = new[] { 5, 6, 7 }, SomeBytea = new byte[] { 5, 6, 7 }, SomeList = new List { 5, 6, 7 }, - SomeMatrix = new[,] { { 10, 11 }, { 12, 13 } }, - SomeArrayOfArrays = new[] { new[] { 10, 11 }, new[] { 12, 13 } }, - SomeListOfLists = new List> { new List { 10, 11 }, new List { 12, 13 } } + SomeMatrix = new[,] { { 10, 11 }, { 12, 13 } } }); ctx.SaveChanges(); } From a3c633263c1da7431fda2db981f00882b139c299 Mon Sep 17 00:00:00 2001 From: Austin Drenski Date: Thu, 31 May 2018 19:01:36 -0400 Subject: [PATCH 12/16] Adds an implementation for NpgsqlQueryOptimizer - Exposes a hook for adding rewriting expression visitors. - Adds `NpgsqlExistsToAnyRewritingExpressionVisitor`. - Modified from `ExistsToAnyRewritingExpressionVisitor` to rewrite `Array.Exists(T[], Predicate)` as an expression of `Any` and `Contains`. --- .../NpgsqlServiceCollectionExtensions.cs | 3 + .../Internal/NpgsqlArrayFragmentTranslator.cs | 13 ++- ...qlExistsToAnyRewritingExpressionVisitor.cs | 87 +++++++++++++++++++ .../NpgsqlSqlTranslatingExpressionVisitor.cs | 3 + .../Query/Internal/NpgsqlQueryOptimizer.cs | 58 +++++++++++++ .../Query/ArrayQueryTest.cs | 16 +--- 6 files changed, 165 insertions(+), 15 deletions(-) create mode 100644 src/EFCore.PG/Query/ExpressionVisitors/NpgsqlExistsToAnyRewritingExpressionVisitor.cs create mode 100644 src/EFCore.PG/Query/Internal/NpgsqlQueryOptimizer.cs diff --git a/src/EFCore.PG/Extensions/NpgsqlServiceCollectionExtensions.cs b/src/EFCore.PG/Extensions/NpgsqlServiceCollectionExtensions.cs index b5bb74743..2025056dc 100644 --- a/src/EFCore.PG/Extensions/NpgsqlServiceCollectionExtensions.cs +++ b/src/EFCore.PG/Extensions/NpgsqlServiceCollectionExtensions.cs @@ -28,6 +28,7 @@ using Microsoft.EntityFrameworkCore.Query; using Microsoft.EntityFrameworkCore.Query.ExpressionTranslators; using Microsoft.EntityFrameworkCore.Query.ExpressionVisitors; +using Microsoft.EntityFrameworkCore.Query.Internal; using Microsoft.EntityFrameworkCore.Query.Sql; using Microsoft.EntityFrameworkCore.Storage; using Microsoft.EntityFrameworkCore.Update; @@ -50,6 +51,7 @@ // ReSharper disable once CheckNamespace namespace Microsoft.Extensions.DependencyInjection { + // ReSharper disable once UnusedMember.Global public static class NpgsqlEntityFrameworkServicesBuilderExtensions { /// @@ -106,6 +108,7 @@ public static IServiceCollection AddEntityFrameworkNpgsql([NotNull] this IServic .TryAdd() .TryAdd() .TryAdd() + .TryAdd() .TryAdd() .TryAdd() .TryAdd(p => p.GetService()) diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayFragmentTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayFragmentTranslator.cs index 3e2e4953b..7f4d2150e 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayFragmentTranslator.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayFragmentTranslator.cs @@ -49,8 +49,8 @@ public Expression Translate(Expression expression) if (!(expression is SubQueryExpression subQuery)) return null; - if (ContainsResult(subQuery) is Expression contains) - return contains; +// if (ContainsResult(subQuery) is Expression contains) +// return contains; if (subQuery.QueryModel.BodyClauses.Count != 1) return null; @@ -101,6 +101,13 @@ public Expression Translate(Expression expression) static Expression ContainsResult(SubQueryExpression expression) { var model = expression.QueryModel; + + if (!(model.MainFromClause.FromExpression is Expression from)) + return null; + + if (!IsArrayOrList(from.Type)) + return null; + if (model.BodyClauses.Count != 0) return null; @@ -110,7 +117,7 @@ static Expression ContainsResult(SubQueryExpression expression) if (!(model.ResultOperators[0] is ContainsResultOperator contains)) return null; - return new ArrayAnyAllExpression(ArrayComparisonType.ANY, "=", contains.Item, model.MainFromClause.FromExpression); + return new ArrayAnyAllExpression(ArrayComparisonType.ANY, "=", contains.Item, from); } #endregion diff --git a/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlExistsToAnyRewritingExpressionVisitor.cs b/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlExistsToAnyRewritingExpressionVisitor.cs new file mode 100644 index 000000000..45a46793c --- /dev/null +++ b/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlExistsToAnyRewritingExpressionVisitor.cs @@ -0,0 +1,87 @@ +#region License + +// The PostgreSQL License +// +// Copyright (C) 2016 The Npgsql Development Team +// +// Permission to use, copy, modify, and distribute this software and its +// documentation for any purpose, without fee, and without a written +// agreement is hereby granted, provided that the above copyright notice +// and this paragraph and the following two paragraphs appear in all copies. +// +// IN NO EVENT SHALL THE NPGSQL DEVELOPMENT TEAM BE LIABLE TO ANY PARTY +// FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, +// INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS +// DOCUMENTATION, EVEN IF THE NPGSQL DEVELOPMENT TEAM HAS BEEN ADVISED OF +// THE POSSIBILITY OF SUCH DAMAGE. +// +// THE NPGSQL DEVELOPMENT TEAM SPECIFICALLY DISCLAIMS ANY WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS +// ON AN "AS IS" BASIS, AND THE NPGSQL DEVELOPMENT TEAM HAS NO OBLIGATIONS +// TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + +#endregion + +using System; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Extensions.Internal; +using Microsoft.EntityFrameworkCore.Query.ExpressionVisitors; +using Remotion.Linq; +using Remotion.Linq.Clauses; +using Remotion.Linq.Clauses.Expressions; +using Remotion.Linq.Clauses.ResultOperators; +using Remotion.Linq.Parsing.ExpressionVisitors; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionVisitors +{ + /// + /// An expression rewriter for . + /// + public class NpgsqlExistsToAnyRewritingExpressionVisitor : ExpressionVisitorBase + { + /// + /// The generic for . + /// + [NotNull] static readonly MethodInfo Exists = + typeof(Array).GetRuntimeMethods().Single(x => x.Name == nameof(Array.Exists)); + + /// + protected override Expression VisitMethodCall(MethodCallExpression methodCallExpression) + { + if (!methodCallExpression.Method.MethodIsClosedFormOf(Exists)) + return methodCallExpression; + + if (!(methodCallExpression.Arguments[0] is Expression array)) + return methodCallExpression; + + if (!(methodCallExpression.Arguments[1] is LambdaExpression predicate)) + return methodCallExpression; + + var mainFromClause = + new MainFromClause( + "", + array.Type.GetElementType(), + array); + + var qsre = new QuerySourceReferenceExpression(mainFromClause); + var queryModel = new QueryModel(mainFromClause, new SelectClause(qsre)); + + var where = + new WhereClause( + ReplacingExpressionVisitor.Replace( + predicate.Parameters[0], + qsre, + predicate.Body)); + + queryModel.BodyClauses.Add(where); + queryModel.ResultOperators.Add(new AnyResultOperator()); + queryModel.ResultTypeOverride = typeof(bool); + + return new SubQueryExpression(queryModel); + } + } +} diff --git a/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlSqlTranslatingExpressionVisitor.cs b/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlSqlTranslatingExpressionVisitor.cs index 5a38b2704..4f86df851 100644 --- a/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlSqlTranslatingExpressionVisitor.cs @@ -168,6 +168,9 @@ protected virtual Expression VisitArraySubQuery([NotNull] SubQueryExpression exp case ConcatResultOperator concatResultOperator: return VisitArrayConcat(array, concatResultOperator); + case ContainsResultOperator contains: + return new ArrayAnyAllExpression(ArrayComparisonType.ANY, "=", Visit(contains.Item) ?? contains.Item, array); + case CountResultOperator countResultOperator: return VisitArrayCount(array, countResultOperator); diff --git a/src/EFCore.PG/Query/Internal/NpgsqlQueryOptimizer.cs b/src/EFCore.PG/Query/Internal/NpgsqlQueryOptimizer.cs new file mode 100644 index 000000000..a10d1425f --- /dev/null +++ b/src/EFCore.PG/Query/Internal/NpgsqlQueryOptimizer.cs @@ -0,0 +1,58 @@ +#region License + +// The PostgreSQL License +// +// Copyright (C) 2016 The Npgsql Development Team +// +// Permission to use, copy, modify, and distribute this software and its +// documentation for any purpose, without fee, and without a written +// agreement is hereby granted, provided that the above copyright notice +// and this paragraph and the following two paragraphs appear in all copies. +// +// IN NO EVENT SHALL THE NPGSQL DEVELOPMENT TEAM BE LIABLE TO ANY PARTY +// FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, +// INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS +// DOCUMENTATION, EVEN IF THE NPGSQL DEVELOPMENT TEAM HAS BEEN ADVISED OF +// THE POSSIBILITY OF SUCH DAMAGE. +// +// THE NPGSQL DEVELOPMENT TEAM SPECIFICALLY DISCLAIMS ANY WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS +// ON AN "AS IS" BASIS, AND THE NPGSQL DEVELOPMENT TEAM HAS NO OBLIGATIONS +// TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + +#endregion + +using System.Linq.Expressions; +using Microsoft.EntityFrameworkCore.Query; +using Microsoft.EntityFrameworkCore.Query.Internal; +using Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionVisitors; +using Remotion.Linq; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.Internal +{ + /// + /// The default relational LINQ query optimizer for Npgsql. + /// + public class NpgsqlQueryOptimizer : QueryOptimizer + { + /// + /// The default expression visitors registered by the Npgsql provider. + /// + static readonly ExpressionVisitor[] ExpressionVisitors = + { + new NpgsqlExistsToAnyRewritingExpressionVisitor() + }; + + /// + public override void Optimize(QueryCompilationContext queryCompilationContext, QueryModel queryModel) + { + base.Optimize(queryCompilationContext, queryModel); + + for (int i = 0; i < ExpressionVisitors.Length; i++) + { + queryModel.TransformExpressions(ExpressionVisitors[i].Visit); + } + } + } +} diff --git a/test/EFCore.PG.FunctionalTests/Query/ArrayQueryTest.cs b/test/EFCore.PG.FunctionalTests/Query/ArrayQueryTest.cs index 8135ad0d7..d6b8796b0 100644 --- a/test/EFCore.PG.FunctionalTests/Query/ArrayQueryTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/ArrayQueryTest.cs @@ -676,7 +676,7 @@ public void Array_Exists_equals_with_parameter_array_and_column_array_element() { var array = new[] { 0, 1, 2 }; var _ = ctx.SomeEntities.Where(x => Array.Exists(array, y => y == x.SomeArray[0])).ToList(); - AssertContainsInSql(@"WHERE x.""SomeArray""[1] = ANY (@__array_0) = TRUE"); + AssertContainsInSql(@"WHERE x.""SomeArray""[1] IN (0, 1, 2)"); } } @@ -687,11 +687,7 @@ public void List_Exists_equals_with_parameter_array_and_column_list_element() { var list = new List { 0, 1, 2 }; var _ = ctx.SomeEntities.Where(x => list.Exists(y => y == x.SomeList[0])).ToList(); - - // What we don't want: - AssertDoesNotContainInSql(@"WHERE (x.""SomeList""[1]) IN (0, 1, 2)"); - // What we do want: - AssertContainsInSql(@"WHERE x.""SomeList""[1] = ANY (@__list_0) = TRUE"); + AssertContainsInSql(@"WHERE x.""SomeList""[1] IN (0, 1, 2)"); } } @@ -702,7 +698,7 @@ public void Array_Exists_equals_with_parameter_array_and_column_array_element_fl { var array = new[] { 0, 1, 2 }; var _ = ctx.SomeEntities.Where(x => Array.Exists(array, y => x.SomeArray[0] == y)).ToList(); - AssertContainsInSql(@"WHERE x.""SomeArray""[1] = ANY (@__array_0) = TRUE"); + AssertContainsInSql(@"WHERE x.""SomeArray""[1] IN (0, 1, 2)"); } } @@ -713,11 +709,7 @@ public void List_Exists_equals_with_parameter_array_and_column_list_element_flip { var list = new List { 0, 1, 2 }; var _ = ctx.SomeEntities.Where(x => list.Exists(y => x.SomeList[0] == y)).ToList(); - - // What we don't want: - AssertDoesNotContainInSql(@"WHERE (x.""SomeList""[1]) IN (0, 1, 2)"); - // What we do want: - AssertContainsInSql(@"WHERE x.""SomeList""[1] = ANY (@__list_0) = TRUE"); + AssertContainsInSql(@"WHERE x.""SomeList""[1] IN (0, 1, 2)"); } } From 2282c6d3c76061138b6d7d23545e51ec4128d4c9 Mon Sep 17 00:00:00 2001 From: Austin Drenski Date: Sat, 2 Jun 2018 20:03:56 -0400 Subject: [PATCH 13/16] Updates to PgFunctionExpression Changed the private constructor to internal to make it available for expression visitors. Specifically, the following commit needs to visit `PgFunctionExpression` instances that may have been constructed in an early visitor. The constructor pass-through logic was a bit tangled, so each public constructor now passes directly to the internal one. This saves a few redundant null checks, but mostly it makes it clear which arguments are set to an internal default. The `ToString` and `GetHashCode` logic has been cleaned up, as well as adding an `IEquatable` constraint. The thinking here is that since the methods were already there, we might as well let callers know. Something to consider for future use would be whether or not this expression should "own" its own SQL representation. Basically, the `ToString` logic duplicates a lot of the same code as `NpgsqlQuerySqlGenerator` (just without the instance/argument visits). --- src/EFCore.PG/Query/Expressions/Internal/PgFunctionExpression.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/EFCore.PG/Query/Expressions/Internal/PgFunctionExpression.cs b/src/EFCore.PG/Query/Expressions/Internal/PgFunctionExpression.cs index ccb1fc548..9b2c179c6 100644 --- a/src/EFCore.PG/Query/Expressions/Internal/PgFunctionExpression.cs +++ b/src/EFCore.PG/Query/Expressions/Internal/PgFunctionExpression.cs @@ -29,7 +29,6 @@ using System.Diagnostics; using System.Linq; using System.Linq.Expressions; -using System.Text; using JetBrains.Annotations; using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Sql.Internal; using Npgsql.EntityFrameworkCore.PostgreSQL.Utilities; From 0318b2a666fb47f235e1e57c7b3d0274cdf2d33d Mon Sep 17 00:00:00 2001 From: Austin Drenski Date: Sat, 2 Jun 2018 20:16:45 -0400 Subject: [PATCH 14/16] Update array/list/string indexers !# Result operator handling Result operator handling is being moved (when possible) from `NpgsqlSqlTranslatingVisitor` to expression fragment visitors. Right now, this only affects array types with specialized result operator methods. - Moved: - `ConcatResultOperator` - `CountResultOperator` -Planned: - `ContainsResultOperator` - `AnyResultOperator` - `AllResultOperator` !# Array indexers This commit also changes the way that array indexes are handled. Because both `T[]` and `List` map to PostgreSQL arrays, we should handle array access with the same code. We can do this by rewriting `ExpressionType.ArrayIndex` as `ExpressionType.Index`. This should open up support on things like substring translation for `IEnumerable` methods in addition to simple indexers. Another benefit is cleaner SQL. We don't need to worry about wrapping indexers in parentheses because PostgreSQL oeprator precedence is well-documented. ```sql -- before SELECT (x."SomeArray"[2]); -- after SELECT x."SomeArray"[2]; ``` !# String indexers String indexers are translated to `substr` in PostgreSQL. However, `substr` returns `text` not `char`. Even if `substr` did return `char`, C# expression trees lift `char` types to `int`. So this form of translation occurs: ```c# someString[0] == 'T' ``` ```sql -- fails with 'operator does not exist: text = integer' substr("someString", 1, 1) = 84 ``` To avoid that failure, we wrap the `substr` call in `ascii` to get the ASCII or UTF8 integer value. An interesting alternative would be to cast the `text` to `bytea` and then call `get_byte` which reverts to zero-based indexes and saves us from specifying the unitary length of `substr`. ```sql -- current ascii(subsr("someString", 1, 1)) = 84 -- possible alternative get_byte("someString"::bytea, 0) = 84 ``` Opening 450 to gather feedback on this. !# Visitor dispatching This commit begins to change methods from public to protected on `NpgsqlQuerySqlGenerator` and moves the responsibility for dispatching from each expression's `Accept` method to the generator's `VisitExtensions` method. --- QueryBaseline.cs | 50 +++++ .../Internal/NpgsqlArrayFragmentTranslator.cs | 85 ++++++- .../Internal/NpgsqlArrayTranslator.cs | 56 +++-- .../NpgsqlSqlTranslatingExpressionVisitor.cs | 117 +++++----- .../Internal/ArrayAnyAllExpression.cs | 4 - .../Sql/Internal/NpgsqlQuerySqlGenerator.cs | 209 ++++++++++-------- .../Query/ArrayQueryTest.cs | 147 +++++++++++- 7 files changed, 470 insertions(+), 198 deletions(-) diff --git a/QueryBaseline.cs b/QueryBaseline.cs index c0803e86f..6d9d51beb 100644 --- a/QueryBaseline.cs +++ b/QueryBaseline.cs @@ -4014,3 +4014,53 @@ +System.RuntimeMethodHandle.InvokeMethod(Object target, Object[] arguments, Signature sig, Boolean constructor, Boolean wrapExceptions) : + AssertSql( + @"SELECT (o.""OrderDate"" + MAKE_INTERVAL(secs => 1)) AS ""OrderDate"" +FROM ""Orders"" AS o +WHERE ((o.""OrderDate"" <> NULL) OR (o.""OrderDate"" IS NULL OR NULL IS NULL)) AND (o.""OrderDate"" IS NOT NULL OR NULL IS NOT NULL)"); + + + +System.RuntimeMethodHandle.InvokeMethod(Object target, Object[] arguments, Signature sig, Boolean constructor, Boolean wrapExceptions) : + AssertSql( + @"SELECT (o.""OrderDate"" + MAKE_INTERVAL(years => 1)) AS ""OrderDate"" +FROM ""Orders"" AS o +WHERE ((o.""OrderDate"" <> NULL) OR (o.""OrderDate"" IS NULL OR NULL IS NULL)) AND (o.""OrderDate"" IS NOT NULL OR NULL IS NOT NULL)"); + + + +System.RuntimeMethodHandle.InvokeMethod(Object target, Object[] arguments, Signature sig, Boolean constructor, Boolean wrapExceptions) : + AssertSql( + @"SELECT (o.""OrderDate"" + MAKE_INTERVAL(mins => 1)) AS ""OrderDate"" +FROM ""Orders"" AS o +WHERE ((o.""OrderDate"" <> NULL) OR (o.""OrderDate"" IS NULL OR NULL IS NULL)) AND (o.""OrderDate"" IS NOT NULL OR NULL IS NOT NULL)"); + + + +System.RuntimeMethodHandle.InvokeMethod(Object target, Object[] arguments, Signature sig, Boolean constructor, Boolean wrapExceptions) : + AssertSql( + @"@__years_0='2' + +SELECT (o.""OrderDate"" + MAKE_INTERVAL(years => @__years_0)) AS ""OrderDate"" +FROM ""Orders"" AS o +WHERE ((o.""OrderDate"" <> NULL) OR (o.""OrderDate"" IS NULL OR NULL IS NULL)) AND (o.""OrderDate"" IS NOT NULL OR NULL IS NOT NULL)"); + + + +System.RuntimeMethodHandle.InvokeMethod(Object target, Object[] arguments, Signature sig, Boolean constructor, Boolean wrapExceptions) : + AssertSql( + @"SELECT (o.""OrderDate"" + MAKE_INTERVAL(months => 1)) AS ""OrderDate"" +FROM ""Orders"" AS o +WHERE ((o.""OrderDate"" <> NULL) OR (o.""OrderDate"" IS NULL OR NULL IS NULL)) AND (o.""OrderDate"" IS NOT NULL OR NULL IS NOT NULL)"); + + + +System.RuntimeMethodHandle.InvokeMethod(Object target, Object[] arguments, Signature sig, Boolean constructor, Boolean wrapExceptions) : + AssertSql( + @"SELECT (o.""OrderDate"" + MAKE_INTERVAL(hours => 1)) AS ""OrderDate"" +FROM ""Orders"" AS o +WHERE ((o.""OrderDate"" <> NULL) OR (o.""OrderDate"" IS NULL OR NULL IS NULL)) AND (o.""OrderDate"" IS NOT NULL OR NULL IS NOT NULL)"); + + + diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayFragmentTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayFragmentTranslator.cs index 7f4d2150e..37025ac01 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayFragmentTranslator.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayFragmentTranslator.cs @@ -27,8 +27,10 @@ using System.Collections.Generic; using System.Linq.Expressions; using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Query.Expressions; 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; @@ -49,13 +51,22 @@ public Expression Translate(Expression expression) if (!(expression is SubQueryExpression subQuery)) return null; -// if (ContainsResult(subQuery) is Expression contains) + var model = subQuery.QueryModel; + + if (ConcatResult(model) is Expression concat) + return concat; + + // TODO: catches too much. +// if (ContainsResult(model) is Expression contains) // return contains; - if (subQuery.QueryModel.BodyClauses.Count != 1) + if (CountResult(model) is Expression count) + return count; + + if (model.BodyClauses.Count != 1) return null; - if (!(subQuery.QueryModel.BodyClauses[0] is WhereClause where)) + if (!(model.BodyClauses[0] is WhereClause where)) return null; if (!(where.Predicate is BinaryExpression b)) @@ -98,9 +109,45 @@ public Expression Translate(Expression expression) #region SubQueries [CanBeNull] - static Expression ContainsResult(SubQueryExpression expression) + static Expression ContainsResult([NotNull] QueryModel model) { - var model = expression.QueryModel; + if (model.BodyClauses.Count != 0) + return null; + + if (model.ResultOperators.Count != 1) + return null; + + if (!(model.ResultOperators[0] is ContainsResultOperator contains)) + return null; + + if (!(model.MainFromClause.FromExpression is Expression from)) + return null; + + return + IsArrayOrList(from.Type) + ? new ArrayAnyAllExpression(ArrayComparisonType.ANY, "=", contains.Item, from) + : null; + } + + /// + /// 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; if (!(model.MainFromClause.FromExpression is Expression from)) return null; @@ -108,16 +155,40 @@ static Expression ContainsResult(SubQueryExpression expression) if (!IsArrayOrList(from.Type)) return null; + return from.Type.IsArray + ? (Expression)Expression.ArrayLength(from) + : new SqlFunctionExpression("array_length", typeof(int), new[] { from, Expression.Constant(1) }); + } + + /// + /// 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 ContainsResultOperator contains)) + if (!(model.ResultOperators[0] is ConcatResultOperator concat)) + return null; + + if (!(model.MainFromClause.FromExpression is Expression from)) + return null; + + if (!IsArrayOrList(from.Type)) return null; - return new ArrayAnyAllExpression(ArrayComparisonType.ANY, "=", contains.Item, from); + return + IsArrayOrList(concat.Source2.Type) + ? new CustomBinaryExpression(from, concat.Source2, "||", from.Type) + : null; } #endregion diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayTranslator.cs index f7f1a8ea9..b804b5f44 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayTranslator.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayTranslator.cs @@ -58,16 +58,11 @@ public Expression Translate(MethodCallExpression expression) #region EnumerableStaticMethods case nameof(Enumerable.ElementAt): - return Expression.MakeIndex(expression.Arguments[0], expression.Arguments[0].Type.GetRuntimeProperty("Item"), new[] { expression.Arguments[1] }); + return Expression.MakeIndex(expression.Arguments[0], GetIndexer(expression.Arguments[0].Type, expression.Arguments.Skip(1)), new[] { expression.Arguments[1] }); - // TODO: Currently handled in NpgsqlSqlTranslatingExpressionVisitor. case nameof(Enumerable.Append): - case nameof(Enumerable.Concat) when IsArrayOrList(expression.Arguments[1].Type): return new CustomBinaryExpression(expression.Arguments[0], expression.Arguments[1], "||", expression.Arguments[0].Type); - case nameof(Enumerable.Count): - return new SqlFunctionExpression("array_length", typeof(int), new[] { expression.Arguments[0], Expression.Constant(1) }); - case nameof(Enumerable.Prepend): return new CustomBinaryExpression(expression.Arguments[1], expression.Arguments[0], "||", expression.Arguments[0].Type); @@ -91,7 +86,8 @@ public Expression Translate(MethodCallExpression expression) #region ArrayStaticMethods - case nameof(Array.IndexOf) when expression.Method.DeclaringType == typeof(Array): + case nameof(Array.IndexOf) + when expression.Method.DeclaringType == typeof(Array): return new SqlFunctionExpression( "COALESCE", @@ -107,7 +103,7 @@ public Expression Translate(MethodCallExpression expression) #region ListInstanceMethods case "get_Item" when expression.Object is Expression instance: - return Expression.MakeIndex(instance, instance.Type.GetRuntimeProperty("Item"), expression.Arguments); + return Expression.MakeIndex(instance, GetIndexer(instance.Type, expression.Arguments), expression.Arguments); case nameof(IList.IndexOf) when IsArrayOrList(expression.Method.DeclaringType): return @@ -122,6 +118,13 @@ public Expression Translate(MethodCallExpression expression) #endregion + #region StringInstanceMethods + + case "get_Chars" when expression.Object is Expression instance: + return Expression.MakeIndex(instance, GetIndexer(instance.Type, expression.Arguments), expression.Arguments); + + #endregion + default: return null; } @@ -150,23 +153,18 @@ static bool IsTypeSupported([NotNull] MethodCallExpression expression) declaringType != typeof(Enumerable)) return false; + switch (expression.Object) + { // Instance methods are only translated for T[] and List. - if (expression.Object is Expression instance) + case Expression instance: return IsArrayOrList(instance.Type); // Extension methods may only be translated when a parameter is T[] or List - if (expression.Object is null) - { - // Static method with no parameters? Skip. - if (expression.Arguments.Count == 0) - return false; - - // Is this an extension method on T[] or List? - return IsArrayOrList(expression.Arguments[0].Type); + case null: + // Static method with no parameters? Return null. + // Static method on T[] or List? + return expression.Arguments.Count != 0 && IsArrayOrList(expression.Arguments[0].Type); } - - // Something else? Skip. - return false; } /// @@ -176,7 +174,23 @@ static bool IsTypeSupported([NotNull] MethodCallExpression expression) /// /// True if is an array or a ; otherwise, false. /// - static bool IsArrayOrList(Type type) => type.IsArray || type.IsGenericType && typeof(List<>) == type.GetGenericTypeDefinition(); + static bool IsArrayOrList(Type type) => type.IsArray || type == typeof(string) || type.IsGenericType && typeof(List<>) == type.GetGenericTypeDefinition(); + + /// + /// Finds the for the indexer property with the given parameters, or null if none is found. + /// + /// The type to search. + /// The indexer parameters. + /// + /// The or null. + /// + [CanBeNull] + static PropertyInfo GetIndexer([NotNull] Type type, [NotNull] [ItemNotNull] IEnumerable arguments) + => type.GetRuntimeProperties() + .Where(x => x.Name == type.GetCustomAttribute()?.MemberName) + .Select(x => (Indexer: x, Parameters: x.GetGetMethod().GetParameters().Select(y => y.ParameterType))) + .SingleOrDefault(x => x.Parameters.SequenceEqual(arguments.Select(y => y.Type))) + .Indexer; #endregion } diff --git a/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlSqlTranslatingExpressionVisitor.cs b/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlSqlTranslatingExpressionVisitor.cs index 4f86df851..b0edb5b79 100644 --- a/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlSqlTranslatingExpressionVisitor.cs @@ -26,6 +26,7 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Linq; using System.Linq.Expressions; using System.Reflection; using JetBrains.Annotations; @@ -33,7 +34,6 @@ 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; @@ -80,11 +80,6 @@ public class NpgsqlSqlTranslatingExpressionVisitor : SqlTranslatingExpressionVis #endregion - /// - /// The query model visitor. - /// - [NotNull] readonly RelationalQueryModelVisitor _queryModelVisitor; - /// public NpgsqlSqlTranslatingExpressionVisitor( [NotNull] SqlTranslatingExpressionVisitorDependencies dependencies, @@ -92,31 +87,62 @@ public NpgsqlSqlTranslatingExpressionVisitor( [CanBeNull] SelectExpression targetSelectExpression = null, [CanBeNull] Expression topLevelPredicate = null, bool inProjection = false) - : base(dependencies, queryModelVisitor, targetSelectExpression, topLevelPredicate, inProjection) - => _queryModelVisitor = queryModelVisitor; + : base(dependencies, queryModelVisitor, targetSelectExpression, topLevelPredicate, inProjection) {} #region Overrides /// protected override Expression VisitBinary(BinaryExpression expression) + => expression.NodeType is ExpressionType.ArrayIndex + ? Expression.MakeIndex( + Visit(expression.Left) ?? expression.Left, + indexer: null, + new[] { Visit(expression.Right) ?? expression.Right, }) + : base.VisitBinary(expression); + + /// + [CanBeNull] + protected override Expression VisitExtension(Expression expression) { - if (expression.NodeType == ExpressionType.ArrayIndex) + switch (expression) { - 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) ?? expression.Left; - var right = Visit(expression.Right) ?? expression.Right; - - return Expression.MakeBinary(ExpressionType.ArrayIndex, left, right); - } - } + case SqlFunctionExpression e: + return + new SqlFunctionExpression( + e.FunctionName, + e.Type, + e.Schema, + e.Arguments.Select(x => Visit(x) ?? x)); + + case PgFunctionExpression e: + return + new PgFunctionExpression( + e.Instance, + e.FunctionName, + e.Schema, + e.Type, + e.PositionalArguments.Select(x => Visit(x) ?? x), + e.NamedArguments.ToDictionary(x => x.Key, x => Visit(x.Value) ?? x.Value)); + + case CustomBinaryExpression e: + return + new CustomBinaryExpression( + Visit(e.Left) ?? e.Left, + Visit(e.Right) ?? e.Right, + e.Operator, + e.Type); + + case CustomUnaryExpression e: + return + new CustomUnaryExpression( + Visit(e.Operand) ?? e.Operand, + e.Operator, + e.Type, + e.Postfix); - return base.VisitBinary(expression); + default: + return base.VisitExtension(expression); + } } /// @@ -124,6 +150,12 @@ protected override Expression VisitBinary(BinaryExpression expression) protected override Expression VisitSubQuery(SubQueryExpression expression) => base.VisitSubQuery(expression) ?? VisitArraySubQuery(expression); + /// + protected override Expression VisitUnary(UnaryExpression expression) + => Visit(expression.Operand) is Expression operand + ? Expression.MakeUnary(expression.NodeType, operand, expression.Type) + : base.VisitUnary(expression); + #endregion #region ArraySubQueries @@ -165,15 +197,9 @@ protected virtual Expression VisitArraySubQuery([NotNull] SubQueryExpression exp case AllResultOperator allResultOperator: return VisitArrayAll(array, allResultOperator); - case ConcatResultOperator concatResultOperator: - return VisitArrayConcat(array, concatResultOperator); - case ContainsResultOperator contains: return new ArrayAnyAllExpression(ArrayComparisonType.ANY, "=", Visit(contains.Item) ?? contains.Item, array); - case CountResultOperator countResultOperator: - return VisitArrayCount(array, countResultOperator); - default: return null; } @@ -217,39 +243,6 @@ protected virtual Expression VisitArrayAll([NotNull] Expression array, [NotNull] => VisitArrayLike(array, allResultOperator.Predicate, ArrayComparisonType.ALL) ?? VisitArrayContains(array, allResultOperator.Predicate, ArrayComparisonType.ALL); - /// - /// Visits an array-based concatenation expression: {array|value} || {array|value}. - /// - /// The source expression. - /// The result operator. - /// - /// An expression or null. - /// - [CanBeNull] - protected virtual Expression VisitArrayConcat([NotNull] Expression array, [NotNull] ConcatResultOperator concatResultOperator) - { - var other = Visit(concatResultOperator.Source2) ?? concatResultOperator.Source2; - return IsArrayOrList(other.Type) ? new CustomBinaryExpression(array, other, "||", array.Type) : null; - } - - /// - /// Visits an array-based count expression: {array}.Length, {list}.Count, {array|list}.Count(), {array|list}.Count({predicate}). - /// - /// The source expression. - /// The result operator. - /// - /// An expression or null. - /// - [CanBeNull] - protected virtual Expression VisitArrayCount([NotNull] Expression array, [NotNull] CountResultOperator countResultOperator) - { - // TODO: handle count operation with predicate. - - return array.Type.IsArray - ? (Expression)Expression.ArrayLength(array) - : new SqlFunctionExpression("array_length", typeof(int), new[] { array, Expression.Constant(1) }); - } - /// /// Visits an array-based comparison for an LIKE or ILIKE expression: {operand} {LIKE|ILIKE} {ANY|ALL} ({array}). /// diff --git a/src/EFCore.PG/Query/Expressions/Internal/ArrayAnyAllExpression.cs b/src/EFCore.PG/Query/Expressions/Internal/ArrayAnyAllExpression.cs index 3bdac9c92..4cbe035d8 100644 --- a/src/EFCore.PG/Query/Expressions/Internal/ArrayAnyAllExpression.cs +++ b/src/EFCore.PG/Query/Expressions/Internal/ArrayAnyAllExpression.cs @@ -27,7 +27,6 @@ using System.Linq.Expressions; using JetBrains.Annotations; using Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionVisitors; -using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Sql.Internal; using Npgsql.EntityFrameworkCore.PostgreSQL.Utilities; namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal @@ -106,9 +105,6 @@ protected override Expression Accept(ExpressionVisitor visitor) { switch (visitor) { - case NpgsqlQuerySqlGenerator npsgqlGenerator: - return npsgqlGenerator.VisitArrayAnyAll(this); - case NpgsqlSqlTranslatingExpressionVisitor npgsqlVisitor: return VisitChildren(npgsqlVisitor); diff --git a/src/EFCore.PG/Query/Sql/Internal/NpgsqlQuerySqlGenerator.cs b/src/EFCore.PG/Query/Sql/Internal/NpgsqlQuerySqlGenerator.cs index c30a5f5ee..e16974a3e 100644 --- a/src/EFCore.PG/Query/Sql/Internal/NpgsqlQuerySqlGenerator.cs +++ b/src/EFCore.PG/Query/Sql/Internal/NpgsqlQuerySqlGenerator.cs @@ -24,7 +24,6 @@ #endregion using System; -using System.Diagnostics; using System.Linq.Expressions; using System.Text.RegularExpressions; using JetBrains.Annotations; @@ -39,7 +38,6 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.Sql.Internal { public class NpgsqlQuerySqlGenerator : DefaultQuerySqlGenerator { - // ReSharper disable once NotAccessedField.Local readonly bool _reverseNullOrderingEnabled; /// @@ -56,6 +54,8 @@ public NpgsqlQuerySqlGenerator( : base(dependencies, selectExpression) => _reverseNullOrderingEnabled = reverseNullOrderingEnabled; + #region Generators + /// protected override void GenerateTop(SelectExpression selectExpression) { @@ -85,6 +85,81 @@ protected override void GenerateLimitOffset(SelectExpression selectExpression) } } + /// + /// PostgreSQL array indexing is 1-based. If the index happens to be a constant, + /// just increment it. Otherwise, append a +1 in the SQL. + /// + protected virtual Expression GenerateOneBasedIndexExpression(Expression expression) + => expression is ConstantExpression constantExpression + ? Expression.Constant(Convert.ToInt32(constantExpression.Value) + 1) + : (Expression)Expression.Add(expression, Expression.Constant(1)); + + /// + protected override string GenerateOperator(Expression expression) + { + switch (expression.NodeType) + { + case ExpressionType.Add: + if (expression.Type == typeof(string)) + return " || "; + goto default; + case ExpressionType.And: + if (expression.Type == typeof(bool)) + return " AND "; + goto default; + case ExpressionType.Or: + if (expression.Type == typeof(bool)) + return " OR "; + goto default; + default: + return base.GenerateOperator(expression); + } + } + + /// + protected override void GenerateOrdering(Ordering ordering) + { + base.GenerateOrdering(ordering); + if (_reverseNullOrderingEnabled) + Sql.Append( + ordering.OrderingDirection == OrderingDirection.Asc + ? " NULLS FIRST" + : " NULLS LAST"); + } + + #endregion + + #region Visitors + + /// + protected override Expression VisitExtension(Expression expression) + { + switch (expression) + { + case ArrayAnyAllExpression arrayAnyAllExpression: + return VisitArrayAnyAll(arrayAnyAllExpression); + + default: + return base.VisitExtension(expression); + } + } + + /// + /// Produces expressions like: 1 = ANY ('{0,1,2}') or 'cat' LIKE ANY ('{a%,b%,c%}'). + /// + protected virtual Expression VisitArrayAnyAll(ArrayAnyAllExpression arrayAnyAllExpression) + { + Visit(arrayAnyAllExpression.Operand); + Sql.Append(' '); + Sql.Append(arrayAnyAllExpression.Operator); + Sql.Append(' '); + Sql.Append(arrayAnyAllExpression.ArrayComparisonType.ToString()); + Sql.Append(" ("); + Visit(arrayAnyAllExpression.Array); + Sql.Append(')'); + return arrayAnyAllExpression; + } + /// public override Expression VisitSqlFunction(SqlFunctionExpression sqlFunctionExpression) { @@ -135,10 +210,6 @@ protected override Expression VisitBinary(BinaryExpression expression) goto default; } - case ExpressionType.ArrayIndex: - VisitArrayIndex(expression); - return expression; - default: return base.VisitBinary(expression); } @@ -156,38 +227,39 @@ protected override Expression VisitUnary(UnaryExpression expression) return base.VisitUnary(expression); } - protected virtual void VisitArrayIndex([NotNull] BinaryExpression expression) - { - Debug.Assert(expression.NodeType == ExpressionType.ArrayIndex); - - if (expression.Left.Type == typeof(byte[])) - { - // bytea cannot be subscripted, but there's get_byte - VisitSqlFunction(new SqlFunctionExpression("get_byte", typeof(byte), - new[] { expression.Left, expression.Right })); - return; - } - - if (expression.Left.Type == typeof(string)) - { - // text cannot be subscripted, use substr - // PostgreSQL substr() is 1-based. - - VisitSqlFunction(new SqlFunctionExpression("substr", typeof(char), - new[] { expression.Left, expression.Right, Expression.Constant(1) })); - return; - } - - // Regular array from here - Visit(expression.Left); - Sql.Append('['); - Visit(GenerateOneBasedIndexExpression(expression.Right)); - Sql.Append(']'); - } - /// protected override Expression VisitIndex(IndexExpression expression) { + // bytea cannot be subscripted + if (expression.Object.Type == typeof(byte[])) + return + VisitSqlFunction( + new SqlFunctionExpression( + "get_byte", + typeof(byte), + new[] { expression.Object, expression.Arguments[0] })); + + // text cannot be subscripted + if (expression.Object.Type == typeof(string)) + return + VisitSqlFunction( + // TODO: Expression tree parses `string[0] == 'T'` as though its `string[0] == 84`. + new SqlFunctionExpression( + "ascii", + typeof(int), + new[] + { + 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++) { @@ -195,38 +267,14 @@ protected override Expression VisitIndex(IndexExpression expression) Visit(GenerateOneBasedIndexExpression(expression.Arguments[i])); Sql.Append(']'); } - return expression; - } - /// - /// Produces expressions like: 1 = ANY ('{0,1,2}') or 'cat' LIKE ANY ('{a%,b%,c%}'). - /// - public Expression VisitArrayAnyAll(ArrayAnyAllExpression arrayAnyAllExpression) - { - Visit(arrayAnyAllExpression.Operand); - Sql.Append(' '); - Sql.Append(arrayAnyAllExpression.Operator); - Sql.Append(' '); - Sql.Append(arrayAnyAllExpression.ArrayComparisonType.ToString()); - Sql.Append(" ("); - Visit(arrayAnyAllExpression.Array); - Sql.Append(')'); - return arrayAnyAllExpression; + return expression; } - /// - /// PostgreSQL array indexing is 1-based. If the index happens to be a constant, - /// just increment it. Otherwise, append a +1 in the SQL. - /// - static Expression GenerateOneBasedIndexExpression(Expression expression) - => expression is ConstantExpression constantExpression - ? Expression.Constant(Convert.ToInt32(constantExpression.Value) + 1) - : (Expression)Expression.Add(expression, Expression.Constant(1)); - /// /// See: http://www.postgresql.org/docs/current/static/functions-matching.html /// - public Expression VisitRegexMatch([NotNull] RegexMatchExpression regexMatchExpression) + public virtual Expression VisitRegexMatch([NotNull] RegexMatchExpression regexMatchExpression) { Check.NotNull(regexMatchExpression, nameof(regexMatchExpression)); var options = regexMatchExpression.Options; @@ -257,7 +305,7 @@ public Expression VisitRegexMatch([NotNull] RegexMatchExpression regexMatchExpre return regexMatchExpression; } - public Expression VisitAtTimeZone([NotNull] AtTimeZoneExpression atTimeZoneExpression) + public virtual Expression VisitAtTimeZone([NotNull] AtTimeZoneExpression atTimeZoneExpression) { Check.NotNull(atTimeZoneExpression, nameof(atTimeZoneExpression)); Visit(atTimeZoneExpression.TimestampExpression); @@ -287,7 +335,7 @@ public virtual Expression VisitILike(ILikeExpression iLikeExpression) return iLikeExpression; } - public Expression VisitExplicitStoreTypeCast([NotNull] ExplicitStoreTypeCastExpression castExpression) + public virtual Expression VisitExplicitStoreTypeCast([NotNull] ExplicitStoreTypeCastExpression castExpression) { Sql.Append("CAST("); @@ -302,39 +350,6 @@ public Expression VisitExplicitStoreTypeCast([NotNull] ExplicitStoreTypeCastExpr return castExpression; } - /// - protected override string GenerateOperator(Expression expression) - { - switch (expression.NodeType) - { - case ExpressionType.Add: - if (expression.Type == typeof(string)) - return " || "; - goto default; - case ExpressionType.And: - if (expression.Type == typeof(bool)) - return " AND "; - goto default; - case ExpressionType.Or: - if (expression.Type == typeof(bool)) - return " OR "; - goto default; - default: - return base.GenerateOperator(expression); - } - } - - /// - protected override void GenerateOrdering(Ordering ordering) - { - base.GenerateOrdering(ordering); - if (_reverseNullOrderingEnabled) - Sql.Append( - ordering.OrderingDirection == OrderingDirection.Asc - ? " NULLS FIRST" - : " NULLS LAST"); - } - public virtual Expression VisitCustomBinary(CustomBinaryExpression expression) { Check.NotNull(expression, nameof(expression)); @@ -408,5 +423,7 @@ public virtual Expression VisitPgFunction(PgFunctionExpression e) //_typeMapping = parentTypeMapping; return e; } + + #endregion } } diff --git a/test/EFCore.PG.FunctionalTests/Query/ArrayQueryTest.cs b/test/EFCore.PG.FunctionalTests/Query/ArrayQueryTest.cs index d6b8796b0..a1e79318c 100644 --- a/test/EFCore.PG.FunctionalTests/Query/ArrayQueryTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/ArrayQueryTest.cs @@ -49,7 +49,7 @@ public void Array_Index_with_constant() { var actual = ctx.SomeEntities.Where(e => e.SomeArray[0] == 3).ToList(); Assert.Equal(1, actual.Count); - AssertContainsInSql(@"WHERE (e.""SomeArray""[1]) = 3"); + AssertContainsInSql(@"WHERE e.""SomeArray""[1] = 3"); } } @@ -64,6 +64,38 @@ public void List_Index_with_constant() } } + [Fact] + public void Array_Index_bytea_with_constant() + { + using (var ctx = 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"); + } + } + + [Fact] + public void String_Index_text_with_constant_char_as_int() + { + using (var ctx = CreateContext()) + { + var actual = ctx.SomeEntities.Where(e => e.SomeString[0] == 'T').ToList(); + Assert.Equal(1, actual.Count); + AssertContainsInSql(@"WHERE ascii(substr(e.""SomeString"", 1, 1)) = 84"); + } + } + + [Fact] + public void String_Index_text_with_constant_string() + { + using (var ctx = CreateContext()) + { + var _ = ctx.SomeEntities.Where(e => e.SomeString[0].ToString() == "T").ToList(); + AssertContainsInSql(@"WHERE CAST(ascii(substr(e.""SomeString"", 1, 1)) AS text) = 'T'"); + } + } + [Fact] public void Array_ElementAt_with_constant() { @@ -86,6 +118,37 @@ public void List_ElementAt_with_constant() } } + [Fact] + public void Array_ElementAt_bytea_with_constant() + { + using (var ctx = CreateContext()) + { + var actual = ctx.SomeEntities.Where(e => e.SomeBytea.ElementAt(0) == 3).ToList(); + Assert.Equal(1, actual.Count); + AssertContainsInSql(@"WHERE get_byte(e.""SomeBytea"", 0) = 3"); + } + } + + [Fact] + public void String_ElementAt_text_with_constant_char_as_int() + { + using (var ctx = CreateContext()) + { + var _ = ctx.SomeEntities.Where(e => e.SomeString.ElementAt(0) == 'T').ToList(); + AssertContainsInSql(@"WHERE ascii(substr(e.""SomeString"", 1, 1)) = 84"); + } + } + + [Fact] + public void String_ElementAt_text_with_constant_string() + { + using (var ctx = CreateContext()) + { + var _ = ctx.SomeEntities.Where(e => e.SomeString.ElementAt(0).ToString() == "T").ToList(); + AssertContainsInSql(@"WHERE CAST(ascii(substr(e.""SomeString"", 1, 1)) AS text) = 'T'"); + } + } + [Fact] public void Array_Index_with_non_constant() { @@ -95,7 +158,7 @@ public void Array_Index_with_non_constant() 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"); + AssertContainsInSql(@"WHERE e.""SomeArray""[@__x_0 + 1] = 3"); } } @@ -112,6 +175,44 @@ public void List_Index_with_non_constant() } } + [Fact] + public void Array_Index_bytea_with_non_constant() + { + using (var ctx = CreateContext()) + { + // ReSharper disable once ConvertToConstant.Local + var x = 0; + var actual = ctx.SomeEntities.Where(e => e.SomeBytea[x] == 3).ToList(); + Assert.Equal(1, actual.Count); + AssertContainsInSql(@"WHERE get_byte(e.""SomeBytea"", @__x_0) = 3"); + } + } + + [Fact] + public void String_Index_text_with_non_constant_char_as_int() + { + using (var ctx = CreateContext()) + { + // ReSharper disable once ConvertToConstant.Local + var x = 0; + var actual = ctx.SomeEntities.Where(e => e.SomeString[x] == 'T').ToList(); + Assert.Equal(1, actual.Count); + AssertContainsInSql(@"WHERE ascii(substr(e.""SomeString"", @__x_0 + 1, 1)) = 84"); + } + } + + [Fact] + public void String_Index_text_with_non_constant_string() + { + using (var ctx = CreateContext()) + { + // ReSharper disable once ConvertToConstant.Local + var x = 0; + var _ = ctx.SomeEntities.Where(e => e.SomeString[x].ToString() == "T").ToList(); + AssertContainsInSql(@"WHERE CAST(ascii(substr(e.""SomeString"", @__x_0 + 1, 1)) AS text) = 'T'"); + } + } + [Fact] public void Array_ElementAt_with_non_constant() { @@ -126,7 +227,7 @@ public void Array_ElementAt_with_non_constant() } [Fact] - public void List_IndexElementAt_with_non_constant() + public void List_ElementAt_with_non_constant() { using (var ctx = CreateContext()) { @@ -139,13 +240,40 @@ public void List_IndexElementAt_with_non_constant() } [Fact] - public void Array_Index_bytea_with_constant() + public void Array_ElementAt_bytea_with_non_constant() { using (var ctx = CreateContext()) { - var actual = ctx.SomeEntities.Where(e => e.SomeBytea[0] == 3).ToList(); + // ReSharper disable once ConvertToConstant.Local + var x = 0; + var actual = ctx.SomeEntities.Where(e => e.SomeBytea.ElementAt(x) == 3).ToList(); Assert.Equal(1, actual.Count); - AssertContainsInSql(@"WHERE (get_byte(e.""SomeBytea"", 0)) = 3"); + AssertContainsInSql(@"WHERE get_byte(e.""SomeBytea"", @__x_0) = 3"); + } + } + + [Fact] + public void String_ElementAt_text_with_non_constant_char_as_int() + { + using (var ctx = CreateContext()) + { + // ReSharper disable once ConvertToConstant.Local + var x = 0; + var actual = ctx.SomeEntities.Where(e => e.SomeString.ElementAt(x) == 'T').ToList(); + Assert.Equal(1, actual.Count); + AssertContainsInSql(@"WHERE ascii(substr(e.""SomeString"", @__x_0 + 1, 1)) = 84"); + } + } + + [Fact] + public void String_ElementAt_text_with_non_constant_sting() + { + using (var ctx = CreateContext()) + { + // ReSharper disable once ConvertToConstant.Local + var x = 0; + var _ = ctx.SomeEntities.Where(e => e.SomeString.ElementAt(x).ToString() == "T").ToList(); + AssertContainsInSql(@"WHERE CAST(ascii(substr(e.""SomeString"", @__x_0 + 1, 1)) AS text) = 'T'"); } } @@ -880,6 +1008,7 @@ public class SomeArrayEntity public List SomeList { get; set; } public int[,] SomeMatrix { get; set; } public byte[] SomeBytea { get; set; } + public string SomeString { get; set; } } /// @@ -911,7 +1040,8 @@ public ArrayFixture() SomeArray = new[] { 3, 4 }, SomeBytea = new byte[] { 3, 4 }, SomeList = new List { 3, 4 }, - SomeMatrix = new[,] { { 5, 6 }, { 7, 8 } } + SomeMatrix = new[,] { { 5, 6 }, { 7, 8 } }, + SomeString = "This_is_a_test" }); ctx.SomeEntities.Add(new SomeArrayEntity { @@ -919,7 +1049,8 @@ public ArrayFixture() SomeArray = new[] { 5, 6, 7 }, SomeBytea = new byte[] { 5, 6, 7 }, SomeList = new List { 5, 6, 7 }, - SomeMatrix = new[,] { { 10, 11 }, { 12, 13 } } + SomeMatrix = new[,] { { 10, 11 }, { 12, 13 } }, + SomeString = "this_is_a_test" }); ctx.SaveChanges(); } From 2a5e41a9116dc86707e2362672335e2d8bd75afc Mon Sep 17 00:00:00 2001 From: Austin Drenski Date: Sun, 3 Jun 2018 03:05:21 -0400 Subject: [PATCH 15/16] Skips tests -- CompiledQuery behavior difference - `CompiledQueryNpgsqlTests.Query_with_array_parameter()` - `CompiledQueryNpgsqlTests.Query_with_array_parameter_async()` Both throw: ```c# System.InvalidCastException : Can't write CLR type System.String[] with handler type TextHandler ``` Tests are overridden for now with an example showing the non-compiled form does not throw. Needs further investigation. --- .../Sql/Internal/NpgsqlQuerySqlGenerator.cs | 12 +++++- .../Query/CompiledQueryNpgsqlTest.cs | 42 ++++++++++++++++++- 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/src/EFCore.PG/Query/Sql/Internal/NpgsqlQuerySqlGenerator.cs b/src/EFCore.PG/Query/Sql/Internal/NpgsqlQuerySqlGenerator.cs index e16974a3e..1ff54ce37 100644 --- a/src/EFCore.PG/Query/Sql/Internal/NpgsqlQuerySqlGenerator.cs +++ b/src/EFCore.PG/Query/Sql/Internal/NpgsqlQuerySqlGenerator.cs @@ -243,7 +243,6 @@ protected override Expression VisitIndex(IndexExpression expression) if (expression.Object.Type == typeof(string)) return VisitSqlFunction( - // TODO: Expression tree parses `string[0] == 'T'` as though its `string[0] == 84`. new SqlFunctionExpression( "ascii", typeof(int), @@ -260,6 +259,17 @@ protected override Expression VisitIndex(IndexExpression expression) }) })); +// // TODO: discussion: https://github.com/npgsql/Npgsql.EntityFrameworkCore.PostgreSQL/issues/450 +// VisitSqlFunction( +// new SqlFunctionExpression( +// "get_byte", +// typeof(int), +// new[] +// { +// new CustomUnaryExpression(expression.Object, "::bytea", typeof(int), true), +// expression.Arguments[0] +// })); + Visit(expression.Object); for (int i = 0; i < expression.Arguments.Count; i++) { diff --git a/test/EFCore.PG.FunctionalTests/Query/CompiledQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/CompiledQueryNpgsqlTest.cs index 0e49db804..b00d841a1 100644 --- a/test/EFCore.PG.FunctionalTests/Query/CompiledQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/CompiledQueryNpgsqlTest.cs @@ -1,15 +1,55 @@ -using Microsoft.EntityFrameworkCore.Query; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Query; +using Microsoft.EntityFrameworkCore.TestModels.Northwind; using Microsoft.EntityFrameworkCore.TestUtilities; +using Microsoft.EntityFrameworkCore.TestUtilities.Xunit; +using Xunit; using Xunit.Abstractions; namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query { public class CompiledQueryNpgsqlTest : CompiledQueryTestBase> { + // ReSharper disable once UnusedParameter.Local public CompiledQueryNpgsqlTest(NorthwindQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) : base(fixture) { fixture.TestSqlLoggerFactory.Clear(); } + + [ConditionalFact(Skip = "Throws: Can't write CLR type System.String[] with handler type TextHandler")] + public override void Query_with_array_parameter() + { + var query = EF.CompileQuery( + (NorthwindContext context, string[] args) + => context.Customers.Where(c => c.CustomerID == args[0])); + + using (var context = CreateContext()) + { + var args = new[] { "ALFKI" }; + + // BUG: this passes + var _ = context.Customers.Where(c => c.CustomerID == args[0]).ToList(); + + // BUG: this throws + // System.InvalidCastException : Can't write CLR type System.String[] with handler type TextHandler + var result = query(context, args).First().CustomerID; + + Assert.Equal("ALFKI", result); + } + + using (var context = CreateContext()) + { + Assert.Equal("ANATR", query(context, new[] { "ANATR" }).First().CustomerID); + } + } + + [ConditionalFact(Skip = "Throws: Can't write CLR type System.String[] with handler type TextHandler")] + public override async Task Query_with_array_parameter_async() + { + await base.Query_with_array_parameter_async(); + } } } From a917e004cb4452fb65b9d37cbcbfbcacc53be912 Mon Sep 17 00:00:00 2001 From: Austin Drenski Date: Sun, 3 Jun 2018 04:10:59 -0400 Subject: [PATCH 16/16] Fixes the previous commit, unskips tests. Something to do with `MemberAccessBindingExpressionVisitor` happens under some specific circumstances that coincide with a compiled query. Added an `IsSafeToVisit` helper to `NpgsqlSqlTranslating...Visitor` for an additional guard when handling arrays that could be some type of replaced parameter. --- QueryBaseline.cs | 50 ------------------- .../Extensions/NpgsqlArrayExtensions.cs | 30 ++++++++--- .../NpgsqlSqlTranslatingExpressionVisitor.cs | 34 +++++++++++-- .../Internal/PgFunctionExpression.cs | 1 + .../Utilities/ClientEvaluationNotSupported.cs | 48 ------------------ .../Query/ArrayQueryTest.cs | 44 ++-------------- .../Query/CompiledQueryNpgsqlTest.cs | 42 +--------------- 7 files changed, 59 insertions(+), 190 deletions(-) delete mode 100644 src/EFCore.PG/Utilities/ClientEvaluationNotSupported.cs diff --git a/QueryBaseline.cs b/QueryBaseline.cs index 6d9d51beb..c0803e86f 100644 --- a/QueryBaseline.cs +++ b/QueryBaseline.cs @@ -4014,53 +4014,3 @@ -System.RuntimeMethodHandle.InvokeMethod(Object target, Object[] arguments, Signature sig, Boolean constructor, Boolean wrapExceptions) : - AssertSql( - @"SELECT (o.""OrderDate"" + MAKE_INTERVAL(secs => 1)) AS ""OrderDate"" -FROM ""Orders"" AS o -WHERE ((o.""OrderDate"" <> NULL) OR (o.""OrderDate"" IS NULL OR NULL IS NULL)) AND (o.""OrderDate"" IS NOT NULL OR NULL IS NOT NULL)"); - - - -System.RuntimeMethodHandle.InvokeMethod(Object target, Object[] arguments, Signature sig, Boolean constructor, Boolean wrapExceptions) : - AssertSql( - @"SELECT (o.""OrderDate"" + MAKE_INTERVAL(years => 1)) AS ""OrderDate"" -FROM ""Orders"" AS o -WHERE ((o.""OrderDate"" <> NULL) OR (o.""OrderDate"" IS NULL OR NULL IS NULL)) AND (o.""OrderDate"" IS NOT NULL OR NULL IS NOT NULL)"); - - - -System.RuntimeMethodHandle.InvokeMethod(Object target, Object[] arguments, Signature sig, Boolean constructor, Boolean wrapExceptions) : - AssertSql( - @"SELECT (o.""OrderDate"" + MAKE_INTERVAL(mins => 1)) AS ""OrderDate"" -FROM ""Orders"" AS o -WHERE ((o.""OrderDate"" <> NULL) OR (o.""OrderDate"" IS NULL OR NULL IS NULL)) AND (o.""OrderDate"" IS NOT NULL OR NULL IS NOT NULL)"); - - - -System.RuntimeMethodHandle.InvokeMethod(Object target, Object[] arguments, Signature sig, Boolean constructor, Boolean wrapExceptions) : - AssertSql( - @"@__years_0='2' - -SELECT (o.""OrderDate"" + MAKE_INTERVAL(years => @__years_0)) AS ""OrderDate"" -FROM ""Orders"" AS o -WHERE ((o.""OrderDate"" <> NULL) OR (o.""OrderDate"" IS NULL OR NULL IS NULL)) AND (o.""OrderDate"" IS NOT NULL OR NULL IS NOT NULL)"); - - - -System.RuntimeMethodHandle.InvokeMethod(Object target, Object[] arguments, Signature sig, Boolean constructor, Boolean wrapExceptions) : - AssertSql( - @"SELECT (o.""OrderDate"" + MAKE_INTERVAL(months => 1)) AS ""OrderDate"" -FROM ""Orders"" AS o -WHERE ((o.""OrderDate"" <> NULL) OR (o.""OrderDate"" IS NULL OR NULL IS NULL)) AND (o.""OrderDate"" IS NOT NULL OR NULL IS NOT NULL)"); - - - -System.RuntimeMethodHandle.InvokeMethod(Object target, Object[] arguments, Signature sig, Boolean constructor, Boolean wrapExceptions) : - AssertSql( - @"SELECT (o.""OrderDate"" + MAKE_INTERVAL(hours => 1)) AS ""OrderDate"" -FROM ""Orders"" AS o -WHERE ((o.""OrderDate"" <> NULL) OR (o.""OrderDate"" IS NULL OR NULL IS NULL)) AND (o.""OrderDate"" IS NOT NULL OR NULL IS NOT NULL)"); - - - diff --git a/src/EFCore.PG/Extensions/NpgsqlArrayExtensions.cs b/src/EFCore.PG/Extensions/NpgsqlArrayExtensions.cs index afbe423c6..674152580 100644 --- a/src/EFCore.PG/Extensions/NpgsqlArrayExtensions.cs +++ b/src/EFCore.PG/Extensions/NpgsqlArrayExtensions.cs @@ -23,9 +23,10 @@ #endregion +using System; using System.Collections.Generic; +using System.Runtime.CompilerServices; using JetBrains.Annotations; -using Npgsql.EntityFrameworkCore.PostgreSQL.Utilities; // ReSharper disable once CheckNamespace namespace Microsoft.EntityFrameworkCore @@ -49,7 +50,7 @@ public static class NpgsqlArrayExtensions /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. /// public static string ArrayToString([CanBeNull] this DbFunctions _, [NotNull] T[] array, [CanBeNull] string delimiter) - => throw new ClientEvaluationNotSupportedException(); + => throw ClientEvaluationNotSupportedException(); /// /// Concatenates elements using the supplied delimiter. @@ -65,7 +66,7 @@ public static string ArrayToString([CanBeNull] this DbFunctions _, [NotNull] /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. /// public static string ArrayToString([CanBeNull] this DbFunctions _, [NotNull] List list, [CanBeNull] string delimiter) - => throw new ClientEvaluationNotSupportedException(); + => throw ClientEvaluationNotSupportedException(); /// /// Concatenates elements using the supplied delimiter and the string representation for null elements. @@ -82,7 +83,7 @@ public static string ArrayToString([CanBeNull] this DbFunctions _, [NotNull] /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. /// public static string ArrayToString([CanBeNull] this DbFunctions _, [NotNull] T[] array, [CanBeNull] string delimiter, [CanBeNull] string nullString) - => throw new ClientEvaluationNotSupportedException(); + => throw ClientEvaluationNotSupportedException(); /// /// Concatenates elements using the supplied delimiter and the string representation for null elements. @@ -99,7 +100,7 @@ public static string ArrayToString([CanBeNull] this DbFunctions _, [NotNull] /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. /// public static string ArrayToString([CanBeNull] this DbFunctions _, [NotNull] List list, [CanBeNull] string delimiter, [CanBeNull] string nullString) - => throw new ClientEvaluationNotSupportedException(); + => throw ClientEvaluationNotSupportedException(); /// /// Converts the input string into an array using the supplied delimiter and the string representation for null elements. @@ -116,7 +117,7 @@ public static string ArrayToString([CanBeNull] this DbFunctions _, [NotNull] /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. /// public static T[] StringToArray([CanBeNull] this DbFunctions _, [NotNull] string input, [CanBeNull] string delimiter, [CanBeNull] string nullString) - => throw new ClientEvaluationNotSupportedException(); + => throw ClientEvaluationNotSupportedException(); /// /// Converts the input string into a using the supplied delimiter and the string representation for null elements. @@ -133,6 +134,21 @@ public static T[] StringToArray([CanBeNull] this DbFunctions _, [NotNull] str /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. /// public static List StringToList([CanBeNull] this DbFunctions _, [NotNull] string input, [CanBeNull] string delimiter, [CanBeNull] string nullString) - => throw new ClientEvaluationNotSupportedException(); + => throw ClientEvaluationNotSupportedException(); + + #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/ExpressionVisitors/NpgsqlSqlTranslatingExpressionVisitor.cs b/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlSqlTranslatingExpressionVisitor.cs index b0edb5b79..23b1f215b 100644 --- a/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlSqlTranslatingExpressionVisitor.cs @@ -34,6 +34,7 @@ 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; @@ -80,6 +81,17 @@ public class NpgsqlSqlTranslatingExpressionVisitor : SqlTranslatingExpressionVis #endregion + /// + /// The current query model visitor. + /// + [NotNull] readonly RelationalQueryModelVisitor _queryModelVisitor; + + /// + /// The current query compilation context. + /// + [NotNull] + RelationalQueryCompilationContext Context => _queryModelVisitor.QueryCompilationContext; + /// public NpgsqlSqlTranslatingExpressionVisitor( [NotNull] SqlTranslatingExpressionVisitorDependencies dependencies, @@ -87,17 +99,18 @@ public NpgsqlSqlTranslatingExpressionVisitor( [CanBeNull] SelectExpression targetSelectExpression = null, [CanBeNull] Expression topLevelPredicate = null, bool inProjection = false) - : base(dependencies, queryModelVisitor, targetSelectExpression, topLevelPredicate, inProjection) {} + : base(dependencies, queryModelVisitor, targetSelectExpression, topLevelPredicate, inProjection) + => _queryModelVisitor = queryModelVisitor; #region Overrides /// protected override Expression VisitBinary(BinaryExpression expression) - => expression.NodeType is ExpressionType.ArrayIndex - ? Expression.MakeIndex( + => expression.NodeType is ExpressionType.ArrayIndex && + IsSafeToVisit(expression, Context) + ? Expression.ArrayAccess( Visit(expression.Left) ?? expression.Left, - indexer: null, - new[] { Visit(expression.Right) ?? expression.Right, }) + Visit(expression.Right) ?? expression.Right) : base.VisitBinary(expression); /// @@ -312,6 +325,17 @@ protected virtual Expression VisitArrayContains([NotNull] Expression array, [Not /// static bool IsArrayOrList([NotNull] Type type) => type.IsArray || type.IsGenericType && typeof(List<>) == type.GetGenericTypeDefinition(); + /// + /// True if the expression is safe to visitat this stage. + /// + /// The expression to check + /// The context to use. + /// + /// True to visit this expression; otherwise false. + /// + static bool IsSafeToVisit(BinaryExpression expression, RelationalQueryCompilationContext context) + => MemberAccessBindingExpressionVisitor.GetPropertyPath(expression.Left, context, out _).Count != 0; + #endregion } } diff --git a/src/EFCore.PG/Query/Expressions/Internal/PgFunctionExpression.cs b/src/EFCore.PG/Query/Expressions/Internal/PgFunctionExpression.cs index 9b2c179c6..ccb1fc548 100644 --- a/src/EFCore.PG/Query/Expressions/Internal/PgFunctionExpression.cs +++ b/src/EFCore.PG/Query/Expressions/Internal/PgFunctionExpression.cs @@ -29,6 +29,7 @@ using System.Diagnostics; using System.Linq; using System.Linq.Expressions; +using System.Text; using JetBrains.Annotations; using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Sql.Internal; using Npgsql.EntityFrameworkCore.PostgreSQL.Utilities; diff --git a/src/EFCore.PG/Utilities/ClientEvaluationNotSupported.cs b/src/EFCore.PG/Utilities/ClientEvaluationNotSupported.cs deleted file mode 100644 index 8fce1fdbb..000000000 --- a/src/EFCore.PG/Utilities/ClientEvaluationNotSupported.cs +++ /dev/null @@ -1,48 +0,0 @@ -#region License - -// The PostgreSQL License -// -// Copyright (C) 2016 The Npgsql Development Team -// -// Permission to use, copy, modify, and distribute this software and its -// documentation for any purpose, without fee, and without a written -// agreement is hereby granted, provided that the above copyright notice -// and this paragraph and the following two paragraphs appear in all copies. -// -// IN NO EVENT SHALL THE NPGSQL DEVELOPMENT TEAM BE LIABLE TO ANY PARTY -// FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, -// INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS -// DOCUMENTATION, EVEN IF THE NPGSQL DEVELOPMENT TEAM HAS BEEN ADVISED OF -// THE POSSIBILITY OF SUCH DAMAGE. -// -// THE NPGSQL DEVELOPMENT TEAM SPECIFICALLY DISCLAIMS ANY WARRANTIES, -// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY -// AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS -// ON AN "AS IS" BASIS, AND THE NPGSQL DEVELOPMENT TEAM HAS NO OBLIGATIONS -// TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. - -#endregion - -using System; -using System.Runtime.CompilerServices; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Utilities -{ - /// - /// The exception that is thrown when a method intended for SQL translation is evaluated by the client. - /// - public class ClientEvaluationNotSupportedException : NotSupportedException - { - readonly string _callerMemberName; - - /// - public override string Message - => $"{_callerMemberName} is only intended for use via SQL translation as part of an EF Core LINQ query."; - - /// - public ClientEvaluationNotSupportedException([CallerMemberName] string method = default) - { - _callerMemberName = method; - } - } -} diff --git a/test/EFCore.PG.FunctionalTests/Query/ArrayQueryTest.cs b/test/EFCore.PG.FunctionalTests/Query/ArrayQueryTest.cs index a1e79318c..8c37eb4e5 100644 --- a/test/EFCore.PG.FunctionalTests/Query/ArrayQueryTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/ArrayQueryTest.cs @@ -947,50 +947,22 @@ public void List_Exists_equals_with_column_array_element_flipped() #region Support - /// - /// Provides resources for unit tests. - /// ArrayFixture Fixture { get; } - /// - /// Initializes resources for unit tests. - /// - /// The fixture of resources for testing. public ArrayQueryTest(ArrayFixture fixture) { Fixture = fixture; Fixture.TestSqlLoggerFactory.Clear(); } - /// - /// Creates a new . - /// - /// - /// An for testing. - /// ArrayContext CreateContext() => Fixture.CreateContext(); - /// - /// Asserts that the SQL fragment appears in the logs. - /// - /// The SQL statement or fragment to search for in the logs. void AssertContainsInSql(string expected) => Assert.Contains(expected, Fixture.TestSqlLoggerFactory.Sql); - /// - /// Asserts that the SQL fragment does not appear in the logs. - /// - /// The SQL statement or fragment to search for in the logs. void AssertDoesNotContainInSql(string expected) => Assert.DoesNotContain(expected, Fixture.TestSqlLoggerFactory.Sql); - #endregion Support - - #region Fixtures - - /// - /// Represents a database suitable for testing operations with PostgreSQL arrays. - /// public class ArrayContext : DbContext { public DbSet SomeEntities { get; set; } @@ -998,22 +970,18 @@ public ArrayContext(DbContextOptions options) : base(options) {} protected override void OnModelCreating(ModelBuilder builder) {} } - /// - /// Represents an entity suitable for testing operations with PostgreSQL arrays. - /// public class SomeArrayEntity { public int Id { get; set; } public int[] SomeArray { get; set; } - public List SomeList { get; set; } public int[,] SomeMatrix { get; set; } + public List SomeList { get; set; } public byte[] SomeBytea { get; set; } + + // ReSharper disable once UnusedMember.Global public string SomeString { get; set; } } - /// - /// Represents a fixture suitable for testing operations with PostgreSQL arrays. - /// public class ArrayFixture : IDisposable { readonly DbContextOptions _options; @@ -1039,18 +1007,16 @@ public ArrayFixture() Id = 1, SomeArray = new[] { 3, 4 }, SomeBytea = new byte[] { 3, 4 }, - SomeList = new List { 3, 4 }, SomeMatrix = new[,] { { 5, 6 }, { 7, 8 } }, - SomeString = "This_is_a_test" + SomeList = new List { 3, 4 } }); ctx.SomeEntities.Add(new SomeArrayEntity { Id = 2, SomeArray = new[] { 5, 6, 7 }, SomeBytea = new byte[] { 5, 6, 7 }, - SomeList = new List { 5, 6, 7 }, SomeMatrix = new[,] { { 10, 11 }, { 12, 13 } }, - SomeString = "this_is_a_test" + SomeList = new List { 3, 4 } }); ctx.SaveChanges(); } diff --git a/test/EFCore.PG.FunctionalTests/Query/CompiledQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/CompiledQueryNpgsqlTest.cs index b00d841a1..0e49db804 100644 --- a/test/EFCore.PG.FunctionalTests/Query/CompiledQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/CompiledQueryNpgsqlTest.cs @@ -1,55 +1,15 @@ -using System.Linq; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Query; -using Microsoft.EntityFrameworkCore.TestModels.Northwind; +using Microsoft.EntityFrameworkCore.Query; using Microsoft.EntityFrameworkCore.TestUtilities; -using Microsoft.EntityFrameworkCore.TestUtilities.Xunit; -using Xunit; using Xunit.Abstractions; namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query { public class CompiledQueryNpgsqlTest : CompiledQueryTestBase> { - // ReSharper disable once UnusedParameter.Local public CompiledQueryNpgsqlTest(NorthwindQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) : base(fixture) { fixture.TestSqlLoggerFactory.Clear(); } - - [ConditionalFact(Skip = "Throws: Can't write CLR type System.String[] with handler type TextHandler")] - public override void Query_with_array_parameter() - { - var query = EF.CompileQuery( - (NorthwindContext context, string[] args) - => context.Customers.Where(c => c.CustomerID == args[0])); - - using (var context = CreateContext()) - { - var args = new[] { "ALFKI" }; - - // BUG: this passes - var _ = context.Customers.Where(c => c.CustomerID == args[0]).ToList(); - - // BUG: this throws - // System.InvalidCastException : Can't write CLR type System.String[] with handler type TextHandler - var result = query(context, args).First().CustomerID; - - Assert.Equal("ALFKI", result); - } - - using (var context = CreateContext()) - { - Assert.Equal("ANATR", query(context, new[] { "ANATR" }).First().CustomerID); - } - } - - [ConditionalFact(Skip = "Throws: Can't write CLR type System.String[] with handler type TextHandler")] - public override async Task Query_with_array_parameter_async() - { - await base.Query_with_array_parameter_async(); - } } }