From 4e1820ddb0c3fc5b84b6cb3c85aff2c519a8967e Mon Sep 17 00:00:00 2001 From: Austin Drenski Date: Mon, 14 May 2018 03:30:22 -0400 Subject: [PATCH 1/6] Rebase and manual touchup. Includes work for translating PostgreSQL range operators with extension methods and provides hooks for consumers to include local evaulation implementations. Adds basic tests to verify SQL generation. --- src/EFCore.PG/NpgsqlRangeExtensions.cs | 253 +++++++++ .../NpgsqlCompositeMethodCallTranslator.cs | 3 +- .../Internal/NpgsqlRangeOperatorTranslator.cs | 91 ++++ .../Internal/NpgsqlRangeOperatorExpression.cs | 400 ++++++++++++++ .../Sql/Internal/NpgsqlQuerySqlGenerator.cs | 34 ++ .../EFCore.PG.FunctionalTests.csproj | 2 +- .../Query/RangeQueryNpgsqlFixture.cs | 158 ++++++ .../Query/RangeQueryNpgsqlTest.cs | 514 ++++++++++++++++++ 8 files changed, 1453 insertions(+), 2 deletions(-) create mode 100644 src/EFCore.PG/NpgsqlRangeExtensions.cs create mode 100644 src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRangeOperatorTranslator.cs create mode 100644 src/EFCore.PG/Query/Expressions/Internal/NpgsqlRangeOperatorExpression.cs create mode 100644 test/EFCore.PG.FunctionalTests/Query/RangeQueryNpgsqlFixture.cs create mode 100644 test/EFCore.PG.FunctionalTests/Query/RangeQueryNpgsqlTest.cs diff --git a/src/EFCore.PG/NpgsqlRangeExtensions.cs b/src/EFCore.PG/NpgsqlRangeExtensions.cs new file mode 100644 index 000000000..c5010f430 --- /dev/null +++ b/src/EFCore.PG/NpgsqlRangeExtensions.cs @@ -0,0 +1,253 @@ +#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 Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal; +using NpgsqlTypes; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL +{ + /// + /// Provides extension methods for supporting PostgreSQL translation. + /// + public static class NpgsqlRangeExtensions + { + /// + /// Determines whether a range contains a specified value. + /// + /// + /// The range in which to locate the value. + /// + /// + /// The value to locate in the range. + /// + /// + /// The type of the elements of . + /// + /// + /// true if the range contains the specified value; otherwise, false. + /// + [NpgsqlRangeOperator(NpgsqlRangeOperatorType.Contains)] + public static bool Contains(this NpgsqlRange range, T value) where T : IComparable => throw new NotImplementedException(); + + /// + /// Determines whether a range contains a specified range. + /// + /// + /// The range in which to locate the specified range. + /// + /// + /// The specified range to locate in the range. + /// + /// + /// The type of the elements of . + /// + /// + /// true if the range contains the specified range; otherwise, false. + /// + [NpgsqlRangeOperator(NpgsqlRangeOperatorType.Contains)] + public static bool Contains(this NpgsqlRange a, NpgsqlRange b) where T : IComparable => throw new NotImplementedException(); + + /// + /// Determines whether a range is contained by a specified range. + /// + /// + /// The specified range to locate in the range. + /// + /// + /// The range in which to locate the specified range. + /// + /// + /// The type of the elements of . + /// + /// + /// true if the range contains the specified range; otherwise, false. + /// + [NpgsqlRangeOperator(NpgsqlRangeOperatorType.ContainedBy)] + public static bool ContainedBy(this NpgsqlRange a, NpgsqlRange b) where T : IComparable => b.Contains(a); + + /// + /// Determines whether a range overlaps another range. + /// + /// + /// The first range. + /// + /// + /// The second range. + /// + /// + /// The type of the elements of . + /// + /// + /// true if the ranges overlap (share points in common); otherwise, false. + /// + [NpgsqlRangeOperator(NpgsqlRangeOperatorType.Overlaps)] + public static bool Overlaps(this NpgsqlRange a, NpgsqlRange b) where T : IComparable => throw new NotImplementedException(); + + /// + /// Determines whether a range is strictly to the left of another range. + /// + /// + /// The first range. + /// + /// + /// The second range. + /// + /// + /// The type of the elements of . + /// + /// + /// true if the first range is strictly to the left of the second; otherwise, false. + /// + [NpgsqlRangeOperator(NpgsqlRangeOperatorType.IsStrictlyLeftOf)] + public static bool IsStrictlyLeftOf(this NpgsqlRange a, NpgsqlRange b) where T : IComparable => throw new NotImplementedException(); + + /// + /// Determines whether a range is strictly to the right of another range. + /// + /// + /// The first range. + /// + /// + /// The second range. + /// + /// + /// The type of the elements of . + /// + /// + /// true if the first range is strictly to the right of the second; otherwise, false. + /// + [NpgsqlRangeOperator(NpgsqlRangeOperatorType.IsStrictlyRightOf)] + public static bool IsStrictlyRightOf(this NpgsqlRange a, NpgsqlRange b) where T : IComparable => throw new NotImplementedException(); + + /// + /// Determines whether a range does not extend to the left of another range. + /// + /// + /// The first range. + /// + /// + /// The second range. + /// + /// + /// The type of the elements of . + /// + /// + /// true if the first range does not extend to the left of the second; otherwise, false. + /// + [NpgsqlRangeOperator(NpgsqlRangeOperatorType.DoesNotExtendLeftOf)] + public static bool DoesNotExtendLeftOf(this NpgsqlRange a, NpgsqlRange b) where T : IComparable => throw new NotImplementedException(); + + /// + /// Determines whether a range does not extend to the right of another range. + /// + /// + /// The first range. + /// + /// + /// The second range. + /// + /// + /// The type of the elements of . + /// + /// + /// true if the first range does not extend to the right of the second; otherwise, false. + /// + [NpgsqlRangeOperator(NpgsqlRangeOperatorType.DoesNotExtendRightOf)] + public static bool DoesNotExtendRightOf(this NpgsqlRange a, NpgsqlRange b) where T : IComparable => throw new NotImplementedException(); + + /// + /// Determines whether a range is adjacent to another range. + /// + /// + /// The first range. + /// + /// + /// The second range. + /// + /// + /// The type of the elements of . + /// + /// + /// true if the ranges are adjacent; otherwise, false. + /// + [NpgsqlRangeOperator(NpgsqlRangeOperatorType.IsAdjacentTo)] + public static bool IsAdjacentTo(this NpgsqlRange a, NpgsqlRange b) where T : IComparable => throw new NotImplementedException(); + + /// + /// Returns the set union, which means unique elements that appear in either of two ranges. + /// + /// + /// The first range. + /// + /// + /// The second range. + /// + /// + /// The type of the elements of . + /// + /// + /// The unique elements that appear in either range. + /// + [NpgsqlRangeOperator(NpgsqlRangeOperatorType.Union)] + public static NpgsqlRange Union(this NpgsqlRange a, NpgsqlRange b) where T : IComparable => throw new NotImplementedException(); + + /// + /// Returns the set intersection, which means elements that appear in each of two ranges. + /// + /// + /// The first range. + /// + /// + /// The second range. + /// + /// + /// The type of the elements of . + /// + /// + /// The elements that appear in both ranges. + /// + [NpgsqlRangeOperator(NpgsqlRangeOperatorType.Intersection)] + public static NpgsqlRange Intersect(this NpgsqlRange a, NpgsqlRange b) where T : IComparable => throw new NotImplementedException(); + + /// + /// Returns the set difference, which means the elements of one range that do not appear in a second range. + /// + /// + /// The first range. + /// + /// + /// The second range. + /// + /// + /// The type of the elements of . + /// + /// + /// The elements that appear in the first range, but not the second range. + /// + [NpgsqlRangeOperator(NpgsqlRangeOperatorType.Difference)] + public static NpgsqlRange Except(this NpgsqlRange a, NpgsqlRange b) where T : IComparable => throw new NotImplementedException(); + } +} diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlCompositeMethodCallTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlCompositeMethodCallTranslator.cs index 54feb84ab..b8ff5875c 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlCompositeMethodCallTranslator.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlCompositeMethodCallTranslator.cs @@ -50,7 +50,8 @@ public class NpgsqlCompositeMethodCallTranslator : RelationalCompositeMethodCall new NpgsqlStringTrimEndTranslator(), new NpgsqlStringTrimStartTranslator(), new NpgsqlRegexIsMatchTranslator(), - new NpgsqlFullTextSearchMethodTranslator() + new NpgsqlFullTextSearchMethodTranslator(), + new NpgsqlRangeOperatorTranslator() }; public NpgsqlCompositeMethodCallTranslator( diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRangeOperatorTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRangeOperatorTranslator.cs new file mode 100644 index 000000000..c1d8793fe --- /dev/null +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRangeOperatorTranslator.cs @@ -0,0 +1,91 @@ +#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.Concurrent; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Query.ExpressionTranslators; +using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Internal +{ + /// + /// Translates a method call into a . + /// + public class NpgsqlRangeOperatorTranslator : IMethodCallTranslator + { + /// + /// Maps the generic definitions of the methods supported by this translator to the appropriate PostgreSQL operator. + /// + [NotNull] static readonly ConcurrentDictionary SupportedMethodTranslations = + new ConcurrentDictionary( + typeof(NpgsqlRangeExtensions) + .GetRuntimeMethods() + .Select(x => (MethodInfo: x, Attribute: x.GetCustomAttribute())) + .Where(x => x.Attribute != null) + .ToDictionary(x => x.MethodInfo, x => x.Attribute.OperatorType)); + + /// + [CanBeNull] + public Expression Translate(MethodCallExpression methodCallExpression) => + TryGetSupportedTranslation(methodCallExpression.Method, out NpgsqlRangeOperatorType operatorType) + ? new NpgsqlRangeOperatorExpression(methodCallExpression.Arguments[0], methodCallExpression.Arguments[1], operatorType) + : null; + + /// + /// Tries to find the by looking up the in . + /// If the is not found, but the method is checked for a . + /// If one is found, it is registered with and the is returned. + /// + /// + /// The for lookup. + /// + /// + /// The if successful. + /// + /// + /// True if the was found or registered; otherwise false. + /// + static bool TryGetSupportedTranslation([NotNull] MethodInfo methodInfo, out NpgsqlRangeOperatorType operatorType) + { + MethodInfo info = methodInfo.IsGenericMethod ? methodInfo.GetGenericMethodDefinition() : methodInfo; + + if (SupportedMethodTranslations.TryGetValue(info, out operatorType)) + { + return true; + } + + if (info.GetCustomAttribute() is NpgsqlRangeOperatorAttribute attribute) + { + operatorType = attribute.OperatorType; + return SupportedMethodTranslations.TryAdd(info, operatorType); + } + + return false; + } + } +} diff --git a/src/EFCore.PG/Query/Expressions/Internal/NpgsqlRangeOperatorExpression.cs b/src/EFCore.PG/Query/Expressions/Internal/NpgsqlRangeOperatorExpression.cs new file mode 100644 index 000000000..32bea1747 --- /dev/null +++ b/src/EFCore.PG/Query/Expressions/Internal/NpgsqlRangeOperatorExpression.cs @@ -0,0 +1,400 @@ +#region License + +// The PostgreSQL License +// +// Copyright (C) 2016 The Npgsql Development Team +// +// Permission to use, copy, modify, and distribute this software and its +// documentation for any purpose, without fee, and without a written +// agreement is hereby granted, provided that the above copyright notice +// and this paragraph and the following two paragraphs appear in all copies. +// +// IN NO EVENT SHALL THE NPGSQL DEVELOPMENT TEAM BE LIABLE TO ANY PARTY +// FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, +// INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS +// DOCUMENTATION, EVEN IF THE NPGSQL DEVELOPMENT TEAM HAS BEEN ADVISED OF +// THE POSSIBILITY OF SUCH DAMAGE. +// +// THE NPGSQL DEVELOPMENT TEAM SPECIFICALLY DISCLAIMS ANY WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS +// ON AN "AS IS" BASIS, AND THE NPGSQL DEVELOPMENT TEAM HAS NO OBLIGATIONS +// TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + +#endregion + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using JetBrains.Annotations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Sql.Internal; +using Npgsql.EntityFrameworkCore.PostgreSQL.Utilities; +using NpgsqlTypes; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal +{ + /// + /// Represents a PostgreSQL range operator. + /// + /// + /// See https://www.postgresql.org/docs/current/static/functions-range.html + /// + public class NpgsqlRangeOperatorExpression : Expression + { + /// + /// Maps standard binary operators from to . + /// + [NotNull] static readonly Dictionary ExpressionOperators = + new Dictionary + { + [ExpressionType.Equal] = NpgsqlRangeOperatorType.Equal, + [ExpressionType.NotEqual] = NpgsqlRangeOperatorType.NotEqual, + [ExpressionType.LessThan] = NpgsqlRangeOperatorType.LessThan, + [ExpressionType.GreaterThan] = NpgsqlRangeOperatorType.GreaterThan, + [ExpressionType.LessThanOrEqual] = NpgsqlRangeOperatorType.LessThanOrEqual, + [ExpressionType.GreaterThanOrEqual] = NpgsqlRangeOperatorType.GreaterThanOrEqual + }; + + /// + /// Maps the to a PostgreSQL operator symbol. + /// + [NotNull] static readonly Dictionary OperatorSymbols; + + /// + /// Maps the to a return type. + /// + [NotNull] static readonly Dictionary OperatorReturnTypes; + + /// + /// Initializes instances of found on . + /// + static NpgsqlRangeOperatorExpression() + { + (NpgsqlRangeOperatorType Enum, NpgsqlOperatorAttribute Attribute)[] map = + typeof(NpgsqlRangeOperatorType) + .GetEnumValues() + .Cast() + .Select(x => (Enum: x, Attribute: GetAttribute(x))) + .ToArray(); + + OperatorSymbols = map.ToDictionary(x => x.Enum, x => x.Attribute.Symbol); + OperatorReturnTypes = map.ToDictionary(x => x.Enum, x => x.Attribute.ReturnType); + + NpgsqlOperatorAttribute GetAttribute(NpgsqlRangeOperatorType operatorType) => + typeof(NpgsqlRangeOperatorType) + .GetMember(operatorType.ToString()) + .Single() + .GetCustomAttribute(); + } + + /// + /// The generic type definition for . + /// + [NotNull] static readonly Type NpgsqlRangeType = typeof(NpgsqlRange<>); + + /// + public override ExpressionType NodeType { get; } = ExpressionType.Extension; + + /// + public override Type Type { get; } + + /// + /// Gets the item to the left of the operator. + /// + public virtual Expression Left { get; } + + /// + /// Gets the item to the right of the operator. + /// + public virtual Expression Right { get; } + + /// + /// The operator. + /// + public virtual NpgsqlRangeOperatorType Operator { get; } + + /// + /// The operator symbol. + /// + [NotNull] + public virtual string OperatorSymbol => OperatorSymbols[Operator]; + + /// + /// Creates a new instance of . + /// + /// + /// The item to the left of the operator. + /// + /// + /// The item to the right of the operator. + /// + /// + /// The type of range operation. + /// + public NpgsqlRangeOperatorExpression([NotNull] Expression left, [NotNull] Expression right, NpgsqlRangeOperatorType operatorType) + { + Check.NotNull(left, nameof(left)); + Check.NotNull(right, nameof(right)); + + Left = left; + Right = right; + Operator = operatorType; + + // TODO: will the left type arguments always be valid? + Type type = OperatorReturnTypes[operatorType]; + Type = type.IsGenericType ? type.MakeGenericType(left.Type.GetGenericArguments()) : type; + } + + /// + protected override Expression Accept(ExpressionVisitor visitor) + { + Check.NotNull(visitor, nameof(visitor)); + + return + visitor is NpgsqlQuerySqlGenerator npsgqlGenerator + ? npsgqlGenerator.VisitRangeOperator(this) + : base.Accept(visitor); + } + + /// + protected override Expression VisitChildren(ExpressionVisitor visitor) + { + if (!(visitor.Visit(Left) is Expression newLeft)) + throw new ArgumentNullException(nameof(newLeft)); + + if (!(visitor.Visit(Right) is Expression newRight)) + throw new ArgumentNullException(nameof(newRight)); + + return + Left == newLeft && Right == newRight + ? this + : new NpgsqlRangeOperatorExpression(newLeft, newRight, Operator); + } + + /// + /// Returns a if applicable. + /// + /// + /// This returns a NpgsqlRangeOperatorExpression IFF: + /// 1. Both left and right are + /// 2. The expression node type is one of: + /// - Equal + /// - NotEqual + /// - LessThan + /// - GreaterThan + /// - LessThanOrEqual + /// - GreaterThanOrEqual + /// + /// + /// The binary expression to test. + /// + /// + /// A or null. + /// + [CanBeNull] + public static NpgsqlRangeOperatorExpression TryVisitBinary([NotNull] BinaryExpression expression) + { + Check.NotNull(expression, nameof(expression)); + + Type leftType = expression.Left.Type; + Type rightType = expression.Right.Type; + + if (!leftType.IsGenericType || leftType.GetGenericTypeDefinition() != NpgsqlRangeType) + return null; + if (!rightType.IsGenericType || rightType.GetGenericTypeDefinition() != NpgsqlRangeType) + return null; + + return + ExpressionOperators.TryGetValue(expression.NodeType, out NpgsqlRangeOperatorType operatorType) + ? new NpgsqlRangeOperatorExpression(expression.Left, expression.Right, operatorType) + : null; + } + + /// + public override string ToString() => $"{Left} {OperatorSymbol} {Right}"; + + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + return false; + if (ReferenceEquals(this, obj)) + return true; + if (!(obj is NpgsqlRangeOperatorExpression other)) + return false; + + return + Equals(Left, other.Left) && + Equals(Right, other.Right) && + NodeType == other.NodeType && + Operator == other.Operator && + Type == other.Type; + } + + /// + public override int GetHashCode() + { + unchecked + { + int hashCode = Left?.GetHashCode() ?? 0; + hashCode = (hashCode * 397) ^ (Right?.GetHashCode() ?? 0); + hashCode = (hashCode * 397) ^ (int)NodeType; + hashCode = (hashCode * 397) ^ (int)Operator; + hashCode = (hashCode * 397) ^ Type.GetHashCode(); + return hashCode; + } + } + } + + /// + /// Indicates that a method can be translated to a PostgreSQL range operator. + /// + /// + /// This attribute allows other extension methods to hook into the range operator translations. + /// Along with simplifying the code required to identify overloaded generics, this attribute provides + /// a transparent way in which to transition from extension methods in the EF Core assembly to + /// instance methods on . + /// + [AttributeUsage(AttributeTargets.Method, Inherited = false)] + public class NpgsqlRangeOperatorAttribute : Attribute + { + /// + /// The operator represented by the method. + /// + public NpgsqlRangeOperatorType OperatorType { get; } + + /// + /// Indicates that a method can be translated to a PostgreSQL range operator. + /// + /// + /// The type of operator the method represents. + /// + public NpgsqlRangeOperatorAttribute(NpgsqlRangeOperatorType operatorType) + { + OperatorType = operatorType; + } + } + + /// + /// Represents information about a PostgreSQL operator. + /// + /// + /// This attribute should be applied to the members of an enum to record metadata for database translation. + /// + [AttributeUsage(AttributeTargets.Field)] + public class NpgsqlOperatorAttribute : Attribute + { + /// + /// The operator symbol. + /// + [NotNull] + public string Symbol { get; set; } = string.Empty; + + /// + /// The operator represented by the method. + /// + [NotNull] + public Type ReturnType { get; set; } = typeof(void); + } + + /// + /// Describes the operator type of a range expression. + /// + /// + /// See: https://www.postgresql.org/docs/current/static/functions-range.html + /// + public enum NpgsqlRangeOperatorType + { + /// + /// No operator specified. + /// + [NpgsqlOperator] None, + + /// + /// The = operator. + /// + [NpgsqlOperator(Symbol = "=", ReturnType = typeof(bool))] Equal, + + /// + /// The <> operator. + /// + [NpgsqlOperator(Symbol = "<>", ReturnType = typeof(bool))] NotEqual, + + /// + /// The < operator. + /// + [NpgsqlOperator(Symbol = "<", ReturnType = typeof(bool))] LessThan, + + /// + /// The > operator. + /// + [NpgsqlOperator(Symbol = ">", ReturnType = typeof(bool))] GreaterThan, + + /// + /// The <= operator. + /// + [NpgsqlOperator(Symbol = "<=", ReturnType = typeof(bool))] LessThanOrEqual, + + /// + /// The >= operator. + /// + [NpgsqlOperator(Symbol = ">=", ReturnType = typeof(bool))] GreaterThanOrEqual, + + /// + /// The @> operator. + /// + [NpgsqlOperator(Symbol = "@>", ReturnType = typeof(bool))] Contains, + + /// + /// The <@ operator. + /// + [NpgsqlOperator(Symbol = "<@", ReturnType = typeof(bool))] ContainedBy, + + /// + /// The && operator. + /// + [NpgsqlOperator(Symbol = "&&", ReturnType = typeof(bool))] Overlaps, + + /// + /// The << operator. + /// + [NpgsqlOperator(Symbol = "<<", ReturnType = typeof(bool))] IsStrictlyLeftOf, + + /// + /// The >> operator. + /// + [NpgsqlOperator(Symbol = ">>", ReturnType = typeof(bool))] IsStrictlyRightOf, + + /// + /// The &< operator. + /// + [NpgsqlOperator(Symbol = "&<", ReturnType = typeof(bool))] DoesNotExtendRightOf, + + /// + /// The &> operator. + /// + [NpgsqlOperator(Symbol = "&>", ReturnType = typeof(bool))] DoesNotExtendLeftOf, + + /// + /// The -|- operator. + /// + [NpgsqlOperator(Symbol = "-|-", ReturnType = typeof(bool))] IsAdjacentTo, + + /// + /// The + operator. + /// + [NpgsqlOperator(Symbol = "+", ReturnType = typeof(NpgsqlRange<>))] Union, + + /// + /// The * operator. + /// + [NpgsqlOperator(Symbol = "*", ReturnType = typeof(NpgsqlRange<>))] Intersection, + + /// + /// The - operator. + /// + [NpgsqlOperator(Symbol = "-", ReturnType = typeof(NpgsqlRange<>))] Difference + } +} diff --git a/src/EFCore.PG/Query/Sql/Internal/NpgsqlQuerySqlGenerator.cs b/src/EFCore.PG/Query/Sql/Internal/NpgsqlQuerySqlGenerator.cs index 6aa8a5704..60c16e7b4 100644 --- a/src/EFCore.PG/Query/Sql/Internal/NpgsqlQuerySqlGenerator.cs +++ b/src/EFCore.PG/Query/Sql/Internal/NpgsqlQuerySqlGenerator.cs @@ -109,6 +109,23 @@ public override Expression VisitSqlFunction(SqlFunctionExpression sqlFunctionExp protected override Expression VisitBinary(BinaryExpression expression) { + // We need to intercept some of the standard operators for NpgsqlRange. + // This returns a NpgsqlRangeOperatorExpression if both conditions are met: + // 1. Both left and right are NpgsqlRange. + // 2. The expression node type is one of: + // - Equal + // - NotEqual + // - LessThan + // - GreaterThan + // - LessThanOrEqual + // - GreaterThanOrEqual + // + // Otherwise, this is null and the method should continue. + NpgsqlRangeOperatorExpression rangeOperatorExpression = NpgsqlRangeOperatorExpression.TryVisitBinary(expression); + + if (rangeOperatorExpression != null) + return VisitRangeOperator(rangeOperatorExpression); + switch (expression.NodeType) { case ExpressionType.Add: @@ -184,6 +201,23 @@ public Expression VisitArrayAny(ArrayAnyExpression arrayAnyExpression) return arrayAnyExpression; } + /// + /// Visits a and generates the operator symbols. + /// + /// + /// The expression to visit. + /// + /// + /// The visited expression. + /// + public Expression VisitRangeOperator(NpgsqlRangeOperatorExpression rangeOperatorExpression) + { + Visit(rangeOperatorExpression.Left); + Sql.Append($" {rangeOperatorExpression.OperatorSymbol} "); + Visit(rangeOperatorExpression.Right); + return rangeOperatorExpression; + } + // 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) diff --git a/test/EFCore.PG.FunctionalTests/EFCore.PG.FunctionalTests.csproj b/test/EFCore.PG.FunctionalTests/EFCore.PG.FunctionalTests.csproj index 050106099..e6f2d8420 100644 --- a/test/EFCore.PG.FunctionalTests/EFCore.PG.FunctionalTests.csproj +++ b/test/EFCore.PG.FunctionalTests/EFCore.PG.FunctionalTests.csproj @@ -26,4 +26,4 @@ - + \ No newline at end of file diff --git a/test/EFCore.PG.FunctionalTests/Query/RangeQueryNpgsqlFixture.cs b/test/EFCore.PG.FunctionalTests/Query/RangeQueryNpgsqlFixture.cs new file mode 100644 index 000000000..9ee91b1e8 --- /dev/null +++ b/test/EFCore.PG.FunctionalTests/Query/RangeQueryNpgsqlFixture.cs @@ -0,0 +1,158 @@ +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.TestUtilities; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities; +using NpgsqlTypes; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query +{ + /// + /// Represents a fixture suitable for testing range operators. + /// + public class RangeQueryNpgsqlFixture : IDisposable + { + /// + /// The used for testing. + /// + private readonly NpgsqlTestStore _testStore; + + /// + /// The used for testing. + /// + private readonly DbContextOptions _options; + + /// + /// The logger factory used for testing. + /// + public TestSqlLoggerFactory TestSqlLoggerFactory { get; } + + /// + /// Initializes a . + /// + // ReSharper disable once UnusedMember.Global + public RangeQueryNpgsqlFixture() + { + TestSqlLoggerFactory = new TestSqlLoggerFactory(); + + _testStore = NpgsqlTestStore.CreateScratch(); + + _options = + new DbContextOptionsBuilder() + .UseNpgsql(_testStore.ConnectionString, b => b.ApplyConfiguration()) + .UseInternalServiceProvider( + new ServiceCollection() + .AddEntityFrameworkNpgsql() + .AddSingleton(TestSqlLoggerFactory) + .BuildServiceProvider()) + .Options; + + using (RangeContext context = CreateContext()) + { + context.Database.EnsureCreated(); + + context.RangeTestEntities.AddRange( + new RangeTestEntity + { + Id = 1, + // (0, 10) + Range = new NpgsqlRange(0, false, false, 10, false, false), + }, + new RangeTestEntity + { + Id = 2, + // [0, 10) + Range = new NpgsqlRange(0, true, false, 10, false, false) + }, + new RangeTestEntity + { + Id = 3, + // [0, 10] + Range = new NpgsqlRange(0, true, false, 10, true, false) + }, + new RangeTestEntity + { + Id = 4, + // [0, ∞) + Range = new NpgsqlRange(0, true, false, 0, false, true) + }, + new RangeTestEntity + { + Id = 5, + // (-∞, 10] + Range = new NpgsqlRange(0, false, true, 10, true, false) + }, + new RangeTestEntity + { + Id = 6, + // (-∞, ∞) + Range = new NpgsqlRange(0, false, true, 0, false, true) + }, + new RangeTestEntity + { + Id = 7, + // (-∞, ∞) + Range = new NpgsqlRange(0, false, true, 0, false, true) + }); + + context.SaveChanges(); + } + } + + /// + /// Creates a new . + /// + /// + /// A for testing. + /// + public RangeContext CreateContext() + { + return new RangeContext(_options); + } + + /// + public void Dispose() + { + _testStore.Dispose(); + } + } + + /// + /// Represents an entity suitable for testing range operators. + /// + public class RangeTestEntity + { + /// + /// The primary key. + /// + public int Id { get; set; } + + /// + /// The range of integers. + /// + public NpgsqlRange Range { get; set; } + } + + /// + /// Represents a database suitable for testing range operators. + /// + public class RangeContext : DbContext + { + /// + /// Represents a set of entities with properties. + /// + public DbSet RangeTestEntities { get; set; } + + /// + /// Initializes a . + /// + /// + /// The options to be used for configuration. + /// + public RangeContext(DbContextOptions options) : base(options) { } + + /// + protected override void OnModelCreating(ModelBuilder builder) { } + } +} diff --git a/test/EFCore.PG.FunctionalTests/Query/RangeQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/RangeQueryNpgsqlTest.cs new file mode 100644 index 000000000..cc359e759 --- /dev/null +++ b/test/EFCore.PG.FunctionalTests/Query/RangeQueryNpgsqlTest.cs @@ -0,0 +1,514 @@ +using System.Collections.Generic; +using System.Linq; +using NpgsqlTypes; +using Xunit; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query +{ + /// + /// Provides unit tests for range operator translations. + /// + public class RangeQueryNpgsqlTest : IClassFixture + { + /// + /// Provides resources for unit tests. + /// + RangeQueryNpgsqlFixture Fixture { get; } + + /// + /// Provides theory data for integers. + /// + public static IEnumerable IntegerTheoryData => Enumerable.Range(-10, 10).Select(x => new object[] { x }); + + /// + /// Provides theory data for ranges. + /// + public static IEnumerable RangeTheoryData => + new List + { + // (0,5) + new object[] { new NpgsqlRange(0, false, false, 5, false, false) }, + // [0,5] + new object[] { new NpgsqlRange(0, true, false, 5, true, false) }, + // (,) + new object[] { new NpgsqlRange(0, false, true, 0, false, true) }, + // (,) + new object[] { new NpgsqlRange(0, false, true, 5, false, true) }, + // (0,) + new object[] { new NpgsqlRange(0, false, false, 0, false, true) }, + // (0,) + new object[] { new NpgsqlRange(0, false, false, 5, false, true) }, + // (,5) + new object[] { new NpgsqlRange(0, false, true, 5, false, false) } + }; + + /// + /// Initializes resources for unit tests. + /// + /// + /// The fixture of resources for testing. + /// + public RangeQueryNpgsqlTest(RangeQueryNpgsqlFixture fixture) + { + Fixture = fixture; + Fixture.TestSqlLoggerFactory.Clear(); + } + + /// + /// Tests translation for . + /// + [Theory] + [MemberData(nameof(RangeTheoryData))] + public void RangeContainsRange(NpgsqlRange range) + { + using (RangeContext context = Fixture.CreateContext()) + { + RangeTestEntity[] _ = + context.RangeTestEntities + .Where(x => x.Range.Contains(range)) + .ToArray(); + + Assert.Contains("WHERE x.\"Range\" @> @__range_0 = TRUE", Fixture.TestSqlLoggerFactory.Sql); + } + } + + /// + /// Tests inverse translation for . + /// + [Theory] + [MemberData(nameof(RangeTheoryData))] + public void RangeDoesNotContainRange(NpgsqlRange range) + { + using (RangeContext context = Fixture.CreateContext()) + { + RangeTestEntity[] _ = + context.RangeTestEntities + .Where(x => !x.Range.Contains(range)) + .ToArray(); + + Assert.Contains("WHERE NOT (x.\"Range\" @> @__range_0 = TRUE)", Fixture.TestSqlLoggerFactory.Sql); + } + } + + /// + /// Tests translation for . + /// + [Theory] + [MemberData(nameof(IntegerTheoryData))] + public void RangeContainsValue(int value) + { + using (RangeContext context = Fixture.CreateContext()) + { + RangeTestEntity[] _ = + context.RangeTestEntities + .Where(x => x.Range.Contains(value)) + .ToArray(); + + Assert.Contains("WHERE x.\"Range\" @> @__value_0", Fixture.TestSqlLoggerFactory.Sql); + } + } + + /// + /// Tests inverse translation for . + /// + [Theory] + [MemberData(nameof(IntegerTheoryData))] + public void RangeDoesNotContainValue(int value) + { + using (RangeContext context = Fixture.CreateContext()) + { + RangeTestEntity[] _ = + context.RangeTestEntities + .Where(x => !x.Range.Contains(value)) + .ToArray(); + + Assert.Contains("WHERE NOT (x.\"Range\" @> @__value_0 = TRUE)", Fixture.TestSqlLoggerFactory.Sql); + } + } + + /// + /// Tests translation for . + /// + [Theory] + [MemberData(nameof(RangeTheoryData))] + public void RangeContainedByRange(NpgsqlRange range) + { + using (RangeContext context = Fixture.CreateContext()) + { + RangeTestEntity[] _ = + context.RangeTestEntities + .Where(x => range.ContainedBy(x.Range)) + .ToArray(); + + Assert.Contains("WHERE @__range_0 <@ x.\"Range\" = TRUE", Fixture.TestSqlLoggerFactory.Sql); + } + } + + /// + /// Tests inverse containment translation for . + /// + [Theory] + [MemberData(nameof(RangeTheoryData))] + public void RangeNotContainedByRange(NpgsqlRange range) + { + using (RangeContext context = Fixture.CreateContext()) + { + RangeTestEntity[] _ = + context.RangeTestEntities + .Where(x => !range.ContainedBy(x.Range)) + .ToArray(); + + Assert.Contains("WHERE NOT (@__range_0 <@ x.\"Range\" = TRUE)", Fixture.TestSqlLoggerFactory.Sql); + } + } + + /// + /// Tests translation for via the symbolic operator. + /// + [Theory] + [MemberData(nameof(RangeTheoryData))] + public void RangeEqualsRange_Operator(NpgsqlRange range) + { + using (RangeContext context = Fixture.CreateContext()) + { + RangeTestEntity[] _ = + context.RangeTestEntities + .Where(x => x.Range == range) + .ToArray(); + + Assert.Contains("WHERE x.\"Range\" = @__range_0", Fixture.TestSqlLoggerFactory.Sql); + } + } + + /// + /// Tests translation for via method call. + /// + [Theory] + [MemberData(nameof(RangeTheoryData))] + public void RangeEqualsRange_Method(NpgsqlRange range) + { + using (RangeContext context = Fixture.CreateContext()) + { + RangeTestEntity[] _ = + context.RangeTestEntities + .Where(x => x.Range.Equals(range)) + .ToArray(); + + Assert.Contains("WHERE x.\"Range\" = @__range_0", Fixture.TestSqlLoggerFactory.Sql); + } + } + + /// + /// Tests inverse translation for via symbolic operator. + /// + [Theory] + [MemberData(nameof(RangeTheoryData))] + public void RangeDoesNotEqualsRange_Operator(NpgsqlRange range) + { + using (RangeContext context = Fixture.CreateContext()) + { + RangeTestEntity[] _ = + context.RangeTestEntities + .Where(x => x.Range != range) + .ToArray(); + + Assert.Contains("WHERE x.\"Range\" <> @__range_0", Fixture.TestSqlLoggerFactory.Sql); + } + } + + /// + /// Tests inverse translation for via method call. + /// + [Theory] + [MemberData(nameof(RangeTheoryData))] + public void RangeDoesNotEqualsRange_Method(NpgsqlRange range) + { + using (RangeContext context = Fixture.CreateContext()) + { + RangeTestEntity[] _ = + context.RangeTestEntities + .Where(x => !x.Range.Equals(range)) + .ToArray(); + + Assert.Contains("WHERE x.\"Range\" <> @__range_0", Fixture.TestSqlLoggerFactory.Sql); + } + } + + /// + /// Tests translation for . + /// + [Theory] + [MemberData(nameof(RangeTheoryData))] + public void RangeOvelapsRange(NpgsqlRange range) + { + using (RangeContext context = Fixture.CreateContext()) + { + RangeTestEntity[] _ = + context.RangeTestEntities + .Where(x => x.Range.Overlaps(range)) + .ToArray(); + + Assert.Contains("WHERE x.\"Range\" && @__range_0", Fixture.TestSqlLoggerFactory.Sql); + } + } + + /// + /// Tests inverse translation for . + /// + [Theory] + [MemberData(nameof(RangeTheoryData))] + public void RangeDoesNotOvelapRange(NpgsqlRange range) + { + using (RangeContext context = Fixture.CreateContext()) + { + RangeTestEntity[] _ = + context.RangeTestEntities + .Where(x => !x.Range.Overlaps(range)) + .ToArray(); + + Assert.Contains("WHERE NOT (x.\"Range\" && @__range_0 = TRUE)", Fixture.TestSqlLoggerFactory.Sql); + } + } + + /// + /// Tests translation for . + /// + [Theory] + [MemberData(nameof(RangeTheoryData))] + public void RangeIsStrictlyLeftOfRange(NpgsqlRange range) + { + using (RangeContext context = Fixture.CreateContext()) + { + RangeTestEntity[] _ = + context.RangeTestEntities + .Where(x => x.Range.IsStrictlyLeftOf(range)) + .ToArray(); + + Assert.Contains("WHERE x.\"Range\" << @__range_0", Fixture.TestSqlLoggerFactory.Sql); + } + } + + /// + /// Tests inverse translation for . + /// + [Theory] + [MemberData(nameof(RangeTheoryData))] + public void RangeIsNotStrictlyLeftOfRange(NpgsqlRange range) + { + using (RangeContext context = Fixture.CreateContext()) + { + RangeTestEntity[] _ = + context.RangeTestEntities + .Where(x => !x.Range.IsStrictlyLeftOf(range)) + .ToArray(); + + Assert.Contains("WHERE NOT (x.\"Range\" << @__range_0 = TRUE)", Fixture.TestSqlLoggerFactory.Sql); + } + } + + /// + /// Tests translation for . + /// + [Theory] + [MemberData(nameof(RangeTheoryData))] + public void RangeIsStrictlyRightOfRange(NpgsqlRange range) + { + using (RangeContext context = Fixture.CreateContext()) + { + RangeTestEntity[] _ = + context.RangeTestEntities + .Where(x => x.Range.IsStrictlyRightOf(range)) + .ToArray(); + + Assert.Contains("WHERE x.\"Range\" >> @__range_0", Fixture.TestSqlLoggerFactory.Sql); + } + } + + /// + /// Tests inverse translation for . + /// + [Theory] + [MemberData(nameof(RangeTheoryData))] + public void RangeIsNotStrictlyRightOfRange(NpgsqlRange range) + { + using (RangeContext context = Fixture.CreateContext()) + { + RangeTestEntity[] _ = + context.RangeTestEntities + .Where(x => !x.Range.IsStrictlyRightOf(range)) + .ToArray(); + + Assert.Contains("WHERE NOT (x.\"Range\" >> @__range_0 = TRUE)", Fixture.TestSqlLoggerFactory.Sql); + } + } + + /// + /// Tests translation for . + /// + [Theory] + [MemberData(nameof(RangeTheoryData))] + public void RangeDoesNotExtendLeftOfRange(NpgsqlRange range) + { + using (RangeContext context = Fixture.CreateContext()) + { + RangeTestEntity[] _ = + context.RangeTestEntities + .Where(x => x.Range.DoesNotExtendLeftOf(range)) + .ToArray(); + + Assert.Contains("WHERE x.\"Range\" &> @__range_0", Fixture.TestSqlLoggerFactory.Sql); + } + } + + /// + /// Tests inverse translation for . + /// + [Theory] + [MemberData(nameof(RangeTheoryData))] + public void RangeDoesExtendLeftOfRange(NpgsqlRange range) + { + using (RangeContext context = Fixture.CreateContext()) + { + RangeTestEntity[] _ = + context.RangeTestEntities + .Where(x => !x.Range.DoesNotExtendLeftOf(range)) + .ToArray(); + + Assert.Contains("WHERE NOT (x.\"Range\" &> @__range_0 = TRUE)", Fixture.TestSqlLoggerFactory.Sql); + } + } + + /// + /// Tests translation for . + /// + [Theory] + [MemberData(nameof(RangeTheoryData))] + public void RangeDoesNotExtendRightOfRange(NpgsqlRange range) + { + using (RangeContext context = Fixture.CreateContext()) + { + RangeTestEntity[] _ = + context.RangeTestEntities + .Where(x => x.Range.DoesNotExtendRightOf(range)) + .ToArray(); + + Assert.Contains("WHERE x.\"Range\" &< @__range_0", Fixture.TestSqlLoggerFactory.Sql); + } + } + + /// + /// Tests inverse translation for . + /// + [Theory] + [MemberData(nameof(RangeTheoryData))] + public void RangeDoesExtendRightOfRange(NpgsqlRange range) + { + using (RangeContext context = Fixture.CreateContext()) + { + RangeTestEntity[] _ = + context.RangeTestEntities + .Where(x => !x.Range.DoesNotExtendRightOf(range)) + .ToArray(); + + Assert.Contains("WHERE NOT (x.\"Range\" &< @__range_0 = TRUE)", Fixture.TestSqlLoggerFactory.Sql); + } + } + + /// + /// Tests translation for . + /// + [Theory] + [MemberData(nameof(RangeTheoryData))] + public void RangeIsAdjacentToRange(NpgsqlRange range) + { + using (RangeContext context = Fixture.CreateContext()) + { + RangeTestEntity[] _ = + context.RangeTestEntities + .Where(x => x.Range.IsAdjacentTo(range)) + .ToArray(); + + Assert.Contains("WHERE x.\"Range\" -|- @__range_0", Fixture.TestSqlLoggerFactory.Sql); + } + } + + /// + /// Tests inverse translation for . + /// + [Theory] + [MemberData(nameof(RangeTheoryData))] + public void RangeIsNotAdjacentToRange(NpgsqlRange range) + { + using (RangeContext context = Fixture.CreateContext()) + { + RangeTestEntity[] _ = + context.RangeTestEntities + .Where(x => !x.Range.IsAdjacentTo(range)) + .ToArray(); + + Assert.Contains("WHERE NOT (x.\"Range\" -|- @__range_0 = TRUE)", Fixture.TestSqlLoggerFactory.Sql); + } + } + + /// + /// Tests translation for . + /// + [Theory] + [MemberData(nameof(RangeTheoryData))] + public void RangeUnionRange(NpgsqlRange range) + { + using (RangeContext context = Fixture.CreateContext()) + { + NpgsqlRange[] _ = + context.RangeTestEntities + .Select(x => x.Range.Union(range)) + .ToArray(); + + Assert.Contains("SELECT x.\"Range\" + @__range_0", Fixture.TestSqlLoggerFactory.Sql); + } + } + + /// + /// Tests translation for . + /// + [Theory] + [MemberData(nameof(RangeTheoryData))] + public void RangeIntersectsRange(NpgsqlRange range) + { + using (RangeContext context = Fixture.CreateContext()) + { + NpgsqlRange[] _ = + context.RangeTestEntities + .Select(x => x.Range.Intersect(range)) + .ToArray(); + + Assert.Contains("SELECT x.\"Range\" * @__range_0", Fixture.TestSqlLoggerFactory.Sql); + } + } + + /// + /// Tests translation for . + /// + [Theory] + [MemberData(nameof(RangeTheoryData))] + public void RangeExceptRange(NpgsqlRange range) + { + using (RangeContext context = Fixture.CreateContext()) + { + try + { + NpgsqlRange[] _ = + context.RangeTestEntities + .Select(x => x.Range.Except(range)) + .ToArray(); + } + catch (PostgresException) + { + // ignore: Npgsql.PostgresException : 22000: result of range difference would not be contiguous. + } + + Assert.Contains("SELECT x.\"Range\" - @__range_0", Fixture.TestSqlLoggerFactory.Sql); + } + } + } +} From 403908a650dbecc70082c7d309bbd83dc7637441 Mon Sep 17 00:00:00 2001 From: Austin Drenski Date: Mon, 14 May 2018 12:59:17 -0400 Subject: [PATCH 2/6] Refactoring to use CustomBinaryExpression. --- src/EFCore.PG/NpgsqlRangeExtensions.cs | 2 +- .../Internal/NpgsqlOperatorAttribute.cs | 55 +++ .../Internal/NpgsqlRangeOperatorTranslator.cs | 260 ++++++++++-- .../Internal/NpgsqlRangeOperatorExpression.cs | 400 ------------------ .../Sql/Internal/NpgsqlQuerySqlGenerator.cs | 34 -- 5 files changed, 289 insertions(+), 462 deletions(-) create mode 100644 src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlOperatorAttribute.cs delete mode 100644 src/EFCore.PG/Query/Expressions/Internal/NpgsqlRangeOperatorExpression.cs diff --git a/src/EFCore.PG/NpgsqlRangeExtensions.cs b/src/EFCore.PG/NpgsqlRangeExtensions.cs index c5010f430..f450c340d 100644 --- a/src/EFCore.PG/NpgsqlRangeExtensions.cs +++ b/src/EFCore.PG/NpgsqlRangeExtensions.cs @@ -24,7 +24,7 @@ #endregion using System; -using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal; +using Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Internal; using NpgsqlTypes; namespace Npgsql.EntityFrameworkCore.PostgreSQL diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlOperatorAttribute.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlOperatorAttribute.cs new file mode 100644 index 000000000..f6873f44b --- /dev/null +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlOperatorAttribute.cs @@ -0,0 +1,55 @@ +using System; +using System.Linq.Expressions; +using JetBrains.Annotations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal; +using Npgsql.EntityFrameworkCore.PostgreSQL.Utilities; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Internal +{ + /// + /// Represents information about a PostgreSQL operator. + /// + /// + /// This attribute should be applied to the members of an enum to record metadata for database translation. + /// + [AttributeUsage(AttributeTargets.Field)] + public class NpgsqlOperatorAttribute : Attribute + { + /// + /// The operator symbol. + /// + [NotNull] + public string Symbol { get; set; } = string.Empty; + + /// + /// The operator represented by the method. + /// + [NotNull] + public Type ReturnType { get; set; } = typeof(void); + + /// + /// Creates a representing the operator. + /// + /// + /// The left-hand expression. + /// + /// + /// The right-hand expression. + /// + /// + /// A representing the operator. + /// + /// + [NotNull] + public CustomBinaryExpression Create([NotNull] Expression left, [NotNull] Expression right) + { + Check.NotNull(left, nameof(left)); + Check.NotNull(right, nameof(right)); + + // TODO: will the left type arguments always be valid? + Type type = ReturnType.IsGenericType ? ReturnType.MakeGenericType(left.Type.GetGenericArguments()) : ReturnType; + + return new CustomBinaryExpression(left, right, Symbol, type); + } + } +} diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRangeOperatorTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRangeOperatorTranslator.cs index c1d8793fe..0b7d3c27e 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRangeOperatorTranslator.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRangeOperatorTranslator.cs @@ -23,69 +23,275 @@ #endregion -using System.Collections.Concurrent; +using System; +using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Reflection; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Query.ExpressionTranslators; using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal; +using Npgsql.EntityFrameworkCore.PostgreSQL.Utilities; +using NpgsqlTypes; namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Internal { /// - /// Translates a method call into a . + /// Translates a method call into a PostgreSQL range operator. /// public class NpgsqlRangeOperatorTranslator : IMethodCallTranslator { + /// + /// Constructs a from two arguments. + /// + /// + /// The left-hand argument. + /// + /// + /// The left-hand argument. + /// + [NotNull] + delegate CustomBinaryExpression CustomBinaryExpressionFunction([NotNull] Expression left, [NotNull] Expression right); + /// /// Maps the generic definitions of the methods supported by this translator to the appropriate PostgreSQL operator. /// - [NotNull] static readonly ConcurrentDictionary SupportedMethodTranslations = - new ConcurrentDictionary( - typeof(NpgsqlRangeExtensions) - .GetRuntimeMethods() - .Select(x => (MethodInfo: x, Attribute: x.GetCustomAttribute())) - .Where(x => x.Attribute != null) - .ToDictionary(x => x.MethodInfo, x => x.Attribute.OperatorType)); + [NotNull] static readonly Dictionary SupportedMethodTranslations; + + /// + /// Initialize the with extension methods from . + /// + static NpgsqlRangeOperatorTranslator() + { + SupportedMethodTranslations = new Dictionary(); + + foreach (MethodInfo methodInfo in typeof(NpgsqlRangeExtensions).GetRuntimeMethods()) + { + TryAddSupportedMethod(methodInfo); + } + } + + /// + /// Adds a method that can be translated as a PostgreSQL range operator. This method must declare a . + /// + /// + /// The method to register. + /// + /// + public static void AddSupportedMethod([NotNull] MethodInfo methodInfo) + { + Check.NotNull(methodInfo, nameof(methodInfo)); + + NpgsqlRangeOperatorAttribute attribute = + methodInfo.GetCustomAttribute(); + + if (attribute is null) + throw new ArgumentException($"{nameof(NpgsqlRangeOperatorAttribute)} is not declared on {methodInfo.Name}."); + + NpgsqlOperatorAttribute operatorAttribute = + typeof(NpgsqlRangeOperatorType) + .GetMember(attribute.OperatorType.ToString()) + .Single() + .GetCustomAttribute(); + + if (operatorAttribute is null) + throw new ArgumentException($"{nameof(NpgsqlOperatorAttribute)} is not declared on the member '{attribute.OperatorType}' of {nameof(NpgsqlRangeOperatorType)}"); + + SupportedMethodTranslations.Add(methodInfo, (a, b) => operatorAttribute.Create(a, b)); + } + + /// + /// Attempts to add a method that can be translated as a PostgreSQL range operator. This method must declare a . + /// + /// + /// The method to register. + /// + /// + /// True if the method was added successfully; otherwise, false. + /// + public static bool TryAddSupportedMethod([NotNull] MethodInfo methodInfo) + { + Check.NotNull(methodInfo, nameof(methodInfo)); + + try + { + AddSupportedMethod(methodInfo); + return true; + } + catch (Exception) + { + return false; + } + } + + /// + /// Removes a method from the list of supported translations. + /// + /// + /// The method to register. + /// + /// + [UsedImplicitly] + public static void RemoveSupportedMethod([NotNull] MethodInfo methodInfo) + { + Check.NotNull(methodInfo, nameof(methodInfo)); + + SupportedMethodTranslations.Remove(methodInfo); + } /// [CanBeNull] public Expression Translate(MethodCallExpression methodCallExpression) => - TryGetSupportedTranslation(methodCallExpression.Method, out NpgsqlRangeOperatorType operatorType) - ? new NpgsqlRangeOperatorExpression(methodCallExpression.Arguments[0], methodCallExpression.Arguments[1], operatorType) + TryGetSupportedTranslation(methodCallExpression.Method, out CustomBinaryExpressionFunction operatorFunction) + ? operatorFunction(methodCallExpression.Arguments[0], methodCallExpression.Arguments[1]) : null; /// - /// Tries to find the by looking up the in . - /// If the is not found, but the method is checked for a . - /// If one is found, it is registered with and the is returned. + /// Tries to find create a by looking up the in . /// /// /// The for lookup. /// - /// - /// The if successful. + /// + /// A delegate that constructs a . /// /// /// True if the was found or registered; otherwise false. /// - static bool TryGetSupportedTranslation([NotNull] MethodInfo methodInfo, out NpgsqlRangeOperatorType operatorType) + static bool TryGetSupportedTranslation([NotNull] MethodInfo methodInfo, out CustomBinaryExpressionFunction operatorFunction) { MethodInfo info = methodInfo.IsGenericMethod ? methodInfo.GetGenericMethodDefinition() : methodInfo; - if (SupportedMethodTranslations.TryGetValue(info, out operatorType)) - { - return true; - } + return SupportedMethodTranslations.TryGetValue(info, out operatorFunction); + } + } - if (info.GetCustomAttribute() is NpgsqlRangeOperatorAttribute attribute) - { - operatorType = attribute.OperatorType; - return SupportedMethodTranslations.TryAdd(info, operatorType); - } + /// + /// Indicates that a method can be translated to a PostgreSQL range operator. + /// + /// + /// This attribute allows other extension methods to hook into the range operator translations. + /// Along with simplifying the code required to identify overloaded generics, this attribute provides + /// a transparent way in which to transition from extension methods in the EF Core assembly to + /// instance methods on . + /// + [AttributeUsage(AttributeTargets.Method, Inherited = false)] + public class NpgsqlRangeOperatorAttribute : Attribute + { + /// + /// The operator represented by the method. + /// + public NpgsqlRangeOperatorType OperatorType { get; } - return false; + /// + /// Indicates that a method can be translated to a PostgreSQL range operator. + /// + /// + /// The type of operator the method represents. + /// + public NpgsqlRangeOperatorAttribute(NpgsqlRangeOperatorType operatorType) + { + OperatorType = operatorType; } } + + /// + /// Describes the operator type of a range expression. + /// + /// + /// See: https://www.postgresql.org/docs/current/static/functions-range.html + /// + [PublicAPI] + public enum NpgsqlRangeOperatorType + { + /// + /// No operator specified. + /// + [NpgsqlOperator] None, + + /// + /// The = operator. + /// + [NpgsqlOperator(Symbol = "=", ReturnType = typeof(bool))] Equal, + + /// + /// The <> operator. + /// + [NpgsqlOperator(Symbol = "<>", ReturnType = typeof(bool))] NotEqual, + + /// + /// The < operator. + /// + [NpgsqlOperator(Symbol = "<", ReturnType = typeof(bool))] LessThan, + + /// + /// The > operator. + /// + [NpgsqlOperator(Symbol = ">", ReturnType = typeof(bool))] GreaterThan, + + /// + /// The <= operator. + /// + [NpgsqlOperator(Symbol = "<=", ReturnType = typeof(bool))] LessThanOrEqual, + + /// + /// The >= operator. + /// + [NpgsqlOperator(Symbol = ">=", ReturnType = typeof(bool))] GreaterThanOrEqual, + + /// + /// The @> operator. + /// + [NpgsqlOperator(Symbol = "@>", ReturnType = typeof(bool))] Contains, + + /// + /// The <@ operator. + /// + [NpgsqlOperator(Symbol = "<@", ReturnType = typeof(bool))] ContainedBy, + + /// + /// The && operator. + /// + [NpgsqlOperator(Symbol = "&&", ReturnType = typeof(bool))] Overlaps, + + /// + /// The << operator. + /// + [NpgsqlOperator(Symbol = "<<", ReturnType = typeof(bool))] IsStrictlyLeftOf, + + /// + /// The >> operator. + /// + [NpgsqlOperator(Symbol = ">>", ReturnType = typeof(bool))] IsStrictlyRightOf, + + /// + /// The &< operator. + /// + [NpgsqlOperator(Symbol = "&<", ReturnType = typeof(bool))] DoesNotExtendRightOf, + + /// + /// The &> operator. + /// + [NpgsqlOperator(Symbol = "&>", ReturnType = typeof(bool))] DoesNotExtendLeftOf, + + /// + /// The -|- operator. + /// + [NpgsqlOperator(Symbol = "-|-", ReturnType = typeof(bool))] IsAdjacentTo, + + /// + /// The + operator. + /// + [NpgsqlOperator(Symbol = "+", ReturnType = typeof(NpgsqlRange<>))] Union, + + /// + /// The * operator. + /// + [NpgsqlOperator(Symbol = "*", ReturnType = typeof(NpgsqlRange<>))] Intersection, + + /// + /// The - operator. + /// + [NpgsqlOperator(Symbol = "-", ReturnType = typeof(NpgsqlRange<>))] Difference + } } diff --git a/src/EFCore.PG/Query/Expressions/Internal/NpgsqlRangeOperatorExpression.cs b/src/EFCore.PG/Query/Expressions/Internal/NpgsqlRangeOperatorExpression.cs deleted file mode 100644 index 32bea1747..000000000 --- a/src/EFCore.PG/Query/Expressions/Internal/NpgsqlRangeOperatorExpression.cs +++ /dev/null @@ -1,400 +0,0 @@ -#region License - -// The PostgreSQL License -// -// Copyright (C) 2016 The Npgsql Development Team -// -// Permission to use, copy, modify, and distribute this software and its -// documentation for any purpose, without fee, and without a written -// agreement is hereby granted, provided that the above copyright notice -// and this paragraph and the following two paragraphs appear in all copies. -// -// IN NO EVENT SHALL THE NPGSQL DEVELOPMENT TEAM BE LIABLE TO ANY PARTY -// FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, -// INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS -// DOCUMENTATION, EVEN IF THE NPGSQL DEVELOPMENT TEAM HAS BEEN ADVISED OF -// THE POSSIBILITY OF SUCH DAMAGE. -// -// THE NPGSQL DEVELOPMENT TEAM SPECIFICALLY DISCLAIMS ANY WARRANTIES, -// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY -// AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS -// ON AN "AS IS" BASIS, AND THE NPGSQL DEVELOPMENT TEAM HAS NO OBLIGATIONS -// TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. - -#endregion - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Reflection; -using JetBrains.Annotations; -using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Sql.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Utilities; -using NpgsqlTypes; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal -{ - /// - /// Represents a PostgreSQL range operator. - /// - /// - /// See https://www.postgresql.org/docs/current/static/functions-range.html - /// - public class NpgsqlRangeOperatorExpression : Expression - { - /// - /// Maps standard binary operators from to . - /// - [NotNull] static readonly Dictionary ExpressionOperators = - new Dictionary - { - [ExpressionType.Equal] = NpgsqlRangeOperatorType.Equal, - [ExpressionType.NotEqual] = NpgsqlRangeOperatorType.NotEqual, - [ExpressionType.LessThan] = NpgsqlRangeOperatorType.LessThan, - [ExpressionType.GreaterThan] = NpgsqlRangeOperatorType.GreaterThan, - [ExpressionType.LessThanOrEqual] = NpgsqlRangeOperatorType.LessThanOrEqual, - [ExpressionType.GreaterThanOrEqual] = NpgsqlRangeOperatorType.GreaterThanOrEqual - }; - - /// - /// Maps the to a PostgreSQL operator symbol. - /// - [NotNull] static readonly Dictionary OperatorSymbols; - - /// - /// Maps the to a return type. - /// - [NotNull] static readonly Dictionary OperatorReturnTypes; - - /// - /// Initializes instances of found on . - /// - static NpgsqlRangeOperatorExpression() - { - (NpgsqlRangeOperatorType Enum, NpgsqlOperatorAttribute Attribute)[] map = - typeof(NpgsqlRangeOperatorType) - .GetEnumValues() - .Cast() - .Select(x => (Enum: x, Attribute: GetAttribute(x))) - .ToArray(); - - OperatorSymbols = map.ToDictionary(x => x.Enum, x => x.Attribute.Symbol); - OperatorReturnTypes = map.ToDictionary(x => x.Enum, x => x.Attribute.ReturnType); - - NpgsqlOperatorAttribute GetAttribute(NpgsqlRangeOperatorType operatorType) => - typeof(NpgsqlRangeOperatorType) - .GetMember(operatorType.ToString()) - .Single() - .GetCustomAttribute(); - } - - /// - /// The generic type definition for . - /// - [NotNull] static readonly Type NpgsqlRangeType = typeof(NpgsqlRange<>); - - /// - public override ExpressionType NodeType { get; } = ExpressionType.Extension; - - /// - public override Type Type { get; } - - /// - /// Gets the item to the left of the operator. - /// - public virtual Expression Left { get; } - - /// - /// Gets the item to the right of the operator. - /// - public virtual Expression Right { get; } - - /// - /// The operator. - /// - public virtual NpgsqlRangeOperatorType Operator { get; } - - /// - /// The operator symbol. - /// - [NotNull] - public virtual string OperatorSymbol => OperatorSymbols[Operator]; - - /// - /// Creates a new instance of . - /// - /// - /// The item to the left of the operator. - /// - /// - /// The item to the right of the operator. - /// - /// - /// The type of range operation. - /// - public NpgsqlRangeOperatorExpression([NotNull] Expression left, [NotNull] Expression right, NpgsqlRangeOperatorType operatorType) - { - Check.NotNull(left, nameof(left)); - Check.NotNull(right, nameof(right)); - - Left = left; - Right = right; - Operator = operatorType; - - // TODO: will the left type arguments always be valid? - Type type = OperatorReturnTypes[operatorType]; - Type = type.IsGenericType ? type.MakeGenericType(left.Type.GetGenericArguments()) : type; - } - - /// - protected override Expression Accept(ExpressionVisitor visitor) - { - Check.NotNull(visitor, nameof(visitor)); - - return - visitor is NpgsqlQuerySqlGenerator npsgqlGenerator - ? npsgqlGenerator.VisitRangeOperator(this) - : base.Accept(visitor); - } - - /// - protected override Expression VisitChildren(ExpressionVisitor visitor) - { - if (!(visitor.Visit(Left) is Expression newLeft)) - throw new ArgumentNullException(nameof(newLeft)); - - if (!(visitor.Visit(Right) is Expression newRight)) - throw new ArgumentNullException(nameof(newRight)); - - return - Left == newLeft && Right == newRight - ? this - : new NpgsqlRangeOperatorExpression(newLeft, newRight, Operator); - } - - /// - /// Returns a if applicable. - /// - /// - /// This returns a NpgsqlRangeOperatorExpression IFF: - /// 1. Both left and right are - /// 2. The expression node type is one of: - /// - Equal - /// - NotEqual - /// - LessThan - /// - GreaterThan - /// - LessThanOrEqual - /// - GreaterThanOrEqual - /// - /// - /// The binary expression to test. - /// - /// - /// A or null. - /// - [CanBeNull] - public static NpgsqlRangeOperatorExpression TryVisitBinary([NotNull] BinaryExpression expression) - { - Check.NotNull(expression, nameof(expression)); - - Type leftType = expression.Left.Type; - Type rightType = expression.Right.Type; - - if (!leftType.IsGenericType || leftType.GetGenericTypeDefinition() != NpgsqlRangeType) - return null; - if (!rightType.IsGenericType || rightType.GetGenericTypeDefinition() != NpgsqlRangeType) - return null; - - return - ExpressionOperators.TryGetValue(expression.NodeType, out NpgsqlRangeOperatorType operatorType) - ? new NpgsqlRangeOperatorExpression(expression.Left, expression.Right, operatorType) - : null; - } - - /// - public override string ToString() => $"{Left} {OperatorSymbol} {Right}"; - - /// - public override bool Equals(object obj) - { - if (ReferenceEquals(null, obj)) - return false; - if (ReferenceEquals(this, obj)) - return true; - if (!(obj is NpgsqlRangeOperatorExpression other)) - return false; - - return - Equals(Left, other.Left) && - Equals(Right, other.Right) && - NodeType == other.NodeType && - Operator == other.Operator && - Type == other.Type; - } - - /// - public override int GetHashCode() - { - unchecked - { - int hashCode = Left?.GetHashCode() ?? 0; - hashCode = (hashCode * 397) ^ (Right?.GetHashCode() ?? 0); - hashCode = (hashCode * 397) ^ (int)NodeType; - hashCode = (hashCode * 397) ^ (int)Operator; - hashCode = (hashCode * 397) ^ Type.GetHashCode(); - return hashCode; - } - } - } - - /// - /// Indicates that a method can be translated to a PostgreSQL range operator. - /// - /// - /// This attribute allows other extension methods to hook into the range operator translations. - /// Along with simplifying the code required to identify overloaded generics, this attribute provides - /// a transparent way in which to transition from extension methods in the EF Core assembly to - /// instance methods on . - /// - [AttributeUsage(AttributeTargets.Method, Inherited = false)] - public class NpgsqlRangeOperatorAttribute : Attribute - { - /// - /// The operator represented by the method. - /// - public NpgsqlRangeOperatorType OperatorType { get; } - - /// - /// Indicates that a method can be translated to a PostgreSQL range operator. - /// - /// - /// The type of operator the method represents. - /// - public NpgsqlRangeOperatorAttribute(NpgsqlRangeOperatorType operatorType) - { - OperatorType = operatorType; - } - } - - /// - /// Represents information about a PostgreSQL operator. - /// - /// - /// This attribute should be applied to the members of an enum to record metadata for database translation. - /// - [AttributeUsage(AttributeTargets.Field)] - public class NpgsqlOperatorAttribute : Attribute - { - /// - /// The operator symbol. - /// - [NotNull] - public string Symbol { get; set; } = string.Empty; - - /// - /// The operator represented by the method. - /// - [NotNull] - public Type ReturnType { get; set; } = typeof(void); - } - - /// - /// Describes the operator type of a range expression. - /// - /// - /// See: https://www.postgresql.org/docs/current/static/functions-range.html - /// - public enum NpgsqlRangeOperatorType - { - /// - /// No operator specified. - /// - [NpgsqlOperator] None, - - /// - /// The = operator. - /// - [NpgsqlOperator(Symbol = "=", ReturnType = typeof(bool))] Equal, - - /// - /// The <> operator. - /// - [NpgsqlOperator(Symbol = "<>", ReturnType = typeof(bool))] NotEqual, - - /// - /// The < operator. - /// - [NpgsqlOperator(Symbol = "<", ReturnType = typeof(bool))] LessThan, - - /// - /// The > operator. - /// - [NpgsqlOperator(Symbol = ">", ReturnType = typeof(bool))] GreaterThan, - - /// - /// The <= operator. - /// - [NpgsqlOperator(Symbol = "<=", ReturnType = typeof(bool))] LessThanOrEqual, - - /// - /// The >= operator. - /// - [NpgsqlOperator(Symbol = ">=", ReturnType = typeof(bool))] GreaterThanOrEqual, - - /// - /// The @> operator. - /// - [NpgsqlOperator(Symbol = "@>", ReturnType = typeof(bool))] Contains, - - /// - /// The <@ operator. - /// - [NpgsqlOperator(Symbol = "<@", ReturnType = typeof(bool))] ContainedBy, - - /// - /// The && operator. - /// - [NpgsqlOperator(Symbol = "&&", ReturnType = typeof(bool))] Overlaps, - - /// - /// The << operator. - /// - [NpgsqlOperator(Symbol = "<<", ReturnType = typeof(bool))] IsStrictlyLeftOf, - - /// - /// The >> operator. - /// - [NpgsqlOperator(Symbol = ">>", ReturnType = typeof(bool))] IsStrictlyRightOf, - - /// - /// The &< operator. - /// - [NpgsqlOperator(Symbol = "&<", ReturnType = typeof(bool))] DoesNotExtendRightOf, - - /// - /// The &> operator. - /// - [NpgsqlOperator(Symbol = "&>", ReturnType = typeof(bool))] DoesNotExtendLeftOf, - - /// - /// The -|- operator. - /// - [NpgsqlOperator(Symbol = "-|-", ReturnType = typeof(bool))] IsAdjacentTo, - - /// - /// The + operator. - /// - [NpgsqlOperator(Symbol = "+", ReturnType = typeof(NpgsqlRange<>))] Union, - - /// - /// The * operator. - /// - [NpgsqlOperator(Symbol = "*", ReturnType = typeof(NpgsqlRange<>))] Intersection, - - /// - /// The - operator. - /// - [NpgsqlOperator(Symbol = "-", ReturnType = typeof(NpgsqlRange<>))] Difference - } -} diff --git a/src/EFCore.PG/Query/Sql/Internal/NpgsqlQuerySqlGenerator.cs b/src/EFCore.PG/Query/Sql/Internal/NpgsqlQuerySqlGenerator.cs index 60c16e7b4..6aa8a5704 100644 --- a/src/EFCore.PG/Query/Sql/Internal/NpgsqlQuerySqlGenerator.cs +++ b/src/EFCore.PG/Query/Sql/Internal/NpgsqlQuerySqlGenerator.cs @@ -109,23 +109,6 @@ public override Expression VisitSqlFunction(SqlFunctionExpression sqlFunctionExp protected override Expression VisitBinary(BinaryExpression expression) { - // We need to intercept some of the standard operators for NpgsqlRange. - // This returns a NpgsqlRangeOperatorExpression if both conditions are met: - // 1. Both left and right are NpgsqlRange. - // 2. The expression node type is one of: - // - Equal - // - NotEqual - // - LessThan - // - GreaterThan - // - LessThanOrEqual - // - GreaterThanOrEqual - // - // Otherwise, this is null and the method should continue. - NpgsqlRangeOperatorExpression rangeOperatorExpression = NpgsqlRangeOperatorExpression.TryVisitBinary(expression); - - if (rangeOperatorExpression != null) - return VisitRangeOperator(rangeOperatorExpression); - switch (expression.NodeType) { case ExpressionType.Add: @@ -201,23 +184,6 @@ public Expression VisitArrayAny(ArrayAnyExpression arrayAnyExpression) return arrayAnyExpression; } - /// - /// Visits a and generates the operator symbols. - /// - /// - /// The expression to visit. - /// - /// - /// The visited expression. - /// - public Expression VisitRangeOperator(NpgsqlRangeOperatorExpression rangeOperatorExpression) - { - Visit(rangeOperatorExpression.Left); - Sql.Append($" {rangeOperatorExpression.OperatorSymbol} "); - Visit(rangeOperatorExpression.Right); - return rangeOperatorExpression; - } - // 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) From 35f7d0017896115c9957e7c8adb979b570dafed7 Mon Sep 17 00:00:00 2001 From: Austin Drenski Date: Mon, 14 May 2018 16:23:12 -0400 Subject: [PATCH 3/6] Changes new attribute visibility and provides hooks for registering new range operator methods for translation (e.g. client code can define and register a method that supports client evaluation). The goal is to build on CustomBinaryExpression and make it easier to add new operators (e.g. XML operators from issue #7). With the current structure, additional operator classes can be codified in 3 files: Npgsql{type_name}OperatorType, Npgsql{type_name}OperatorAttribute, and Npgsql{type_name}OperatorTranslator. --- src/EFCore.PG/NpgsqlRangeExtensions.cs | 2 +- ...te.cs => NpgsqlBinaryOperatorAttribute.cs} | 34 ++- .../Internal/NpgsqlRangeOperatorTranslator.cs | 200 ++++-------------- .../NpgsqlRangeOperatorAttribute.cs | 59 ++++++ .../NpgsqlRangeOperatorType.cs | 131 ++++++++++++ 5 files changed, 267 insertions(+), 159 deletions(-) rename src/EFCore.PG/Query/ExpressionTranslators/Internal/{NpgsqlOperatorAttribute.cs => NpgsqlBinaryOperatorAttribute.cs} (53%) create mode 100644 src/EFCore.PG/Query/ExpressionTranslators/NpgsqlRangeOperatorAttribute.cs create mode 100644 src/EFCore.PG/Query/ExpressionTranslators/NpgsqlRangeOperatorType.cs diff --git a/src/EFCore.PG/NpgsqlRangeExtensions.cs b/src/EFCore.PG/NpgsqlRangeExtensions.cs index f450c340d..49031bf83 100644 --- a/src/EFCore.PG/NpgsqlRangeExtensions.cs +++ b/src/EFCore.PG/NpgsqlRangeExtensions.cs @@ -24,7 +24,7 @@ #endregion using System; -using Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Internal; +using Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators; using NpgsqlTypes; namespace Npgsql.EntityFrameworkCore.PostgreSQL diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlOperatorAttribute.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlBinaryOperatorAttribute.cs similarity index 53% rename from src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlOperatorAttribute.cs rename to src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlBinaryOperatorAttribute.cs index f6873f44b..2ef20ed41 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlOperatorAttribute.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlBinaryOperatorAttribute.cs @@ -1,4 +1,29 @@ -using System; +#region License + +// The PostgreSQL License +// +// Copyright (C) 2016 The Npgsql Development Team +// +// Permission to use, copy, modify, and distribute this software and its +// documentation for any purpose, without fee, and without a written +// agreement is hereby granted, provided that the above copyright notice +// and this paragraph and the following two paragraphs appear in all copies. +// +// IN NO EVENT SHALL THE NPGSQL DEVELOPMENT TEAM BE LIABLE TO ANY PARTY +// FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, +// INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS +// DOCUMENTATION, EVEN IF THE NPGSQL DEVELOPMENT TEAM HAS BEEN ADVISED OF +// THE POSSIBILITY OF SUCH DAMAGE. +// +// THE NPGSQL DEVELOPMENT TEAM SPECIFICALLY DISCLAIMS ANY WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS +// ON AN "AS IS" BASIS, AND THE NPGSQL DEVELOPMENT TEAM HAS NO OBLIGATIONS +// TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + +#endregion + +using System; using System.Linq.Expressions; using JetBrains.Annotations; using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal; @@ -10,10 +35,11 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Inte /// Represents information about a PostgreSQL operator. /// /// - /// This attribute should be applied to the members of an enum to record metadata for database translation. + /// This attribute stores metadata describing the representation of a binary operator + /// in PostgreSQL on enum members representing the available binary operators for a PostgreSQL type. /// [AttributeUsage(AttributeTargets.Field)] - public class NpgsqlOperatorAttribute : Attribute + internal class NpgsqlBinaryOperatorAttribute : Attribute { /// /// The operator symbol. @@ -41,7 +67,7 @@ public class NpgsqlOperatorAttribute : Attribute /// /// [NotNull] - public CustomBinaryExpression Create([NotNull] Expression left, [NotNull] Expression right) + public Expression Create([NotNull] Expression left, [NotNull] Expression right) { Check.NotNull(left, nameof(left)); Check.NotNull(right, nameof(right)); diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRangeOperatorTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRangeOperatorTranslator.cs index 0b7d3c27e..0e727166c 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRangeOperatorTranslator.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRangeOperatorTranslator.cs @@ -25,24 +25,25 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Linq.Expressions; using System.Reflection; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Query.ExpressionTranslators; -using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal; using Npgsql.EntityFrameworkCore.PostgreSQL.Utilities; -using NpgsqlTypes; namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Internal { /// /// Translates a method call into a PostgreSQL range operator. /// + /// + /// By default, this class supports translation for methods declared on . + /// Additional methods can be supported so long as they declare a . + /// public class NpgsqlRangeOperatorTranslator : IMethodCallTranslator { /// - /// Constructs a from two arguments. + /// Constructs a from two arguments. /// /// /// The left-hand argument. @@ -51,19 +52,19 @@ public class NpgsqlRangeOperatorTranslator : IMethodCallTranslator /// The left-hand argument. /// [NotNull] - delegate CustomBinaryExpression CustomBinaryExpressionFunction([NotNull] Expression left, [NotNull] Expression right); + delegate Expression BinaryExpressionFunction([NotNull] Expression left, [NotNull] Expression right); /// /// Maps the generic definitions of the methods supported by this translator to the appropriate PostgreSQL operator. /// - [NotNull] static readonly Dictionary SupportedMethodTranslations; + [NotNull] static readonly Dictionary SupportedMethodTranslations; /// /// Initialize the with extension methods from . /// static NpgsqlRangeOperatorTranslator() { - SupportedMethodTranslations = new Dictionary(); + SupportedMethodTranslations = new Dictionary(); foreach (MethodInfo methodInfo in typeof(NpgsqlRangeExtensions).GetRuntimeMethods()) { @@ -72,7 +73,7 @@ static NpgsqlRangeOperatorTranslator() } /// - /// Adds a method that can be translated as a PostgreSQL range operator. This method must declare a . + /// Adds a method that can be translated as a PostgreSQL range operator. This method must declare a . /// /// /// The method to register. @@ -86,22 +87,21 @@ public static void AddSupportedMethod([NotNull] MethodInfo methodInfo) methodInfo.GetCustomAttribute(); if (attribute is null) - throw new ArgumentException($"{nameof(NpgsqlRangeOperatorAttribute)} is not declared on {methodInfo.Name}."); + throw new ArgumentException($"{nameof(NpgsqlRangeOperatorAttribute)} is not declared on the method '{methodInfo.Name}' of {nameof(methodInfo.DeclaringType)}."); - NpgsqlOperatorAttribute operatorAttribute = + NpgsqlBinaryOperatorAttribute operatorAttribute = typeof(NpgsqlRangeOperatorType) - .GetMember(attribute.OperatorType.ToString()) - .Single() - .GetCustomAttribute(); + .GetRuntimeField(attribute.OperatorType.ToString()) + .GetCustomAttribute(); if (operatorAttribute is null) - throw new ArgumentException($"{nameof(NpgsqlOperatorAttribute)} is not declared on the member '{attribute.OperatorType}' of {nameof(NpgsqlRangeOperatorType)}"); + throw new ArgumentException($"{nameof(NpgsqlBinaryOperatorAttribute)} is not declared on the member '{attribute.OperatorType}' of {nameof(NpgsqlRangeOperatorType)}"); SupportedMethodTranslations.Add(methodInfo, (a, b) => operatorAttribute.Create(a, b)); } /// - /// Attempts to add a method that can be translated as a PostgreSQL range operator. This method must declare a . + /// Attempts to add a method that can be translated as a PostgreSQL range operator. This method must declare a . /// /// /// The method to register. @@ -128,7 +128,7 @@ public static bool TryAddSupportedMethod([NotNull] MethodInfo methodInfo) /// Removes a method from the list of supported translations. /// /// - /// The method to register. + /// The method to unregister. /// /// [UsedImplicitly] @@ -139,159 +139,51 @@ public static void RemoveSupportedMethod([NotNull] MethodInfo methodInfo) SupportedMethodTranslations.Remove(methodInfo); } + /// + /// Attempts to remove a method from the list of supported translations. + /// + /// + /// The method to unregister. + /// + /// + [UsedImplicitly] + public static bool TryRemoveSupportedMethod([NotNull] MethodInfo methodInfo) + { + Check.NotNull(methodInfo, nameof(methodInfo)); + + try + { + RemoveSupportedMethod(methodInfo); + return true; + } + catch (Exception) + { + return false; + } + } + /// [CanBeNull] public Expression Translate(MethodCallExpression methodCallExpression) => - TryGetSupportedTranslation(methodCallExpression.Method, out CustomBinaryExpressionFunction operatorFunction) + TryGetSupportedTranslation(methodCallExpression.Method, out BinaryExpressionFunction operatorFunction) ? operatorFunction(methodCallExpression.Arguments[0], methodCallExpression.Arguments[1]) : null; /// - /// Tries to find create a by looking up the in . + /// Tries to find create a by looking up the in . /// /// /// The for lookup. /// /// - /// A delegate that constructs a . + /// A delegate that constructs a . /// /// /// True if the was found or registered; otherwise false. /// - static bool TryGetSupportedTranslation([NotNull] MethodInfo methodInfo, out CustomBinaryExpressionFunction operatorFunction) - { - MethodInfo info = methodInfo.IsGenericMethod ? methodInfo.GetGenericMethodDefinition() : methodInfo; - - return SupportedMethodTranslations.TryGetValue(info, out operatorFunction); - } - } - - /// - /// Indicates that a method can be translated to a PostgreSQL range operator. - /// - /// - /// This attribute allows other extension methods to hook into the range operator translations. - /// Along with simplifying the code required to identify overloaded generics, this attribute provides - /// a transparent way in which to transition from extension methods in the EF Core assembly to - /// instance methods on . - /// - [AttributeUsage(AttributeTargets.Method, Inherited = false)] - public class NpgsqlRangeOperatorAttribute : Attribute - { - /// - /// The operator represented by the method. - /// - public NpgsqlRangeOperatorType OperatorType { get; } - - /// - /// Indicates that a method can be translated to a PostgreSQL range operator. - /// - /// - /// The type of operator the method represents. - /// - public NpgsqlRangeOperatorAttribute(NpgsqlRangeOperatorType operatorType) - { - OperatorType = operatorType; - } - } - - /// - /// Describes the operator type of a range expression. - /// - /// - /// See: https://www.postgresql.org/docs/current/static/functions-range.html - /// - [PublicAPI] - public enum NpgsqlRangeOperatorType - { - /// - /// No operator specified. - /// - [NpgsqlOperator] None, - - /// - /// The = operator. - /// - [NpgsqlOperator(Symbol = "=", ReturnType = typeof(bool))] Equal, - - /// - /// The <> operator. - /// - [NpgsqlOperator(Symbol = "<>", ReturnType = typeof(bool))] NotEqual, - - /// - /// The < operator. - /// - [NpgsqlOperator(Symbol = "<", ReturnType = typeof(bool))] LessThan, - - /// - /// The > operator. - /// - [NpgsqlOperator(Symbol = ">", ReturnType = typeof(bool))] GreaterThan, - - /// - /// The <= operator. - /// - [NpgsqlOperator(Symbol = "<=", ReturnType = typeof(bool))] LessThanOrEqual, - - /// - /// The >= operator. - /// - [NpgsqlOperator(Symbol = ">=", ReturnType = typeof(bool))] GreaterThanOrEqual, - - /// - /// The @> operator. - /// - [NpgsqlOperator(Symbol = "@>", ReturnType = typeof(bool))] Contains, - - /// - /// The <@ operator. - /// - [NpgsqlOperator(Symbol = "<@", ReturnType = typeof(bool))] ContainedBy, - - /// - /// The && operator. - /// - [NpgsqlOperator(Symbol = "&&", ReturnType = typeof(bool))] Overlaps, - - /// - /// The << operator. - /// - [NpgsqlOperator(Symbol = "<<", ReturnType = typeof(bool))] IsStrictlyLeftOf, - - /// - /// The >> operator. - /// - [NpgsqlOperator(Symbol = ">>", ReturnType = typeof(bool))] IsStrictlyRightOf, - - /// - /// The &< operator. - /// - [NpgsqlOperator(Symbol = "&<", ReturnType = typeof(bool))] DoesNotExtendRightOf, - - /// - /// The &> operator. - /// - [NpgsqlOperator(Symbol = "&>", ReturnType = typeof(bool))] DoesNotExtendLeftOf, - - /// - /// The -|- operator. - /// - [NpgsqlOperator(Symbol = "-|-", ReturnType = typeof(bool))] IsAdjacentTo, - - /// - /// The + operator. - /// - [NpgsqlOperator(Symbol = "+", ReturnType = typeof(NpgsqlRange<>))] Union, - - /// - /// The * operator. - /// - [NpgsqlOperator(Symbol = "*", ReturnType = typeof(NpgsqlRange<>))] Intersection, - - /// - /// The - operator. - /// - [NpgsqlOperator(Symbol = "-", ReturnType = typeof(NpgsqlRange<>))] Difference + static bool TryGetSupportedTranslation([NotNull] MethodInfo methodInfo, out BinaryExpressionFunction operatorFunction) => + methodInfo.IsGenericMethod + ? SupportedMethodTranslations.TryGetValue(methodInfo.GetGenericMethodDefinition(), out operatorFunction) + : SupportedMethodTranslations.TryGetValue(methodInfo, out operatorFunction); } } diff --git a/src/EFCore.PG/Query/ExpressionTranslators/NpgsqlRangeOperatorAttribute.cs b/src/EFCore.PG/Query/ExpressionTranslators/NpgsqlRangeOperatorAttribute.cs new file mode 100644 index 000000000..1eed954c8 --- /dev/null +++ b/src/EFCore.PG/Query/ExpressionTranslators/NpgsqlRangeOperatorAttribute.cs @@ -0,0 +1,59 @@ +#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 NpgsqlTypes; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators +{ + /// + /// Indicates that a method can be translated to a PostgreSQL range operator. + /// + /// + /// This attribute allows other extension methods to hook into the range operator translations. + /// Along with simplifying the code required to identify overloaded generics, this attribute provides + /// a transparent way in which to transition from extension methods in the EF Core assembly to + /// instance methods on . + /// + [AttributeUsage(AttributeTargets.Method, Inherited = false)] + public class NpgsqlRangeOperatorAttribute : Attribute + { + /// + /// The operator represented by the method. + /// + public NpgsqlRangeOperatorType OperatorType { get; } + + /// + /// Indicates that a method can be translated to a PostgreSQL range operator. + /// + /// + /// The type of operator the method represents. + /// + public NpgsqlRangeOperatorAttribute(NpgsqlRangeOperatorType operatorType) + { + OperatorType = operatorType; + } + } +} diff --git a/src/EFCore.PG/Query/ExpressionTranslators/NpgsqlRangeOperatorType.cs b/src/EFCore.PG/Query/ExpressionTranslators/NpgsqlRangeOperatorType.cs new file mode 100644 index 000000000..1f3c0d2e1 --- /dev/null +++ b/src/EFCore.PG/Query/ExpressionTranslators/NpgsqlRangeOperatorType.cs @@ -0,0 +1,131 @@ +#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 JetBrains.Annotations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Internal; +using NpgsqlTypes; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators +{ + /// + /// Describes the operator type of a range expression. + /// + /// + /// See: https://www.postgresql.org/docs/current/static/functions-range.html + /// + [PublicAPI] + public enum NpgsqlRangeOperatorType + { + /// + /// No operator specified. + /// + [NpgsqlBinaryOperator] None, + + /// + /// The = operator. + /// + [NpgsqlBinaryOperator(Symbol = "=", ReturnType = typeof(bool))] Equal, + + /// + /// The <> operator. + /// + [NpgsqlBinaryOperator(Symbol = "<>", ReturnType = typeof(bool))] NotEqual, + + /// + /// The < operator. + /// + [NpgsqlBinaryOperator(Symbol = "<", ReturnType = typeof(bool))] LessThan, + + /// + /// The > operator. + /// + [NpgsqlBinaryOperator(Symbol = ">", ReturnType = typeof(bool))] GreaterThan, + + /// + /// The <= operator. + /// + [NpgsqlBinaryOperator(Symbol = "<=", ReturnType = typeof(bool))] LessThanOrEqual, + + /// + /// The >= operator. + /// + [NpgsqlBinaryOperator(Symbol = ">=", ReturnType = typeof(bool))] GreaterThanOrEqual, + + /// + /// The @> operator. + /// + [NpgsqlBinaryOperator(Symbol = "@>", ReturnType = typeof(bool))] Contains, + + /// + /// The <@ operator. + /// + [NpgsqlBinaryOperator(Symbol = "<@", ReturnType = typeof(bool))] ContainedBy, + + /// + /// The && operator. + /// + [NpgsqlBinaryOperator(Symbol = "&&", ReturnType = typeof(bool))] Overlaps, + + /// + /// The << operator. + /// + [NpgsqlBinaryOperator(Symbol = "<<", ReturnType = typeof(bool))] IsStrictlyLeftOf, + + /// + /// The >> operator. + /// + [NpgsqlBinaryOperator(Symbol = ">>", ReturnType = typeof(bool))] IsStrictlyRightOf, + + /// + /// The &< operator. + /// + [NpgsqlBinaryOperator(Symbol = "&<", ReturnType = typeof(bool))] DoesNotExtendRightOf, + + /// + /// The &> operator. + /// + [NpgsqlBinaryOperator(Symbol = "&>", ReturnType = typeof(bool))] DoesNotExtendLeftOf, + + /// + /// The -|- operator. + /// + [NpgsqlBinaryOperator(Symbol = "-|-", ReturnType = typeof(bool))] IsAdjacentTo, + + /// + /// The + operator. + /// + [NpgsqlBinaryOperator(Symbol = "+", ReturnType = typeof(NpgsqlRange<>))] Union, + + /// + /// The * operator. + /// + [NpgsqlBinaryOperator(Symbol = "*", ReturnType = typeof(NpgsqlRange<>))] Intersection, + + /// + /// The - operator. + /// + [NpgsqlBinaryOperator(Symbol = "-", ReturnType = typeof(NpgsqlRange<>))] Difference + } +} From 7fb701cde931b5caeb92d7397faa7f150837bd93 Mon Sep 17 00:00:00 2001 From: Austin Drenski Date: Tue, 15 May 2018 08:36:58 -0400 Subject: [PATCH 4/6] Addressing code review comments (2018-05-15). --- src/EFCore.PG/NpgsqlRangeExtensions.cs | 179 +++++------------ .../Internal/NpgsqlBinaryOperatorAttribute.cs | 81 -------- .../NpgsqlCompositeMethodCallTranslator.cs | 2 +- .../Internal/NpgsqlRangeOperatorTranslator.cs | 189 ------------------ .../Internal/NpgsqlRangeTranslator.cs | 108 ++++++++++ .../NpgsqlRangeOperatorAttribute.cs | 59 ------ .../NpgsqlRangeOperatorType.cs | 131 ------------ .../Query/RangeQueryNpgsqlTest.cs | 63 +++--- 8 files changed, 191 insertions(+), 621 deletions(-) delete mode 100644 src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlBinaryOperatorAttribute.cs delete mode 100644 src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRangeOperatorTranslator.cs create mode 100644 src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRangeTranslator.cs delete mode 100644 src/EFCore.PG/Query/ExpressionTranslators/NpgsqlRangeOperatorAttribute.cs delete mode 100644 src/EFCore.PG/Query/ExpressionTranslators/NpgsqlRangeOperatorType.cs diff --git a/src/EFCore.PG/NpgsqlRangeExtensions.cs b/src/EFCore.PG/NpgsqlRangeExtensions.cs index 49031bf83..d50b555bf 100644 --- a/src/EFCore.PG/NpgsqlRangeExtensions.cs +++ b/src/EFCore.PG/NpgsqlRangeExtensions.cs @@ -24,7 +24,6 @@ #endregion using System; -using Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators; using NpgsqlTypes; namespace Npgsql.EntityFrameworkCore.PostgreSQL @@ -37,217 +36,133 @@ public static class NpgsqlRangeExtensions /// /// Determines whether a range contains a specified value. /// - /// - /// The range in which to locate the value. - /// - /// - /// The value to locate in the range. - /// - /// - /// The type of the elements of . - /// + /// The range in which to locate the value. + /// The value to locate in the range. + /// The type of the elements of . /// /// true if the range contains the specified value; otherwise, false. /// - [NpgsqlRangeOperator(NpgsqlRangeOperatorType.Contains)] - public static bool Contains(this NpgsqlRange range, T value) where T : IComparable => throw new NotImplementedException(); + public static bool Contains(this NpgsqlRange range, T value) where T : IComparable => throw new NotSupportedException(); /// /// Determines whether a range contains a specified range. /// - /// - /// The range in which to locate the specified range. - /// - /// - /// The specified range to locate in the range. - /// - /// - /// The type of the elements of . - /// + /// The range in which to locate the specified range. + /// The specified range to locate in the range. + /// The type of the elements of . /// /// true if the range contains the specified range; otherwise, false. /// - [NpgsqlRangeOperator(NpgsqlRangeOperatorType.Contains)] - public static bool Contains(this NpgsqlRange a, NpgsqlRange b) where T : IComparable => throw new NotImplementedException(); + public static bool Contains(this NpgsqlRange a, NpgsqlRange b) where T : IComparable => throw new NotSupportedException(); /// /// Determines whether a range is contained by a specified range. /// - /// - /// The specified range to locate in the range. - /// - /// - /// The range in which to locate the specified range. - /// - /// - /// The type of the elements of . - /// + /// The specified range to locate in the range. + /// The range in which to locate the specified range. + /// The type of the elements of . /// /// true if the range contains the specified range; otherwise, false. /// - [NpgsqlRangeOperator(NpgsqlRangeOperatorType.ContainedBy)] public static bool ContainedBy(this NpgsqlRange a, NpgsqlRange b) where T : IComparable => b.Contains(a); /// /// Determines whether a range overlaps another range. /// - /// - /// The first range. - /// - /// - /// The second range. - /// - /// - /// The type of the elements of . - /// + /// The first range. + /// The second range. + /// The type of the elements of . /// /// true if the ranges overlap (share points in common); otherwise, false. /// - [NpgsqlRangeOperator(NpgsqlRangeOperatorType.Overlaps)] - public static bool Overlaps(this NpgsqlRange a, NpgsqlRange b) where T : IComparable => throw new NotImplementedException(); + public static bool Overlaps(this NpgsqlRange a, NpgsqlRange b) where T : IComparable => throw new NotSupportedException(); /// /// Determines whether a range is strictly to the left of another range. /// - /// - /// The first range. - /// - /// - /// The second range. - /// - /// - /// The type of the elements of . - /// + /// The first range. + /// The second range. + /// The type of the elements of . /// /// true if the first range is strictly to the left of the second; otherwise, false. /// - [NpgsqlRangeOperator(NpgsqlRangeOperatorType.IsStrictlyLeftOf)] - public static bool IsStrictlyLeftOf(this NpgsqlRange a, NpgsqlRange b) where T : IComparable => throw new NotImplementedException(); + public static bool IsStrictlyLeftOf(this NpgsqlRange a, NpgsqlRange b) where T : IComparable => throw new NotSupportedException(); /// /// Determines whether a range is strictly to the right of another range. /// - /// - /// The first range. - /// - /// - /// The second range. - /// - /// - /// The type of the elements of . - /// + /// The first range. + /// The second range. + /// The type of the elements of . /// /// true if the first range is strictly to the right of the second; otherwise, false. /// - [NpgsqlRangeOperator(NpgsqlRangeOperatorType.IsStrictlyRightOf)] - public static bool IsStrictlyRightOf(this NpgsqlRange a, NpgsqlRange b) where T : IComparable => throw new NotImplementedException(); + public static bool IsStrictlyRightOf(this NpgsqlRange a, NpgsqlRange b) where T : IComparable => throw new NotSupportedException(); /// /// Determines whether a range does not extend to the left of another range. /// - /// - /// The first range. - /// - /// - /// The second range. - /// - /// - /// The type of the elements of . - /// + /// The first range. + /// The second range. + /// The type of the elements of . /// /// true if the first range does not extend to the left of the second; otherwise, false. /// - [NpgsqlRangeOperator(NpgsqlRangeOperatorType.DoesNotExtendLeftOf)] - public static bool DoesNotExtendLeftOf(this NpgsqlRange a, NpgsqlRange b) where T : IComparable => throw new NotImplementedException(); + public static bool DoesNotExtendLeftOf(this NpgsqlRange a, NpgsqlRange b) where T : IComparable => throw new NotSupportedException(); /// /// Determines whether a range does not extend to the right of another range. /// - /// - /// The first range. - /// - /// - /// The second range. - /// - /// - /// The type of the elements of . - /// + /// The first range. + /// The second range. + /// The type of the elements of . /// /// true if the first range does not extend to the right of the second; otherwise, false. /// - [NpgsqlRangeOperator(NpgsqlRangeOperatorType.DoesNotExtendRightOf)] - public static bool DoesNotExtendRightOf(this NpgsqlRange a, NpgsqlRange b) where T : IComparable => throw new NotImplementedException(); + public static bool DoesNotExtendRightOf(this NpgsqlRange a, NpgsqlRange b) where T : IComparable => throw new NotSupportedException(); /// /// Determines whether a range is adjacent to another range. /// - /// - /// The first range. - /// - /// - /// The second range. - /// - /// - /// The type of the elements of . - /// + /// The first range. + /// The second range. + /// The type of the elements of . /// /// true if the ranges are adjacent; otherwise, false. /// - [NpgsqlRangeOperator(NpgsqlRangeOperatorType.IsAdjacentTo)] - public static bool IsAdjacentTo(this NpgsqlRange a, NpgsqlRange b) where T : IComparable => throw new NotImplementedException(); + public static bool IsAdjacentTo(this NpgsqlRange a, NpgsqlRange b) where T : IComparable => throw new NotSupportedException(); /// /// Returns the set union, which means unique elements that appear in either of two ranges. /// - /// - /// The first range. - /// - /// - /// The second range. - /// - /// - /// The type of the elements of . - /// + /// The first range. + /// The second range. + /// The type of the elements of . /// /// The unique elements that appear in either range. /// - [NpgsqlRangeOperator(NpgsqlRangeOperatorType.Union)] - public static NpgsqlRange Union(this NpgsqlRange a, NpgsqlRange b) where T : IComparable => throw new NotImplementedException(); + public static NpgsqlRange Union(this NpgsqlRange a, NpgsqlRange b) where T : IComparable => throw new NotSupportedException(); /// /// Returns the set intersection, which means elements that appear in each of two ranges. /// - /// - /// The first range. - /// - /// - /// The second range. - /// - /// - /// The type of the elements of . - /// + /// The first range. + /// The second range. + /// The type of the elements of . /// /// The elements that appear in both ranges. /// - [NpgsqlRangeOperator(NpgsqlRangeOperatorType.Intersection)] - public static NpgsqlRange Intersect(this NpgsqlRange a, NpgsqlRange b) where T : IComparable => throw new NotImplementedException(); + public static NpgsqlRange Intersect(this NpgsqlRange a, NpgsqlRange b) where T : IComparable => throw new NotSupportedException(); /// /// Returns the set difference, which means the elements of one range that do not appear in a second range. /// - /// - /// The first range. - /// - /// - /// The second range. - /// - /// - /// The type of the elements of . - /// + /// The first range. + /// The second range. + /// The type of the elements of . /// /// The elements that appear in the first range, but not the second range. /// - [NpgsqlRangeOperator(NpgsqlRangeOperatorType.Difference)] - public static NpgsqlRange Except(this NpgsqlRange a, NpgsqlRange b) where T : IComparable => throw new NotImplementedException(); + public static NpgsqlRange Except(this NpgsqlRange a, NpgsqlRange b) where T : IComparable => throw new NotSupportedException(); } } diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlBinaryOperatorAttribute.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlBinaryOperatorAttribute.cs deleted file mode 100644 index 2ef20ed41..000000000 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlBinaryOperatorAttribute.cs +++ /dev/null @@ -1,81 +0,0 @@ -#region License - -// The PostgreSQL License -// -// Copyright (C) 2016 The Npgsql Development Team -// -// Permission to use, copy, modify, and distribute this software and its -// documentation for any purpose, without fee, and without a written -// agreement is hereby granted, provided that the above copyright notice -// and this paragraph and the following two paragraphs appear in all copies. -// -// IN NO EVENT SHALL THE NPGSQL DEVELOPMENT TEAM BE LIABLE TO ANY PARTY -// FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, -// INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS -// DOCUMENTATION, EVEN IF THE NPGSQL DEVELOPMENT TEAM HAS BEEN ADVISED OF -// THE POSSIBILITY OF SUCH DAMAGE. -// -// THE NPGSQL DEVELOPMENT TEAM SPECIFICALLY DISCLAIMS ANY WARRANTIES, -// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY -// AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS -// ON AN "AS IS" BASIS, AND THE NPGSQL DEVELOPMENT TEAM HAS NO OBLIGATIONS -// TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. - -#endregion - -using System; -using System.Linq.Expressions; -using JetBrains.Annotations; -using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Utilities; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Internal -{ - /// - /// Represents information about a PostgreSQL operator. - /// - /// - /// This attribute stores metadata describing the representation of a binary operator - /// in PostgreSQL on enum members representing the available binary operators for a PostgreSQL type. - /// - [AttributeUsage(AttributeTargets.Field)] - internal class NpgsqlBinaryOperatorAttribute : Attribute - { - /// - /// The operator symbol. - /// - [NotNull] - public string Symbol { get; set; } = string.Empty; - - /// - /// The operator represented by the method. - /// - [NotNull] - public Type ReturnType { get; set; } = typeof(void); - - /// - /// Creates a representing the operator. - /// - /// - /// The left-hand expression. - /// - /// - /// The right-hand expression. - /// - /// - /// A representing the operator. - /// - /// - [NotNull] - public Expression Create([NotNull] Expression left, [NotNull] Expression right) - { - Check.NotNull(left, nameof(left)); - Check.NotNull(right, nameof(right)); - - // TODO: will the left type arguments always be valid? - Type type = ReturnType.IsGenericType ? ReturnType.MakeGenericType(left.Type.GetGenericArguments()) : ReturnType; - - return new CustomBinaryExpression(left, right, Symbol, type); - } - } -} diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlCompositeMethodCallTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlCompositeMethodCallTranslator.cs index b8ff5875c..3011d4b67 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlCompositeMethodCallTranslator.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlCompositeMethodCallTranslator.cs @@ -51,7 +51,7 @@ public class NpgsqlCompositeMethodCallTranslator : RelationalCompositeMethodCall new NpgsqlStringTrimStartTranslator(), new NpgsqlRegexIsMatchTranslator(), new NpgsqlFullTextSearchMethodTranslator(), - new NpgsqlRangeOperatorTranslator() + new NpgsqlRangeTranslator() }; public NpgsqlCompositeMethodCallTranslator( diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRangeOperatorTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRangeOperatorTranslator.cs deleted file mode 100644 index 0e727166c..000000000 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRangeOperatorTranslator.cs +++ /dev/null @@ -1,189 +0,0 @@ -#region License - -// The PostgreSQL License -// -// Copyright (C) 2016 The Npgsql Development Team -// -// Permission to use, copy, modify, and distribute this software and its -// documentation for any purpose, without fee, and without a written -// agreement is hereby granted, provided that the above copyright notice -// and this paragraph and the following two paragraphs appear in all copies. -// -// IN NO EVENT SHALL THE NPGSQL DEVELOPMENT TEAM BE LIABLE TO ANY PARTY -// FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, -// INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS -// DOCUMENTATION, EVEN IF THE NPGSQL DEVELOPMENT TEAM HAS BEEN ADVISED OF -// THE POSSIBILITY OF SUCH DAMAGE. -// -// THE NPGSQL DEVELOPMENT TEAM SPECIFICALLY DISCLAIMS ANY WARRANTIES, -// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY -// AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS -// ON AN "AS IS" BASIS, AND THE NPGSQL DEVELOPMENT TEAM HAS NO OBLIGATIONS -// TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. - -#endregion - -using System; -using System.Collections.Generic; -using System.Linq.Expressions; -using System.Reflection; -using JetBrains.Annotations; -using Microsoft.EntityFrameworkCore.Query.ExpressionTranslators; -using Npgsql.EntityFrameworkCore.PostgreSQL.Utilities; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Internal -{ - /// - /// Translates a method call into a PostgreSQL range operator. - /// - /// - /// By default, this class supports translation for methods declared on . - /// Additional methods can be supported so long as they declare a . - /// - public class NpgsqlRangeOperatorTranslator : IMethodCallTranslator - { - /// - /// Constructs a from two arguments. - /// - /// - /// The left-hand argument. - /// - /// - /// The left-hand argument. - /// - [NotNull] - delegate Expression BinaryExpressionFunction([NotNull] Expression left, [NotNull] Expression right); - - /// - /// Maps the generic definitions of the methods supported by this translator to the appropriate PostgreSQL operator. - /// - [NotNull] static readonly Dictionary SupportedMethodTranslations; - - /// - /// Initialize the with extension methods from . - /// - static NpgsqlRangeOperatorTranslator() - { - SupportedMethodTranslations = new Dictionary(); - - foreach (MethodInfo methodInfo in typeof(NpgsqlRangeExtensions).GetRuntimeMethods()) - { - TryAddSupportedMethod(methodInfo); - } - } - - /// - /// Adds a method that can be translated as a PostgreSQL range operator. This method must declare a . - /// - /// - /// The method to register. - /// - /// - public static void AddSupportedMethod([NotNull] MethodInfo methodInfo) - { - Check.NotNull(methodInfo, nameof(methodInfo)); - - NpgsqlRangeOperatorAttribute attribute = - methodInfo.GetCustomAttribute(); - - if (attribute is null) - throw new ArgumentException($"{nameof(NpgsqlRangeOperatorAttribute)} is not declared on the method '{methodInfo.Name}' of {nameof(methodInfo.DeclaringType)}."); - - NpgsqlBinaryOperatorAttribute operatorAttribute = - typeof(NpgsqlRangeOperatorType) - .GetRuntimeField(attribute.OperatorType.ToString()) - .GetCustomAttribute(); - - if (operatorAttribute is null) - throw new ArgumentException($"{nameof(NpgsqlBinaryOperatorAttribute)} is not declared on the member '{attribute.OperatorType}' of {nameof(NpgsqlRangeOperatorType)}"); - - SupportedMethodTranslations.Add(methodInfo, (a, b) => operatorAttribute.Create(a, b)); - } - - /// - /// Attempts to add a method that can be translated as a PostgreSQL range operator. This method must declare a . - /// - /// - /// The method to register. - /// - /// - /// True if the method was added successfully; otherwise, false. - /// - public static bool TryAddSupportedMethod([NotNull] MethodInfo methodInfo) - { - Check.NotNull(methodInfo, nameof(methodInfo)); - - try - { - AddSupportedMethod(methodInfo); - return true; - } - catch (Exception) - { - return false; - } - } - - /// - /// Removes a method from the list of supported translations. - /// - /// - /// The method to unregister. - /// - /// - [UsedImplicitly] - public static void RemoveSupportedMethod([NotNull] MethodInfo methodInfo) - { - Check.NotNull(methodInfo, nameof(methodInfo)); - - SupportedMethodTranslations.Remove(methodInfo); - } - - /// - /// Attempts to remove a method from the list of supported translations. - /// - /// - /// The method to unregister. - /// - /// - [UsedImplicitly] - public static bool TryRemoveSupportedMethod([NotNull] MethodInfo methodInfo) - { - Check.NotNull(methodInfo, nameof(methodInfo)); - - try - { - RemoveSupportedMethod(methodInfo); - return true; - } - catch (Exception) - { - return false; - } - } - - /// - [CanBeNull] - public Expression Translate(MethodCallExpression methodCallExpression) => - TryGetSupportedTranslation(methodCallExpression.Method, out BinaryExpressionFunction operatorFunction) - ? operatorFunction(methodCallExpression.Arguments[0], methodCallExpression.Arguments[1]) - : null; - - /// - /// Tries to find create a by looking up the in . - /// - /// - /// The for lookup. - /// - /// - /// A delegate that constructs a . - /// - /// - /// True if the was found or registered; otherwise false. - /// - static bool TryGetSupportedTranslation([NotNull] MethodInfo methodInfo, out BinaryExpressionFunction operatorFunction) => - methodInfo.IsGenericMethod - ? SupportedMethodTranslations.TryGetValue(methodInfo.GetGenericMethodDefinition(), out operatorFunction) - : SupportedMethodTranslations.TryGetValue(methodInfo, out operatorFunction); - } -} diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRangeTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRangeTranslator.cs new file mode 100644 index 000000000..e58427d9f --- /dev/null +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRangeTranslator.cs @@ -0,0 +1,108 @@ +#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.Expressions; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Query.ExpressionTranslators; +using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Internal +{ + /// + /// Provides translation services for PostgreSQL range operators. + /// + /// + /// See: https://www.postgresql.org/docs/current/static/functions-range.html + /// + public class NpgsqlRangeTranslator : IMethodCallTranslator + { + /// + [CanBeNull] + public Expression Translate(MethodCallExpression methodCallExpression) => + TryTranslateOperator(methodCallExpression); + + /// + /// Attempts to translate the as a PostgreSQL range operator. + /// + /// The to be translated. + /// + /// The expression if successful; otherwise, null. + /// + static Expression TryTranslateOperator([NotNull] MethodCallExpression expression) + { + switch (expression.Method.Name) + { + case nameof(NpgsqlRangeExtensions.Contains): + return MakeBinaryExpression(expression, "@>", typeof(bool)); + + case nameof(NpgsqlRangeExtensions.ContainedBy): + return MakeBinaryExpression(expression, "<@", typeof(bool)); + + case nameof(NpgsqlRangeExtensions.Overlaps): + return MakeBinaryExpression(expression, "&&", typeof(bool)); + + case nameof(NpgsqlRangeExtensions.IsStrictlyLeftOf): + return MakeBinaryExpression(expression, "<<", typeof(bool)); + + case nameof(NpgsqlRangeExtensions.IsStrictlyRightOf): + return MakeBinaryExpression(expression, ">>", typeof(bool)); + + case nameof(NpgsqlRangeExtensions.DoesNotExtendRightOf): + return MakeBinaryExpression(expression, "&<", typeof(bool)); + + case nameof(NpgsqlRangeExtensions.DoesNotExtendLeftOf): + return MakeBinaryExpression(expression, "&>", typeof(bool)); + + case nameof(NpgsqlRangeExtensions.IsAdjacentTo): + return MakeBinaryExpression(expression, "-|-", typeof(bool)); + + case nameof(NpgsqlRangeExtensions.Union): + return MakeBinaryExpression(expression, "+", expression.Arguments[0].Type); + + case nameof(NpgsqlRangeExtensions.Intersect): + return MakeBinaryExpression(expression, "*", expression.Arguments[0].Type); + + case nameof(NpgsqlRangeExtensions.Except): + return MakeBinaryExpression(expression, "-", expression.Arguments[0].Type); + + default: + return null; + } + } + + /// + /// Constructs a . + /// + /// The containing two parameters. + /// The symbolic operator for PostgreSQL. + /// The return type of the operator. + /// + /// A . + /// + static Expression MakeBinaryExpression([NotNull] MethodCallExpression expression, [NotNull] string symbol, [NotNull] Type returnType) => + new CustomBinaryExpression(expression.Arguments[0], expression.Arguments[1], symbol, returnType); + } +} diff --git a/src/EFCore.PG/Query/ExpressionTranslators/NpgsqlRangeOperatorAttribute.cs b/src/EFCore.PG/Query/ExpressionTranslators/NpgsqlRangeOperatorAttribute.cs deleted file mode 100644 index 1eed954c8..000000000 --- a/src/EFCore.PG/Query/ExpressionTranslators/NpgsqlRangeOperatorAttribute.cs +++ /dev/null @@ -1,59 +0,0 @@ -#region License - -// The PostgreSQL License -// -// Copyright (C) 2016 The Npgsql Development Team -// -// Permission to use, copy, modify, and distribute this software and its -// documentation for any purpose, without fee, and without a written -// agreement is hereby granted, provided that the above copyright notice -// and this paragraph and the following two paragraphs appear in all copies. -// -// IN NO EVENT SHALL THE NPGSQL DEVELOPMENT TEAM BE LIABLE TO ANY PARTY -// FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, -// INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS -// DOCUMENTATION, EVEN IF THE NPGSQL DEVELOPMENT TEAM HAS BEEN ADVISED OF -// THE POSSIBILITY OF SUCH DAMAGE. -// -// THE NPGSQL DEVELOPMENT TEAM SPECIFICALLY DISCLAIMS ANY WARRANTIES, -// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY -// AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS -// ON AN "AS IS" BASIS, AND THE NPGSQL DEVELOPMENT TEAM HAS NO OBLIGATIONS -// TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. - -#endregion - -using System; -using NpgsqlTypes; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators -{ - /// - /// Indicates that a method can be translated to a PostgreSQL range operator. - /// - /// - /// This attribute allows other extension methods to hook into the range operator translations. - /// Along with simplifying the code required to identify overloaded generics, this attribute provides - /// a transparent way in which to transition from extension methods in the EF Core assembly to - /// instance methods on . - /// - [AttributeUsage(AttributeTargets.Method, Inherited = false)] - public class NpgsqlRangeOperatorAttribute : Attribute - { - /// - /// The operator represented by the method. - /// - public NpgsqlRangeOperatorType OperatorType { get; } - - /// - /// Indicates that a method can be translated to a PostgreSQL range operator. - /// - /// - /// The type of operator the method represents. - /// - public NpgsqlRangeOperatorAttribute(NpgsqlRangeOperatorType operatorType) - { - OperatorType = operatorType; - } - } -} diff --git a/src/EFCore.PG/Query/ExpressionTranslators/NpgsqlRangeOperatorType.cs b/src/EFCore.PG/Query/ExpressionTranslators/NpgsqlRangeOperatorType.cs deleted file mode 100644 index 1f3c0d2e1..000000000 --- a/src/EFCore.PG/Query/ExpressionTranslators/NpgsqlRangeOperatorType.cs +++ /dev/null @@ -1,131 +0,0 @@ -#region License - -// The PostgreSQL License -// -// Copyright (C) 2016 The Npgsql Development Team -// -// Permission to use, copy, modify, and distribute this software and its -// documentation for any purpose, without fee, and without a written -// agreement is hereby granted, provided that the above copyright notice -// and this paragraph and the following two paragraphs appear in all copies. -// -// IN NO EVENT SHALL THE NPGSQL DEVELOPMENT TEAM BE LIABLE TO ANY PARTY -// FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, -// INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS -// DOCUMENTATION, EVEN IF THE NPGSQL DEVELOPMENT TEAM HAS BEEN ADVISED OF -// THE POSSIBILITY OF SUCH DAMAGE. -// -// THE NPGSQL DEVELOPMENT TEAM SPECIFICALLY DISCLAIMS ANY WARRANTIES, -// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY -// AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS -// ON AN "AS IS" BASIS, AND THE NPGSQL DEVELOPMENT TEAM HAS NO OBLIGATIONS -// TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. - -#endregion - -using JetBrains.Annotations; -using Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Internal; -using NpgsqlTypes; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators -{ - /// - /// Describes the operator type of a range expression. - /// - /// - /// See: https://www.postgresql.org/docs/current/static/functions-range.html - /// - [PublicAPI] - public enum NpgsqlRangeOperatorType - { - /// - /// No operator specified. - /// - [NpgsqlBinaryOperator] None, - - /// - /// The = operator. - /// - [NpgsqlBinaryOperator(Symbol = "=", ReturnType = typeof(bool))] Equal, - - /// - /// The <> operator. - /// - [NpgsqlBinaryOperator(Symbol = "<>", ReturnType = typeof(bool))] NotEqual, - - /// - /// The < operator. - /// - [NpgsqlBinaryOperator(Symbol = "<", ReturnType = typeof(bool))] LessThan, - - /// - /// The > operator. - /// - [NpgsqlBinaryOperator(Symbol = ">", ReturnType = typeof(bool))] GreaterThan, - - /// - /// The <= operator. - /// - [NpgsqlBinaryOperator(Symbol = "<=", ReturnType = typeof(bool))] LessThanOrEqual, - - /// - /// The >= operator. - /// - [NpgsqlBinaryOperator(Symbol = ">=", ReturnType = typeof(bool))] GreaterThanOrEqual, - - /// - /// The @> operator. - /// - [NpgsqlBinaryOperator(Symbol = "@>", ReturnType = typeof(bool))] Contains, - - /// - /// The <@ operator. - /// - [NpgsqlBinaryOperator(Symbol = "<@", ReturnType = typeof(bool))] ContainedBy, - - /// - /// The && operator. - /// - [NpgsqlBinaryOperator(Symbol = "&&", ReturnType = typeof(bool))] Overlaps, - - /// - /// The << operator. - /// - [NpgsqlBinaryOperator(Symbol = "<<", ReturnType = typeof(bool))] IsStrictlyLeftOf, - - /// - /// The >> operator. - /// - [NpgsqlBinaryOperator(Symbol = ">>", ReturnType = typeof(bool))] IsStrictlyRightOf, - - /// - /// The &< operator. - /// - [NpgsqlBinaryOperator(Symbol = "&<", ReturnType = typeof(bool))] DoesNotExtendRightOf, - - /// - /// The &> operator. - /// - [NpgsqlBinaryOperator(Symbol = "&>", ReturnType = typeof(bool))] DoesNotExtendLeftOf, - - /// - /// The -|- operator. - /// - [NpgsqlBinaryOperator(Symbol = "-|-", ReturnType = typeof(bool))] IsAdjacentTo, - - /// - /// The + operator. - /// - [NpgsqlBinaryOperator(Symbol = "+", ReturnType = typeof(NpgsqlRange<>))] Union, - - /// - /// The * operator. - /// - [NpgsqlBinaryOperator(Symbol = "*", ReturnType = typeof(NpgsqlRange<>))] Intersection, - - /// - /// The - operator. - /// - [NpgsqlBinaryOperator(Symbol = "-", ReturnType = typeof(NpgsqlRange<>))] Difference - } -} diff --git a/test/EFCore.PG.FunctionalTests/Query/RangeQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/RangeQueryNpgsqlTest.cs index cc359e759..b70a11676 100644 --- a/test/EFCore.PG.FunctionalTests/Query/RangeQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/RangeQueryNpgsqlTest.cs @@ -45,15 +45,22 @@ public class RangeQueryNpgsqlTest : IClassFixture /// /// Initializes resources for unit tests. /// - /// - /// The fixture of resources for testing. - /// + /// The fixture of resources for testing. public RangeQueryNpgsqlTest(RangeQueryNpgsqlFixture fixture) { Fixture = fixture; Fixture.TestSqlLoggerFactory.Clear(); } + /// + /// Asserts that the SQL fragment appears in the logs. + /// + /// The SQL statement or fragment to search for in the logs. + public void AssertContainsSql(string sql) + { + Assert.Contains(sql, Fixture.TestSqlLoggerFactory.Sql); + } + /// /// Tests translation for . /// @@ -68,7 +75,7 @@ public void RangeContainsRange(NpgsqlRange range) .Where(x => x.Range.Contains(range)) .ToArray(); - Assert.Contains("WHERE x.\"Range\" @> @__range_0 = TRUE", Fixture.TestSqlLoggerFactory.Sql); + AssertContainsSql("WHERE x.\"Range\" @> @__range_0 = TRUE"); } } @@ -86,7 +93,7 @@ public void RangeDoesNotContainRange(NpgsqlRange range) .Where(x => !x.Range.Contains(range)) .ToArray(); - Assert.Contains("WHERE NOT (x.\"Range\" @> @__range_0 = TRUE)", Fixture.TestSqlLoggerFactory.Sql); + AssertContainsSql("WHERE NOT (x.\"Range\" @> @__range_0 = TRUE)"); } } @@ -104,7 +111,7 @@ public void RangeContainsValue(int value) .Where(x => x.Range.Contains(value)) .ToArray(); - Assert.Contains("WHERE x.\"Range\" @> @__value_0", Fixture.TestSqlLoggerFactory.Sql); + AssertContainsSql("WHERE x.\"Range\" @> @__value_0"); } } @@ -122,7 +129,7 @@ public void RangeDoesNotContainValue(int value) .Where(x => !x.Range.Contains(value)) .ToArray(); - Assert.Contains("WHERE NOT (x.\"Range\" @> @__value_0 = TRUE)", Fixture.TestSqlLoggerFactory.Sql); + AssertContainsSql("WHERE NOT (x.\"Range\" @> @__value_0 = TRUE)"); } } @@ -140,7 +147,7 @@ public void RangeContainedByRange(NpgsqlRange range) .Where(x => range.ContainedBy(x.Range)) .ToArray(); - Assert.Contains("WHERE @__range_0 <@ x.\"Range\" = TRUE", Fixture.TestSqlLoggerFactory.Sql); + AssertContainsSql("WHERE @__range_0 <@ x.\"Range\" = TRUE"); } } @@ -158,7 +165,7 @@ public void RangeNotContainedByRange(NpgsqlRange range) .Where(x => !range.ContainedBy(x.Range)) .ToArray(); - Assert.Contains("WHERE NOT (@__range_0 <@ x.\"Range\" = TRUE)", Fixture.TestSqlLoggerFactory.Sql); + AssertContainsSql("WHERE NOT (@__range_0 <@ x.\"Range\" = TRUE)"); } } @@ -176,7 +183,7 @@ public void RangeEqualsRange_Operator(NpgsqlRange range) .Where(x => x.Range == range) .ToArray(); - Assert.Contains("WHERE x.\"Range\" = @__range_0", Fixture.TestSqlLoggerFactory.Sql); + AssertContainsSql("WHERE x.\"Range\" = @__range_0"); } } @@ -194,7 +201,7 @@ public void RangeEqualsRange_Method(NpgsqlRange range) .Where(x => x.Range.Equals(range)) .ToArray(); - Assert.Contains("WHERE x.\"Range\" = @__range_0", Fixture.TestSqlLoggerFactory.Sql); + AssertContainsSql("WHERE x.\"Range\" = @__range_0"); } } @@ -212,7 +219,7 @@ public void RangeDoesNotEqualsRange_Operator(NpgsqlRange range) .Where(x => x.Range != range) .ToArray(); - Assert.Contains("WHERE x.\"Range\" <> @__range_0", Fixture.TestSqlLoggerFactory.Sql); + AssertContainsSql("WHERE x.\"Range\" <> @__range_0"); } } @@ -230,7 +237,7 @@ public void RangeDoesNotEqualsRange_Method(NpgsqlRange range) .Where(x => !x.Range.Equals(range)) .ToArray(); - Assert.Contains("WHERE x.\"Range\" <> @__range_0", Fixture.TestSqlLoggerFactory.Sql); + AssertContainsSql("WHERE x.\"Range\" <> @__range_0"); } } @@ -248,7 +255,7 @@ public void RangeOvelapsRange(NpgsqlRange range) .Where(x => x.Range.Overlaps(range)) .ToArray(); - Assert.Contains("WHERE x.\"Range\" && @__range_0", Fixture.TestSqlLoggerFactory.Sql); + AssertContainsSql("WHERE x.\"Range\" && @__range_0"); } } @@ -266,7 +273,7 @@ public void RangeDoesNotOvelapRange(NpgsqlRange range) .Where(x => !x.Range.Overlaps(range)) .ToArray(); - Assert.Contains("WHERE NOT (x.\"Range\" && @__range_0 = TRUE)", Fixture.TestSqlLoggerFactory.Sql); + AssertContainsSql("WHERE NOT (x.\"Range\" && @__range_0 = TRUE)"); } } @@ -284,7 +291,7 @@ public void RangeIsStrictlyLeftOfRange(NpgsqlRange range) .Where(x => x.Range.IsStrictlyLeftOf(range)) .ToArray(); - Assert.Contains("WHERE x.\"Range\" << @__range_0", Fixture.TestSqlLoggerFactory.Sql); + AssertContainsSql("WHERE x.\"Range\" << @__range_0"); } } @@ -302,7 +309,7 @@ public void RangeIsNotStrictlyLeftOfRange(NpgsqlRange range) .Where(x => !x.Range.IsStrictlyLeftOf(range)) .ToArray(); - Assert.Contains("WHERE NOT (x.\"Range\" << @__range_0 = TRUE)", Fixture.TestSqlLoggerFactory.Sql); + AssertContainsSql("WHERE NOT (x.\"Range\" << @__range_0 = TRUE)"); } } @@ -320,7 +327,7 @@ public void RangeIsStrictlyRightOfRange(NpgsqlRange range) .Where(x => x.Range.IsStrictlyRightOf(range)) .ToArray(); - Assert.Contains("WHERE x.\"Range\" >> @__range_0", Fixture.TestSqlLoggerFactory.Sql); + AssertContainsSql("WHERE x.\"Range\" >> @__range_0"); } } @@ -338,7 +345,7 @@ public void RangeIsNotStrictlyRightOfRange(NpgsqlRange range) .Where(x => !x.Range.IsStrictlyRightOf(range)) .ToArray(); - Assert.Contains("WHERE NOT (x.\"Range\" >> @__range_0 = TRUE)", Fixture.TestSqlLoggerFactory.Sql); + AssertContainsSql("WHERE NOT (x.\"Range\" >> @__range_0 = TRUE)"); } } @@ -356,7 +363,7 @@ public void RangeDoesNotExtendLeftOfRange(NpgsqlRange range) .Where(x => x.Range.DoesNotExtendLeftOf(range)) .ToArray(); - Assert.Contains("WHERE x.\"Range\" &> @__range_0", Fixture.TestSqlLoggerFactory.Sql); + AssertContainsSql("WHERE x.\"Range\" &> @__range_0"); } } @@ -374,7 +381,7 @@ public void RangeDoesExtendLeftOfRange(NpgsqlRange range) .Where(x => !x.Range.DoesNotExtendLeftOf(range)) .ToArray(); - Assert.Contains("WHERE NOT (x.\"Range\" &> @__range_0 = TRUE)", Fixture.TestSqlLoggerFactory.Sql); + AssertContainsSql("WHERE NOT (x.\"Range\" &> @__range_0 = TRUE)"); } } @@ -392,7 +399,7 @@ public void RangeDoesNotExtendRightOfRange(NpgsqlRange range) .Where(x => x.Range.DoesNotExtendRightOf(range)) .ToArray(); - Assert.Contains("WHERE x.\"Range\" &< @__range_0", Fixture.TestSqlLoggerFactory.Sql); + AssertContainsSql("WHERE x.\"Range\" &< @__range_0"); } } @@ -410,7 +417,7 @@ public void RangeDoesExtendRightOfRange(NpgsqlRange range) .Where(x => !x.Range.DoesNotExtendRightOf(range)) .ToArray(); - Assert.Contains("WHERE NOT (x.\"Range\" &< @__range_0 = TRUE)", Fixture.TestSqlLoggerFactory.Sql); + AssertContainsSql("WHERE NOT (x.\"Range\" &< @__range_0 = TRUE)"); } } @@ -428,7 +435,7 @@ public void RangeIsAdjacentToRange(NpgsqlRange range) .Where(x => x.Range.IsAdjacentTo(range)) .ToArray(); - Assert.Contains("WHERE x.\"Range\" -|- @__range_0", Fixture.TestSqlLoggerFactory.Sql); + AssertContainsSql("WHERE x.\"Range\" -|- @__range_0"); } } @@ -446,7 +453,7 @@ public void RangeIsNotAdjacentToRange(NpgsqlRange range) .Where(x => !x.Range.IsAdjacentTo(range)) .ToArray(); - Assert.Contains("WHERE NOT (x.\"Range\" -|- @__range_0 = TRUE)", Fixture.TestSqlLoggerFactory.Sql); + AssertContainsSql("WHERE NOT (x.\"Range\" -|- @__range_0 = TRUE)"); } } @@ -464,7 +471,7 @@ public void RangeUnionRange(NpgsqlRange range) .Select(x => x.Range.Union(range)) .ToArray(); - Assert.Contains("SELECT x.\"Range\" + @__range_0", Fixture.TestSqlLoggerFactory.Sql); + AssertContainsSql("SELECT x.\"Range\" + @__range_0"); } } @@ -482,7 +489,7 @@ public void RangeIntersectsRange(NpgsqlRange range) .Select(x => x.Range.Intersect(range)) .ToArray(); - Assert.Contains("SELECT x.\"Range\" * @__range_0", Fixture.TestSqlLoggerFactory.Sql); + AssertContainsSql("SELECT x.\"Range\" * @__range_0"); } } @@ -507,7 +514,7 @@ public void RangeExceptRange(NpgsqlRange range) // ignore: Npgsql.PostgresException : 22000: result of range difference would not be contiguous. } - Assert.Contains("SELECT x.\"Range\" - @__range_0", Fixture.TestSqlLoggerFactory.Sql); + AssertContainsSql("SELECT x.\"Range\" - @__range_0"); } } } From ed0b754ec039ebea19b33b3f4a83c5b18199182d Mon Sep 17 00:00:00 2001 From: Austin Drenski Date: Tue, 15 May 2018 09:33:48 -0400 Subject: [PATCH 5/6] Fixing up some annotations for consistency. --- .../ExpressionTranslators/Internal/NpgsqlRangeTranslator.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRangeTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRangeTranslator.cs index e58427d9f..30abcf839 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRangeTranslator.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRangeTranslator.cs @@ -51,6 +51,7 @@ public Expression Translate(MethodCallExpression methodCallExpression) => /// /// The expression if successful; otherwise, null. /// + [CanBeNull] static Expression TryTranslateOperator([NotNull] MethodCallExpression expression) { switch (expression.Method.Name) @@ -102,6 +103,7 @@ static Expression TryTranslateOperator([NotNull] MethodCallExpression expression /// /// A . /// + [NotNull] static Expression MakeBinaryExpression([NotNull] MethodCallExpression expression, [NotNull] string symbol, [NotNull] Type returnType) => new CustomBinaryExpression(expression.Arguments[0], expression.Arguments[1], symbol, returnType); } From fc9ff223d650b2f903a6216cdbb7a2303a65d27f Mon Sep 17 00:00:00 2001 From: Austin Drenski Date: Tue, 15 May 2018 12:51:51 -0400 Subject: [PATCH 6/6] Addressing code review comments #2 (2018-05-15). --- .../Internal/NpgsqlRangeTranslator.cs | 49 +--- .../Query/RangeQueryNpgsqlFixture.cs | 158 ------------ .../Query/RangeQueryNpgsqlTest.cs | 244 +++++++++++++++--- 3 files changed, 219 insertions(+), 232 deletions(-) delete mode 100644 test/EFCore.PG.FunctionalTests/Query/RangeQueryNpgsqlFixture.cs diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRangeTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRangeTranslator.cs index 30abcf839..39127c444 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRangeTranslator.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRangeTranslator.cs @@ -23,7 +23,6 @@ #endregion -using System; using System.Linq.Expressions; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Query.ExpressionTranslators; @@ -41,70 +40,46 @@ public class NpgsqlRangeTranslator : IMethodCallTranslator { /// [CanBeNull] - public Expression Translate(MethodCallExpression methodCallExpression) => - TryTranslateOperator(methodCallExpression); - - /// - /// Attempts to translate the as a PostgreSQL range operator. - /// - /// The to be translated. - /// - /// The expression if successful; otherwise, null. - /// - [CanBeNull] - static Expression TryTranslateOperator([NotNull] MethodCallExpression expression) + public Expression Translate(MethodCallExpression expression) { switch (expression.Method.Name) { case nameof(NpgsqlRangeExtensions.Contains): - return MakeBinaryExpression(expression, "@>", typeof(bool)); + return new CustomBinaryExpression(expression.Arguments[0], expression.Arguments[1], "@>", typeof(bool)); case nameof(NpgsqlRangeExtensions.ContainedBy): - return MakeBinaryExpression(expression, "<@", typeof(bool)); + return new CustomBinaryExpression(expression.Arguments[0], expression.Arguments[1], "<@", typeof(bool)); case nameof(NpgsqlRangeExtensions.Overlaps): - return MakeBinaryExpression(expression, "&&", typeof(bool)); + return new CustomBinaryExpression(expression.Arguments[0], expression.Arguments[1], "&&", typeof(bool)); case nameof(NpgsqlRangeExtensions.IsStrictlyLeftOf): - return MakeBinaryExpression(expression, "<<", typeof(bool)); + return new CustomBinaryExpression(expression.Arguments[0], expression.Arguments[1], "<<", typeof(bool)); case nameof(NpgsqlRangeExtensions.IsStrictlyRightOf): - return MakeBinaryExpression(expression, ">>", typeof(bool)); + return new CustomBinaryExpression(expression.Arguments[0], expression.Arguments[1], ">>", typeof(bool)); case nameof(NpgsqlRangeExtensions.DoesNotExtendRightOf): - return MakeBinaryExpression(expression, "&<", typeof(bool)); + return new CustomBinaryExpression(expression.Arguments[0], expression.Arguments[1], "&<", typeof(bool)); case nameof(NpgsqlRangeExtensions.DoesNotExtendLeftOf): - return MakeBinaryExpression(expression, "&>", typeof(bool)); + return new CustomBinaryExpression(expression.Arguments[0], expression.Arguments[1], "&>", typeof(bool)); case nameof(NpgsqlRangeExtensions.IsAdjacentTo): - return MakeBinaryExpression(expression, "-|-", typeof(bool)); + return new CustomBinaryExpression(expression.Arguments[0], expression.Arguments[1], "-|-", typeof(bool)); case nameof(NpgsqlRangeExtensions.Union): - return MakeBinaryExpression(expression, "+", expression.Arguments[0].Type); + return new CustomBinaryExpression(expression.Arguments[0], expression.Arguments[1], "+", expression.Arguments[0].Type); case nameof(NpgsqlRangeExtensions.Intersect): - return MakeBinaryExpression(expression, "*", expression.Arguments[0].Type); + return new CustomBinaryExpression(expression.Arguments[0], expression.Arguments[1], "*", expression.Arguments[0].Type); case nameof(NpgsqlRangeExtensions.Except): - return MakeBinaryExpression(expression, "-", expression.Arguments[0].Type); + return new CustomBinaryExpression(expression.Arguments[0], expression.Arguments[1], "-", expression.Arguments[0].Type); default: return null; } } - - /// - /// Constructs a . - /// - /// The containing two parameters. - /// The symbolic operator for PostgreSQL. - /// The return type of the operator. - /// - /// A . - /// - [NotNull] - static Expression MakeBinaryExpression([NotNull] MethodCallExpression expression, [NotNull] string symbol, [NotNull] Type returnType) => - new CustomBinaryExpression(expression.Arguments[0], expression.Arguments[1], symbol, returnType); } } diff --git a/test/EFCore.PG.FunctionalTests/Query/RangeQueryNpgsqlFixture.cs b/test/EFCore.PG.FunctionalTests/Query/RangeQueryNpgsqlFixture.cs deleted file mode 100644 index 9ee91b1e8..000000000 --- a/test/EFCore.PG.FunctionalTests/Query/RangeQueryNpgsqlFixture.cs +++ /dev/null @@ -1,158 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.TestUtilities; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities; -using NpgsqlTypes; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query -{ - /// - /// Represents a fixture suitable for testing range operators. - /// - public class RangeQueryNpgsqlFixture : IDisposable - { - /// - /// The used for testing. - /// - private readonly NpgsqlTestStore _testStore; - - /// - /// The used for testing. - /// - private readonly DbContextOptions _options; - - /// - /// The logger factory used for testing. - /// - public TestSqlLoggerFactory TestSqlLoggerFactory { get; } - - /// - /// Initializes a . - /// - // ReSharper disable once UnusedMember.Global - public RangeQueryNpgsqlFixture() - { - TestSqlLoggerFactory = new TestSqlLoggerFactory(); - - _testStore = NpgsqlTestStore.CreateScratch(); - - _options = - new DbContextOptionsBuilder() - .UseNpgsql(_testStore.ConnectionString, b => b.ApplyConfiguration()) - .UseInternalServiceProvider( - new ServiceCollection() - .AddEntityFrameworkNpgsql() - .AddSingleton(TestSqlLoggerFactory) - .BuildServiceProvider()) - .Options; - - using (RangeContext context = CreateContext()) - { - context.Database.EnsureCreated(); - - context.RangeTestEntities.AddRange( - new RangeTestEntity - { - Id = 1, - // (0, 10) - Range = new NpgsqlRange(0, false, false, 10, false, false), - }, - new RangeTestEntity - { - Id = 2, - // [0, 10) - Range = new NpgsqlRange(0, true, false, 10, false, false) - }, - new RangeTestEntity - { - Id = 3, - // [0, 10] - Range = new NpgsqlRange(0, true, false, 10, true, false) - }, - new RangeTestEntity - { - Id = 4, - // [0, ∞) - Range = new NpgsqlRange(0, true, false, 0, false, true) - }, - new RangeTestEntity - { - Id = 5, - // (-∞, 10] - Range = new NpgsqlRange(0, false, true, 10, true, false) - }, - new RangeTestEntity - { - Id = 6, - // (-∞, ∞) - Range = new NpgsqlRange(0, false, true, 0, false, true) - }, - new RangeTestEntity - { - Id = 7, - // (-∞, ∞) - Range = new NpgsqlRange(0, false, true, 0, false, true) - }); - - context.SaveChanges(); - } - } - - /// - /// Creates a new . - /// - /// - /// A for testing. - /// - public RangeContext CreateContext() - { - return new RangeContext(_options); - } - - /// - public void Dispose() - { - _testStore.Dispose(); - } - } - - /// - /// Represents an entity suitable for testing range operators. - /// - public class RangeTestEntity - { - /// - /// The primary key. - /// - public int Id { get; set; } - - /// - /// The range of integers. - /// - public NpgsqlRange Range { get; set; } - } - - /// - /// Represents a database suitable for testing range operators. - /// - public class RangeContext : DbContext - { - /// - /// Represents a set of entities with properties. - /// - public DbSet RangeTestEntities { get; set; } - - /// - /// Initializes a . - /// - /// - /// The options to be used for configuration. - /// - public RangeContext(DbContextOptions options) : base(options) { } - - /// - protected override void OnModelCreating(ModelBuilder builder) { } - } -} diff --git a/test/EFCore.PG.FunctionalTests/Query/RangeQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/RangeQueryNpgsqlTest.cs index b70a11676..c31d32016 100644 --- a/test/EFCore.PG.FunctionalTests/Query/RangeQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/RangeQueryNpgsqlTest.cs @@ -1,5 +1,11 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.TestUtilities; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities; using NpgsqlTypes; using Xunit; @@ -8,40 +14,13 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query /// /// Provides unit tests for range operator translations. /// - public class RangeQueryNpgsqlTest : IClassFixture + public class RangeQueryNpgsqlTest : IClassFixture { /// /// Provides resources for unit tests. /// RangeQueryNpgsqlFixture Fixture { get; } - /// - /// Provides theory data for integers. - /// - public static IEnumerable IntegerTheoryData => Enumerable.Range(-10, 10).Select(x => new object[] { x }); - - /// - /// Provides theory data for ranges. - /// - public static IEnumerable RangeTheoryData => - new List - { - // (0,5) - new object[] { new NpgsqlRange(0, false, false, 5, false, false) }, - // [0,5] - new object[] { new NpgsqlRange(0, true, false, 5, true, false) }, - // (,) - new object[] { new NpgsqlRange(0, false, true, 0, false, true) }, - // (,) - new object[] { new NpgsqlRange(0, false, true, 5, false, true) }, - // (0,) - new object[] { new NpgsqlRange(0, false, false, 0, false, true) }, - // (0,) - new object[] { new NpgsqlRange(0, false, false, 5, false, true) }, - // (,5) - new object[] { new NpgsqlRange(0, false, true, 5, false, false) } - }; - /// /// Initializes resources for unit tests. /// @@ -52,14 +31,7 @@ public RangeQueryNpgsqlTest(RangeQueryNpgsqlFixture fixture) Fixture.TestSqlLoggerFactory.Clear(); } - /// - /// Asserts that the SQL fragment appears in the logs. - /// - /// The SQL statement or fragment to search for in the logs. - public void AssertContainsSql(string sql) - { - Assert.Contains(sql, Fixture.TestSqlLoggerFactory.Sql); - } + #region Tests /// /// Tests translation for . @@ -517,5 +489,203 @@ public void RangeExceptRange(NpgsqlRange range) AssertContainsSql("SELECT x.\"Range\" - @__range_0"); } } + + #endregion Tests + + #region TheoryData + + /// + /// Provides theory data for integers. + /// + public static IEnumerable IntegerTheoryData => Enumerable.Range(-10, 10).Select(x => new object[] { x }); + + /// + /// Provides theory data for ranges. + /// + public static IEnumerable RangeTheoryData => + new List + { + // (0,5) + new object[] { new NpgsqlRange(0, false, false, 5, false, false) }, + // [0,5] + new object[] { new NpgsqlRange(0, true, false, 5, true, false) }, + // (,) + new object[] { new NpgsqlRange(0, false, true, 0, false, true) }, + // (,) + new object[] { new NpgsqlRange(0, false, true, 5, false, true) }, + // (0,) + new object[] { new NpgsqlRange(0, false, false, 0, false, true) }, + // (0,) + new object[] { new NpgsqlRange(0, false, false, 5, false, true) }, + // (,5) + new object[] { new NpgsqlRange(0, false, true, 5, false, false) } + }; + + #endregion + + #region Fixtures + + /// + /// Represents a fixture suitable for testing range operators. + /// + public class RangeQueryNpgsqlFixture : IDisposable + { + /// + /// The used for testing. + /// + private readonly NpgsqlTestStore _testStore; + + /// + /// The used for testing. + /// + private readonly DbContextOptions _options; + + /// + /// The logger factory used for testing. + /// + public TestSqlLoggerFactory TestSqlLoggerFactory { get; } + + /// + /// Initializes a . + /// + // ReSharper disable once UnusedMember.Global + public RangeQueryNpgsqlFixture() + { + TestSqlLoggerFactory = new TestSqlLoggerFactory(); + + _testStore = NpgsqlTestStore.CreateScratch(); + + _options = + new DbContextOptionsBuilder() + .UseNpgsql(_testStore.ConnectionString, b => b.ApplyConfiguration()) + .UseInternalServiceProvider( + new ServiceCollection() + .AddEntityFrameworkNpgsql() + .AddSingleton(TestSqlLoggerFactory) + .BuildServiceProvider()) + .Options; + + using (RangeContext context = CreateContext()) + { + context.Database.EnsureCreated(); + + context.RangeTestEntities.AddRange( + new RangeTestEntity + { + Id = 1, + // (0, 10) + Range = new NpgsqlRange(0, false, false, 10, false, false), + }, + new RangeTestEntity + { + Id = 2, + // [0, 10) + Range = new NpgsqlRange(0, true, false, 10, false, false) + }, + new RangeTestEntity + { + Id = 3, + // [0, 10] + Range = new NpgsqlRange(0, true, false, 10, true, false) + }, + new RangeTestEntity + { + Id = 4, + // [0, ∞) + Range = new NpgsqlRange(0, true, false, 0, false, true) + }, + new RangeTestEntity + { + Id = 5, + // (-∞, 10] + Range = new NpgsqlRange(0, false, true, 10, true, false) + }, + new RangeTestEntity + { + Id = 6, + // (-∞, ∞) + Range = new NpgsqlRange(0, false, true, 0, false, true) + }, + new RangeTestEntity + { + Id = 7, + // (-∞, ∞) + Range = new NpgsqlRange(0, false, true, 0, false, true) + }); + + context.SaveChanges(); + } + } + + /// + /// Creates a new . + /// + /// + /// A for testing. + /// + public RangeContext CreateContext() + { + return new RangeContext(_options); + } + + /// + public void Dispose() + { + _testStore.Dispose(); + } + } + + /// + /// Represents an entity suitable for testing range operators. + /// + public class RangeTestEntity + { + /// + /// The primary key. + /// + public int Id { get; set; } + + /// + /// The range of integers. + /// + public NpgsqlRange Range { get; set; } + } + + /// + /// Represents a database suitable for testing range operators. + /// + public class RangeContext : DbContext + { + /// + /// Represents a set of entities with properties. + /// + public DbSet RangeTestEntities { get; set; } + + /// + /// Initializes a . + /// + /// + /// The options to be used for configuration. + /// + public RangeContext(DbContextOptions options) : base(options) { } + + /// + protected override void OnModelCreating(ModelBuilder builder) { } + } + + #endregion + + #region Helpers + + /// + /// Asserts that the SQL fragment appears in the logs. + /// + /// The SQL statement or fragment to search for in the logs. + public void AssertContainsSql(string sql) + { + Assert.Contains(sql, Fixture.TestSqlLoggerFactory.Sql); + } + + #endregion } }