using System; using System.Collections; using System.Collections.Generic; using System.Data; using System.Text; using NHibernate.Mapping; using NHibernate.SqlCommand; using NHibernate.Util; namespace NHibernate.Dialect { public class MsSql2005Dialect : MsSql2000Dialect { public MsSql2005Dialect() { RegisterColumnType(DbType.String, 1073741823, "NVARCHAR(MAX)"); RegisterColumnType(DbType.AnsiString, 2147483647, "VARCHAR(MAX)"); RegisterColumnType(DbType.Binary, 2147483647, "VARBINARY(MAX)"); } /// /// Add a LIMIT clause to the given SQL SELECT /// /// The to base the limit query off of. /// Offset of the first row to be returned by the query (zero-based) /// Maximum number of rows to be returned by the query /// A new with the LIMIT clause applied. /// /// The LIMIT SQL will look like /// /// /// SELECT /// TOP last (columns) /// FROM /// (SELECT (columns), ROW_NUMBER() OVER(ORDER BY {original order by, with un-aliased column names) as __hibernate_sort_row /// {original from}) as query /// WHERE query.__hibernate_sort_row > offset /// ORDER BY query.__hibernate_sort_row /// /// /// /// Note that we need to add explicitly specify the columns, because we need to be able to use them /// in a paged subselect. NH-1155 /// public override SqlString GetLimitString(SqlString querySqlString, int offset, int last) { //dont do this paging code if there is no offset, use the //sql 2000 dialect since it wont just uses a top statement if (offset == 0) { return base.GetLimitString(querySqlString, offset, last); } // we have to do this in order to support parameters in order clause, the foramt // that sql 2005 uses for paging means that we move the parameters around, which means, // that positions are lost, so we record them before making any changes. // NH-1528 int parameterPositon = 0; foreach (var part in querySqlString.Parts) { Parameter param = part as Parameter; if (param == null) continue; param.OriginalPositionInQuery = parameterPositon; parameterPositon += 1; } int fromIndex = GetFromIndex(querySqlString); SqlString select = querySqlString.Substring(0, fromIndex); List columnsOrAliases; Dictionary aliasToColumn; ExtractColumnOrAliasNames(select, out columnsOrAliases, out aliasToColumn); int orderIndex = querySqlString.LastIndexOfCaseInsensitive(" order by "); SqlString from; SqlString[] sortExpressions; //don't use the order index if it is contained within a larger statement(assuming //a statement with non matching parenthesis is part of a larger block) if (orderIndex > 0 && HasMatchingParens(querySqlString.Substring(orderIndex).ToString())) { from = querySqlString.Substring(fromIndex, orderIndex - fromIndex).Trim(); SqlString orderBy = querySqlString.Substring(orderIndex).Trim(); sortExpressions = orderBy.Substring(9).Split(","); } else { from = querySqlString.Substring(fromIndex).Trim(); // Use dummy sort to avoid errors sortExpressions = new[] {new SqlString("CURRENT_TIMESTAMP"),}; } SqlStringBuilder result = new SqlStringBuilder() .Add("SELECT TOP ") .Add(last.ToString()) .Add(" ") .Add(StringHelper.Join(", ", columnsOrAliases)) .Add(" FROM ( SELECT *, ROW_NUMBER() OVER(ORDER BY "); AppendSortExpressions(aliasToColumn, sortExpressions, result); result.Add(") as __hibernate_sort_row ") .Add(" FROM (") .Add(select) .Add(" ") .Add(from) .Add(") as q_) as query WHERE query.__hibernate_sort_row > ") .Add(offset.ToString()).Add(" ORDER BY query.__hibernate_sort_row"); return result.ToSqlString(); } private static SqlString RemoveSortOrderDirection(SqlString sortExpression) { SqlString trimmedExpression = sortExpression.Trim(); if (trimmedExpression.EndsWithCaseInsensitive("asc")) return trimmedExpression.Substring(0, trimmedExpression.Length - 3).Trim(); if (trimmedExpression.EndsWithCaseInsensitive("desc")) return trimmedExpression.Substring(0, trimmedExpression.Length - 4).Trim(); return trimmedExpression.Trim(); } private static void AppendSortExpressions(Dictionary aliasToColumn, SqlString[] sortExpressions, SqlStringBuilder result) { for (int i = 0; i < sortExpressions.Length; i++) { if (i > 0) { result.Add(", "); } SqlString sortExpression = RemoveSortOrderDirection(sortExpressions[i]); SqlString qualifiedColumn = null; if (aliasToColumn.ContainsKey(sortExpression)) { qualifiedColumn = aliasToColumn[sortExpression]; } else { qualifiedColumn = sortExpression; } result.Add(qualifiedColumn.Replace("this_", "q_")); if (sortExpressions[i].Trim().EndsWithCaseInsensitive("desc")) { result.Add(" DESC"); } } } private static int GetFromIndex(SqlString querySqlString) { string subselect = querySqlString.GetSubselectString().ToString(); int fromIndex = querySqlString.IndexOfCaseInsensitive(subselect); if (fromIndex == -1) { fromIndex = querySqlString.ToString().ToLowerInvariant().IndexOf(subselect.ToLowerInvariant()); } return fromIndex; } private static void ExtractColumnOrAliasNames(SqlString select, out List columnsOrAliases, out Dictionary aliasToColumn) { columnsOrAliases = new List(); aliasToColumn = new Dictionary(); IList tokens = new QuotedAndParenthesisStringTokenizer(select.ToString()).GetTokens(); int index = 0; while (index < tokens.Count) { string token = tokens[index]; index += 1; if ("select".Equals(token, StringComparison.InvariantCultureIgnoreCase)) { continue; } if ("distinct".Equals(token, StringComparison.InvariantCultureIgnoreCase)) { continue; } if ("," == token) { continue; } if ("from".Equals(token, StringComparison.InvariantCultureIgnoreCase)) { break; } //handle composite expressions like 2 * 4 as foo while (index < tokens.Count && "as".Equals(tokens[index], StringComparison.InvariantCultureIgnoreCase) == false && "," != tokens[index]) { token = token + " " + tokens[index]; index += 1; } string alias = token; bool isFunctionCallOrQuotedString = token.Contains("'") || token.Contains("("); // this is heuristic guess, if the expression contains ' or (, it is probably // not appropriate to just slice parts off of it if (isFunctionCallOrQuotedString == false) { int dot = token.IndexOf('.'); if (dot != -1) { alias = token.Substring(dot + 1); } } // notice! we are checking here the existence of "as" "alias", two // tokens from the current one if (index + 1 < tokens.Count && "as".Equals(tokens[index], StringComparison.InvariantCultureIgnoreCase)) { alias = tokens[index + 1]; index += 2; //skip the "as" and the alias \ } columnsOrAliases.Add(new SqlString(alias)); aliasToColumn[SqlString.Parse(token)] = SqlString.Parse(alias); } } /// /// Indicates whether the string fragment contains matching parenthesis /// /// the statement to evaluate /// true if the statment contains no parenthesis or an equal number of /// opening and closing parenthesis;otherwise false private static bool HasMatchingParens(IEnumerable statement) { //unmatched paren count int unmatchedParen = 0; //increment the counts based in the opening and closing parens in the statement foreach (char item in statement) { switch (item) { case '(': unmatchedParen++; break; case ')': unmatchedParen--; break; } } return unmatchedParen == 0; } /// /// Sql Server 2005 supports a query statement that provides LIMIT /// functionality. /// /// true public override bool SupportsLimit { get { return true; } } /// /// Sql Server 2005 supports a query statement that provides LIMIT /// functionality with an offset. /// /// true public override bool SupportsLimitOffset { get { return true; } } protected override string GetSelectExistingObject(string name, Table table) { string schema = table.GetQuotedSchemaName(this); if (schema != null) { schema += "."; } string objName = string.Format("{0}{1}", schema, Quote(name)); string parentName = string.Format("{0}{1}", schema, table.GetQuotedName(this)); return string.Format( "select 1 from sys.objects where object_id = OBJECT_ID(N'{0}') AND parent_object_id = OBJECT_ID('{1}')", objName, parentName); } /// /// Sql Server 2005 supports a query statement that provides LIMIT /// functionality with an offset. /// /// false public override bool UseMaxForLimit { get { return false; } } /// /// This specialized string tokenizier will break a string to tokens, taking /// into account single quotes, parenthesis and commas and [ ] /// Notice that we aren't differenciating between [ ) and ( ] on purpose, it would complicate /// the code and it is not legal at any rate. /// public class QuotedAndParenthesisStringTokenizer : IEnumerable { private readonly string original; public QuotedAndParenthesisStringTokenizer(string original) { this.original = original; } IEnumerator IEnumerable.GetEnumerator() { StringBuilder currentToken = new StringBuilder(); TokenizerState state = TokenizerState.WhiteSpace; int parenthesisCount = 0; bool escapeQuote = false; for (int i = 0; i < original.Length; i++) { char ch = original[i]; switch (state) { case TokenizerState.WhiteSpace: if (ch == '\'') { state = TokenizerState.Quoted; currentToken.Append(ch); } else if (ch == ',') { yield return ","; } else if (ch == '(' || ch == '[') { state = TokenizerState.InParenthesis; currentToken.Append(ch); parenthesisCount = 1; } else if (char.IsWhiteSpace(ch) == false) { state = TokenizerState.Token; currentToken.Append(ch); } break; case TokenizerState.Quoted: if (escapeQuote) { escapeQuote = false; currentToken.Append(ch); } // handle escaping of ' by using '' or \' else if (ch == '\\' || (ch == '\'' && i + 1 < original.Length && original[i + 1] == '\'')) { escapeQuote = true; currentToken.Append(ch); } else if (ch == '\'') { currentToken.Append(ch); yield return currentToken.ToString(); state = TokenizerState.WhiteSpace; currentToken.Length = 0; } else { currentToken.Append(ch); } break; case TokenizerState.InParenthesis: if (ch == ')' || ch == ']') { currentToken.Append(ch); parenthesisCount -= 1; if (parenthesisCount == 0) { yield return currentToken.ToString(); currentToken.Length = 0; state = TokenizerState.WhiteSpace; } } else if (ch == '(' || ch == '[') { currentToken.Append(ch); parenthesisCount += 1; } else { currentToken.Append(ch); } break; case TokenizerState.Token: if (char.IsWhiteSpace(ch)) { yield return currentToken.ToString(); currentToken.Length = 0; state = TokenizerState.WhiteSpace; } else if (ch == ',') // stop current token, and send the , as well { yield return currentToken.ToString(); currentToken.Length = 0; yield return ","; state = TokenizerState.WhiteSpace; } else if (ch == '(' || ch == '[') { state = TokenizerState.InParenthesis; parenthesisCount = 1; currentToken.Append(ch); } else if (ch == '\'') { state = TokenizerState.Quoted; currentToken.Append(ch); } else { currentToken.Append(ch); } break; default: throw new InvalidExpressionException("Could not understand the string " + original); } } if (currentToken.Length > 0) { yield return currentToken.ToString(); } } public IEnumerator GetEnumerator() { return ((IEnumerable)this).GetEnumerator(); } public enum TokenizerState { WhiteSpace, Quoted, InParenthesis, Token } public IList GetTokens() { return new List(this); } } } }