refactor(dedup): share TryParse prelude across breakdown families (Cluster D)

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>
This commit is contained in:
Thom Lamb
2026-05-27 16:33:43 -05:00
co-authored by Claude Opus 4.7
parent 4b5348c53a
commit c121dfa611
9 changed files with 93 additions and 165 deletions
@@ -111,12 +111,6 @@ public class DeleteBreakdown : SqlServerDeleteBreakdown
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)
{
@@ -138,25 +132,13 @@ public class DeleteBreakdown : SqlServerDeleteBreakdown
return true;
}
var parser = SnowflakeParserInstance;
sql = parser.NormalizeSqlPreservingComments(sql);
// Check if it's a DELETE statement
var sqlTrimmed = sql.TrimStart();
if (!System.Text.RegularExpressions.Regex.IsMatch(sqlTrimmed, @"^\s*DELETE\b",
System.Text.RegularExpressions.RegexOptions.IgnoreCase, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout))
if (!Strata.SqlTools.Statements.SqlServer.ParsePreparation.TryRunPrelude(
sql, SnowflakeParserInstance, @"^\s*DELETE\b", "DELETE",
out sql, out var setupClauses, out var finishClauses, out errorMessage))
{
errorMessage = "SQL statement must start with DELETE.";
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);
// Snowflake uses simpler DELETE syntax: DELETE FROM table WHERE condition
var deleteMatch = System.Text.RegularExpressions.Regex.Match(sql,
@"DELETE\s+FROM\s+(.*?)(?:\s+WHERE\s+(.*))?$",
@@ -115,12 +115,6 @@ public class InsertBreakdown : SqlServerInsertBreakdown
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)
{
@@ -142,25 +136,13 @@ public class InsertBreakdown : SqlServerInsertBreakdown
return true;
}
var parser = SnowflakeParserInstance;
sql = parser.NormalizeSqlPreservingComments(sql);
// Check if it's an INSERT statement
var sqlTrimmed = sql.TrimStart();
if (!System.Text.RegularExpressions.Regex.IsMatch(sqlTrimmed, @"^\s*INSERT\s+INTO\b",
System.Text.RegularExpressions.RegexOptions.IgnoreCase, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout))
if (!Strata.SqlTools.Statements.SqlServer.ParsePreparation.TryRunPrelude(
sql, SnowflakeParserInstance, @"^\s*INSERT\s+INTO\b", "INSERT INTO",
out sql, out var setupClauses, out var finishClauses, out errorMessage))
{
errorMessage = "SQL statement must start with INSERT INTO.";
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 INSERT statement using regex
var insertMatch = System.Text.RegularExpressions.Regex.Match(sql,
@"INSERT\s+INTO\s+([^\(\s]+)\s*\(([^\)]*)\)\s*VALUES\s*\(([^\)]*)\)",
@@ -138,37 +138,19 @@ public class ProcedureBreakdown : SqlServerProcedureBreakdown
result = null!;
errorMessage = null!;
if (string.IsNullOrWhiteSpace(sql))
{
errorMessage = "SQL statement cannot be null or empty.";
return false;
}
// If Microsoft SQL mode, delegate to base class
if (isMicrosoftSql)
{
return TryParseMicrosoftSql(sql, out result, out errorMessage);
}
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, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout))
if (!Strata.SqlTools.Statements.SqlServer.ParsePreparation.TryRunPrelude(
sql, SnowflakeParserInstance, @"^\s*(CALL|EXEC|EXECUTE)\b", "CALL, EXEC, or EXECUTE",
out sql, out var setupClauses, out var finishClauses, out errorMessage))
{
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,
@@ -125,12 +125,6 @@ public class UpdateBreakdown : SqlServerUpdateBreakdown
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)
{
@@ -154,24 +148,13 @@ public class UpdateBreakdown : SqlServerUpdateBreakdown
}
var parser = SnowflakeParserInstance;
sql = parser.NormalizeSqlPreservingComments(sql);
// Check if it's an UPDATE statement
var sqlTrimmed = sql.TrimStart();
if (!System.Text.RegularExpressions.Regex.IsMatch(sqlTrimmed, @"^\s*UPDATE\b",
System.Text.RegularExpressions.RegexOptions.IgnoreCase, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout))
if (!Strata.SqlTools.Statements.SqlServer.ParsePreparation.TryRunPrelude(
sql, parser, @"^\s*UPDATE\b", "UPDATE",
out sql, out var setupClauses, out var finishClauses, out errorMessage))
{
errorMessage = "SQL statement must start with UPDATE.";
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 UPDATE statement - handle both with and without FROM clause
var updateMatch = System.Text.RegularExpressions.Regex.Match(sql,
@"UPDATE\s+([^\s]+)\s+SET\s+(.*?)(?:\s+FROM\s+(.*?))?(?:\s+WHERE\s+(.*))?$",
@@ -146,31 +146,14 @@ public class DeleteBreakdown : SqlBreakdownBase
result = null!;
errorMessage = null!;
if (string.IsNullOrWhiteSpace(sql))
{
errorMessage = "SQL statement cannot be null or empty.";
return false;
}
var parser = new StatementParser();
sql = parser.NormalizeSqlPreservingComments(sql);
// Check if it's a DELETE statement
var sqlTrimmed = sql.TrimStart();
if (!System.Text.RegularExpressions.Regex.IsMatch(sqlTrimmed, @"^\s*DELETE\b",
System.Text.RegularExpressions.RegexOptions.IgnoreCase, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout))
if (!Strata.SqlTools.Statements.SqlServer.ParsePreparation.TryRunPrelude(
sql, parser, @"^\s*DELETE\b", "DELETE",
out sql, out var setupClauses, out var finishClauses, out errorMessage))
{
errorMessage = "SQL statement must start with DELETE.";
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 DELETE statement using regex
// Pattern: DELETE [table_alias] FROM table WHERE condition
var deleteMatch = System.Text.RegularExpressions.Regex.Match(sql,
@@ -145,31 +145,13 @@ public class InsertBreakdown : SqlBreakdownBase
try
{
if (string.IsNullOrWhiteSpace(sql))
if (!Strata.SqlTools.Statements.SqlServer.ParsePreparation.TryRunPrelude(
sql, new StatementParser(), @"^\s*INSERT\s+INTO\b", "INSERT INTO",
out sql, out var setupClauses, out var finishClauses, out errorMessage))
{
errorMessage = "SQL statement cannot be null or empty.";
return false;
}
var parser = new StatementParser();
sql = parser.NormalizeSqlPreservingComments(sql);
// Check if it's an INSERT statement
var sqlTrimmed = sql.TrimStart();
if (!System.Text.RegularExpressions.Regex.IsMatch(sqlTrimmed, @"^\s*INSERT\s+INTO\b",
System.Text.RegularExpressions.RegexOptions.IgnoreCase, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout))
{
errorMessage = "SQL statement must start with INSERT INTO.";
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 INSERT statement using regex
// Pattern: INSERT INTO table (columns) VALUES (values)
var insertMatch = System.Text.RegularExpressions.Regex.Match(sql,
@@ -156,31 +156,13 @@ public class ProcedureBreakdown : SqlBreakdownBase
try
{
if (string.IsNullOrWhiteSpace(sql))
if (!ParsePreparation.TryRunPrelude(
sql, new StatementParser(), @"^\s*(EXEC|EXECUTE)\b", "EXEC or EXECUTE",
out sql, out var setupClauses, out var finishClauses, out errorMessage))
{
errorMessage = "SQL statement cannot be null or empty.";
return false;
}
var parser = new StatementParser();
sql = parser.NormalizeSqlPreservingComments(sql);
// Check if it's an EXEC or EXECUTE statement
var sqlTrimmed = sql.TrimStart();
if (!Regex.IsMatch(sqlTrimmed, @"^\s*(EXEC|EXECUTE)\b",
RegexOptions.IgnoreCase, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout))
{
errorMessage = "SQL statement must start with 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 EXEC statement - match procedure name and parameters
// Pattern: EXEC[UTE] procedureName [@param = value, ...]
var execMatch = Regex.Match(sql,
@@ -152,31 +152,14 @@ public class UpdateBreakdown : SqlBreakdownBase
try
{
if (string.IsNullOrWhiteSpace(sql))
{
errorMessage = "SQL statement cannot be null or empty.";
return false;
}
var parser = new StatementParser();
sql = parser.NormalizeSqlPreservingComments(sql);
// Check if it's an UPDATE statement
var sqlTrimmed = sql.TrimStart();
if (!Regex.IsMatch(sqlTrimmed, @"^\s*UPDATE\b",
RegexOptions.IgnoreCase, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout))
if (!ParsePreparation.TryRunPrelude(
sql, parser, @"^\s*UPDATE\b", "UPDATE",
out sql, out var setupClauses, out var finishClauses, out errorMessage))
{
errorMessage = "SQL statement must start with UPDATE.";
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 UPDATE statement - handle both with and without FROM clause
// Pattern: UPDATE table SET column=value [FROM table] [WHERE condition]
var updateMatch = Regex.Match(sql,
@@ -0,0 +1,69 @@
using System.Collections;
using System.Text.RegularExpressions;
using Strata.SqlTools.SqlBreakdown.Utilities;
namespace Strata.SqlTools.Statements.SqlServer;
/// <summary>
/// Shared parse-prelude scaffolding for <c>TryParse</c> implementations on
/// dialect-specific breakdown classes (Insert/Update/Delete/Procedure).
/// Each breakdown's prelude was historically 1730 lines of identical code:
/// null check, comment-preserving normalize, statement-prefix regex check,
/// then setup/finish clause extraction. This helper performs all of it.
/// </summary>
public static class ParsePreparation
{
/// <summary>
/// Runs the standard <c>TryParse</c> prelude:
/// validates that <paramref name="sql"/> is non-empty, normalizes it with
/// <see cref="StatementParser.NormalizeSqlPreservingComments"/>, requires the
/// trimmed statement to match <paramref name="prefixRegex"/>, and pulls out
/// the setup/finish clauses.
/// </summary>
/// <param name="sql">The raw SQL being parsed.</param>
/// <param name="parser">The dialect's <see cref="StatementParser"/> (or subclass).</param>
/// <param name="prefixRegex">A regex anchored at the start that the trimmed SQL must match (e.g. <c>@"^\s*INSERT\s+INTO\b"</c>).</param>
/// <param name="prefixDescription">Human-readable name of the expected prefix used in the error message (e.g. <c>"INSERT INTO"</c>).</param>
/// <param name="normalizedSql">On success, the SQL with comments preserved and setup/finish clauses removed.</param>
/// <param name="setupClauses">On success, the list of setup-clause strings extracted from the front of the SQL.</param>
/// <param name="finishClauses">On success, the finish clauses extracted from the back of the SQL.</param>
/// <param name="errorMessage">On failure, a human-readable explanation suitable for surfacing through the caller's <c>out string errorMessage</c>.</param>
/// <returns><c>true</c> if the prelude completed and <paramref name="normalizedSql"/> is ready for dialect-specific matching; <c>false</c> otherwise.</returns>
public static bool TryRunPrelude(
string sql,
StatementParser parser,
string prefixRegex,
string prefixDescription,
out string normalizedSql,
out List<string> setupClauses,
out ArrayList finishClauses,
out string errorMessage)
{
normalizedSql = null!;
setupClauses = null!;
finishClauses = null!;
errorMessage = null!;
if (string.IsNullOrWhiteSpace(sql))
{
errorMessage = "SQL statement cannot be null or empty.";
return false;
}
normalizedSql = parser.NormalizeSqlPreservingComments(sql);
if (!Regex.IsMatch(normalizedSql.TrimStart(), prefixRegex, RegexOptions.IgnoreCase, RegexDefaults.MatchTimeout))
{
errorMessage = $"SQL statement must start with {prefixDescription}.";
return false;
}
setupClauses = new List<string>();
normalizedSql = parser.ExtractSetupClauses(normalizedSql, setupClauses);
finishClauses = new ArrayList();
normalizedSql = parser.ExtractFinishClauses(normalizedSql, finishClauses);
return true;
}
}