Skip to content

Add StringComparison option for Case Sensitivity #138

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

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
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
27 changes: 27 additions & 0 deletions src/Searchlight/Exceptions/InvalidEngineSetting.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
namespace Searchlight.Exceptions
{
/// <summary>
/// Exception to be thrown if the SearchlightEngine was configured incorrectly
/// </summary>
public class InvalidEngineSetting : SearchlightException
{
public string OriginalFilter { get; internal set; }

/// <summary>
/// Fields that are missing or incorrect
/// </summary>
public string[] Fields { get; set; }

public string ErrorMessage =>
$"These fields are either missing or are set incorrectly: {string.Join(",", Fields)}";

/// <summary>
/// Constructor
/// </summary>
/// <param name="fields"></param>
public InvalidEngineSetting(params string[] fields)
{
Fields = fields;
}
}
}
18 changes: 10 additions & 8 deletions src/Searchlight/LinqExecutor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Runtime.CompilerServices;
using Searchlight.Parsing;
using Searchlight.Query;

Expand Down Expand Up @@ -149,6 +150,7 @@ private static Expression BuildOneExpression<T>(ParameterExpression select, Base
Expression field;
Expression value;
Expression result;
var comparison = src?.Engine?.StringComparison ?? StringComparison.OrdinalIgnoreCase;

var t = typeof(T);

Expand Down Expand Up @@ -179,7 +181,7 @@ private static Expression BuildOneExpression<T>(ParameterExpression select, Base
// ReSharper disable once AssignNullToNotNullAttribute
typeof(string).GetMethod("Equals",
new [] { typeof(string), typeof(string), typeof(StringComparison) }),
field, value, Expression.Constant(StringComparison.OrdinalIgnoreCase));
field, value, Expression.Constant(comparison));
}
else
{
Expand All @@ -197,7 +199,7 @@ private static Expression BuildOneExpression<T>(ParameterExpression select, Base
{
typeof(string), typeof(string), typeof(StringComparison)
}),
field, value, Expression.Constant(StringComparison.OrdinalIgnoreCase)),
field, value, Expression.Constant(comparison)),
Expression.Constant(0)));
}
else
Expand All @@ -216,7 +218,7 @@ private static Expression BuildOneExpression<T>(ParameterExpression select, Base
{
typeof(string), typeof(string), typeof(StringComparison)
}),
field, value, Expression.Constant(StringComparison.OrdinalIgnoreCase)),
field, value, Expression.Constant(comparison)),
Expression.Constant(0)));
}
else
Expand All @@ -235,7 +237,7 @@ private static Expression BuildOneExpression<T>(ParameterExpression select, Base
{
typeof(string), typeof(string), typeof(StringComparison)
}),
field, value, Expression.Constant(StringComparison.OrdinalIgnoreCase)),
field, value, Expression.Constant(comparison)),
Expression.Constant(0)));
}
else
Expand All @@ -254,7 +256,7 @@ private static Expression BuildOneExpression<T>(ParameterExpression select, Base
{
typeof(string), typeof(string), typeof(StringComparison)
}),
field, value, Expression.Constant(StringComparison.OrdinalIgnoreCase)),
field, value, Expression.Constant(comparison)),
Expression.Constant(0)));
}
else
Expand All @@ -268,7 +270,7 @@ private static Expression BuildOneExpression<T>(ParameterExpression select, Base
// ReSharper disable once AssignNullToNotNullAttribute
typeof(string).GetMethod("StartsWith",
new [] { typeof(string), typeof(StringComparison) }),
value, Expression.Constant(StringComparison.OrdinalIgnoreCase)),
value, Expression.Constant(comparison)),
Expression.MakeCatchBlock(typeof(Exception), null,
Expression.Constant(false, typeof(Boolean)), null)
);
Expand All @@ -280,7 +282,7 @@ private static Expression BuildOneExpression<T>(ParameterExpression select, Base
// ReSharper disable once AssignNullToNotNullAttribute
typeof(string).GetMethod("EndsWith",
new [] { typeof(string), typeof(StringComparison) }),
value, Expression.Constant(StringComparison.OrdinalIgnoreCase)),
value, Expression.Constant(comparison)),
Expression.MakeCatchBlock(typeof(Exception), null,
Expression.Constant(false, typeof(Boolean)), null)
);
Expand All @@ -291,7 +293,7 @@ private static Expression BuildOneExpression<T>(ParameterExpression select, Base
// ReSharper disable once AssignNullToNotNullAttribute
typeof(string).GetMethod("Contains",
new [] { typeof(string), typeof(StringComparison) }),
value, Expression.Constant(StringComparison.OrdinalIgnoreCase)),
value, Expression.Constant(comparison)),
Expression.MakeCatchBlock(typeof(Exception), null,
Expression.Constant(false, typeof(Boolean)), null)
);
Expand Down
17 changes: 17 additions & 0 deletions src/Searchlight/SearchlightEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,23 @@ public class SearchlightEngine
/// </summary>
public bool useNoCount { get; set; } = true;

