using System.Collections; using System.Text; using Strata.SqlTools.SqlBreakdown.Classes; using Strata.SqlTools.SqlBreakdown.Expressions; using Strata.SqlTools.SqlBreakdown.Interfaces; using Strata.SqlTools.SqlBreakdown.Interfaces.Core; using CommandVisitor = Strata.SqlTools.Visitors.Snowflake.CommandVisitor; using SqlClause = Strata.SqlTools.SqlBreakdown.Classes.SqlClause; using SqlExpressionClause = Strata.SqlTools.SqlBreakdown.Classes.SqlExpressionClause; using SqlServerCommandVisitor = Strata.SqlTools.Visitors.SqlServer.CommandVisitor; using SqlServerQueryBreakdown = Strata.SqlTools.Breakdowns.SqlServer.QueryBreakdown; using StatementExpressionParser = Strata.SqlTools.Statements.Snowflake.StatementExpressionParser; using StatementParser = Strata.SqlTools.Statements.Snowflake.StatementParser; namespace Strata.SqlTools.Breakdowns.Snowflake; /// /// Represents a Snowflake SQL query breakdown with all clauses, following Snowflake SQL standards. /// Handles both :parameter and @parameter syntax for Snowflake compatibility. /// public class QueryBreakdown : SqlServerQueryBreakdown { private const string ExpressionNullErrorMessage = "Expression cannot be null."; private static readonly StatementParser SnowflakeParserInstance = new StatementParser(); /// /// Initializes a new instance of the class. /// public QueryBreakdown() : base() { } /// /// Initializes a new instance of the class with SELECT and FROM clauses. /// /// The SELECT clause. /// The FROM clause. /// If true, uses Microsoft T-SQL parsing rules. If false, uses Snowflake parsing rules. Defaults to false. public QueryBreakdown(string selectClause, string fromClause, bool isMicrosoftSql = false) : base() { var parser = isMicrosoftSql ? Parser : SnowflakeParserInstance; parser.PopulateClauseWithComments(selectClause, SelectClause); parser.PopulateClauseWithComments(fromClause, FromClause); } /// /// Initializes a new instance of the class with SELECT, FROM, and WHERE clauses. /// /// The SELECT clause. /// The FROM clause. /// The WHERE clause. /// If true, uses Microsoft T-SQL parsing rules. If false, uses Snowflake parsing rules. Defaults to false. public QueryBreakdown(string selectClause, string fromClause, string whereClause, bool isMicrosoftSql = false) : this(selectClause, fromClause, isMicrosoftSql) { var parser = isMicrosoftSql ? Parser : SnowflakeParserInstance; var cleanWhere = parser.ExtractSqlComments(whereClause, out var whereComments); WhereClause.Clause = cleanWhere.Trim(); WhereClause.Comment = whereComments.Count > 0 ? string.Join(" ", whereComments) : null; } /// /// Initializes a new instance of the class with SELECT, FROM, WHERE, and ORDER BY clauses. /// /// The SELECT clause. /// The FROM clause. /// The WHERE clause. /// The ORDER BY clause. /// If true, uses Microsoft T-SQL parsing rules. If false, uses Snowflake parsing rules. Defaults to false. public QueryBreakdown(string selectClause, string fromClause, string whereClause, string orderByClause, bool isMicrosoftSql = false) : this(selectClause, fromClause, whereClause, isMicrosoftSql) { var parser = isMicrosoftSql ? Parser : SnowflakeParserInstance; var cleanOrderBy = parser.ExtractSqlComments(orderByClause, out var orderByComments); OrderByClause.Clause = cleanOrderBy.Trim(); OrderByClause.Comment = orderByComments.Count > 0 ? string.Join(" ", orderByComments) : null; } /// /// Adds a parameter to the query using Snowflake's :param format. /// Also adds @param format for compatibility. /// /// The parameter name (with or without : or @). /// The parameter value. public new void AddParameter(string parameterName, object value) { // Convert to Snowflake format (: prefix) var colonName = NormalizeParameterName(parameterName); var atName = "@" + colonName.TrimStart(':', '@'); // Use base class internal list base.AddParameter(colonName.TrimStart(':', '@'), value); // Add both formats to dictionary for compatibility Parameters.Remove($"@{colonName.TrimStart(':', '@')}"); Parameters[colonName] = value; Parameters[atName] = value; } /// /// Sets the value of a parameter using Snowflake's :param format. /// Also updates @param format for compatibility. /// /// The parameter name (with or without : or @). /// The parameter value. public new void SetParameterValue(string parameterName, object value) { var colonName = NormalizeParameterName(parameterName); var atName = "@" + colonName.TrimStart(':', '@'); Parameters[colonName] = value; Parameters[atName] = value; } /// /// Normalizes parameter name to Snowflake format (:param). /// private static string NormalizeParameterName(string parameterName) { if (string.IsNullOrEmpty(parameterName)) { return parameterName; } // If it already has : or @, preserve the prefix but prefer : if (parameterName.StartsWith(':')) { return parameterName; } if (parameterName.StartsWith('@')) { return string.Concat(":", parameterName.AsSpan(1)); } // Add : prefix return ":" + parameterName; } /// /// Ensures parameters exist in both @ and : formats for compatibility. /// private void NormalizeParameterFormats() { var paramKeys = Parameters.Keys.ToList(); foreach (var paramName in paramKeys) { if (paramName.StartsWith(':')) { // Add @param version var atParam = string.Concat("@", paramName.AsSpan(1)); if (!Parameters.ContainsKey(atParam)) { Parameters[atParam] = Parameters[paramName]; } } else if (paramName.StartsWith('@')) { // Add :param version var colonParam = string.Concat(":", paramName.AsSpan(1)); if (!Parameters.ContainsKey(colonParam)) { Parameters[colonParam] = Parameters[paramName]; } } } } /// /// Adds an expression to the SELECT clause. /// /// The expression to add. /// Optional comment to add with the expression. /// If true, uses Microsoft T-SQL formatting. If false, uses Snowflake formatting. Defaults to false. public void AddSelectExpression(Expression expression, string? comment = null, bool isMicrosoftSql = false) { if (expression is null) { throw new ArgumentNullException(nameof(expression), ExpressionNullErrorMessage); } var visitor = isMicrosoftSql ? (IVisitor)new SqlServerCommandVisitor() : new CommandVisitor(); var sql = expression.Accept(visitor); if (string.IsNullOrWhiteSpace(SelectClause.Clause)) { SelectClause.Clause = sql; } else { SelectClause.Clause = $"{SelectClause.Clause}, {sql}"; } if (!string.IsNullOrWhiteSpace(comment)) { if (string.IsNullOrWhiteSpace(SelectClause.Comment)) { SelectClause.Comment = comment; } else { SelectClause.Comment = $"{SelectClause.Comment} {comment}"; } } } /// /// Adds an expression to the WHERE clause. /// /// The expression to add. /// Optional comment to add with the expression. /// The logical operation ("and" or "or"). Defaults to "and". /// If true, uses Microsoft T-SQL formatting. If false, uses Snowflake formatting. Defaults to false. public void AddWhereExpression(Expression expression, string? comment = null, string operation = "and", bool isMicrosoftSql = false) { if (expression is null) { throw new ArgumentNullException(nameof(expression), ExpressionNullErrorMessage); } var visitor = isMicrosoftSql ? (IVisitor)new SqlServerCommandVisitor() : new CommandVisitor(); AppendToClause(WhereClause, expression.Accept(visitor), operation, comment); } /// /// Adds a WHERE clause condition. Defaults to "and" operation. /// /// The SQL condition to add. /// If true, uses Microsoft T-SQL parsing rules. If false, uses Snowflake parsing rules. Defaults to false. public void AddWhereClause(string sql, bool isMicrosoftSql = false) { AddWhereClause(sql, "and", isMicrosoftSql); } /// /// Adds a WHERE clause condition with a specific logical operation. /// Extracts and preserves any SQL comments in the clause. /// Uses Snowflake parsing rules by default. /// /// The SQL condition to add. /// The logical operation ("and" or "or"). public override void AddWhereClause(string sql, string operation) { AddWhereClause(sql, operation, isMicrosoftSql: false); } /// /// Adds a WHERE clause condition with a specific logical operation. /// Extracts and preserves any SQL comments in the clause. /// Automatically extracts parameters from the WHERE clause and adds them to the Parameters dictionary. /// /// The SQL condition to add. /// The logical operation ("and" or "or"). /// If true, uses Microsoft T-SQL parsing rules. If false, uses Snowflake parsing rules. Defaults to false. public void AddWhereClause(string sql, string operation, bool isMicrosoftSql) { if (string.IsNullOrWhiteSpace(sql)) { return; } // Use appropriate parser based on SQL dialect var parser = isMicrosoftSql ? Parser : SnowflakeParserInstance; // Extract comments from the incoming SQL var cleanSql = parser.ExtractSqlComments(sql, out var comments); var commentText = comments.Count > 0 ? string.Join(" ", comments) : null; AppendToClause(WhereClause, cleanSql.Trim(), operation, commentText); // Extract and add parameters from the WHERE clause using appropriate parser ExtractAndAddParametersWithParser(cleanSql, parser); } /// /// Extracts parameters from a SQL clause and adds them to the Parameters dictionary using the specified parser. /// /// The SQL clause to extract parameters from. /// The parser to use for extracting parameters. private void ExtractAndAddParametersWithParser(string sql, Statements.SqlServer.StatementParser parser) { if (string.IsNullOrWhiteSpace(sql)) { return; } // Create a temporary dictionary to extract parameters var tempParams = new Dictionary(); parser.ExtractParameters(tempParams, sql); // Add each parameter using the managed add method from base class foreach (var kvp in tempParams) { AddOrUpdateParameter(kvp.Key, kvp.Value); } } /// /// Adds an expression to the GROUP BY clause. /// /// The expression to add. /// Optional comment to add with the expression. public override void AddGroupByExpression(Expression expression, string? comment = null) { AddGroupByExpression(expression, comment, isMicrosoftSql: false); } /// /// Adds an expression to the GROUP BY clause. /// /// The expression to add. /// Optional comment to add with the expression. /// If true, uses Microsoft T-SQL formatting. If false, uses Snowflake formatting. Defaults to false. public void AddGroupByExpression(Expression expression, string? comment, bool isMicrosoftSql) { if (expression is null) { throw new ArgumentNullException(nameof(expression), ExpressionNullErrorMessage); } var visitor = isMicrosoftSql ? (IVisitor)new SqlServerCommandVisitor() : new CommandVisitor(); var sql = expression.Accept(visitor); if (string.IsNullOrWhiteSpace(GroupByClause.Clause)) { GroupByClause.Clause = sql; } else { GroupByClause.Clause = $"{GroupByClause.Clause}, {sql}"; } if (!string.IsNullOrWhiteSpace(comment)) { if (string.IsNullOrWhiteSpace(GroupByClause.Comment)) { GroupByClause.Comment = comment; } else { GroupByClause.Comment = $"{GroupByClause.Comment} {comment}"; } } } /// /// Adds an expression to the ORDER BY clause. /// /// The expression to add. /// Optional comment to add with the expression. public override void AddOrderByExpression(Expression expression, string? comment = null) { AddOrderByExpression(expression, comment, isMicrosoftSql: false); } /// /// Adds an expression to the ORDER BY clause. /// /// The expression to add. /// Optional comment to add with the expression. /// If true, uses Microsoft T-SQL formatting. If false, uses Snowflake formatting. Defaults to false. public void AddOrderByExpression(Expression expression, string? comment, bool isMicrosoftSql) { if (expression is null) { throw new ArgumentNullException(nameof(expression), ExpressionNullErrorMessage); } var visitor = isMicrosoftSql ? (IVisitor)new SqlServerCommandVisitor() : new CommandVisitor(); var sql = expression.Accept(visitor); if (string.IsNullOrWhiteSpace(OrderByClause.Clause)) { OrderByClause.Clause = sql; } else { OrderByClause.Clause = $"{OrderByClause.Clause}, {sql}"; } if (!string.IsNullOrWhiteSpace(comment)) { if (string.IsNullOrWhiteSpace(OrderByClause.Comment)) { OrderByClause.Comment = comment; } else { OrderByClause.Comment = $"{OrderByClause.Comment} {comment}"; } } } /// /// Adds an expression to the HAVING clause. /// /// The expression to add. /// Optional comment to add with the expression. /// The logical operation ("and" or "or"). Defaults to "and". public override void AddHavingExpression(Expression expression, string? comment = null, string operation = "and") { AddHavingExpression(expression, comment, operation, isMicrosoftSql: false); } /// /// Adds an expression to the HAVING clause. /// /// The expression to add. /// Optional comment to add with the expression. /// The logical operation ("and" or "or"). Defaults to "and". /// If true, uses Microsoft T-SQL formatting. If false, uses Snowflake formatting. Defaults to false. public void AddHavingExpression(Expression expression, string? comment, string operation, bool isMicrosoftSql) { if (expression is null) { throw new ArgumentNullException(nameof(expression), ExpressionNullErrorMessage); } var visitor = isMicrosoftSql ? (IVisitor)new SqlServerCommandVisitor() : new CommandVisitor(); AppendToClause(HavingClause, expression.Accept(visitor), operation, comment); } /// /// Gets the complete Snowflake SQL query string with proper formatting. /// /// Whether to include setup and finish clauses. /// The Snowflake SQL query string. #pragma warning disable S3776 // Cognitive Complexity of methods should not be too high public override string GetSql(bool includeSetupFinish = true) #pragma warning restore S3776 { var sb = new StringBuilder(); if (includeSetupFinish) { foreach (string setup in SetupClauses) { sb.AppendLine(setup); } } AppendWithClauseSection(sb, withClauseIndent: " ", queryBodyIndent: " "); // Snowflake SELECT syntax sb.Append(StatementParser.KeywordSelect); // Handle TOP equivalent using LIMIT in Snowflake sb.AppendLine(); if (!string.IsNullOrEmpty(SelectClause.Comment)) { sb.AppendLine($" {SelectClause.Comment}"); } sb.AppendLine($" {SelectClause.Clause}"); if (IsUsingFromClause) { sb.AppendLine(StatementParser.KeywordFrom); if (!string.IsNullOrEmpty(FromClause.Comment)) { sb.AppendLine($" {FromClause.Comment}"); } sb.AppendLine($" {FromClause.Clause}"); } if (IsUsingWhereClause) { sb.AppendLine(StatementParser.KeywordWhere); if (!string.IsNullOrEmpty(WhereClause.Comment)) { sb.AppendLine($" {WhereClause.Comment}"); } sb.AppendLine($" {WhereClause.Clause}"); } if (IsUsingGroupByClause) { sb.AppendLine(StatementParser.KeywordGroupBy); if (!string.IsNullOrEmpty(GroupByClause.Comment)) { sb.AppendLine($" {GroupByClause.Comment}"); } sb.AppendLine($" {GroupByClause.Clause}"); } if (IsUsingHavingClause) { sb.AppendLine(StatementParser.KeywordHaving); if (!string.IsNullOrEmpty(HavingClause.Comment)) { sb.AppendLine($" {HavingClause.Comment}"); } sb.AppendLine($" {HavingClause.Clause}"); } if (IsUsingOrderByClause) { sb.AppendLine(StatementParser.KeywordOrderBy); if (!string.IsNullOrEmpty(OrderByClause.Comment)) { sb.AppendLine($" {OrderByClause.Comment}"); } sb.AppendLine($" {OrderByClause.Clause}"); } if (includeSetupFinish) { foreach (string finish in FinishClauses) { sb.AppendLine(finish); } } return sb.ToString(); } /// /// Creates a deep clone of this Snowflake query breakdown. /// /// A cloned SnowflakeQueryBreakdown instance. public new object Clone() { // Use the base class clone method but return as SnowflakeQueryBreakdown var baseClone = (SqlServerQueryBreakdown)base.Clone(); var clone = new QueryBreakdown { SelectClause = baseClone.SelectClause, FromClause = baseClone.FromClause, WhereClause = baseClone.WhereClause, GroupByClause = baseClone.GroupByClause, HavingClause = baseClone.HavingClause, OrderByClause = baseClone.OrderByClause, SetupClauses = new List(baseClone.SetupClauses), FinishClauses = new ArrayList(baseClone.FinishClauses) }; // Copy parameters foreach (var kvp in baseClone.Parameters) { clone.Parameters[kvp.Key] = kvp.Value; } return clone; } /// /// Adds a Common Table Expression (CTE) to the WITH clause using a raw SQL string. /// Parses the SQL using Snowflake SQL rules. /// /// The table name for the WITH clause. /// The SQL query for the WITH table. /// If true, uses Microsoft T-SQL parsing rules instead of Snowflake rules. Defaults to true. public override void AddWithClause(string withTableName, string withTableSql, bool isMicrosoftSql = true) { if (string.IsNullOrWhiteSpace(withTableName)) { throw new ArgumentNullException(nameof(withTableName), "WITH table name cannot be null or empty."); } if (string.IsNullOrWhiteSpace(withTableSql)) { throw new ArgumentNullException(nameof(withTableSql), "WITH table SQL cannot be null or empty."); } // Parse the SQL string into a SnowflakeQueryBreakdown object using specified parsing rules var parsedQuery = QueryBreakdown.Parse(withTableSql, isMicrosoftSql); // Delegate to the IQueryBreakdown overload AddWithClause(withTableName, parsedQuery); } /// /// Creates a Snowflake-specific statement expression parser. /// /// A Snowflake IStatementExpressionParser instance. protected override IStatementExpressionParser CreateExpressionParser() => new StatementExpressionParser(); /// /// Parses a Snowflake SQL SELECT statement into a SnowflakeQueryBreakdown object. /// Supports both :parameter and @parameter syntax. /// /// The Snowflake SQL SELECT statement to parse. /// If true, uses Microsoft T-SQL parsing rules instead of Snowflake rules. Defaults to false. /// A SnowflakeQueryBreakdown object representing the parsed query. /// Thrown when sql is null or empty. /// Thrown when the SQL statement cannot be parsed. public static QueryBreakdown Parse(string sql, bool isMicrosoftSql = false) { if (string.IsNullOrWhiteSpace(sql)) { throw new ArgumentNullException(nameof(sql), "SQL statement cannot be null or empty."); } if (!TryParse(sql, out var result, out var error, isMicrosoftSql)) { throw new FormatException($"Failed to parse {(isMicrosoftSql ? "T-SQL" : "Snowflake SQL")} statement: {error}"); } return result; } /// /// Attempts to parse a Snowflake SQL SELECT statement into a SnowflakeQueryBreakdown object. /// /// The Snowflake SQL SELECT statement to parse. /// When this method returns, contains the parsed SnowflakeQueryBreakdown if successful, or null if parsing failed. /// If true, uses Microsoft T-SQL parsing rules instead of Snowflake rules. Defaults to false. /// true if the SQL was successfully parsed; otherwise, false. public static bool TryParse(string sql, out QueryBreakdown result, bool isMicrosoftSql = false) => TryParse(sql, out result, out _, isMicrosoftSql); /// /// Attempts to parse a Snowflake SQL SELECT statement into a SnowflakeQueryBreakdown object. /// Handles Snowflake-specific syntax including :parameter and @parameter formats. /// /// The Snowflake SQL SELECT statement to parse. /// When this method returns, contains the parsed SnowflakeQueryBreakdown if successful, or null if parsing failed. /// When this method returns false, contains a message describing why parsing failed. /// If true, uses Microsoft T-SQL parsing rules instead of Snowflake rules. Defaults to false. /// true if the SQL was successfully parsed; otherwise, false. public static bool TryParse(string sql, out QueryBreakdown result, out string errorMessage, bool isMicrosoftSql = false) { result = null!; errorMessage = null!; try { if (string.IsNullOrWhiteSpace(sql)) { errorMessage = "SQL statement cannot be null or empty."; return false; } // If Microsoft SQL mode, delegate to base class if (isMicrosoftSql) { if (!SqlServerQueryBreakdown.TryParse(sql, out var baseResult, out errorMessage)) { return false; } // Convert base QueryBreakdown to SnowflakeQueryBreakdown result = new QueryBreakdown { SelectClause = baseResult.SelectClause, FromClause = baseResult.FromClause, WhereClause = baseResult.WhereClause, GroupByClause = baseResult.GroupByClause, HavingClause = baseResult.HavingClause, OrderByClause = baseResult.OrderByClause, SetupClauses = baseResult.SetupClauses, FinishClauses = baseResult.FinishClauses }; // Preserve WITH clause using protected helper result.SetWithClauseValue(baseResult.GetWithClauseValue()); // Copy parameters foreach (var param in baseResult.Parameters) { result.Parameters[param.Key] = param.Value; } return true; } // Normalize the SQL: remove extra whitespace, handle line breaks, preserve comments sql = SnowflakeParserInstance.NormalizeSqlPreservingComments(sql); // Extract setup clauses (everything before the main SELECT) var setupClauses = new List(); sql = SnowflakeParserInstance.ExtractSetupClauses(sql, setupClauses); // Extract finish clauses (cleanup statements after the main query) var finishClauses = new ArrayList(); sql = SnowflakeParserInstance.ExtractFinishClauses(sql, finishClauses); // Parse WITH clause separately if present string? withClause = null; if (SnowflakeParserInstance.TryParseWithClause(sql, out withClause, out var mainQuery)) { sql = mainQuery; // Continue parsing with the main query } // Parse the main SELECT statement if (!SnowflakeParserInstance.TryParseSelectStatement(sql, out var clauses, out errorMessage)) { return false; } // Create the SnowflakeQueryBreakdown object result = new QueryBreakdown { SelectClause = clauses!.SelectClause ?? new SqlExpressionClause(splitOnComma: true), FromClause = clauses.FromClause ?? new SqlClause(), WhereClause = clauses.WhereClause ?? new SqlExpressionClause(splitOnComma: false), GroupByClause = clauses.GroupByClause ?? new SqlExpressionClause(splitOnComma: true), HavingClause = clauses.HavingClause ?? new SqlExpressionClause(splitOnComma: false), OrderByClause = clauses.OrderByClause ?? new SqlExpressionClause(splitOnComma: true), SetupClauses = setupClauses, FinishClauses = finishClauses }; // Preserve WITH clause result.SetWithClauseValue(withClause?.Trim()); // Extract parameters from all clauses (use comment-free version for this) var sqlWithoutComments = SnowflakeParserInstance.RemoveSqlComments(sql); SnowflakeParserInstance.ExtractParameters(result.Parameters, sqlWithoutComments); // Normalize parameters to include both @ and : formats for compatibility result.NormalizeParameterFormats(); return true; } catch (Exception ex) { errorMessage = $"Unexpected error during Snowflake SQL parsing: {ex.Message}"; return false; } } /// /// Gets a LINQ to SQL query of the specified type based on this breakdown. /// /// The entity type for the query. /// An empty queryable by default, as QueryBreakdown operates on SQL. Override in derived classes to provide LINQ query reconstruction. /// /// This Snowflake-specific implementation returns an empty queryable since Snowflake QueryBreakdown /// represents parsed SQL statements and has no built-in way to create LINQ queries. /// Derived classes can override this method to reconstruct LINQ queries from the analyzed components. /// public override IQueryable GetQuery() where T : class => Enumerable.Empty().AsQueryable(); }