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; /// /// Represents a Snowflake stored procedure call breakdown with procedure name and parameters. /// public class ProcedureBreakdown : SqlServerProcedureBreakdown { private static readonly StatementParser SnowflakeParserInstance = new StatementParser(); /// /// Initializes a new instance of the class. /// public ProcedureBreakdown() : base() { } /// /// Initializes a new instance of the class. /// /// The stored procedure name. /// If true, uses Microsoft T-SQL parsing rules. If false, uses Snowflake parsing rules. Defaults to false. 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; } /// /// Initializes a new instance of the class. /// /// The stored procedure name. /// The parameters dictionary (parameter name -> value expression). /// If true, uses Microsoft T-SQL parsing rules. If false, uses Snowflake parsing rules. Defaults to false. public ProcedureBreakdown(string procedureName, Dictionary parameters, bool isMicrosoftSql = false) : this(procedureName, isMicrosoftSql) { Parameters = parameters ?? []; } /// /// Gets the SQL breakdown as a string for Snowflake. /// /// The CALL SQL statement (Snowflake uses CALL instead of EXEC). protected override string GetSqlBreakdown() { var sb = new StringBuilder(); sb.Append("CALL "); sb.Append(ProcedureName.Clause); sb.Append('('); if (IsUsingParameters) { var paramList = new List(); 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 /// /// Parses a Snowflake CALL SQL statement into a ProcedureBreakdown object. /// /// The CALL SQL statement to parse. /// If true, uses Microsoft T-SQL parsing rules. If false, uses Snowflake parsing rules. Defaults to false. /// A ProcedureBreakdown object representing the parsed statement. /// Thrown when sql is null or empty. /// Thrown when the SQL statement cannot be parsed. 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; } /// /// Attempts to parse a Snowflake CALL SQL statement into a ProcedureBreakdown object. /// /// The CALL SQL statement to parse. /// When this method returns, contains the parsed ProcedureBreakdown if successful, or null if parsing failed. /// If true, uses Microsoft T-SQL parsing rules instead of Snowflake rules. Defaults to false. /// true if the SQL was successfully parsed; otherwise, false. public static bool TryParse(string sql, out ProcedureBreakdown result, bool isMicrosoftSql = false) => TryParse(sql, out result, out _, isMicrosoftSql); /// /// Attempts to parse a Snowflake CALL SQL statement into a ProcedureBreakdown object. /// Handles Snowflake-specific syntax including CALL procedureName(param >= value). /// /// The CALL SQL statement to parse. /// When this method returns, contains the parsed ProcedureBreakdown if successful, or null if parsing failed. /// When this method returns false, contains a message describing why parsing failed. /// If true, uses Microsoft T-SQL parsing rules instead of Snowflake rules. Defaults to false. /// true if the SQL was successfully parsed; otherwise, false. public static bool TryParse(string sql, out ProcedureBreakdown result, out string errorMessage, bool isMicrosoftSql = false) { result = null!; errorMessage = null!; try { return TryParseCore(sql, out result, out errorMessage, isMicrosoftSql); } catch (Exception ex) { errorMessage = $"Unexpected error during parsing: {ex.Message}"; return false; } } private static bool TryParseCore(string sql, out ProcedureBreakdown result, out string errorMessage, bool isMicrosoftSql) { result = null!; errorMessage = null!; if (string.IsNullOrWhiteSpace(sql)) { errorMessage = "SQL statement cannot be null or empty."; return false; } // If Microsoft SQL mode, delegate to base class if (isMicrosoftSql) { return TryParseMicrosoftSql(sql, out result, out errorMessage); } 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, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout)) { errorMessage = "SQL statement must start with CALL, EXEC, or EXECUTE."; return false; } // Extract setup and finish clauses var setupClauses = new List(); 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, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout); 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; result = new ProcedureBreakdown(procedureName, ParseCallParameters(parametersText), isMicrosoftSql: false) { SetupClauses = setupClauses, FinishClauses = finishClauses }; return true; } /// /// Parses Microsoft T-SQL CALL/EXEC syntax by delegating to the SQL Server breakdown, /// then projecting the result onto a Snowflake . /// private static bool TryParseMicrosoftSql(string sql, out ProcedureBreakdown result, out string errorMessage) { result = null!; if (!SqlServerProcedureBreakdown.TryParse(sql, out var baseResult, out errorMessage)) { return false; } result = new ProcedureBreakdown { ProcedureName = baseResult.ProcedureName, Parameters = baseResult.Parameters, SetupClauses = baseResult.SetupClauses, FinishClauses = baseResult.FinishClauses }; return true; } /// /// Parses the parameter list of a Snowflake CALL statement. Supports named (param => value) /// syntax, falling back to positional values when no named parameters are present. /// private static Dictionary ParseCallParameters(string parametersText) { var parameters = new Dictionary(); if (string.IsNullOrWhiteSpace(parametersText)) { return parameters; } // Parse parameters - Snowflake uses param => value syntax var paramMatches = System.Text.RegularExpressions.Regex.Matches(parametersText, @"(\w+)\s*=>\s*([^,]+)(?:,|$)", System.Text.RegularExpressions.RegexOptions.IgnoreCase, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout); foreach (var groups in paramMatches.Cast().Select(paramMatch => paramMatch.Groups)) { // Store with @ prefix for consistency with SQL Server parameters["@" + groups[1].Value.Trim()] = groups[2].Value.Trim(); } // If no named parameters found, try positional parameters (just values) if (parameters.Count == 0) { var positionalParams = parametersText.Split(','); for (int i = 0; i < positionalParams.Length; i++) { parameters[$"@param{i + 1}"] = positionalParams[i].Trim(); } } return parameters; } #endregion }