From c673edc99defc5a3b87180b02feeb4a9a0f68fb8 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Mon, 5 Mar 2018 09:23:57 +0200 Subject: [PATCH] Fix StartsWith on citext Fixes #319 --- .../NpgsqlStringStartsWithTranslator.cs | 20 ++- .../ExplicitStoreTypeCastExpression.cs | 162 ++++++++++++++++++ .../Sql/Internal/NpgsqlQuerySqlGenerator.cs | 19 ++ 3 files changed, 196 insertions(+), 5 deletions(-) create mode 100644 src/EFCore.PG/Query/Expressions/Internal/ExplicitStoreTypeCastExpression.cs diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlStringStartsWithTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlStringStartsWithTranslator.cs index 6e358f6c2..f975d07b0 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlStringStartsWithTranslator.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlStringStartsWithTranslator.cs @@ -24,6 +24,7 @@ using System.Linq.Expressions; using System.Reflection; using System.Text.RegularExpressions; +using Microsoft.EntityFrameworkCore.Internal; using Microsoft.EntityFrameworkCore.Query.Expressions; namespace Microsoft.EntityFrameworkCore.Query.ExpressionTranslators.Internal @@ -56,14 +57,23 @@ public virtual Expression Translate(MethodCallExpression e) // First run LIKE against the *unescaped* pattern (which will efficiently use indices), // but then add another test to filter out false positives. var pattern = e.Arguments[0]; + + Expression leftExpr = new SqlFunctionExpression("LEFT", typeof(string), new[] + { + e.Object, + new SqlFunctionExpression("LENGTH", typeof(int), new[] { pattern }), + }); + + // If StartsWith is being invoked on a citext, the LEFT() function above will return a reglar text + // and the comparison will be case-sensitive. So we need to explicitly cast LEFT()'s return type + // to citext. See #319. + if (e.Object.FindProperty(typeof(string))?.GetConfiguredColumnType() == "citext") + leftExpr = new ExplicitStoreTypeCastExpression(leftExpr, typeof(string), "citext"); + return Expression.AndAlso( new LikeExpression(e.Object, Expression.Add(pattern, Expression.Constant("%"), _concat)), Expression.Equal( - new SqlFunctionExpression("LEFT", typeof(string), new[] - { - e.Object, - new SqlFunctionExpression("LENGTH", typeof(int), new[] { pattern }), - }), + leftExpr, pattern ) ); diff --git a/src/EFCore.PG/Query/Expressions/Internal/ExplicitStoreTypeCastExpression.cs b/src/EFCore.PG/Query/Expressions/Internal/ExplicitStoreTypeCastExpression.cs new file mode 100644 index 000000000..6385f5dcf --- /dev/null +++ b/src/EFCore.PG/Query/Expressions/Internal/ExplicitStoreTypeCastExpression.cs @@ -0,0 +1,162 @@ +#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.Sql; +using Microsoft.EntityFrameworkCore.Query.Sql.Internal; +using Microsoft.EntityFrameworkCore.Utilities; + +namespace Microsoft.EntityFrameworkCore.Query.Expressions +{ + /// + /// Represents a SQL CAST expression to a store type specified as a string rather than a CLR type. + /// + public class ExplicitStoreTypeCastExpression : Expression + { + readonly Type _type; + readonly string _storeType; + + /// + /// Creates a new instance of a ExplicitCastExpression.. + /// + /// The operand. + /// The target type. + /// The store type name. + public ExplicitStoreTypeCastExpression( + [NotNull] Expression operand, + [NotNull] Type type, + [NotNull] string storeType) + { + Check.NotNull(operand, nameof(operand)); + Check.NotNull(type, nameof(type)); + Check.NotNull(storeType, nameof(storeType)); + + Operand = operand; + _type = type; + _storeType = storeType; + } + + /// + /// Gets the operand. + /// + /// + /// The operand. + /// + public virtual Expression Operand { get; } + + /// + /// Returns the node type of this . (Inherited from .) + /// + /// The that represents this expression. + public override ExpressionType NodeType => ExpressionType.Extension; + + /// + /// Gets the static type of the expression that this represents. (Inherited from .) + /// + /// The that represents the static type of the expression. + public override Type Type => _type; + + public string StoreType=> _storeType; + + /// + /// Dispatches to the specific visit method for this node type. + /// + protected override Expression Accept(ExpressionVisitor visitor) + { + Check.NotNull(visitor, nameof(visitor)); + + return visitor is NpgsqlQuerySqlGenerator npgsqlVisitor + ? npgsqlVisitor.VisitExplicitStoreTypeCast(this) + : base.Accept(visitor); + } + + /// + /// Reduces the node and then calls the method passing the + /// reduced expression. + /// Throws an exception if the node isn't reducible. + /// + /// An instance of . + /// The expression being visited, or an expression which should replace it in the tree. + /// + /// Override this method to provide logic to walk the node's children. + /// A typical implementation will call visitor.Visit on each of its + /// children, and if any of them change, should return a new copy of + /// itself with the modified children. + /// + protected override Expression VisitChildren(ExpressionVisitor visitor) + { + var newOperand = visitor.Visit(Operand); + + return newOperand != Operand + ? new ExplicitStoreTypeCastExpression(newOperand, _type, _storeType) + : this; + } + + /// + /// Tests if this object is considered equal to another. + /// + /// The object to compare with the current object. + /// + /// true if the objects are considered equal, false if they are not. + /// + public override bool Equals(object obj) + { + if (obj is null) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + return obj.GetType() == GetType() && Equals((ExplicitStoreTypeCastExpression)obj); + } + + private bool Equals(ExplicitStoreTypeCastExpression other) + => _type == other._type && _storeType == other._storeType && Equals(Operand, other.Operand); + + /// + /// Returns a hash code for this object. + /// + /// + /// A hash code for this object. + /// + public override int GetHashCode() + { + unchecked + { + return (_type.GetHashCode() * 397) ^ (_storeType.GetHashCode() * 397) ^ Operand.GetHashCode(); + } + } + + /// + /// Creates a representation of the Expression. + /// + /// A representation of the Expression. + public override string ToString() => "CAST(" + Operand + " AS " + _storeType + ")"; + } +} diff --git a/src/EFCore.PG/Query/Sql/Internal/NpgsqlQuerySqlGenerator.cs b/src/EFCore.PG/Query/Sql/Internal/NpgsqlQuerySqlGenerator.cs index ed8ad7af7..a3a9e56f5 100644 --- a/src/EFCore.PG/Query/Sql/Internal/NpgsqlQuerySqlGenerator.cs +++ b/src/EFCore.PG/Query/Sql/Internal/NpgsqlQuerySqlGenerator.cs @@ -267,6 +267,25 @@ public virtual Expression VisitILike(ILikeExpression iLikeExpression) return iLikeExpression; } + public Expression VisitExplicitStoreTypeCast([NotNull] ExplicitStoreTypeCastExpression castExpression) + { + Sql.Append("CAST("); + + //var parentTypeMapping = _typeMapping; + //_typeMapping = InferTypeMappingFromColumn(castExpression.Operand); + + Visit(castExpression.Operand); + + Sql + .Append(" AS ") + .Append(castExpression.StoreType) + .Append(")"); + + //_typeMapping = parentTypeMapping; + + return castExpression; + } + protected override string GenerateOperator(Expression expression) { switch (expression.NodeType)