using System.Collections; using System.Text; using System.Text.RegularExpressions; using Strata.SqlTools.SqlBreakdown.Classes; using Strata.SqlTools.SqlBreakdown.Enums.SQL; using Strata.SqlTools.SqlBreakdown.Utilities; namespace Strata.SqlTools.Statements.SqlServer; /// /// Provides shared SQL parsing utilities for normalizing and cleaning SQL statements. /// public class StatementParser { #region Constants public const string KeywordWith = "WITH"; public const string KeywordSelect = "SELECT"; public const string KeywordFrom = "FROM"; public const string KeywordWhere = "WHERE"; public const string KeywordGroupBy = "GROUP BY"; public const string KeywordHaving = "HAVING"; public const string KeywordOrderBy = "ORDER BY"; private static readonly char[] separator = new[] { '\r', '\n' }; private static readonly char[] semicolonSeparator = new[] { ';' }; #endregion #region Normalization Methods /// /// Normalizes SQL by removing comments and extra whitespace. /// /// The SQL statement to normalize. /// The normalized SQL statement. public virtual string NormalizeSql(string sql) { // Remove SQL comments before processing sql = RemoveSqlComments(sql); // Replace multiple whitespace/newlines with single space sql = Regex.Replace(sql, @"\s+", " ", RegexOptions.None, RegexDefaults.MatchTimeout); return sql.Trim(); } /// /// Normalizes SQL whitespace while preserving comments. /// /// The SQL statement to normalize. /// The normalized SQL statement with comments preserved. public virtual string NormalizeSqlPreservingComments(string sql) { // Replace multiple spaces/tabs with single space, but preserve newlines for comment handling sql = Regex.Replace(sql, @"[ \t]+", " ", RegexOptions.None, RegexDefaults.MatchTimeout); // Remove leading/trailing whitespace from each line var lines = sql.Split(separator, StringSplitOptions.None); sql = string.Join("\n", lines.Select(line => line.Trim())); return sql.Trim(); } /// /// Removes single-line (--) and multi-line (/* */) SQL comments from the SQL statement. /// /// The SQL statement containing comments. /// The SQL statement with comments removed. #pragma warning disable S3776 // Cognitive Complexity of methods should not be too high #pragma warning disable S127 // "for" loop stop conditions should be invariant public virtual string RemoveSqlComments(string sql) { var result = new StringBuilder(); var inString = false; char stringChar = '\0'; var inMultiLineComment = false; for (int i = 0; i < sql.Length; i++) { if (inMultiLineComment) { if (i + 1 < sql.Length && sql[i] == '*' && sql[i + 1] == '/') { inMultiLineComment = false; i++; // Skip the '/' } continue; } if (!inString) { // Check for single-line comment if (i + 1 < sql.Length && sql[i] == '-' && sql[i + 1] == '-') { i++; // Skip the second '-' // Skip until end of line while (i < sql.Length && sql[i] != '\n' && sql[i] != '\r') { i++; } if (i < sql.Length) { result.Append(sql[i]); // Keep the newline } continue; } // Check for multi-line comment if (i + 1 < sql.Length && sql[i] == '/' && sql[i + 1] == '*') { inMultiLineComment = true; i++; // Skip the second character '*', loop increment will advance past it continue; } // Check for string start if (sql[i] == '\'' || sql[i] == '"') { inString = true; stringChar = sql[i]; } } else { // Check for string end if (sql[i] == stringChar && (i == 0 || sql[i - 1] != '\\')) { inString = false; } } result.Append(sql[i]); } return result.ToString(); } #pragma warning restore S127 #pragma warning restore S3776 /// /// Extracts single-line (--) and multi-line (/* */) SQL comments from the SQL statement. /// Returns both the SQL without comments and the extracted comments. /// /// The SQL statement containing comments. /// The extracted comments as a list of strings. /// The SQL statement with comments removed. #pragma warning disable S3776 // Cognitive Complexity of methods should not be too high #pragma warning disable S127 // "for" loop stop conditions should be invariant public virtual string ExtractSqlComments(string sql, out List comments) { comments = []; var result = new StringBuilder(); var inString = false; char stringChar = '\0'; var inMultiLineComment = false; var currentComment = new StringBuilder(); for (int i = 0; i < sql.Length; i++) { if (inMultiLineComment) { currentComment.Append(sql[i]); if (i + 1 < sql.Length && sql[i] == '*' && sql[i + 1] == '/') { currentComment.Append(sql[i + 1]); // Add '/' comments.Add(currentComment.ToString()); currentComment.Clear(); inMultiLineComment = false; i++; // Skip the '/' } continue; } if (!inString) { // Check for single-line comment if (i + 1 < sql.Length && sql[i] == '-' && sql[i + 1] == '-') { currentComment.Append(sql[i]); i++; // Skip the second '-' currentComment.Append(sql[i]); // Collect until end of line while (i + 1 < sql.Length && sql[i + 1] != '\n' && sql[i + 1] != '\r') { i++; currentComment.Append(sql[i]); } comments.Add(currentComment.ToString()); currentComment.Clear(); // The newline will be handled by the normal loop flow continue; } // Check for multi-line comment if (i + 1 < sql.Length && sql[i] == '/' && sql[i + 1] == '*') { inMultiLineComment = true; currentComment.Append(sql[i]); i++; // Skip the second character '*' currentComment.Append(sql[i]); continue; } // Check for string start if (sql[i] == '\'' || sql[i] == '"') { inString = true; stringChar = sql[i]; } } else { // Check for string end if (sql[i] == stringChar && (i == 0 || sql[i - 1] != '\\')) { inString = false; } } result.Append(sql[i]); } return result.ToString(); } #pragma warning restore S127 #pragma warning restore S3776 #endregion #region Clause Extraction Methods /// /// Gets the setup keywords to look for before the main SELECT statement. /// /// Array of setup keywords. protected virtual string[] GetSetupKeywords() { return new[] { "CREATE", "DECLARE", "SET" }; } public virtual string ExtractSetupClauses(string sql, List setupClauses) { // Look for common setup patterns like CREATE TABLE, DECLARE, etc. var setupKeywords = GetSetupKeywords(); // Simple extraction: look for statements before the main SELECT var selectIndex = Regex.Match( sql, @"\bSELECT\b", RegexOptions.IgnoreCase, RegexDefaults.MatchTimeout).Index; if (selectIndex > 0) { var beforeSelect = sql.Substring(0, selectIndex).Trim(); var matchingKeyword = setupKeywords.FirstOrDefault(keyword => beforeSelect.StartsWith(keyword, StringComparison.OrdinalIgnoreCase)); if (matchingKeyword != null) { // Extract setup clauses (simplified - would need more robust parsing for production) var statements = beforeSelect.Split(semicolonSeparator, StringSplitOptions.RemoveEmptyEntries) .Select(stmt => stmt.Trim()) .Where(trimmed => !string.IsNullOrEmpty(trimmed)); setupClauses.AddRange(statements); return sql.Substring(selectIndex); } } return sql; } /// /// Gets the regex pattern for finish clauses (DROP statements, etc.). /// /// Regex pattern string. protected virtual string GetFinishClausePattern() { return @";\s*(DROP\s+TABLE|DROP\s+PROCEDURE)"; } public virtual string ExtractFinishClauses(string sql, ArrayList finishClauses) { // Look for DROP TABLE or other cleanup statements after the main query var finishPattern = GetFinishClausePattern(); var match = Regex.Match( sql, finishPattern, RegexOptions.IgnoreCase, RegexDefaults.MatchTimeout); if (match.Success) { var finishSql = sql.Substring(match.Index + 1).Trim(); var statements = finishSql.Split(semicolonSeparator, StringSplitOptions.RemoveEmptyEntries); foreach (var stmt in statements) { var trimmed = stmt.Trim(); if (!string.IsNullOrEmpty(trimmed)) { finishClauses.Add(trimmed); } } return sql.Substring(0, match.Index); } return sql; } public virtual bool TryParseWithClause(string sql, out string? withClause, out string mainQuery) { withClause = null; mainQuery = sql; // Check if SQL starts with WITH var withMatch = Regex.Match( sql, @"^\s*WITH\b", RegexOptions.IgnoreCase, RegexDefaults.MatchTimeout); if (!withMatch.Success) { return false; } // Find the main SELECT that follows the WITH clause // We need to find the top-level SELECT (not one inside a CTE) int depth = 0; int withStart = withMatch.Index + withMatch.Length; int selectIndex = -1; for (int i = withStart; i < sql.Length; i++) { if (sql[i] == '(') { depth++; } else if (sql[i] == ')') { depth--; } else if (depth == 0 && i + 6 <= sql.Length) { // Check if we're at a SELECT keyword at top level var selectMatch = Regex.Match( sql.Substring(i), @"^\s*SELECT\b", RegexOptions.IgnoreCase, RegexDefaults.MatchTimeout); if (selectMatch.Success && IsTopLevelKeyword(sql, i + selectMatch.Index)) { selectIndex = i + selectMatch.Index; break; } } } if (selectIndex > withStart) { withClause = sql.Substring(withStart, selectIndex - withStart).Trim(); mainQuery = sql.Substring(selectIndex).Trim(); return true; } return false; } #endregion #region SELECT Statement Parsing /// /// Gets the array of SQL keywords to search for in the statement. /// /// Array of keywords to find. protected virtual string[] GetKeywordsToFind() => [KeywordWith, KeywordSelect, KeywordFrom, KeywordWhere, KeywordGroupBy, KeywordHaving, KeywordOrderBy]; public virtual bool TryParseSelectStatement(string sql, out SqlClauses? clauses, out string errorMessage) { clauses = null; errorMessage = null!; try { // Check if it starts with WITH or SELECT (skip leading comments) var sqlTrimmed = sql.TrimStart(); // Skip leading comments while (sqlTrimmed.StartsWith("--") || sqlTrimmed.StartsWith("/*")) { if (sqlTrimmed.StartsWith("--")) { // Skip to end of line var newlineIdx = sqlTrimmed.IndexOf('\n'); if (newlineIdx < 0) { break; // No newline found, can't continue } sqlTrimmed = sqlTrimmed.Substring(newlineIdx + 1).TrimStart(); } else if (sqlTrimmed.StartsWith("/*")) { // Skip to end of multiline comment var endIdx = sqlTrimmed.IndexOf("*/"); if (endIdx < 0) { break; // No end found, can't continue } sqlTrimmed = sqlTrimmed.Substring(endIdx + 2).TrimStart(); } } if (!Regex.IsMatch(sqlTrimmed, @"^\s*(WITH|SELECT)\b", RegexOptions.IgnoreCase, RegexDefaults.MatchTimeout)) { errorMessage = "SQL statement must start with WITH or SELECT."; clauses = null; return false; } var clausePositions = FindClausePositions(sql); if (!clausePositions.ContainsKey(KeywordSelect)) { errorMessage = "No SELECT clause found."; clauses = null; return false; } clauses = ExtractAllClauses(sql, clausePositions); return true; } catch (Exception ex) { errorMessage = $"Error parsing SELECT statement: {ex.Message}"; clauses = null; return false; } } #pragma warning disable S3776 // Cognitive Complexity of methods should not be too high public virtual Dictionary FindClausePositions(string sql) { var clausePositions = new Dictionary(); var keywords = GetKeywordsToFind(); var keywordSet = new HashSet(keywords, StringComparer.OrdinalIgnoreCase); // Tokenize the SQL using dialect-specific rules var tokens = TokenizeSql(sql); // Now find keywords at appropriate depth levels ProcessTokensForKeywords(tokens, keywords, keywordSet, clausePositions); return clausePositions; } #pragma warning restore S127 #pragma warning restore S3776 /// /// Tokenizes SQL statement into a list of tokens for keyword extraction. /// SQL Server-specific: Treats both single and double quotes as string literals. /// Handles comments during tokenization. /// /// The SQL statement to tokenize. /// List of tokens with type, value, and position. #pragma warning disable S3776 // Cognitive Complexity of methods should not be too high #pragma warning disable S127 // "for" loop stop conditions should be invariant protected virtual List<(TokenType type, string value, int position)> TokenizeSql(string sql) { var tokens = new List<(TokenType type, string value, int position)>(); // Manual tokenization that respects SQL syntax int i = 0; bool inSingleLineComment = false; bool inMultiLineComment = false; while (i < sql.Length) { char c = sql[i]; // Handle comments - skip them during tokenization (if enabled) if (ShouldHandleComments()) { if (inSingleLineComment) { if (c == '\n' || c == '\r') { inSingleLineComment = false; } i++; continue; } if (inMultiLineComment) { if (i + 1 < sql.Length && c == '*' && sql[i + 1] == '/') { inMultiLineComment = false; i += 2; // Skip */ continue; } i++; continue; } // Check for start of single-line comment if (i + 1 < sql.Length && c == '-' && sql[i + 1] == '-') { inSingleLineComment = true; i += 2; continue; } // Check for start of multi-line comment if (i + 1 < sql.Length && c == '/' && sql[i + 1] == '*') { inMultiLineComment = true; i += 2; continue; } } // Skip whitespace but track position if (char.IsWhiteSpace(c)) { i++; continue; } // Handle parentheses if (c == '(') { tokens.Add((TokenType.LeftParenthesis, "(", i)); i++; continue; } if (c == ')') { tokens.Add((TokenType.RightParenthesis, ")", i)); i++; continue; } // Handle double-quote (dialect-specific: string literal or identifier) if (c == '"') { var (token, newPosition) = HandleDoubleQuote(sql, i); tokens.Add(token); i = newPosition; continue; } // Handle bracketed identifiers [ColumnName] if (c == '[') { int start = i; i++; // Skip opening bracket var identifier = new StringBuilder(); while (i < sql.Length && sql[i] != ']') { identifier.Append(sql[i]); i++; } if (i < sql.Length) { i++; // Skip closing bracket } tokens.Add((TokenType.ColumnIdentifier, identifier.ToString(), start)); continue; } // Handle single-quoted string literals if (c == '\'') { char quote = c; int start = i; var str = new StringBuilder(); str.Append(c); i++; while (i < sql.Length && sql[i] != quote) { str.Append(sql[i]); i++; } if (i < sql.Length) { str.Append(sql[i]); // Include closing quote i++; } tokens.Add((TokenType.String, str.ToString(), start)); continue; } // Handle words (potential keywords or identifiers) if (IsWordStartCharacter(c)) { int start = i; var word = new StringBuilder(); while (i < sql.Length && (char.IsLetterOrDigit(sql[i]) || sql[i] == '_')) { word.Append(sql[i]); i++; } tokens.Add((TokenType.String, word.ToString(), start)); continue; } // Skip other characters (operators, commas, etc.) i++; } return tokens; } #pragma warning restore S127 #pragma warning restore S3776 /// /// Determines whether comments should be handled during tokenization. /// SQL Server: true (handles -- and /* */ comments). /// /// True if comments should be handled during tokenization. protected virtual bool ShouldHandleComments() => true; /// /// Determines whether a character can start a word (keyword or identifier). /// SQL Server: Only letters can start words. /// /// The character to check. /// True if the character can start a word. protected virtual bool IsWordStartCharacter(char c) => char.IsLetter(c); /// /// Handles double-quote character during tokenization. /// SQL Server: Treats double-quote as string literal (same as single quote). /// /// The SQL statement being tokenized. /// Current position in the SQL string. /// Token and new position after the token. protected virtual ((TokenType type, string value, int position) token, int newPosition) HandleDoubleQuote(string sql, int position) { // SQL Server: double-quote is a string literal (same as single quote) char quote = '"'; int start = position; var str = new StringBuilder(); str.Append(quote); position++; while (position < sql.Length && sql[position] != quote) { str.Append(sql[position]); position++; } if (position < sql.Length) { str.Append(sql[position]); // Include closing quote position++; } return ((TokenType.String, str.ToString(), start), position); } /// /// Helper used by dialects (PostgreSQL, Snowflake) where double-quoted text is an /// identifier rather than a string literal. Reads from the opening quote at /// and returns the inner text as a /// token. /// /// The SQL statement being tokenized. /// The current position in the SQL string (must point at the opening "). /// The identifier token and the new position past the closing ". protected static ((TokenType type, string value, int position) token, int newPosition) HandleDoubleQuoteAsIdentifier(string sql, int position) { int start = position; position++; // Skip opening quote var identifier = new StringBuilder(); while (position < sql.Length && sql[position] != '"') { identifier.Append(sql[position]); position++; } if (position < sql.Length) { position++; // Skip closing quote } return ((TokenType.ColumnIdentifier, identifier.ToString(), start), position); } /// /// Processes a list of tokens to find SQL keywords at the top level (outside parentheses). /// /// The list of parsed tokens. /// Array of keywords to search for. /// HashSet for efficient keyword lookup. /// Dictionary to populate with found keyword positions. protected virtual void ProcessTokensForKeywords( List<(TokenType type, string value, int position)> tokens, string[] keywords, HashSet keywordSet, Dictionary clausePositions) { int parenthesisDepth = 0; bool skipNextToken = false; for (int tokenIndex = 0; tokenIndex < tokens.Count; tokenIndex++) { // Handle skipping of consumed tokens (e.g., "BY" after "GROUP" or "ORDER") if (skipNextToken) { skipNextToken = false; continue; } var (type, value, position) = tokens[tokenIndex]; if (type == TokenType.LeftParenthesis) { parenthesisDepth++; continue; } if (type == TokenType.RightParenthesis) { parenthesisDepth--; continue; } // Only process keywords at top level (outside parentheses) if (parenthesisDepth == 0 && type == TokenType.String) { skipNextToken = TryRecordKeywordAtToken(tokens, tokenIndex, value, position, keywords, keywordSet, clausePositions); } } } /// /// Records the keyword at the given token (single-word or multi-word) into . /// /// True if a multi-word keyword (GROUP BY / ORDER BY) was matched and the following token should be skipped. private static bool TryRecordKeywordAtToken( List<(TokenType type, string value, int position)> tokens, int tokenIndex, string value, int position, string[] keywords, HashSet keywordSet, Dictionary clausePositions) { // Check for multi-word keywords (GROUP BY, ORDER BY); they consume the following BY token. if (TryRecordMultiWordKeyword(tokens, tokenIndex, "GROUP", KeywordGroupBy, clausePositions) || TryRecordMultiWordKeyword(tokens, tokenIndex, "ORDER", KeywordOrderBy, clausePositions)) { return true; } if (keywordSet.Contains(value)) { var matchedKeyword = keywords.FirstOrDefault(k => string.Equals(k, value, StringComparison.OrdinalIgnoreCase)); if (matchedKeyword != null && !clausePositions.ContainsKey(matchedKeyword)) { clausePositions[matchedKeyword] = position; } } return false; } /// /// Records a two-word keyword (e.g., "GROUP BY") when the token at matches /// and is immediately followed by "BY". /// /// True if the multi-word keyword pattern matched. private static bool TryRecordMultiWordKeyword( List<(TokenType type, string value, int position)> tokens, int tokenIndex, string firstWord, string canonicalKeyword, Dictionary clausePositions) { var (_, value, position) = tokens[tokenIndex]; if (!string.Equals(value, firstWord, StringComparison.OrdinalIgnoreCase) || tokenIndex + 1 >= tokens.Count || tokens[tokenIndex + 1].type != TokenType.String || !string.Equals(tokens[tokenIndex + 1].value, "BY", StringComparison.OrdinalIgnoreCase)) { return false; } if (!clausePositions.ContainsKey(canonicalKeyword)) { clausePositions[canonicalKeyword] = position; } return true; } public virtual SqlClauses ExtractAllClauses(string sql, Dictionary clausePositions) { var clauses = new SqlClauses { SelectClause = ExtractExpressionClauseWithComments(sql, clausePositions, KeywordSelect, KeywordSelect.Length, GetNextClausePosition(clausePositions, sql.Length, KeywordFrom), splitOnComma: true), FromClause = clausePositions.ContainsKey(KeywordFrom) ? ExtractClauseWithComments(sql, clausePositions, KeywordFrom, KeywordFrom.Length, GetNextClausePosition(clausePositions, sql.Length, KeywordWhere, KeywordGroupBy, KeywordOrderBy)) : null, WhereClause = clausePositions.ContainsKey(KeywordWhere) ? ExtractExpressionClauseWithComments(sql, clausePositions, KeywordWhere, KeywordWhere.Length, GetNextClausePosition(clausePositions, sql.Length, KeywordGroupBy, KeywordOrderBy), splitOnComma: false) : null, GroupByClause = clausePositions.ContainsKey(KeywordGroupBy) ? ExtractExpressionClauseWithComments(sql, clausePositions, KeywordGroupBy, KeywordGroupBy.Length, GetNextClausePosition(clausePositions, sql.Length, KeywordHaving, KeywordOrderBy), splitOnComma: true) : null, HavingClause = clausePositions.ContainsKey(KeywordHaving) ? ExtractExpressionClauseWithComments(sql, clausePositions, KeywordHaving, KeywordHaving.Length, GetNextClausePosition(clausePositions, sql.Length, KeywordOrderBy), splitOnComma: false) : null, OrderByClause = clausePositions.ContainsKey(KeywordOrderBy) ? ExtractExpressionClauseWithComments(sql, clausePositions, KeywordOrderBy, KeywordOrderBy.Length, sql.Length, splitOnComma: true) : null }; // Allow derived classes to post-process clauses for dialect-specific pagination (LIMIT, OFFSET, TOP, etc.) PostProcessClauses(clauses, sql, clausePositions); return clauses; } /// /// Post-processes extracted clauses to handle dialect-specific pagination syntax (LIMIT, OFFSET, TOP, etc.). /// Base implementation does nothing - override in derived classes for dialect-specific behavior. /// /// The extracted clauses to post-process. /// The original SQL statement. /// Dictionary of keyword positions. protected virtual void PostProcessClauses(SqlClauses clauses, string sql, Dictionary clausePositions) { // Base implementation: no post-processing needed for standard SQL Server } public virtual SqlClause ExtractClauseWithComments(string sql, Dictionary positions, string keyword, int keywordLength, int endPosition) { var startPosition = positions[keyword] + keywordLength; var clauseText = sql.Substring(startPosition, endPosition - startPosition); // Extract comments from this clause var clauseWithoutComments = ExtractSqlComments(clauseText, out var comments); return new SqlClause { Clause = clauseWithoutComments.Trim(), Comment = comments.Count > 0 ? string.Join(" ", comments) : null }; } /// /// Extracts a SQL expression clause (SELECT, WHERE, HAVING) with comments. /// public virtual SqlExpressionClause ExtractExpressionClauseWithComments(string sql, Dictionary positions, string keyword, int keywordLength, int endPosition, bool splitOnComma) { var startPosition = positions[keyword] + keywordLength; var clauseText = sql.Substring(startPosition, endPosition - startPosition); // Extract comments from this clause var clauseWithoutComments = ExtractSqlComments(clauseText, out var comments); return new SqlExpressionClause(splitOnComma) { Clause = clauseWithoutComments.Trim(), Comment = comments.Count > 0 ? string.Join(" ", comments) : null }; } public virtual string ExtractClause(string sql, Dictionary positions, string keyword, int keywordLength, int endPosition) { var startPosition = positions[keyword] + keywordLength; return sql.Substring(startPosition, endPosition - startPosition).Trim(); } public virtual int GetNextClausePosition(Dictionary clausePositions, int defaultEnd, params string[] keywords) { return keywords.Where(clausePositions.ContainsKey) .Select(keyword => clausePositions[keyword]) .DefaultIfEmpty(defaultEnd) .First(); } #endregion #region Parameter Extraction public virtual void ExtractParameters(Dictionary parameters, string sql) { ExtractParameters(parameters, sql, @"@([a-zA-Z_][a-zA-Z0-9_]*)"); } /// /// Extracts parameters from SQL using the specified pattern. /// /// The parameter dictionary to populate. /// The SQL statement to extract parameters from. /// The regex pattern to match parameter names. protected virtual void ExtractParameters(Dictionary parameters, string sql, string paramPattern) { var matches = Regex.Matches(sql, paramPattern, RegexOptions.None, RegexDefaults.MatchTimeout); var paramNames = matches.Cast() .Select(match => match.Value) .Distinct(); foreach (var paramName in paramNames) { // Initialize parameter with null value // User can set actual values later using SetParameterValue parameters[paramName] = null!; } } #endregion #region Helper Methods #pragma warning disable S3776 // Cognitive Complexity of methods should not be too high public virtual bool IsTopLevelKeyword(string sql, int position) #pragma warning restore S3776 { // Check if the keyword at 'position' is at the top level (not in a subquery or string) int parenthesisDepth = 0; bool inString = false; char stringChar = '\0'; for (int i = 0; i < position; i++) { if (!inString) { if (sql[i] == '(' && (i == 0 || sql[i - 1] != '\\')) { parenthesisDepth++; } else if (sql[i] == ')' && (i == 0 || sql[i - 1] != '\\')) { parenthesisDepth--; } else if ((sql[i] == '\'' || sql[i] == '"') && (i == 0 || sql[i - 1] != '\\')) { inString = true; stringChar = sql[i]; } } else { if (sql[i] == stringChar && (i == 0 || sql[i - 1] != '\\')) { inString = false; } } } return parenthesisDepth == 0 && !inString; } #endregion }