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>
166 lines
5.4 KiB
C#
166 lines
5.4 KiB
C#
using System.Collections;
|
|
using System.Text;
|
|
using Strata.SqlTools.SqlBreakdown.Interfaces;
|
|
|
|
namespace Strata.SqlTools.SqlBreakdown.Classes;
|
|
|
|
/// <summary>
|
|
/// Base class for SQL query breakdowns that provides common setup/finish clause handling and cloning.
|
|
/// </summary>
|
|
public abstract class SqlBreakdownBase : ISqlBreakdown
|
|
{
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="SqlBreakdownBase"/> class.
|
|
/// </summary>
|
|
protected SqlBreakdownBase()
|
|
{
|
|
SetupClauses = [];
|
|
FinishClauses = [];
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the setup clauses to execute before the main query.
|
|
/// </summary>
|
|
public List<string> SetupClauses { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets a value indicating whether setup clauses are being used.
|
|
/// </summary>
|
|
public bool IsUsingSetupClause => SetupClauses.Count > 0;
|
|
|
|
/// <summary>
|
|
/// Gets or sets the raw/original SQL statement before parsing and breakdown.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This property stores the original SQL text that was parsed to create this breakdown.
|
|
/// It's useful for auditing, logging, and batch statement retrieval.
|
|
/// </remarks>
|
|
public string? RawSql { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the finish clauses to execute after the main query.
|
|
/// </summary>
|
|
public ArrayList FinishClauses { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets a value indicating whether finish clauses are being used.
|
|
/// </summary>
|
|
public bool IsUsingFinishClause => FinishClauses.Count > 0;
|
|
|
|
/// <summary>
|
|
/// Gets the SQL breakdown as a string. Must be implemented by derived classes.
|
|
/// </summary>
|
|
/// <returns>The SQL query string.</returns>
|
|
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>
|
|
/// Gets a LINQ to SQL query of the specified type based on this breakdown.
|
|
/// </summary>
|
|
/// <typeparam name="T">The entity type for the query.</typeparam>
|
|
/// <returns>An IQueryable of the specified type; an empty queryable if the breakdown cannot be converted to a LINQ query.</returns>
|
|
/// <remarks>
|
|
/// This method allows derived breakdown classes to reconstruct or generate LINQ queries
|
|
/// from the analyzed components (SELECT, WHERE, ORDER BY, etc.).
|
|
/// The default implementation returns an empty queryable.
|
|
/// </remarks>
|
|
public virtual IQueryable<T> GetQuery<T>() where T : class => Enumerable.Empty<T>().AsQueryable();
|
|
|
|
/// <summary>
|
|
/// Gets the complete SQL query including optional setup and finish clauses.
|
|
/// </summary>
|
|
/// <param name="includeSetupFinish">Whether to include setup and finish clauses.</param>
|
|
/// <returns>The complete SQL query string.</returns>
|
|
public string GetSql(bool includeSetupFinish = true)
|
|
{
|
|
var sb = new StringBuilder();
|
|
|
|
if (includeSetupFinish)
|
|
{
|
|
// Write out any setup clauses
|
|
sb.AppendLine();
|
|
foreach (string setup in SetupClauses)
|
|
{
|
|
sb.AppendLine(setup);
|
|
}
|
|
sb.AppendLine();
|
|
}
|
|
|
|
sb.Append(GetSqlBreakdown());
|
|
|
|
if (includeSetupFinish)
|
|
{
|
|
// Write out any finish clauses
|
|
sb.AppendLine();
|
|
sb.AppendLine();
|
|
foreach (string finish in FinishClauses)
|
|
{
|
|
sb.AppendLine(finish);
|
|
}
|
|
}
|
|
|
|
return sb.ToString();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the SQL query as a string.
|
|
/// </summary>
|
|
/// <returns>The SQL query string.</returns>
|
|
public override string ToString()
|
|
=> GetSql();
|
|
|
|
/// <summary>
|
|
/// Creates a deep clone of this object.
|
|
/// </summary>
|
|
/// <returns>A cloned instance.</returns>
|
|
public object Clone()
|
|
{
|
|
// Create a shallow copy
|
|
var clone = (SqlBreakdownBase)MemberwiseClone();
|
|
|
|
// Deep copy the SetupClauses list
|
|
clone.SetupClauses = new List<string>(SetupClauses);
|
|
|
|
// Deep copy the FinishClauses ArrayList
|
|
clone.FinishClauses = new ArrayList(FinishClauses);
|
|
|
|
return clone;
|
|
}
|
|
}
|
|
|