From a21aa079ff0c746bd8679088acea0d2900789222 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 18 Dec 2013 17:22:00 +1100 Subject: [PATCH] Adds ability to use the query builder with string matches based on an NText column --- .../Persistence/PetaPocoExtensions.cs | 14 ++-- .../Querying/BaseExpressionHelper.cs | 8 +- .../Querying/ModelToSqlExpressionHelper.cs | 60 +++++++++++++- .../Querying/SqlStringExtensions.cs | 30 +++++++ .../Persistence/Querying/TextColumnType.cs | 8 ++ .../SqlSyntax/ISqlSyntaxProvider.cs | 6 ++ .../Persistence/SqlSyntax/MySqlSyntax.cs | 10 +++ .../SqlSyntax/MySqlSyntaxProvider.cs | 8 -- .../Persistence/SqlSyntax/SqlCeSyntax.cs | 10 +++ .../SqlSyntax/SqlCeSyntaxProvider.cs | 65 +++++++++++++-- .../Persistence/SqlSyntax/SqlServerSyntax.cs | 10 +++ .../SqlSyntax/SqlServerSyntaxProvider.cs | 79 +++++++++++++------ .../SqlSyntax/SqlServerVersionName.cs | 16 ++++ .../SqlSyntax/SqlSyntaxProviderBase.cs | 25 ++++++ src/Umbraco.Core/StringExtensions.cs | 5 ++ src/Umbraco.Core/Umbraco.Core.csproj | 6 ++ 16 files changed, 307 insertions(+), 53 deletions(-) create mode 100644 src/Umbraco.Core/Persistence/Querying/SqlStringExtensions.cs create mode 100644 src/Umbraco.Core/Persistence/Querying/TextColumnType.cs create mode 100644 src/Umbraco.Core/Persistence/SqlSyntax/MySqlSyntax.cs create mode 100644 src/Umbraco.Core/Persistence/SqlSyntax/SqlCeSyntax.cs create mode 100644 src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntax.cs create mode 100644 src/Umbraco.Core/Persistence/SqlSyntax/SqlServerVersionName.cs diff --git a/src/Umbraco.Core/Persistence/PetaPocoExtensions.cs b/src/Umbraco.Core/Persistence/PetaPocoExtensions.cs index 7ae81aaa16..cf3fd1511f 100644 --- a/src/Umbraco.Core/Persistence/PetaPocoExtensions.cs +++ b/src/Umbraco.Core/Persistence/PetaPocoExtensions.cs @@ -21,14 +21,18 @@ namespace Umbraco.Core.Persistence /// /// This will escape single @ symbols for peta poco values so it doesn't think it's a parameter /// - /// /// /// - public static string EscapeAtSymbols(this Database db, string value) + public static string EscapeAtSymbols(string value) { - //this fancy regex will only match a single @ not a double, etc... - var regex = new Regex("(?(this Database db) diff --git a/src/Umbraco.Core/Persistence/Querying/BaseExpressionHelper.cs b/src/Umbraco.Core/Persistence/Querying/BaseExpressionHelper.cs index ac84b5cee3..73fa98b275 100644 --- a/src/Umbraco.Core/Persistence/Querying/BaseExpressionHelper.cs +++ b/src/Umbraco.Core/Persistence/Querying/BaseExpressionHelper.cs @@ -64,13 +64,7 @@ namespace Umbraco.Core.Persistence.Querying public virtual string EscapeAtArgument(string exp) { - /*if (exp.StartsWith("@")) - return string.Concat("@", exp);*/ - - if (exp.Contains("@")) - return exp.Replace("@", "@@"); - - return exp; + return PetaPocoExtensions.EscapeAtSymbols(exp); } public virtual bool ShouldQuoteValue(Type fieldType) diff --git a/src/Umbraco.Core/Persistence/Querying/ModelToSqlExpressionHelper.cs b/src/Umbraco.Core/Persistence/Querying/ModelToSqlExpressionHelper.cs index 738cc8a23c..611d9c63a3 100644 --- a/src/Umbraco.Core/Persistence/Querying/ModelToSqlExpressionHelper.cs +++ b/src/Umbraco.Core/Persistence/Querying/ModelToSqlExpressionHelper.cs @@ -2,9 +2,11 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Globalization; +using System.Linq; using System.Linq.Expressions; using System.Text; using Umbraco.Core.Persistence.Mappers; +using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Querying { @@ -226,6 +228,39 @@ namespace Umbraco.Core.Persistence.Querying } + private string HandleStringComparison(string col, string val, string verb, TextColumnType columnType) + { + switch (verb) + { + case "Equals": + return SqlSyntaxContext.SqlSyntaxProvider.GetStringColumnEqualComparison(col, EscapeAtArgument(RemoveQuote(val)), columnType); + case "StartsWith": + return SqlSyntaxContext.SqlSyntaxProvider.GetStringColumnStartsWithComparison(col, EscapeAtArgument(RemoveQuote(val)), columnType); + case "EndsWith": + return SqlSyntaxContext.SqlSyntaxProvider.GetStringColumnEndsWithComparison(col, EscapeAtArgument(RemoveQuote(val)), columnType); + case "Contains": + return SqlSyntaxContext.SqlSyntaxProvider.GetStringColumnContainsComparison(col, EscapeAtArgument(RemoveQuote(val)), columnType); + case "InvariantEquals": + case "SqlEquals": + //recurse + return HandleStringComparison(col, val, "Equals", columnType); + case "InvariantStartsWith": + case "SqlStartsWith": + //recurse + return HandleStringComparison(col, val, "StartsWith", columnType); + case "InvariantEndsWith": + case "SqlEndsWith": + //recurse + return HandleStringComparison(col, val, "EndsWith", columnType); + case "InvariantContains": + case "SqlContains": + //recurse + return HandleStringComparison(col, val, "Contains", columnType); + default: + throw new ArgumentOutOfRangeException("verb"); + } + } + protected virtual string VisitMethodCall(MethodCallExpression m) { List args = this.VisitExpressionList(m.Arguments); @@ -245,12 +280,31 @@ namespace Umbraco.Core.Persistence.Querying return string.Format("upper({0})", r); case "ToLower": return string.Format("lower({0})", r); + case "StartsWith": - return string.Format("upper({0}) like '{1}%'", r, EscapeAtArgument(RemoveQuote(args[0].ToString().ToUpper()))); case "EndsWith": - return string.Format("upper({0}) like '%{1}'", r, EscapeAtArgument(RemoveQuote(args[0].ToString()).ToUpper())); case "Contains": - return string.Format("{0} like '%{1}%'", r, EscapeAtArgument(RemoveQuote(args[0].ToString()).ToUpper())); + case "Equals": + case "SqlStartsWith": + case "SqlEndsWith": + case "SqlContains": + case "SqlEquals": + case "InvariantStartsWith": + case "InvariantEndsWith": + case "InvariantContains": + case "InvariantEquals": + //default + var colType = TextColumnType.NVarchar; + //then check if this arg has been passed in + if (m.Arguments.Count > 1) + { + var colTypeArg = m.Arguments.FirstOrDefault(x => x is ConstantExpression && x.Type == typeof(TextColumnType)); + if (colTypeArg != null) + { + colType = (TextColumnType) ((ConstantExpression) colTypeArg).Value; + } + } + return HandleStringComparison(r.ToString(), args[0].ToString(), m.Method.Name, colType); case "Substring": var startIndex = Int32.Parse(args[0].ToString()) + 1; if (args.Count == 2) diff --git a/src/Umbraco.Core/Persistence/Querying/SqlStringExtensions.cs b/src/Umbraco.Core/Persistence/Querying/SqlStringExtensions.cs new file mode 100644 index 0000000000..82c158a9c6 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Querying/SqlStringExtensions.cs @@ -0,0 +1,30 @@ +using System; + +namespace Umbraco.Core.Persistence.Querying +{ + /// + /// String extension methods used specifically to translate into SQL + /// + internal static class SqlStringExtensions + { + public static bool SqlContains(this string str, string txt, TextColumnType columnType) + { + return str.InvariantContains(txt); + } + + public static bool SqlEquals(this string str, string txt, TextColumnType columnType) + { + return str.InvariantEquals(txt); + } + + public static bool SqlStartsWith(this string str, string txt, TextColumnType columnType) + { + return str.InvariantStartsWith(txt); + } + + public static bool SqlEndsWith(this string str, string txt, TextColumnType columnType) + { + return str.InvariantEndsWith(txt); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Querying/TextColumnType.cs b/src/Umbraco.Core/Persistence/Querying/TextColumnType.cs new file mode 100644 index 0000000000..f33022fcbe --- /dev/null +++ b/src/Umbraco.Core/Persistence/Querying/TextColumnType.cs @@ -0,0 +1,8 @@ +namespace Umbraco.Core.Persistence.Querying +{ + public enum TextColumnType + { + NVarchar, + NText + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/ISqlSyntaxProvider.cs b/src/Umbraco.Core/Persistence/SqlSyntax/ISqlSyntaxProvider.cs index e2a13d1fef..9fbf0456b6 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/ISqlSyntaxProvider.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/ISqlSyntaxProvider.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using Umbraco.Core.Persistence.DatabaseAnnotations; using Umbraco.Core.Persistence.DatabaseModelDefinitions; +using Umbraco.Core.Persistence.Querying; namespace Umbraco.Core.Persistence.SqlSyntax { @@ -10,6 +11,11 @@ namespace Umbraco.Core.Persistence.SqlSyntax /// public interface ISqlSyntaxProvider { + string GetStringColumnEqualComparison(string column, string value, TextColumnType columnType); + string GetStringColumnStartsWithComparison(string column, string value, TextColumnType columnType); + string GetStringColumnEndsWithComparison(string column, string value, TextColumnType columnType); + string GetStringColumnContainsComparison(string column, string value, TextColumnType columnType); + string GetQuotedTableName(string tableName); string GetQuotedColumnName(string columnName); string GetQuotedName(string name); diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/MySqlSyntax.cs b/src/Umbraco.Core/Persistence/SqlSyntax/MySqlSyntax.cs new file mode 100644 index 0000000000..00b07ae15a --- /dev/null +++ b/src/Umbraco.Core/Persistence/SqlSyntax/MySqlSyntax.cs @@ -0,0 +1,10 @@ +namespace Umbraco.Core.Persistence.SqlSyntax +{ + /// + /// Static class that provides simple access to the MySql SqlSyntax Provider + /// + internal static class MySqlSyntax + { + public static ISqlSyntaxProvider Provider { get { return new MySqlSyntaxProvider(); } } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/MySqlSyntaxProvider.cs b/src/Umbraco.Core/Persistence/SqlSyntax/MySqlSyntaxProvider.cs index 8f466e3829..5f5d412ab3 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/MySqlSyntaxProvider.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/MySqlSyntaxProvider.cs @@ -6,14 +6,6 @@ using Umbraco.Core.Persistence.DatabaseModelDefinitions; namespace Umbraco.Core.Persistence.SqlSyntax { - /// - /// Static class that provides simple access to the MySql SqlSyntax Provider - /// - internal static class MySqlSyntax - { - public static ISqlSyntaxProvider Provider { get { return new MySqlSyntaxProvider(); } } - } - /// /// Represents an SqlSyntaxProvider for MySql /// diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/SqlCeSyntax.cs b/src/Umbraco.Core/Persistence/SqlSyntax/SqlCeSyntax.cs new file mode 100644 index 0000000000..29660355fe --- /dev/null +++ b/src/Umbraco.Core/Persistence/SqlSyntax/SqlCeSyntax.cs @@ -0,0 +1,10 @@ +namespace Umbraco.Core.Persistence.SqlSyntax +{ + /// + /// Static class that provides simple access to the Sql CE SqlSyntax Provider + /// + internal static class SqlCeSyntax + { + public static ISqlSyntaxProvider Provider { get { return new SqlCeSyntaxProvider(); } } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/SqlCeSyntaxProvider.cs b/src/Umbraco.Core/Persistence/SqlSyntax/SqlCeSyntaxProvider.cs index 106b45ffed..d908f5c9b6 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/SqlCeSyntaxProvider.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/SqlCeSyntaxProvider.cs @@ -3,17 +3,10 @@ using System.Collections.Generic; using System.Linq; using Umbraco.Core.Persistence.DatabaseAnnotations; using Umbraco.Core.Persistence.DatabaseModelDefinitions; +using Umbraco.Core.Persistence.Querying; namespace Umbraco.Core.Persistence.SqlSyntax { - /// - /// Static class that provides simple access to the Sql CE SqlSyntax Provider - /// - internal static class SqlCeSyntax - { - public static ISqlSyntaxProvider Provider { get { return new SqlCeSyntaxProvider(); } } - } - /// /// Represents an SqlSyntaxProvider for Sql Ce /// @@ -67,6 +60,62 @@ namespace Umbraco.Core.Persistence.SqlSyntax return indexType; } + public override string GetStringColumnEqualComparison(string column, string value, TextColumnType columnType) + { + switch (columnType) + { + case TextColumnType.NVarchar: + return base.GetStringColumnEqualComparison(column, value, columnType); + case TextColumnType.NText: + //MSSQL doesn't allow for = comparison with NText columns but allows this syntax + return string.Format("{0} LIKE '{1}'", column, value); + default: + throw new ArgumentOutOfRangeException("columnType"); + } + } + + public override string GetStringColumnStartsWithComparison(string column, string value, TextColumnType columnType) + { + switch (columnType) + { + case TextColumnType.NVarchar: + return base.GetStringColumnStartsWithComparison(column, value, columnType); + case TextColumnType.NText: + //MSSQL doesn't allow for upper methods with NText columns + return string.Format("{0} LIKE '{1}%'", column, value); + default: + throw new ArgumentOutOfRangeException("columnType"); + } + } + + public override string GetStringColumnEndsWithComparison(string column, string value, TextColumnType columnType) + { + switch (columnType) + { + case TextColumnType.NVarchar: + return base.GetStringColumnEndsWithComparison(column, value, columnType); + case TextColumnType.NText: + //MSSQL doesn't allow for upper methods with NText columns + return string.Format("{0} LIKE '%{1}'", column, value); + default: + throw new ArgumentOutOfRangeException("columnType"); + } + } + + public override string GetStringColumnContainsComparison(string column, string value, TextColumnType columnType) + { + switch (columnType) + { + case TextColumnType.NVarchar: + return base.GetStringColumnContainsComparison(column, value, columnType); + case TextColumnType.NText: + //MSSQL doesn't allow for upper methods with NText columns + return string.Format("{0} LIKE '%{1}%'", column, value); + default: + throw new ArgumentOutOfRangeException("columnType"); + } + } + public override string GetQuotedTableName(string tableName) { return string.Format("[{0}]", tableName); diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntax.cs b/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntax.cs new file mode 100644 index 0000000000..a2f000e4b8 --- /dev/null +++ b/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntax.cs @@ -0,0 +1,10 @@ +namespace Umbraco.Core.Persistence.SqlSyntax +{ + /// + /// Static class that provides simple access to the Sql Server SqlSyntax Provider + /// + internal static class SqlServerSyntax + { + public static ISqlSyntaxProvider Provider { get { return new SqlServerSyntaxProvider(); } } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs b/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs index 3e37674ddf..24177c61d5 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs @@ -2,31 +2,10 @@ using System.Collections.Generic; using System.Linq; using Umbraco.Core.Persistence.DatabaseModelDefinitions; +using Umbraco.Core.Persistence.Querying; namespace Umbraco.Core.Persistence.SqlSyntax { - /// - /// Static class that provides simple access to the Sql Server SqlSyntax Provider - /// - internal static class SqlServerSyntax - { - public static ISqlSyntaxProvider Provider { get { return new SqlServerSyntaxProvider(); } } - } - - /// - /// Represents the version name of SQL server (i.e. the year 2008, 2005, etc...) - /// - internal enum SqlServerVersionName - { - Invalid = -1, - V7 = 0, - V2000 = 1, - V2005 = 2, - V2008 = 3, - V2012 = 4, - Other = 5 - } - /// /// Represents an SqlSyntaxProvider for Sql Server /// @@ -55,6 +34,62 @@ namespace Umbraco.Core.Persistence.SqlSyntax /// internal Lazy VersionName { get; set; } + public override string GetStringColumnEqualComparison(string column, string value, TextColumnType columnType) + { + switch (columnType) + { + case TextColumnType.NVarchar: + return base.GetStringColumnEqualComparison(column, value, columnType); + case TextColumnType.NText: + //MSSQL doesn't allow for = comparison with NText columns but allows this syntax + return string.Format("{0} LIKE '{1}'", column, value); + default: + throw new ArgumentOutOfRangeException("columnType"); + } + } + + public override string GetStringColumnStartsWithComparison(string column, string value, TextColumnType columnType) + { + switch (columnType) + { + case TextColumnType.NVarchar: + return base.GetStringColumnStartsWithComparison(column, value, columnType); + case TextColumnType.NText: + //MSSQL doesn't allow for upper methods with NText columns + return string.Format("{0} LIKE '{1}%'", column, value); + default: + throw new ArgumentOutOfRangeException("columnType"); + } + } + + public override string GetStringColumnEndsWithComparison(string column, string value, TextColumnType columnType) + { + switch (columnType) + { + case TextColumnType.NVarchar: + return base.GetStringColumnEndsWithComparison(column, value, columnType); + case TextColumnType.NText: + //MSSQL doesn't allow for upper methods with NText columns + return string.Format("{0} LIKE '%{1}'", column, value); + default: + throw new ArgumentOutOfRangeException("columnType"); + } + } + + public override string GetStringColumnContainsComparison(string column, string value, TextColumnType columnType) + { + switch (columnType) + { + case TextColumnType.NVarchar: + return base.GetStringColumnContainsComparison(column, value, columnType); + case TextColumnType.NText: + //MSSQL doesn't allow for upper methods with NText columns + return string.Format("{0} LIKE '%{1}%'", column, value); + default: + throw new ArgumentOutOfRangeException("columnType"); + } + } + public override string GetQuotedTableName(string tableName) { return string.Format("[{0}]", tableName); diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerVersionName.cs b/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerVersionName.cs new file mode 100644 index 0000000000..37870a9536 --- /dev/null +++ b/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerVersionName.cs @@ -0,0 +1,16 @@ +namespace Umbraco.Core.Persistence.SqlSyntax +{ + /// + /// Represents the version name of SQL server (i.e. the year 2008, 2005, etc...) + /// + internal enum SqlServerVersionName + { + Invalid = -1, + V7 = 0, + V2000 = 1, + V2005 = 2, + V2008 = 3, + V2012 = 4, + Other = 5 + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs b/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs index e86222ac50..8505d1e553 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Text; using Umbraco.Core.Persistence.DatabaseAnnotations; using Umbraco.Core.Persistence.DatabaseModelDefinitions; +using Umbraco.Core.Persistence.Querying; namespace Umbraco.Core.Persistence.SqlSyntax { @@ -102,6 +103,30 @@ namespace Umbraco.Core.Persistence.SqlSyntax DbTypeMap.Set(DbType.Binary, BlobColumnDefinition); } + public virtual string GetStringColumnEqualComparison(string column, string value, TextColumnType columnType) + { + //use the 'upper' method to always ensure strings are matched without case sensitivity no matter what the db setting. + return string.Format("upper({0}) = '{1}'", column, value.ToUpper()); + } + + public virtual string GetStringColumnStartsWithComparison(string column, string value, TextColumnType columnType) + { + //use the 'upper' method to always ensure strings are matched without case sensitivity no matter what the db setting. + return string.Format("upper({0}) like '{1}%'", column, value.ToUpper()); + } + + public virtual string GetStringColumnEndsWithComparison(string column, string value, TextColumnType columnType) + { + //use the 'upper' method to always ensure strings are matched without case sensitivity no matter what the db setting. + return string.Format("upper({0}) like '%{1}'", column, value.ToUpper()); + } + + public virtual string GetStringColumnContainsComparison(string column, string value, TextColumnType columnType) + { + //use the 'upper' method to always ensure strings are matched without case sensitivity no matter what the db setting. + return string.Format("upper({0}) like '%{1}%'", column, value.ToUpper()); + } + public virtual string GetQuotedTableName(string tableName) { return string.Format("\"{0}\"", tableName); diff --git a/src/Umbraco.Core/StringExtensions.cs b/src/Umbraco.Core/StringExtensions.cs index 1832e6906b..d14dbc7c6b 100644 --- a/src/Umbraco.Core/StringExtensions.cs +++ b/src/Umbraco.Core/StringExtensions.cs @@ -465,6 +465,11 @@ namespace Umbraco.Core return compare.StartsWith(compareTo, StringComparison.InvariantCultureIgnoreCase); } + public static bool InvariantEndsWith(this string compare, string compareTo) + { + return compare.EndsWith(compareTo, StringComparison.InvariantCultureIgnoreCase); + } + public static bool InvariantContains(this string compare, string compareTo) { return compare.IndexOf(compareTo, StringComparison.OrdinalIgnoreCase) >= 0; diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 0ce3f0bf9a..48e9d5c5be 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -200,6 +200,12 @@ + + + + + +