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