The standard `TryParse(...)` prelude — null/empty check, parser construction, comment-preserving normalize, statement-prefix regex validation, setup/finish clause extraction — was copy-pasted in **eight** breakdown classes across the SqlServer and Snowflake dialects. Sonar flagged it as a six-way duplicate cluster on the shorter (~17-line) common block, and as additional pairwise duplicates on the longer (~30-line) version. Introduces `Strata.SqlTools.Statements.SqlServer.ParsePreparation` with a single `TryRunPrelude(sql, parser, prefixRegex, prefixDescription, out ...)` method. Each `TryParse` now calls it once and proceeds straight to dialect-specific match logic. Touched callers: - `SqlServer.InsertBreakdown`, `SqlServer.DeleteBreakdown`, `SqlServer.UpdateBreakdown`, `SqlServer.ProcedureBreakdown` - `Snowflake.InsertBreakdown`, `Snowflake.DeleteBreakdown`, `Snowflake.UpdateBreakdown`, `Snowflake.ProcedureBreakdown` The Microsoft-SQL fallback path in the Snowflake breakdowns (which delegates to the SqlServer breakdown's TryParse before the prelude even runs) is preserved unchanged. `ParsePreparation` is `public` because it sits in the SqlServer assembly and is consumed cross-assembly by Snowflake/PostgreSql. This is a new public type but it's deliberately a thin scaffold — external consumers should still be calling the breakdown classes' own `TryParse` methods. All 1180 tests stay green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
215 lines
8.1 KiB
C#
215 lines
8.1 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// Represents a SQL Server stored procedure call breakdown with procedure name and parameters.
|
|
/// </summary>
|
|
public class ProcedureBreakdown : SqlBreakdownBase
|
|
{
|
|
protected readonly StatementParser Parser;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="ProcedureBreakdown"/> class.
|
|
/// </summary>
|
|
public ProcedureBreakdown()
|
|
{
|
|
Parser = new StatementParser();
|
|
ProcedureName = new SqlClause();
|
|
Parameters = [];
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="ProcedureBreakdown"/> class.
|
|
/// </summary>
|
|
/// <param name="procedureName">The stored procedure name.</param>
|
|
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;
|
|
}
|
|
|
|
/// <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>
|
|
public ProcedureBreakdown(string procedureName, Dictionary<string, string> parameters) : this(procedureName)
|
|
{
|
|
Parameters = parameters ?? [];
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the stored procedure name.
|
|
/// </summary>
|
|
public SqlClause ProcedureName { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the parameters dictionary (parameter name -> value expression).
|
|
/// </summary>
|
|
public Dictionary<string, string> Parameters { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets a value indicating whether parameters are being used.
|
|
/// </summary>
|
|
#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
|
|
|
|
/// <summary>
|
|
/// Adds a parameter to the stored procedure call.
|
|
/// </summary>
|
|
/// <param name="parameterName">The parameter name (with or without @).</param>
|
|
/// <param name="valueExpression">The value expression or literal.</param>
|
|
#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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the SQL breakdown as a string.
|
|
/// </summary>
|
|
/// <returns>The EXECUTE/EXEC SQL statement.</returns>
|
|
protected override string GetSqlBreakdown()
|
|
{
|
|
var sb = new StringBuilder();
|
|
|
|
sb.Append("EXEC ");
|
|
sb.Append(ProcedureName.Clause);
|
|
|
|
if (IsUsingParameters)
|
|
{
|
|
sb.AppendLine();
|
|
var paramList = new List<string>();
|
|
foreach (var param in Parameters)
|
|
{
|
|
paramList.Add($" {param.Key} = {param.Value}");
|
|
}
|
|
sb.Append(string.Join($",{Environment.NewLine}", paramList));
|
|
}
|
|
|
|
return sb.ToString();
|
|
}
|
|
|
|
#region Parse Methods
|
|
|
|
/// <summary>
|
|
/// Parses an EXEC/EXECUTE SQL statement into a ProcedureBreakdown object.
|
|
/// </summary>
|
|
/// <param name="sql">The EXEC/EXECUTE SQL statement to parse.</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)
|
|
{
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Attempts to parse an EXEC/EXECUTE SQL statement into a ProcedureBreakdown object.
|
|
/// </summary>
|
|
/// <param name="sql">The EXEC/EXECUTE SQL statement to parse.</param>
|
|
/// <param name="result">When this method returns, contains the parsed ProcedureBreakdown if successful, or null if parsing failed.</param>
|
|
/// <returns>true if the SQL was successfully parsed; otherwise, false.</returns>
|
|
public static bool TryParse(string sql, out ProcedureBreakdown result)
|
|
=> TryParse(sql, out result, out _);
|
|
|
|
/// <summary>
|
|
/// Attempts to parse an EXEC/EXECUTE SQL statement into a ProcedureBreakdown object.
|
|
/// </summary>
|
|
/// <param name="sql">The EXEC/EXECUTE 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>
|
|
/// <returns>true if the SQL was successfully parsed; otherwise, false.</returns>
|
|
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<string, string>();
|
|
|
|
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<System.Text.RegularExpressions.Match>()
|
|
.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
|
|
}
|