提交 b79fd0cf 编写于 作者: S Shay Rojansky

Array operation translation

Array indexing, length, SequenceEqual, Contains

Closes #120
上级 53fe45a1
......@@ -33,6 +33,7 @@
using Microsoft.EntityFrameworkCore.Query;
using Microsoft.EntityFrameworkCore.Query.ExpressionTranslators;
using Microsoft.EntityFrameworkCore.Query.ExpressionTranslators.Internal;
using Microsoft.EntityFrameworkCore.Query.ExpressionVisitors;
using Microsoft.EntityFrameworkCore.Query.Internal;
using Microsoft.EntityFrameworkCore.Query.Sql;
using Microsoft.EntityFrameworkCore.Query.Sql.Internal;
......@@ -104,6 +105,7 @@ public static IServiceCollection AddEntityFrameworkNpgsql([NotNull] this IServic
.TryAdd<IMemberTranslator, NpgsqlCompositeMemberTranslator>()
.TryAdd<ICompositeMethodCallTranslator, NpgsqlCompositeMethodCallTranslator>()
.TryAdd<IQuerySqlGeneratorFactory, NpgsqlQuerySqlGeneratorFactory>()
.TryAdd<ISqlTranslatingExpressionVisitorFactory, NpgsqlSqlTranslatingExpressionVisitorFactory>()
.TryAddProviderSpecificServices(b => b
.TryAddSingleton<INpgsqlValueGeneratorCache, NpgsqlValueGeneratorCache>()
.TryAddScoped<INpgsqlSequenceValueGeneratorFactory, NpgsqlSequenceValueGeneratorFactory>()
......
#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 System.Text.RegularExpressions;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Query.Expressions.Internal;
namespace Microsoft.EntityFrameworkCore.Query.ExpressionTranslators.Internal
{
/// <summary>
/// Translates Enumerable.SequenceEqual on arrays into PostgreSQL array equality operations.
/// </summary>
/// <remarks>
/// https://www.postgresql.org/docs/current/static/functions-array.html
/// </remarks>
public class NpgsqlArraySequenceEqualTranslator : IMethodCallTranslator
{
static readonly MethodInfo SequenceEqualMethodInfo = typeof(Enumerable).GetTypeInfo().GetDeclaredMethods(nameof(Enumerable.SequenceEqual)).Single(m =>
m.IsGenericMethodDefinition &&
m.GetParameters().Length == 2
);
[CanBeNull]
public Expression Translate(MethodCallExpression methodCallExpression)
{
var method = methodCallExpression.Method;
if (method.IsGenericMethod &&
ReferenceEquals(method.GetGenericMethodDefinition(), SequenceEqualMethodInfo) &&
methodCallExpression.Arguments.All(a => a.Type.IsArray))
{
return Expression.MakeBinary(ExpressionType.Equal,
methodCallExpression.Arguments[0],
methodCallExpression.Arguments[1]);
}
return null;
}
}
}
......@@ -28,8 +28,9 @@ namespace Microsoft.EntityFrameworkCore.Query.ExpressionTranslators.Internal
{
public class NpgsqlCompositeMethodCallTranslator : RelationalCompositeMethodCallTranslator
{
private static readonly IMethodCallTranslator[] _methodCallTranslators =
static readonly IMethodCallTranslator[] _methodCallTranslators =
{
new NpgsqlArraySequenceEqualTranslator(),
new NpgsqlConvertTranslator(),
new NpgsqlStringSubstringTranslator(),
new NpgsqlLikeTranslator(),
......@@ -46,7 +47,7 @@ public class NpgsqlCompositeMethodCallTranslator : RelationalCompositeMethodCall
new NpgsqlStringTrimTranslator(),
new NpgsqlStringTrimEndTranslator(),
new NpgsqlStringTrimStartTranslator(),
new NpgsqlRegexIsMatchTranslator(),
new NpgsqlRegexIsMatchTranslator()
};
public NpgsqlCompositeMethodCallTranslator(
......
using System.Linq;
using System.Linq.Expressions;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Query.Expressions;
using Microsoft.EntityFrameworkCore.Query.Expressions.Internal;
using Microsoft.EntityFrameworkCore.Query.ExpressionVisitors.Internal;
using Remotion.Linq.Clauses.Expressions;
using Remotion.Linq.Clauses.ResultOperators;
namespace Microsoft.EntityFrameworkCore.Query.ExpressionVisitors
{
public class NpgsqlSqlTranslatingExpressionVisitor : SqlTranslatingExpressionVisitor
{
private readonly RelationalQueryModelVisitor _queryModelVisitor;
public NpgsqlSqlTranslatingExpressionVisitor(
[NotNull] SqlTranslatingExpressionVisitorDependencies dependencies,
[NotNull] RelationalQueryModelVisitor queryModelVisitor,
[CanBeNull] SelectExpression targetSelectExpression = null,
[CanBeNull] Expression topLevelPredicate = null,
bool inProjection = false)
: base(dependencies, queryModelVisitor, targetSelectExpression, topLevelPredicate, inProjection)
{
_queryModelVisitor = queryModelVisitor;
}
protected override Expression VisitSubQuery(SubQueryExpression expression)
{
// Prefer the default EF Core translation if one exists
var result = base.VisitSubQuery(expression);
if (result != null)
return result;
var subQueryModel = expression.QueryModel;
var fromExpression = subQueryModel.MainFromClause.FromExpression;
var properties = MemberAccessBindingExpressionVisitor.GetPropertyPath(
fromExpression, _queryModelVisitor.QueryCompilationContext, out var qsre);
if (properties.Count == 0)
return null;
var lastPropertyType = properties[properties.Count - 1].ClrType;
if (lastPropertyType.IsArray && lastPropertyType.GetArrayRank() == 1)
{
// Translate someArray.Length
if (subQueryModel.ResultOperators.First() is CountResultOperator)
return Expression.ArrayLength(Visit(fromExpression));
// 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));
}
}
return null;
}
protected override Expression VisitBinary(BinaryExpression expression)
{
if (expression.NodeType == ExpressionType.ArrayIndex)
{
var properties = MemberAccessBindingExpressionVisitor.GetPropertyPath(
expression.Left, _queryModelVisitor.QueryCompilationContext, out var qsre);
if (properties.Count == 0)
return base.VisitBinary(expression);
var lastPropertyType = properties[properties.Count - 1].ClrType;
if (lastPropertyType.IsArray && lastPropertyType.GetArrayRank() == 1)
{
var left = Visit(expression.Left);
var right = Visit(expression.Right);
return left != null && right != null
? Expression.MakeBinary(ExpressionType.ArrayIndex, left, right)
: null;
}
}
return base.VisitBinary(expression);
}
}
}
using System.Linq.Expressions;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Query.Expressions;
using Microsoft.EntityFrameworkCore.Query.ExpressionTranslators;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.EntityFrameworkCore.Utilities;
namespace Microsoft.EntityFrameworkCore.Query.ExpressionVisitors
{
public class NpgsqlSqlTranslatingExpressionVisitorFactory : SqlTranslatingExpressionVisitorFactory
{
/// <summary>
/// Creates a new instance of <see cref="SqlTranslatingExpressionVisitorFactory" />.
/// </summary>
/// <param name="dependencies"> Parameter object containing dependencies for this service. </param>
public NpgsqlSqlTranslatingExpressionVisitorFactory([NotNull] SqlTranslatingExpressionVisitorDependencies dependencies)
: base(dependencies) {}
/// <summary>
/// Creates a new NpgsqlTranslatingExpressionVisitor.
/// </summary>
/// <param name="queryModelVisitor"> The query model visitor. </param>
/// <param name="targetSelectExpression"> The target select expression. </param>
/// <param name="topLevelPredicate"> The top level predicate. </param>
/// <param name="inProjection"> true if we are translating a projection. </param>
/// <returns>
/// A SqlTranslatingExpressionVisitor.
/// </returns>
public override SqlTranslatingExpressionVisitor Create(
RelationalQueryModelVisitor queryModelVisitor,
SelectExpression targetSelectExpression = null,
Expression topLevelPredicate = null,
bool inProjection = false)
=> new NpgsqlSqlTranslatingExpressionVisitor(
Dependencies,
Check.NotNull(queryModelVisitor, nameof(queryModelVisitor)),
targetSelectExpression,
topLevelPredicate,
inProjection);
}
}
#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.Diagnostics;
using System.Linq.Expressions;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Query.Sql.Internal;
using Microsoft.EntityFrameworkCore.Utilities;
namespace Microsoft.EntityFrameworkCore.Query.Expressions.Internal
{
/// <summary>
/// Represents a PostgreSQL ANY expression (e.g. scalar = ANY (array))
/// </summary>
/// <remarks>
/// See https://www.postgresql.org/docs/current/static/functions-comparisons.html
/// </remarks>
public class ArrayAnyExpression : Expression
{
/// <summary>
/// Creates a new instance of InExpression.
/// </summary>
/// <param name="operand"> The operand. </param>
/// <param name="array"> The array. </param>
public ArrayAnyExpression(
[NotNull] Expression operand,
[NotNull] Expression array)
{
Check.NotNull(operand, nameof(operand));
Check.NotNull(array, nameof(array));
Debug.Assert(array.Type.IsArray);
Operand = operand;
Array = array;
}
/// <summary>
/// Gets the operand.
/// </summary>
/// <value>
/// The operand.
/// </value>
public virtual Expression Operand { get; }
/// <summary>
/// Gets the array.
/// </summary>
/// <value>
/// The array.
/// </value>
public virtual Expression Array { get; }
/// <summary>
/// Returns the node type of this <see cref="Expression" />. (Inherited from <see cref="Expression" />.)
/// </summary>
/// <returns>The <see cref="ExpressionType" /> that represents this expression.</returns>
public override ExpressionType NodeType => ExpressionType.Extension;
/// <summary>
/// Gets the static type of the expression that this <see cref="Expression" /> represents. (Inherited from <see cref="Expression" />.)
/// </summary>
/// <returns>The <see cref="Type" /> that represents the static type of the expression.</returns>
public override Type Type => typeof(bool);
/// <summary>
/// Dispatches to the specific visit method for this node type.
/// </summary>
protected override Expression Accept(ExpressionVisitor visitor)
{
Check.NotNull(visitor, nameof(visitor));
return visitor is NpgsqlQuerySqlGenerator npsgqlGenerator
? npsgqlGenerator.VisitArrayAny(this)
: base.Accept(visitor);
}
/// <summary>
/// Reduces the node and then calls the <see cref="ExpressionVisitor.Visit(System.Linq.Expressions.Expression)" /> method passing the
/// reduced expression.
/// Throws an exception if the node isn't reducible.
/// </summary>
/// <param name="visitor"> An instance of <see cref="ExpressionVisitor" />. </param>
/// <returns> The expression being visited, or an expression which should replace it in the tree. </returns>
/// <remarks>
/// Override this method to provide logic to walk the node's children.
/// A typical implementation will call visitor.Visit on each of its
/// children, and if any of them change, should return a new copy of
/// itself with the modified children.
/// </remarks>
///
protected override Expression VisitChildren(ExpressionVisitor visitor)
{
var newOperand = visitor.Visit(Operand);
var newArray = visitor.Visit(Array);
return newOperand != Operand || newArray != Array
? new ArrayAnyExpression(newOperand, newArray)
: this;
}
/// <summary>
/// Tests if this object is considered equal to another.
/// </summary>
/// <param name="obj"> The object to compare with the current object. </param>
/// <returns>
/// true if the objects are considered equal, false if they are not.
/// </returns>
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj))
{
return false;
}
if (ReferenceEquals(this, obj))
{
return true;
}
return obj.GetType() == GetType() && Equals((ArrayAnyExpression)obj);
}
bool Equals(ArrayAnyExpression other)
=> Operand.Equals(other.Operand) && Array.Equals(other.Array);
/// <summary>
/// Returns a hash code for this object.
/// </summary>
/// <returns>
/// A hash code for this object.
/// </returns>
public override int GetHashCode()
{
unchecked
{
return (Operand.GetHashCode() * 397) ^ Array.GetHashCode();
}
}
/// <summary>
/// Creates a <see cref="string" /> representation of the Expression.
/// </summary>
/// <returns>A <see cref="string" /> representation of the Expression.</returns>
public override string ToString() => $"{Operand} = ANY ({Array})";
}
}
......@@ -51,10 +51,8 @@ protected override Expression Accept([NotNull] ExpressionVisitor visitor)
{
Check.NotNull(visitor, nameof(visitor));
var specificVisitor = visitor as NpgsqlQuerySqlGenerator;
return specificVisitor != null
? specificVisitor.VisitAtTimeZone(this)
return visitor is NpgsqlQuerySqlGenerator npgsqlGenerator
? npgsqlGenerator.VisitAtTimeZone(this)
: base.Accept(visitor);
}
......
......@@ -54,7 +54,9 @@ protected override Expression Accept([NotNull] ExpressionVisitor visitor)
{
Check.NotNull(visitor, nameof(visitor));
return (visitor as NpgsqlQuerySqlGenerator)?.VisitRegexMatch(this) ?? base.Accept(visitor);
return visitor is NpgsqlQuerySqlGenerator npsgqlGenerator
? npsgqlGenerator.VisitRegexMatch(this)
: base.Accept(visitor);
}
protected override Expression VisitChildren(ExpressionVisitor visitor)
......
......@@ -22,18 +22,14 @@
#endregion
using System;
using System.Collections.Generic;
using System.Linq;
using System.Diagnostics;
using System.Linq.Expressions;
using System.Reflection;
using System.Text.RegularExpressions;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Internal;
using Microsoft.EntityFrameworkCore.Query.Expressions;
using Microsoft.EntityFrameworkCore.Query.Expressions.Internal;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.EntityFrameworkCore.Utilities;
using Remotion.Linq.Parsing;
namespace Microsoft.EntityFrameworkCore.Query.Sql.Internal
{
......@@ -49,12 +45,12 @@ public class NpgsqlQuerySqlGenerator : DefaultQuerySqlGenerator
{
}
protected override void GenerateTop([NotNull]SelectExpression selectExpression)
protected override void GenerateTop(SelectExpression selectExpression)
{
// No TOP() in PostgreSQL, see GenerateLimitOffset
}
protected override void GenerateLimitOffset([NotNull] SelectExpression selectExpression)
protected override void GenerateLimitOffset(SelectExpression selectExpression)
{
Check.NotNull(selectExpression, nameof(selectExpression));
......@@ -103,24 +99,90 @@ public override Expression VisitSqlFunction(SqlFunctionExpression sqlFunctionExp
return expr;
}
protected override Expression VisitBinary(BinaryExpression binaryExpression)
protected override Expression VisitBinary(BinaryExpression expression)
{
// PostgreSQL 9.4 and below has some weird operator precedence fixed in 9.5 and described here:
// http://git.postgresql.org/gitweb/?p=postgresql.git&a=commitdiff&h=c6b3c939b7e0f1d35f4ed4996e71420a993810d2
// As a result we must surround string concatenation with parentheses
if (binaryExpression.NodeType == ExpressionType.Add &&
binaryExpression.Left.Type == typeof(string) &&
binaryExpression.Right.Type == typeof(string))
switch (expression.NodeType)
{
case ExpressionType.Add:
{
Sql.Append("(");
var exp = base.VisitBinary(binaryExpression);
Sql.Append(")");
return exp;
// PostgreSQL 9.4 and below has some weird operator precedence fixed in 9.5 and described here:
// http://git.postgresql.org/gitweb/?p=postgresql.git&a=commitdiff&h=c6b3c939b7e0f1d35f4ed4996e71420a993810d2
// As a result we must surround string concatenation with parentheses
if (expression.Left.Type == typeof(string) &&
expression.Right.Type == typeof(string))
{
Sql.Append("(");
var exp = base.VisitBinary(expression);
Sql.Append(")");
return exp;
}
break;
}
return base.VisitBinary(binaryExpression);
case ExpressionType.ArrayIndex:
GenerateArrayIndex(expression);
return expression;
}
return base.VisitBinary(expression);
}
protected override Expression VisitUnary(UnaryExpression expression)
{
if (expression.NodeType == ExpressionType.ArrayLength)
{
VisitSqlFunction(new SqlFunctionExpression("array_length", typeof(int), new[] { expression.Operand, Expression.Constant(1) }));
return expression;
}
return base.VisitUnary(expression);
}
void GenerateArrayIndex([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(']');
}
public Expression VisitArrayAny(ArrayAnyExpression arrayAnyExpression)
{
Visit(arrayAnyExpression.Operand);
Sql.Append(" = ANY (");
Visit(arrayAnyExpression.Array);
Sql.Append(")");
return arrayAnyExpression;
}
// PostgreSQL array indexing is 1-based. If the index happens to be a constant,
// just increment it. Otherwise, append a +1 in the SQL.
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)
{
......@@ -158,6 +220,7 @@ public Expression VisitRegexMatch([NotNull] RegexMatchExpression regexMatchExpre
Sql.Append(")' || ");
Visit(regexMatchExpression.Pattern);
Sql.Append(')');
return regexMatchExpression;
}
......@@ -170,6 +233,7 @@ public Expression VisitAtTimeZone([NotNull] AtTimeZoneExpression atTimeZoneExpre
Sql.Append(" AT TIME ZONE '");
Sql.Append(atTimeZoneExpression.TimeZone);
Sql.Append('\'');
return atTimeZoneExpression;
}
......
......@@ -26,6 +26,7 @@
using System.Data;
using System.Data.Common;
using System.Linq;
using System.Text;
using JetBrains.Annotations;
using Npgsql;
using NpgsqlTypes;
......@@ -49,6 +50,23 @@ public override RelationalTypeMapping Clone(string storeType, int? size)
=> new NpgsqlTypeMapping(storeType, ClrType, NpgsqlDbType);
protected override string GenerateNonNullSqlLiteral(object value)
=> throw new NotSupportedException("Can't generate array literals (yet)");
{
// Only support one-dimensional arrays (at least for now)
var arr = (Array)value;
if (arr.Rank != 1)
throw new NotSupportedException("Multidimensional array literals aren't supported yet");
var sb = new StringBuilder();
sb.Append("ARRAY[");
for (var i = 0; i < arr.Length; i++)
{
sb.Append(ElementMapping.GenerateSqlLiteral(arr.GetValue(i)));
if (i < arr.Length - 1)
sb.Append(",");
}
sb.Append("]");
return sb.ToString();
}
}
}
using System;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Utilities;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Xunit;
namespace Microsoft.EntityFrameworkCore.Query
{
public class ArrayQueryTest : IClassFixture<ArrayFixture>
{
[Fact]
public void Roundtrip()
{
using (var ctx = CreateContext())
{
var x = ctx.SomeEntities.Single(e => e.Id == 1);
Assert.Equal(new[] { 3, 4 }, x.SomeArray);
}
}
[Fact]
public void Index_with_constant()
{
using (var ctx = CreateContext())
{
var actual = ctx.SomeEntities.Where(e => e.SomeArray[0] == 3).ToList();
Assert.Equal(1, actual.Count);
AssertContainsInSql(@"WHERE (""e"".""SomeArray""[1]) = 3");
}
}
[Fact]
public void Index_with_non_constant()
{
using (var ctx = CreateContext())
{
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");
}
}
[Fact]
public void 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 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]
public void 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"" = @");
}
}
[Fact]
public void SequenceEqual_with_array_literal()
{
using (var ctx = CreateContext())
{
var x = ctx.SomeEntities.Single(e => e.SomeArray.SequenceEqual(new[] { 3, 4 }));
Assert.Equal(new[] { 3, 4 }, x.SomeArray);
AssertContainsInSql(@"WHERE ""e"".""SomeArray"" = ARRAY[3,4]");
}
}
[Fact]
public void Contains_with_literal()
{
using (var ctx = CreateContext())
{
var x = ctx.SomeEntities.Single(e => e.SomeArray.Contains(3));
Assert.Equal(new[] { 3, 4 }, x.SomeArray);
AssertContainsInSql(@"WHERE 3 = ANY (""e"".""SomeArray"")");
}
}
[Fact]
public void Contains_with_parameter()
{
using (var ctx = CreateContext())
{
var p = 3;
var x = ctx.SomeEntities.Single(e => e.SomeArray.Contains(p));
Assert.Equal(new[] { 3, 4 }, x.SomeArray);
AssertContainsInSql(@"WHERE @__p_0 = ANY (""e"".""SomeArray"")");
}
}
[Fact]
public void Contains_with_column()
{
using (var ctx = CreateContext())
{
var x = ctx.SomeEntities.Single(e => e.SomeArray.Contains(e.Id + 2));
Assert.Equal(new[] { 3, 4 }, x.SomeArray);
AssertContainsInSql(@"WHERE ""e"".""Id"" + 2 = ANY (""e"".""SomeArray"")");
}
}
[Fact]
public void Length()
{
using (var ctx = CreateContext())
{
var x = ctx.SomeEntities.Single(e => e.SomeArray.Length == 2);
Assert.Equal(new[] { 3, 4 }, x.SomeArray);
AssertContainsInSql(@"WHERE array_length(""e"".""SomeArray"", 1) = 2");
}
}
[Fact(Skip="https://github.com/aspnet/EntityFramework/issues/9242")]
public void Length_on_EF_Property()
{
using (var ctx = CreateContext())
{
// TODO: This fails
var x = ctx.SomeEntities.Single(e => EF.Property<int[]>(e, nameof(SomeEntity.SomeArray)).Length == 2);
Assert.Equal(new[] { 3, 4 }, x.SomeArray);
AssertContainsInSql(@"WHERE array_length(""e"".""SomeArray"", 1) = 2");
}
}
[Fact]
public void Length_on_literal_not_translated()
{
using (var ctx = CreateContext())
{
var x = ctx.SomeEntities.Where(e => new[] { 1, 2, 3 }.Length == e.Id).ToList();
AssertDoesNotContainInSql("array_length");
}
}
#region Support
ArrayFixture Fixture { get; }
public ArrayQueryTest(ArrayFixture fixture)
{
Fixture = fixture;
Fixture.TestSqlLoggerFactory.Clear();
}
ArrayContext CreateContext() => Fixture.CreateContext();
void AssertContainsInSql(string expected)
=> Assert.Contains(expected, Fixture.TestSqlLoggerFactory.Sql);
void AssertDoesNotContainInSql(string expected)
=> Assert.DoesNotContain(expected, Fixture.TestSqlLoggerFactory.Sql);
#endregion Support
}
public class ArrayContext : DbContext
{
public DbSet<SomeEntity> SomeEntities { get; set; }
public ArrayContext(DbContextOptions options) : base(options) {}
protected override void OnModelCreating(ModelBuilder builder)
{
}
}
public class SomeEntity
{
public int Id { get; set; }
public int[] SomeArray { get; set; }
public int[,] SomeMatrix { get; set; }
public byte[] SomeBytea { get; set; }
public string SomeText { get; set; }
}
public class ArrayFixture : IDisposable
{
readonly DbContextOptions _options;
public TestSqlLoggerFactory TestSqlLoggerFactory { get; } = new TestSqlLoggerFactory();
public ArrayFixture()
{
_testStore = NpgsqlTestStore.CreateScratch();
_options = new DbContextOptionsBuilder()
.UseNpgsql(_testStore.Connection, b => b.ApplyConfiguration())
.UseInternalServiceProvider(
new ServiceCollection()
.AddEntityFrameworkNpgsql()
.AddSingleton<ILoggerFactory>(TestSqlLoggerFactory)
.BuildServiceProvider())
.Options;
using (var ctx = CreateContext())
{
ctx.Database.EnsureCreated();
ctx.SomeEntities.Add(new SomeEntity
{
Id=1,
SomeArray = new[] { 3, 4 },
SomeBytea = new byte[] { 3, 4 },
SomeMatrix = new[,] { { 5, 6 }, { 7, 8 } }
});
ctx.SomeEntities.Add(new SomeEntity
{
Id=2,
SomeArray = new[] { 5, 6, 7 },
SomeBytea = new byte[] { 5, 6, 7 },
SomeMatrix = new[,] { { 10, 11 }, { 12, 13 } }
});
ctx.SaveChanges();
}
}
readonly NpgsqlTestStore _testStore;
public ArrayContext CreateContext() => new ArrayContext(_options);
public void Dispose() => _testStore.Dispose();
}
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册