Merge pull request 'refactor(dedup): second pass at src/ duplication — clear 10 of 13 remaining blocks' (#21) from chore/sonarqube-src-dedup-pass2 into main
SonarQube Analysis / sonarqube (push) Successful in 4m32s

Reviewed-on: #21
This commit was merged in pull request #21.
This commit is contained in:
2026-05-27 17:14:00 -05:00
11 changed files with 90 additions and 123 deletions
@@ -10,6 +10,7 @@ using Strata.SqlTools.SqlBreakdown.Expressions.Functions.Aggregate;
using Strata.SqlTools.SqlBreakdown.Expressions.Functions.Conditional;
using Strata.SqlTools.SqlBreakdown.Expressions.Literals;
using Strata.SqlTools.SqlBreakdown.Interfaces.Core;
using static Strata.SqlTools.Markdown.Internal.MarkdownTextHelpers;
namespace Strata.SqlTools.Markdown.Expressions;
@@ -360,8 +361,6 @@ public class ExpressionGenerator : IVisitor<string>
.Replace("]", "&#93;");
}
private static string TruncateText(string text, int maxLength)
=> Internal.MarkdownTextHelpers.TruncateText(text, maxLength);
#region IVisitor Implementation
@@ -1,6 +1,7 @@
using System.Text;
using Strata.SqlTools.Breakdowns.SqlServer;
using Strata.SqlTools.SqlBreakdown.Interfaces;
using static Strata.SqlTools.Markdown.Internal.MarkdownTextHelpers;
namespace Strata.SqlTools.Markdown.SqlServer;
@@ -191,7 +192,5 @@ public static class QueryBreakdownGenerator
.Replace(">", "&gt;");
}
private static string TruncateText(string text, int maxLength)
=> Internal.MarkdownTextHelpers.TruncateText(text, maxLength);
}
@@ -1,5 +1,6 @@
using System.Text;
using Strata.SqlTools.SqlBreakdown.Interfaces;
using static Strata.SqlTools.Markdown.Internal.MarkdownTextHelpers;
namespace Strata.SqlTools.Markdown.SqlServer;
@@ -112,9 +113,6 @@ public static class SqlStatementGenerator
.Replace("\r", "");
}
private static string TruncateText(string text, int maxLength)
=> Internal.MarkdownTextHelpers.TruncateText(text, maxLength);
/// <summary>
/// Cleans table name for use in Mermaid diagrams.
/// </summary>
@@ -36,14 +36,8 @@ public class QueryBreakdown : SqlServerQueryBreakdown
public QueryBreakdown(string selectClause, string fromClause, bool isMicrosoftSql = false) : base()
{
var parser = isMicrosoftSql ? Parser : PostgreSqlParserInstance;
var cleanSelect = parser.ExtractSqlComments(selectClause, out var selectComments);
SelectClause.Clause = cleanSelect.Trim();
SelectClause.Comment = selectComments.Count > 0 ? string.Join(" ", selectComments) : null;
var cleanFrom = parser.ExtractSqlComments(fromClause, out var fromComments);
FromClause.Clause = cleanFrom.Trim();
FromClause.Comment = fromComments.Count > 0 ? string.Join(" ", fromComments) : null;
parser.PopulateClauseWithComments(selectClause, SelectClause);
parser.PopulateClauseWithComments(fromClause, FromClause);
}
/// <summary>
@@ -104,76 +104,39 @@ public class StatementReader : SqlServerStatementReader
if (CurrentCharacter == '=')
{
// Handle => operator (used in PostgreSQL for hstore and other operations)
// =, => (PostgreSQL hstore + other operations)
MovePosition();
if (CurrentCharacter == '>')
{
MovePosition();
_currentToken = new Token(TokenType.Operator, "=>");
return true;
}
// Single = is handled as regular operator
if (TryMatchTwoCharOperator('>', "=>")) return true;
_currentToken = new Token(TokenType.Operator, "=");
return true;
}
if (CurrentCharacter == '|')
{
// Handle || concatenation operator
// |, ||
MovePosition();
if (CurrentCharacter == '|')
{
MovePosition();
_currentToken = new Token(TokenType.Operator, "||");
return true;
}
// Single | is also an operator
if (TryMatchTwoCharOperator('|', "||")) return true;
_currentToken = new Token(TokenType.Operator, "|");
return true;
}
if (CurrentCharacter == '<')
{
// Handle <, <=, <>, << operators
// <, <=, <>, <<
MovePosition();
if (CurrentCharacter == '=')
{
MovePosition();
_currentToken = new Token(TokenType.Operator, "<=");
return true;
}
if (CurrentCharacter == '>')
{
MovePosition();
_currentToken = new Token(TokenType.Operator, "<>");
return true;
}
if (CurrentCharacter == '<')
{
MovePosition();
_currentToken = new Token(TokenType.Operator, "<<");
return true;
}
if (TryMatchTwoCharOperator('=', "<=")) return true;
if (TryMatchTwoCharOperator('>', "<>")) return true;
if (TryMatchTwoCharOperator('<', "<<")) return true;
_currentToken = new Token(TokenType.Operator, "<");
return true;
}
if (CurrentCharacter == '>')
{
// Handle >, >=, >> operators
// >, >=, >>
MovePosition();
if (CurrentCharacter == '=')
{
MovePosition();
_currentToken = new Token(TokenType.Operator, ">=");
return true;
}
if (CurrentCharacter == '>')
{
MovePosition();
_currentToken = new Token(TokenType.Operator, ">>");
return true;
}
if (TryMatchTwoCharOperator('=', ">=")) return true;
if (TryMatchTwoCharOperator('>', ">>")) return true;
_currentToken = new Token(TokenType.Operator, ">");
return true;
}
@@ -244,6 +207,23 @@ public class StatementReader : SqlServerStatementReader
return stringValue.ToString();
}
/// <summary>
/// If the position is currently sitting on <paramref name="nextChar"/>, advances past it,
/// emits <paramref name="twoCharOperator"/> as the current Operator token, and returns
/// <c>true</c>. Otherwise leaves position untouched and returns <c>false</c>. Used by the
/// multi-character operator dispatch (e.g. <c>&lt;</c>/<c>&lt;=</c>/<c>&lt;&gt;</c>/<c>&lt;&lt;</c>).
/// </summary>
private bool TryMatchTwoCharOperator(char nextChar, string twoCharOperator)
{
if (CurrentCharacter != nextChar)
{
return false;
}
MovePosition();
_currentToken = new Token(TokenType.Operator, twoCharOperator);
return true;
}
}
@@ -143,21 +143,12 @@ public class InsertBreakdown : SqlServerInsertBreakdown
return false;
}
// Parse INSERT statement using regex
var insertMatch = System.Text.RegularExpressions.Regex.Match(sql,
@"INSERT\s+INTO\s+([^\(\s]+)\s*\(([^\)]*)\)\s*VALUES\s*\(([^\)]*)\)",
System.Text.RegularExpressions.RegexOptions.IgnoreCase | System.Text.RegularExpressions.RegexOptions.Singleline, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout);
if (!insertMatch.Success)
if (!Strata.SqlTools.Statements.SqlServer.ParsePreparation.TryMatchInsertSql(
sql, out var tableName, out var columnsClause, out var valuesClause, out errorMessage))
{
errorMessage = "Could not parse INSERT statement. Expected format: INSERT INTO table (columns) VALUES (values)";
return false;
}
var tableName = insertMatch.Groups[1].Value.Trim();
var columnsClause = insertMatch.Groups[2].Value.Trim();
var valuesClause = insertMatch.Groups[3].Value.Trim();
result = new InsertBreakdown(tableName, columnsClause, valuesClause, isMicrosoftSql: false)
{
SetupClauses = setupClauses,
@@ -39,14 +39,8 @@ public class QueryBreakdown : SqlServerQueryBreakdown
public QueryBreakdown(string selectClause, string fromClause, bool isMicrosoftSql = false) : base()
{
var parser = isMicrosoftSql ? Parser : SnowflakeParserInstance;
var cleanSelect = parser.ExtractSqlComments(selectClause, out var selectComments);
SelectClause.Clause = cleanSelect.Trim();
SelectClause.Comment = selectComments.Count > 0 ? string.Join(" ", selectComments) : null;
var cleanFrom = parser.ExtractSqlComments(fromClause, out var fromComments);
FromClause.Clause = cleanFrom.Trim();
FromClause.Comment = fromComments.Count > 0 ? string.Join(" ", fromComments) : null;
parser.PopulateClauseWithComments(selectClause, SelectClause);
parser.PopulateClauseWithComments(fromClause, FromClause);
}
/// <summary>
@@ -44,35 +44,8 @@ public class UpdateBreakdown : SqlServerUpdateBreakdown
WhereClause.Comment = whereComments.Count > 0 ? string.Join(" ", whereComments) : null;
}
/// <summary>
/// Gets the SQL breakdown as a string for Snowflake.
/// </summary>
/// <returns>The UPDATE SQL statement.</returns>
protected override string GetSqlBreakdown()
{
var sb = new StringBuilder();
sb.AppendLine("UPDATE ");
sb.AppendLine($" {TableName.Clause}");
sb.AppendLine("SET ");
sb.AppendLine($" {SetClause.Clause}");
if (IsUsingFromClause)
{
// Snowflake supports FROM clause in UPDATE
sb.AppendLine("FROM ");
sb.AppendLine($" {FromClause.Clause}");
}
if (IsUsingWhereClause)
{
sb.AppendLine("WHERE ");
sb.AppendLine($" {WhereClause.Clause}");
}
return sb.ToString();
}
// GetSqlBreakdown() inherited from SqlServer.UpdateBreakdown — Snowflake's UPDATE syntax
// (including the optional FROM clause) is identical at the formatter level, so no override needed.
#region Parse Methods
@@ -152,22 +152,12 @@ public class InsertBreakdown : SqlBreakdownBase
return false;
}
// Parse INSERT statement using regex
// Pattern: INSERT INTO table (columns) VALUES (values)
var insertMatch = System.Text.RegularExpressions.Regex.Match(sql,
@"INSERT\s+INTO\s+([^\(\s]+)\s*\(([^\)]*)\)\s*VALUES\s*\(([^\)]*)\)",
System.Text.RegularExpressions.RegexOptions.IgnoreCase | System.Text.RegularExpressions.RegexOptions.Singleline, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout);
if (!insertMatch.Success)
if (!Strata.SqlTools.Statements.SqlServer.ParsePreparation.TryMatchInsertSql(
sql, out var tableName, out var columnsClause, out var valuesClause, out errorMessage))
{
errorMessage = "Could not parse INSERT statement. Expected format: INSERT INTO table (columns) VALUES (values)";
return false;
}
var tableName = insertMatch.Groups[1].Value.Trim();
var columnsClause = insertMatch.Groups[2].Value.Trim();
var valuesClause = insertMatch.Groups[3].Value.Trim();
result = new InsertBreakdown(tableName, columnsClause, valuesClause)
{
SetupClauses = setupClauses,
@@ -66,4 +66,37 @@ public static class ParsePreparation
return true;
}
/// <summary>
/// Runs the shared INSERT-statement match used by SqlServer / Snowflake (and any future
/// dialect that accepts the same <c>INSERT INTO table (cols) VALUES (vals)</c> grammar).
/// </summary>
/// <param name="sql">The SQL passed through <see cref="TryRunPrelude"/>.</param>
/// <param name="tableName">On success, the matched table name (trimmed).</param>
/// <param name="columnsClause">On success, the matched column list (trimmed).</param>
/// <param name="valuesClause">On success, the matched values list (trimmed).</param>
/// <param name="errorMessage">On failure, a human-readable parse-error message.</param>
/// <returns><c>true</c> if the regex matched and the three groups are populated; <c>false</c> otherwise.</returns>
public static bool TryMatchInsertSql(string sql, out string tableName, out string columnsClause, out string valuesClause, out string errorMessage)
{
tableName = null!;
columnsClause = null!;
valuesClause = null!;
errorMessage = null!;
var insertMatch = Regex.Match(sql,
@"INSERT\s+INTO\s+([^\(\s]+)\s*\(([^\)]*)\)\s*VALUES\s*\(([^\)]*)\)",
RegexOptions.IgnoreCase | RegexOptions.Singleline, RegexDefaults.MatchTimeout);
if (!insertMatch.Success)
{
errorMessage = "Could not parse INSERT statement. Expected format: INSERT INTO table (columns) VALUES (values)";
return false;
}
tableName = insertMatch.Groups[1].Value.Trim();
columnsClause = insertMatch.Groups[2].Value.Trim();
valuesClause = insertMatch.Groups[3].Value.Trim();
return true;
}
}
@@ -140,6 +140,22 @@ public class StatementParser
/// <param name="sql">The SQL statement containing comments.</param>
/// <param name="comments">The extracted comments as a list of strings.</param>
/// <returns>The SQL statement with comments removed.</returns>
/// <summary>
/// Runs <see cref="ExtractSqlComments"/> on <paramref name="rawText"/> and assigns the
/// cleaned text to <paramref name="target"/>'s <see cref="ISqlClause.Clause"/> (trimmed)
/// and the merged comments to its <see cref="ISqlClause.Comment"/>. Helper for
/// dialect-specific <c>QueryBreakdown</c> constructors that need to ingest
/// comment-bearing SQL fragments.
/// </summary>
/// <param name="rawText">The SQL fragment to clean.</param>
/// <param name="target">The clause to populate.</param>
public void PopulateClauseWithComments(string rawText, ISqlClause target)
{
var clean = ExtractSqlComments(rawText, out var comments);
target.Clause = clean.Trim();
target.Comment = comments.Count > 0 ? string.Join(" ", comments) : null;
}
#pragma warning disable S3776 // Cognitive Complexity of methods should not be too high
#pragma warning disable S127 // "for" loop stop conditions should be invariant
public virtual string ExtractSqlComments(string sql, out List<string> comments)