Skip to content

Commit

Permalink
Fix StartsWith on citext
Browse files Browse the repository at this point in the history
Fixes #319
  • Loading branch information
roji committed Mar 5, 2018
1 parent 5892bcb commit 9f8cd0c
Show file tree
Hide file tree
Showing 3 changed files with 196 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
)
);
Expand Down
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Represents a SQL CAST expression to a store type specified as a string rather than a CLR type.
/// </summary>
public class ExplicitStoreTypeCastExpression : Expression
{
readonly Type _type;
readonly string _storeType;

/// <summary>
/// Creates a new instance of a ExplicitCastExpression..
/// </summary>
/// <param name="operand"> The operand. </param>
/// <param name="type"> The target type. </param>
/// <param name="storeType"> The store type name. </param>
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;
}

/// <summary>
/// Gets the operand.
/// </summary>
/// <value>
/// The operand.
/// </value>
public virtual Expression Operand { get; }

/// <summary>
/// Returns the node type of this <see cref="Expression" />. (Inherited from <see cref="Expression" />.)
/// </summary>
/// <returns>The <see cref="ExpressionType" /> that represents this expression.</returns>
public override ExpressionType NodeType => ExpressionType.Extension;

/// <summary>
/// Gets the static type of the expression that this <see cref="Expression" /> represents. (Inherited from <see cref="Expression" />.)
/// </summary>
/// <returns>The <see cref="Type" /> that represents the static type of the expression.</returns>
public override Type Type => _type;

public string StoreType=> _storeType;

/// <summary>
/// Dispatches to the specific visit method for this node type.
/// </summary>
protected override Expression Accept(ExpressionVisitor visitor)
{
Check.NotNull(visitor, nameof(visitor));

return visitor is NpgsqlQuerySqlGenerator npgsqlVisitor
? npgsqlVisitor.VisitExplicitStoreTypeCast(this)
: base.Accept(visitor);
}

/// <summary>
/// Reduces the node and then calls the <see cref="ExpressionVisitor.Visit(Expression)" /> method passing the
/// reduced expression.
/// Throws an exception if the node isn't reducible.
/// </summary>
/// <param name="visitor"> An instance of <see cref="ExpressionVisitor" />. </param>
/// <returns> The expression being visited, or an expression which should replace it in the tree. </returns>
/// <remarks>
/// Override this method to provide logic to walk the node's children.
/// A typical implementation will call visitor.Visit on each of its
/// children, and if any of them change, should return a new copy of
/// itself with the modified children.
/// </remarks>
protected override Expression VisitChildren(ExpressionVisitor visitor)
{
var newOperand = visitor.Visit(Operand);

return newOperand != Operand
? new ExplicitStoreTypeCastExpression(newOperand, _type, _storeType)
: this;
}

/// <summary>
/// Tests if this object is considered equal to another.
/// </summary>
/// <param name="obj"> The object to compare with the current object. </param>
/// <returns>
/// true if the objects are considered equal, false if they are not.
/// </returns>
public override bool Equals(object obj)
{
if (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);

/// <summary>
/// Returns a hash code for this object.
/// </summary>
/// <returns>
/// A hash code for this object.
/// </returns>
public override int GetHashCode()
{
unchecked
{
return (_type.GetHashCode() * 397) ^ (_storeType.GetHashCode() * 397) ^ Operand.GetHashCode();
}
}

/// <summary>
/// Creates a <see cref="string" /> representation of the Expression.
/// </summary>
/// <returns>A <see cref="string" /> representation of the Expression.</returns>
public override string ToString() => "CAST(" + Operand + " AS " + _storeType + ")";
}
}
19 changes: 19 additions & 0 deletions src/EFCore.PG/Query/Sql/Internal/NpgsqlQuerySqlGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down

0 comments on commit 9f8cd0c

Please sign in to comment.