chore: initial git load of code space

This commit is contained in:
Thom Lamb
2026-05-12 08:52:33 -05:00
parent 9abada692f
commit 5e467bcc9c
384 changed files with 65960 additions and 2 deletions
@@ -0,0 +1,191 @@
using System.Collections;
using System.Text;
using SqlServerDeleteBreakdown = Strata.SqlTools.Breakdowns.SqlServer.DeleteBreakdown;
using StatementParser = Strata.SqlTools.Statements.Snowflake.StatementParser;
namespace Strata.SqlTools.Breakdowns.Snowflake;
/// <summary>
/// Represents a DELETE SQL statement breakdown with FROM and WHERE clauses for Snowflake.
/// </summary>
[Serializable]
public class DeleteBreakdown : SqlServerDeleteBreakdown
{
private static readonly StatementParser SnowflakeParserInstance = new StatementParser();
/// <summary>
/// Initializes a new instance of the <see cref="DeleteBreakdown"/> class.
/// </summary>
public DeleteBreakdown() : base()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="DeleteBreakdown"/> class.
/// </summary>
/// <param name="fromClause">The FROM clause.</param>
/// <param name="whereClause">The WHERE clause.</param>
/// <param name="isMicrosoftSql">If true, uses Microsoft T-SQL parsing rules. If false, uses Snowflake parsing rules. Defaults to false.</param>
public DeleteBreakdown(string fromClause, string whereClause, bool isMicrosoftSql = false)
: base()
{
var parser = isMicrosoftSql ? Parser : SnowflakeParserInstance;
var cleanFrom = parser.ExtractSqlComments(fromClause, out var fromComments);
FromClause.Clause = cleanFrom.Trim();
FromClause.Comment = fromComments.Count > 0 ? string.Join(" ", fromComments) : null;
var cleanWhere = parser.ExtractSqlComments(whereClause, out var whereComments);
WhereClause.Clause = cleanWhere.Trim();
WhereClause.Comment = whereComments.Count > 0 ? string.Join(" ", whereComments) : null;
}
/// <summary>
/// Gets the SQL breakdown as a string for Snowflake.
/// </summary>
/// <returns>The DELETE SQL statement.</returns>
protected override string GetSqlBreakdown()
{
var sb = new StringBuilder();
// Snowflake DELETE syntax is simpler - no DELETE clause with alias
sb.AppendLine("DELETE FROM ");
sb.AppendLine($" {FromClause.Clause}");
if (IsUsingWhereClause)
{
sb.AppendLine("WHERE ");
sb.AppendLine($" {WhereClause.Clause}");
}
return sb.ToString();
}
#region Parse Methods
/// <summary>
/// Parses a Snowflake DELETE SQL statement into a DeleteBreakdown object.
/// </summary>
/// <param name="sql">The DELETE SQL statement to parse.</param>
/// <param name="isMicrosoftSql">If true, uses Microsoft T-SQL parsing rules. If false, uses Snowflake parsing rules. Defaults to false.</param>
/// <returns>A DeleteBreakdown object representing the parsed statement.</returns>
/// <exception cref="ArgumentNullException">Thrown when sql is null or empty.</exception>
/// <exception cref="FormatException">Thrown when the SQL statement cannot be parsed.</exception>
public static DeleteBreakdown 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")} DELETE statement: {error}");
}
return result;
}
/// <summary>
/// Attempts to parse a Snowflake DELETE SQL statement into a DeleteBreakdown object.
/// </summary>
/// <param name="sql">The DELETE SQL statement to parse.</param>
/// <param name="result">When this method returns, contains the parsed DeleteBreakdown if successful, or null if parsing failed.</param>
/// <param name="isMicrosoftSql">If true, uses Microsoft T-SQL parsing rules instead of Snowflake rules. Defaults to false.</param>
/// <returns>true if the SQL was successfully parsed; otherwise, false.</returns>
public static bool TryParse(string sql, out DeleteBreakdown result, bool isMicrosoftSql = false)
=> TryParse(sql, out result, out _, isMicrosoftSql);
/// <summary>
/// Attempts to parse a Snowflake DELETE SQL statement into a DeleteBreakdown object.
/// Handles Snowflake-specific syntax.
/// </summary>
/// <param name="sql">The DELETE SQL statement to parse.</param>
/// <param name="result">When this method returns, contains the parsed DeleteBreakdown if successful, or null if parsing failed.</param>
/// <param name="errorMessage">When this method returns false, contains a message describing why parsing failed.</param>
/// <param name="isMicrosoftSql">If true, uses Microsoft T-SQL parsing rules instead of Snowflake rules. Defaults to false.</param>
/// <returns>true if the SQL was successfully parsed; otherwise, false.</returns>
public static bool TryParse(string sql, out DeleteBreakdown 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 (!SqlServerDeleteBreakdown.TryParse(sql, out var baseResult, out errorMessage))
{
return false;
}
// Convert to Snowflake DeleteBreakdown
result = new DeleteBreakdown
{
FromClause = baseResult.FromClause,
WhereClause = baseResult.WhereClause,
DeleteClause = baseResult.DeleteClause,
SetupClauses = baseResult.SetupClauses,
FinishClauses = baseResult.FinishClauses
};
return true;
}
var parser = SnowflakeParserInstance;
sql = parser.NormalizeSqlPreservingComments(sql);
// Check if it's a DELETE statement
var sqlTrimmed = sql.TrimStart();
if (!System.Text.RegularExpressions.Regex.IsMatch(sqlTrimmed, @"^\s*DELETE\b",
System.Text.RegularExpressions.RegexOptions.IgnoreCase))
{
errorMessage = "SQL statement must start with DELETE.";
return false;
}
// Extract setup and finish clauses
var setupClauses = new List<string>();
sql = parser.ExtractSetupClauses(sql, setupClauses);
var finishClauses = new ArrayList();
sql = parser.ExtractFinishClauses(sql, finishClauses);
// Snowflake uses simpler DELETE syntax: DELETE FROM table WHERE condition
var deleteMatch = System.Text.RegularExpressions.Regex.Match(sql,
@"DELETE\s+FROM\s+(.*?)(?:\s+WHERE\s+(.*))?$",
System.Text.RegularExpressions.RegexOptions.IgnoreCase | System.Text.RegularExpressions.RegexOptions.Singleline);
if (!deleteMatch.Success)
{
errorMessage = "Could not parse DELETE statement. Expected format: DELETE FROM table [WHERE condition]";
return false;
}
var fromClause = deleteMatch.Groups[1].Value.Trim();
var whereClause = deleteMatch.Groups.Count > 2 ? deleteMatch.Groups[2].Value.Trim() : string.Empty;
result = new DeleteBreakdown(fromClause, whereClause, isMicrosoftSql: false)
{
SetupClauses = setupClauses,
FinishClauses = finishClauses
};
return true;
}
catch (Exception ex)
{
errorMessage = $"Unexpected error during parsing: {ex.Message}";
return false;
}
}
#endregion
}
@@ -0,0 +1,197 @@
using System.Collections;
using Strata.SqlTools.SqlBreakdown.Utilities;
using SqlServerInsertBreakdown = Strata.SqlTools.Breakdowns.SqlServer.InsertBreakdown;
using StatementParser = Strata.SqlTools.Statements.Snowflake.StatementParser;
namespace Strata.SqlTools.Breakdowns.Snowflake;
/// <summary>
/// Represents an INSERT SQL statement breakdown with column and value clauses for Snowflake.
/// </summary>
[Serializable]
public class InsertBreakdown : SqlServerInsertBreakdown
{
private static readonly StatementParser SnowflakeParserInstance = new StatementParser();
/// <summary>
/// Initializes a new instance of the <see cref="InsertBreakdown"/> class.
/// </summary>
public InsertBreakdown() : base()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="InsertBreakdown"/> class.
/// </summary>
/// <param name="tableName">The table name.</param>
/// <param name="insertIntoClause">The column list for the INSERT.</param>
/// <param name="valuesClause">The values clause.</param>
/// <param name="isMicrosoftSql">If true, uses Microsoft T-SQL parsing rules. If false, uses Snowflake parsing rules. Defaults to false.</param>
public InsertBreakdown(string tableName, string insertIntoClause, string valuesClause, bool isMicrosoftSql = false)
: base()
{
var parser = isMicrosoftSql ? Parser : SnowflakeParserInstance;
var cleanTable = parser.ExtractSqlComments(tableName, out var tableComments);
TableName.Clause = cleanTable.Trim();
TableName.Comment = tableComments.Count > 0 ? string.Join(" ", tableComments) : null;
var cleanInsert = parser.ExtractSqlComments(insertIntoClause, out var insertComments);
InsertIntoClause.Clause = cleanInsert.Trim();
InsertIntoClause.Comment = insertComments.Count > 0 ? string.Join(" ", insertComments) : null;
var cleanValues = parser.ExtractSqlComments(valuesClause, out var valuesComments);
ValuesClause.Clause = cleanValues.Trim();
ValuesClause.Comment = valuesComments.Count > 0 ? string.Join(" ", valuesComments) : null;
}
/// <summary>
/// Initializes a new instance of the <see cref="InsertBreakdown"/> class from a list of column names.
/// </summary>
/// <param name="tableName">The table name.</param>
/// <param name="columnNames">The list of column names.</param>
public InsertBreakdown(string tableName, List<string> columnNames)
: base()
{
TableName.Clause = tableName;
InsertIntoClause.Clause = SqlUtils.GetSqlSafeColumnList(columnNames);
// Generate parameter names for values (Snowflake uses :parameter syntax)
var valuesList = new List<string>();
foreach (string item in columnNames)
{
valuesList.Add($":{item}");
}
ValuesClause.Clause = string.Join(",", valuesList);
}
#region Parse Methods
/// <summary>
/// Parses a Snowflake INSERT SQL statement into an InsertBreakdown object.
/// </summary>
/// <param name="sql">The INSERT SQL statement to parse.</param>
/// <param name="isMicrosoftSql">If true, uses Microsoft T-SQL parsing rules. If false, uses Snowflake parsing rules. Defaults to false.</param>
/// <returns>An InsertBreakdown object representing the parsed statement.</returns>
/// <exception cref="ArgumentNullException">Thrown when sql is null or empty.</exception>
/// <exception cref="FormatException">Thrown when the SQL statement cannot be parsed.</exception>
public static InsertBreakdown 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")} INSERT statement: {error}");
}
return result;
}
/// <summary>
/// Attempts to parse a Snowflake INSERT SQL statement into an InsertBreakdown object.
/// </summary>
/// <param name="sql">The INSERT SQL statement to parse.</param>
/// <param name="result">When this method returns, contains the parsed InsertBreakdown if successful, or null if parsing failed.</param>
/// <param name="isMicrosoftSql">If true, uses Microsoft T-SQL parsing rules instead of Snowflake rules. Defaults to false.</param>
/// <returns>true if the SQL was successfully parsed; otherwise, false.</returns>
public static bool TryParse(string sql, out InsertBreakdown result, bool isMicrosoftSql = false)
=> TryParse(sql, out result, out _, isMicrosoftSql);
/// <summary>
/// Attempts to parse a Snowflake INSERT SQL statement into an InsertBreakdown object.
/// Handles Snowflake-specific syntax.
/// </summary>
/// <param name="sql">The INSERT SQL statement to parse.</param>
/// <param name="result">When this method returns, contains the parsed InsertBreakdown if successful, or null if parsing failed.</param>
/// <param name="errorMessage">When this method returns false, contains a message describing why parsing failed.</param>
/// <param name="isMicrosoftSql">If true, uses Microsoft T-SQL parsing rules instead of Snowflake rules. Defaults to false.</param>
/// <returns>true if the SQL was successfully parsed; otherwise, false.</returns>
public static bool TryParse(string sql, out InsertBreakdown 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 (!SqlServerInsertBreakdown.TryParse(sql, out var baseResult, out errorMessage))
{
return false;
}
// Convert to Snowflake InsertBreakdown
result = new InsertBreakdown
{
TableName = baseResult.TableName,
InsertIntoClause = baseResult.InsertIntoClause,
ValuesClause = baseResult.ValuesClause,
SetupClauses = baseResult.SetupClauses,
FinishClauses = baseResult.FinishClauses
};
return true;
}
var parser = SnowflakeParserInstance;
sql = parser.NormalizeSqlPreservingComments(sql);
// Check if it's an INSERT statement
var sqlTrimmed = sql.TrimStart();
if (!System.Text.RegularExpressions.Regex.IsMatch(sqlTrimmed, @"^\s*INSERT\s+INTO\b",
System.Text.RegularExpressions.RegexOptions.IgnoreCase))
{
errorMessage = "SQL statement must start with INSERT INTO.";
return false;
}
// Extract setup and finish clauses
var setupClauses = new List<string>();
sql = parser.ExtractSetupClauses(sql, setupClauses);
var finishClauses = new ArrayList();
sql = parser.ExtractFinishClauses(sql, finishClauses);
// Parse INSERT statement using regex
var insertMatch = System.Text.RegularExpressions.Regex.Match(sql,
@"INSERT\s+INTO\s+([^\(\s]+)\s*\(([^\)]*)\)\s*VALUES\s*\(([^\)]*)\)",
System.Text.RegularExpressions.RegexOptions.IgnoreCase | System.Text.RegularExpressions.RegexOptions.Singleline);
if (!insertMatch.Success)
{
errorMessage = "Could not parse INSERT statement. Expected format: INSERT INTO table (columns) VALUES (values)";
return false;
}
var tableName = insertMatch.Groups[1].Value.Trim();
var columnsClause = insertMatch.Groups[2].Value.Trim();
var valuesClause = insertMatch.Groups[3].Value.Trim();
result = new InsertBreakdown(tableName, columnsClause, valuesClause, isMicrosoftSql: false)
{
SetupClauses = setupClauses,
FinishClauses = finishClauses
};
return true;
}
catch (Exception ex)
{
errorMessage = $"Unexpected error during parsing: {ex.Message}";
return false;
}
}
#endregion
}
@@ -0,0 +1,232 @@
using System.Collections;
using System.Text;
using SqlServerProcedureBreakdown = Strata.SqlTools.Breakdowns.SqlServer.ProcedureBreakdown;
using StatementParser = Strata.SqlTools.Statements.Snowflake.StatementParser;
namespace Strata.SqlTools.Breakdowns.Snowflake;
/// <summary>
/// Represents a Snowflake stored procedure call breakdown with procedure name and parameters.
/// </summary>
[Serializable]
public class ProcedureBreakdown : SqlServerProcedureBreakdown
{
private static readonly StatementParser SnowflakeParserInstance = new StatementParser();
/// <summary>
/// Initializes a new instance of the <see cref="ProcedureBreakdown"/> class.
/// </summary>
public ProcedureBreakdown() : base()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ProcedureBreakdown"/> class.
/// </summary>
/// <param name="procedureName">The stored procedure name.</param>
/// <param name="isMicrosoftSql">If true, uses Microsoft T-SQL parsing rules. If false, uses Snowflake parsing rules. Defaults to false.</param>
public ProcedureBreakdown(string procedureName, bool isMicrosoftSql = false) : base()
{
var parser = isMicrosoftSql ? Parser : SnowflakeParserInstance;
var cleanName = parser.ExtractSqlComments(procedureName, out var nameComments);
ProcedureName.Clause = cleanName.Trim();
ProcedureName.Comment = nameComments.Count > 0 ? string.Join(" ", nameComments) : null;
}
/// <summary>
/// Initializes a new instance of the <see cref="ProcedureBreakdown"/> class.
/// </summary>
/// <param name="procedureName">The stored procedure name.</param>
/// <param name="parameters">The parameters dictionary (parameter name -> value expression).</param>
/// <param name="isMicrosoftSql">If true, uses Microsoft T-SQL parsing rules. If false, uses Snowflake parsing rules. Defaults to false.</param>
public ProcedureBreakdown(string procedureName, Dictionary<string, string> parameters, bool isMicrosoftSql = false)
: this(procedureName, isMicrosoftSql)
{
Parameters = parameters ?? new Dictionary<string, string>();
}
/// <summary>
/// Gets the SQL breakdown as a string for Snowflake.
/// </summary>
/// <returns>The CALL SQL statement (Snowflake uses CALL instead of EXEC).</returns>
protected override string GetSqlBreakdown()
{
var sb = new StringBuilder();
sb.Append("CALL ");
sb.Append(ProcedureName.Clause);
sb.Append("(");
if (IsUsingParameters)
{
var paramList = new List<string>();
foreach (var param in Parameters)
{
// Snowflake uses positional or named parameters with => syntax
paramList.Add($"{param.Key.TrimStart('@')} => {param.Value}");
}
sb.Append(string.Join(", ", paramList));
}
sb.Append(")");
return sb.ToString();
}
#region Parse Methods
/// <summary>
/// Parses a Snowflake CALL SQL statement into a ProcedureBreakdown object.
/// </summary>
/// <param name="sql">The CALL SQL statement to parse.</param>
/// <param name="isMicrosoftSql">If true, uses Microsoft T-SQL parsing rules. If false, uses Snowflake parsing rules. Defaults to false.</param>
/// <returns>A ProcedureBreakdown object representing the parsed statement.</returns>
/// <exception cref="ArgumentNullException">Thrown when sql is null or empty.</exception>
/// <exception cref="FormatException">Thrown when the SQL statement cannot be parsed.</exception>
public static ProcedureBreakdown 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 ? "EXEC" : "CALL")} statement: {error}");
}
return result;
}
/// <summary>
/// Attempts to parse a Snowflake CALL SQL statement into a ProcedureBreakdown object.
/// </summary>
/// <param name="sql">The CALL SQL statement to parse.</param>
/// <param name="result">When this method returns, contains the parsed ProcedureBreakdown if successful, or null if parsing failed.</param>
/// <param name="isMicrosoftSql">If true, uses Microsoft T-SQL parsing rules instead of Snowflake rules. Defaults to false.</param>
/// <returns>true if the SQL was successfully parsed; otherwise, false.</returns>
public static bool TryParse(string sql, out ProcedureBreakdown result, bool isMicrosoftSql = false)
=> TryParse(sql, out result, out _, isMicrosoftSql);
/// <summary>
/// Attempts to parse a Snowflake CALL SQL statement into a ProcedureBreakdown object.
/// Handles Snowflake-specific syntax including CALL procedureName(param &gt;= value).
/// </summary>
/// <param name="sql">The CALL SQL statement to parse.</param>
/// <param name="result">When this method returns, contains the parsed ProcedureBreakdown if successful, or null if parsing failed.</param>
/// <param name="errorMessage">When this method returns false, contains a message describing why parsing failed.</param>
/// <param name="isMicrosoftSql">If true, uses Microsoft T-SQL parsing rules instead of Snowflake rules. Defaults to false.</param>
/// <returns>true if the SQL was successfully parsed; otherwise, false.</returns>
public static bool TryParse(string sql, out ProcedureBreakdown 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 (!SqlServerProcedureBreakdown.TryParse(sql, out var baseResult, out errorMessage))
{
return false;
}
// Convert to Snowflake ProcedureBreakdown
result = new ProcedureBreakdown
{
ProcedureName = baseResult.ProcedureName,
Parameters = baseResult.Parameters,
SetupClauses = baseResult.SetupClauses,
FinishClauses = baseResult.FinishClauses
};
return true;
}
var parser = SnowflakeParserInstance;
sql = parser.NormalizeSqlPreservingComments(sql);
// Check if it's a CALL statement (Snowflake syntax) or EXEC (for compatibility)
var sqlTrimmed = sql.TrimStart();
if (!System.Text.RegularExpressions.Regex.IsMatch(sqlTrimmed, @"^\s*(CALL|EXEC|EXECUTE)\b",
System.Text.RegularExpressions.RegexOptions.IgnoreCase))
{
errorMessage = "SQL statement must start with CALL, EXEC, or EXECUTE.";
return false;
}
// Extract setup and finish clauses
var setupClauses = new List<string>();
sql = parser.ExtractSetupClauses(sql, setupClauses);
var finishClauses = new ArrayList();
sql = parser.ExtractFinishClauses(sql, finishClauses);
// Parse CALL statement - match procedure name and parameters
// Pattern: CALL procedureName(param => value, ...)
var callMatch = System.Text.RegularExpressions.Regex.Match(sql,
@"(?:CALL|EXEC|EXECUTE)\s+([^\s\(]+)(?:\s*\((.*?)\))?$",
System.Text.RegularExpressions.RegexOptions.IgnoreCase | System.Text.RegularExpressions.RegexOptions.Singleline);
if (!callMatch.Success)
{
errorMessage = "Could not parse CALL statement. Expected format: CALL procedureName(param => value, ...)";
return false;
}
var procedureName = callMatch.Groups[1].Value.Trim();
var parametersText = callMatch.Groups.Count > 2 ? callMatch.Groups[2].Value.Trim() : string.Empty;
var parameters = new Dictionary<string, string>();
if (!string.IsNullOrWhiteSpace(parametersText))
{
// Parse parameters - Snowflake uses param => value syntax
var paramMatches = System.Text.RegularExpressions.Regex.Matches(parametersText,
@"(\w+)\s*=>\s*([^,]+)(?:,|$)",
System.Text.RegularExpressions.RegexOptions.IgnoreCase);
foreach (System.Text.RegularExpressions.Match paramMatch in paramMatches)
{
var paramName = paramMatch.Groups[1].Value.Trim();
var paramValue = paramMatch.Groups[2].Value.Trim();
// Store with @ prefix for consistency with SQL Server
parameters["@" + paramName] = paramValue;
}
// If no named parameters found, try positional parameters (just values)
if (parameters.Count == 0 && !string.IsNullOrWhiteSpace(parametersText))
{
var positionalParams = parametersText.Split(',');
for (int i = 0; i < positionalParams.Length; i++)
{
parameters[$"@param{i + 1}"] = positionalParams[i].Trim();
}
}
}
result = new ProcedureBreakdown(procedureName, parameters, isMicrosoftSql: false)
{
SetupClauses = setupClauses,
FinishClauses = finishClauses
};
return true;
}
catch (Exception ex)
{
errorMessage = $"Unexpected error during parsing: {ex.Message}";
return false;
}
}
#endregion
}
@@ -0,0 +1,881 @@
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;
/// <summary>
/// Represents a Snowflake SQL query breakdown with all clauses, following Snowflake SQL standards.
/// Handles both :parameter and @parameter syntax for Snowflake compatibility.
/// </summary>
[Serializable]
public class QueryBreakdown : SqlServerQueryBreakdown
{
private const string ExpressionNullErrorMessage = "Expression cannot be null.";
private static readonly StatementParser SnowflakeParserInstance = new StatementParser();
/// <summary>
/// Initializes a new instance of the <see cref="QueryBreakdown"/> class.
/// </summary>
public QueryBreakdown() : base()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="QueryBreakdown"/> class with SELECT and FROM clauses.
/// </summary>
/// <param name="selectClause">The SELECT clause.</param>
/// <param name="fromClause">The FROM clause.</param>
/// <param name="isMicrosoftSql">If true, uses Microsoft T-SQL parsing rules. If false, uses Snowflake parsing rules. Defaults to false.</param>
public QueryBreakdown(string selectClause, string fromClause, bool isMicrosoftSql = false) : base()
{
var parser = isMicrosoftSql ? Parser : SnowflakeParserInstance;
var cleanSelect = parser.ExtractSqlComments(selectClause, out var selectComments);
SelectClause.Clause = cleanSelect.Trim();
SelectClause.Comment = selectComments.Count > 0 ? string.Join(" ", selectComments) : null;
var cleanFrom = parser.ExtractSqlComments(fromClause, out var fromComments);
FromClause.Clause = cleanFrom.Trim();
FromClause.Comment = fromComments.Count > 0 ? string.Join(" ", fromComments) : null;
}
/// <summary>
/// Initializes a new instance of the <see cref="QueryBreakdown"/> class with SELECT, FROM, and WHERE clauses.
/// </summary>
/// <param name="selectClause">The SELECT clause.</param>
/// <param name="fromClause">The FROM clause.</param>
/// <param name="whereClause">The WHERE clause.</param>
/// <param name="isMicrosoftSql">If true, uses Microsoft T-SQL parsing rules. If false, uses Snowflake parsing rules. Defaults to false.</param>
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;
}
/// <summary>
/// Initializes a new instance of the <see cref="QueryBreakdown"/> class with SELECT, FROM, WHERE, and ORDER BY clauses.
/// </summary>
/// <param name="selectClause">The SELECT clause.</param>
/// <param name="fromClause">The FROM clause.</param>
/// <param name="whereClause">The WHERE clause.</param>
/// <param name="orderByClause">The ORDER BY clause.</param>
/// <param name="isMicrosoftSql">If true, uses Microsoft T-SQL parsing rules. If false, uses Snowflake parsing rules. Defaults to false.</param>
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;
}
/// <summary>
/// Adds a parameter to the query using Snowflake's :param format.
/// Also adds @param format for compatibility.
/// </summary>
/// <param name="parameterName">The parameter name (with or without : or @).</param>
/// <param name="value">The parameter value.</param>
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
if (Parameters.ContainsKey($"@{colonName.TrimStart(':', '@')}"))
{
Parameters.Remove($"@{colonName.TrimStart(':', '@')}");
}
Parameters[colonName] = value;
Parameters[atName] = value;
}
/// <summary>
/// Sets the value of a parameter using Snowflake's :param format.
/// Also updates @param format for compatibility.
/// </summary>
/// <param name="parameterName">The parameter name (with or without : or @).</param>
/// <param name="value">The parameter value.</param>
public new void SetParameterValue(string parameterName, object value)
{
var colonName = NormalizeParameterName(parameterName);
var atName = "@" + colonName.TrimStart(':', '@');
Parameters[colonName] = value;
Parameters[atName] = value;
}
/// <summary>
/// Normalizes parameter name to Snowflake format (:param).
/// </summary>
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 ":" + parameterName.Substring(1);
}
// Add : prefix
return ":" + parameterName;
}
/// <summary>
/// Ensures parameters exist in both @ and : formats for compatibility.
/// </summary>
private void NormalizeParameterFormats()
{
var paramKeys = Parameters.Keys.ToList();
foreach (var paramName in paramKeys)
{
if (paramName.StartsWith(':'))
{
// Add @param version
var atParam = "@" + paramName.Substring(1);
if (!Parameters.ContainsKey(atParam))
{
Parameters[atParam] = Parameters[paramName];
}
}
else if (paramName.StartsWith('@'))
{
// Add :param version
var colonParam = ":" + paramName.Substring(1);
if (!Parameters.ContainsKey(colonParam))
{
Parameters[colonParam] = Parameters[paramName];
}
}
}
}
/// <summary>
/// Adds an expression to the SELECT clause.
/// </summary>
/// <param name="expression">The expression to add.</param>
/// <param name="comment">Optional comment to add with the expression.</param>
/// <param name="isMicrosoftSql">If true, uses Microsoft T-SQL formatting. If false, uses Snowflake formatting. Defaults to false.</param>
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<string>)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}";
}
}
}
/// <summary>
/// Adds an expression to the WHERE clause.
/// </summary>
/// <param name="expression">The expression to add.</param>
/// <param name="comment">Optional comment to add with the expression.</param>
/// <param name="operation">The logical operation ("and" or "or"). Defaults to "and".</param>
/// <param name="isMicrosoftSql">If true, uses Microsoft T-SQL formatting. If false, uses Snowflake formatting. Defaults to false.</param>
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<string>)new SqlServerCommandVisitor()
: new CommandVisitor();
var sql = expression.Accept(visitor);
if (string.IsNullOrWhiteSpace(WhereClause.Clause))
{
WhereClause.Clause = sql;
}
else
{
WhereClause.Clause = $"{WhereClause.Clause} {operation} {sql}";
}
if (!string.IsNullOrWhiteSpace(comment))
{
if (string.IsNullOrWhiteSpace(WhereClause.Comment))
{
WhereClause.Comment = comment;
}
else
{
WhereClause.Comment = $"{WhereClause.Comment} {comment}";
}
}
}
/// <summary>
/// Adds a WHERE clause condition. Defaults to "and" operation.
/// </summary>
/// <param name="sql">The SQL condition to add.</param>
/// <param name="isMicrosoftSql">If true, uses Microsoft T-SQL parsing rules. If false, uses Snowflake parsing rules. Defaults to false.</param>
public void AddWhereClause(string sql, bool isMicrosoftSql = false)
{
AddWhereClause(sql, "and", isMicrosoftSql);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="sql">The SQL condition to add.</param>
/// <param name="operation">The logical operation ("and" or "or").</param>
public override void AddWhereClause(string sql, string operation)
{
AddWhereClause(sql, operation, isMicrosoftSql: false);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="sql">The SQL condition to add.</param>
/// <param name="operation">The logical operation ("and" or "or").</param>
/// <param name="isMicrosoftSql">If true, uses Microsoft T-SQL parsing rules. If false, uses Snowflake parsing rules. Defaults to false.</param>
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);
if (string.IsNullOrWhiteSpace(WhereClause.Clause))
{
WhereClause.Clause = cleanSql.Trim();
}
else
{
WhereClause.Clause = $"{WhereClause.Clause} {operation} {cleanSql.Trim()}";
}
// Merge comments
if (comments.Count > 0)
{
var newComment = string.Join(" ", comments);
if (string.IsNullOrWhiteSpace(WhereClause.Comment))
{
WhereClause.Comment = newComment;
}
else
{
WhereClause.Comment = $"{WhereClause.Comment} {newComment}";
}
}
// Extract and add parameters from the WHERE clause using appropriate parser
ExtractAndAddParametersWithParser(cleanSql, parser);
}
/// <summary>
/// Extracts parameters from a SQL clause and adds them to the Parameters dictionary using the specified parser.
/// </summary>
/// <param name="sql">The SQL clause to extract parameters from.</param>
/// <param name="parser">The parser to use for extracting parameters.</param>
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<string, object>();
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);
}
}
/// <summary>
/// Adds an expression to the GROUP BY clause.
/// </summary>
/// <param name="expression">The expression to add.</param>
/// <param name="comment">Optional comment to add with the expression.</param>
public override void AddGroupByExpression(Expression expression, string? comment = null)
{
AddGroupByExpression(expression, comment, isMicrosoftSql: false);
}
/// <summary>
/// Adds an expression to the GROUP BY clause.
/// </summary>
/// <param name="expression">The expression to add.</param>
/// <param name="comment">Optional comment to add with the expression.</param>
/// <param name="isMicrosoftSql">If true, uses Microsoft T-SQL formatting. If false, uses Snowflake formatting. Defaults to false.</param>
public void AddGroupByExpression(Expression expression, string? comment, bool isMicrosoftSql)
{
if (expression is null)
{
throw new ArgumentNullException(nameof(expression), ExpressionNullErrorMessage);
}
var visitor = isMicrosoftSql
? (IVisitor<string>)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}";
}
}
}
/// <summary>
/// Adds an expression to the ORDER BY clause.
/// </summary>
/// <param name="expression">The expression to add.</param>
/// <param name="comment">Optional comment to add with the expression.</param>
public override void AddOrderByExpression(Expression expression, string? comment = null)
{
AddOrderByExpression(expression, comment, isMicrosoftSql: false);
}
/// <summary>
/// Adds an expression to the ORDER BY clause.
/// </summary>
/// <param name="expression">The expression to add.</param>
/// <param name="comment">Optional comment to add with the expression.</param>
/// <param name="isMicrosoftSql">If true, uses Microsoft T-SQL formatting. If false, uses Snowflake formatting. Defaults to false.</param>
public void AddOrderByExpression(Expression expression, string? comment, bool isMicrosoftSql)
{
if (expression is null)
{
throw new ArgumentNullException(nameof(expression), ExpressionNullErrorMessage);
}
var visitor = isMicrosoftSql
? (IVisitor<string>)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}";
}
}
}
/// <summary>
/// Adds an expression to the HAVING clause.
/// </summary>
/// <param name="expression">The expression to add.</param>
/// <param name="comment">Optional comment to add with the expression.</param>
/// <param name="operation">The logical operation ("and" or "or"). Defaults to "and".</param>
public override void AddHavingExpression(Expression expression, string? comment = null, string operation = "and")
{
AddHavingExpression(expression, comment, operation, isMicrosoftSql: false);
}
/// <summary>
/// Adds an expression to the HAVING clause.
/// </summary>
/// <param name="expression">The expression to add.</param>
/// <param name="comment">Optional comment to add with the expression.</param>
/// <param name="operation">The logical operation ("and" or "or"). Defaults to "and".</param>
/// <param name="isMicrosoftSql">If true, uses Microsoft T-SQL formatting. If false, uses Snowflake formatting. Defaults to false.</param>
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<string>)new SqlServerCommandVisitor()
: new CommandVisitor();
var sql = expression.Accept(visitor);
if (string.IsNullOrWhiteSpace(HavingClause.Clause))
{
HavingClause.Clause = sql;
}
else
{
HavingClause.Clause = $"{HavingClause.Clause} {operation} {sql}";
}
if (!string.IsNullOrWhiteSpace(comment))
{
if (string.IsNullOrWhiteSpace(HavingClause.Comment))
{
HavingClause.Comment = comment;
}
else
{
HavingClause.Comment = $"{HavingClause.Comment} {comment}";
}
}
}
/// <summary>
/// Gets the complete Snowflake SQL query string with proper formatting.
/// </summary>
/// <param name="includeSetupFinish">Whether to include setup and finish clauses.</param>
/// <returns>The Snowflake SQL query string.</returns>
#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);
}
}
if (IsUsingWithClause)
{
// Check if any WITH clause is recursive
bool hasRecursive = WithClauses.Any(wc => wc.IsRecursive);
sb.Append("WITH");
if (hasRecursive)
{
sb.Append(" RECURSIVE");
}
sb.AppendLine();
for (int i = 0; i < WithClauses.Count; i++)
{
var withClause = WithClauses[i];
if (i > 0)
{
sb.Append(",");
sb.AppendLine();
}
// Include comment if present
if (!string.IsNullOrWhiteSpace(withClause.Comment))
{
sb.AppendLine($" {withClause.Comment}");
}
// Write CTE name with optional column list
var cteName = withClause.TableName;
if (withClause.ColumnList != null && withClause.ColumnList.Count > 0)
{
var columnList = string.Join(", ", withClause.ColumnList);
cteName = $"{withClause.TableName} ({columnList})";
}
sb.AppendLine($" {cteName} AS (");
if (withClause.IsRecursive && withClause.RecursiveQuery != null)
{
// For recursive CTEs: anchor query UNION ALL recursive query
var anchorSql = withClause.Query?.GetSql(includeSetupFinish: false).Trim() ?? string.Empty;
var recursiveSql = withClause.RecursiveQuery.GetSql(includeSetupFinish: false).Trim() ?? string.Empty;
sb.AppendLine($" {anchorSql}");
sb.AppendLine(" UNION ALL");
sb.AppendLine($" {recursiveSql}");
}
else
{
// For non-recursive CTEs: just the single query
var withSql = withClause.Query?.GetSql(includeSetupFinish: false).Trim() ?? string.Empty;
sb.AppendLine($" {withSql}");
}
sb.Append(" )");
}
sb.AppendLine();
}
// 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();
}
/// <summary>
/// Creates a deep clone of this Snowflake query breakdown.
/// </summary>
/// <returns>A cloned SnowflakeQueryBreakdown instance.</returns>
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<string>(baseClone.SetupClauses),
FinishClauses = new ArrayList(baseClone.FinishClauses)
};
// Copy parameters
foreach (var kvp in baseClone.Parameters)
{
clone.Parameters[kvp.Key] = kvp.Value;
}
return clone;
}
/// <summary>
/// Adds a Common Table Expression (CTE) to the WITH clause using a raw SQL string.
/// Parses the SQL using Snowflake SQL rules.
/// </summary>
/// <param name="withTableName">The table name for the WITH clause.</param>
/// <param name="withTableSql">The SQL query for the WITH table.</param>
/// <param name="isMicrosoftSql">If true, uses Microsoft T-SQL parsing rules instead of Snowflake rules. Defaults to true.</param>
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);
}
/// <summary>
/// Creates a Snowflake-specific statement expression parser.
/// </summary>
/// <returns>A Snowflake IStatementExpressionParser instance.</returns>
protected override IStatementExpressionParser CreateExpressionParser()
=> new StatementExpressionParser();
/// <summary>
/// Parses a Snowflake SQL SELECT statement into a SnowflakeQueryBreakdown object.
/// Supports both :parameter and @parameter syntax.
/// </summary>
/// <param name="sql">The Snowflake SQL SELECT statement to parse.</param>
/// <param name="isMicrosoftSql">If true, uses Microsoft T-SQL parsing rules instead of Snowflake rules. Defaults to false.</param>
/// <returns>A SnowflakeQueryBreakdown object representing the parsed query.</returns>
/// <exception cref="ArgumentNullException">Thrown when sql is null or empty.</exception>
/// <exception cref="FormatException">Thrown when the SQL statement cannot be parsed.</exception>
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;
}
/// <summary>
/// Attempts to parse a Snowflake SQL SELECT statement into a SnowflakeQueryBreakdown object.
/// </summary>
/// <param name="sql">The Snowflake SQL SELECT statement to parse.</param>
/// <param name="result">When this method returns, contains the parsed SnowflakeQueryBreakdown if successful, or null if parsing failed.</param>
/// <param name="isMicrosoftSql">If true, uses Microsoft T-SQL parsing rules instead of Snowflake rules. Defaults to false.</param>
/// <returns>true if the SQL was successfully parsed; otherwise, false.</returns>
public static bool TryParse(string sql, out QueryBreakdown result, bool isMicrosoftSql = false)
=> TryParse(sql, out result, out _, isMicrosoftSql);
/// <summary>
/// Attempts to parse a Snowflake SQL SELECT statement into a SnowflakeQueryBreakdown object.
/// Handles Snowflake-specific syntax including :parameter and @parameter formats.
/// </summary>
/// <param name="sql">The Snowflake SQL SELECT statement to parse.</param>
/// <param name="result">When this method returns, contains the parsed SnowflakeQueryBreakdown if successful, or null if parsing failed.</param>
/// <param name="errorMessage">When this method returns false, contains a message describing why parsing failed.</param>
/// <param name="isMicrosoftSql">If true, uses Microsoft T-SQL parsing rules instead of Snowflake rules. Defaults to false.</param>
/// <returns>true if the SQL was successfully parsed; otherwise, false.</returns>
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<string>();
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;
}
}
/// <summary>
/// Gets a LINQ to SQL query of the specified type based on this breakdown.
/// </summary>
/// <typeparam name="T">The entity type for the query.</typeparam>
/// <returns>null by default, as QueryBreakdown operates on SQL. Override in derived classes to provide LINQ query reconstruction.</returns>
/// <remarks>
/// This Snowflake-specific implementation returns null since Snowflake QueryBreakdown represents parsed SQL statements.
/// Derived classes can override this method to reconstruct LINQ queries from the analyzed components.
/// </remarks>
public override IQueryable<T>? GetQuery<T>() where T : class
{
// Snowflake breakdown represents parsed SQL statements and does not have a built-in way to create LINQ queries
// Override in derived classes to provide LINQ query reconstruction if needed
return null;
}
}
@@ -0,0 +1,940 @@
using System.Text;
using Strata.SqlTools.SqlBreakdown.Classes;
using Strata.SqlTools.SqlBreakdown.Interfaces;
namespace Strata.SqlTools.Breakdowns.Snowflake;
/// <summary>
/// Snowflake SQL-specific collection for managing multiple QueryBreakdown objects.
/// </summary>
/// <remarks>
/// This class extends SqlBreakdownCollection with Snowflake-specific functionality,
/// including support for Snowflake features like semi-structured data, stage references,
/// time travel, snowflake-specific parameters (:parameter and @parameter syntax),
/// and proper batch handling.
/// </remarks>
[Serializable]
public class QueryBreakdownCollection : SqlBreakdownCollection
{
private readonly List<QueryBreakdown> _queryBreakdowns;
/// <summary>
/// Initializes a new instance of the <see cref="QueryBreakdownCollection"/> class for Snowflake.
/// </summary>
public QueryBreakdownCollection() : base()
{
_queryBreakdowns = new List<QueryBreakdown>();
}
/// <summary>
/// Initializes a new instance of the <see cref="QueryBreakdownCollection"/> class with initial query breakdowns.
/// </summary>
/// <param name="queryBreakdowns">The initial collection of query breakdowns.</param>
public QueryBreakdownCollection(IEnumerable<QueryBreakdown> queryBreakdowns)
: base(queryBreakdowns?.Cast<ISqlBreakdown>() ?? Enumerable.Empty<ISqlBreakdown>())
{
_queryBreakdowns = new List<QueryBreakdown>(queryBreakdowns ?? Enumerable.Empty<QueryBreakdown>());
}
/// <summary>
/// Gets the collection of QueryBreakdown objects.
/// </summary>
public IReadOnlyList<QueryBreakdown> QueryBreakdowns => _queryBreakdowns.AsReadOnly();
/// <summary>
/// Adds a QueryBreakdown to the collection.
/// </summary>
/// <param name="queryBreakdown">The query breakdown to add.</param>
/// <exception cref="ArgumentNullException">Thrown when queryBreakdown is null.</exception>
public void Add(QueryBreakdown queryBreakdown)
{
if (queryBreakdown == null)
{
throw new ArgumentNullException(nameof(queryBreakdown));
}
_queryBreakdowns.Add(queryBreakdown);
base.Add(queryBreakdown);
}
/// <summary>
/// Adds multiple QueryBreakdowns to the collection.
/// </summary>
/// <param name="queryBreakdowns">The query breakdowns to add.</param>
/// <exception cref="ArgumentNullException">Thrown when queryBreakdowns is null.</exception>
public void AddRange(IEnumerable<QueryBreakdown> queryBreakdowns)
{
if (queryBreakdowns == null)
{
throw new ArgumentNullException(nameof(queryBreakdowns));
}
foreach (var breakdown in queryBreakdowns)
{
Add(breakdown);
}
}
/// <summary>
/// Removes a QueryBreakdown from the collection.
/// </summary>
/// <param name="queryBreakdown">The query breakdown to remove.</param>
/// <returns>True if removed; otherwise, false.</returns>
public bool Remove(QueryBreakdown queryBreakdown)
{
var removed = _queryBreakdowns.Remove(queryBreakdown);
if (removed)
{
base.Remove(queryBreakdown);
}
return removed;
}
/// <summary>
/// Clears all query breakdowns from the collection.
/// </summary>
public new void Clear()
{
_queryBreakdowns.Clear();
base.Clear();
}
/// <summary>
/// Gets the Snowflake SQL batch representation with Snowflake-specific formatting.
/// </summary>
/// <remarks>
/// Generates Snowflake SQL with proper statement separation and optional session setup.
/// Snowflake uses semicolons as statement separators instead of GO.
/// </remarks>
/// <param name="includeSetupFinish">Whether to include setup and finish clauses.</param>
/// <param name="includeSessionSetup">Whether to include session context setup statements.</param>
/// <returns>The formatted Snowflake SQL batch.</returns>
public string GetSnowflakeBatch(bool includeSetupFinish = true, bool includeSessionSetup = false)
{
var sb = new StringBuilder();
// Add session setup if requested
if (includeSessionSetup)
{
sb.AppendLine("-- Snowflake Session Setup");
sb.AppendLine("ALTER SESSION SET NULLABLE_AS_NULL = FALSE;");
sb.AppendLine("ALTER SESSION SET ERROR_ON_NONDETERMINISTIC_UPDATE = FALSE;");
sb.AppendLine();
}
// Add all queries with semicolon separators
if (_queryBreakdowns.Count > 0)
{
for (int i = 0; i < _queryBreakdowns.Count; i++)
{
var query = _queryBreakdowns[i];
var sql = query.GetSql(includeSetupFinish);
// Ensure proper termination
var trimmed = sql.TrimEnd();
sb.Append(trimmed);
if (!trimmed.EndsWith(';'))
{
sb.Append(";");
}
// Add spacing between statements
if (i < _queryBreakdowns.Count - 1)
{
sb.AppendLine();
sb.AppendLine();
}
}
}
return sb.ToString();
}
/// <summary>
/// Filters queries that reference Snowflake stages (using @ or @~ syntax).
/// </summary>
/// <remarks>
/// Stage references use the pattern @stage_name/ or @~/stage_name/.
/// This specifically matches stage references and avoids false positives from @parameter syntax.
/// </remarks>
/// <param name="stageName">Optional stage name to filter by. If null, returns all queries using any stage.</param>
/// <returns>Query breakdowns that reference stages.</returns>
public IEnumerable<QueryBreakdown> WhereUseStageReference(string? stageName = null)
{
return _queryBreakdowns.Where(q =>
{
var sql = q.GetSql();
// Use regex to match stage references: @stage_name/ or @~/stage_name/
// This avoids false positives from @parameter syntax
var stagePattern = @"@[\w~]+/";
if (!System.Text.RegularExpressions.Regex.IsMatch(sql, stagePattern))
{
return false;
}
if (stageName == null)
{
return true;
}
var specificPattern = stageName.Contains("~")
? $@"@~/{System.Text.RegularExpressions.Regex.Escape(stageName.TrimStart('@', '~', '/'))}/"
: $@"@{System.Text.RegularExpressions.Regex.Escape(stageName.TrimStart('@'))}/";
return System.Text.RegularExpressions.Regex.IsMatch(sql, specificPattern, System.Text.RegularExpressions.RegexOptions.IgnoreCase);
});
}
/// <summary>
/// Filters queries that reference JSON/semi-structured data using Snowflake's JSON operators.
/// </summary>
/// <returns>Query breakdowns that use JSON functions or colon notation.</returns>
public IEnumerable<QueryBreakdown> WhereUseSemiStructuredData()
{
return _queryBreakdowns.Where(q =>
{
var sql = q.GetSql().ToUpperInvariant();
// Check for JSON functions or colon notation used in semi-structured data
return sql.Contains("JSON_") ||
sql.Contains("OBJECT_") ||
sql.Contains("ARRAY_") ||
sql.Contains("FLATTEN(") ||
sql.Contains(":VALUE") ||
sql.Contains(":NAME") ||
sql.Contains(":TYPE");
});
}
/// <summary>
/// Filters queries that use Snowflake-specific parameter syntax (:param or @param).
/// </summary>
/// <param name="parameterName">The parameter name (with or without : or @).</param>
/// <returns>Query breakdowns using the specified Snowflake parameter.</returns>
public IEnumerable<QueryBreakdown> WhereUseSnowflakeParameter(string parameterName)
{
if (string.IsNullOrWhiteSpace(parameterName))
{
throw new ArgumentNullException(nameof(parameterName));
}
// Normalize parameter name (remove : or @)
var cleanName = parameterName.TrimStart(':', '@');
return _queryBreakdowns.Where(q =>
{
var sql = q.GetSql();
return sql.Contains($":{cleanName}", StringComparison.OrdinalIgnoreCase) ||
sql.Contains($"@{cleanName}", StringComparison.OrdinalIgnoreCase);
});
}
/// <summary>
/// Filters queries that use Snowflake time travel features.
/// </summary>
/// <remarks>
/// Detects use of BEFORE, AT, or MATCH_CONDITION clauses for time travel queries.
/// </remarks>
/// <returns>Query breakdowns using time travel syntax.</returns>
public IEnumerable<QueryBreakdown> WhereUseTimeTravelFeature()
{
return _queryBreakdowns.Where(q =>
{
var sql = q.GetSql().ToUpperInvariant();
return sql.Contains("BEFORE (") ||
sql.Contains("AT (") ||
sql.Contains("MATCH_CONDITION");
});
}
/// <summary>
/// Filters queries that use Snowflake functions (PARSE_JSON, OBJECT_INSERT, ARRAY, etc.).
/// </summary>
/// <returns>Query breakdowns using Snowflake-specific functions.</returns>
public IEnumerable<QueryBreakdown> WhereUseSnowflakeFunctions()
{
return _queryBreakdowns.Where(q =>
{
var sql = q.GetSql().ToUpperInvariant();
var snowflakeFunctions = new[]
{
"PARSE_JSON", "OBJECT_INSERT", "ARRAY_CONSTRUCT", "ARRAY_AGG",
"FLATTEN", "GET_PATH", "TRY_PARSE_JSON", "JSON_EXTRACT_PATH_TEXT",
"JSON_EXTRACT_PATH_WITH_DEFAULT", "HASHAGGREGATE", "LISTAGG",
"APPROX_COUNT_DISTINCT", "APPROX_PERCENTILE", "GREATEST", "LEAST",
"NULLIF", "ZEROIFNULL", "STRTOK", "SPLIT_PART", "PIVOT", "UNPIVOT"
};
return snowflakeFunctions.Any(func => sql.Contains(func));
});
}
/// <summary>
/// Filters queries that reference temporary or dynamic tables.
/// </summary>
/// <returns>Query breakdowns using temporary tables.</returns>
public IEnumerable<QueryBreakdown> WhereUseTemporaryTables()
{
return _queryBreakdowns.Where(q =>
{
var sql = q.GetSql().ToUpperInvariant();
return sql.Contains("TEMPORARY TABLE") ||
sql.Contains("TEMP TABLE") ||
sql.Contains("CREATE TEMP ") ||
sql.Contains("DYNAMIC TABLE");
});
}
/// <summary>
/// Filters queries that reference external tables or stages.
/// </summary>
/// <returns>Query breakdowns using external data sources.</returns>
public IEnumerable<QueryBreakdown> WhereUseExternalData()
{
return _queryBreakdowns.Where(q =>
{
var sql = q.GetSql().ToUpperInvariant();
return sql.Contains("EXTERNAL TABLE") ||
sql.Contains(" FROM @") ||
sql.Contains("COPY INTO @");
});
}
/// <summary>
/// Filters queries by the SELECT clause content using Snowflake's format.
/// </summary>
/// <param name="selectContains">The text to find in the SELECT clause.</param>
/// <returns>Filtered query breakdowns.</returns>
public IEnumerable<QueryBreakdown> WhereSelectContains(string selectContains)
{
if (string.IsNullOrWhiteSpace(selectContains))
{
throw new ArgumentNullException(nameof(selectContains));
}
return _queryBreakdowns.Where(q =>
q.SelectClause?.Clause?.Contains(selectContains, StringComparison.OrdinalIgnoreCase) ?? false);
}
/// <summary>
/// Filters queries that reference specific tables or schemas.
/// </summary>
/// <param name="tableNameContains">The table name or schema pattern to find.</param>
/// <returns>Filtered query breakdowns.</returns>
public IEnumerable<QueryBreakdown> WhereTableContains(string tableNameContains)
{
if (string.IsNullOrWhiteSpace(tableNameContains))
{
throw new ArgumentNullException(nameof(tableNameContains));
}
return _queryBreakdowns.Where(q =>
q.FromClause?.Clause?.Contains(tableNameContains, StringComparison.OrdinalIgnoreCase) ?? false);
}
/// <summary>
/// Filters queries that have WHERE clauses.
/// </summary>
/// <returns>Query breakdowns with WHERE clauses.</returns>
public IEnumerable<QueryBreakdown> WhereHaveWhereClause()
{
return _queryBreakdowns.Where(q =>
!string.IsNullOrWhiteSpace(q.WhereClause?.Clause));
}
/// <summary>
/// Filters queries without WHERE clauses (potentially risky for full table scans).
/// </summary>
/// <returns>Query breakdowns without WHERE clauses.</returns>
public IEnumerable<QueryBreakdown> WhereHaveNoWhereClause()
{
return _queryBreakdowns.Where(q =>
string.IsNullOrWhiteSpace(q.WhereClause?.Clause));
}
/// <summary>
/// Filters queries that have GROUP BY clauses.
/// </summary>
/// <returns>Query breakdowns with GROUP BY clauses.</returns>
public IEnumerable<QueryBreakdown> WhereHaveGroupByClause()
{
return _queryBreakdowns.Where(q =>
!string.IsNullOrWhiteSpace(q.GroupByClause?.Clause));
}
/// <summary>
/// Filters queries that have ORDER BY clauses.
/// </summary>
/// <returns>Query breakdowns with ORDER BY clauses.</returns>
public IEnumerable<QueryBreakdown> WhereHaveOrderByClause()
{
return _queryBreakdowns.Where(q =>
!string.IsNullOrWhiteSpace(q.OrderByClause?.Clause));
}
/// <summary>
/// Gets a comprehensive analysis of all queries in the collection.
/// </summary>
/// <returns>Analysis summary for each query.</returns>
public IEnumerable<SnowflakeQueryAnalysis> AnalyzeQueries()
{
return _queryBreakdowns.Select((q, index) => new SnowflakeQueryAnalysis
{
Index = index,
HasSelectClause = !string.IsNullOrWhiteSpace(q.SelectClause?.Clause),
HasFromClause = !string.IsNullOrWhiteSpace(q.FromClause?.Clause),
HasWhereClause = !string.IsNullOrWhiteSpace(q.WhereClause?.Clause),
HasGroupByClause = !string.IsNullOrWhiteSpace(q.GroupByClause?.Clause),
HasOrderByClause = !string.IsNullOrWhiteSpace(q.OrderByClause?.Clause),
UsesSemiStructuredData = UseSemiStructuredData(q),
UsesStageReference = UsesStageReference(q),
UsesTimeTravelFeature = UsesTimeTravelFeature(q),
UsesSnowflakeFunctions = UsesSnowflakeFunctions(q),
UsesExternalData = UsesExternalData(q),
ColumnCount = !string.IsNullOrWhiteSpace(q.SelectClause?.Clause) ? q.SelectClause.Clause.Split(',').Length : 0,
ParameterCount = q.ParameterList.Count()
});
}
/// <summary>
/// Gets the combined Snowflake SQL from all breakdowns.
/// </summary>
/// <param name="includeSetupFinish">Whether to include setup and finish clauses.</param>
/// <returns>The combined Snowflake SQL.</returns>
public string GetCombinedSql(bool includeSetupFinish = true)
{
return GetSnowflakeBatch(includeSetupFinish);
}
/// <summary>
/// Helper method to check if a query uses semi-structured data.
/// </summary>
private static bool UseSemiStructuredData(QueryBreakdown query)
{
var sql = query.GetSql().ToUpperInvariant();
return sql.Contains("JSON_") ||
sql.Contains("OBJECT_") ||
sql.Contains("ARRAY_") ||
sql.Contains("FLATTEN(");
}
/// <summary>
/// Helper method to check if a query uses stage references.
/// </summary>
private static bool UsesStageReference(QueryBreakdown query)
{
return query.GetSql().Contains("@") &&
(query.GetSql().Contains("FROM @") || query.GetSql().Contains(" @"));
}
/// <summary>
/// Helper method to check if a query uses time travel features.
/// </summary>
private static bool UsesTimeTravelFeature(QueryBreakdown query)
{
var sql = query.GetSql().ToUpperInvariant();
return sql.Contains("BEFORE (") ||
sql.Contains("AT (") ||
sql.Contains("MATCH_CONDITION");
}
/// <summary>
/// Helper method to check if a query uses Snowflake-specific functions.
/// </summary>
private static bool UsesSnowflakeFunctions(QueryBreakdown query)
{
var sql = query.GetSql().ToUpperInvariant();
var snowflakeFunctions = new[]
{
"PARSE_JSON", "OBJECT_INSERT", "ARRAY_CONSTRUCT", "FLATTEN",
"LISTAGG", "APPROX_COUNT_DISTINCT", "HASH", "ZEROIFNULL"
};
return snowflakeFunctions.Any(func => sql.Contains(func));
}
/// <summary>
/// Helper method to check if a query uses external data.
/// </summary>
private static bool UsesExternalData(QueryBreakdown query)
{
var sql = query.GetSql().ToUpperInvariant();
return sql.Contains("EXTERNAL TABLE") ||
sql.Contains(" FROM @") ||
sql.Contains("COPY INTO @");
}
/// <summary>
/// Synchronizes parameter values across all queries in the collection.
/// Ensures that if a parameter with the same name exists in multiple queries, they all have the same value.
/// </summary>
public void SynchronizeParameters()
{
// Get all unique parameter names across all queries
var allParameterNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var query in _queryBreakdowns)
{
foreach (var paramName in query.Parameters.Keys)
{
allParameterNames.Add(paramName);
}
}
// For each parameter, use the last query's value and sync to all queries that have it
foreach (var paramName in allParameterNames)
{
object? lastValue = null;
bool parameterFound = false;
// Find the last query that has this parameter and get its value
for (int i = _queryBreakdowns.Count - 1; i >= 0; i--)
{
if (_queryBreakdowns[i].Parameters.ContainsKey(paramName))
{
lastValue = _queryBreakdowns[i].Parameters[paramName];
parameterFound = true;
break;
}
}
// Synchronize the parameter value to all queries that have it
if (parameterFound)
{
foreach (var query in _queryBreakdowns.Where(q => q.Parameters.ContainsKey(paramName)))
{
query.Parameters[paramName] = lastValue!;
}
}
}
}
/// <summary>
/// Adds a parameter with a specific value to all queries in the collection.
/// </summary>
/// <param name="parameterName">The name of the parameter (without the : or @ prefix).</param>
/// <param name="value">The value to assign to the parameter. Can be null.</param>
public void AddParameterToAll(string parameterName, object? value)
{
if (string.IsNullOrWhiteSpace(parameterName))
{
throw new ArgumentException("Parameter name cannot be null or empty.", nameof(parameterName));
}
foreach (var query in _queryBreakdowns)
{
query.Parameters[parameterName] = value!;
}
}
/// <summary>
/// Gets all unique parameters from all queries in the collection as a combined dictionary.
/// </summary>
/// <returns>A dictionary containing all unique parameters across all queries.</returns>
protected Dictionary<string, object?> GetCombinedParameterDictionary()
{
var combinedParams = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
foreach (var query in _queryBreakdowns)
{
// Add from ParameterList (parsed parameters)
foreach (var param in query.ParameterList)
{
combinedParams[param.Name] = param.Value;
}
// Add/override from Parameters dictionary (manually added parameters)
foreach (var param in query.Parameters)
{
combinedParams[param.Key] = param.Value;
}
}
return combinedParams;
}
/// <summary>
/// Gets a formatted string representation of all unique parameters with Snowflake-specific syntax.
/// </summary>
/// <param name="includeDataTypes">If true, includes Snowflake data types in the output format.</param>
/// <returns>
/// A formatted string such as ":paramName = value" or ":paramName = value -- VARIANT"
/// for each unique parameter.
/// </returns>
public string GetParametersAsString(bool includeDataTypes = false)
{
var parameters = GetCombinedParameterDictionary();
if (parameters.Count == 0)
{
return string.Empty;
}
var sb = new StringBuilder();
var isFirst = true;
foreach (var param in parameters.OrderBy(p => p.Key, StringComparer.OrdinalIgnoreCase))
{
if (!isFirst)
{
sb.AppendLine(",");
}
sb.Append($":{param.Key} = {FormatParameterValue(param.Value)}");
if (includeDataTypes)
{
var dataType = GetSnowflakeDataType(param.Value);
sb.Append($" -- {dataType}");
}
isFirst = false;
}
return sb.ToString();
}
/// <summary>
/// Gets a detailed usage report for all parameters across the queries in the collection.
/// </summary>
/// <returns>An enumerable of ParameterUsageReport objects with usage statistics.</returns>
/// <summary>
/// Gets a report of parameter usage across all queries.
/// </summary>
/// <returns>Parameter usage information.</returns>
public IEnumerable<ParameterUsageReport> GetParameterUsageReport()
{
// Collect all unique parameter names from both ParameterList and Parameters dictionary
var allParamNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var query in _queryBreakdowns)
{
// Add from ParameterList (parsed parameters)
foreach (var param in query.ParameterList)
{
allParamNames.Add(param.Name);
}
// Add from Parameters dictionary (manually added parameters)
foreach (var paramName in query.Parameters.Keys)
{
allParamNames.Add(paramName);
}
}
foreach (var paramName in allParamNames)
{
var queriesUsing = 0;
object? lastValue = null;
foreach (var query in _queryBreakdowns)
{
// Check ParameterList first (parsed)
var param = query.ParameterList.FirstOrDefault(p => p.Name.Equals(paramName, StringComparison.OrdinalIgnoreCase));
if (param != null)
{
queriesUsing++;
lastValue = param.Value;
}
// Also check Parameters dictionary (manually added)
else if (query.Parameters.TryGetValue(paramName, out var dictValue))
{
queriesUsing++;
lastValue = dictValue;
}
}
yield return new ParameterUsageReport
{
ParameterName = paramName,
Value = lastValue,
UsedInQueryCount = queriesUsing,
TotalQueries = _queryBreakdowns.Count
};
}
}
/// <summary>
/// Gets the total number of columns selected across all queries.
/// </summary>
/// <returns>Total column count.</returns>
public int GetTotalSelectedColumns()
{
return _queryBreakdowns.Sum(q =>
!string.IsNullOrWhiteSpace(q.SelectClause?.Clause)
? q.SelectClause.Clause.Split(',').Length
: 0);
}
/// <summary>
/// Gets all unique table names referenced across all queries.
/// </summary>
/// <remarks>
/// This provides a quick overview of which tables are being queried.
/// Note: This is a best-effort extraction and may not capture all table references,
/// especially in complex subqueries or with aliasing.
/// </remarks>
/// <returns>List of unique table names.</returns>
public IEnumerable<string> GetUniqueTableReferences()
{
var tables = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var tableNames = _queryBreakdowns
.Where(q => !string.IsNullOrWhiteSpace(q.FromClause?.Clause))
.SelectMany(q => ExtractTableNames(q.FromClause!.Clause!));
foreach (var table in tableNames)
{
tables.Add(table);
}
return tables;
}
/// <summary>
/// Gets a summary of all queries including their types and basic composition.
/// </summary>
/// <returns>Summary information for each query.</returns>
public IEnumerable<SnowflakeQueryAnalysis> GetQuerySummaries()
{
var stageQueries = WhereUseStageReference().ToHashSet();
var semiStructuredQueries = WhereUseSemiStructuredData().ToHashSet();
return _queryBreakdowns.Select((q, index) => new SnowflakeQueryAnalysis
{
Index = index,
HasSelectClause = !string.IsNullOrWhiteSpace(q.SelectClause?.Clause),
HasFromClause = !string.IsNullOrWhiteSpace(q.FromClause?.Clause),
HasWhereClause = !string.IsNullOrWhiteSpace(q.WhereClause?.Clause),
HasGroupByClause = !string.IsNullOrWhiteSpace(q.GroupByClause?.Clause),
HasOrderByClause = !string.IsNullOrWhiteSpace(q.OrderByClause?.Clause),
HasCTE = q.WithClauses.Count > 0,
UsesSemiStructuredData = semiStructuredQueries.Contains(q),
UsesStageReference = stageQueries.Contains(q),
UsesTimeTravelFeature = UsesTimeTravelFeature(q),
UsesSnowflakeFunctions = false,
UsesExternalData = false,
ColumnCount = !string.IsNullOrWhiteSpace(q.SelectClause?.Clause) ? q.SelectClause.Clause.Split(',').Length : 0,
ParameterCount = q.ParameterList.Count()
});
}
/// <summary>
/// Helper method to extract table names from a FROM clause.
/// </summary>
private static IEnumerable<string> ExtractTableNames(string fromClause)
{
if (string.IsNullOrWhiteSpace(fromClause))
{
yield break;
}
var parts = fromClause.Split(',');
foreach (var part in parts)
{
var trimmed = part.Trim();
var tokens = trimmed.Split(new[] { " AS ", " " }, StringSplitOptions.RemoveEmptyEntries);
if (tokens.Length > 0)
{
var tableName = tokens[0].Trim();
if (!string.IsNullOrWhiteSpace(tableName))
{
yield return tableName;
}
}
}
}
/// <summary>
/// Helper method to convert a .NET object to its corresponding Snowflake data type string.
/// </summary>
private static string GetSnowflakeDataType(object? value)
{
return value switch
{
null => "VARIANT",
bool => "BOOLEAN",
byte or sbyte or short or ushort or int or uint or long or ulong => "NUMBER",
float or double or decimal => "NUMBER",
DateTime or DateTimeOffset => "TIMESTAMP_NTZ",
TimeSpan => "TIME",
string => value.ToString()!.Length > 255 ? "VARCHAR(MAX)" : "VARCHAR(255)",
byte[] => "BINARY",
_ => "VARIANT"
};
}
/// <summary>
/// Helper method to format a parameter value for safe inclusion in Snowflake SQL statements.
/// </summary>
private static string FormatParameterValue(object? value)
{
return value switch
{
null => "NULL",
bool b => b ? "TRUE" : "FALSE",
string s => $"'{s.Replace("'", "''")}'",
DateTime dt => $"'{dt:yyyy-MM-dd HH:mm:ss}'",
DateTimeOffset dto => $"'{dto:yyyy-MM-dd HH:mm:ss}'",
_ => value.ToString() ?? "NULL"
};
}
}
/// <summary>
/// Analysis information about a Snowflake query.
/// </summary>
public class SnowflakeQueryAnalysis
{
/// <summary>
/// Gets or sets the index of the query in the collection.
/// </summary>
public int Index { get; set; }
/// <summary>
/// Gets or sets whether the query has a SELECT clause.
/// </summary>
public bool HasSelectClause { get; set; }
/// <summary>
/// Gets or sets whether the query has a FROM clause.
/// </summary>
public bool HasFromClause { get; set; }
/// <summary>
/// Gets or sets whether the query has a WHERE clause.
/// </summary>
public bool HasWhereClause { get; set; }
/// <summary>
/// Gets or sets whether the query has a GROUP BY clause.
/// </summary>
public bool HasGroupByClause { get; set; }
/// <summary>
/// Gets or sets whether the query has an ORDER BY clause.
/// </summary>
public bool HasOrderByClause { get; set; }
/// <summary>
/// Gets or sets whether the query has Common Table Expressions (CTEs).
/// </summary>
public bool HasCTE { get; set; }
/// <summary>
/// Gets or sets whether the query uses semi-structured data functions.
/// </summary>
public bool UsesSemiStructuredData { get; set; }
/// <summary>
/// Gets or sets whether the query references Snowflake stages.
/// </summary>
public bool UsesStageReference { get; set; }
/// <summary>
/// Gets or sets whether the query uses Snowflake time travel features.
/// </summary>
public bool UsesTimeTravelFeature { get; set; }
/// <summary>
/// Gets or sets whether the query uses Snowflake-specific functions.
/// </summary>
public bool UsesSnowflakeFunctions { get; set; }
/// <summary>
/// Gets or sets whether the query uses external data sources.
/// </summary>
public bool UsesExternalData { get; set; }
/// <summary>
/// Gets or sets the number of columns in the SELECT clause.
/// </summary>
public int ColumnCount { get; set; }
/// <summary>
/// Gets or sets the number of parameters used.
/// </summary>
public int ParameterCount { get; set; }
/// <summary>
/// Returns a string representation of the query analysis.
/// </summary>
public override string ToString()
{
var sb = new StringBuilder();
sb.AppendLine($"Snowflake Query #{Index}");
sb.AppendLine($" Basic Structure:");
sb.AppendLine($" SELECT: {(HasSelectClause ? "Yes" : "No")} ({ColumnCount} columns)");
sb.AppendLine($" FROM: {(HasFromClause ? "Yes" : "No")}");
sb.AppendLine($" WHERE: {(HasWhereClause ? "Yes" : "No")}");
sb.AppendLine($" GROUP BY: {(HasGroupByClause ? "Yes" : "No")}");
sb.AppendLine($" ORDER BY: {(HasOrderByClause ? "Yes" : "No")}");
sb.AppendLine($" Snowflake Features:");
sb.AppendLine($" Semi-Structured Data: {(UsesSemiStructuredData ? "Yes" : "No")}");
sb.AppendLine($" Stage Reference: {(UsesStageReference ? "Yes" : "No")}");
sb.AppendLine($" Time Travel: {(UsesTimeTravelFeature ? "Yes" : "No")}");
sb.AppendLine($" Snowflake Functions: {(UsesSnowflakeFunctions ? "Yes" : "No")}");
sb.AppendLine($" External Data: {(UsesExternalData ? "Yes" : "No")}");
sb.Append($" Parameters: {ParameterCount}");
return sb.ToString();
}
}
/// <summary>
/// Reports parameter usage statistics across queries in a Snowflake collection.
/// </summary>
public class ParameterUsageReport
{
/// <summary>
/// Gets or sets the name of the parameter.
/// </summary>
public string ParameterName { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the current value of the parameter.
/// </summary>
public object? Value { get; set; }
/// <summary>
/// Gets or sets the number of queries using this parameter.
/// </summary>
public int UsedInQueryCount { get; set; }
/// <summary>
/// Gets or sets the total number of queries in the collection.
/// </summary>
public int TotalQueries { get; set; }
/// <summary>
/// Gets a value indicating whether this parameter is used in all queries.
/// </summary>
public bool IsUsedInAllQueries => UsedInQueryCount == TotalQueries && TotalQueries > 0;
/// <summary>
/// Returns a string representation of the parameter usage report.
/// </summary>
public override string ToString()
{
if (TotalQueries == 0)
{
return $"{ParameterName}: No queries";
}
var percentage = (UsedInQueryCount * 100.0) / TotalQueries;
var valueStr = Value switch
{
null => "NULL",
string s => $"'{s}'",
_ => Value.ToString() ?? "NULL"
};
return $"{ParameterName} = {valueStr} ({UsedInQueryCount}/{TotalQueries} queries - {percentage:F1}%)";
}
}
@@ -0,0 +1,90 @@
using System.Data;
using System.Data.Common;
using System.Diagnostics;
namespace Strata.SqlTools.Breakdowns.Snowflake;
/// <summary>
/// A trace listener that writes trace messages to a Snowflake database.
/// </summary>
public class TraceListener : System.Diagnostics.TraceListener
{
private readonly string _serverName;
private readonly string _traceDbConnectionString;
private readonly string _providerName;
/// <summary>
/// Initializes a new instance of the <see cref="TraceListener"/> class.
/// </summary>
/// <param name="serverName">The server name for logging.</param>
/// <param name="traceDbConnectionString">The connection string to the trace database.</param>
/// <param name="providerName">The provider name (default: "Snowflake.Data.Client").</param>
public TraceListener(string serverName, string traceDbConnectionString, string providerName = "Snowflake.Data.Client")
{
_serverName = serverName;
_traceDbConnectionString = traceDbConnectionString;
_providerName = providerName;
}
/// <summary>
/// Writes a message to the trace database.
/// </summary>
/// <param name="message">The message to write.</param>
public override void Write(string? message)
{
WriteTrace(message);
}
/// <summary>
/// Writes a message followed by a line terminator to the trace database.
/// </summary>
/// <param name="message">The message to write.</param>
public override void WriteLine(string? message)
{
WriteTrace(message);
}
/// <summary>
/// Writes a trace message to the Snowflake database.
/// </summary>
/// <param name="message">The message to write.</param>
private void WriteTrace(string? message)
{
try
{
var factory = DbProviderFactories.GetFactory(_providerName);
using var connection = factory.CreateConnection();
if (connection == null)
{
return;
}
connection.ConnectionString = _traceDbConnectionString;
connection.Open();
using var command = connection.CreateCommand();
command.CommandType = CommandType.Text;
command.CommandText = "INSERT INTO Trace (SERVER, MESSAGE) VALUES(:SERVER, :MESSAGE)";
var serverParam = command.CreateParameter();
serverParam.ParameterName = "SERVER";
serverParam.Value = _serverName;
command.Parameters.Add(serverParam);
var messageParam = command.CreateParameter();
messageParam.ParameterName = "MESSAGE";
messageParam.Value = message ?? (object)DBNull.Value;
command.Parameters.Add(messageParam);
command.ExecuteNonQuery();
}
catch (Exception)
{
if (Debugger.IsAttached)
{
Debugger.Break();
}
}
}
}
@@ -0,0 +1,215 @@
using System.Collections;
using System.Text;
using SqlServerUpdateBreakdown = Strata.SqlTools.Breakdowns.SqlServer.UpdateBreakdown;
using StatementParser = Strata.SqlTools.Statements.Snowflake.StatementParser;
namespace Strata.SqlTools.Breakdowns.Snowflake;
/// <summary>
/// Represents an UPDATE SQL statement breakdown with SET, FROM, and WHERE clauses for Snowflake.
/// </summary>
[Serializable]
public class UpdateBreakdown : SqlServerUpdateBreakdown
{
private static readonly StatementParser SnowflakeParserInstance = new StatementParser();
/// <summary>
/// Initializes a new instance of the <see cref="UpdateBreakdown"/> class.
/// </summary>
public UpdateBreakdown() : base()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="UpdateBreakdown"/> class.
/// </summary>
/// <param name="tableName">The table name.</param>
/// <param name="setClause">The SET clause.</param>
/// <param name="whereClause">The WHERE clause.</param>
/// <param name="isMicrosoftSql">If true, uses Microsoft T-SQL parsing rules. If false, uses Snowflake parsing rules. Defaults to false.</param>
public UpdateBreakdown(string tableName, string setClause, string whereClause, bool isMicrosoftSql = false)
: base()
{
var parser = isMicrosoftSql ? Parser : SnowflakeParserInstance;
var cleanTable = parser.ExtractSqlComments(tableName, out var tableComments);
TableName.Clause = cleanTable.Trim();
TableName.Comment = tableComments.Count > 0 ? string.Join(" ", tableComments) : null;
var cleanSet = parser.ExtractSqlComments(setClause, out var setComments);
SetClause.Clause = cleanSet.Trim();
SetClause.Comment = setComments.Count > 0 ? string.Join(" ", setComments) : null;
var cleanWhere = parser.ExtractSqlComments(whereClause, out var whereComments);
WhereClause.Clause = cleanWhere.Trim();
WhereClause.Comment = whereComments.Count > 0 ? string.Join(" ", whereComments) : null;
}
/// <summary>
/// Gets the SQL breakdown as a string for Snowflake.
/// </summary>
/// <returns>The UPDATE SQL statement.</returns>
protected override string GetSqlBreakdown()
{
var sb = new StringBuilder();
sb.AppendLine("UPDATE ");
sb.AppendLine($" {TableName.Clause}");
sb.AppendLine("SET ");
sb.AppendLine($" {SetClause.Clause}");
if (IsUsingFromClause)
{
// Snowflake supports FROM clause in UPDATE
sb.AppendLine("FROM ");
sb.AppendLine($" {FromClause.Clause}");
}
if (IsUsingWhereClause)
{
sb.AppendLine("WHERE ");
sb.AppendLine($" {WhereClause.Clause}");
}
return sb.ToString();
}
#region Parse Methods
/// <summary>
/// Parses a Snowflake UPDATE SQL statement into an UpdateBreakdown object.
/// </summary>
/// <param name="sql">The UPDATE SQL statement to parse.</param>
/// <param name="isMicrosoftSql">If true, uses Microsoft T-SQL parsing rules. If false, uses Snowflake parsing rules. Defaults to false.</param>
/// <returns>An UpdateBreakdown object representing the parsed statement.</returns>
/// <exception cref="ArgumentNullException">Thrown when sql is null or empty.</exception>
/// <exception cref="FormatException">Thrown when the SQL statement cannot be parsed.</exception>
public static UpdateBreakdown 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")} UPDATE statement: {error}");
}
return result;
}
/// <summary>
/// Attempts to parse a Snowflake UPDATE SQL statement into an UpdateBreakdown object.
/// </summary>
/// <param name="sql">The UPDATE SQL statement to parse.</param>
/// <param name="result">When this method returns, contains the parsed UpdateBreakdown if successful, or null if parsing failed.</param>
/// <param name="isMicrosoftSql">If true, uses Microsoft T-SQL parsing rules instead of Snowflake rules. Defaults to false.</param>
/// <returns>true if the SQL was successfully parsed; otherwise, false.</returns>
public static bool TryParse(string sql, out UpdateBreakdown result, bool isMicrosoftSql = false)
=> TryParse(sql, out result, out _, isMicrosoftSql);
/// <summary>
/// Attempts to parse a Snowflake UPDATE SQL statement into an UpdateBreakdown object.
/// Handles Snowflake-specific syntax.
/// </summary>
/// <param name="sql">The UPDATE SQL statement to parse.</param>
/// <param name="result">When this method returns, contains the parsed UpdateBreakdown if successful, or null if parsing failed.</param>
/// <param name="errorMessage">When this method returns false, contains a message describing why parsing failed.</param>
/// <param name="isMicrosoftSql">If true, uses Microsoft T-SQL parsing rules instead of Snowflake rules. Defaults to false.</param>
/// <returns>true if the SQL was successfully parsed; otherwise, false.</returns>
public static bool TryParse(string sql, out UpdateBreakdown 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 (!SqlServerUpdateBreakdown.TryParse(sql, out var baseResult, out errorMessage))
{
return false;
}
// Convert to Snowflake UpdateBreakdown
result = new UpdateBreakdown
{
TableName = baseResult.TableName,
SetClause = baseResult.SetClause,
FromClause = baseResult.FromClause,
WhereClause = baseResult.WhereClause,
SetupClauses = baseResult.SetupClauses,
FinishClauses = baseResult.FinishClauses
};
return true;
}
var parser = SnowflakeParserInstance;
sql = parser.NormalizeSqlPreservingComments(sql);
// Check if it's an UPDATE statement
var sqlTrimmed = sql.TrimStart();
if (!System.Text.RegularExpressions.Regex.IsMatch(sqlTrimmed, @"^\s*UPDATE\b",
System.Text.RegularExpressions.RegexOptions.IgnoreCase))
{
errorMessage = "SQL statement must start with UPDATE.";
return false;
}
// Extract setup and finish clauses
var setupClauses = new List<string>();
sql = parser.ExtractSetupClauses(sql, setupClauses);
var finishClauses = new ArrayList();
sql = parser.ExtractFinishClauses(sql, finishClauses);
// Parse UPDATE statement - handle both with and without FROM clause
var updateMatch = System.Text.RegularExpressions.Regex.Match(sql,
@"UPDATE\s+([^\s]+)\s+SET\s+(.*?)(?:\s+FROM\s+(.*?))?(?:\s+WHERE\s+(.*))?$",
System.Text.RegularExpressions.RegexOptions.IgnoreCase | System.Text.RegularExpressions.RegexOptions.Singleline);
if (!updateMatch.Success)
{
errorMessage = "Could not parse UPDATE statement. Expected format: UPDATE table SET column=value [FROM table] [WHERE condition]";
return false;
}
var tableName = updateMatch.Groups[1].Value.Trim();
var setClause = updateMatch.Groups[2].Value.Trim();
var fromClause = updateMatch.Groups.Count > 3 ? updateMatch.Groups[3].Value.Trim() : string.Empty;
var whereClause = updateMatch.Groups.Count > 4 ? updateMatch.Groups[4].Value.Trim() : string.Empty;
result = new UpdateBreakdown(tableName, setClause, whereClause, isMicrosoftSql: false)
{
SetupClauses = setupClauses,
FinishClauses = finishClauses
};
if (!string.IsNullOrWhiteSpace(fromClause))
{
var cleanFrom = parser.ExtractSqlComments(fromClause, out var fromComments);
result.FromClause.Clause = cleanFrom.Trim();
result.FromClause.Comment = fromComments.Count > 0 ? string.Join(" ", fromComments) : null;
}
return true;
}
catch (Exception ex)
{
errorMessage = $"Unexpected error during parsing: {ex.Message}";
return false;
}
}
#endregion
}
@@ -0,0 +1,26 @@
namespace Strata.SqlTools.Snowflake.ExpressionFactory;
/// <summary>
/// Snowflake-specific factory class for creating boolean expressions and SQL filter conditions from Filter objects.
/// Inherits from the SQL Server implementation and extends it with Snowflake-specific syntax support.
/// </summary>
public abstract class ExpressionFactory : SqlServer.ExpressionFactory.ExpressionFactory
{
/// <summary>
/// Initializes a new instance of the <see cref="ExpressionFactory"/> class with the default system time provider.
/// </summary>
protected ExpressionFactory() : base()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ExpressionFactory"/> class with the specified time provider.
/// </summary>
/// <param name="timeProvider">The time provider implementation for date/time operations.</param>
protected ExpressionFactory(TimeProvider timeProvider) : base(timeProvider)
{
}
// Snowflake-specific expression methods can be added here as needed
// For example, support for Snowflake-specific date functions, parameter syntax (@param and :param), etc.
}
@@ -0,0 +1,12 @@
namespace Strata.SqlTools.Snowflake.ExpressionFactory.Query;
public enum AggregationType
{
Sum = 0,
Count = 1,
CountDistinct = 2,
Avg = 3,
Median = 4,
Min = 5,
Max = 6
}
@@ -0,0 +1,12 @@
namespace Strata.SqlTools.Snowflake.ExpressionFactory.Query;
public class CalculationFilter : Filter
{
public IEnumerable<string> AliasedDataColumnIds { get; }
public CalculationFilter(int dataColumnId, IEnumerable<string> aliasedDataColumnIds, IEnumerable<object> values, IEnumerable<FilterCondition> conditions)
: base(dataColumnId, FilterType.Conditions, values, conditions, DatePart.Continuous, false, 0, 0)
{
AliasedDataColumnIds = aliasedDataColumnIds;
}
}
@@ -0,0 +1,36 @@
using System.Text.Json.Serialization;
namespace Strata.SqlTools.Snowflake.ExpressionFactory.Query;
public class CalculationFilterGroup
{
[JsonIgnore]
private IEnumerable<CalculationFilter> _filters;
// Hereditary logical operation applied to all Filters
public LogicalOperator LogicalOperator { get; set; }
public IEnumerable<CalculationFilter> Filters
{
get => _filters?.Where(x => x.IsValid()).ToList() ?? new List<CalculationFilter>();
set => _filters = value;
}
public CalculationFilterGroup()
{
LogicalOperator = LogicalOperator.And;
_filters = new List<CalculationFilter>();
}
[JsonConstructor]
public CalculationFilterGroup(IEnumerable<CalculationFilter> filters, LogicalOperator logicalOperator)
{
_filters = filters;
LogicalOperator = logicalOperator;
}
public bool IsValid()
{
return Filters != null && Filters.Any();
}
}
@@ -0,0 +1,23 @@
using System.Text.Json.Serialization;
namespace Strata.SqlTools.Snowflake.ExpressionFactory.Query;
public class ColumnQueryConfig
{
public int DataColumnId { get; set; }
public DatePart DatePart { get; set; }
public Filter? Filter { get; set; }
public int RowLimit { get; set; }
[JsonConstructor]
public ColumnQueryConfig(int dataColumnId, DatePart datePart, Filter? filter, int rowLimit)
{
DataColumnId = dataColumnId;
DatePart = datePart;
Filter = filter != null && filter.IsValid() ? filter : null;
RowLimit = rowLimit;
}
}
@@ -0,0 +1,13 @@
namespace Strata.SqlTools.Snowflake.ExpressionFactory.Query;
public enum DatePart
{
Continuous = 0,
Year,
Quarter,
Month,
Week,
Day,
FiscalYear,
FiscalQuarter
}
@@ -0,0 +1,8 @@
namespace Strata.SqlTools.Snowflake.ExpressionFactory.Query;
public class Field
{
public string ColumnAlias { get; set; } = string.Empty;
public int DataColumnId { get; set; }
public DatePart DatePart { get; set; }
}
@@ -0,0 +1,91 @@
using System.Text.Json.Serialization;
namespace Strata.SqlTools.Snowflake.ExpressionFactory.Query;
/// <summary>
/// Represents a filter criteria for querying data with support for various filter types including lists, date ranges, and timeframes.
/// Filters can be applied to specific data columns and support different date granularities.
/// </summary>
public class Filter
{
/// <summary>
/// Gets the identifier of the data column to which this filter applies.
/// </summary>
public int DataColumnId { get; }
/// <summary>
/// Gets the type of filter being applied (e.g., List, Calendar, Timeframe).
/// </summary>
public FilterType FilterType { get; }
/// <summary>
/// Gets the collection of values to filter by. The interpretation depends on the <see cref="FilterType"/>.
/// </summary>
public IEnumerable<object> Values { get; }
/// <summary>
/// Gets the collection of filter conditions that define complex filtering logic.
/// Only valid conditions are retained.
/// </summary>
public IEnumerable<FilterCondition> Conditions { get; }
/// <summary>
/// Gets the date granularity part for date-based filtering (e.g., Year, Month, Day, FiscalYear).
/// </summary>
public DatePart DatePart { get; }
/// <summary>
/// Gets a value indicating whether to use NOT IN instead of IN for list-type filters.
/// Only applies when <see cref="FilterType"/> is List.
/// </summary>
public bool ListUseNotIn { get; }
/// <summary>
/// Gets the offset from the current time for timeframe-based filters.
/// Used in conjunction with <see cref="DateTimeFrameCount"/> to define relative time periods.
/// </summary>
public int DateTimeFrameOffset { get; }
/// <summary>
/// Gets the zero-based number of time increments from the offset.
/// A value of 0 means current period, -1 means one period backward, and 1 means one period forward.
/// The unit (year, month, day, etc.) is determined by the <see cref="DatePart"/> property.
/// </summary>
public int DateTimeFrameCount { get; }
/// <summary>
/// Initializes a new instance of the <see cref="Filter"/> class with the specified filter criteria.
/// </summary>
/// <param name="dataColumnId">The identifier of the data column to filter.</param>
/// <param name="filterType">The type of filter to apply.</param>
/// <param name="values">The collection of values for the filter.</param>
/// <param name="conditions">The collection of filter conditions (invalid conditions are automatically removed).</param>
/// <param name="datePart">The date granularity for date-based filtering.</param>
/// <param name="listUseNotIn">Whether to use NOT IN for list filters; false to use IN.</param>
/// <param name="dateTimeFrameOffset">The offset from current time for timeframe filters.</param>
/// <param name="dateTimeFrameCount">The number of time increments from the offset (0 = current, negative = past, positive = future).</param>
[JsonConstructor]
public Filter(int dataColumnId, FilterType filterType, IEnumerable<object> values, IEnumerable<FilterCondition> conditions, DatePart datePart, bool listUseNotIn, int dateTimeFrameOffset, int dateTimeFrameCount)
{
DataColumnId = dataColumnId;
FilterType = filterType;
Values = values;
Conditions = conditions.Where(x => x.IsValid()).ToList();
DatePart = datePart;
ListUseNotIn = listUseNotIn;
DateTimeFrameOffset = dateTimeFrameOffset;
DateTimeFrameCount = dateTimeFrameCount;
}
/// <summary>
/// Determines whether this filter has valid criteria that can be applied.
/// A filter is valid if it has values, conditions, or non-default timeframe settings.
/// </summary>
/// <returns>
/// <c>true</c> if the filter has values, conditions, or timeframe settings; otherwise, <c>false</c>.
/// </returns>
public bool IsValid()
{
return (Values != null && Values.Any()) || (Conditions != null && Conditions.Any()) || (DateTimeFrameCount != default || DateTimeFrameOffset != default);
}
}
@@ -0,0 +1,17 @@
namespace Strata.SqlTools.Snowflake.ExpressionFactory.Query;
public class FilterCondition
{
public FilterOperator Operator { get; set; }
public IEnumerable<object>? Values { get; set; }
// This is not hereditary to Values; it is used for combination with the next FilterCondition in the set
// todo: That could be indexed to ensure accuracy
public LogicalOperator LogicalOperator { get; set; }
public bool IsValid()
{
return Values != null && Values.Any();
}
}
@@ -0,0 +1,29 @@
using System.Text.Json.Serialization;
namespace Strata.SqlTools.Snowflake.ExpressionFactory.Query;
public class FilterGroup
{
// Hereditary logical operation applied to all Filters
public LogicalOperator LogicalOperator { get; set; }
public IEnumerable<Filter> Filters { get; }
public FilterGroup()
{
LogicalOperator = LogicalOperator.And;
Filters = new List<Filter>();
}
[JsonConstructor]
public FilterGroup(IEnumerable<Filter> filters, LogicalOperator logicalOperator)
{
Filters = filters.Where(x => x.IsValid()).ToList();
LogicalOperator = logicalOperator;
}
public bool IsValid()
{
return Filters != null && Filters.Any();
}
}
@@ -0,0 +1,15 @@
namespace Strata.SqlTools.Snowflake.ExpressionFactory.Query;
public enum FilterOperator
{
Equals = 0,
NotEquals = 1,
LessThan = 2,
LessThanOrEqualTo = 3,
GreaterThan = 4,
GreaterThanOrEqualTo = 5,
Between = 6, // this is a function, not a comparison - x BETWEEN a AND b is the same as: x >= a AND x <= z
Contains = 7,
StartsWith = 8,
EndsWith = 9
}
@@ -0,0 +1,9 @@
namespace Strata.SqlTools.Snowflake.ExpressionFactory.Query;
public enum FilterType
{
List = 0,
Conditions = 1,
Calendar = 2,
Timeframe = 3
}
@@ -0,0 +1,30 @@
using System.ComponentModel.DataAnnotations;
namespace Strata.SqlTools.Snowflake.ExpressionFactory.Query;
public enum LogicalOperator
{
[Display(Name = "and")]
And,
[Display(Name = "or")]
Or
}
public static class LogicalOperatorExtensions
{
public static string ToSql(this LogicalOperator logicalOperator, bool withSpaces = true)
{
var sql = "";
switch (logicalOperator)
{
case LogicalOperator.And:
sql = "and";
break;
case LogicalOperator.Or:
sql = "or";
break;
}
return withSpaces ? $" {sql} " : sql;
}
}
@@ -0,0 +1,33 @@
using System.Text.Json.Serialization;
namespace Strata.SqlTools.Snowflake.ExpressionFactory.Query;
public class QueryConfig
{
public IEnumerable<Row> Rows { get; set; }
public IEnumerable<Value> Values { get; set; }
public IEnumerable<FilterGroup> FilterGroups { get; }
public bool WithTotals { get; set; }
public int RowLimit { get; set; }
public QueryConfig()
{
Rows = new List<Row>();
Values = new List<Value>();
FilterGroups = new List<FilterGroup>();
}
[JsonConstructor]
public QueryConfig(IEnumerable<FilterGroup> filterGroups, IEnumerable<Row> rows, IEnumerable<Value> values, bool withTotals, int rowLimit)
{
FilterGroups = filterGroups.Where(x => x.IsValid()).ToList();
Rows = rows;
Values = values;
WithTotals = withTotals;
RowLimit = rowLimit;
}
}
@@ -0,0 +1,19 @@
namespace Strata.SqlTools.Snowflake.ExpressionFactory.Query;
public static class QueryConfigExtensions
{
/// <summary>
/// Gets all column ids referenced by this <see cref="QueryConfig"/>
/// </summary>
/// <param name="queryConfig"></param>
/// <returns></returns>
public static int[] GetAllColumnIds(this QueryConfig queryConfig)
{
return queryConfig.Values.SelectMany(value => value.CalculationDataColumnIds)
.Union(queryConfig.Values.SelectMany(x => x.FilterGroups.SelectMany(y => y.Filters.Select(f => f.DataColumnId))))
.Union(queryConfig.Rows.Select(row => row.DataColumnId))
.Union(queryConfig.FilterGroups.SelectMany(filterGroup => filterGroup.Filters.Select(filter => filter.DataColumnId)))
.ToArray();
}
}
@@ -0,0 +1,11 @@
namespace Strata.SqlTools.Snowflake.ExpressionFactory.Query;
public class Row : Field
{
public SortDirection sortDirection { get; set; }
public Row()
{
sortDirection = SortDirection.Asc;
}
}
@@ -0,0 +1,15 @@
namespace Strata.SqlTools.Snowflake.ExpressionFactory.Query;
public enum SortDirection
{
Asc = 0,
Desc = 1
}
public static class SortDirectionExtensions
{
public static string ToSql(this SortDirection sortDirection)
{
return sortDirection.ToString().ToUpper();
}
}
@@ -0,0 +1,13 @@
namespace Strata.SqlTools.Snowflake.ExpressionFactory.Query;
public class SqlResponse
{
public string SqlQuery { get; }
public IEnumerable<KeyValuePair<string, object>> Parameters { get; }
public SqlResponse(string sqlQuery, IEnumerable<KeyValuePair<string, object>> parameters)
{
SqlQuery = sqlQuery;
Parameters = parameters;
}
}
@@ -0,0 +1,28 @@
using System.Text.Json.Serialization;
namespace Strata.SqlTools.Snowflake.ExpressionFactory.Query;
public class Value
{
public string ColumnAlias { get; }
public string Calculation { get; }
public IEnumerable<int> CalculationDataColumnIds { get; }
public IEnumerable<string> AliasedIds { get; }
public IEnumerable<CalculationFilterGroup> FilterGroups { get; }
public Value() : this(string.Empty, string.Empty, new int[0], new string[0], new CalculationFilterGroup[0])
{
// FilterGroups = new List<CalculationFilterGroup>();
}
[JsonConstructor]
public Value(string columnAlias, string calculation, IEnumerable<int> calculationDataColumnIds, IEnumerable<string> aliasedIds, IEnumerable<CalculationFilterGroup> filterGroups)
{
ColumnAlias = columnAlias;
Calculation = calculation;
CalculationDataColumnIds = calculationDataColumnIds ?? Array.Empty<int>();
AliasedIds = aliasedIds ?? aliasedIds ?? Array.Empty<string>();
FilterGroups = filterGroups?.Where(x => x.IsValid()).ToList() ?? new List<CalculationFilterGroup>();
}
}
@@ -0,0 +1,8 @@
namespace Strata.SqlTools.Snowflake.ExpressionFactory.Query;
public class ValueFilter
{
public int DataColumnId { get; set; }
public object? FilterValue { get; set; }
}
@@ -0,0 +1,50 @@
using Strata.SqlTools.SqlBreakdown.Interfaces;
using SqlServerStatementExpressionParser = Strata.SqlTools.Statements.SqlServer.StatementExpressionParser;
namespace Strata.SqlTools.Statements.Snowflake;
/// <summary>
/// Snowflake-specific SQL statement parser that follows Snowflake SQL naming and coding conventions.
/// Extends the base SQL parser to handle Snowflake-specific syntax including double-quoted identifiers
/// and Snowflake naming conventions (typically uppercase).
/// </summary>
public class StatementExpressionParser : SqlServerStatementExpressionParser
{
/// <summary>
/// Creates a Snowflake-specific statement reader for tokenizing SQL.
/// </summary>
/// <param name="sqlStatement">The SQL statement to tokenize.</param>
/// <returns>A Snowflake StatementReader instance.</returns>
protected override IStatementReader CreateStatementReader(string sqlStatement) => new StatementReader(sqlStatement);
/// <summary>
/// Extracts the column ID from a Snowflake token string.
/// Handles both numeric identifiers (e.g., "1_REVENUE") and non-numeric identifiers (e.g., "REVENUE")
/// by using hash codes for non-numeric identifiers.
/// </summary>
/// <param name="columnToken">The column token string.</param>
/// <returns>The extracted or generated column ID.</returns>
protected override int GetColumnIdFromToken(string columnToken)
{
if (char.IsDigit(columnToken[0]))
{
return int.Parse(columnToken.Split('_')[0]);
}
// For non-numeric column identifiers, use a hash code as ID
return Math.Abs(columnToken.GetHashCode());
}
/// <summary>
/// Gets the default column name for unknown column IDs in Snowflake.
/// Snowflake identifiers are typically uppercase by convention.
/// </summary>
/// <param name="columnToken">The column token string.</param>
/// <returns>The column name in uppercase.</returns>
protected override string GetDefaultColumnName(string columnToken)
{
return columnToken.ToUpperInvariant();
}
}
@@ -0,0 +1,156 @@
using System.Text;
using SqlClauses = Strata.SqlTools.SqlBreakdown.Classes.SqlClauses;
using SqlExpressionClause = Strata.SqlTools.SqlBreakdown.Classes.SqlExpressionClause;
using SqlServerStatementParser = Strata.SqlTools.Statements.SqlServer.StatementParser;
using TokenType = Strata.SqlTools.SqlBreakdown.Enums.SQL.TokenType;
namespace Strata.SqlTools.Statements.Snowflake;
/// <summary>
/// Provides Snowflake-specific SQL parsing utilities for normalizing and cleaning Snowflake SQL statements.
/// Extends <see cref="Strata.SqlTools.Statements.SqlServer.StatementParser"/> for common operations and handles Snowflake-specific syntax
/// including double-quoted identifiers, :parameter syntax, QUALIFY and LIMIT keywords.
/// </summary>
public class StatementParser : SqlServerStatementParser
{
#region Constants
// Snowflake-specific keywords
public const string KeywordLimit = "LIMIT";
public const string KeywordQualify = "QUALIFY";
#endregion
#region Clause Extraction Methods
/// <summary>
/// Gets the Snowflake-specific setup keywords.
/// Includes "ALTER SESSION" which is not in standard T-SQL setup patterns.
/// </summary>
/// <returns>Array of Snowflake setup keywords.</returns>
protected override string[] GetSetupKeywords()
=> [.. base.GetSetupKeywords(), .. GetSnowflakeSpecificSetupKeywords()];
/// <summary>
/// Gets Snowflake-specific setup keywords.
/// Includes "ALTER SESSION".
/// </summary>
/// <returns>Array of Snowflake-specific setup keywords.</returns>
/// <remarks>
/// These keywords are specific to Snowflake and are not part of standard T-SQL setup clauses.
/// </remarks>
private static string[] GetSnowflakeSpecificSetupKeywords()
=> ["ALTER SESSION"];
/// <summary>
/// Gets the Snowflake-specific finish clause pattern.
/// Includes "DROP STAGE" which is Snowflake-specific.
/// </summary>
/// <returns>Regex pattern for Snowflake finish clauses.</returns>
protected override string GetFinishClausePattern()
{
return @";\s*(DROP\s+TABLE|DROP\s+VIEW|DROP\s+STAGE)";
}
#endregion
#region SELECT Statement Parsing
/// <summary>
/// Gets the array of SQL keywords to search for in Snowflake statements.
/// Includes Snowflake-specific LIMIT and QUALIFY keywords.
/// </summary>
/// <returns>Array of keywords to find.</returns>
protected override string[] GetKeywordsToFind()
=> [.. base.GetKeywordsToFind(), .. GetSnowflakeSpecificKeywords()];
/// <summary>
/// Gets Snowflake-specific keywords.
/// Includes "LIMIT" and "QUALIFY".
/// </summary>
/// <returns>Array of Snowflake-specific keywords.</returns>
/// <remarks>
/// These keywords are specific to Snowflake and are not part of standard T-SQL clauses.
/// </remarks>
private static string[] GetSnowflakeSpecificKeywords()
=> [KeywordLimit, KeywordQualify];
/// <summary>
/// Determines whether a character can start a word (keyword or identifier).
/// Snowflake: Letters or underscores can start identifiers.
/// </summary>
/// <param name="c">The character to check.</param>
/// <returns>True if the character is a letter or underscore.</returns>
protected override bool IsWordStartCharacter(char c) => char.IsLetter(c) || c == '_';
/// <summary>
/// Handles double-quote character during tokenization.
/// Snowflake: Treats double-quote as identifier (not string literal).
/// </summary>
/// <param name="sql">The SQL statement being tokenized.</param>
/// <param name="position">Current position in the SQL string.</param>
/// <returns>Token and new position after the token.</returns>
protected override ((TokenType type, string value, int position) token, int newPosition) HandleDoubleQuote(string sql, int position)
{
// Snowflake: double-quote is an identifier (like [brackets])
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);
}
/// <summary>
/// Post-processes extracted clauses to handle Snowflake-specific LIMIT clause.
/// Appends LIMIT to ORDER BY clause as per Snowflake syntax.
/// </summary>
/// <param name="clauses">The extracted clauses to post-process.</param>
/// <param name="sql">The original SQL statement.</param>
/// <param name="clausePositions">Dictionary of keyword positions.</param>
protected override void PostProcessClauses(SqlClauses clauses, string sql, Dictionary<string, int> clausePositions)
{
// Snowflake-specific: Append LIMIT to ORDER BY if present
if (clausePositions.ContainsKey(KeywordLimit))
{
var limitClause = sql.Substring(clausePositions[KeywordLimit]).Trim();
if (clauses.OrderByClause != null)
{
clauses.OrderByClause.Clause = string.IsNullOrEmpty(clauses.OrderByClause.Clause)
? limitClause
: $"{clauses.OrderByClause.Clause} {limitClause}";
}
else
{
clauses.OrderByClause = new SqlExpressionClause(splitOnComma: true) { Clause = limitClause };
}
}
}
#endregion
#region Parameter Extraction
/// <summary>
/// Extracts Snowflake parameters from SQL and populates the parameter dictionary.
/// Snowflake-specific: Searches for :paramName (Snowflake native) syntax only.
/// </summary>
/// <param name="parameters">The parameter dictionary to populate.</param>
/// <param name="sql">The SQL statement to extract parameters from.</param>
public override void ExtractParameters(Dictionary<string, object> parameters, string sql)
{
// Extract Snowflake native :param format only
ExtractParameters(parameters, sql, @":([a-zA-Z_][a-zA-Z0-9_]*)");
}
#endregion
}
@@ -0,0 +1,58 @@
using Strata.SqlTools.SqlBreakdown.Classes;
using Strata.SqlTools.SqlBreakdown.Enums.SQL;
using Strata.SqlTools.SqlBreakdown.Exceptions;
using SqlServerStatementReader = Strata.SqlTools.Statements.SqlServer.StatementReader;
namespace Strata.SqlTools.Statements.Snowflake;
/// <summary>
/// Snowflake-specific tokenizer class that reads a string representation of a Snowflake SQL statement
/// and parses out each part as a token. Handles Snowflake's double-quoted identifiers and naming conventions.
/// </summary>
public class StatementReader : SqlServerStatementReader
{
public StatementReader(string sqlStatement) : base(sqlStatement)
{
}
/// <summary>
/// Handles Snowflake-specific characters: double-quotes (") for delimited identifiers.
/// </summary>
/// <returns>True if the character was handled; false otherwise.</returns>
protected override bool TryHandleAdditionalCharacter()
{
if (CurrentCharacter == '"')
{
// Snowflake uses double quotes for delimited identifiers
MovePosition();
var quotedIdentifier = GrabStringValue();
_currentToken = new Token(TokenType.ColumnIdentifier, quotedIdentifier);
if (CurrentCharacter != '"')
{
throw new InvalidSyntaxException(
$"Invalid syntax at position {Position}. Expected closing double quote.");
}
MovePosition();
return true;
}
return false;
}
/// <summary>
/// Handles Snowflake-specific identifier prefixes: underscores (_) can start identifiers.
/// </summary>
/// <returns>True if the character was handled; false otherwise.</returns>
protected override bool TryHandleIdentifierPrefix()
{
if (CurrentCharacter == '_')
{
var underscoreIdentifier = GrabStringValue();
_currentToken = new Token(TokenType.ColumnIdentifier, underscoreIdentifier);
return true;
}
return false;
}
}
@@ -0,0 +1,46 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<!-- NuGet Package Metadata -->
<PackageId>Strata.SqlTools.Snowflake</PackageId>
<Version>1.0.0</Version>
<Authors>Strata Decision Technology</Authors>
<Company>Strata Decision Technology</Company>
<Product>Strata SQL Utilities - Snowflake</Product>
<Description>Snowflake SQL specific implementations for Strata.SqlTools, including query breakdown, statement parsing, and SQL generation for Snowflake SQL dialect with support for both :parameter and @parameter syntax.</Description>
<PackageTags>snowflake;sql;query-builder;sql-parser;database;snowflake-sql</PackageTags>
<PackageProjectUrl>https://github.com/stratadecision/sql-builder</PackageProjectUrl>
<RepositoryUrl>https://github.com/stratadecision/sql-builder</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageReleaseNotes>Initial release with Snowflake SQL query parsing, generation, and breakdown support.</PackageReleaseNotes>
<Copyright>Copyright © Strata Decision Technology 2024-2026</Copyright>
<!-- Build Configuration -->
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>symbols.nupkg</SymbolPackageFormat>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<ContinuousIntegrationBuild Condition="'$(CI)' == 'true'">true</ContinuousIntegrationBuild>
<!-- Code Analysis -->
<EnableNETAnalyzers>true</EnableNETAnalyzers>
<AnalysisLevel>latest</AnalysisLevel>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
</PropertyGroup>
<ItemGroup>
<None Include="..\..\README.md" Pack="true" PackagePath="\" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Strata.SqlTools.SqlServer\Strata.SqlTools.SqlServer.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,51 @@
using Strata.SqlTools.SqlBreakdown.Expressions.Conditional;
using SqlServerCommandVisitor = Strata.SqlTools.Visitors.SqlServer.CommandVisitor;
namespace Strata.SqlTools.Visitors.Snowflake;
/// <summary>
/// Implements the visitor pattern to convert SQL expression objects into Snowflake-compatible SQL command strings.
/// Inherits from SqlServer.CommandVisitor and overrides only the dialect-specific formatting methods.
/// </summary>
public class CommandVisitor : SqlServerCommandVisitor
{
/// <summary>
/// Formats an identifier for Snowflake (no special quoting required for standard identifiers).
/// Returns the identifier without brackets or quotes.
/// </summary>
/// <param name="identifier">The identifier to format.</param>
/// <returns>The unquoted identifier.</returns>
protected override string FormatIdentifier(string identifier) => identifier;
/// <summary>
/// Formats a parameter name for Snowflake using colon prefix.
/// </summary>
/// <param name="parameterName">The parameter name to format.</param>
/// <returns>A SQL string in the format ":parameterName".</returns>
protected override string FormatParameterName(string parameterName) => $":{parameterName}";
/// <summary>
/// Formats a boolean literal for Snowflake using TRUE/FALSE keywords.
/// </summary>
/// <param name="value">The boolean value to format.</param>
/// <returns>The string "TRUE" or "FALSE" in uppercase.</returns>
protected override string FormatBooleanLiteral(bool value) => value.ToString().ToUpper();
/// <summary>
/// Formats a string literal for Snowflake (no special escaping shown in original implementation).
/// </summary>
/// <param name="value">The string value to format.</param>
/// <returns>A SQL string literal enclosed in single quotes.</returns>
protected override string FormatStringLiteral(string value) => $"'{value}'";
/// <summary>
/// Formats a case-insensitive LIKE expression for Snowflake using ILIKE keyword.
/// </summary>
/// <param name="likeExpression">The LIKE expression to format.</param>
/// <returns>A SQL string in the format "expression ILIKE pattern".</returns>
protected override string FormatCaseInsensitiveLike(LikeExpression likeExpression)
{
return $"{likeExpression.Subject.Accept(this)} ILIKE {likeExpression.Pattern.Accept(this)}";
}
}