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
}
}