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>
`SqlServer.QueryBreakdown.GetSqlBreakdown` and
`Snowflake.QueryBreakdown.GetSql` each carried a 24-line copy of the
same CTE-rendering loop ("WITH" keyword, optional RECURSIVE, per-clause
header, anchor/UNION ALL/recursive query, closing parens). The two
copies differed only by indent (5/10 spaces vs 4/8) and a trailing
space after the keyword.
Hoist the loop into `protected virtual void AppendWithClauseSection(
StringBuilder, string withClauseIndent, string queryBodyIndent)` on
`SqlServer.QueryBreakdown`. Each caller invokes it with its dialect's
preferred indents; Snowflake's mid-method copy is deleted entirely.
Standardizes on the no-trailing-space "WITH" form (Snowflake's) — was
"WITH " (trailing space) in the SqlServer original. Visible only as a
trailing space before the newline in non-recursive output, which no
tests assert on.
All 1180 tests stay green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>