refactor(dedup): adapter-based Markdown CollectionGenerator dedup (the big one)
SonarQube Analysis / sonarqube (pull_request) Successful in 3m42s

Final cluster Sonar was reporting: the 82-line copy-paste between
`Markdown.SqlServer.QueryBreakdownCollectionGenerator` and
`Markdown.PostgreSql.QueryBreakdownCollectionGenerator`. Both classes
existed because each dialect has a different concrete
`QueryBreakdownCollection` type with its own `ParameterUsageReport`
class — no shared base for the methods to operate on.

Resolves it with an adapter pattern in `Markdown.Common`:

- **`ICollectionMarkdownData`** (new, internal): dialect-neutral view
  exposing query count, parameter / column / table totals, queries-
  for-report list, and parameter-rows (already-mapped to the writer's
  `ParameterUsageRow` type).
- **`CollectionMarkdownGenerator`** (new, internal static): single
  template that takes the data + `MarkdownDialectFormat` and routes
  through `CollectionReportWriter`. The six `GenerateX` methods that
  were duplicated three times now live here once.
- **SqlServer / PostgreSql wrappers**: shrunk to a `Format` static, a
  thin one-line forwarder per public method, and a private sealed
  `Adapter : ICollectionMarkdownData` nested class that does the
  dialect-specific extraction (including the `ParameterUsageReport →
  ParameterUsageRow` mapping that was previously duplicated three
  times as `MapParameters`).

Public API unchanged — the existing `Markdown.SqlServer.QueryBreakdownCollectionGenerator.GenerateCollectionReport(collection, title)`
etc. continue to work as before; their bodies just delegate. The
three dialect-specific `ParameterUsageReport` classes are
deliberately *not* unified yet — their `ToString()` overrides differ
meaningfully per dialect and unifying would be a separate API
discussion.

Snowflake wrapper not touched in this commit — Sonar didn't flag it
(its `GenerateSnowflakeFeaturesAnalysis` and feature-aware
`QueryCompositionReport` callback make it structurally distinct).
Consistency follow-up could move it onto the same adapter pattern
without behavior change.

