diff --git a/src/Strata.SqlTools.Markdown/Common/CollectionMarkdownGenerator.cs b/src/Strata.SqlTools.Markdown/Common/CollectionMarkdownGenerator.cs
new file mode 100644
index 0000000..977042b
--- /dev/null
+++ b/src/Strata.SqlTools.Markdown/Common/CollectionMarkdownGenerator.cs
@@ -0,0 +1,37 @@
+namespace Strata.SqlTools.Markdown.Common;
+
+///
+/// Shared method bodies for the per-dialect QueryBreakdownCollectionGenerator
+/// classes. Each dialect wraps its QueryBreakdownCollection in an
+/// adapter and supplies a dialect-specific
+/// ; this template handles the rest.
+///
+internal static class CollectionMarkdownGenerator
+{
+ public static string GenerateCollectionReport(ICollectionMarkdownData data, MarkdownDialectFormat format, string? title)
+ => CollectionReportWriter.CollectionReport(title, new[]
+ {
+ GenerateCollectionSummary(data),
+ GenerateParameterAnalysis(data, format),
+ GenerateQueryCompositionReport(data, format)
+ });
+
+ public static string GenerateCollectionSummary(ICollectionMarkdownData data)
+ => CollectionReportWriter.CollectionSummary(
+ data.QueryCount,
+ data.UniqueParameterCount,
+ data.TotalSelectedColumns,
+ data.UniqueTableCount);
+
+ public static string GenerateParameterAnalysis(ICollectionMarkdownData data, MarkdownDialectFormat format)
+ => CollectionReportWriter.ParameterAnalysis(data.ParameterRows.ToList(), data.QueriesForReport, format);
+
+ public static string GenerateParameterDependencyDiagram(ICollectionMarkdownData data, MarkdownDialectFormat format)
+ => CollectionReportWriter.ParameterDependencyDiagram(data.QueriesForReport, format);
+
+ public static string GenerateQueryCompositionReport(ICollectionMarkdownData data, MarkdownDialectFormat format)
+ => CollectionReportWriter.QueryCompositionReport(data.QueriesForReport, format);
+
+ public static string GenerateBatchFlowDiagram(ICollectionMarkdownData data, string? openLabel, string? closeLabel)
+ => CollectionReportWriter.BatchFlowDiagram(data.QueryCount, openLabel, closeLabel);
+}
diff --git a/src/Strata.SqlTools.Markdown/Common/ICollectionMarkdownData.cs b/src/Strata.SqlTools.Markdown/Common/ICollectionMarkdownData.cs
new file mode 100644
index 0000000..0dd70d1
--- /dev/null
+++ b/src/Strata.SqlTools.Markdown/Common/ICollectionMarkdownData.cs
@@ -0,0 +1,30 @@
+using SqlServerBreakdowns = Strata.SqlTools.Breakdowns.SqlServer;
+
+namespace Strata.SqlTools.Markdown.Common;
+
+///
+/// Dialect-neutral view of a QueryBreakdownCollection as seen by the Markdown
+/// generators. Each per-dialect QueryBreakdownCollectionGenerator wraps its
+/// dialect-specific collection in an implementation of this interface so the shared
+/// template can operate uniformly.
+///
+internal interface ICollectionMarkdownData
+{
+ int QueryCount { get; }
+ int UniqueParameterCount { get; }
+ int TotalSelectedColumns { get; }
+ int UniqueTableCount { get; }
+
+ ///
+ /// Queries as the base type. Used
+ /// directly by
+ /// (which needs ) and via covariance by the other
+ /// writer methods that accept .
+ ///
+ IReadOnlyList QueriesForReport { get; }
+
+ ///
+ /// Parameter usage already mapped into the writer's dialect-agnostic row type.
+ ///
+ IEnumerable ParameterRows { get; }
+}
diff --git a/src/Strata.SqlTools.Markdown/PostgreSql/QueryBreakdownCollectionGenerator.cs b/src/Strata.SqlTools.Markdown/PostgreSql/QueryBreakdownCollectionGenerator.cs
index 4fb82fd..1a28895 100644
--- a/src/Strata.SqlTools.Markdown/PostgreSql/QueryBreakdownCollectionGenerator.cs
+++ b/src/Strata.SqlTools.Markdown/PostgreSql/QueryBreakdownCollectionGenerator.cs
@@ -23,79 +23,63 @@ public static class QueryBreakdownCollectionGenerator
///
/// Generates a comprehensive collection report in Markdown format with PostgreSQL-specific information.
///
- /// The QueryBreakdownCollection to document.
- /// Optional title for the report.
- /// A string containing the Markdown documentation.
public static string GenerateCollectionReport(QueryBreakdownCollection collection, string? title = null)
- => CollectionReportWriter.CollectionReport(title, new[]
- {
- GenerateCollectionSummary(collection),
- GenerateParameterAnalysis(collection),
- GenerateQueryCompositionReport(collection)
- });
+ => CollectionMarkdownGenerator.GenerateCollectionReport(Adapt(collection), Format, title);
///
/// Generates a summary section for the collection.
///
- /// The QueryBreakdownCollection to summarize.
- /// Markdown summary section.
public static string GenerateCollectionSummary(QueryBreakdownCollection collection)
- => CollectionReportWriter.CollectionSummary(
- collection.QueryBreakdowns.Count,
- collection.GetAllUniqueParameters().Count(),
- collection.GetTotalSelectedColumns(),
- collection.GetUniqueTableReferences().Count());
+ => CollectionMarkdownGenerator.GenerateCollectionSummary(Adapt(collection));
///
/// Generates a parameter analysis report with PostgreSQL parameter syntax support.
///
- /// The QueryBreakdownCollection to analyze.
- /// Markdown parameter analysis section.
public static string GenerateParameterAnalysis(QueryBreakdownCollection collection)
- => CollectionReportWriter.ParameterAnalysis(MapParameters(collection), collection.QueryBreakdowns, Format);
+ => CollectionMarkdownGenerator.GenerateParameterAnalysis(Adapt(collection), Format);
///
/// Generates a Mermaid diagram showing parameter dependencies across queries.
///
- /// The QueryBreakdownCollection to visualize.
- /// Mermaid diagram markdown.
public static string GenerateParameterDependencyDiagram(QueryBreakdownCollection collection)
- => CollectionReportWriter.ParameterDependencyDiagram(collection.QueryBreakdowns, Format);
+ => CollectionMarkdownGenerator.GenerateParameterDependencyDiagram(Adapt(collection), Format);
///
/// Generates a detailed query composition report.
///
- /// The QueryBreakdownCollection to report on.
- /// Markdown composition report section.
public static string GenerateQueryCompositionReport(QueryBreakdownCollection collection)
- => CollectionReportWriter.QueryCompositionReport(collection.QueryBreakdowns, Format);
+ => CollectionMarkdownGenerator.GenerateQueryCompositionReport(Adapt(collection), Format);
///
/// Generates a batch execution flow diagram for PostgreSQL.
///
- /// The QueryBreakdownCollection to visualize.
- /// Whether to show transaction wrapping.
- /// Mermaid diagram markdown.
public static string GenerateBatchFlowDiagram(QueryBreakdownCollection collection, bool includeTransaction = false)
- => CollectionReportWriter.BatchFlowDiagram(
- collection.QueryBreakdowns.Count,
+ => CollectionMarkdownGenerator.GenerateBatchFlowDiagram(
+ Adapt(collection),
includeTransaction ? "BEGIN" : null,
includeTransaction ? "COMMIT" : null);
- ///
- /// 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
+ private static ICollectionMarkdownData Adapt(QueryBreakdownCollection collection) => new Adapter(collection);
+
+ private sealed class Adapter : ICollectionMarkdownData
+ {
+ private readonly QueryBreakdownCollection _c;
+ public Adapter(QueryBreakdownCollection c) { _c = c; }
+ public int QueryCount => _c.QueryBreakdowns.Count;
+ public int UniqueParameterCount => _c.GetAllUniqueParameters().Count();
+ public int TotalSelectedColumns => _c.GetTotalSelectedColumns();
+ public int UniqueTableCount => _c.GetUniqueTableReferences().Count();
+ public IReadOnlyList QueriesForReport => _c.QueryBreakdowns;
+ public IEnumerable ParameterRows
+ => _c.GetParameterUsageReport().Select(p => new ParameterUsageRow
{
ParameterName = p.ParameterName,
IsUsedInAllQueries = p.IsUsedInAllQueries,
UsedInQueryCount = p.UsedInQueryCount,
TotalQueries = p.TotalQueries,
Value = p.Value
- })
- .ToList();
+ });
+ }
///
/// Formats a parameter using PostgreSQL syntax: $n for positional, :name for named.
diff --git a/src/Strata.SqlTools.Markdown/SqlServer/QueryBreakdownCollectionGenerator.cs b/src/Strata.SqlTools.Markdown/SqlServer/QueryBreakdownCollectionGenerator.cs
index 723907d..07ba541 100644
--- a/src/Strata.SqlTools.Markdown/SqlServer/QueryBreakdownCollectionGenerator.cs
+++ b/src/Strata.SqlTools.Markdown/SqlServer/QueryBreakdownCollectionGenerator.cs
@@ -22,79 +22,63 @@ public static class QueryBreakdownCollectionGenerator
///
/// Generates a comprehensive collection report in Markdown format.
///
- /// The QueryBreakdownCollection to document.
- /// Optional title for the report.
- /// A string containing the Markdown documentation.
public static string GenerateCollectionReport(QueryBreakdownCollection collection, string? title = null)
- => CollectionReportWriter.CollectionReport(title, new[]
- {
- GenerateCollectionSummary(collection),
- GenerateParameterAnalysis(collection),
- GenerateQueryCompositionReport(collection)
- });
+ => CollectionMarkdownGenerator.GenerateCollectionReport(Adapt(collection), Format, title);
///
/// Generates a summary section for the collection.
///
- /// The QueryBreakdownCollection to summarize.
- /// Markdown summary section.
public static string GenerateCollectionSummary(QueryBreakdownCollection collection)
- => CollectionReportWriter.CollectionSummary(
- collection.QueryBreakdowns.Count,
- collection.GetAllUniqueParameters().Count(),
- collection.GetTotalSelectedColumns(),
- collection.GetUniqueTableReferences().Count());
+ => CollectionMarkdownGenerator.GenerateCollectionSummary(Adapt(collection));
///
/// Generates a parameter analysis report.
///
- /// The QueryBreakdownCollection to analyze.
- /// Markdown parameter analysis section.
public static string GenerateParameterAnalysis(QueryBreakdownCollection collection)
- => CollectionReportWriter.ParameterAnalysis(MapParameters(collection), collection.QueryBreakdowns, Format);
+ => CollectionMarkdownGenerator.GenerateParameterAnalysis(Adapt(collection), Format);
///
/// Generates a Mermaid diagram showing parameter dependencies across queries.
///
- /// The QueryBreakdownCollection to visualize.
- /// Mermaid diagram markdown.
public static string GenerateParameterDependencyDiagram(QueryBreakdownCollection collection)
- => CollectionReportWriter.ParameterDependencyDiagram(collection.QueryBreakdowns, Format);
+ => CollectionMarkdownGenerator.GenerateParameterDependencyDiagram(Adapt(collection), Format);
///
/// Generates a detailed query composition report.
///
- /// The QueryBreakdownCollection to report on.
- /// Markdown composition report section.
public static string GenerateQueryCompositionReport(QueryBreakdownCollection collection)
- => CollectionReportWriter.QueryCompositionReport(collection.QueryBreakdowns, Format);
+ => CollectionMarkdownGenerator.GenerateQueryCompositionReport(Adapt(collection), Format);
///
/// Generates a batch execution flow diagram.
///
- /// The QueryBreakdownCollection to visualize.
- /// Whether to show transaction wrapping.
- /// Mermaid diagram markdown.
public static string GenerateBatchFlowDiagram(QueryBreakdownCollection collection, bool includeTransaction = false)
- => CollectionReportWriter.BatchFlowDiagram(
- collection.QueryBreakdowns.Count,
+ => CollectionMarkdownGenerator.GenerateBatchFlowDiagram(
+ Adapt(collection),
includeTransaction ? "BEGIN TRANSACTION" : null,
includeTransaction ? "COMMIT TRANSACTION" : null);
- ///
- /// 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
+ private static ICollectionMarkdownData Adapt(QueryBreakdownCollection collection) => new Adapter(collection);
+
+ private sealed class Adapter : ICollectionMarkdownData
+ {
+ private readonly QueryBreakdownCollection _c;
+ public Adapter(QueryBreakdownCollection c) { _c = c; }
+ public int QueryCount => _c.QueryBreakdowns.Count;
+ public int UniqueParameterCount => _c.GetAllUniqueParameters().Count();
+ public int TotalSelectedColumns => _c.GetTotalSelectedColumns();
+ public int UniqueTableCount => _c.GetUniqueTableReferences().Count();
+ public IReadOnlyList QueriesForReport => _c.QueryBreakdowns;
+ public IEnumerable ParameterRows
+ => _c.GetParameterUsageReport().Select(p => new ParameterUsageRow
{
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.
diff --git a/src/Strata.SqlTools.Snowflake/Breakdowns/QueryBreakdown.cs b/src/Strata.SqlTools.Snowflake/Breakdowns/QueryBreakdown.cs
index 637a8f1..ae37519 100644
--- a/src/Strata.SqlTools.Snowflake/Breakdowns/QueryBreakdown.cs
+++ b/src/Strata.SqlTools.Snowflake/Breakdowns/QueryBreakdown.cs
@@ -451,62 +451,7 @@ public class QueryBreakdown : SqlServerQueryBreakdown
}
}
- if (IsUsingWithClause)
- {
- // Check if any WITH clause is recursive
- bool hasRecursive = WithClauses.Any(wc => wc.IsRecursive);
- sb.Append("WITH");
- if (hasRecursive)
- {
- sb.Append(" RECURSIVE");
- }
- sb.AppendLine();
-
- for (int i = 0; i < WithClauses.Count; i++)
- {
- var withClause = WithClauses[i];
-
- if (i > 0)
- {
- sb.Append(',');
- sb.AppendLine();
- }
-
- // Include comment if present
- if (!string.IsNullOrWhiteSpace(withClause.Comment))
- {
- sb.AppendLine($" {withClause.Comment}");
- }
-
- // Write CTE name with optional column list
- var cteName = withClause.TableName;
- if (withClause.ColumnList != null && withClause.ColumnList.Count > 0)
- {
- var columnList = string.Join(", ", withClause.ColumnList);
- cteName = $"{withClause.TableName} ({columnList})";
- }
-
- sb.AppendLine($" {cteName} AS (");
-
- if (withClause.IsRecursive && withClause.RecursiveQuery != null)
- {
- // For recursive CTEs: anchor query UNION ALL recursive query
- var anchorSql = withClause.Query?.GetSql(includeSetupFinish: false).Trim() ?? string.Empty;
- var recursiveSql = withClause.RecursiveQuery.GetSql(includeSetupFinish: false).Trim() ?? string.Empty;
- sb.AppendLine($" {anchorSql}");
- sb.AppendLine(" UNION ALL");
- sb.AppendLine($" {recursiveSql}");
- }
- else
- {
- // For non-recursive CTEs: just the single query
- var withSql = withClause.Query?.GetSql(includeSetupFinish: false).Trim() ?? string.Empty;
- sb.AppendLine($" {withSql}");
- }
- sb.Append(" )");
- }
- sb.AppendLine();
- }
+ AppendWithClauseSection(sb, withClauseIndent: " ", queryBodyIndent: " ");
// Snowflake SELECT syntax
sb.Append(StatementParser.KeywordSelect);
diff --git a/src/Strata.SqlTools.SqlServer/Breakdowns/QueryBreakdown.cs b/src/Strata.SqlTools.SqlServer/Breakdowns/QueryBreakdown.cs
index 9d8187c..2382e06 100644
--- a/src/Strata.SqlTools.SqlServer/Breakdowns/QueryBreakdown.cs
+++ b/src/Strata.SqlTools.SqlServer/Breakdowns/QueryBreakdown.cs
@@ -547,74 +547,79 @@ public class QueryBreakdown : SqlBreakdownBase, IQueryBreakdown
return mergedParams;
}
-#pragma warning disable S3776 // Cognitive Complexity of methods should not be too high
+ ///
+ /// Renders the WITH [RECURSIVE] CTE section into if any
+ /// are present. Shared by dialects (SqlServer uses 5-space
+ /// outer / 10-space inner indents; Snowflake passes 4-/8-space).
+ ///
+ /// The builder accumulating the SQL.
+ /// Indent used for the CTE name line and the closing ).
+ /// Indent used for each anchor/recursive query body line and the UNION ALL.
+ protected virtual void AppendWithClauseSection(StringBuilder sb, string withClauseIndent, string queryBodyIndent)
+ {
+ if (!IsUsingWithClause)
+ {
+ return;
+ }
+
+ bool hasRecursive = _withClauses.Any(wc => wc.IsRecursive);
+ sb.Append("WITH");
+ if (hasRecursive)
+ {
+ sb.Append(" RECURSIVE");
+ }
+ sb.AppendLine();
+
+ for (int i = 0; i < _withClauses.Count; i++)
+ {
+ var withClause = _withClauses[i];
+ var anchorSql = withClause.Query?.GetSql(includeSetupFinish: false).Trim() ?? string.Empty;
+
+ if (i > 0)
+ {
+ sb.Append(',');
+ sb.AppendLine();
+ }
+
+ if (!string.IsNullOrWhiteSpace(withClause.Comment))
+ {
+ sb.AppendLine($"{withClauseIndent}{withClause.Comment}");
+ }
+
+ var cteName = withClause.TableName;
+ if (withClause.ColumnList != null && withClause.ColumnList.Count > 0)
+ {
+ var columnList = string.Join(", ", withClause.ColumnList);
+ cteName = $"{withClause.TableName} ({columnList})";
+ }
+ sb.AppendLine($"{withClauseIndent}{cteName} AS (");
+
+ if (withClause.IsRecursive && withClause.RecursiveQuery != null)
+ {
+ var recursiveSql = withClause.RecursiveQuery.GetSql(includeSetupFinish: false).Trim();
+ sb.AppendLine($"{queryBodyIndent}{anchorSql}");
+ sb.AppendLine($"{queryBodyIndent}UNION ALL");
+ sb.AppendLine($"{queryBodyIndent}{recursiveSql}");
+ }
+ else
+ {
+ sb.AppendLine($"{queryBodyIndent}{anchorSql}");
+ }
+
+ sb.Append($"{withClauseIndent})");
+ }
+ sb.AppendLine();
+ }
+
///
/// Gets the SQL breakdown as a string (query-specific implementation).
///
/// The SELECT SQL statement.
protected override string GetSqlBreakdown()
-#pragma warning restore S3776
{
var sb = new StringBuilder();
- if (IsUsingWithClause)
- {
- // Check if any CTE is recursive - if so, add RECURSIVE keyword
- bool hasRecursive = _withClauses.Any(wc => wc.IsRecursive);
- sb.Append("WITH ");
- if (hasRecursive)
- {
- sb.AppendLine("RECURSIVE");
- }
- else
- {
- sb.AppendLine();
- }
-
- for (int i = 0; i < _withClauses.Count; i++)
- {
- var withClause = _withClauses[i];
- var anchorSql = withClause.Query?.GetSql(includeSetupFinish: false).Trim() ?? string.Empty;
-
- if (i > 0)
- {
- sb.Append(',');
- sb.AppendLine();
- }
-
- // Include comment if present
- if (!string.IsNullOrWhiteSpace(withClause.Comment))
- {
- sb.AppendLine($" {withClause.Comment}");
- }
-
- // Write CTE name with optional column list
- var cteName = withClause.TableName;
- if (withClause.ColumnList != null && withClause.ColumnList.Count > 0)
- {
- var columnList = string.Join(", ", withClause.ColumnList);
- cteName = $"{withClause.TableName} ({columnList})";
- }
- sb.AppendLine($" {cteName} AS (");
-
- if (withClause.IsRecursive && withClause.RecursiveQuery != null)
- {
- // For recursive CTEs: anchor query UNION ALL recursive query
- var recursiveSql = withClause.RecursiveQuery.GetSql(includeSetupFinish: false).Trim();
- sb.AppendLine($" {anchorSql}");
- sb.AppendLine(" UNION ALL");
- sb.AppendLine($" {recursiveSql}");
- }
- else
- {
- // For non-recursive CTEs: just the single query
- sb.AppendLine($" {anchorSql}");
- }
-
- sb.Append(" )");
- }
- sb.AppendLine();
- }
+ AppendWithClauseSection(sb, withClauseIndent: " ", queryBodyIndent: " ");
sb.AppendLine("SELECT ");
if (!string.IsNullOrEmpty(SelectClause.Comment))