Commit Graph
25 Commits
Author SHA1 Message Date
Thom Lamb fbedea3941 test(sql): Add comprehensive unit tests for breakdown classes and utilities
Introduces new unit tests for Snowflake and SQL Server-specific SQL breakdown
classes (DELETE, INSERT, UPDATE, PROCEDURE) and various general SQL utility
functions. This significantly increases test coverage, ensuring robustness
and correctness across different SQL dialects and helper logic.
2026-06-15 08:16:22 -05:00
bermudalamb f362ec7e53 Merge pull request 'chore(sonar): clear a code smell' (#19) from chore/ci-coverage-reporting into main
SonarQube Analysis / sonarqube (push) Successful in 3m52s
Reviewed-on: #19
2026-05-27 15:51:13 -05:00
Thom Lamb 39f1d30783 chore(sonar): clear a code smell
SonarQube Analysis / sonarqube (pull_request) Successful in 3m38s
2026-05-27 15:50:03 -05:00
Thom LambandClaude Opus 4.7 f55d8130e6 chore(sonar): clear residual test-file smells (NUnit2046, NUnit2045, CS8604)
SonarQube Analysis / sonarqube (pull_request) Successful in 3m41s
- **NUnit2046 (15)**: extend the prior regex sweep to also catch the
  `Is.GreaterThan(n)` / `Is.GreaterThanOrEqualTo(n)` variants on
  `.Count` and `.Length` — the previous pass only handled `Is.EqualTo`.
  Affects PG/Snowflake `QueryBreakdownTests`, PG `StatementReaderTests`,
  `LinqToSql` `QueryComparatorTests` / `QueryValidatorTests`,
  `Markdown.Tests` `ExpressionGeneratorTests` /
  `QueryBreakdownGeneratorTests`, and `JsonTokenReaderTests`.

- **CS8604 (8)**: `merged[someKey]` access inside `Assert.Multiple(() =>
  { ... })` lambdas where `someKey` is `string?` from `FirstOrDefault`.
  The preceding `Assert.That(someKey, Is.Not.Null)` does not propagate
  null-narrowing into the lambda scope, so add `!` to the dictionary
  index. (Tests fail loud with a meaningful message if the key really
  is null, so this is safe.)

- **NUnit2045 (1)**: wrap a four-assert block in
  `JsonTokenReaderTests.cs:65-68` in `Assert.Multiple`. Mirrors the
  surrounding two `Assert.Multiple` groups in that method.

All 1180 tests stay green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 15:29:20 -05:00
Thom LambandClaude Opus 4.7 423108a7dc chore(sonar)!: mark Markdown generator classes as 'public static class' (S1118)
SonarQube Analysis / sonarqube (pull_request) Successful in 4m22s
After the CA1822 cascade in #15 left every method on the eight
Markdown generator classes static, the classes themselves were
instantiable shells that consumers couldn't usefully `new`. This
commit flips the `class` modifier to `static class` on all eight:

- `Markdown.SqlServer.QueryBreakdownGenerator`
- `Markdown.SqlServer.SqlStatementGenerator`
- `Markdown.LinqToSql.QueryBreakdownGenerator`
- `Markdown.LinqToSql.SqlStatementGenerator`
- `Markdown.PostgreSql.QueryBreakdownGenerator`
- `Markdown.PostgreSql.SqlStatementGenerator`
- `Markdown.Snowflake.QueryBreakdownGenerator`
- `Markdown.Snowflake.SqlStatementGenerator`

Test fixtures drop the now-meaningless `_generator = new …()` field
and `[SetUp]` (kept the existing Setup body where unrelated state was
also initialized — `QueryMarkdownGenerationTests` and
`LinqToSql.QueryBreakdownGeneratorTests`).

BREAKING CHANGE: External NuGet consumers can no longer write
`new Markdown.Snowflake.SqlStatementGenerator()` (or any of the other
seven classes above) — the type is now a static container and may only
be referenced by name, e.g.
`Markdown.Snowflake.SqlStatementGenerator.GenerateSequenceDiagram(…)`.
The call syntax for the static methods is unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 15:08:48 -05:00
Thom LambandClaude Opus 4.7 6175dcde96 chore(sonar)!: cascade CA1822 through Markdown dialect wrappers
SonarQube Analysis / sonarqube (pull_request) Successful in 2m50s
Commit 5202d93 made the SqlServer-namespaced Markdown generator methods
static, which left the LinqToSql / PostgreSql / Snowflake wrapper
classes' instance methods delegating to nothing but a static call.
SonarQube re-flagged those 10 wrapper methods as CA1822 on the next
scan.

This sweep:

- Makes all 10 wrapper instance methods `static` (`dotnet format` driven).
- Makes `Markdown.LinqToSql.QueryBreakdownGenerator.GenerateCombinedDiagram`
  static preemptively — it composes two static helpers and would otherwise
  be the next-iteration cascade flag.
- Removes the now-dead `_baseGenerator` field and its initializing
  constructor from all six dialect wrappers (LinqToSql / PostgreSql /
  Snowflake × QueryBreakdownGenerator + SqlStatementGenerator). The
  classes keep their implicit parameterless constructor so `new
  Snowflake.QueryBreakdownGenerator()` still compiles.
- Updates the one test call site (`GenerateCombinedDiagram`) the fixer
  didn't catch to use type-name form.

BREAKING CHANGE: External NuGet consumers calling
`instance.Generate*Diagram(...)` on `Markdown.LinqToSql.*`,
`Markdown.PostgreSql.*`, or `Markdown.Snowflake.*` generators must
switch to type-name form, e.g. `Markdown.Snowflake.SqlStatementGenerator.GenerateSequenceDiagram(...)`.
The class types and parameterless constructors remain — only the call
syntax for these methods changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 14:38:58 -05:00
Thom LambandClaude Opus 4.7 5202d93e8e chore(sonar)!: mark public Markdown generator methods static (CA1822)
SonarQube Analysis / sonarqube (pull_request) Successful in 3m30s
Three public methods on the SqlServer-namespaced Markdown generators no
longer touch instance state and now carry the `static` keyword:

- `Markdown.SqlServer.QueryBreakdownGenerator.GenerateMermaidDiagram(QueryBreakdown, string?)`
- `Markdown.SqlServer.SqlStatementGenerator.GenerateSequenceDiagram(ISqlBreakdown, string?)`
- `Markdown.SqlServer.SqlStatementGenerator.GenerateEntityRelationshipDiagram(IEnumerable<string>, string?)`

Plus one private bonus the analyzer caught on the same pass:
- `LinqExpressionVisitor.ExtractSelectExpression` → static (non-breaking).

Internal callers in the Snowflake/LinqToSql/PostgreSql wrapper classes
and in the test fixtures are updated to the type-name form
(`SqlServer.SqlStatementGenerator.GenerateSequenceDiagram(...)`).
The wrappers retain their `_baseGenerator` field for now even though it
is no longer used — that S4487 / unused-field cleanup is its own commit.

BREAKING CHANGE: External NuGet consumers calling
`generatorInstance.GenerateMermaidDiagram(...)`,
`generatorInstance.GenerateSequenceDiagram(...)`, or
`generatorInstance.GenerateEntityRelationshipDiagram(...)` on the
SqlServer-namespaced generators must switch to type-name form, e.g.
`Markdown.SqlServer.SqlStatementGenerator.GenerateSequenceDiagram(...)`.
Calls through the Snowflake / LinqToSql / PostgreSql wrapper classes are
unaffected at the call site.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 14:22:27 -05:00
Thom LambandClaude Opus 4.7 2c23ba4a88 chore(sonar)!: return empty IQueryable instead of null from GetQuery<T> (S1168)
Replaces `null` returns from `SqlBreakdownBase.GetQuery<T>()` and its four
overrides (LinqQueryBreakdown, SqlServer/PostgreSql/Snowflake QueryBreakdown)
with `Enumerable.Empty<T>().AsQueryable()`, and tightens the signature from
`IQueryable<T>?` to `IQueryable<T>`. The "we can't reconstruct" semantic
now lives in "the query yields zero rows" rather than a nullable return,
which is what callers in LINQ pipelines actually want.

Three LinqQueryBreakdownTests tests asserting `Is.Null` are renamed and
updated to assert `Is.Empty`.

BREAKING CHANGE: SqlBreakdownBase.GetQuery<T> and the SqlServer / PostgreSql /
Snowflake / LinqToSql QueryBreakdown.GetQuery<T> overrides no longer return
`IQueryable<T>?`; they now return a non-nullable `IQueryable<T>` that is
empty when reconstruction isn't possible. External NuGet consumers null-
checking the result must switch to `.Any()` / `Is.Empty` checks instead.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 14:18:31 -05:00
Thom LambandClaude Opus 4.7 4fe9eb36e6 chore(sonar): second sweep — catch .Length, Is.Not.Empty, and newly-exposed Multiple groups (NUnit2046, NUnit2045)
SonarQube Analysis / sonarqube (pull_request) Successful in 2m59s
Re-runs `dotnet format analyzers --diagnostics NUnit2046 NUnit2045` after
the Tier 2 Assert.Multiple wrap, which exposed:

- `Has.Length.EqualTo(n)` rewrites for `string[]`/array `.Length` checks
  (the first pass only knew about `.Count`).
- `Is.Not.Empty` rewrites for `Count, Is.GreaterThan(0)`.
- A handful of new NUnit2045 groups that became wrappable once the
  initial Multiple blocks settled the surrounding indentation.

Tests still 1180/1180 passing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 12:14:13 -05:00
Thom LambandClaude Opus 4.7 470cb009b9 chore(sonar): cache JsonSerializerOptions in test fixture (CA1869)
SonarQube Analysis / sonarqube (pull_request) Successful in 5m12s
Promote the `new JsonSerializerOptions { Converters = { ... } }` instance
that `OneTimeSetup` was constructing on each fixture run to a
`private static readonly JsonSerializerOptions _jsonOptions` field, so
the converter list isn't rebuilt per fixture. Strictly cosmetic here
(OneTimeSetup runs once) but it's the change the analyzer wants and the
field is the more idiomatic JsonSerializer pattern.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 12:02:25 -05:00
Thom LambandClaude Opus 4.7 77eb5d8b45 chore(sonar): hoist constant arrays to static readonly fields in tests (CA1861)
Two inline `new[] { ... }` literals inside TestCaseSource yield-returns
get hoisted to `static readonly string[]` fields with descriptive names
(`monthListValues`, `calendarRange`) matching the surrounding test-case
identifiers. Avoids reconstructing the same array on each call.

Only the test-file CA1861 sites are touched; the three production-code
sites flagged for the same rule are out of scope for this branch and
will land separately.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 12:01:34 -05:00
Thom LambandClaude Opus 4.7 67512d23e1 chore(sonar): wrap independent assertions in Assert.Multiple (NUnit2045)
Driven by `dotnet format analyzers --diagnostics NUnit2045`. The fixer
groups consecutive independent `Assert.That(...)` calls into
`Assert.Multiple(() => { ... })`, so a failing assertion no longer
short-circuits the block — every failure inside the group is reported,
which gives much better diagnostics on multi-property tests.

Audit confirmed no Assert.Throws / Assert.Fail / Assert.Catch / Assert.Pass
/ Assert.DoesNotThrow got pulled inside a Multiple block (those need to
short-circuit). All 1180 tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 11:59:47 -05:00
Thom LambandClaude Opus 4.7 505ada5017 chore(sonar): adopt Has.Count, Is.Empty, Does.Contain, char overloads in tests (NUnit2046, NUnit2011, CA1866)
- NUnit2046: `Assert.That(x.Count, Is.EqualTo(n))` → `Assert.That(x, Has.Count.EqualTo(n))` (or `Is.Empty` when n==0)
- NUnit2011: `Assert.That(s.Contains(x))` → `Assert.That(s, Does.Contain(x))` for richer failure messages
- CA1866: `.StartsWith("$"|"@"|":")` → `.StartsWith('$'|'@'|':')` char overload

Driven by `dotnet format analyzers --diagnostics NUnit2046 NUnit2011 CA1866`
for the cases the Roslyn fixer handles, plus a regex sweep for the remaining
`Count == n` (n>0) cases which the fixer doesn't address. CA1866 had no
associated code fix and was edited by hand (3 sites in 2 files). All tests
green (1180 passing).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 11:58:00 -05:00
Thom LambandClaude Opus 4.7 6af239035b chore(sonar): use concrete return types where binary-safe (CA1859)
CA1859 has no `dotnet format` batch fixer, so applied manually after
verifying each site is private/internal/test (no public-surface impact):

- Markdown.TryParseLogicalOperation: Expression? -> BoolExpr?  (private)
- Markdown.TryParseComparison:       Expression? -> Comparison? (private)
- RuleSet.GetAllSingleRules(IGroup): IEnumerable<SingleRule> -> List<SingleRule> (private overload)
- SqlExpressionClause.SplitOnComma:  IEnumerable<string> -> List<string> (private)
- QueryBreakdownRepositoryTests._repository: IQueryBreakdownRepository -> QueryBreakdownRepository (test private field)
- UnitTest1.StartsWith(...,string):  Expression -> MethodCallExpression (test private helper)

The public `RuleSet.GetAllSingleRules()` overload still returns
IEnumerable<SingleRule> — only the private recursive helper was tightened.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 15:49:20 -05:00
Thom LambandClaude Opus 4.7 f5d539b906 chore(sonar): hoist constant array literals to static readonly fields (CA1861)
Applied via `dotnet format analyzers --diagnostics CA1861 --severity info`,
plus manual cleanup:

- Renamed two cryptic fixer-generated field names:
  - QueryBreakdownCollection.stringArray -> SnowflakeFunctionNames (and
    inlined the now-redundant local alias)
  - ExpressionObjectTests.arg2 -> NotInValues
- Deduped three identical `separator = ['\r','\n']` fields the fixer
  emitted in the same test class (kept the first declaration; the other
  two test methods now reuse it).

8 files touched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 15:45:16 -05:00
Thom LambandClaude Opus 4.7 84b06557e5 chore(sonar)!: mark instance-data-free members static (CA1822)
Applied via `dotnet format analyzers --diagnostics CA1822 --severity info`.
12 files touched. The fixer also updated internal callers in tests to use
the type-name form (e.g. `gen.Method(x)` -> `Generator.Method(x)`); build
and full test suite remain green.

BREAKING CHANGE: two public methods become static and therefore can no
longer be invoked through an instance reference by external consumers:
- Strata.SqlTools.Markdown.LinqToSql.QueryBreakdownGenerator.GenerateMethodChainDiagram
- Strata.SqlTools.Markdown.LinqToSql.SqlStatementGenerator.GenerateLinqPipelineDiagram

Both are stateless utility methods on Generator classes — the static form
is the correct shape; the only callers in this repo already used the
type-name form. External code should change `gen.GenerateMethodChainDiagram(...)`
to `QueryBreakdownGenerator.GenerateMethodChainDiagram(...)`.

All other CA1822 hits in this commit are on private/protected members
(no public-surface impact).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 15:41:46 -05:00
Thom LambandClaude Opus 4.7 2c2a8b1193 chore(sonar): bulk-fix mechanical CA/IDE analyzer warnings
Applied via `dotnet format analyzers --diagnostics IDE0028 CA1825 CA1834
CA1845 CA1847 CA1860 CA1866 CA1853 CA1830 CA1846 CA1806 CA1869 CA2249
--severity info`. 19 files touched, all mechanical syntactic rewrites:

- CA1847: string.Contains("x") -> string.Contains('x')
- CA2249: s.IndexOf(c) == -1 -> !s.Contains(c)
- CA1830: sb.Append(sb.ToString()) -> sb.Append(sb)
- CA1834: StringBuilder.Append("x") -> Append('x')
- CA1825, CA1860, CA1866, CA1853, IDE0028, CA1845: corresponding fixers

Three rules in the batch had no batch fixer available (CA1846 ×4,
CA1806 ×1, CA1869 ×1) and stay open for separate manual handling.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 15:38:54 -05:00
Thom Lamb 012e693fe1 chore: refactor for sonarqube issues
SonarQube Analysis / sonarqube (pull_request) Successful in 6m3s
2026-05-22 17:13:31 -05:00
Thom LambandClaude Opus 4.7 2fae7738e3 test(pgsql): wrap CommandVisitor asserts in Assert.Multiple
Resolves SonarQube NUnit2045. The three independent parameter-index
assertions now report together instead of short-circuiting on the first
failure. The Is.Not.Null guard in the reflection helper stays outside the
multiple block because it gates the subsequent Invoke.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 13:02:28 -05:00
Thom LambandClaude Opus 4.7 5cb32d2311 refactor(query): extract shared Strata.SqlTools.Query model project
Moves the byte-identical 19-file ExpressionFactory/Query tree (duplicated
across Snowflake and SqlServer) into the new Strata.SqlTools.Query project
under the flat namespace Strata.SqlTools.Query. Both dialect projects now
reference the shared project; the Snowflake copies are deleted.

Also folds in the IDE0028 fix on CalculationFilterGroup.GetValidFilters
(collection expression []), which resolves both new-code IDE0028 smells
in one place now that there is a single copy.

Eliminates the 63 new duplicate lines flagged on the PR and removes the
largest contributor to the project's 11.1% duplication density. No
behavioral change: the moved types are identical to the originals.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 12:04:01 -05:00
Thom LambandClaude Opus 4.7 f96161a664 test(pgsql): align CommandVisitorTests namespace and drop dead fallback
Code review follow-up on acfe29e:
- Switch namespace to Strata.SqlTools.SqlBreakdown.Tests.PostgreSql to
  match the dominant convention in the sibling test folder.
- Remove the unreachable SqlServer.CommandVisitor fallback in the
  reflection helper. The Assert.That guard alone is enough to surface
  a future regression.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 16:39:31 -05:00
Thom LambandClaude Opus 4.7 acfe29ee98 fix(pgsql): make CommandVisitor parameter index instance-scoped
Resolves SonarQube S2696 in CommandVisitor.cs.

The static _parameterIndex field was mutated from an instance method,
causing every new CommandVisitor to inherit the previous instance's
counter and never reset. Parameter indices now restart at $1 per
visitor, which is the intended PostgreSQL behavior.

Adds CommandVisitorTests.TwoVisitors_HaveIndependentParameterIndices to
lock in the contract via reflection (FormatParameterName is protected).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 16:21:17 -05:00
Thom Lamb 8b0368e810 test: fix SqlBreakdown tests project
SonarQube Analysis / sonarqube (push) Failing after 3m21s
2026-05-15 13:39:14 -05:00
bermudalamb 79879c62b4 Update tests/Strata.SqlTools.Rules.Tests/Strata.SqlTools.Rules.Tests.csproj
SonarQube Analysis / sonarqube (push) Failing after 2m49s
2026-05-15 13:26:23 -05:00
Thom Lamb 5e467bcc9c chore: initial git load of code space 2026-05-12 08:52:33 -05:00