using System.Runtime.Serialization; using System.Text; using Strata.SqlTools.SqlBreakdown.Classes; using Strata.SqlTools.SqlBreakdown.Interfaces; namespace Strata.SqlTools.Breakdowns.PostgreSql; /// /// PostgreSQL-specific collection for managing multiple QueryBreakdown objects. /// /// /// 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. /// [Serializable] public class QueryBreakdownCollection : SqlBreakdownCollection { private readonly List _queryBreakdowns; /// /// Initializes a new instance of the class for PostgreSQL. /// public QueryBreakdownCollection() : base() { _queryBreakdowns = new List(); } /// /// Initializes a new instance of the class with initial query breakdowns. /// /// The initial collection of query breakdowns. public QueryBreakdownCollection(IEnumerable queryBreakdowns) : base(queryBreakdowns?.Cast() ?? Enumerable.Empty()) { _queryBreakdowns = queryBreakdowns?.ToList() ?? new List(); } /// /// Validates that the backing list survived deserialization, since deserialization bypasses /// the constructors that normally initialize it (SonarQube rule S5766). /// /// The streaming context for the deserialization operation. [OnDeserialized] private void OnDeserialized(StreamingContext context) { if (_queryBreakdowns is null) { throw new SerializationException("Deserialized QueryBreakdownCollection is missing its backing list."); } } /// /// Gets the collection of QueryBreakdown objects. /// public IReadOnlyList QueryBreakdowns => _queryBreakdowns.AsReadOnly(); /// /// Adds a QueryBreakdown to the collection. /// /// The QueryBreakdown to add. public void Add(QueryBreakdown queryBreakdown) { if (queryBreakdown != null) { _queryBreakdowns.Add(queryBreakdown); base.Add(queryBreakdown); } } /// /// Adds multiple QueryBreakdowns to the collection. /// /// The QueryBreakdowns to add. public void AddRange(IEnumerable queryBreakdowns) { foreach (var qb in queryBreakdowns ?? new List()) { Add(qb); } } /// /// Removes a QueryBreakdown from the collection. /// /// True if removed; otherwise, false. public bool Remove(QueryBreakdown queryBreakdown) { base.Remove(queryBreakdown); return _queryBreakdowns.Remove(queryBreakdown); } /// /// Clears all query breakdowns from the collection. /// public new void Clear() { _queryBreakdowns.Clear(); base.Clear(); } /// /// Gets the PostgreSQL SQL batch representation with proper statement separation. /// /// /// Generates PostgreSQL SQL with proper semi-colon separation for multiple statements. /// /// The complete SQL batch as a single string. 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(); } /// /// Parses a batch of PostgreSQL SQL statements into a collection. /// /// The SQL batch to parse. /// True if parsing succeeded; false otherwise. 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; } } /// /// Gets a summary of all queries including their types and basic composition. /// /// Summary information for each query. public IEnumerable 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 }); } /// /// Gets the total number of selected columns across all queries. /// /// Total column count. public int GetTotalSelectedColumns() { return _queryBreakdowns.Sum(q => !string.IsNullOrWhiteSpace(q.SelectClause?.Clause) ? q.SelectClause.Clause.Split(',').Length : 0); } /// /// Gets all unique table names referenced across all queries. /// /// /// 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. /// /// List of unique table names. public IEnumerable GetUniqueTableReferences() { var tables = new HashSet(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; } /// /// Gets parameter usage information across all queries. /// /// Parameter usage information. public IEnumerable GetParameterUsageReport() { // Collect all unique parameter names from both ParameterList and Parameters dictionary var allParamNames = new HashSet(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 }; } } /// /// Helper method to extract table names from a FROM clause. /// private static IEnumerable 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; } } } } } /// /// Represents parameter usage information for a specific parameter across all queries in a collection. /// public class ParameterUsageReport { /// /// Gets or sets the parameter name. /// public string ParameterName { get; set; } = string.Empty; /// /// Gets or sets the parameter value. /// public object? Value { get; set; } /// /// Gets or sets the number of queries using this parameter. /// public int UsedInQueryCount { get; set; } /// /// Gets or sets the total number of queries in the collection. /// public int TotalQueries { get; set; } /// /// Gets whether the parameter is used in all queries. /// public bool IsUsedInAllQueries => UsedInQueryCount == TotalQueries; /// /// Returns a string representation of the parameter usage report for PostgreSQL parameters. /// 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"}"; } }