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>
220 lines
8.2 KiB
C#
220 lines
8.2 KiB
C#
using System.Collections;
|
|
using System.Text;
|
|
using Strata.SqlTools.SqlBreakdown.Classes;
|
|
using Strata.SqlTools.Statements.SqlServer;
|
|
|
|
namespace Strata.SqlTools.Breakdowns.SqlServer;
|
|
|
|
/// <summary>
|
|
/// Represents a DELETE SQL statement breakdown with FROM and WHERE clauses for SQL Server.
|
|
/// </summary>
|
|
public class DeleteBreakdown : SqlBreakdownBase
|
|
{
|
|
protected readonly StatementParser Parser;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="DeleteBreakdown"/> class.
|
|
/// </summary>
|
|
public DeleteBreakdown()
|
|
{
|
|
Parser = new StatementParser();
|
|
FromClause = new SqlClause();
|
|
WhereClause = new SqlClause();
|
|
DeleteClause = new SqlClause();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="DeleteBreakdown"/> class.
|
|
/// </summary>
|
|
/// <param name="fromClause">The FROM clause.</param>
|
|
/// <param name="whereClause">The WHERE clause.</param>
|
|
public DeleteBreakdown(string fromClause, string whereClause) : this()
|
|
{
|
|
var cleanFrom = Parser.ExtractSqlComments(fromClause, out var fromComments);
|
|
FromClause.Clause = cleanFrom.Trim();
|
|
FromClause.Comment = fromComments.Count > 0 ? string.Join(" ", fromComments) : null;
|
|
|
|
var cleanWhere = Parser.ExtractSqlComments(whereClause, out var whereComments);
|
|
WhereClause.Clause = cleanWhere.Trim();
|
|
WhereClause.Comment = whereComments.Count > 0 ? string.Join(" ", whereComments) : null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the FROM clause.
|
|
/// </summary>
|
|
public SqlClause FromClause { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the DELETE clause (optional, for DELETE with alias).
|
|
/// </summary>
|
|
public SqlClause DeleteClause { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets a value indicating whether a WHERE clause is being used.
|
|
/// </summary>
|
|
public bool IsUsingWhereClause => !string.IsNullOrWhiteSpace(WhereClause.Clause);
|
|
|
|
/// <summary>
|
|
/// Gets or sets the WHERE clause.
|
|
/// </summary>
|
|
public SqlClause WhereClause { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets the SQL breakdown as a string.
|
|
/// </summary>
|
|
/// <returns>The DELETE SQL statement.</returns>
|
|
protected override string GetSqlBreakdown()
|
|
{
|
|
var sb = new StringBuilder();
|
|
|
|
sb.AppendLine("DELETE ");
|
|
if (!string.IsNullOrWhiteSpace(DeleteClause.Clause))
|
|
{
|
|
sb.AppendLine($" {DeleteClause.Clause}");
|
|
}
|
|
|
|
sb.AppendLine("FROM ");
|
|
sb.AppendLine($" {FromClause.Clause}");
|
|
|
|
if (IsUsingWhereClause)
|
|
{
|
|
sb.AppendLine("WHERE ");
|
|
sb.AppendLine($" {WhereClause.Clause}");
|
|
}
|
|
|
|
return sb.ToString();
|
|
}
|
|
|
|
#region Parse Methods
|
|
|
|
/// <summary>
|
|
/// Parses a DELETE SQL statement into a DeleteBreakdown object.
|
|
/// </summary>
|
|
/// <param name="sql">The DELETE SQL statement to parse.</param>
|
|
/// <returns>A DeleteBreakdown 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 DeleteBreakdown 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 DELETE statement: {error}");
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Attempts to parse a DELETE SQL statement into a DeleteBreakdown object.
|
|
/// </summary>
|
|
/// <param name="sql">The DELETE SQL statement to parse.</param>
|
|
/// <param name="result">When this method returns, contains the parsed DeleteBreakdown 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 DeleteBreakdown result)
|
|
=> TryParse(sql, out result, out _);
|
|
|
|
/// <summary>
|
|
/// Attempts to parse a DELETE SQL statement into a DeleteBreakdown object.
|
|
/// </summary>
|
|
/// <param name="sql">The DELETE SQL statement to parse.</param>
|
|
/// <param name="result">When this method returns, contains the parsed DeleteBreakdown 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 DeleteBreakdown result, out string errorMessage)
|
|
{
|
|
result = null!;
|
|
errorMessage = null!;
|
|
|
|
try
|
|
{
|
|
return TryParseCore(sql, out result, out errorMessage);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
errorMessage = $"Unexpected error during parsing: {ex.Message}";
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private static bool TryParseCore(string sql, out DeleteBreakdown result, out string errorMessage)
|
|
{
|
|
result = null!;
|
|
errorMessage = null!;
|
|
|
|
var parser = new StatementParser();
|
|
if (!Strata.SqlTools.Statements.SqlServer.ParsePreparation.TryRunPrelude(
|
|
sql, parser, @"^\s*DELETE\b", "DELETE",
|
|
out sql, out var setupClauses, out var finishClauses, out errorMessage))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Parse DELETE statement using regex
|
|
// Pattern: DELETE [table_alias] FROM table WHERE condition
|
|
var deleteMatch = System.Text.RegularExpressions.Regex.Match(sql,
|
|
@"DELETE\s+(.*?)\s+FROM\s+(.*?)(?:\s+WHERE\s+(.*))?$",
|
|
System.Text.RegularExpressions.RegexOptions.IgnoreCase | System.Text.RegularExpressions.RegexOptions.Singleline, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout);
|
|
|
|
if (deleteMatch.Success)
|
|
{
|
|
var deleteClause = deleteMatch.Groups[1].Value.Trim();
|
|
var fromClause = deleteMatch.Groups[2].Value.Trim();
|
|
var whereClause = deleteMatch.Groups.Count > 3 ? deleteMatch.Groups[3].Value.Trim() : string.Empty;
|
|
|
|
result = new DeleteBreakdown(fromClause, whereClause)
|
|
{
|
|
SetupClauses = setupClauses,
|
|
FinishClauses = finishClauses
|
|
};
|
|
|
|
ApplyDeleteClauseComments(result, parser, deleteClause);
|
|
return true;
|
|
}
|
|
|
|
// Try simpler pattern: DELETE FROM table WHERE condition
|
|
deleteMatch = System.Text.RegularExpressions.Regex.Match(sql,
|
|
@"DELETE\s+FROM\s+(.*?)(?:\s+WHERE\s+(.*))?$",
|
|
System.Text.RegularExpressions.RegexOptions.IgnoreCase | System.Text.RegularExpressions.RegexOptions.Singleline, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout);
|
|
|
|
if (!deleteMatch.Success)
|
|
{
|
|
errorMessage = "Could not parse DELETE statement. Expected format: DELETE [alias] FROM table [WHERE condition]";
|
|
return false;
|
|
}
|
|
|
|
var simpleFromClause = deleteMatch.Groups[1].Value.Trim();
|
|
var simpleWhereClause = deleteMatch.Groups.Count > 2 ? deleteMatch.Groups[2].Value.Trim() : string.Empty;
|
|
|
|
result = new DeleteBreakdown(simpleFromClause, simpleWhereClause)
|
|
{
|
|
SetupClauses = setupClauses,
|
|
FinishClauses = finishClauses
|
|
};
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Extracts inline comments from the DELETE clause text and applies the cleaned value and
|
|
/// combined comment to <paramref name="result"/>. No-op when the clause is blank.
|
|
/// </summary>
|
|
private static void ApplyDeleteClauseComments(DeleteBreakdown result, StatementParser parser, string deleteClause)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(deleteClause))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var cleanDelete = parser.ExtractSqlComments(deleteClause, out var deleteComments);
|
|
result.DeleteClause.Clause = cleanDelete.Trim();
|
|
result.DeleteClause.Comment = deleteComments.Count > 0 ? string.Join(" ", deleteComments) : null;
|
|
}
|
|
|
|
#endregion
|
|
}
|