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();
}