diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArraySequenceEqualTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArraySequenceEqualTranslator.cs index 3fb959ff57..863ac10d14 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/NpgsqlCompositeMemberTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlCompositeMemberTranslator.cs index 0e14630d4a..9fcc5ff6af 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlCompositeMemberTranslator.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlCompositeMemberTranslator.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.Collections.Generic; @@ -28,23 +30,37 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Internal { + /// + /// A composite member translator that dispatches to multiple specialized member translators specific to Npgsql. + /// public class NpgsqlCompositeMemberTranslator : RelationalCompositeMemberTranslator { + /// + /// The default member translators registered by the Npgsql provider. + /// + static readonly IMemberTranslator[] MemberTranslators = + { + new NpgsqlStringLengthTranslator(), + new NpgsqlDateTimeMemberTranslator() + }; + + /// public NpgsqlCompositeMemberTranslator( [NotNull] RelationalCompositeMemberTranslatorDependencies dependencies, [NotNull] INpgsqlOptions npgsqlOptions) : base(dependencies) { - AddTranslators(new List - { - new NpgsqlStringLengthTranslator(), - new NpgsqlDateTimeMemberTranslator() - }); + // ReSharper disable once VirtualMemberCallInConstructor + AddTranslators(MemberTranslators); foreach (var plugin in npgsqlOptions.Plugins) plugin.AddMemberTranslators(this); } + /// + /// 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/ExpressionTranslators/Internal/NpgsqlCompositeMethodCallTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlCompositeMethodCallTranslator.cs index 5935a105d5..ba535f2c66 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlCompositeMethodCallTranslator.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlCompositeMethodCallTranslator.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.Collections.Generic; @@ -28,9 +30,15 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Internal { + /// + /// A composite method call translator that dispatches to multiple specialized method call translators specific to Npgsql. + /// public class NpgsqlCompositeMethodCallTranslator : RelationalCompositeMethodCallTranslator { - static readonly IMethodCallTranslator[] _methodCallTranslators = + /// + /// The default method call translators registered by the Npgsql provider. + /// + static readonly IMethodCallTranslator[] MethodCallTranslators = { new NpgsqlArraySequenceEqualTranslator(), new NpgsqlConvertTranslator(), @@ -52,21 +60,27 @@ public class NpgsqlCompositeMethodCallTranslator : RelationalCompositeMethodCall new NpgsqlStringTrimStartTranslator(), new NpgsqlRegexIsMatchTranslator(), new NpgsqlFullTextSearchMethodTranslator(), - new NpgsqlRangeTranslator() + new NpgsqlRangeTranslator(), + new NpgsqlListTranslator() }; + /// public NpgsqlCompositeMethodCallTranslator( [NotNull] RelationalCompositeMethodCallTranslatorDependencies dependencies, [NotNull] INpgsqlOptions npgsqlOptions) : base(dependencies) { // ReSharper disable once DoNotCallOverridableMethodsInConstructor - AddTranslators(_methodCallTranslators); + AddTranslators(MethodCallTranslators); foreach (var plugin in npgsqlOptions.Plugins) plugin.AddMethodCallTranslators(this); } + /// + /// 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/ExpressionTranslators/Internal/NpgsqlListTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlListTranslator.cs new file mode 100644 index 0000000000..d0797311eb --- /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 9945b5f0af..9fa6322d73 100644 --- a/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlSqlTranslatingExpressionVisitor.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 JetBrains.Annotations; @@ -34,10 +37,14 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionVisitors { + /// + /// The default relational LINQ translating expression visitor for Npgsql. + /// public class NpgsqlSqlTranslatingExpressionVisitor : SqlTranslatingExpressionVisitor { private readonly RelationalQueryModelVisitor _queryModelVisitor; + /// public NpgsqlSqlTranslatingExpressionVisitor( [NotNull] SqlTranslatingExpressionVisitorDependencies dependencies, [NotNull] RelationalQueryModelVisitor queryModelVisitor, @@ -49,6 +56,8 @@ public NpgsqlSqlTranslatingExpressionVisitor( _queryModelVisitor = queryModelVisitor; } + /// + [CanBeNull] protected override Expression VisitSubQuery(SubQueryExpression expression) { // Prefer the default EF Core translation if one exists @@ -65,24 +74,22 @@ protected override Expression VisitSubQuery(SubQueryExpression expression) 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 ArrayAnyExpression(containsItem, Visit(fromExpression)); - } + if (Visit(contains.Item) is Expression containsItem && Visit(fromExpression) is Expression source) + return new ArrayAnyExpression(containsItem, source); } return null; } + /// protected override Expression VisitBinary(BinaryExpression expression) { if (expression.NodeType == ExpressionType.ArrayIndex) @@ -102,6 +109,7 @@ protected override Expression VisitBinary(BinaryExpression expression) : null; } } + return base.VisitBinary(expression); } } diff --git a/src/EFCore.PG/Query/Expressions/Internal/ArrayAnyExpression.cs b/src/EFCore.PG/Query/Expressions/Internal/ArrayAnyExpression.cs index b3826dd7f3..d99a522821 100644 --- a/src/EFCore.PG/Query/Expressions/Internal/ArrayAnyExpression.cs +++ b/src/EFCore.PG/Query/Expressions/Internal/ArrayAnyExpression.cs @@ -1,4 +1,5 @@ #region License + // The PostgreSQL License // // Copyright (C) 2016 The Npgsql Development Team @@ -19,10 +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; -using System.Diagnostics; using System.Linq.Expressions; using JetBrains.Annotations; using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Sql.Internal; @@ -49,7 +50,6 @@ public ArrayAnyExpression( { Check.NotNull(operand, nameof(operand)); Check.NotNull(array, nameof(array)); - Debug.Assert(array.Type.IsArray); Operand = operand; Array = array; diff --git a/src/EFCore.PG/Query/Sql/Internal/NpgsqlQuerySqlGenerator.cs b/src/EFCore.PG/Query/Sql/Internal/NpgsqlQuerySqlGenerator.cs index 2877642382..e447943b97 100644 --- a/src/EFCore.PG/Query/Sql/Internal/NpgsqlQuerySqlGenerator.cs +++ b/src/EFCore.PG/Query/Sql/Internal/NpgsqlQuerySqlGenerator.cs @@ -41,23 +41,27 @@ public class NpgsqlQuerySqlGenerator : DefaultQuerySqlGenerator { readonly bool _reverseNullOrderingEnabled; + /// protected override string TypedTrueLiteral => "TRUE::bool"; + + /// protected override string TypedFalseLiteral => "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)); @@ -80,6 +84,7 @@ protected override void GenerateLimitOffset(SelectExpression selectExpression) } } + /// public override Expression VisitSqlFunction(SqlFunctionExpression sqlFunctionExpression) { var expr = base.VisitSqlFunction(sqlFunctionExpression); @@ -107,6 +112,7 @@ public override Expression VisitSqlFunction(SqlFunctionExpression sqlFunctionExp return expr; } + /// protected override Expression VisitBinary(BinaryExpression expression) { switch (expression.NodeType) @@ -125,19 +131,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) })); @@ -176,6 +186,24 @@ 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; + } + public Expression VisitArrayAny(ArrayAnyExpression arrayAnyExpression) { Visit(arrayAnyExpression.Operand); @@ -283,6 +311,7 @@ public Expression VisitExplicitStoreTypeCast([NotNull] ExplicitStoreTypeCastExpr return castExpression; } + /// protected override string GenerateOperator(Expression expression) { switch (expression.NodeType) @@ -307,6 +336,7 @@ protected override string GenerateOperator(Expression expression) } } + /// protected override void GenerateOrdering(Ordering ordering) { base.GenerateOrdering(ordering); diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlListTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlListTypeMapping.cs index 1f024448f6..866bdeaec9 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 293db92853..180fbd23dd 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 } }