Skip to content

NH-3726: Added escape character support in SqlMethods.Like #484

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 60 additions & 28 deletions src/NHibernate.Test/Linq/FunctionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,38 @@ where NHibernate.Linq.SqlMethods.Like(e.FirstName, "Ma%et")
Assert.That(query[0].FirstName, Is.EqualTo("Margaret"));
}

[Test]
public void LikeFunctionWithEscapeCharacter()
{
using (var tx = session.BeginTransaction())
{
var employeeName = "Mar%aret";
var escapeChar = '\\';
var employeeNameEscaped = employeeName.Replace("%", escapeChar + "%");

//This entity will be flushed to the db, but rolled back when the test completes

session.Save(new Employee { FirstName = employeeName, LastName = "LastName" });
session.Flush();


var query = (from e in db.Employees
where NHibernate.Linq.SqlMethods.Like(e.FirstName, employeeNameEscaped, escapeChar)
select e).ToList();

Assert.That(query.Count, Is.EqualTo(1));
Assert.That(query[0].FirstName, Is.EqualTo(employeeName));

Assert.Throws<ArgumentException>(() =>
{
(from e in db.Employees
where NHibernate.Linq.SqlMethods.Like(e.FirstName, employeeNameEscaped, e.FirstName.First())
select e).ToList();
});
tx.Rollback();
}
}

private static class SqlMethods
{
public static bool Like(string expression, string pattern)
Expand All @@ -48,8 +80,8 @@ where NHibernate.Test.Linq.FunctionTests.SqlMethods.Like(e.FirstName, "Ma%et")
public void SubstringFunction2()
{
var query = (from e in db.Employees
where e.FirstName.Substring(0, 2) == "An"
select e).ToList();
where e.FirstName.Substring(0, 2) == "An"
select e).ToList();

Assert.That(query.Count, Is.EqualTo(2));
}
Expand All @@ -58,8 +90,8 @@ where e.FirstName.Substring(0, 2) == "An"
public void SubstringFunction1()
{
var query = (from e in db.Employees
where e.FirstName.Substring(3) == "rew"
select e).ToList();
where e.FirstName.Substring(3) == "rew"
select e).ToList();

Assert.That(query.Count, Is.EqualTo(1));
Assert.That(query[0].FirstName, Is.EqualTo("Andrew"));
Expand All @@ -83,12 +115,12 @@ public void ReplaceFunction()
var query = from e in db.Employees
where e.FirstName.StartsWith("An")
select new
{
Before = e.FirstName,
AfterMethod = e.FirstName.Replace("An", "Zan"),
AfterExtension = ExtensionMethods.Replace(e.FirstName, "An", "Zan"),
AfterExtension2 = e.FirstName.ReplaceExtension("An", "Zan")
};
{
Before = e.FirstName,
AfterMethod = e.FirstName.Replace("An", "Zan"),
AfterExtension = ExtensionMethods.Replace(e.FirstName, "An", "Zan"),
AfterExtension2 = e.FirstName.ReplaceExtension("An", "Zan")
};

var s = ObjectDumper.Write(query);
}
Expand Down Expand Up @@ -124,7 +156,7 @@ public void IndexOfFunctionProjection()
{
if (!TestDialect.SupportsLocate)
Assert.Ignore("Locate function not supported.");

var query = from e in db.Employees
where e.FirstName.Contains("a")
select e.FirstName.IndexOf('A', 3);
Expand All @@ -139,7 +171,7 @@ public void TwoFunctionExpression()
Assert.Ignore("Locate function not supported.");

var query = from e in db.Employees
where e.FirstName.IndexOf("A") == e.BirthDate.Value.Month
where e.FirstName.IndexOf("A") == e.BirthDate.Value.Month
select e.FirstName;

ObjectDumper.Write(query);
Expand Down Expand Up @@ -176,9 +208,9 @@ public void Trim()
{
using (session.BeginTransaction())
{
AnotherEntity ae1 = new AnotherEntity {Input = " hi "};
AnotherEntity ae2 = new AnotherEntity {Input = "hi"};
AnotherEntity ae3 = new AnotherEntity {Input = "heh"};
AnotherEntity ae1 = new AnotherEntity { Input = " hi " };
AnotherEntity ae2 = new AnotherEntity { Input = "hi" };
AnotherEntity ae3 = new AnotherEntity { Input = "heh" };
session.Save(ae1);
session.Save(ae2);
session.Save(ae3);
Expand All @@ -201,9 +233,9 @@ public void TrimTrailingWhitespace()
{
using (session.BeginTransaction())
{
session.Save(new AnotherEntity {Input = " hi "});
session.Save(new AnotherEntity {Input = "hi"});
session.Save(new AnotherEntity {Input = "heh"});
session.Save(new AnotherEntity { Input = " hi " });
session.Save(new AnotherEntity { Input = "hi" });
session.Save(new AnotherEntity { Input = "heh" });
session.Flush();

Assert.AreEqual(TestDialect.IgnoresTrailingWhitespace ? 2 : 1, session.Query<AnotherEntity>().Where(e => e.Input.TrimStart() == "hi ").Count());
Expand Down Expand Up @@ -256,7 +288,7 @@ public void WhereBoolConstantEqual()
var query = from item in db.Role
where item.IsActive.Equals(true)
select item;

ObjectDumper.Write(query);
}

Expand All @@ -266,7 +298,7 @@ public void WhereBoolParameterEqual()
var query = from item in db.Role
where item.IsActive.Equals(1 == 1)
select item;

ObjectDumper.Write(query);
}

Expand All @@ -286,8 +318,8 @@ where item.IsActive.Equals(f())
public void WhereLongEqual()
{
var query = from item in db.PatientRecords
where item.Id.Equals(-1)
select item;
where item.Id.Equals(-1)
select item;

ObjectDumper.Write(query);
}
Expand All @@ -301,7 +333,7 @@ where item.RegisteredAt.Equals(DateTime.Today)

ObjectDumper.Write(query);
}

[Test]
public void WhereGuidEqual()
{
Expand All @@ -310,7 +342,7 @@ where item.Reference.Equals(Guid.Empty)
select item;

ObjectDumper.Write(query);
}
}

[Test]
public void WhereDoubleEqual()
Expand All @@ -320,8 +352,8 @@ where item.BodyWeight.Equals(-1)
select item;

ObjectDumper.Write(query);
}
}

