using System.Collections; using System.Text; using System.Text.RegularExpressions; using Strata.SqlTools.SqlBreakdown.Classes; using Strata.SqlTools.Statements.SqlServer; namespace Strata.SqlTools.Breakdowns.SqlServer; /// /// Represents a SQL Server stored procedure call breakdown with procedure name and parameters. /// public class ProcedureBreakdown : SqlBreakdownBase { protected readonly StatementParser Parser; /// /// Initializes a new instance of the class. /// public ProcedureBreakdown() { Parser = new StatementParser(); ProcedureName = new SqlClause(); Parameters = []; } /// /// Initializes a new instance of the class. /// /// The stored procedure name. public ProcedureBreakdown(string procedureName) : this() { 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). public ProcedureBreakdown(string procedureName, Dictionary parameters) : this(procedureName) { Parameters = parameters ?? []; } /// /// Gets or sets the stored procedure name. /// public SqlClause ProcedureName { get; set; } /// /// Gets or sets the parameters dictionary (parameter name -> value expression). /// public Dictionary Parameters { get; set; } /// /// Gets a value indicating whether parameters are being used. /// #pragma warning disable S2325 // Methods and properties that don't access instance data should be static public bool IsUsingParameters => Parameters.Count > 0; #pragma warning restore S2325 /// /// Adds a parameter to the stored procedure call. /// /// The parameter name (with or without @). /// The value expression or literal. #pragma warning disable S2325 // Methods and properties that don't access instance data should be static public void AddParameter(string parameterName, string valueExpression) #pragma warning restore S2325 { if (string.IsNullOrWhiteSpace(parameterName)) { throw new ArgumentNullException(nameof(parameterName)); } // Ensure parameter name starts with @ if (!parameterName.StartsWith('@')) { parameterName = "@" + parameterName; } Parameters[parameterName] = valueExpression; } /// /// Gets the SQL breakdown as a string. /// /// The EXECUTE/EXEC SQL statement. protected override string GetSqlBreakdown() { var sb = new StringBuilder(); sb.Append("EXEC "); sb.Append(ProcedureName.Clause); if (IsUsingParameters) { sb.AppendLine(); var paramList = new List(); foreach (var param in Parameters) { paramList.Add($" {param.Key} = {param.Value}"); } sb.Append(string.Join($",{Environment.NewLine}", paramList)); } return sb.ToString(); } #region Parse Methods /// /// Parses an EXEC/EXECUTE SQL statement into a ProcedureBreakdown object. /// /// The EXEC/EXECUTE SQL statement to parse. /// 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) { 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)) { throw new FormatException($"Failed to parse EXEC statement: {error}"); } return result; } /// /// Attempts to parse an EXEC/EXECUTE SQL statement into a ProcedureBreakdown object. /// /// The EXEC/EXECUTE SQL statement to parse. /// When this method returns, contains the parsed ProcedureBreakdown if successful, or null if parsing failed. /// true if the SQL was successfully parsed; otherwise, false. public static bool TryParse(string sql, out ProcedureBreakdown result) => TryParse(sql, out result, out _); /// /// Attempts to parse an EXEC/EXECUTE SQL statement into a ProcedureBreakdown object. /// /// The EXEC/EXECUTE 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. /// true if the SQL was successfully parsed; otherwise, false. public static bool TryParse(string sql, out ProcedureBreakdown result, out string errorMessage) { result = null!; errorMessage = null!; try { if (!ParsePreparation.TryRunPrelude( sql, new StatementParser(), @"^\s*(EXEC|EXECUTE)\b", "EXEC or EXECUTE", out sql, out var setupClauses, out var finishClauses, out errorMessage)) { return false; } // Parse EXEC statement - match procedure name and parameters // Pattern: EXEC[UTE] procedureName [@param = value, ...] var execMatch = Regex.Match(sql, @"(?:EXEC|EXECUTE)\s+([^\s@,]+)(?:\s+(.*))?$", RegexOptions.IgnoreCase | RegexOptions.Singleline, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout); if (!execMatch.Success) { errorMessage = "Could not parse EXEC statement. Expected format: EXEC procedureName [@param = value, ...]"; return false; } var procedureName = execMatch.Groups[1].Value.Trim(); var parametersText = execMatch.Groups.Count > 2 ? execMatch.Groups[2].Value.Trim() : string.Empty; var parameters = new Dictionary(); if (!string.IsNullOrWhiteSpace(parametersText)) { // Parse parameters - handle both @param = value and positional parameters var paramMatches = Regex.Matches(parametersText, @"(@\w+)\s*=\s*([^,]+)(?:,|$)", RegexOptions.IgnoreCase, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout); parameters = paramMatches .Cast() .ToDictionary( paramMatch => paramMatch.Groups[1].Value.Trim(), paramMatch => paramMatch.Groups[2].Value.Trim() ); } result = new ProcedureBreakdown(procedureName, parameters) { SetupClauses = setupClauses, FinishClauses = finishClauses }; return true; } catch (Exception ex) { errorMessage = $"Unexpected error during parsing: {ex.Message}"; return false; } } #endregion }