diff --git a/src/Strata.SqlTools.Markdown/Common/CollectionReportWriter.cs b/src/Strata.SqlTools.Markdown/Common/CollectionReportWriter.cs new file mode 100644 index 0000000..0223dff --- /dev/null +++ b/src/Strata.SqlTools.Markdown/Common/CollectionReportWriter.cs @@ -0,0 +1,456 @@ +using System.Text; +using Strata.SqlTools.Breakdowns.SqlServer; + +namespace Strata.SqlTools.Markdown.Common; + +/// +/// Describes the dialect-specific formatting choices used when rendering a collection report. +/// +/// +/// The structural Markdown is identical across dialects; only these hooks differ (parameter syntax, +/// data-type names, and Mermaid node colors), so the per-dialect generators supply an instance of this +/// type and delegate the heavy lifting to . +/// +internal sealed class MarkdownDialectFormat +{ + /// Formats a parameter name as it appears in tables and bullet lists (e.g. @id). + public required Func ParameterTableLabel { get; init; } + + /// Formats a parameter name as it appears inside a Mermaid node (e.g. :id). + public required Func ParameterNodeLabel { get; init; } + + /// Maps a parameter value to the dialect's data-type name. + public required Func ParameterTypeName { get; init; } + + /// Mermaid fill color for parameter nodes. + public required string ParameterNodeFill { get; init; } + + /// Mermaid fill color for query nodes. + public required string QueryNodeFill { get; init; } + + /// Whether the query-composition table includes a HAVING clause row. + public required bool IncludeHavingRow { get; init; } +} + +/// +/// Carries the precomputed usage statistics for a single parameter, decoupling the writer from each +/// dialect's concrete parameter-usage report type. +/// +internal sealed class ParameterUsageRow +{ + /// The parameter name (without any dialect prefix). + public required string ParameterName { get; init; } + + /// Whether the parameter is used by every query in the collection. + public required bool IsUsedInAllQueries { get; init; } + + /// The number of queries that use the parameter. + public required int UsedInQueryCount { get; init; } + + /// The total number of queries in the collection. + public required int TotalQueries { get; init; } + + /// The parameter's last-seen value. + public required object? Value { get; init; } +} + +/// +/// Renders the dialect-agnostic Markdown for query breakdown collection reports. The per-dialect +/// generators supply a and precomputed data; this writer produces +/// the identical structural Markdown that previously lived in each dialect generator. +/// +internal static class CollectionReportWriter +{ + /// + /// Assembles a full collection report from a title and the already-rendered sections, separating + /// sections with a blank line. + /// + public static string CollectionReport(string? title, IEnumerable sections) + { + var sb = new StringBuilder(); + + if (!string.IsNullOrWhiteSpace(title)) + { + sb.AppendLine($"# {title}"); + sb.AppendLine(); + } + + var first = true; + foreach (var section in sections) + { + if (!first) + { + sb.AppendLine(); + } + + sb.Append(section); + first = false; + } + + return sb.ToString(); + } + + /// + /// Renders the collection summary table. + /// + public static string CollectionSummary(int queryCount, int uniqueParameterCount, int totalSelectedColumns, int uniqueTableCount) + { + var sb = new StringBuilder(); + sb.AppendLine("## Collection Summary"); + sb.AppendLine(); + + sb.AppendLine("| Metric | Value |"); + sb.AppendLine("|--------|-------|"); + sb.AppendLine($"| Total Queries | {queryCount} |"); + sb.AppendLine($"| Total Parameters | {uniqueParameterCount} |"); + sb.AppendLine($"| Total Columns Selected | {totalSelectedColumns} |"); + sb.AppendLine($"| Unique Tables | {uniqueTableCount} |"); + + return sb.ToString(); + } + + /// + /// Renders the parameter analysis section, including the dependency diagram. + /// + public static string ParameterAnalysis( + IReadOnlyList parameters, + IEnumerable queries, + MarkdownDialectFormat format) + { + var sb = new StringBuilder(); + sb.AppendLine("## Parameter Analysis"); + sb.AppendLine(); + + if (parameters.Count == 0) + { + sb.AppendLine("### Parameters"); + sb.AppendLine(); + sb.AppendLine("No parameters are used in this collection."); + return sb.ToString(); + } + + sb.AppendLine("### Parameters"); + sb.AppendLine(); + sb.AppendLine("| Parameter | Type | Used In | Value |"); + sb.AppendLine("|-----------|------|---------|-------|"); + + foreach (var param in parameters.OrderBy(p => p.ParameterName)) + { + var usageIndicator = param.IsUsedInAllQueries ? "✓ All" : $"{param.UsedInQueryCount}/{param.TotalQueries}"; + var value = param.Value?.ToString() ?? "NULL"; + + sb.AppendLine($"| {format.ParameterTableLabel(param.ParameterName)} | {format.ParameterTypeName(param.Value)} | {usageIndicator} | `{EscapeMarkdown(value)}` |"); + } + + sb.AppendLine(); + sb.AppendLine("### Parameter Dependency Diagram"); + sb.AppendLine(); + sb.Append(ParameterDependencyDiagram(queries, format)); + + return sb.ToString(); + } + + /// + /// Renders the Mermaid diagram showing parameter dependencies across queries. + /// + public static string ParameterDependencyDiagram(IEnumerable queries, MarkdownDialectFormat format) + { + var queryBreakdowns = queries as IReadOnlyList ?? queries.ToList(); + + var sb = new StringBuilder(); + sb.AppendLine("```mermaid"); + sb.AppendLine("graph TD"); + sb.AppendLine(); + + // Collect all unique parameter names from both ParameterList and Parameters dictionary + var allParamNames = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (var query in queryBreakdowns) + { + foreach (var param in query.ParameterList) + { + allParamNames.Add(param.Name); + } + foreach (var paramName in query.Parameters.Keys) + { + allParamNames.Add(paramName); + } + } + + var parameters = allParamNames.OrderBy(p => p).ToList(); + + // Create parameter nodes + for (int i = 0; i < parameters.Count; i++) + { + var paramNode = $"param{i}"; + sb.AppendLine($" {paramNode}[\"{format.ParameterNodeLabel(parameters[i])}\"]"); + sb.AppendLine($" style {paramNode} fill:{format.ParameterNodeFill}"); + } + + sb.AppendLine(); + + // Create query nodes and connections + for (int i = 0; i < queryBreakdowns.Count; i++) + { + var query = queryBreakdowns[i]; + var queryNode = $"query{i}"; + var queryType = DetermineQueryType(query); + + sb.AppendLine($" {queryNode}[\"Query #{i}: {queryType}\"]"); + sb.AppendLine($" style {queryNode} fill:{format.QueryNodeFill}"); + + // Collect all parameter names used by this query + var queryParamNames = new HashSet(StringComparer.OrdinalIgnoreCase); + + // Add from ParameterList (parsed parameters) + foreach (var param in query.ParameterList) + { + queryParamNames.Add(param.Name); + } + + // Add from Parameters dictionary (manually added parameters) + foreach (var paramName in query.Parameters.Keys) + { + queryParamNames.Add(paramName); + } + + // Connect parameters to this query + foreach (var paramName in queryParamNames) + { + var paramIndex = parameters.FindIndex(p => p.Equals(paramName, StringComparison.OrdinalIgnoreCase)); + if (paramIndex >= 0) + { + var paramNode = $"param{paramIndex}"; + sb.AppendLine($" {paramNode} --> {queryNode}"); + } + } + + sb.AppendLine(); + } + + sb.AppendLine("```"); + sb.AppendLine(); + + return sb.ToString(); + } + + /// + /// Renders the per-query composition report. The optional hook lets a + /// dialect append additional per-query sections (for example, Snowflake feature notes). + /// + public static string QueryCompositionReport( + IReadOnlyList queries, + MarkdownDialectFormat format, + Action? appendExtra = null) + where TQuery : QueryBreakdown + { + var sb = new StringBuilder(); + sb.AppendLine("## Query Composition Report"); + sb.AppendLine(); + + for (int i = 0; i < queries.Count; i++) + { + var query = queries[i]; + + AppendQueryCompositionTable(sb, i, query, format.IncludeHavingRow); + AppendQueryParameters(sb, query, format); + AppendQueryCteSections(sb, query); + appendExtra?.Invoke(sb, query); + } + + return sb.ToString(); + } + + /// + /// Renders a batch execution flow diagram. and + /// are the optional opening/closing node labels (for example, transaction or session-setup statements); + /// pass null to omit either. + /// + public static string BatchFlowDiagram(int queryCount, string? openLabel, string? closeLabel) + { + var sb = new StringBuilder(); + sb.AppendLine("```mermaid"); + sb.AppendLine("flowchart TD"); + sb.AppendLine(); + + // Handle empty collection + if (queryCount == 0) + { + if (openLabel != null) + { + sb.AppendLine($" Start([Batch Start]) --> node0[\"{openLabel}\"]"); + if (closeLabel != null) + { + sb.AppendLine($" node0 --> node1[\"{closeLabel}\"]"); + sb.AppendLine($" node1 --> End([Batch Complete])"); + } + else + { + sb.AppendLine($" node0 --> End([Batch Complete])"); + } + } + else + { + sb.AppendLine($" Start([Batch Start]) --> End([Batch Complete])"); + } + sb.AppendLine("```"); + sb.AppendLine(); + return sb.ToString(); + } + + int nodeId = 0; + + // Start node + if (openLabel != null) + { + sb.AppendLine($" node{nodeId}[\"{openLabel}\"]"); + sb.AppendLine($" Start([Batch Start]) --> node{nodeId}"); + nodeId++; + sb.AppendLine($" node{nodeId - 1} --> node{nodeId}"); + } + else + { + sb.AppendLine($" Start([Batch Start]) --> node{nodeId}"); + } + + // Query nodes + for (int i = 0; i < queryCount; i++) + { + if (i < queryCount - 1) + { + // Not the last query - connect to next + sb.AppendLine($" node{nodeId}[\"Query {i}\"] --> node{nodeId + 1}"); + nodeId++; + } + else if (closeLabel != null) + { + // Last query - connect to the closing node + sb.AppendLine($" node{nodeId}[\"Query {i}\"] --> node{nodeId + 1}"); + nodeId++; + sb.AppendLine($" node{nodeId}[\"{closeLabel}\"]"); + sb.AppendLine($" node{nodeId} --> End([Batch Complete])"); + } + else + { + // Last query - connect straight to the end + sb.AppendLine($" node{nodeId}[\"Query {i}\"] --> End([Batch Complete])"); + } + } + + sb.AppendLine("```"); + sb.AppendLine(); + + return sb.ToString(); + } + + /// + /// Appends the query composition table for a single query. + /// + private static void AppendQueryCompositionTable(StringBuilder sb, int queryIndex, QueryBreakdown query, bool includeHavingRow) + { + var hasSelect = !string.IsNullOrWhiteSpace(query.SelectClause?.Clause); + + sb.AppendLine($"### Query #{queryIndex}"); + sb.AppendLine(); + sb.AppendLine("| Aspect | Present |"); + sb.AppendLine("|--------|---------|"); + sb.AppendLine($"| SELECT Clause | {FormatClausePresence(hasSelect)} |"); + sb.AppendLine($"| FROM Clause | {FormatClausePresence(!string.IsNullOrWhiteSpace(query.FromClause?.Clause))} |"); + sb.AppendLine($"| WHERE Clause | {FormatClausePresence(!string.IsNullOrWhiteSpace(query.WhereClause?.Clause))} |"); + sb.AppendLine($"| GROUP BY Clause | {FormatClausePresence(!string.IsNullOrWhiteSpace(query.GroupByClause?.Clause))} |"); + if (includeHavingRow) + { + sb.AppendLine($"| HAVING Clause | {FormatClausePresence(!string.IsNullOrWhiteSpace(query.HavingClause?.Clause))} |"); + } + sb.AppendLine($"| ORDER BY Clause | {FormatClausePresence(!string.IsNullOrWhiteSpace(query.OrderByClause?.Clause))} |"); + sb.AppendLine($"| CTE (WITH) | {FormatClausePresence(query.WithClauses.Count > 0)} |"); + sb.AppendLine($"| Columns | {(hasSelect ? query.SelectClause!.Clause!.Split(',').Length : 0)} |"); + sb.AppendLine($"| Parameters | {query.ParameterList.Count()} |"); + sb.AppendLine(); + } + + /// + /// Appends parameter information for a query. + /// + private static void AppendQueryParameters(StringBuilder sb, QueryBreakdown query, MarkdownDialectFormat format) + { + // Collect all unique parameters from both ParameterList and Parameters dictionary + var allParams = new Dictionary(StringComparer.OrdinalIgnoreCase); + + // Add from ParameterList (parsed parameters) + foreach (var param in query.ParameterList) + { + allParams[param.Name] = param.Value; + } + + // Add from Parameters dictionary (manually added parameters) + foreach (var param in query.Parameters) + { + allParams[param.Key] = param.Value; + } + + if (allParams.Count == 0) + { + return; + } + + sb.AppendLine("**Parameters Used:**"); + sb.AppendLine(); + foreach (var paramName in allParams.Keys.OrderBy(k => k, StringComparer.OrdinalIgnoreCase)) + { + var value = allParams[paramName]; + sb.AppendLine($"- `{format.ParameterNodeLabel(paramName)}` = `{value?.ToString() ?? "NULL"}`"); + } + sb.AppendLine(); + } + + /// + /// Appends the CTE section for a query when it defines any. + /// + private static void AppendQueryCteSections(StringBuilder sb, QueryBreakdown query) + { + if (query.WithClauses.Count == 0) + { + return; + } + + sb.AppendLine("**CTEs Defined:**"); + sb.AppendLine(); + foreach (var cte in query.WithClauses) + { + sb.AppendLine($"- `{cte.TableName}`"); + } + sb.AppendLine(); + } + + /// + /// Escapes special Markdown characters. + /// + private static string EscapeMarkdown(string text) + { + return text + .Replace("\\", "\\\\") + .Replace("|", "\\|") + .Replace("\n", "\\n"); + } + + /// + /// Formats clause presence as Yes/No with checkmark/cross. + /// + private static string FormatClausePresence(bool isPresent) + => isPresent ? "✓ Yes" : "✗ No"; + + /// + /// Determines the query type from a QueryBreakdown. + /// + private static string DetermineQueryType(QueryBreakdown query) + { + var hasSelect = !string.IsNullOrWhiteSpace(query.SelectClause?.Clause); + if (hasSelect) + { + return "SELECT"; + } + + var hasFrom = !string.IsNullOrWhiteSpace(query.FromClause?.Clause); + return hasFrom ? "FROM" : "QUERY"; + } +} diff --git a/src/Strata.SqlTools.Markdown/PostgreSql/QueryBreakdownCollectionGenerator.cs b/src/Strata.SqlTools.Markdown/PostgreSql/QueryBreakdownCollectionGenerator.cs index 3dce045..4fb82fd 100644 --- a/src/Strata.SqlTools.Markdown/PostgreSql/QueryBreakdownCollectionGenerator.cs +++ b/src/Strata.SqlTools.Markdown/PostgreSql/QueryBreakdownCollectionGenerator.cs @@ -1,6 +1,5 @@ -using System.Text; using Strata.SqlTools.Breakdowns.PostgreSql; -using QuerySummary = Strata.SqlTools.Breakdowns.SqlServer.QuerySummary; +using Strata.SqlTools.Markdown.Common; namespace Strata.SqlTools.Markdown.PostgreSql; @@ -11,6 +10,16 @@ namespace Strata.SqlTools.Markdown.PostgreSql; /// public static class QueryBreakdownCollectionGenerator { + private static readonly MarkdownDialectFormat Format = new() + { + ParameterTableLabel = FormatParameter, + ParameterNodeLabel = FormatParameter, + ParameterTypeName = GetParameterType, + ParameterNodeFill = "#e8f5e9", + QueryNodeFill = "#fff3e0", + IncludeHavingRow = true + }; + /// /// Generates a comprehensive collection report in Markdown format with PostgreSQL-specific information. /// @@ -18,28 +27,12 @@ public static class QueryBreakdownCollectionGenerator /// Optional title for the report. /// A string containing the Markdown documentation. public static string GenerateCollectionReport(QueryBreakdownCollection collection, string? title = null) - { - var sb = new StringBuilder(); - - if (!string.IsNullOrWhiteSpace(title)) + => CollectionReportWriter.CollectionReport(title, new[] { - sb.AppendLine($"# {title}"); - sb.AppendLine(); - } - - // Collection Summary - sb.Append(GenerateCollectionSummary(collection)); - sb.AppendLine(); - - // Parameter Analysis - sb.Append(GenerateParameterAnalysis(collection)); - sb.AppendLine(); - - // Query Composition Report - sb.Append(GenerateQueryCompositionReport(collection)); - - return sb.ToString(); - } + GenerateCollectionSummary(collection), + GenerateParameterAnalysis(collection), + GenerateQueryCompositionReport(collection) + }); /// /// Generates a summary section for the collection. @@ -47,20 +40,11 @@ public static class QueryBreakdownCollectionGenerator /// The QueryBreakdownCollection to summarize. /// Markdown summary section. public static string GenerateCollectionSummary(QueryBreakdownCollection collection) - { - var sb = new StringBuilder(); - sb.AppendLine("## Collection Summary"); - sb.AppendLine(); - - sb.AppendLine("| Metric | Value |"); - sb.AppendLine("|--------|-------|"); - sb.AppendLine($"| Total Queries | {collection.QueryBreakdowns.Count} |"); - sb.AppendLine($"| Total Parameters | {collection.GetAllUniqueParameters().Count()} |"); - sb.AppendLine($"| Total Columns Selected | {collection.GetTotalSelectedColumns()} |"); - sb.AppendLine($"| Unique Tables | {collection.GetUniqueTableReferences().Count()} |"); - - return sb.ToString(); - } + => CollectionReportWriter.CollectionSummary( + collection.QueryBreakdowns.Count, + collection.GetAllUniqueParameters().Count(), + collection.GetTotalSelectedColumns(), + collection.GetUniqueTableReferences().Count()); /// /// Generates a parameter analysis report with PostgreSQL parameter syntax support. @@ -68,44 +52,7 @@ public static class QueryBreakdownCollectionGenerator /// The QueryBreakdownCollection to analyze. /// Markdown parameter analysis section. public static string GenerateParameterAnalysis(QueryBreakdownCollection collection) - { - var sb = new StringBuilder(); - var paramReport = collection.GetParameterUsageReport().ToList(); - - sb.AppendLine("## Parameter Analysis"); - sb.AppendLine(); - - if (paramReport.Count == 0) - { - sb.AppendLine("### Parameters"); - sb.AppendLine(); - sb.AppendLine("No parameters are used in this collection."); - return sb.ToString(); - } - - sb.AppendLine("### Parameters"); - sb.AppendLine(); - sb.AppendLine("| Parameter | Type | Used In | Value |"); - sb.AppendLine("|-----------|------|---------|-------|"); - - foreach (var param in paramReport.OrderBy(p => p.ParameterName)) - { - var usageIndicator = param.IsUsedInAllQueries ? "✓ All" : $"{param.UsedInQueryCount}/{param.TotalQueries}"; - var value = param.Value?.ToString() ?? "NULL"; - // PostgreSQL supports both $n positional and :named parameters - var paramSyntax = int.TryParse(param.ParameterName, out _) - ? $"${param.ParameterName}" - : $":{param.ParameterName}"; - sb.AppendLine($"| {paramSyntax} | {GetParameterType(param.Value)} | {usageIndicator} | `{EscapeMarkdown(value)}` |"); - } - - sb.AppendLine(); - sb.AppendLine("### Parameter Dependency Diagram"); - sb.AppendLine(); - sb.Append(GenerateParameterDependencyDiagram(collection)); - - return sb.ToString(); - } + => CollectionReportWriter.ParameterAnalysis(MapParameters(collection), collection.QueryBreakdowns, Format); /// /// Generates a Mermaid diagram showing parameter dependencies across queries. @@ -113,87 +60,7 @@ public static class QueryBreakdownCollectionGenerator /// The QueryBreakdownCollection to visualize. /// Mermaid diagram markdown. public static string GenerateParameterDependencyDiagram(QueryBreakdownCollection collection) - { - var sb = new StringBuilder(); - sb.AppendLine("```mermaid"); - sb.AppendLine("graph TD"); - sb.AppendLine(); - - var queryBreakdowns = collection.QueryBreakdowns; - - // Collect all unique parameter names from both ParameterList and Parameters dictionary - var allParamNames = new HashSet(StringComparer.OrdinalIgnoreCase); - foreach (var query in queryBreakdowns) - { - foreach (var param in query.ParameterList) - { - allParamNames.Add(param.Name); - } - foreach (var paramName in query.Parameters.Keys) - { - allParamNames.Add(paramName); - } - } - - var parameters = allParamNames.OrderBy(p => p).ToList(); - - // Create parameter nodes - for (int i = 0; i < parameters.Count; i++) - { - var paramNode = $"param{i}"; - var paramSyntax = int.TryParse(parameters[i], out _) - ? $"${parameters[i]}" - : $":{parameters[i]}"; - sb.AppendLine($" {paramNode}[\"{paramSyntax}\"]"); - sb.AppendLine($" style {paramNode} fill:#e8f5e9"); - } - - sb.AppendLine(); - - // Create query nodes and connections - for (int i = 0; i < queryBreakdowns.Count; i++) - { - var query = queryBreakdowns[i]; - var queryNode = $"query{i}"; - var queryType = DetermineQueryType(query); - - sb.AppendLine($" {queryNode}[\"Query #{i}: {queryType}\"]"); - sb.AppendLine($" style {queryNode} fill:#fff3e0"); - - // Collect all parameter names used by this query - var queryParamNames = new HashSet(StringComparer.OrdinalIgnoreCase); - - // Add from ParameterList (parsed parameters) - foreach (var param in query.ParameterList) - { - queryParamNames.Add(param.Name); - } - - // Add from Parameters dictionary (manually added parameters) - foreach (var paramName in query.Parameters.Keys) - { - queryParamNames.Add(paramName); - } - - // Connect parameters to this query - foreach (var paramName in queryParamNames) - { - var paramIndex = parameters.FindIndex(p => p.Equals(paramName, StringComparison.OrdinalIgnoreCase)); - if (paramIndex >= 0) - { - var paramNode = $"param{paramIndex}"; - sb.AppendLine($" {paramNode} --> {queryNode}"); - } - } - - sb.AppendLine(); - } - - sb.AppendLine("```"); - sb.AppendLine(); - - return sb.ToString(); - } + => CollectionReportWriter.ParameterDependencyDiagram(collection.QueryBreakdowns, Format); /// /// Generates a detailed query composition report. @@ -201,103 +68,7 @@ public static class QueryBreakdownCollectionGenerator /// The QueryBreakdownCollection to report on. /// Markdown composition report section. public static string GenerateQueryCompositionReport(QueryBreakdownCollection collection) - { - var sb = new StringBuilder(); - sb.AppendLine("## Query Composition Report"); - sb.AppendLine(); - - var summaries = collection.GetQuerySummaries().ToList(); - - for (int i = 0; i < summaries.Count; i++) - { - var summary = summaries[i]; - var query = collection.QueryBreakdowns[i]; - - AppendQueryCompositionTable(sb, i, summary); - AppendQueryParameters(sb, query); - AppendQueryCteSections(sb, summary, query); - } - - return sb.ToString(); - } - - /// - /// Appends the query composition table for a single query. - /// - private static void AppendQueryCompositionTable(StringBuilder sb, int queryIndex, QuerySummary summary) - { - sb.AppendLine($"### Query #{queryIndex}"); - sb.AppendLine(); - sb.AppendLine("| Aspect | Present |"); - sb.AppendLine("|--------|---------|"); - sb.AppendLine($"| SELECT Clause | {FormatClausePresence(summary.HasSelectClause)} |"); - sb.AppendLine($"| FROM Clause | {FormatClausePresence(summary.HasFromClause)} |"); - sb.AppendLine($"| WHERE Clause | {FormatClausePresence(summary.HasWhereClause)} |"); - sb.AppendLine($"| GROUP BY Clause | {FormatClausePresence(summary.HasGroupByClause)} |"); - sb.AppendLine($"| HAVING Clause | {FormatClausePresence(summary.HasHavingClause)} |"); - sb.AppendLine($"| ORDER BY Clause | {FormatClausePresence(summary.HasOrderByClause)} |"); - sb.AppendLine($"| CTE (WITH) | {FormatClausePresence(summary.HasCTE)} |"); - sb.AppendLine($"| Columns | {summary.ColumnCount} |"); - sb.AppendLine($"| Parameters | {summary.ParameterCount} |"); - sb.AppendLine(); - } - - /// - /// Appends parameter information for a query. - /// - private static void AppendQueryParameters(StringBuilder sb, QueryBreakdown query) - { - // Collect all unique parameters from both ParameterList and Parameters dictionary - var allParams = new Dictionary(StringComparer.OrdinalIgnoreCase); - - // Add from ParameterList (parsed parameters) - foreach (var param in query.ParameterList) - { - allParams[param.Name] = param.Value; - } - - // Add from Parameters dictionary (manually added parameters) - foreach (var param in query.Parameters) - { - allParams[param.Key] = param.Value; - } - - if (allParams.Count == 0) - { - return; - } - - sb.AppendLine("**Parameters Used:**"); - sb.AppendLine(); - foreach (var paramName in allParams.Keys.OrderBy(k => k, StringComparer.OrdinalIgnoreCase)) - { - var value = allParams[paramName]; - var paramSyntax = int.TryParse(paramName, out _) - ? $"${paramName}" - : $":{paramName}"; - sb.AppendLine($"- `{paramSyntax}` = `{value?.ToString() ?? "NULL"}`"); - } - sb.AppendLine(); - } - - /// - /// Appends CTE section for a query. - /// - private static void AppendQueryCteSections(StringBuilder sb, QuerySummary summary, QueryBreakdown query) - { - if (!summary.HasCTE) - { - return; - } - - sb.AppendLine("**CTEs Defined:**"); - sb.AppendLine(); - foreach (var cte in query.WithClauses) - { - sb.AppendLine($"- `{cte.TableName}`"); - } - sb.AppendLine(); - } + => CollectionReportWriter.QueryCompositionReport(collection.QueryBreakdowns, Format); /// /// Generates a batch execution flow diagram for PostgreSQL. @@ -306,76 +77,31 @@ public static class QueryBreakdownCollectionGenerator /// Whether to show transaction wrapping. /// Mermaid diagram markdown. public static string GenerateBatchFlowDiagram(QueryBreakdownCollection collection, bool includeTransaction = false) - { - var sb = new StringBuilder(); - sb.AppendLine("```mermaid"); - sb.AppendLine("flowchart TD"); - sb.AppendLine(); + => CollectionReportWriter.BatchFlowDiagram( + collection.QueryBreakdowns.Count, + includeTransaction ? "BEGIN" : null, + includeTransaction ? "COMMIT" : null); - int nodeId = 0; - - // Handle empty collection - if (collection.QueryBreakdowns.Count == 0) - { - if (includeTransaction) + /// + /// Maps the collection's parameter usage report into the writer's dialect-agnostic rows. + /// + private static List MapParameters(QueryBreakdownCollection collection) + => collection.GetParameterUsageReport() + .Select(p => new ParameterUsageRow { - sb.AppendLine($" Start([Batch Start]) --> node0[\"BEGIN\"]"); - sb.AppendLine($" node0 --> node1[\"COMMIT\"]"); - sb.AppendLine($" node1 --> End([Batch Complete])"); - } - else - { - sb.AppendLine($" Start([Batch Start]) --> End([Batch Complete])"); - } - sb.AppendLine("```"); - sb.AppendLine(); - return sb.ToString(); - } + ParameterName = p.ParameterName, + IsUsedInAllQueries = p.IsUsedInAllQueries, + UsedInQueryCount = p.UsedInQueryCount, + TotalQueries = p.TotalQueries, + Value = p.Value + }) + .ToList(); - // Start node - if (includeTransaction) - { - sb.AppendLine($" node{nodeId}[\"BEGIN\"]"); - sb.AppendLine($" Start([Batch Start]) --> node{nodeId}"); - nodeId++; - sb.AppendLine($" node{nodeId - 1} --> node{nodeId}"); - } - else - { - sb.AppendLine($" Start([Batch Start]) --> node{nodeId}"); - } - - // Query nodes - for (int i = 0; i < collection.QueryBreakdowns.Count; i++) - { - if (i < collection.QueryBreakdowns.Count - 1) - { - // Not the last query - connect to next - sb.AppendLine($" node{nodeId}[\"Query {i}\"] --> node{nodeId + 1}"); - nodeId++; - } - else - { - // Last query - connect to End (or COMMIT if transaction) - if (includeTransaction) - { - sb.AppendLine($" node{nodeId}[\"Query {i}\"] --> node{nodeId + 1}"); - nodeId++; - sb.AppendLine($" node{nodeId}[\"COMMIT\"]"); - sb.AppendLine($" node{nodeId} --> End([Batch Complete])"); - } - else - { - sb.AppendLine($" node{nodeId}[\"Query {i}\"] --> End([Batch Complete])"); - } - } - } - - sb.AppendLine("```"); - sb.AppendLine(); - - return sb.ToString(); - } + /// + /// Formats a parameter using PostgreSQL syntax: $n for positional, :name for named. + /// + private static string FormatParameter(string name) + => int.TryParse(name, out _) ? $"${name}" : $":{name}"; /// /// Gets the parameter type name from a parameter value using PostgreSQL types. @@ -397,36 +123,4 @@ public static class QueryBreakdownCollectionGenerator _ => "UNKNOWN" }; } - - /// - /// Escapes special Markdown characters. - /// - private static string EscapeMarkdown(string text) - { - return text - .Replace("\\", "\\\\") - .Replace("|", "\\|") - .Replace("\n", "\\n"); - } - - /// - /// Formats clause presence as Yes/No with checkmark/cross. - /// - private static string FormatClausePresence(bool isPresent) - => isPresent ? "✓ Yes" : "✗ No"; - - /// - /// Determines the query type from a QueryBreakdown. - /// - private static string DetermineQueryType(QueryBreakdown query) - { - var hasSelect = !string.IsNullOrWhiteSpace(query.SelectClause?.Clause); - if (hasSelect) - { - return "SELECT"; - } - - var hasFrom = !string.IsNullOrWhiteSpace(query.FromClause?.Clause); - return hasFrom ? "FROM" : "QUERY"; - } } diff --git a/src/Strata.SqlTools.Markdown/Snowflake/QueryBreakdownCollectionGenerator.cs b/src/Strata.SqlTools.Markdown/Snowflake/QueryBreakdownCollectionGenerator.cs index f91502f..42b09dd 100644 --- a/src/Strata.SqlTools.Markdown/Snowflake/QueryBreakdownCollectionGenerator.cs +++ b/src/Strata.SqlTools.Markdown/Snowflake/QueryBreakdownCollectionGenerator.cs @@ -1,5 +1,6 @@ using System.Text; using Strata.SqlTools.Breakdowns.Snowflake; +using Strata.SqlTools.Markdown.Common; namespace Strata.SqlTools.Markdown.Snowflake; @@ -10,6 +11,16 @@ namespace Strata.SqlTools.Markdown.Snowflake; /// public static class QueryBreakdownCollectionGenerator { + private static readonly MarkdownDialectFormat Format = new() + { + ParameterTableLabel = name => $":{name} / @{name}", + ParameterNodeLabel = name => $":{name}", + ParameterTypeName = GetParameterType, + ParameterNodeFill = "#e0f2f1", + QueryNodeFill = "#f1f8e9", + IncludeHavingRow = false + }; + /// /// Generates a comprehensive collection report in Markdown format with Snowflake-specific information. /// @@ -17,32 +28,13 @@ public static class QueryBreakdownCollectionGenerator /// Optional title for the report. /// A string containing the Markdown documentation. public static string GenerateCollectionReport(QueryBreakdownCollection collection, string? title = null) - { - var sb = new StringBuilder(); - - if (!string.IsNullOrWhiteSpace(title)) + => CollectionReportWriter.CollectionReport(title, new[] { - sb.AppendLine($"# {title}"); - sb.AppendLine(); - } - - // Collection Summary - sb.Append(GenerateCollectionSummary(collection)); - sb.AppendLine(); - - // Snowflake Features Analysis - sb.Append(GenerateSnowflakeFeaturesAnalysis(collection)); - sb.AppendLine(); - - // Parameter Analysis - sb.Append(GenerateParameterAnalysis(collection)); - sb.AppendLine(); - - // Query Composition Report - sb.Append(GenerateQueryCompositionReport(collection)); - - return sb.ToString(); - } + GenerateCollectionSummary(collection), + GenerateSnowflakeFeaturesAnalysis(collection), + GenerateParameterAnalysis(collection), + GenerateQueryCompositionReport(collection) + }); /// /// Generates a summary section for the collection. @@ -50,20 +42,11 @@ public static class QueryBreakdownCollectionGenerator /// The QueryBreakdownCollection to summarize. /// Markdown summary section. public static string GenerateCollectionSummary(QueryBreakdownCollection collection) - { - var sb = new StringBuilder(); - sb.AppendLine("## Collection Summary"); - sb.AppendLine(); - - sb.AppendLine("| Metric | Value |"); - sb.AppendLine("|--------|-------|"); - sb.AppendLine($"| Total Queries | {collection.QueryBreakdowns.Count} |"); - sb.AppendLine($"| Total Parameters | {collection.GetAllUniqueParameters().Count()} |"); - sb.AppendLine($"| Total Columns Selected | {collection.GetTotalSelectedColumns()} |"); - sb.AppendLine($"| Unique Tables | {collection.GetUniqueTableReferences().Count()} |"); - - return sb.ToString(); - } + => CollectionReportWriter.CollectionSummary( + collection.QueryBreakdowns.Count, + collection.GetAllUniqueParameters().Count(), + collection.GetTotalSelectedColumns(), + collection.GetUniqueTableReferences().Count()); /// /// Generates a Snowflake-specific features analysis section. @@ -93,41 +76,7 @@ public static class QueryBreakdownCollectionGenerator /// The QueryBreakdownCollection to analyze. /// Markdown parameter analysis section. public static string GenerateParameterAnalysis(QueryBreakdownCollection collection) - { - var sb = new StringBuilder(); - var paramReport = collection.GetParameterUsageReport().ToList(); - - sb.AppendLine("## Parameter Analysis"); - sb.AppendLine(); - - if (paramReport.Count == 0) - { - sb.AppendLine("### Parameters"); - sb.AppendLine(); - sb.AppendLine("No parameters are used in this collection."); - return sb.ToString(); - } - - sb.AppendLine("### Parameters"); - sb.AppendLine(); - sb.AppendLine("| Parameter | Type | Used In | Value |"); - sb.AppendLine("|-----------|------|---------|-------|"); - - foreach (var param in paramReport.OrderBy(p => p.ParameterName)) - { - var usageIndicator = param.IsUsedInAllQueries ? "✓ All" : $"{param.UsedInQueryCount}/{param.TotalQueries}"; - var value = param.Value?.ToString() ?? "NULL"; - // Snowflake supports both : and @ syntax for parameters - sb.AppendLine($"| :{param.ParameterName} / @{param.ParameterName} | {GetParameterType(param.Value)} | {usageIndicator} | `{EscapeMarkdown(value)}` |"); - } - - sb.AppendLine(); - sb.AppendLine("### Parameter Dependency Diagram"); - sb.AppendLine(); - sb.Append(GenerateParameterDependencyDiagram(collection)); - - return sb.ToString(); - } + => CollectionReportWriter.ParameterAnalysis(MapParameters(collection), collection.QueryBreakdowns, Format); /// /// Generates a Mermaid diagram showing parameter dependencies across queries. @@ -135,84 +84,7 @@ public static class QueryBreakdownCollectionGenerator /// The QueryBreakdownCollection to visualize. /// Mermaid diagram markdown. public static string GenerateParameterDependencyDiagram(QueryBreakdownCollection collection) - { - var sb = new StringBuilder(); - sb.AppendLine("```mermaid"); - sb.AppendLine("graph TD"); - sb.AppendLine(); - - var queryBreakdowns = collection.QueryBreakdowns; - - // Collect all unique parameter names from both ParameterList and Parameters dictionary - var allParamNames = new HashSet(StringComparer.OrdinalIgnoreCase); - foreach (var query in queryBreakdowns) - { - foreach (var param in query.ParameterList) - { - allParamNames.Add(param.Name); - } - foreach (var paramName in query.Parameters.Keys) - { - allParamNames.Add(paramName); - } - } - - var parameters = allParamNames.OrderBy(p => p).ToList(); - - // Create parameter nodes - for (int i = 0; i < parameters.Count; i++) - { - var paramNode = $"param{i}"; - sb.AppendLine($" {paramNode}[\":{parameters[i]}\"]"); - sb.AppendLine($" style {paramNode} fill:#e0f2f1"); - } - - sb.AppendLine(); - - // Create query nodes and connections - for (int i = 0; i < queryBreakdowns.Count; i++) - { - var query = queryBreakdowns[i]; - var queryNode = $"query{i}"; - var queryType = DetermineQueryType(query); - - sb.AppendLine($" {queryNode}[\"Query #{i}: {queryType}\"]"); - sb.AppendLine($" style {queryNode} fill:#f1f8e9"); - - // Collect all parameter names used by this query - var queryParamNames = new HashSet(StringComparer.OrdinalIgnoreCase); - - // Add from ParameterList (parsed parameters) - foreach (var param in query.ParameterList) - { - queryParamNames.Add(param.Name); - } - - // Add from Parameters dictionary (manually added parameters) - foreach (var paramName in query.Parameters.Keys) - { - queryParamNames.Add(paramName); - } - - // Connect parameters to this query - foreach (var paramName in queryParamNames) - { - var paramIndex = parameters.FindIndex(p => p.Equals(paramName, StringComparison.OrdinalIgnoreCase)); - if (paramIndex >= 0) - { - var paramNode = $"param{paramIndex}"; - sb.AppendLine($" {paramNode} --> {queryNode}"); - } - } - - sb.AppendLine(); - } - - sb.AppendLine("```"); - sb.AppendLine(); - - return sb.ToString(); - } + => CollectionReportWriter.ParameterDependencyDiagram(collection.QueryBreakdowns, Format); /// /// Generates a detailed query composition report with Snowflake-specific information. @@ -221,101 +93,41 @@ public static class QueryBreakdownCollectionGenerator /// Markdown composition report section. public static string GenerateQueryCompositionReport(QueryBreakdownCollection collection) { - var sb = new StringBuilder(); - sb.AppendLine("## Query Composition Report"); - sb.AppendLine(); - - var summaries = collection.GetQuerySummaries().ToList(); var stageQueries = collection.WhereUseStageReference().ToList(); var semiStructured = collection.WhereUseSemiStructuredData().ToList(); - for (int i = 0; i < summaries.Count; i++) - { - var summary = summaries[i]; - var query = collection.QueryBreakdowns[i]; - - AppendQueryCompositionTable(sb, i, summary); - AppendQueryParameters(sb, query); - AppendQueryCteSections(sb, summary, query); - AppendSnowflakeFeatures(sb, query, stageQueries, semiStructured); - } - - return sb.ToString(); + return CollectionReportWriter.QueryCompositionReport( + collection.QueryBreakdowns, + Format, + (sb, query) => AppendSnowflakeFeatures(sb, query, stageQueries, semiStructured)); } /// - /// Appends the query composition table for a single query. + /// Generates a batch execution flow diagram for Snowflake. /// - private static void AppendQueryCompositionTable(StringBuilder sb, int queryIndex, SnowflakeQueryAnalysis summary) - { - sb.AppendLine($"### Query #{queryIndex}"); - sb.AppendLine(); - sb.AppendLine("| Aspect | Present |"); - sb.AppendLine("|--------|---------|"); - sb.AppendLine($"| SELECT Clause | {FormatClausePresence(summary.HasSelectClause)} |"); - sb.AppendLine($"| FROM Clause | {FormatClausePresence(summary.HasFromClause)} |"); - sb.AppendLine($"| WHERE Clause | {FormatClausePresence(summary.HasWhereClause)} |"); - sb.AppendLine($"| GROUP BY Clause | {FormatClausePresence(summary.HasGroupByClause)} |"); - sb.AppendLine($"| ORDER BY Clause | {FormatClausePresence(summary.HasOrderByClause)} |"); - sb.AppendLine($"| CTE (WITH) | {FormatClausePresence(summary.HasCTE)} |"); - sb.AppendLine($"| Columns | {summary.ColumnCount} |"); - sb.AppendLine($"| Parameters | {summary.ParameterCount} |"); - sb.AppendLine(); - } + /// The QueryBreakdownCollection to visualize. + /// Whether to show session setup statements. + /// Mermaid diagram markdown. + public static string GenerateBatchFlowDiagram(QueryBreakdownCollection collection, bool includeSessionSetup = false) + => CollectionReportWriter.BatchFlowDiagram( + collection.QueryBreakdowns.Count, + includeSessionSetup ? "Session Setup" : null, + closeLabel: null); /// - /// Appends parameter information for a query. + /// Maps the collection's parameter usage report into the writer's dialect-agnostic rows. /// - private static void AppendQueryParameters(StringBuilder sb, QueryBreakdown query) - { - // Collect all unique parameters from both ParameterList and Parameters dictionary - var allParams = new Dictionary(StringComparer.OrdinalIgnoreCase); - - // Add from ParameterList (parsed parameters) - foreach (var param in query.ParameterList) - { - allParams[param.Name] = param.Value; - } - - // Add from Parameters dictionary (manually added parameters) - foreach (var param in query.Parameters) - { - allParams[param.Key] = param.Value; - } - - if (allParams.Count == 0) - { - return; - } - - sb.AppendLine("**Parameters Used:**"); - sb.AppendLine(); - foreach (var paramName in allParams.Keys.OrderBy(k => k, StringComparer.OrdinalIgnoreCase)) - { - var value = allParams[paramName]; - sb.AppendLine($"- `:{paramName}` = `{value?.ToString() ?? "NULL"}`"); - } - sb.AppendLine(); - } - - /// - /// Appends CTE section for a query. - /// - private static void AppendQueryCteSections(StringBuilder sb, SnowflakeQueryAnalysis summary, QueryBreakdown query) - { - if (!summary.HasCTE) - { - return; - } - - sb.AppendLine("**CTEs Defined:**"); - sb.AppendLine(); - foreach (var cte in query.WithClauses) - { - sb.AppendLine($"- `{cte.TableName}`"); - } - sb.AppendLine(); - } + private static List MapParameters(QueryBreakdownCollection collection) + => collection.GetParameterUsageReport() + .Select(p => new ParameterUsageRow + { + ParameterName = p.ParameterName, + IsUsedInAllQueries = p.IsUsedInAllQueries, + UsedInQueryCount = p.UsedInQueryCount, + TotalQueries = p.TotalQueries, + Value = p.Value + }) + .ToList(); /// /// Appends Snowflake-specific feature information for a query. @@ -336,80 +148,13 @@ public static class QueryBreakdownCollectionGenerator } /// - /// Formats clause presence as Yes/No with checkmark/cross. + /// Formats feature presence as Yes/No with checkmark/cross. /// - private static string FormatClausePresence(bool isPresent) + private static string FormatFeaturePresence(bool isPresent) => isPresent ? "✓ Yes" : "✗ No"; /// - /// Generates a batch execution flow diagram for Snowflake. - /// - /// The QueryBreakdownCollection to visualize. - /// Whether to show session setup statements. - /// Mermaid diagram markdown. - public static string GenerateBatchFlowDiagram(QueryBreakdownCollection collection, bool includeSessionSetup = false) - { - var sb = new StringBuilder(); - sb.AppendLine("```mermaid"); - sb.AppendLine("flowchart TD"); - sb.AppendLine(); - - // Handle empty collection - if (collection.QueryBreakdowns.Count == 0) - { - if (includeSessionSetup) - { - sb.AppendLine($" Start([Batch Start]) --> node0[\"Session Setup\"]"); - sb.AppendLine($" node0 --> End([Batch Complete])"); - } - else - { - sb.AppendLine($" Start([Batch Start]) --> End([Batch Complete])"); - } - sb.AppendLine("```"); - sb.AppendLine(); - return sb.ToString(); - } - - int nodeId = 0; - - // Start node - if (includeSessionSetup) - { - sb.AppendLine($" node{nodeId}[\"Session Setup\"]"); - sb.AppendLine($" Start([Batch Start]) --> node{nodeId}"); - nodeId++; - sb.AppendLine($" node{nodeId - 1} --> node{nodeId}"); - } - else - { - sb.AppendLine($" Start([Batch Start]) --> node{nodeId}"); - } - - // Query nodes - for (int i = 0; i < collection.QueryBreakdowns.Count; i++) - { - if (i < collection.QueryBreakdowns.Count - 1) - { - // Not the last query - connect to next - sb.AppendLine($" node{nodeId}[\"Query {i}\"] --> node{nodeId + 1}"); - nodeId++; - } - else - { - // Last query - connect to End - sb.AppendLine($" node{nodeId}[\"Query {i}\"] --> End([Batch Complete])"); - } - } - - sb.AppendLine("```"); - sb.AppendLine(); - - return sb.ToString(); - } - - /// - /// Gets the parameter type name from a parameter value. + /// Gets the parameter type name from a parameter value using Snowflake types. /// private static string GetParameterType(object? value) { @@ -424,36 +169,4 @@ public static class QueryBreakdownCollectionGenerator _ => "VARIANT" }; } - - /// - /// Escapes special Markdown characters. - /// - private static string EscapeMarkdown(string text) - { - return text - .Replace("\\", "\\\\") - .Replace("|", "\\|") - .Replace("\n", "\\n"); - } - - /// - /// Formats feature presence as Yes/No with checkmark/cross. - /// - private static string FormatFeaturePresence(bool isPresent) - => isPresent ? "✓ Yes" : "✗ No"; - - /// - /// Determines the query type from a QueryBreakdown. - /// - private static string DetermineQueryType(QueryBreakdown query) - { - var hasSelect = !string.IsNullOrWhiteSpace(query.SelectClause?.Clause); - if (hasSelect) - { - return "SELECT"; - } - - var hasFrom = !string.IsNullOrWhiteSpace(query.FromClause?.Clause); - return hasFrom ? "FROM" : "QUERY"; - } } diff --git a/src/Strata.SqlTools.Markdown/SqlServer/QueryBreakdownCollectionGenerator.cs b/src/Strata.SqlTools.Markdown/SqlServer/QueryBreakdownCollectionGenerator.cs index c8390b2..723907d 100644 --- a/src/Strata.SqlTools.Markdown/SqlServer/QueryBreakdownCollectionGenerator.cs +++ b/src/Strata.SqlTools.Markdown/SqlServer/QueryBreakdownCollectionGenerator.cs @@ -1,5 +1,5 @@ -using System.Text; using Strata.SqlTools.Breakdowns.SqlServer; +using Strata.SqlTools.Markdown.Common; namespace Strata.SqlTools.Markdown.SqlServer; @@ -9,6 +9,16 @@ namespace Strata.SqlTools.Markdown.SqlServer; /// public static class QueryBreakdownCollectionGenerator { + private static readonly MarkdownDialectFormat Format = new() + { + ParameterTableLabel = name => $"@{name}", + ParameterNodeLabel = name => $"@{name}", + ParameterTypeName = GetParameterType, + ParameterNodeFill = "#e1f5ff", + QueryNodeFill = "#f3e5f5", + IncludeHavingRow = true + }; + /// /// Generates a comprehensive collection report in Markdown format. /// @@ -16,28 +26,12 @@ public static class QueryBreakdownCollectionGenerator /// Optional title for the report. /// A string containing the Markdown documentation. public static string GenerateCollectionReport(QueryBreakdownCollection collection, string? title = null) - { - var sb = new StringBuilder(); - - if (!string.IsNullOrWhiteSpace(title)) + => CollectionReportWriter.CollectionReport(title, new[] { - sb.AppendLine($"# {title}"); - sb.AppendLine(); - } - - // Collection Summary - sb.Append(GenerateCollectionSummary(collection)); - sb.AppendLine(); - - // Parameter Analysis - sb.Append(GenerateParameterAnalysis(collection)); - sb.AppendLine(); - - // Query Composition Report - sb.Append(GenerateQueryCompositionReport(collection)); - - return sb.ToString(); - } + GenerateCollectionSummary(collection), + GenerateParameterAnalysis(collection), + GenerateQueryCompositionReport(collection) + }); /// /// Generates a summary section for the collection. @@ -45,20 +39,11 @@ public static class QueryBreakdownCollectionGenerator /// The QueryBreakdownCollection to summarize. /// Markdown summary section. public static string GenerateCollectionSummary(QueryBreakdownCollection collection) - { - var sb = new StringBuilder(); - sb.AppendLine("## Collection Summary"); - sb.AppendLine(); - - sb.AppendLine("| Metric | Value |"); - sb.AppendLine("|--------|-------|"); - sb.AppendLine($"| Total Queries | {collection.QueryBreakdowns.Count} |"); - sb.AppendLine($"| Total Parameters | {collection.GetAllUniqueParameters().Count()} |"); - sb.AppendLine($"| Total Columns Selected | {collection.GetTotalSelectedColumns()} |"); - sb.AppendLine($"| Unique Tables | {collection.GetUniqueTableReferences().Count()} |"); - - return sb.ToString(); - } + => CollectionReportWriter.CollectionSummary( + collection.QueryBreakdowns.Count, + collection.GetAllUniqueParameters().Count(), + collection.GetTotalSelectedColumns(), + collection.GetUniqueTableReferences().Count()); /// /// Generates a parameter analysis report. @@ -66,41 +51,7 @@ public static class QueryBreakdownCollectionGenerator /// The QueryBreakdownCollection to analyze. /// Markdown parameter analysis section. public static string GenerateParameterAnalysis(QueryBreakdownCollection collection) - { - var sb = new StringBuilder(); - var paramReport = collection.GetParameterUsageReport().ToList(); - - sb.AppendLine("## Parameter Analysis"); - sb.AppendLine(); - - if (paramReport.Count == 0) - { - sb.AppendLine("### Parameters"); - sb.AppendLine(); - sb.AppendLine("No parameters are used in this collection."); - return sb.ToString(); - } - - sb.AppendLine("### Parameters"); - sb.AppendLine(); - sb.AppendLine("| Parameter | Type | Used In | Value |"); - sb.AppendLine("|-----------|------|---------|-------|"); - - foreach (var param in paramReport.OrderBy(p => p.ParameterName)) - { - var usageIndicator = param.IsUsedInAllQueries ? "✓ All" : $"{param.UsedInQueryCount}/{param.TotalQueries}"; - var value = param.Value?.ToString() ?? "NULL"; - - sb.AppendLine($"| @{param.ParameterName} | {GetParameterType(param.Value)} | {usageIndicator} | `{EscapeMarkdown(value)}` |"); - } - - sb.AppendLine(); - sb.AppendLine("### Parameter Dependency Diagram"); - sb.AppendLine(); - sb.Append(GenerateParameterDependencyDiagram(collection)); - - return sb.ToString(); - } + => CollectionReportWriter.ParameterAnalysis(MapParameters(collection), collection.QueryBreakdowns, Format); /// /// Generates a Mermaid diagram showing parameter dependencies across queries. @@ -108,84 +59,7 @@ public static class QueryBreakdownCollectionGenerator /// The QueryBreakdownCollection to visualize. /// Mermaid diagram markdown. public static string GenerateParameterDependencyDiagram(QueryBreakdownCollection collection) - { - var sb = new StringBuilder(); - sb.AppendLine("```mermaid"); - sb.AppendLine("graph TD"); - sb.AppendLine(); - - var queryBreakdowns = collection.QueryBreakdowns; - - // Collect all unique parameter names from both ParameterList and Parameters dictionary - var allParamNames = new HashSet(StringComparer.OrdinalIgnoreCase); - foreach (var query in queryBreakdowns) - { - foreach (var param in query.ParameterList) - { - allParamNames.Add(param.Name); - } - foreach (var paramName in query.Parameters.Keys) - { - allParamNames.Add(paramName); - } - } - - var parameters = allParamNames.OrderBy(p => p).ToList(); - - // Create parameter nodes - for (int i = 0; i < parameters.Count; i++) - { - var paramNode = $"param{i}"; - sb.AppendLine($" {paramNode}[\"@{parameters[i]}\"]"); - sb.AppendLine($" style {paramNode} fill:#e1f5ff"); - } - - sb.AppendLine(); - - // Create query nodes and connections - for (int i = 0; i < queryBreakdowns.Count; i++) - { - var query = queryBreakdowns[i]; - var queryNode = $"query{i}"; - var queryType = DetermineQueryType(query); - - sb.AppendLine($" {queryNode}[\"Query #{i}: {queryType}\"]"); - sb.AppendLine($" style {queryNode} fill:#f3e5f5"); - - // Collect all parameter names used by this query - var queryParamNames = new HashSet(StringComparer.OrdinalIgnoreCase); - - // Add from ParameterList (parsed parameters) - foreach (var param in query.ParameterList) - { - queryParamNames.Add(param.Name); - } - - // Add from Parameters dictionary (manually added parameters) - foreach (var paramName in query.Parameters.Keys) - { - queryParamNames.Add(paramName); - } - - // Connect parameters to this query - foreach (var paramName in queryParamNames) - { - var paramIndex = parameters.FindIndex(p => p.Equals(paramName, StringComparison.OrdinalIgnoreCase)); - if (paramIndex >= 0) - { - var paramNode = $"param{paramIndex}"; - sb.AppendLine($" {paramNode} --> {queryNode}"); - } - } - - sb.AppendLine(); - } - - sb.AppendLine("```"); - sb.AppendLine(); - - return sb.ToString(); - } + => CollectionReportWriter.ParameterDependencyDiagram(collection.QueryBreakdowns, Format); /// /// Generates a detailed query composition report. @@ -193,100 +67,7 @@ public static class QueryBreakdownCollectionGenerator /// The QueryBreakdownCollection to report on. /// Markdown composition report section. public static string GenerateQueryCompositionReport(QueryBreakdownCollection collection) - { - var sb = new StringBuilder(); - sb.AppendLine("## Query Composition Report"); - sb.AppendLine(); - - var summaries = collection.GetQuerySummaries().ToList(); - - for (int i = 0; i < summaries.Count; i++) - { - var summary = summaries[i]; - var query = collection.QueryBreakdowns[i]; - - AppendQueryCompositionTable(sb, i, summary); - AppendQueryParameters(sb, query); - AppendQueryCteSections(sb, summary, query); - } - - return sb.ToString(); - } - - /// - /// Appends the query composition table for a single query. - /// - private static void AppendQueryCompositionTable(StringBuilder sb, int queryIndex, QuerySummary summary) - { - sb.AppendLine($"### Query #{queryIndex}"); - sb.AppendLine(); - sb.AppendLine("| Aspect | Present |"); - sb.AppendLine("|--------|---------|"); - sb.AppendLine($"| SELECT Clause | {FormatClausePresence(summary.HasSelectClause)} |"); - sb.AppendLine($"| FROM Clause | {FormatClausePresence(summary.HasFromClause)} |"); - sb.AppendLine($"| WHERE Clause | {FormatClausePresence(summary.HasWhereClause)} |"); - sb.AppendLine($"| GROUP BY Clause | {FormatClausePresence(summary.HasGroupByClause)} |"); - sb.AppendLine($"| HAVING Clause | {FormatClausePresence(summary.HasHavingClause)} |"); - sb.AppendLine($"| ORDER BY Clause | {FormatClausePresence(summary.HasOrderByClause)} |"); - sb.AppendLine($"| CTE (WITH) | {FormatClausePresence(summary.HasCTE)} |"); - sb.AppendLine($"| Columns | {summary.ColumnCount} |"); - sb.AppendLine($"| Parameters | {summary.ParameterCount} |"); - sb.AppendLine(); - } - - /// - /// Appends parameter information for a query. - /// - private static void AppendQueryParameters(StringBuilder sb, QueryBreakdown query) - { - // Collect all unique parameters from both ParameterList and Parameters dictionary - var allParams = new Dictionary(StringComparer.OrdinalIgnoreCase); - - // Add from ParameterList (parsed parameters) - foreach (var param in query.ParameterList) - { - allParams[param.Name] = param.Value; - } - - // Add from Parameters dictionary (manually added parameters) - foreach (var param in query.Parameters) - { - allParams[param.Key] = param.Value; - } - - if (allParams.Count == 0) - { - return; - } - - sb.AppendLine("**Parameters Used:**"); - sb.AppendLine(); - foreach (var paramName in allParams.Keys.OrderBy(k => k, StringComparer.OrdinalIgnoreCase)) - { - var value = allParams[paramName]; - sb.AppendLine($"- `@{paramName}` = `{value?.ToString() ?? "NULL"}`"); - } - sb.AppendLine(); - } - - /// - /// Appends CTE section for a query. - /// - private static void AppendQueryCteSections(StringBuilder sb, QuerySummary summary, QueryBreakdown query) - { - if (!summary.HasCTE) - { - return; - } - - sb.AppendLine("**CTEs Defined:**"); - sb.AppendLine(); - foreach (var cte in query.WithClauses) - { - sb.AppendLine($"- `{cte.TableName}`"); - } - sb.AppendLine(); - } + => CollectionReportWriter.QueryCompositionReport(collection.QueryBreakdowns, Format); /// /// Generates a batch execution flow diagram. @@ -295,76 +76,25 @@ public static class QueryBreakdownCollectionGenerator /// Whether to show transaction wrapping. /// Mermaid diagram markdown. public static string GenerateBatchFlowDiagram(QueryBreakdownCollection collection, bool includeTransaction = false) - { - var sb = new StringBuilder(); - sb.AppendLine("```mermaid"); - sb.AppendLine("flowchart TD"); - sb.AppendLine(); + => CollectionReportWriter.BatchFlowDiagram( + collection.QueryBreakdowns.Count, + includeTransaction ? "BEGIN TRANSACTION" : null, + includeTransaction ? "COMMIT TRANSACTION" : null); - int nodeId = 0; - - // Handle empty collection - if (collection.QueryBreakdowns.Count == 0) - { - if (includeTransaction) + /// + /// Maps the collection's parameter usage report into the writer's dialect-agnostic rows. + /// + private static List MapParameters(QueryBreakdownCollection collection) + => collection.GetParameterUsageReport() + .Select(p => new ParameterUsageRow { - sb.AppendLine($" Start([Batch Start]) --> node0[\"BEGIN TRANSACTION\"]"); - sb.AppendLine($" node0 --> node1[\"COMMIT TRANSACTION\"]"); - sb.AppendLine($" node1 --> End([Batch Complete])"); - } - else - { - sb.AppendLine($" Start([Batch Start]) --> End([Batch Complete])"); - } - sb.AppendLine("```"); - sb.AppendLine(); - return sb.ToString(); - } - - // Start node - if (includeTransaction) - { - sb.AppendLine($" node{nodeId}[\"BEGIN TRANSACTION\"]"); - sb.AppendLine($" Start([Batch Start]) --> node{nodeId}"); - nodeId++; - sb.AppendLine($" node{nodeId - 1} --> node{nodeId}"); - } - else - { - sb.AppendLine($" Start([Batch Start]) --> node{nodeId}"); - } - - // Query nodes - for (int i = 0; i < collection.QueryBreakdowns.Count; i++) - { - if (i < collection.QueryBreakdowns.Count - 1) - { - // Not the last query - connect to next - sb.AppendLine($" node{nodeId}[\"Query {i}\"] --> node{nodeId + 1}"); - nodeId++; - } - else - { - // Last query - connect to End (or COMMIT if transaction) - if (includeTransaction) - { - sb.AppendLine($" node{nodeId}[\"Query {i}\"] --> node{nodeId + 1}"); - nodeId++; - sb.AppendLine($" node{nodeId}[\"COMMIT TRANSACTION\"]"); - sb.AppendLine($" node{nodeId} --> End([Batch Complete])"); - } - else - { - sb.AppendLine($" node{nodeId}[\"Query {i}\"] --> End([Batch Complete])"); - } - } - } - - sb.AppendLine("```"); - sb.AppendLine(); - - return sb.ToString(); - } + ParameterName = p.ParameterName, + IsUsedInAllQueries = p.IsUsedInAllQueries, + UsedInQueryCount = p.UsedInQueryCount, + TotalQueries = p.TotalQueries, + Value = p.Value + }) + .ToList(); /// /// Gets the parameter type name from a parameter value. @@ -387,36 +117,4 @@ public static class QueryBreakdownCollectionGenerator _ => "VARIANT" }; } - - /// - /// Escapes special Markdown characters. - /// - private static string EscapeMarkdown(string text) - { - return text - .Replace("\\", "\\\\") - .Replace("|", "\\|") - .Replace("\n", "\\n"); - } - - /// - /// Formats clause presence as Yes/No with checkmark/cross. - /// - private static string FormatClausePresence(bool isPresent) - => isPresent ? "✓ Yes" : "✗ No"; - - /// - /// Determines the query type from a QueryBreakdown. - /// - private static string DetermineQueryType(QueryBreakdown query) - { - var hasSelect = !string.IsNullOrWhiteSpace(query.SelectClause?.Clause); - if (hasSelect) - { - return "SELECT"; - } - - var hasFrom = !string.IsNullOrWhiteSpace(query.FromClause?.Clause); - return hasFrom ? "FROM" : "QUERY"; - } } diff --git a/src/Strata.SqlTools.PostgreSql/Breakdowns/QueryBreakdownCollection.cs b/src/Strata.SqlTools.PostgreSql/Breakdowns/QueryBreakdownCollection.cs index c370704..b2e6186 100644 --- a/src/Strata.SqlTools.PostgreSql/Breakdowns/QueryBreakdownCollection.cs +++ b/src/Strata.SqlTools.PostgreSql/Breakdowns/QueryBreakdownCollection.cs @@ -21,7 +21,7 @@ public class QueryBreakdownCollection : SqlBreakdownCollection /// public QueryBreakdownCollection() : base() { - _queryBreakdowns = new List(); + _queryBreakdowns = []; } /// @@ -31,7 +31,7 @@ public class QueryBreakdownCollection : SqlBreakdownCollection public QueryBreakdownCollection(IEnumerable queryBreakdowns) : base(queryBreakdowns?.Cast() ?? Enumerable.Empty()) { - _queryBreakdowns = queryBreakdowns?.ToList() ?? new List(); + _queryBreakdowns = queryBreakdowns?.ToList() ?? []; } /// @@ -58,7 +58,7 @@ public class QueryBreakdownCollection : SqlBreakdownCollection /// The QueryBreakdowns to add. public void AddRange(IEnumerable queryBreakdowns) { - foreach (var qb in queryBreakdowns ?? new List()) + foreach (var qb in queryBreakdowns ?? []) { Add(qb); }