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:
co-authored by
Claude Opus 4.7
parent
4b5348c53a
commit
c121dfa611
@@ -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 17–30 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user