chore: initial git load of code space
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user