/// <summary>
/// Whether or not to use case sensitive comparisons
///
/// Note: Odd numbers in the StringComparison enum are case insensitive
/// </summary>
public bool CaseSensitiveComparison => (int)StringComparison % 2 == 0;

/// <summary>
/// The string comparison for the engine
/// </summary>
public StringComparison StringComparison { get; set; } = StringComparison.OrdinalIgnoreCase;

/// <summary>
/// The collation to use for case sensitive comparisons, must be specified for SQL Server
/// </summary>
public string Collation { get; set; } = string.Empty;

/// <summary>
/// Adds a new class to the engine
/// </summary>
Expand Down
80 changes: 70 additions & 10 deletions src/Searchlight/SqlExecutor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ public static SqlQuery ToPostgresCommand(this SyntaxTree query)
private static SqlQuery CreateSql(SqlDialect dialect, SyntaxTree query, SearchlightEngine engine)
{
var sql = new SqlQuery() { Syntax = query };
sql.WhereClause = RenderJoinedClauses(dialect, query.Filter, sql);
sql.WhereClause = RenderJoinedClauses(dialect, query.Filter, sql, engine);
sql.OrderByClause = RenderOrderByClause(query.OrderBy);

// Sanity test - is the query too complicated to be safe to run?
Expand Down Expand Up @@ -171,8 +171,9 @@ private static string RenderOrderByClause(List<SortInfo> list)
/// <param name="dialect"></param>
/// <param name="clause"></param>
/// <param name="sql"></param>
/// <param name="engine"></param>
/// <returns></returns>
private static string RenderJoinedClauses(SqlDialect dialect, List<BaseClause> clause, SqlQuery sql)
private static string RenderJoinedClauses(SqlDialect dialect, List<BaseClause> clause, SqlQuery sql, SearchlightEngine engine)
{
var sb = new StringBuilder();
for (var i = 0; i < clause.Count; i++)
Expand All @@ -193,7 +194,7 @@ private static string RenderJoinedClauses(SqlDialect dialect, List<BaseClause> c
}
}

sb.Append(RenderClause(dialect, clause[i], sql));
sb.Append(RenderClause(dialect, clause[i], sql, engine));
}

