From c121dfa611359c7aec21e0432cbca62ede8f64e0 Mon Sep 17 00:00:00 2001 From: Thom Lamb Date: Wed, 27 May 2026 16:33:43 -0500 Subject: [PATCH] refactor(dedup): share TryParse prelude across breakdown families (Cluster D) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../Breakdowns/DeleteBreakdown.cs | 24 +------ .../Breakdowns/InsertBreakdown.cs | 24 +------ .../Breakdowns/ProcedureBreakdown.cs | 24 +------ .../Breakdowns/UpdateBreakdown.cs | 23 +------ .../Breakdowns/DeleteBreakdown.cs | 23 +------ .../Breakdowns/InsertBreakdown.cs | 24 +------ .../Breakdowns/ProcedureBreakdown.cs | 24 +------ .../Breakdowns/UpdateBreakdown.cs | 23 +------ .../Statements/ParsePreparation.cs | 69 +++++++++++++++++++ 9 files changed, 93 insertions(+), 165 deletions(-) create mode 100644 src/Strata.SqlTools.SqlServer/Statements/ParsePreparation.cs diff --git a/src/Strata.SqlTools.Snowflake/Breakdowns/DeleteBreakdown.cs b/src/Strata.SqlTools.Snowflake/Breakdowns/DeleteBreakdown.cs index 9e2cfeb..b663d80 100644 --- a/src/Strata.SqlTools.Snowflake/Breakdowns/DeleteBreakdown.cs +++ b/src/Strata.SqlTools.Snowflake/Breakdowns/DeleteBreakdown.cs @@ -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(); - 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+(.*))?$", diff --git a/src/Strata.SqlTools.Snowflake/Breakdowns/InsertBreakdown.cs b/src/Strata.SqlTools.Snowflake/Breakdowns/InsertBreakdown.cs index d719ad9..89f3d06 100644 --- a/src/Strata.SqlTools.Snowflake/Breakdowns/InsertBreakdown.cs +++ b/src/Strata.SqlTools.Snowflake/Breakdowns/InsertBreakdown.cs @@ -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(); - 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*\(([^\)]*)\)", diff --git a/src/Strata.SqlTools.Snowflake/Breakdowns/ProcedureBreakdown.cs b/src/Strata.SqlTools.Snowflake/Breakdowns/ProcedureBreakdown.cs index 74d0a21..9edc117 100644 --- a/src/Strata.SqlTools.Snowflake/Breakdowns/ProcedureBreakdown.cs +++ b/src/Strata.SqlTools.Snowflake/Breakdowns/ProcedureBreakdown.cs @@ -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(); - 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, diff --git a/src/Strata.SqlTools.Snowflake/Breakdowns/UpdateBreakdown.cs b/src/Strata.SqlTools.Snowflake/Breakdowns/UpdateBreakdown.cs index f0fcc00..5895dc1 100644 --- a/src/Strata.SqlTools.Snowflake/Breakdowns/UpdateBreakdown.cs +++ b/src/Strata.SqlTools.Snowflake/Breakdowns/UpdateBreakdown.cs @@ -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(); - 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+(.*))?$", diff --git a/src/Strata.SqlTools.SqlServer/Breakdowns/DeleteBreakdown.cs b/src/Strata.SqlTools.SqlServer/Breakdowns/DeleteBreakdown.cs index b8433b9..f7d2271 100644 --- a/src/Strata.SqlTools.SqlServer/Breakdowns/DeleteBreakdown.cs +++ b/src/Strata.SqlTools.SqlServer/Breakdowns/DeleteBreakdown.cs @@ -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(); - 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, diff --git a/src/Strata.SqlTools.SqlServer/Breakdowns/InsertBreakdown.cs b/src/Strata.SqlTools.SqlServer/Breakdowns/InsertBreakdown.cs index 5176880..dd77b77 100644 --- a/src/Strata.SqlTools.SqlServer/Breakdowns/InsertBreakdown.cs +++ b/src/Strata.SqlTools.SqlServer/Breakdowns/InsertBreakdown.cs @@ -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(); - 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, diff --git a/src/Strata.SqlTools.SqlServer/Breakdowns/ProcedureBreakdown.cs b/src/Strata.SqlTools.SqlServer/Breakdowns/ProcedureBreakdown.cs index a114cd5..bc6fb9f 100644 --- a/src/Strata.SqlTools.SqlServer/Breakdowns/ProcedureBreakdown.cs +++ b/src/Strata.SqlTools.SqlServer/Breakdowns/ProcedureBreakdown.cs @@ -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(); - 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, diff --git a/src/Strata.SqlTools.SqlServer/Breakdowns/UpdateBreakdown.cs b/src/Strata.SqlTools.SqlServer/Breakdowns/UpdateBreakdown.cs index b280f81..2d5a894 100644 --- a/src/Strata.SqlTools.SqlServer/Breakdowns/UpdateBreakdown.cs +++ b/src/Strata.SqlTools.SqlServer/Breakdowns/UpdateBreakdown.cs @@ -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(); - 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, diff --git a/src/Strata.SqlTools.SqlServer/Statements/ParsePreparation.cs b/src/Strata.SqlTools.SqlServer/Statements/ParsePreparation.cs new file mode 100644 index 0000000..489a0e7 --- /dev/null +++ b/src/Strata.SqlTools.SqlServer/Statements/ParsePreparation.cs @@ -0,0 +1,69 @@ +using System.Collections; +using System.Text.RegularExpressions; +using Strata.SqlTools.SqlBreakdown.Utilities; + +namespace Strata.SqlTools.Statements.SqlServer; + +/// +/// Shared parse-prelude scaffolding for TryParse 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. +/// +public static class ParsePreparation +{ + /// + /// Runs the standard TryParse prelude: + /// validates that is non-empty, normalizes it with + /// , requires the + /// trimmed statement to match , and pulls out + /// the setup/finish clauses. + /// + /// The raw SQL being parsed. + /// The dialect's (or subclass). + /// A regex anchored at the start that the trimmed SQL must match (e.g. @"^\s*INSERT\s+INTO\b"). + /// Human-readable name of the expected prefix used in the error message (e.g. "INSERT INTO"). + /// On success, the SQL with comments preserved and setup/finish clauses removed. + /// On success, the list of setup-clause strings extracted from the front of the SQL. + /// On success, the finish clauses extracted from the back of the SQL. + /// On failure, a human-readable explanation suitable for surfacing through the caller's out string errorMessage. + /// true if the prelude completed and is ready for dialect-specific matching; false otherwise. + public static bool TryRunPrelude( + string sql, + StatementParser parser, + string prefixRegex, + string prefixDescription, + out string normalizedSql, + out List 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(); + normalizedSql = parser.ExtractSetupClauses(normalizedSql, setupClauses); + + finishClauses = new ArrayList(); + normalizedSql = parser.ExtractFinishClauses(normalizedSql, finishClauses); + + return true; + } +}