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
}