return sb.ToString();
Expand All @@ -205,17 +206,18 @@ private static string RenderJoinedClauses(SqlDialect dialect, List<BaseClause> c
/// <param name="dialect"></param>
/// <param name="clause"></param>
/// <param name="sql"></param>
/// <param name="engine"></param>
/// <returns></returns>
/// <exception cref="Exception"></exception>
private static string RenderClause(SqlDialect dialect, BaseClause clause, SqlQuery sql)
private static string RenderClause(SqlDialect dialect, BaseClause clause, SqlQuery sql, SearchlightEngine engine)
{
switch (clause)
{
case BetweenClause bc:
return
$"{bc.Column.OriginalName} {(bc.Negated ? "NOT " : "")}BETWEEN {sql.AddParameter(bc.LowerValue.GetValue(), bc.Column.FieldType)} AND {sql.AddParameter(bc.UpperValue.GetValue(), bc.Column.FieldType)}";
case CompoundClause compoundClause:
return $"({RenderJoinedClauses(dialect, compoundClause.Children, sql)})";
return $"({RenderJoinedClauses(dialect, compoundClause.Children, sql, engine)})";
case CriteriaClause cc:
var rawValue = cc.Value.GetValue();
switch (cc.Operation)
Expand All @@ -226,7 +228,8 @@ private static string RenderClause(SqlDialect dialect, BaseClause clause, SqlQue
case OperationType.LessThan:
case OperationType.LessThanOrEqual:
case OperationType.NotEqual:
return RenderComparisonClause(cc.Column.OriginalName, cc.Negated, cc.Operation, sql.AddParameter(rawValue, cc.Column.FieldType));
return RenderComparisonClause(dialect, cc,
sql.AddParameter(rawValue, cc.Column.FieldType), engine);
case OperationType.Contains:
return RenderLikeClause(dialect, cc, sql, rawValue, "%", "%");
case OperationType.StartsWith:
Expand Down Expand Up @@ -257,15 +260,51 @@ private static string RenderClause(SqlDialect dialect, BaseClause clause, SqlQue
{ OperationType.GreaterThanOrEqual, new Tuple<string, string>(">=", "<") },
};

private static string RenderComparisonClause(string column, bool negated, OperationType op, string parameter)
private static string RenderComparisonClause(SqlDialect dialect, CriteriaClause cc, string parameter, SearchlightEngine engine)
{
var op = cc.Operation;
var negated = cc.Negated;
var column = cc.Column.OriginalName;
var fieldType = cc.Column.FieldType;

if (!CanonicalOps.TryGetValue(op, out var opstrings))
{
throw new Exception($"Invalid comparison type {op}");
}

var operationSymbol = negated ? opstrings.Item2 : opstrings.Item1;
return $"{column} {operationSymbol} {parameter}";

if (engine.CaseSensitiveComparison)
{
switch (dialect)
{
case SqlDialect.MicrosoftSqlServer:
if (string.IsNullOrEmpty(engine.Collation))
{
throw new InvalidEngineSetting(nameof(SearchlightEngine.Collation));
}

return $"{column} {operationSymbol} {parameter} COLLATE {engine.Collation}";
case SqlDialect.MySql:
return $"{column} {operationSymbol} BINARY {parameter}";
case SqlDialect.PostgreSql:
default:
return $"{column} {operationSymbol} {parameter}";
}
}

// Case insensitive comparison
switch (dialect)
{
case SqlDialect.PostgreSql:
return fieldType == typeof(string)
? $"LOWER({column}) {operationSymbol} LOWER({parameter})"
: $"{column} {operationSymbol} {parameter}";
case SqlDialect.MySql:
case SqlDialect.MicrosoftSqlServer:
default:
return $"{column} {operationSymbol} {parameter}";
}
}

private static string RenderLikeClause(SqlDialect dialect, CriteriaClause clause, SqlQuery sql, object rawValue,
Expand All @@ -285,8 +324,29 @@ private static string RenderLikeClause(SqlDialect dialect, CriteriaClause clause
var escapeCommand = dialect == SqlDialect.MicrosoftSqlServer ? " ESCAPE '\\'" : string.Empty;
var notCommand = clause.Negated ? "NOT " : "";
var likeValue = prefix + EscapeLikeValue(stringValue) + suffix;
return
$"{clause.Column.OriginalName} {notCommand}{likeCommand} {sql.AddParameter(likeValue, clause.Column.FieldType)}{escapeCommand}";

if (!(sql.Syntax?.Source?.Engine?.CaseSensitiveComparison ?? false))
return
$"{clause.Column.OriginalName} {notCommand}{likeCommand} {sql.AddParameter(likeValue, clause.Column.FieldType)}{escapeCommand}";

switch (dialect)
{
case SqlDialect.MySql:
return
$"{clause.Column.OriginalName} {notCommand}{likeCommand} BINARY {sql.AddParameter(likeValue, clause.Column.FieldType)}{escapeCommand}";
case SqlDialect.MicrosoftSqlServer:
if (string.IsNullOrEmpty(sql.Syntax?.Source?.Engine?.Collation))
{
throw new InvalidEngineSetting(nameof(SearchlightEngine.Collation));
}

return
$"{clause.Column.OriginalName} {notCommand}{likeCommand} {sql.AddParameter(likeValue, clause.Column.FieldType)}{escapeCommand} COLLATE {sql.Syntax.Source.Engine.Collation}";
case SqlDialect.PostgreSql:
default:
return
$"{clause.Column.OriginalName} {notCommand}{likeCommand} {sql.AddParameter(likeValue, clause.Column.FieldType)}{escapeCommand}";
}
}

private static string EscapeLikeValue(string stringValue)
Expand Down
Loading