Skip to content

Commit

Permalink
Array operation translation
Browse files Browse the repository at this point in the history
Array indexing, length, SequenceEqual, Contains

Closes #120
  • Loading branch information
roji committed Jul 21, 2017
1 parent 53fe45a commit b79fd0c
Show file tree
Hide file tree
Showing 11 changed files with 706 additions and 27 deletions.
2 changes: 2 additions & 0 deletions src/EFCore.PG/Extensions/NpgsqlServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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>()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
#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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand All @@ -46,7 +47,7 @@ public class NpgsqlCompositeMethodCallTranslator : RelationalCompositeMethodCall
new NpgsqlStringTrimTranslator(),
new NpgsqlStringTrimEndTranslator(),
new NpgsqlStringTrimStartTranslator(),
new NpgsqlRegexIsMatchTranslator(),
new NpgsqlRegexIsMatchTranslator()
};

public NpgsqlCompositeMethodCallTranslator(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
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);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
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);
}
}
167 changes: 167 additions & 0 deletions src/EFCore.PG/Query/Expressions/Internal/ArrayAnyExpression.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
#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})";
}
}
Loading

0 comments on commit b79fd0c

Please sign in to comment.