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);
}