[Test]
public void WhereFloatEqual()
{
Expand All @@ -330,7 +362,7 @@ where item.Float.Equals(-1)
select item;

ObjectDumper.Write(query);
}
}

[Test]
public void WhereCharEqual()
Expand Down Expand Up @@ -362,4 +394,4 @@ where item.Discount.Equals(-1)
ObjectDumper.Write(query);
}
}
}
}
7 changes: 6 additions & 1 deletion src/NHibernate/Hql/Ast/HqlTreeBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,11 @@ public HqlLike Like(HqlExpression lhs, HqlExpression rhs)
return new HqlLike(_factory, lhs, rhs);
}

public HqlLike Like(HqlExpression lhs, HqlExpression rhs, HqlConstant escapeCharacter)
{
return new HqlLike(_factory, lhs, rhs, escapeCharacter);
}

public HqlConcat Concat(params HqlExpression[] args)
{
return new HqlConcat(_factory, args);
Expand Down Expand Up @@ -466,4 +471,4 @@ public HqlTreeNode Indices(HqlExpression dictionary)
return new HqlIndices(_factory, dictionary);
}
}
}
}
29 changes: 21 additions & 8 deletions src/NHibernate/Hql/Ast/HqlTreeNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ protected HqlTreeNode(int type, string text, IASTFactory factory, IEnumerable<Hq
AddChildren(children);
}

protected HqlTreeNode(int type, string text, IASTFactory factory, params HqlTreeNode[] children) : this(type, text, factory, (IEnumerable<HqlTreeNode>) children)
protected HqlTreeNode(int type, string text, IASTFactory factory, params HqlTreeNode[] children) : this(type, text, factory, (IEnumerable<HqlTreeNode>)children)
{
}

Expand Down Expand Up @@ -92,7 +92,7 @@ internal IASTNode AstNode

internal void AddChild(HqlTreeNode child)
{
if (child is HqlExpressionSubTreeHolder)
if (child is HqlExpressionSubTreeHolder)
{
AddChildren(child.Children);
}
Expand All @@ -116,7 +116,7 @@ public static HqlBooleanExpression AsBooleanExpression(this HqlTreeNode node)
{
if (node is HqlDot)
{
return new HqlBooleanDot(node.Factory, (HqlDot) node);
return new HqlBooleanDot(node.Factory, (HqlDot)node);
}

// TODO - nice error handling if cast fails
Expand Down Expand Up @@ -220,8 +220,8 @@ internal HqlIdent(IASTFactory factory, System.Type type)
}
if (type == typeof(DateTimeOffset))
{
SetText("datetimeoffset");
break;
SetText("datetimeoffset");
break;
}
throw new NotSupportedException(string.Format("Don't currently support idents of type {0}", type.Name));
}
Expand Down Expand Up @@ -373,7 +373,7 @@ public HqlSkip(IASTFactory factory, HqlExpression parameter)
public class HqlTake : HqlStatement
{
public HqlTake(IASTFactory factory, HqlExpression parameter)
: base(HqlSqlWalker.TAKE, "take", factory, parameter) {}
: base(HqlSqlWalker.TAKE, "take", factory, parameter) { }
}

