Designs the extraction of the byte-identical 19-file ExpressionFactory/Query tree (duplicated across Snowflake + SqlServer) into a new Strata.SqlTools.Query project under a clean-break namespace, and folds in the 3 remaining new-code SonarQube smells (IDE0028 x2 on CalculationFilterGroup, NUnit2045 on CommandVisitorTests). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5.5 KiB
Extract Strata.SqlTools.Query shared project + clear 3 new-code smells — Design
Date: 2026-05-20
Branch: fix/Sonarqube-Tech-Debt
Author: Thom Lamb (with Claude)
Goal
Resolve the SonarQube tech debt introduced by yesterday's sweep:
- 3 new-code code smells (the only issues in SonarQube's new-code period):
IDE0028—src/Strata.SqlTools.Snowflake/ExpressionFactory/Query/CalculationFilterGroup.cs:27IDE0028—src/Strata.SqlTools.SqlServer/ExpressionFactory/Query/CalculationFilterGroup.cs:27NUnit2045—tests/Strata.SqlTools.PostgreSql.Tests/PostgreSql/CommandVisitorTests.cs:18
- 63 new duplicate lines flagged on the PR — caused by the
CalculationFilterGrouprewrite landing in two byte-identical copies (Snowflake + SqlServer).
Root cause of the duplication
Snowflake and SqlServer each carry a byte-identical 19-file ExpressionFactory/Query/ tree (verified: every file diffs to zero non-namespace lines). Project-wide duplication density is 11.1% (3,147 duplicated lines / 99 blocks); this tree is a major contributor. The CalculationFilterGroup change merely tipped the new-code duplication over threshold.
The two flagged files cannot be extracted in isolation: their transitive type closure pulls in 14 of the 19 types (QueryConfigExtensions → QueryConfig → Value → CalculationFilterGroup → CalculationFilter → Filter → …). The remaining 4 (AggregationType, ColumnQueryConfig, SqlResponse, ValueFilter) are also identical duplicates. Therefore the coherent unit of extraction is the whole 19-file tree.
Approach
Create a new shared class library and move the entire identical tree into it under a single new namespace.
New project
src/Strata.SqlTools.Query/Strata.SqlTools.Query.csprojnet8.0(matches all existing projects)PackageId = Strata.SqlTools.Query,GeneratePackageOnBuild = false(matches dialect packages)- No project references (the 19 types depend only on BCL:
System.Text.Json.Serialization,System.ComponentModel.DataAnnotations)
- Added to
Strata.SqlTools.QueryBreakdown.sln.
Namespace (clean break)
- All 19 types move from
Strata.SqlTools.{Snowflake,SqlServer}.ExpressionFactory.Queryto the single flat namespaceStrata.SqlTools.Query. - File names are unchanged. One physical copy of each type.
- Rationale for clean break: both dialect packages have
GeneratePackageOnBuild = false(not currently published), so the breaking namespace change has no live external consumers. If backward compat is later needed,[TypeForwardedTo]can be added without touching call sites.
Wiring
Strata.SqlTools.SnowflakeandStrata.SqlTools.SqlServerboth add aProjectReferencetoStrata.SqlTools.Query.- Delete all 38 dialect copies (19 in Snowflake, 19 in SqlServer).
- EFCore / LinqToSql / Markdown obtain the types transitively (they already reference the dialect projects).
Consumer migration
Consumers reference these types either via a using directive or via relative resolution through the dialect's ancestor namespace. After the move they need using Strata.SqlTools.Query;. Known explicit-using sites to update:
src/Strata.SqlTools.SqlServer/ExpressionFactory/ExpressionFactory.cstests/Strata.SqlTools.SqlBreakdown.Tests/ExpressionTests/ExpressionFactoryFilterTests.cstests/Strata.SqlTools.SqlBreakdown.Tests/ExpressionTests/ExpressionTestsBase.cs
Any remaining relative references break the build; the compiler enumerates them precisely. The fix is mechanical: add the using (or a per-project global using Strata.SqlTools.Query;).
Collision safety: No single source file references both dialect Query namespaces (verified via set intersection), so merging the two type families into one shared type cannot create ambiguous-overload or duplicate-signature errors.
Code smell fixes (folded into the same branch)
- IDE0028 ×2 → ×1, then fixed. After extraction there is one
CalculationFilterGroup.cs. Change line 27Filters?.Where(x => x.IsValid()).ToList() ?? new List<CalculationFilter>()to use the collection expression?? []. One edit resolves both original issues. - NUnit2045. In
CommandVisitorTests.cs, wrap the three independentAssert.Thatassertions (lines 18–20) inAssert.Multiple(() => { … });so all three report together. (TheAssert.That(method, Is.Not.Null, …)guard in the helper stays as-is — it gates a subsequentInvoke, so it is correctly not inside a multiple block.)
Out of scope
- The other 798 project-wide code smells (S3776 cognitive complexity, CA/IDE info-level, etc.).
- Extracting non-identical dialect code (visitors, breakdowns, statement parsers).
- Publishing/packaging changes beyond the new project's csproj.
- Pushing to the Gitea remote (user works locally; merge decision deferred).
Verification
dotnet build Strata.SqlTools.QueryBreakdown.sln -c Release— clean (errors fail; pre-existing warnings OK).dotnet test Strata.SqlTools.QueryBreakdown.sln -c Release --no-build— all tests green at baseline (~819, including the Task 4CommandVisitorTests).- Optional:
./scan-sonar.ps1then confirm on the dashboard that the 3 new-code smells are gone and new-code duplicated lines drop to 0.
Success criteria
- New-code period shows 0 code smells and 0 duplicated lines.
- Project-wide
duplicated_lines_densitydrops measurably (the identical tree no longer double-counts). - Build clean, full suite green, no behavioral change to query-config semantics.