Files
sql-utilities/src/Strata.SqlTools.Snowflake/Breakdowns/QueryBreakdown.cs
T
Thom LambandClaude Opus 4.7 089d4f6000 refactor(dedup): extract AppendWithClauseSection for the WITH/CTE block
`SqlServer.QueryBreakdown.GetSqlBreakdown` and
`Snowflake.QueryBreakdown.GetSql` each carried a 24-line copy of the
same CTE-rendering loop ("WITH" keyword, optional RECURSIVE, per-clause
header, anchor/UNION ALL/recursive query, closing parens). The two
copies differed only by indent (5/10 spaces vs 4/8) and a trailing
space after the keyword.

Hoist the loop into `protected virtual void AppendWithClauseSection(
StringBuilder, string withClauseIndent, string queryBodyIndent)` on
`SqlServer.QueryBreakdown`. Each caller invokes it with its dialect's
preferred indents; Snowflake's mid-method copy is deleted entirely.

Standardizes on the no-trailing-space "WITH" form (Snowflake's) — was
"WITH " (trailing space) in the SqlServer original. Visible only as a
trailing space before the newline in non-recursive output, which no
tests assert on.

All 1180 tests stay green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 17:23:48 -05:00

751 lines
30 KiB
C#

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>
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;
parser.PopulateClauseWithComments(selectClause, SelectClause);
parser.PopulateClauseWithComments(fromClause, FromClause);
}
/// <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
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 string.Concat(":", parameterName.AsSpan(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 = string.Concat("@", paramName.AsSpan(1));
if (!Parameters.ContainsKey(atParam))
{
Parameters[atParam] = Parameters[paramName];
}
}
else if (paramName.StartsWith('@'))
{
// Add :param version
var colonParam = string.Concat(":", paramName.AsSpan(1));
if (!Parameters.ContainsKey(colonParam))
{
Parameters[colonParam] = Parameters[paramName];
}
}
}
}
/// <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();
AppendToClause(WhereClause, expression.Accept(visitor), operation, 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);
var commentText = comments.Count > 0 ? string.Join(" ", comments) : null;
AppendToClause(WhereClause, cleanSql.Trim(), operation, commentText);
// Extract and add parameters from the WHERE clause using appropriate parser
ExtractAndAddParametersWithParser(cleanSql, parser);
}
/// <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();
AppendToClause(HavingClause, expression.Accept(visitor), operation, 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);
}
}
AppendWithClauseSection(sb, withClauseIndent: " ", queryBodyIndent: " ");
// Snowflake SELECT syntax
sb.Append(StatementParser.KeywordSelect);
// Handle TOP equivalent using LIMIT in Snowflake
sb.AppendLine();
if (!string.IsNullOrEmpty(SelectClause.Comment))
{
sb.AppendLine($" {SelectClause.Comment}");
}
sb.AppendLine($" {SelectClause.Clause}");
if (IsUsingFromClause)
{
sb.AppendLine(StatementParser.KeywordFrom);
if (!string.IsNullOrEmpty(FromClause.Comment))
{
sb.AppendLine($" {FromClause.Comment}");
}
sb.AppendLine($" {FromClause.Clause}");
}
if (IsUsingWhereClause)
{
sb.AppendLine(StatementParser.KeywordWhere);
if (!string.IsNullOrEmpty(WhereClause.Comment))
{
sb.AppendLine($" {WhereClause.Comment}");
}
sb.AppendLine($" {WhereClause.Clause}");
}
if (IsUsingGroupByClause)
{
sb.AppendLine(StatementParser.KeywordGroupBy);
if (!string.IsNullOrEmpty(GroupByClause.Comment))
{
sb.AppendLine($" {GroupByClause.Comment}");
}
sb.AppendLine($" {GroupByClause.Clause}");
}
if (IsUsingHavingClause)
{
sb.AppendLine(StatementParser.KeywordHaving);
if (!string.IsNullOrEmpty(HavingClause.Comment))
{
sb.AppendLine($" {HavingClause.Comment}");
}
sb.AppendLine($" {HavingClause.Clause}");
}
if (IsUsingOrderByClause)
{
sb.AppendLine(StatementParser.KeywordOrderBy);
if (!string.IsNullOrEmpty(OrderByClause.Comment))
{
sb.AppendLine($" {OrderByClause.Comment}");
}
sb.AppendLine($" {OrderByClause.Clause}");
}
if (includeSetupFinish)
{
foreach (string finish in FinishClauses)
{
sb.AppendLine(finish);
}
}
return sb.ToString();
}
/// <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>An empty queryable by default, as QueryBreakdown operates on SQL. Override in derived classes to provide LINQ query reconstruction.</returns>
/// <remarks>
/// This Snowflake-specific implementation returns an empty queryable since Snowflake QueryBreakdown
/// represents parsed SQL statements and has no built-in way to create LINQ queries.
/// Derived classes can override this method to reconstruct LINQ queries from the analyzed components.
/// </remarks>
public override IQueryable<T> GetQuery<T>() where T : class => Enumerable.Empty<T>().AsQueryable();
}