refactor(dedup): cross-dialect QueryBreakdown + StatementParser helpers (Cluster E)
SonarQube Analysis / sonarqube (pull_request) Successful in 4m5s
SonarQube Analysis / sonarqube (pull_request) Successful in 4m5s
Two shared scaffolds for blocks Sonar flagged across the SqlServer,
Snowflake, and PostgreSQL dialects:
1. **`AppendToClause` on `SqlBreakdownBase`** — collapses the "if
clause is empty set it, else append `{operation} {sql}`; then merge
comment with same rule" pattern that was repeated three times in
each of SqlServer/Snowflake `QueryBreakdown`. The matching
`AddWhereExpression` / `AddHavingExpression` / `AddWhereClause(string)`
sites in both files now delegate to a single `protected static`
helper. Operates against `ISqlClause`, so it works for both the
`WhereClause` and `HavingClause` properties.
2. **`HandleDoubleQuoteAsIdentifier` on `SqlServer.StatementParser`** —
PostgreSQL and Snowflake both override SqlServer's
`HandleDoubleQuote` (which produces a string-literal token) to
instead produce a `ColumnIdentifier` token. The two overrides had
identical 14-line bodies. The shared logic now lives once, and
each dialect's override is a one-liner that calls the helper.
Deliberately *not* refactored in this commit:
- The CTE WITH-clause SQL generation in SqlServer/Snowflake QueryBreakdown
(lines ~537-560 / ~579-601 Sonar flagged) — the surrounding logic
differs enough between the two that an extraction would obscure
rather than clarify.
- The PG/Snowflake QueryBreakdown constructor pair (lines 40-58 /
43-61) — only ~10 lines × 2; extracting requires either a new
shared helper for ~20 lines of savings or moving up the inheritance
chain, neither pays for itself.
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
c121dfa611
commit
f577c06558
@@ -84,23 +84,7 @@ public class StatementParser : SqlServerStatementParser
|
|||||||
/// <param name="position">Current position in the SQL string.</param>
|
/// <param name="position">Current position in the SQL string.</param>
|
||||||
/// <returns>Token and new position after the token.</returns>
|
/// <returns>Token and new position after the token.</returns>
|
||||||
protected override ((TokenType type, string value, int position) token, int newPosition) HandleDoubleQuote(string sql, int position)
|
protected override ((TokenType type, string value, int position) token, int newPosition) HandleDoubleQuote(string sql, int position)
|
||||||
{
|
=> HandleDoubleQuoteAsIdentifier(sql, position);
|
||||||
// PostgreSQL: double-quote is an identifier (like [brackets] in T-SQL)
|
|
||||||
int start = position;
|
|
||||||
position++; // Skip opening quote
|
|
||||||
var identifier = new StringBuilder();
|
|
||||||
while (position < sql.Length && sql[position] != '"')
|
|
||||||
{
|
|
||||||
identifier.Append(sql[position]);
|
|
||||||
position++;
|
|
||||||
}
|
|
||||||
if (position < sql.Length)
|
|
||||||
{
|
|
||||||
position++; // Skip closing quote
|
|
||||||
}
|
|
||||||
|
|
||||||
return ((TokenType.ColumnIdentifier, identifier.ToString(), start), position);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Post-processes extracted clauses to handle PostgreSQL-specific LIMIT and OFFSET clauses.
|
/// Post-processes extracted clauses to handle PostgreSQL-specific LIMIT and OFFSET clauses.
|
||||||
|
|||||||
@@ -231,28 +231,7 @@ public class QueryBreakdown : SqlServerQueryBreakdown
|
|||||||
var visitor = isMicrosoftSql
|
var visitor = isMicrosoftSql
|
||||||
? (IVisitor<string>)new SqlServerCommandVisitor()
|
? (IVisitor<string>)new SqlServerCommandVisitor()
|
||||||
: new CommandVisitor();
|
: new CommandVisitor();
|
||||||
var sql = expression.Accept(visitor);
|
AppendToClause(WhereClause, expression.Accept(visitor), operation, comment);
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(WhereClause.Clause))
|
|
||||||
{
|
|
||||||
WhereClause.Clause = sql;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
WhereClause.Clause = $"{WhereClause.Clause} {operation} {sql}";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(comment))
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(WhereClause.Comment))
|
|
||||||
{
|
|
||||||
WhereClause.Comment = comment;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
WhereClause.Comment = $"{WhereClause.Comment} {comment}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -297,29 +276,9 @@ public class QueryBreakdown : SqlServerQueryBreakdown
|
|||||||
|
|
||||||
// Extract comments from the incoming SQL
|
// Extract comments from the incoming SQL
|
||||||
var cleanSql = parser.ExtractSqlComments(sql, out var comments);
|
var cleanSql = parser.ExtractSqlComments(sql, out var comments);
|
||||||
|
var commentText = comments.Count > 0 ? string.Join(" ", comments) : null;
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(WhereClause.Clause))
|
AppendToClause(WhereClause, cleanSql.Trim(), operation, commentText);
|
||||||
{
|
|
||||||
WhereClause.Clause = cleanSql.Trim();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
WhereClause.Clause = $"{WhereClause.Clause} {operation} {cleanSql.Trim()}";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Merge comments
|
|
||||||
if (comments.Count > 0)
|
|
||||||
{
|
|
||||||
var newComment = string.Join(" ", comments);
|
|
||||||
if (string.IsNullOrWhiteSpace(WhereClause.Comment))
|
|
||||||
{
|
|
||||||
WhereClause.Comment = newComment;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
WhereClause.Comment = $"{WhereClause.Comment} {newComment}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract and add parameters from the WHERE clause using appropriate parser
|
// Extract and add parameters from the WHERE clause using appropriate parser
|
||||||
ExtractAndAddParametersWithParser(cleanSql, parser);
|
ExtractAndAddParametersWithParser(cleanSql, parser);
|
||||||
@@ -476,28 +435,7 @@ public class QueryBreakdown : SqlServerQueryBreakdown
|
|||||||
var visitor = isMicrosoftSql
|
var visitor = isMicrosoftSql
|
||||||
? (IVisitor<string>)new SqlServerCommandVisitor()
|
? (IVisitor<string>)new SqlServerCommandVisitor()
|
||||||
: new CommandVisitor();
|
: new CommandVisitor();
|
||||||
var sql = expression.Accept(visitor);
|
AppendToClause(HavingClause, expression.Accept(visitor), operation, comment);
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(HavingClause.Clause))
|
|
||||||
{
|
|
||||||
HavingClause.Clause = sql;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
HavingClause.Clause = $"{HavingClause.Clause} {operation} {sql}";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(comment))
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(HavingClause.Comment))
|
|
||||||
{
|
|
||||||
HavingClause.Comment = comment;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
HavingClause.Comment = $"{HavingClause.Comment} {comment}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -91,23 +91,7 @@ public class StatementParser : SqlServerStatementParser
|
|||||||
/// <param name="position">Current position in the SQL string.</param>
|
/// <param name="position">Current position in the SQL string.</param>
|
||||||
/// <returns>Token and new position after the token.</returns>
|
/// <returns>Token and new position after the token.</returns>
|
||||||
protected override ((TokenType type, string value, int position) token, int newPosition) HandleDoubleQuote(string sql, int position)
|
protected override ((TokenType type, string value, int position) token, int newPosition) HandleDoubleQuote(string sql, int position)
|
||||||
{
|
=> HandleDoubleQuoteAsIdentifier(sql, position);
|
||||||
// Snowflake: double-quote is an identifier (like [brackets])
|
|
||||||
int start = position;
|
|
||||||
position++; // Skip opening quote
|
|
||||||
var identifier = new StringBuilder();
|
|
||||||
while (position < sql.Length && sql[position] != '"')
|
|
||||||
{
|
|
||||||
identifier.Append(sql[position]);
|
|
||||||
position++;
|
|
||||||
}
|
|
||||||
if (position < sql.Length)
|
|
||||||
{
|
|
||||||
position++; // Skip closing quote
|
|
||||||
}
|
|
||||||
|
|
||||||
return ((TokenType.ColumnIdentifier, identifier.ToString(), start), position);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Post-processes extracted clauses to handle Snowflake-specific LIMIT clause.
|
/// Post-processes extracted clauses to handle Snowflake-specific LIMIT clause.
|
||||||
|
|||||||
@@ -53,6 +53,42 @@ public abstract class SqlBreakdownBase : ISqlBreakdown
|
|||||||
/// <returns>The SQL query string.</returns>
|
/// <returns>The SQL query string.</returns>
|
||||||
protected abstract string GetSqlBreakdown();
|
protected abstract string GetSqlBreakdown();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Appends a SQL fragment to an <see cref="ISqlClause"/> with the given logical operation
|
||||||
|
/// (e.g. <c>"and"</c> / <c>"or"</c>), and optionally merges an associated comment.
|
||||||
|
/// If the target clause is empty, the fragment is set as the clause; otherwise it is
|
||||||
|
/// joined with the operation.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="clause">The clause being built up (e.g. <c>WhereClause</c>, <c>HavingClause</c>).</param>
|
||||||
|
/// <param name="sql">The already-generated SQL fragment to append.</param>
|
||||||
|
/// <param name="operation">The logical operation used to join with the existing content.</param>
|
||||||
|
/// <param name="comment">Optional comment to merge into <c>clause.Comment</c>.</param>
|
||||||
|
protected static void AppendToClause(ISqlClause clause, string sql, string operation, string? comment)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(clause.Clause))
|
||||||
|
{
|
||||||
|
clause.Clause = sql;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
clause.Clause = $"{clause.Clause} {operation} {sql}";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(comment))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(clause.Comment))
|
||||||
|
{
|
||||||
|
clause.Comment = comment;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
clause.Comment = $"{clause.Comment} {comment}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets a LINQ to SQL query of the specified type based on this breakdown.
|
/// Gets a LINQ to SQL query of the specified type based on this breakdown.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -822,29 +822,9 @@ public class QueryBreakdown : SqlBreakdownBase, IQueryBreakdown
|
|||||||
|
|
||||||
// Extract comments from the incoming SQL
|
// Extract comments from the incoming SQL
|
||||||
var cleanSql = Parser.ExtractSqlComments(sql, out var comments);
|
var cleanSql = Parser.ExtractSqlComments(sql, out var comments);
|
||||||
|
var commentText = comments.Count > 0 ? string.Join(" ", comments) : null;
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(WhereClause.Clause))
|
AppendToClause(WhereClause, cleanSql.Trim(), operation, commentText);
|
||||||
{
|
|
||||||
WhereClause.Clause = cleanSql.Trim();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
WhereClause.Clause = $"{WhereClause.Clause} {operation} {cleanSql.Trim()}";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Merge comments
|
|
||||||
if (comments.Count > 0)
|
|
||||||
{
|
|
||||||
var newComment = string.Join(" ", comments);
|
|
||||||
if (string.IsNullOrWhiteSpace(WhereClause.Comment))
|
|
||||||
{
|
|
||||||
WhereClause.Comment = newComment;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
WhereClause.Comment = $"{WhereClause.Comment} {newComment}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract and add parameters from the WHERE clause
|
// Extract and add parameters from the WHERE clause
|
||||||
ExtractAndAddParameters(cleanSql);
|
ExtractAndAddParameters(cleanSql);
|
||||||
@@ -901,28 +881,7 @@ public class QueryBreakdown : SqlBreakdownBase, IQueryBreakdown
|
|||||||
}
|
}
|
||||||
|
|
||||||
var visitor = new CommandVisitor();
|
var visitor = new CommandVisitor();
|
||||||
var sql = expression.Accept(visitor);
|
AppendToClause(WhereClause, expression.Accept(visitor), operation, comment);
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(WhereClause.Clause))
|
|
||||||
{
|
|
||||||
WhereClause.Clause = sql;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
WhereClause.Clause = $"{WhereClause.Clause} {operation} {sql}";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(comment))
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(WhereClause.Comment))
|
|
||||||
{
|
|
||||||
WhereClause.Comment = comment;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
WhereClause.Comment = $"{WhereClause.Comment} {comment}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -1013,28 +972,7 @@ public class QueryBreakdown : SqlBreakdownBase, IQueryBreakdown
|
|||||||
}
|
}
|
||||||
|
|
||||||
var visitor = new CommandVisitor();
|
var visitor = new CommandVisitor();
|
||||||
var sql = expression.Accept(visitor);
|
AppendToClause(HavingClause, expression.Accept(visitor), operation, comment);
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(HavingClause.Clause))
|
|
||||||
{
|
|
||||||
HavingClause.Clause = sql;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
HavingClause.Clause = $"{HavingClause.Clause} {operation} {sql}";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(comment))
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(HavingClause.Comment))
|
|
||||||
{
|
|
||||||
HavingClause.Comment = comment;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
HavingClause.Comment = $"{HavingClause.Comment} {comment}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -649,6 +649,32 @@ public class StatementParser
|
|||||||
return ((TokenType.String, str.ToString(), start), position);
|
return ((TokenType.String, str.ToString(), start), position);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Helper used by dialects (PostgreSQL, Snowflake) where double-quoted text is an
|
||||||
|
/// identifier rather than a string literal. Reads from the opening quote at
|
||||||
|
/// <paramref name="position"/> and returns the inner text as a
|
||||||
|
/// <see cref="TokenType.ColumnIdentifier"/> token.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="sql">The SQL statement being tokenized.</param>
|
||||||
|
/// <param name="position">The current position in the SQL string (must point at the opening <c>"</c>).</param>
|
||||||
|
/// <returns>The identifier token and the new position past the closing <c>"</c>.</returns>
|
||||||
|
protected static ((TokenType type, string value, int position) token, int newPosition) HandleDoubleQuoteAsIdentifier(string sql, int position)
|
||||||
|
{
|
||||||
|
int start = position;
|
||||||
|
position++; // Skip opening quote
|
||||||
|
var identifier = new StringBuilder();
|
||||||
|
while (position < sql.Length && sql[position] != '"')
|
||||||
|
{
|
||||||
|
identifier.Append(sql[position]);
|
||||||
|
position++;
|
||||||
|
}
|
||||||
|
if (position < sql.Length)
|
||||||
|
{
|
||||||
|
position++; // Skip closing quote
|
||||||
|
}
|
||||||
|
return ((TokenType.ColumnIdentifier, identifier.ToString(), start), position);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Processes a list of tokens to find SQL keywords at the top level (outside parentheses).
|
/// Processes a list of tokens to find SQL keywords at the top level (outside parentheses).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
Reference in New Issue
Block a user