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