`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>
751 lines
30 KiB
C#
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();
|
|
}
|
|
|
|
|
|
|