All 1180 tests stay green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Thom Lamb
2026-05-27 17:28:16 -05:00
co-authored by Claude Opus 4.7
parent 089d4f6000
commit 4038b3dab5
4 changed files with 111 additions and 76 deletions
@@ -0,0 +1,37 @@
namespace Strata.SqlTools.Markdown.Common;
/// <summary>
/// Shared method bodies for the per-dialect <c>QueryBreakdownCollectionGenerator</c>
/// classes. Each dialect wraps its <c>QueryBreakdownCollection</c> in an
/// <see cref="ICollectionMarkdownData"/> adapter and supplies a dialect-specific
/// <see cref="MarkdownDialectFormat"/>; this template handles the rest.
/// </summary>
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);
}
@@ -0,0 +1,30 @@
using SqlServerBreakdowns = Strata.SqlTools.Breakdowns.SqlServer;
namespace Strata.SqlTools.Markdown.Common;
/// <summary>
/// Dialect-neutral view of a <c>QueryBreakdownCollection</c> as seen by the Markdown
/// generators. Each per-dialect <c>QueryBreakdownCollectionGenerator</c> wraps its
/// dialect-specific collection in an implementation of this interface so the shared
/// <see cref="CollectionMarkdownGenerator"/> template can operate uniformly.
/// </summary>
internal interface ICollectionMarkdownData
{
int QueryCount { get; }
int UniqueParameterCount { get; }
int TotalSelectedColumns { get; }
int UniqueTableCount { get; }
/// <summary>
/// Queries as the base <see cref="SqlServerBreakdowns.QueryBreakdown"/> type. Used
/// directly by <see cref="CollectionReportWriter.QueryCompositionReport"/>
/// (which needs <see cref="IReadOnlyList{T}"/>) and via covariance by the other
/// writer methods that accept <see cref="IEnumerable{T}"/>.
/// </summary>
IReadOnlyList<SqlServerBreakdowns.QueryBreakdown> QueriesForReport { get; }
/// <summary>
/// Parameter usage already mapped into the writer's dialect-agnostic row type.
/// </summary>
IEnumerable<ParameterUsageRow> ParameterRows { get; }
}
@@ -23,79 +23,63 @@ public static class QueryBreakdownCollectionGenerator
/// <summary>
/// Generates a comprehensive collection report in Markdown format with PostgreSQL-specific information.
/// </summary>
/// <param name="collection">The QueryBreakdownCollection to document.</param>
/// <param name="title">Optional title for the report.</param>
/// <returns>A string containing the Markdown documentation.</returns>
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);
/// <summary>
/// Generates a summary section for the collection.
/// </summary>
/// <param name="collection">The QueryBreakdownCollection to summarize.</param>
/// <returns>Markdown summary section.</returns>
public static string GenerateCollectionSummary(QueryBreakdownCollection collection)
=> CollectionReportWriter.CollectionSummary(
collection.QueryBreakdowns.Count,
collection.GetAllUniqueParameters().Count(),
collection.GetTotalSelectedColumns(),
collection.GetUniqueTableReferences().Count());
=> CollectionMarkdownGenerator.GenerateCollectionSummary(Adapt(collection));
/// <summary>
/// Generates a parameter analysis report with PostgreSQL parameter syntax support.
/// </summary>
/// <param name="collection">The QueryBreakdownCollection to analyze.</param>
/// <returns>Markdown parameter analysis section.</returns>
public static string GenerateParameterAnalysis(QueryBreakdownCollection collection)
=> CollectionReportWriter.ParameterAnalysis(MapParameters(collection), collection.QueryBreakdowns, Format);
=> CollectionMarkdownGenerator.GenerateParameterAnalysis(Adapt(collection), Format);
/// <summary>
/// Generates a Mermaid diagram showing parameter dependencies across queries.
/// </summary>
/// <param name="collection">The QueryBreakdownCollection to visualize.</param>
/// <returns>Mermaid diagram markdown.</returns>
public static string GenerateParameterDependencyDiagram(QueryBreakdownCollection collection)
=> CollectionReportWriter.ParameterDependencyDiagram(collection.QueryBreakdowns, Format);
=> CollectionMarkdownGenerator.GenerateParameterDependencyDiagram(Adapt(collection), Format);
/// <summary>
/// Generates a detailed query composition report.
/// </summary>
/// <param name="collection">The QueryBreakdownCollection to report on.</param>
/// <returns>Markdown composition report section.</returns>
public static string GenerateQueryCompositionReport(QueryBreakdownCollection collection)
=> CollectionReportWriter.QueryCompositionReport(collection.QueryBreakdowns, Format);
=> CollectionMarkdownGenerator.GenerateQueryCompositionReport(Adapt(collection), Format);
/// <summary>
/// Generates a batch execution flow diagram for PostgreSQL.
/// </summary>
/// <param name="collection">The QueryBreakdownCollection to visualize.</param>
/// <param name="includeTransaction">Whether to show transaction wrapping.</param>
/// <returns>Mermaid diagram markdown.</returns>
public static string GenerateBatchFlowDiagram(QueryBreakdownCollection collection, bool includeTransaction = false)
=> CollectionReportWriter.BatchFlowDiagram(
collection.QueryBreakdowns.Count,
=> CollectionMarkdownGenerator.GenerateBatchFlowDiagram(
Adapt(collection),
includeTransaction ? "BEGIN" : null,
includeTransaction ? "COMMIT" : null);
/// <summary>
/// Maps the collection's parameter usage report into the writer's dialect-agnostic rows.
/// </summary>
private static List<ParameterUsageRow> 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<Breakdowns.SqlServer.QueryBreakdown> QueriesForReport => _c.QueryBreakdowns;
public IEnumerable<ParameterUsageRow> ParameterRows
=> _c.GetParameterUsageReport().Select(p => new ParameterUsageRow
{
ParameterName = p.ParameterName,
IsUsedInAllQueries = p.IsUsedInAllQueries,
UsedInQueryCount = p.UsedInQueryCount,
TotalQueries = p.TotalQueries,
Value = p.Value
})
.ToList();
});
}
/// <summary>
/// Formats a parameter using PostgreSQL syntax: <c>$n</c> for positional, <c>:name</c> for named.
@@ -22,79 +22,63 @@ public static class QueryBreakdownCollectionGenerator
/// <summary>
/// Generates a comprehensive collection report in Markdown format.
/// </summary>
/// <param name="collection">The QueryBreakdownCollection to document.</param>
/// <param name="title">Optional title for the report.</param>
/// <returns>A string containing the Markdown documentation.</returns>
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);
/// <summary>
/// Generates a summary section for the collection.
/// </summary>
/// <param name="collection">The QueryBreakdownCollection to summarize.</param>
/// <returns>Markdown summary section.</returns>
public static string GenerateCollectionSummary(QueryBreakdownCollection collection)
=> CollectionReportWriter.CollectionSummary(
collection.QueryBreakdowns.Count,
collection.GetAllUniqueParameters().Count(),
collection.GetTotalSelectedColumns(),
collection.GetUniqueTableReferences().Count());
=> CollectionMarkdownGenerator.GenerateCollectionSummary(Adapt(collection));
/// <summary>
/// Generates a parameter analysis report.
/// </summary>
/// <param name="collection">The QueryBreakdownCollection to analyze.</param>
/// <returns>Markdown parameter analysis section.</returns>
public static string GenerateParameterAnalysis(QueryBreakdownCollection collection)
=> CollectionReportWriter.ParameterAnalysis(MapParameters(collection), collection.QueryBreakdowns, Format);
=> CollectionMarkdownGenerator.GenerateParameterAnalysis(Adapt(collection), Format);
/// <summary>
/// Generates a Mermaid diagram showing parameter dependencies across queries.
/// </summary>
/// <param name="collection">The QueryBreakdownCollection to visualize.</param>
/// <returns>Mermaid diagram markdown.</returns>
public static string GenerateParameterDependencyDiagram(QueryBreakdownCollection collection)
=> CollectionReportWriter.ParameterDependencyDiagram(collection.QueryBreakdowns, Format);
=> CollectionMarkdownGenerator.GenerateParameterDependencyDiagram(Adapt(collection), Format);
/// <summary>
/// Generates a detailed query composition report.
/// </summary>
/// <param name="collection">The QueryBreakdownCollection to report on.</param>
/// <returns>Markdown composition report section.</returns>
public static string GenerateQueryCompositionReport(QueryBreakdownCollection collection)
=> CollectionReportWriter.QueryCompositionReport(collection.QueryBreakdowns, Format);
=> CollectionMarkdownGenerator.GenerateQueryCompositionReport(Adapt(collection), Format);
/// <summary>
/// Generates a batch execution flow diagram.
/// </summary>
/// <param name="collection">The QueryBreakdownCollection to visualize.</param>
/// <param name="includeTransaction">Whether to show transaction wrapping.</param>
/// <returns>Mermaid diagram markdown.</returns>
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);
/// <summary>
/// Maps the collection's parameter usage report into the writer's dialect-agnostic rows.
/// </summary>
private static List<ParameterUsageRow> 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<QueryBreakdown> QueriesForReport => _c.QueryBreakdowns;
public IEnumerable<ParameterUsageRow> ParameterRows
=> _c.GetParameterUsageReport().Select(p => new ParameterUsageRow
{
ParameterName = p.ParameterName,
IsUsedInAllQueries = p.IsUsedInAllQueries,
UsedInQueryCount = p.UsedInQueryCount,
TotalQueries = p.TotalQueries,
Value = p.Value
})
.ToList();
});
}
/// <summary>
/// Gets the parameter type name from a parameter value.