public class HqlConstant : HqlExpression
Expand Down Expand Up @@ -690,7 +690,7 @@ public HqlMax(IASTFactory factory, HqlExpression expression)
: base(HqlSqlWalker.AGGREGATE, "max", factory, expression)
{
}
}
}

public class HqlMin : HqlExpression
{
Expand Down Expand Up @@ -818,6 +818,19 @@ public HqlLike(IASTFactory factory, HqlExpression lhs, HqlExpression rhs)
: base(HqlSqlWalker.LIKE, "like", factory, lhs, rhs)
{
}

public HqlLike(IASTFactory factory, HqlExpression lhs, HqlExpression rhs, HqlConstant escapeCharacter)
: base(HqlSqlWalker.LIKE, "like", factory, lhs, rhs, new HqlEscape(factory, escapeCharacter))
{
}
}

public class HqlEscape : HqlStatement
{
public HqlEscape(IASTFactory factory, HqlConstant escapeCharacter)
: base(HqlSqlWalker.ESCAPE, "escape", factory, escapeCharacter)
{
}
}

public class HqlConcat : HqlExpression
Expand Down Expand Up @@ -899,4 +912,4 @@ public HqlInList(IASTFactory factory, HqlTreeNode source)
{
}
}
}
}
25 changes: 19 additions & 6 deletions src/NHibernate/Linq/Functions/StringGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,22 @@ public IEnumerable<MethodInfo> SupportedMethods
public HqlTreeNode BuildHql(MethodInfo method, Expression targetObject, ReadOnlyCollection<Expression> arguments,
HqlTreeBuilder treeBuilder, IHqlExpressionVisitor visitor)
{
return treeBuilder.Like(
visitor.Visit(arguments[0]).AsExpression(),
visitor.Visit(arguments[1]).AsExpression());
if (arguments.Count == 2)
{
return treeBuilder.Like(
visitor.Visit(arguments[0]).AsExpression(),
visitor.Visit(arguments[1]).AsExpression());
}
if (arguments[2].NodeType == ExpressionType.Constant)
{
var escapeCharExpression = (ConstantExpression)arguments[2];
return treeBuilder.Like(
visitor.Visit(arguments[0]).AsExpression(),
visitor.Visit(arguments[1]).AsExpression(),
treeBuilder.Constant(escapeCharExpression.Value));
}
throw new ArgumentException("The escape character must be specified as literal value or a string variable");

}

public bool SupportsMethod(MethodInfo method)
Expand All @@ -34,8 +47,8 @@ public bool SupportsMethod(MethodInfo method)
// to avoid referencing Linq2Sql or Linq2NHibernate, if they so wish.

return method != null && method.Name == "Like" &&
method.GetParameters().Length == 2 &&
method.DeclaringType != null &&
(method.GetParameters().Length == 2 || method.GetParameters().Length == 3) &&
method.DeclaringType != null &&
method.DeclaringType.FullName.EndsWith("SqlMethods");
}

Expand Down Expand Up @@ -284,4 +297,4 @@ public HqlTreeNode BuildHql(MethodInfo method, Expression targetObject, ReadOnly
return treeBuilder.MethodCall("str", visitor.Visit(targetObject).AsExpression());
}
}
}
}
17 changes: 15 additions & 2 deletions src/NHibernate/Linq/SqlMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,29 @@ namespace NHibernate.Linq
public static class SqlMethods
{
/// <summary>
/// Use the SqlMethods.Like() method in a Linq2NHibernate expression to generate
/// Use this method in a Linq2NHibernate expression to generate
/// an SQL LIKE expression. (If you want to avoid depending on the NHibernate.Linq namespace,
/// you can define your own replica of this method. Any 2-argument method named Like in a class named SqlMethods
/// will be translated.) This method can only be used in Linq2NHibernate expression, and will throw
/// will be translated.) This method can only be used in Linq2NHibernate expressions, and will throw
/// if called directly.
/// </summary>
public static bool Like(this string matchExpression, string sqlLikePattern)
{
throw new NotSupportedException(
"The NHibernate.Linq.SqlMethods.Like(string, string) method can only be used in Linq2NHibernate expressions.");
}

/// <summary>
/// Use this method in a Linq2NHibernate expression to generate
/// an SQL LIKE expression with an escape character defined. (If you want to avoid depending on the NHibernate.Linq namespace,
/// you can define your own replica of this method. Any 3-argument method named Like in a class named SqlMethods
/// will be translated.) This method can only be used in Linq2NHibernate expressions, and will throw
/// if called directly.
/// </summary>
public static bool Like(this string matchExpression, string sqlLikePattern, char escapeCharacter)
{
throw new NotSupportedException(
"The NHibernate.Linq.SqlMethods.Like(string, string) method can only be used in Linq2NHibernate expressions.");
}
}
}