SonarQube Analysis / sonarqube (pull_request) Successful in 3m9s
Introduce a default regex match timeout across the library to prevent potential ReDoS attacks (SonarQube rule S6444). Implement `[OnDeserialized]` methods to re-establish object invariants and validate state after deserialization, addressing SonarQube rule S5766.
363 lines
12 KiB
C#
363 lines
12 KiB
C#
using System.Runtime.Serialization;
|
|
using System.Text;
|
|
using Strata.SqlTools.SqlBreakdown.Classes;
|
|
using Strata.SqlTools.SqlBreakdown.Interfaces;
|
|
|
|
namespace Strata.SqlTools.Breakdowns.PostgreSql;
|
|
|
|
/// <summary>
|
|
/// PostgreSQL-specific collection for managing multiple QueryBreakdown objects.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This class extends SqlBreakdownCollection with PostgreSQL-specific functionality,
|
|
/// including support for PostgreSQL features like schema-qualified identifiers,
|
|
/// LIMIT/OFFSET clauses, parameterized queries using $1, $2 syntax, and CTEs.
|
|
/// </remarks>
|
|
[Serializable]
|
|
public class QueryBreakdownCollection : SqlBreakdownCollection
|
|
{
|
|
private readonly List<QueryBreakdown> _queryBreakdowns;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="QueryBreakdownCollection"/> class for PostgreSQL.
|
|
/// </summary>
|
|
public QueryBreakdownCollection() : base()
|
|
{
|
|
_queryBreakdowns = new List<QueryBreakdown>();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="QueryBreakdownCollection"/> class with initial query breakdowns.
|
|
/// </summary>
|
|
/// <param name="queryBreakdowns">The initial collection of query breakdowns.</param>
|
|
public QueryBreakdownCollection(IEnumerable<QueryBreakdown> queryBreakdowns)
|
|
: base(queryBreakdowns?.Cast<ISqlBreakdown>() ?? Enumerable.Empty<ISqlBreakdown>())
|
|
{
|
|
_queryBreakdowns = queryBreakdowns?.ToList() ?? new List<QueryBreakdown>();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Validates that the backing list survived deserialization, since deserialization bypasses
|
|
/// the constructors that normally initialize it (SonarQube rule S5766).
|
|
/// </summary>
|
|
/// <param name="context">The streaming context for the deserialization operation.</param>
|
|
[OnDeserialized]
|
|
private void OnDeserialized(StreamingContext context)
|
|
{
|
|
if (_queryBreakdowns is null)
|
|
{
|
|
throw new SerializationException("Deserialized QueryBreakdownCollection is missing its backing list.");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the collection of QueryBreakdown objects.
|
|
/// </summary>
|
|
public IReadOnlyList<QueryBreakdown> QueryBreakdowns => _queryBreakdowns.AsReadOnly();
|
|
|
|
/// <summary>
|
|
/// Adds a QueryBreakdown to the collection.
|
|
/// </summary>
|
|
/// <param name="queryBreakdown">The QueryBreakdown to add.</param>
|
|
public void Add(QueryBreakdown queryBreakdown)
|
|
{
|
|
if (queryBreakdown != null)
|
|
{
|
|
_queryBreakdowns.Add(queryBreakdown);
|
|
base.Add(queryBreakdown);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds multiple QueryBreakdowns to the collection.
|
|
/// </summary>
|
|
/// <param name="queryBreakdowns">The QueryBreakdowns to add.</param>
|
|
public void AddRange(IEnumerable<QueryBreakdown> queryBreakdowns)
|
|
{
|
|
foreach (var qb in queryBreakdowns ?? new List<QueryBreakdown>())
|
|
{
|
|
Add(qb);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes a QueryBreakdown from the collection.
|
|
/// </summary>
|
|
/// <returns>True if removed; otherwise, false.</returns>
|
|
public bool Remove(QueryBreakdown queryBreakdown)
|
|
{
|
|
base.Remove(queryBreakdown);
|
|
return _queryBreakdowns.Remove(queryBreakdown);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Clears all query breakdowns from the collection.
|
|
/// </summary>
|
|
public new void Clear()
|
|
{
|
|
_queryBreakdowns.Clear();
|
|
base.Clear();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the PostgreSQL SQL batch representation with proper statement separation.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Generates PostgreSQL SQL with proper semi-colon separation for multiple statements.
|
|
/// </remarks>
|
|
/// <returns>The complete SQL batch as a single string.</returns>
|
|
public string GetPostgreSqlBatch()
|
|
{
|
|
if (_queryBreakdowns.Count == 0)
|
|
{
|
|
return string.Empty;
|
|
}
|
|
|
|
var sb = new StringBuilder();
|
|
|
|
foreach (var query in _queryBreakdowns)
|
|
{
|
|
var sql = query.GetSql();
|
|
if (!string.IsNullOrEmpty(sql))
|
|
{
|
|
sb.AppendLine(sql);
|
|
if (!sql.TrimEnd().EndsWith(';'))
|
|
{
|
|
sb.AppendLine(";");
|
|
}
|
|
else
|
|
{
|
|
sb.AppendLine();
|
|
}
|
|
}
|
|
}
|
|
|
|
return sb.ToString().TrimEnd();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses a batch of PostgreSQL SQL statements into a collection.
|
|
/// </summary>
|
|
/// <param name="sqlBatch">The SQL batch to parse.</param>
|
|
/// <returns>True if parsing succeeded; false otherwise.</returns>
|
|
public bool ParseBatch(string sqlBatch)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(sqlBatch))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
try
|
|
{
|
|
Clear();
|
|
var statements = sqlBatch.Split(';');
|
|
|
|
foreach (var statement in statements)
|
|
{
|
|
var trimmedStatement = statement.Trim();
|
|
if (string.IsNullOrEmpty(trimmedStatement))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (QueryBreakdown.TryParse(statement, out var queryBreakdown, out _))
|
|
{
|
|
Add(queryBreakdown);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
catch
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a summary of all queries including their types and basic composition.
|
|
/// </summary>
|
|
/// <returns>Summary information for each query.</returns>
|
|
public IEnumerable<SqlServer.QuerySummary> GetQuerySummaries()
|
|
{
|
|
return _queryBreakdowns.Select((q, index) => new SqlServer.QuerySummary
|
|
{
|
|
Index = index,
|
|
HasSelectClause = !string.IsNullOrWhiteSpace(q.SelectClause?.Clause),
|
|
HasFromClause = !string.IsNullOrWhiteSpace(q.FromClause?.Clause),
|
|
HasWhereClause = !string.IsNullOrWhiteSpace(q.WhereClause?.Clause),
|
|
HasGroupByClause = !string.IsNullOrWhiteSpace(q.GroupByClause?.Clause),
|
|
HasHavingClause = !string.IsNullOrWhiteSpace(q.HavingClause?.Clause),
|
|
HasOrderByClause = !string.IsNullOrWhiteSpace(q.OrderByClause?.Clause),
|
|
HasJoins = false,
|
|
HasCTE = q.WithClauses.Count > 0,
|
|
ColumnCount = !string.IsNullOrWhiteSpace(q.SelectClause?.Clause) ? q.SelectClause.Clause.Split(',').Length : 0,
|
|
ParameterCount = q.ParameterList.Count(),
|
|
JoinCount = 0
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the total number of selected columns across all queries.
|
|
/// </summary>
|
|
/// <returns>Total column count.</returns>
|
|
public int GetTotalSelectedColumns()
|
|
{
|
|
return _queryBreakdowns.Sum(q =>
|
|
!string.IsNullOrWhiteSpace(q.SelectClause?.Clause)
|
|
? q.SelectClause.Clause.Split(',').Length
|
|
: 0);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets all unique table names referenced across all queries.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This provides a quick overview of which tables are being queried.
|
|
/// Note: This is a best-effort extraction and may not capture all table references,
|
|
/// especially in complex subqueries or with aliasing.
|
|
/// </remarks>
|
|
/// <returns>List of unique table names.</returns>
|
|
public IEnumerable<string> GetUniqueTableReferences()
|
|
{
|
|
var tables = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
var tableNames = _queryBreakdowns
|
|
.Where(q => !string.IsNullOrWhiteSpace(q.FromClause?.Clause))
|
|
.SelectMany(q => ExtractTableNames(q.FromClause!.Clause!));
|
|
|
|
foreach (var table in tableNames)
|
|
{
|
|
tables.Add(table);
|
|
}
|
|
|
|
return tables;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets parameter usage information across all queries.
|
|
/// </summary>
|
|
/// <returns>Parameter usage information.</returns>
|
|
public IEnumerable<ParameterUsageReport> GetParameterUsageReport()
|
|
{
|
|
// Collect all unique parameter names from both ParameterList and Parameters dictionary
|
|
var allParamNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
foreach (var query in _queryBreakdowns)
|
|
{
|
|
// Add from ParameterList (parsed parameters)
|
|
foreach (var param in query.ParameterList)
|
|
{
|
|
allParamNames.Add(param.Name);
|
|
}
|
|
|
|
// Add from Parameters dictionary (manually added parameters)
|
|
foreach (var paramName in query.Parameters.Keys)
|
|
{
|
|
allParamNames.Add(paramName);
|
|
}
|
|
}
|
|
|
|
foreach (var paramName in allParamNames)
|
|
{
|
|
var queriesUsing = 0;
|
|
object? lastValue = null;
|
|
|
|
foreach (var query in _queryBreakdowns)
|
|
{
|
|
// Check ParameterList first (parsed)
|
|
var param = query.ParameterList.FirstOrDefault(p => p.Name.Equals(paramName, StringComparison.OrdinalIgnoreCase));
|
|
if (param != null)
|
|
{
|
|
queriesUsing++;
|
|
lastValue = param.Value;
|
|
}
|
|
// Also check Parameters dictionary (manually added)
|
|
else if (query.Parameters.TryGetValue(paramName, out var dictValue))
|
|
{
|
|
queriesUsing++;
|
|
lastValue = dictValue;
|
|
}
|
|
}
|
|
|
|
yield return new ParameterUsageReport
|
|
{
|
|
ParameterName = paramName,
|
|
Value = lastValue,
|
|
UsedInQueryCount = queriesUsing,
|
|
TotalQueries = _queryBreakdowns.Count
|
|
};
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Helper method to extract table names from a FROM clause.
|
|
/// </summary>
|
|
private static IEnumerable<string> ExtractTableNames(string fromClause)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(fromClause))
|
|
{
|
|
yield break;
|
|
}
|
|
|
|
// Simple extraction: split by comma and clean up aliases
|
|
var parts = fromClause.Split(',');
|
|
foreach (var part in parts)
|
|
{
|
|
var trimmed = part.Trim();
|
|
|
|
// Remove alias (assuming format: table AS alias or table alias)
|
|
var tokens = trimmed.Split(new[] { " AS ", " " }, StringSplitOptions.RemoveEmptyEntries);
|
|
if (tokens.Length > 0)
|
|
{
|
|
var tableName = tokens[0].Trim();
|
|
if (!string.IsNullOrWhiteSpace(tableName))
|
|
{
|
|
yield return tableName;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Represents parameter usage information for a specific parameter across all queries in a collection.
|
|
/// </summary>
|
|
public class ParameterUsageReport
|
|
{
|
|
/// <summary>
|
|
/// Gets or sets the parameter name.
|
|
/// </summary>
|
|
public string ParameterName { get; set; } = string.Empty;
|
|
|
|
/// <summary>
|
|
/// Gets or sets the parameter value.
|
|
/// </summary>
|
|
public object? Value { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the number of queries using this parameter.
|
|
/// </summary>
|
|
public int UsedInQueryCount { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the total number of queries in the collection.
|
|
/// </summary>
|
|
public int TotalQueries { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets whether the parameter is used in all queries.
|
|
/// </summary>
|
|
public bool IsUsedInAllQueries => UsedInQueryCount == TotalQueries;
|
|
|
|
/// <summary>
|
|
/// Returns a string representation of the parameter usage report for PostgreSQL parameters.
|
|
/// </summary>
|
|
public override string ToString()
|
|
{
|
|
var usagePercentage = TotalQueries > 0 ? (UsedInQueryCount / (decimal)TotalQueries * 100) : 0;
|
|
var paramSyntax = int.TryParse(ParameterName, out _) ? $"${ParameterName}" : $":{ParameterName}";
|
|
return $"{paramSyntax}: {UsedInQueryCount}/{TotalQueries} queries ({usagePercentage:F1}%) - Value: {Value?.ToString() ?? "NULL"}";
|
|
}
|
|
}
|