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
}