Merge pull request 'Resolve SonarQube Tech Debt' (#3) from fix/Sonarqube-Tech-Debt into main
SonarQube Analysis / sonarqube (push) Successful in 5m24s
SonarQube Analysis / sonarqube (push) Successful in 5m24s
Reviewed-on: #3
This commit was merged in pull request #3.
This commit is contained in:
@@ -13,7 +13,17 @@
|
||||
"PowerShell(git *)",
|
||||
"Bash(dotnet build *)",
|
||||
"Bash(command -v gh)",
|
||||
"Bash(command -v tea)"
|
||||
"Bash(command -v tea)",
|
||||
"PowerShell(if \\($env:SONARQUBE_URL -and $env:SONARQUBE_TOKEN\\) { \"URL set: $\\($env:SONARQUBE_URL\\)\"; \"Token set: $\\($env:SONARQUBE_TOKEN.Substring\\(0,8\\)\\)...\" } else { \"MISSING: URL=$\\($env:SONARQUBE_URL -ne $null\\) TOKEN=$\\($env:SONARQUBE_TOKEN -ne $null\\)\" })",
|
||||
"PowerShell($u = [Environment]::GetEnvironmentVariable\\(\"SONARQUBE_URL\",\"User\"\\); $t = [Environment]::GetEnvironmentVariable\\(\"SONARQUBE_TOKEN\",\"User\"\\); \"User URL: $u\"; if \\($t\\) { \"User TOKEN: $\\($t.Substring\\(0,8\\)\\)... \\(len $\\($t.Length\\)\\)\" } else { \"User TOKEN: <none>\" })",
|
||||
"Bash(grep -iE \"/\\(QueryConfig|CalculationFilter|LogicalOperator\\)\\\\.cs$\")",
|
||||
"PowerShell(dotnet build C:\\\\gitea\\\\sql-utilities --nologo -clp:NoSummary 2>&1 | Select-String -Pattern \"warning S\\\\d+\" | ForEach-Object { $_.Line.Trim\\(\\) } | Sort-Object -Unique)",
|
||||
"Bash(grep -oE 'S[0-9]{3,4}' build-sonar.log)",
|
||||
"Bash(dotnet sln *)",
|
||||
"Bash(Get-ChildItem src/Strata.SqlTools.SqlServer/ExpressionFactory/Query/*.cs)",
|
||||
"Bash(Measure-Object)",
|
||||
"Bash(Select-Object -ExpandProperty Count)",
|
||||
"PowerShell(dotnet build *)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 18
|
||||
VisualStudioVersion = 18.3.11527.330 d18.3
|
||||
VisualStudioVersion = 18.3.11527.330
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Strata.SqlTools.Rules", "src\Strata.SqlTools.Rules\Strata.SqlTools.Rules.csproj", "{11FE5F8E-5D66-4B68-87F5-9586CFA13B4C}"
|
||||
EndProject
|
||||
@@ -39,6 +39,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Strata.SqlTools.LinqToSql.T
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Strata.SqlTools.SqlBreakdown.Tests", "tests\Strata.SqlTools.SqlBreakdown.Tests\Strata.SqlTools.SqlBreakdown.Tests.csproj", "{80DC3F4B-D3C0-A104-39DC-889D65A15FAB}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Strata.SqlTools.Query", "src\Strata.SqlTools.Query\Strata.SqlTools.Query.csproj", "{177F3799-6A91-477B-87CC-BCCE05B87E1C}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -241,6 +243,18 @@ Global
|
||||
{80DC3F4B-D3C0-A104-39DC-889D65A15FAB}.Release|x64.Build.0 = Release|Any CPU
|
||||
{80DC3F4B-D3C0-A104-39DC-889D65A15FAB}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{80DC3F4B-D3C0-A104-39DC-889D65A15FAB}.Release|x86.Build.0 = Release|Any CPU
|
||||
{177F3799-6A91-477B-87CC-BCCE05B87E1C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{177F3799-6A91-477B-87CC-BCCE05B87E1C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{177F3799-6A91-477B-87CC-BCCE05B87E1C}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{177F3799-6A91-477B-87CC-BCCE05B87E1C}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{177F3799-6A91-477B-87CC-BCCE05B87E1C}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{177F3799-6A91-477B-87CC-BCCE05B87E1C}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{177F3799-6A91-477B-87CC-BCCE05B87E1C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{177F3799-6A91-477B-87CC-BCCE05B87E1C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{177F3799-6A91-477B-87CC-BCCE05B87E1C}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{177F3799-6A91-477B-87CC-BCCE05B87E1C}.Release|x64.Build.0 = Release|Any CPU
|
||||
{177F3799-6A91-477B-87CC-BCCE05B87E1C}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{177F3799-6A91-477B-87CC-BCCE05B87E1C}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@@ -262,6 +276,7 @@ Global
|
||||
{3AD058F3-DC6D-4894-939F-F12432683981} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
|
||||
{25773B63-302B-401E-AE45-6BBE7947562D} = {0AF8EC2A-1121-47D3-8011-DEFBB0C74490}
|
||||
{80DC3F4B-D3C0-A104-39DC-889D65A15FAB} = {0AF8EC2A-1121-47D3-8011-DEFBB0C74490}
|
||||
{177F3799-6A91-477B-87CC-BCCE05B87E1C} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {7775F930-B72A-4FA8-BB45-26D55AD44076}
|
||||
|
||||
@@ -0,0 +1,421 @@
|
||||
# Extract `Strata.SqlTools.Query` shared project + clear 3 new-code smells — Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Eliminate the Snowflake/SqlServer `ExpressionFactory/Query` duplication by extracting the byte-identical 19-file tree into a new `Strata.SqlTools.Query` project, and resolve the 3 remaining new-code SonarQube smells.
|
||||
|
||||
**Architecture:** A new standalone class library (`Strata.SqlTools.Query`, BCL-only, zero project references) holds one copy of the 19 query-config model types under the flat namespace `Strata.SqlTools.Query`. Both `Strata.SqlTools.Snowflake` and `Strata.SqlTools.SqlServer` reference it; their local copies are deleted. The existing ~819-test suite is the regression safety net.
|
||||
|
||||
**Tech Stack:** C# / .NET 8 / NUnit / SonarAnalyzer.CSharp 10.4.0 (via `Directory.Build.props`).
|
||||
|
||||
Implements `docs/superpowers/specs/2026-05-20-shared-query-project-design.md`.
|
||||
|
||||
**Working directory:** All paths relative to `C:\gitea\sql-utilities`. Branch `fix/Sonarqube-Tech-Debt` is checked out; `HEAD` is the spec commit `55ab737`.
|
||||
|
||||
---
|
||||
|
||||
## Why this is safe (facts established during planning)
|
||||
|
||||
- The 19 files in `src/Strata.SqlTools.Snowflake/ExpressionFactory/Query/` and `src/Strata.SqlTools.SqlServer/ExpressionFactory/Query/` are **byte-identical** except the `namespace` line.
|
||||
- Those files reference **only each other and BCL types** (no `using` beyond `System.*`; no `SqlBreakdown`/`Rules` references) → the new project needs no project references.
|
||||
- The **only** external consumers with an explicit `using …ExpressionFactory.Query;` are 3 files (all reference the SqlServer namespace):
|
||||
- `src/Strata.SqlTools.SqlServer/ExpressionFactory/ExpressionFactory.cs`
|
||||
- `tests/Strata.SqlTools.SqlBreakdown.Tests/ExpressionTests/ExpressionFactoryFilterTests.cs`
|
||||
- `tests/Strata.SqlTools.SqlBreakdown.Tests/ExpressionTests/ExpressionTestsBase.cs`
|
||||
- Other consumers reference the types **relatively** as `Query.X` (e.g. Snowflake `ExpressionFactory.cs`, whose namespace is `Strata.SqlTools.Snowflake.ExpressionFactory`). After the move these resolve automatically, because `Strata.SqlTools.Query` is reachable as `Query` from any `Strata.SqlTools.*` namespace.
|
||||
- **No file references both dialect Query namespaces**, so collapsing the two type families into one cannot create ambiguous overloads.
|
||||
|
||||
---
|
||||
|
||||
## File map
|
||||
|
||||
**Created:**
|
||||
- `src/Strata.SqlTools.Query/Strata.SqlTools.Query.csproj` — new BCL-only class library
|
||||
- `src/Strata.SqlTools.Query/*.cs` — the 19 moved model files (flat namespace `Strata.SqlTools.Query`)
|
||||
|
||||
**Modified:**
|
||||
- `Strata.SqlTools.QueryBreakdown.sln` — add the new project
|
||||
- `src/Strata.SqlTools.SqlServer/Strata.SqlTools.SqlServer.csproj` — add `ProjectReference` to `Strata.SqlTools.Query`
|
||||
- `src/Strata.SqlTools.Snowflake/Strata.SqlTools.Snowflake.csproj` — add `ProjectReference` to `Strata.SqlTools.Query`
|
||||
- `src/Strata.SqlTools.SqlServer/ExpressionFactory/ExpressionFactory.cs` — update `using`
|
||||
- `tests/Strata.SqlTools.SqlBreakdown.Tests/ExpressionTests/ExpressionFactoryFilterTests.cs` — update `using`
|
||||
- `tests/Strata.SqlTools.SqlBreakdown.Tests/ExpressionTests/ExpressionTestsBase.cs` — update `using`
|
||||
- `src/Strata.SqlTools.Query/CalculationFilterGroup.cs` — IDE0028 fix (`?? []`)
|
||||
- `tests/Strata.SqlTools.PostgreSql.Tests/PostgreSql/CommandVisitorTests.cs` — NUnit2045 fix (`Assert.Multiple`)
|
||||
|
||||
**Deleted:**
|
||||
- `src/Strata.SqlTools.Snowflake/ExpressionFactory/Query/*.cs` (19 files)
|
||||
- `src/Strata.SqlTools.SqlServer/ExpressionFactory/Query/*.cs` (19 files — moved, not copied)
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Scaffold the `Strata.SqlTools.Query` project
|
||||
|
||||
**Files:**
|
||||
- Create: `src/Strata.SqlTools.Query/Strata.SqlTools.Query.csproj`
|
||||
- Modify: `Strata.SqlTools.QueryBreakdown.sln`
|
||||
|
||||
- [ ] **Step 1: Record the baseline test result**
|
||||
|
||||
Run:
|
||||
```powershell
|
||||
dotnet test Strata.SqlTools.QueryBreakdown.sln -c Release --nologo
|
||||
```
|
||||
|
||||
Expected: build succeeds; full suite green. **Write down the total passed count** printed at the end — every later test step must match or exceed it (Task 3 leaves it unchanged; no tests are added or removed by this plan).
|
||||
|
||||
- [ ] **Step 2: Create the project directory and csproj**
|
||||
|
||||
Create `src/Strata.SqlTools.Query/Strata.SqlTools.Query.csproj` with this exact content (mirrors the dialect csprojs minus dialect-specific package refs):
|
||||
|
||||
```xml
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>latest</LangVersion>
|
||||
|
||||
<!-- NuGet Package Metadata -->
|
||||
<PackageId>Strata.SqlTools.Query</PackageId>
|
||||
<Version>1.0.0</Version>
|
||||
<Authors>Strata Decision Technology</Authors>
|
||||
<Company>Strata Decision Technology</Company>
|
||||
<Product>Strata SQL Utilities - Query Model</Product>
|
||||
<Description>Dialect-agnostic query configuration model (QueryConfig, filters, values, rows) shared by the Strata.SqlTools SQL Server and Snowflake dialect packages.</Description>
|
||||
<PackageTags>sql;query-builder;query-config;database</PackageTags>
|
||||
<PackageProjectUrl>https://github.com/stratadecision/sql-builder</PackageProjectUrl>
|
||||
<RepositoryUrl>https://github.com/stratadecision/sql-builder</RepositoryUrl>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<PackageReleaseNotes>Initial release: shared query configuration model extracted from the dialect packages.</PackageReleaseNotes>
|
||||
<Copyright>Copyright © Strata Decision Technology 2024-2026</Copyright>
|
||||
|
||||
<!-- Build Configuration -->
|
||||
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
|
||||
<IncludeSymbols>true</IncludeSymbols>
|
||||
<SymbolPackageFormat>symbols.nupkg</SymbolPackageFormat>
|
||||
<EmbedUntrackedSources>true</EmbedUntrackedSources>
|
||||
<ContinuousIntegrationBuild Condition="'$(CI)' == 'true'">true</ContinuousIntegrationBuild>
|
||||
|
||||
<!-- Code Analysis -->
|
||||
<EnableNETAnalyzers>true</EnableNETAnalyzers>
|
||||
<AnalysisLevel>latest</AnalysisLevel>
|
||||
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="..\..\README.md" Pack="true" PackagePath="\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Add the project to the solution**
|
||||
|
||||
Run:
|
||||
```powershell
|
||||
dotnet sln Strata.SqlTools.QueryBreakdown.sln add src/Strata.SqlTools.Query/Strata.SqlTools.Query.csproj
|
||||
```
|
||||
|
||||
Expected: `Project ... added to the solution.`
|
||||
|
||||
- [ ] **Step 4: Build the (empty) project to confirm it is well-formed**
|
||||
|
||||
Run:
|
||||
```powershell
|
||||
dotnet build src/Strata.SqlTools.Query/Strata.SqlTools.Query.csproj -c Release --nologo
|
||||
```
|
||||
|
||||
Expected: `Build succeeded.` (It compiles to an empty assembly — no .cs files yet.)
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```powershell
|
||||
git add src/Strata.SqlTools.Query/Strata.SqlTools.Query.csproj Strata.SqlTools.QueryBreakdown.sln
|
||||
git commit -m "build(query): scaffold empty Strata.SqlTools.Query shared project
|
||||
|
||||
First step of extracting the duplicated ExpressionFactory/Query model
|
||||
tree. Adds a BCL-only class library and registers it in the solution;
|
||||
files are moved in the next commit.
|
||||
|
||||
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Move the 19 model files into the shared project and rewire references
|
||||
|
||||
This task is atomic: the move, namespace rename, reference wiring, duplicate deletion, and `using` fixes must all land together for the solution to compile. Do **not** split the commit.
|
||||
|
||||
**Files:**
|
||||
- Move (via `git mv`): `src/Strata.SqlTools.SqlServer/ExpressionFactory/Query/*.cs` → `src/Strata.SqlTools.Query/`
|
||||
- Delete: `src/Strata.SqlTools.Snowflake/ExpressionFactory/Query/*.cs`
|
||||
- Modify: both dialect csprojs, the 3 explicit-`using` files, and `src/Strata.SqlTools.Query/CalculationFilterGroup.cs`
|
||||
|
||||
- [ ] **Step 1: Move SqlServer's 19 files into the new project (preserving git history)**
|
||||
|
||||
Run:
|
||||
```powershell
|
||||
$src = "src/Strata.SqlTools.SqlServer/ExpressionFactory/Query"
|
||||
$dst = "src/Strata.SqlTools.Query"
|
||||
Get-ChildItem "$src/*.cs" | ForEach-Object { git mv $_.FullName "$dst/$($_.Name)" }
|
||||
Get-ChildItem "$dst/*.cs" | Measure-Object | Select-Object -ExpandProperty Count
|
||||
```
|
||||
|
||||
Expected: prints `19`. The SqlServer `ExpressionFactory/Query` folder is now empty on disk (git does not track empty folders).
|
||||
|
||||
- [ ] **Step 2: Rename the namespace in all 19 moved files**
|
||||
|
||||
Run:
|
||||
```powershell
|
||||
Get-ChildItem "src/Strata.SqlTools.Query/*.cs" | ForEach-Object {
|
||||
$p = $_.FullName
|
||||
(Get-Content $p) -replace 'namespace Strata\.SqlTools\.SqlServer\.ExpressionFactory\.Query;', 'namespace Strata.SqlTools.Query;' | Set-Content $p
|
||||
}
|
||||
Select-String -Path "src/Strata.SqlTools.Query/*.cs" -Pattern '^namespace ' | Select-Object -ExpandProperty Line | Sort-Object -Unique
|
||||
```
|
||||
|
||||
Expected: the only line printed is `namespace Strata.SqlTools.Query;` (all 19 files now share the flat namespace).
|
||||
|
||||
- [ ] **Step 3: Delete Snowflake's duplicate 19 files**
|
||||
|
||||
Run:
|
||||
```powershell
|
||||
git rm src/Strata.SqlTools.Snowflake/ExpressionFactory/Query/*.cs
|
||||
```
|
||||
|
||||
Expected: `rm 'src/Strata.SqlTools.Snowflake/ExpressionFactory/Query/...'` ×19.
|
||||
|
||||
- [ ] **Step 4: Add the project reference to SqlServer**
|
||||
|
||||
In `src/Strata.SqlTools.SqlServer/Strata.SqlTools.SqlServer.csproj`, locate the `ItemGroup` containing the `SqlBreakdown` project reference:
|
||||
|
||||
```xml
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Strata.SqlTools.SqlBreakdown\Strata.SqlTools.SqlBreakdown.csproj" />
|
||||
</ItemGroup>
|
||||
```
|
||||
|
||||
Replace it with:
|
||||
|
||||
```xml
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Strata.SqlTools.SqlBreakdown\Strata.SqlTools.SqlBreakdown.csproj" />
|
||||
<ProjectReference Include="..\Strata.SqlTools.Query\Strata.SqlTools.Query.csproj" />
|
||||
</ItemGroup>
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Add the project reference to Snowflake**
|
||||
|
||||
In `src/Strata.SqlTools.Snowflake/Strata.SqlTools.Snowflake.csproj`, locate:
|
||||
|
||||
```xml
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Strata.SqlTools.SqlServer\Strata.SqlTools.SqlServer.csproj" />
|
||||
</ItemGroup>
|
||||
```
|
||||
|
||||
Replace it with:
|
||||
|
||||
```xml
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Strata.SqlTools.SqlServer\Strata.SqlTools.SqlServer.csproj" />
|
||||
<ProjectReference Include="..\Strata.SqlTools.Query\Strata.SqlTools.Query.csproj" />
|
||||
</ItemGroup>
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Fix the 3 explicit `using` directives**
|
||||
|
||||
In each of these files, change the line
|
||||
`using Strata.SqlTools.SqlServer.ExpressionFactory.Query;`
|
||||
to
|
||||
`using Strata.SqlTools.Query;`
|
||||
|
||||
- `src/Strata.SqlTools.SqlServer/ExpressionFactory/ExpressionFactory.cs`
|
||||
- `tests/Strata.SqlTools.SqlBreakdown.Tests/ExpressionTests/ExpressionFactoryFilterTests.cs`
|
||||
- `tests/Strata.SqlTools.SqlBreakdown.Tests/ExpressionTests/ExpressionTestsBase.cs`
|
||||
|
||||
Verify:
|
||||
```powershell
|
||||
Get-ChildItem src,tests -Recurse -Filter *.cs | Select-String -Pattern 'SqlServer\.ExpressionFactory\.Query'
|
||||
```
|
||||
|
||||
Expected: **no matches** (no source file references the old namespace anymore).
|
||||
|
||||
- [ ] **Step 7: Fix IDE0028 in the moved `CalculationFilterGroup.cs`**
|
||||
|
||||
In `src/Strata.SqlTools.Query/CalculationFilterGroup.cs`, change the `GetValidFilters` body:
|
||||
|
||||
Before:
|
||||
```csharp
|
||||
public IEnumerable<CalculationFilter> GetValidFilters()
|
||||
{
|
||||
return Filters?.Where(x => x.IsValid()).ToList() ?? new List<CalculationFilter>();
|
||||
}
|
||||
```
|
||||
|
||||
After:
|
||||
```csharp
|
||||
public IEnumerable<CalculationFilter> GetValidFilters()
|
||||
{
|
||||
return Filters?.Where(x => x.IsValid()).ToList() ?? [];
|
||||
}
|
||||
```
|
||||
|
||||
(This single edit resolves both original IDE0028 issues, which were the two now-merged copies.)
|
||||
|
||||
- [ ] **Step 8: Build the full solution**
|
||||
|
||||
Run:
|
||||
```powershell
|
||||
dotnet build Strata.SqlTools.QueryBreakdown.sln -c Release --nologo
|
||||
```
|
||||
|
||||
Expected: `Build succeeded.`
|
||||
|
||||
**If you see `CS0246: The type or namespace name 'X' could not be found`** in a file not listed above: that file referenced a Query type by bare name without resolving it relatively. Fix it by adding a file-scoped `using Strata.SqlTools.Query;` to that specific file (do **not** add a project-wide global using — generic names like `Filter`/`Value`/`Field`/`Row` could become ambiguous). Re-run the build until clean.
|
||||
|
||||
- [ ] **Step 9: Run the full test suite**
|
||||
|
||||
Run:
|
||||
```powershell
|
||||
dotnet test Strata.SqlTools.QueryBreakdown.sln -c Release --no-build --nologo
|
||||
```
|
||||
|
||||
Expected: all tests pass; total count equals the baseline recorded in Task 1 Step 1 (no tests added or removed).
|
||||
|
||||
- [ ] **Step 10: Commit**
|
||||
|
||||
```powershell
|
||||
git add -A
|
||||
git commit -m "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>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Fix NUnit2045 in `CommandVisitorTests`
|
||||
|
||||
**Files:**
|
||||
- Modify: `tests/Strata.SqlTools.PostgreSql.Tests/PostgreSql/CommandVisitorTests.cs:18-20`
|
||||
|
||||
**Background:** SonarQube flags `NUnit2045` because the three independent `Assert.That` calls run sequentially — a failure in the first hides the others. Wrapping them in `Assert.Multiple` reports all three together. The `Assert.That(method, Is.Not.Null, …)` guard in the helper method stays as-is: it gates a subsequent reflection `Invoke`, so it must short-circuit and must **not** be inside a multiple block.
|
||||
|
||||
- [ ] **Step 1: Wrap the three assertions in `Assert.Multiple`**
|
||||
|
||||
In `tests/Strata.SqlTools.PostgreSql.Tests/PostgreSql/CommandVisitorTests.cs`, replace lines 18-20:
|
||||
|
||||
Before:
|
||||
```csharp
|
||||
Assert.That(param1a, Is.EqualTo("$1"), "first visitor's first parameter should be $1");
|
||||
Assert.That(param1b, Is.EqualTo("$2"), "first visitor's second parameter should be $2");
|
||||
Assert.That(param2a, Is.EqualTo("$1"), "second visitor must start at $1, not inherit visitor1's counter");
|
||||
```
|
||||
|
||||
After:
|
||||
```csharp
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(param1a, Is.EqualTo("$1"), "first visitor's first parameter should be $1");
|
||||
Assert.That(param1b, Is.EqualTo("$2"), "first visitor's second parameter should be $2");
|
||||
Assert.That(param2a, Is.EqualTo("$1"), "second visitor must start at $1, not inherit visitor1's counter");
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the affected test**
|
||||
|
||||
Run:
|
||||
```powershell
|
||||
dotnet test tests/Strata.SqlTools.PostgreSql.Tests/Strata.SqlTools.PostgreSql.Tests.csproj -c Release --filter "FullyQualifiedName~CommandVisitorTests" --nologo
|
||||
```
|
||||
|
||||
Expected: PASS (1 test). Behavior is unchanged — only the assertion grouping changed.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```powershell
|
||||
git add tests/Strata.SqlTools.PostgreSql.Tests/PostgreSql/CommandVisitorTests.cs
|
||||
git commit -m "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>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Final verification
|
||||
|
||||
- [ ] **Step 1: Clean working tree, full build + test**
|
||||
|
||||
Run:
|
||||
```powershell
|
||||
git status --short
|
||||
dotnet build Strata.SqlTools.QueryBreakdown.sln -c Release --nologo
|
||||
dotnet test Strata.SqlTools.QueryBreakdown.sln -c Release --no-build --nologo
|
||||
```
|
||||
|
||||
Expected: empty working tree; clean build; full suite green at the baseline count from Task 1 Step 1.
|
||||
|
||||
- [ ] **Step 2: Confirm no duplicate model files remain**
|
||||
|
||||
Run:
|
||||
```powershell
|
||||
Get-ChildItem -Recurse -Filter CalculationFilterGroup.cs src | Select-Object FullName
|
||||
```
|
||||
|
||||
Expected: exactly one path — `src/Strata.SqlTools.Query/CalculationFilterGroup.cs`. (No dialect copies.)
|
||||
|
||||
- [ ] **Step 3: Push a fresh SonarQube analysis (optional but recommended)**
|
||||
|
||||
Requires `$env:SONAR_TOKEN`. Run:
|
||||
```powershell
|
||||
./scan-sonar.ps1
|
||||
```
|
||||
|
||||
Then confirm on `https://snrqbe.bermudalamb.synology.me` (project `sql-utilities`):
|
||||
- New-code period shows **0** code smells (the two IDE0028 and the NUnit2045 are gone).
|
||||
- New-code **duplicated lines = 0**.
|
||||
- Project-wide `duplicated_lines_density` has dropped from 11.1%.
|
||||
|
||||
Ad-hoc check (token as Basic-auth username; see project memory):
|
||||
```powershell
|
||||
$b64 = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$($env:SONARQUBE_TOKEN):"))
|
||||
$h = @{ Authorization = "Basic $b64" }
|
||||
Invoke-RestMethod -Uri "$($env:SONARQUBE_URL)/api/issues/search?componentKeys=sql-utilities&inNewCodePeriod=true&resolved=false" -Headers $h |
|
||||
Select-Object -ExpandProperty total
|
||||
```
|
||||
Expected: `0`.
|
||||
|
||||
- [ ] **Step 4: Merge decision**
|
||||
|
||||
Do **not** push to the Gitea remote automatically (user works locally). Report the commit list (`git log --oneline main..HEAD`) and let the user choose merge / PR / hold.
|
||||
|
||||
---
|
||||
|
||||
## Out of scope (do not do in this plan)
|
||||
|
||||
- The other ~798 project-wide code smells (S3776 cognitive complexity, CA/IDE info-level, etc.).
|
||||
- Extracting non-identical dialect code (visitors, breakdowns, statement parsers).
|
||||
- Backward-compat `[TypeForwardedTo]` shims (packages are unpublished; revisit only if external consumers appear).
|
||||
- Pushing the branch to remote.
|
||||
@@ -0,0 +1,81 @@
|
||||
# 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:
|
||||
|
||||
1. **3 new-code code smells** (the only issues in SonarQube's new-code period):
|
||||
- `IDE0028` — `src/Strata.SqlTools.Snowflake/ExpressionFactory/Query/CalculationFilterGroup.cs:27`
|
||||
- `IDE0028` — `src/Strata.SqlTools.SqlServer/ExpressionFactory/Query/CalculationFilterGroup.cs:27`
|
||||
- `NUnit2045` — `tests/Strata.SqlTools.PostgreSql.Tests/PostgreSql/CommandVisitorTests.cs:18`
|
||||
2. **63 new duplicate lines** flagged on the PR — caused by the `CalculationFilterGroup` rewrite 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.csproj`
|
||||
- `net8.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.Query` to the single flat namespace **`Strata.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.Snowflake` and `Strata.SqlTools.SqlServer` both add a `ProjectReference` to `Strata.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.cs`
|
||||
- `tests/Strata.SqlTools.SqlBreakdown.Tests/ExpressionTests/ExpressionFactoryFilterTests.cs`
|
||||
- `tests/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)
|
||||
|
||||
1. **IDE0028 ×2 → ×1, then fixed.** After extraction there is one `CalculationFilterGroup.cs`. Change line 27 `Filters?.Where(x => x.IsValid()).ToList() ?? new List<CalculationFilter>()` to use the collection expression `?? []`. One edit resolves both original issues.
|
||||
2. **NUnit2045.** In `CommandVisitorTests.cs`, wrap the three independent `Assert.That` assertions (lines 18–20) in `Assert.Multiple(() => { … });` so all three report together. (The `Assert.That(method, Is.Not.Null, …)` guard in the helper stays as-is — it gates a subsequent `Invoke`, 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
|
||||
|
||||
1. `dotnet build Strata.SqlTools.QueryBreakdown.sln -c Release` — clean (errors fail; pre-existing warnings OK).
|
||||
2. `dotnet test Strata.SqlTools.QueryBreakdown.sln -c Release --no-build` — all tests green at baseline (~819, including the Task 4 `CommandVisitorTests`).
|
||||
3. Optional: `./scan-sonar.ps1` then 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_density` drops measurably (the identical tree no longer double-counts).
|
||||
- Build clean, full suite green, no behavioral change to query-config semantics.
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
namespace Strata.SqlTools.Snowflake.ExpressionFactory.Query;
|
||||
namespace Strata.SqlTools.Query;
|
||||
|
||||
public enum AggregationType
|
||||
{
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
namespace Strata.SqlTools.SqlServer.ExpressionFactory.Query;
|
||||
namespace Strata.SqlTools.Query;
|
||||
|
||||
public class CalculationFilter : Filter
|
||||
{
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Strata.SqlTools.Snowflake.ExpressionFactory.Query;
|
||||
namespace Strata.SqlTools.Query;
|
||||
|
||||
public class CalculationFilterGroup
|
||||
{
|
||||
@@ -24,7 +24,7 @@ public class CalculationFilterGroup
|
||||
|
||||
public IEnumerable<CalculationFilter> GetValidFilters()
|
||||
{
|
||||
return Filters?.Where(x => x.IsValid()).ToList() ?? new List<CalculationFilter>();
|
||||
return Filters?.Where(x => x.IsValid()).ToList() ?? [];
|
||||
}
|
||||
|
||||
public bool IsValid()
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Strata.SqlTools.SqlServer.ExpressionFactory.Query;
|
||||
namespace Strata.SqlTools.Query;
|
||||
|
||||
public class ColumnQueryConfig
|
||||
{
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
namespace Strata.SqlTools.SqlServer.ExpressionFactory.Query;
|
||||
namespace Strata.SqlTools.Query;
|
||||
|
||||
public enum DatePart
|
||||
{
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
namespace Strata.SqlTools.SqlServer.ExpressionFactory.Query;
|
||||
namespace Strata.SqlTools.Query;
|
||||
|
||||
public class Field
|
||||
{
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Strata.SqlTools.Snowflake.ExpressionFactory.Query;
|
||||
namespace Strata.SqlTools.Query;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a filter criteria for querying data with support for various filter types including lists, date ranges, and timeframes.
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
namespace Strata.SqlTools.Snowflake.ExpressionFactory.Query;
|
||||
namespace Strata.SqlTools.Query;
|
||||
|
||||
public class FilterCondition
|
||||
{
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Strata.SqlTools.SqlServer.ExpressionFactory.Query;
|
||||
namespace Strata.SqlTools.Query;
|
||||
|
||||
public class FilterGroup
|
||||
{
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
namespace Strata.SqlTools.SqlServer.ExpressionFactory.Query;
|
||||
namespace Strata.SqlTools.Query;
|
||||
|
||||
public enum FilterOperator
|
||||
{
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
namespace Strata.SqlTools.SqlServer.ExpressionFactory.Query;
|
||||
namespace Strata.SqlTools.Query;
|
||||
|
||||
public enum FilterType
|
||||
{
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Strata.SqlTools.SqlServer.ExpressionFactory.Query;
|
||||
namespace Strata.SqlTools.Query;
|
||||
|
||||
public enum LogicalOperator
|
||||
{
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Strata.SqlTools.Snowflake.ExpressionFactory.Query;
|
||||
namespace Strata.SqlTools.Query;
|
||||
|
||||
public class QueryConfig
|
||||
{
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
|
||||
namespace Strata.SqlTools.Snowflake.ExpressionFactory.Query;
|
||||
namespace Strata.SqlTools.Query;
|
||||
|
||||
public static class QueryConfigExtensions
|
||||
{
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
namespace Strata.SqlTools.Snowflake.ExpressionFactory.Query;
|
||||
namespace Strata.SqlTools.Query;
|
||||
|
||||
public class Row : Field
|
||||
{
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
namespace Strata.SqlTools.SqlServer.ExpressionFactory.Query;
|
||||
namespace Strata.SqlTools.Query;
|
||||
|
||||
public enum SortDirection
|
||||
{
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
namespace Strata.SqlTools.Snowflake.ExpressionFactory.Query;
|
||||
namespace Strata.SqlTools.Query;
|
||||
|
||||
public class SqlResponse
|
||||
{
|
||||
@@ -0,0 +1,42 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>latest</LangVersion>
|
||||
|
||||
<!-- NuGet Package Metadata -->
|
||||
<PackageId>Strata.SqlTools.Query</PackageId>
|
||||
<Version>1.0.0</Version>
|
||||
<Authors>Strata Decision Technology</Authors>
|
||||
<Company>Strata Decision Technology</Company>
|
||||
<Product>Strata SQL Utilities - Query Model</Product>
|
||||
<Description>Dialect-agnostic query configuration model (QueryConfig, filters, values, rows) shared by the Strata.SqlTools SQL Server and Snowflake dialect packages.</Description>
|
||||
<PackageTags>sql;query-builder;query-config;database</PackageTags>
|
||||
<PackageProjectUrl>https://github.com/stratadecision/sql-builder</PackageProjectUrl>
|
||||
<RepositoryUrl>https://github.com/stratadecision/sql-builder</RepositoryUrl>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<PackageReleaseNotes>Initial release: shared query configuration model extracted from the dialect packages.</PackageReleaseNotes>
|
||||
<Copyright>Copyright © Strata Decision Technology 2024-2026</Copyright>
|
||||
|
||||
<!-- Build Configuration -->
|
||||
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
|
||||
<IncludeSymbols>true</IncludeSymbols>
|
||||
<SymbolPackageFormat>symbols.nupkg</SymbolPackageFormat>
|
||||
<EmbedUntrackedSources>true</EmbedUntrackedSources>
|
||||
<ContinuousIntegrationBuild Condition="'$(CI)' == 'true'">true</ContinuousIntegrationBuild>
|
||||
|
||||
<!-- Code Analysis -->
|
||||
<EnableNETAnalyzers>true</EnableNETAnalyzers>
|
||||
<AnalysisLevel>latest</AnalysisLevel>
|
||||
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="..\..\README.md" Pack="true" PackagePath="\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Strata.SqlTools.SqlServer.ExpressionFactory.Query;
|
||||
namespace Strata.SqlTools.Query;
|
||||
|
||||
public class Value
|
||||
{
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
namespace Strata.SqlTools.Snowflake.ExpressionFactory.Query;
|
||||
namespace Strata.SqlTools.Query;
|
||||
|
||||
public class ValueFilter
|
||||
{
|
||||
@@ -1,12 +0,0 @@
|
||||
namespace Strata.SqlTools.Snowflake.ExpressionFactory.Query;
|
||||
|
||||
public class CalculationFilter : Filter
|
||||
{
|
||||
public IEnumerable<string> AliasedDataColumnIds { get; }
|
||||
|
||||
public CalculationFilter(int dataColumnId, IEnumerable<string> aliasedDataColumnIds, IEnumerable<object> values, IEnumerable<FilterCondition> conditions)
|
||||
: base(dataColumnId, FilterType.Conditions, values, conditions, DatePart.Continuous, false, 0, 0)
|
||||
{
|
||||
AliasedDataColumnIds = aliasedDataColumnIds;
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Strata.SqlTools.Snowflake.ExpressionFactory.Query;
|
||||
|
||||
public class ColumnQueryConfig
|
||||
{
|
||||
public int DataColumnId { get; set; }
|
||||
|
||||
public DatePart DatePart { get; set; }
|
||||
|
||||
public Filter? Filter { get; set; }
|
||||
|
||||
public int RowLimit { get; set; }
|
||||
|
||||
[JsonConstructor]
|
||||
public ColumnQueryConfig(int dataColumnId, DatePart datePart, Filter? filter, int rowLimit)
|
||||
{
|
||||
DataColumnId = dataColumnId;
|
||||
DatePart = datePart;
|
||||
Filter = filter != null && filter.IsValid() ? filter : null;
|
||||
RowLimit = rowLimit;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
namespace Strata.SqlTools.Snowflake.ExpressionFactory.Query;
|
||||
|
||||
public enum DatePart
|
||||
{
|
||||
Continuous = 0,
|
||||
Year,
|
||||
Quarter,
|
||||
Month,
|
||||
Week,
|
||||
Day,
|
||||
FiscalYear,
|
||||
FiscalQuarter
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
namespace Strata.SqlTools.Snowflake.ExpressionFactory.Query;
|
||||
|
||||
public class Field
|
||||
{
|
||||
public string ColumnAlias { get; set; } = string.Empty;
|
||||
public int DataColumnId { get; set; }
|
||||
public DatePart DatePart { get; set; }
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Strata.SqlTools.Snowflake.ExpressionFactory.Query;
|
||||
|
||||
public class FilterGroup
|
||||
{
|
||||
// Hereditary logical operation applied to all Filters
|
||||
public LogicalOperator LogicalOperator { get; set; }
|
||||
|
||||
public IEnumerable<Filter> Filters { get; }
|
||||
|
||||
public FilterGroup()
|
||||
{
|
||||
LogicalOperator = LogicalOperator.And;
|
||||
Filters = new List<Filter>();
|
||||
}
|
||||
|
||||
[JsonConstructor]
|
||||
public FilterGroup(IEnumerable<Filter> filters, LogicalOperator logicalOperator)
|
||||
{
|
||||
Filters = filters.Where(x => x.IsValid()).ToList();
|
||||
LogicalOperator = logicalOperator;
|
||||
}
|
||||
|
||||
public bool IsValid()
|
||||
{
|
||||
return Filters != null && Filters.Any();
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
namespace Strata.SqlTools.Snowflake.ExpressionFactory.Query;
|
||||
|
||||
public enum FilterOperator
|
||||
{
|
||||
Equals = 0,
|
||||
NotEquals = 1,
|
||||
LessThan = 2,
|
||||
LessThanOrEqualTo = 3,
|
||||
GreaterThan = 4,
|
||||
GreaterThanOrEqualTo = 5,
|
||||
Between = 6, // this is a function, not a comparison - x BETWEEN a AND b is the same as: x >= a AND x <= z
|
||||
Contains = 7,
|
||||
StartsWith = 8,
|
||||
EndsWith = 9
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
namespace Strata.SqlTools.Snowflake.ExpressionFactory.Query;
|
||||
|
||||
public enum FilterType
|
||||
{
|
||||
List = 0,
|
||||
Conditions = 1,
|
||||
Calendar = 2,
|
||||
Timeframe = 3
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Strata.SqlTools.Snowflake.ExpressionFactory.Query;
|
||||
|
||||
public enum LogicalOperator
|
||||
{
|
||||
[Display(Name = "and")]
|
||||
And,
|
||||
[Display(Name = "or")]
|
||||
Or
|
||||
}
|
||||
|
||||
public static class LogicalOperatorExtensions
|
||||
{
|
||||
public static string ToSql(this LogicalOperator logicalOperator, bool withSpaces = true)
|
||||
{
|
||||
var sql = "";
|
||||
switch (logicalOperator)
|
||||
{
|
||||
case LogicalOperator.And:
|
||||
sql = "and";
|
||||
break;
|
||||
case LogicalOperator.Or:
|
||||
sql = "or";
|
||||
break;
|
||||
}
|
||||
|
||||
return withSpaces ? $" {sql} " : sql;
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
namespace Strata.SqlTools.Snowflake.ExpressionFactory.Query;
|
||||
|
||||
public enum SortDirection
|
||||
{
|
||||
Asc = 0,
|
||||
Desc = 1
|
||||
}
|
||||
|
||||
public static class SortDirectionExtensions
|
||||
{
|
||||
public static string ToSql(this SortDirection sortDirection)
|
||||
{
|
||||
return sortDirection.ToString().ToUpper();
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Strata.SqlTools.Snowflake.ExpressionFactory.Query;
|
||||
|
||||
public class Value
|
||||
{
|
||||
public string ColumnAlias { get; }
|
||||
public string Calculation { get; }
|
||||
public IEnumerable<int> CalculationDataColumnIds { get; }
|
||||
public IEnumerable<string> AliasedIds { get; }
|
||||
|
||||
public IEnumerable<CalculationFilterGroup> FilterGroups { get; }
|
||||
|
||||
public Value() : this(string.Empty, string.Empty, new int[0], new string[0], new CalculationFilterGroup[0])
|
||||
{
|
||||
// FilterGroups = new List<CalculationFilterGroup>();
|
||||
}
|
||||
|
||||
[JsonConstructor]
|
||||
public Value(string columnAlias, string calculation, IEnumerable<int> calculationDataColumnIds, IEnumerable<string> aliasedIds, IEnumerable<CalculationFilterGroup> filterGroups)
|
||||
{
|
||||
ColumnAlias = columnAlias;
|
||||
Calculation = calculation;
|
||||
CalculationDataColumnIds = calculationDataColumnIds ?? Array.Empty<int>();
|
||||
AliasedIds = aliasedIds ?? aliasedIds ?? Array.Empty<string>();
|
||||
FilterGroups = filterGroups?.Where(x => x.IsValid()).ToList() ?? new List<CalculationFilterGroup>();
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Strata.SqlTools.SqlServer\Strata.SqlTools.SqlServer.csproj" />
|
||||
<ProjectReference Include="..\Strata.SqlTools.Query\Strata.SqlTools.Query.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -4,7 +4,7 @@ using Strata.SqlTools.SqlBreakdown.Expressions.Conditional;
|
||||
using Strata.SqlTools.SqlBreakdown.Expressions.Conditional.Comparisons;
|
||||
using Strata.SqlTools.SqlBreakdown.Expressions.Functions.Conditional;
|
||||
using Strata.SqlTools.SqlBreakdown.Expressions.Functions.DateTime;
|
||||
using Strata.SqlTools.SqlServer.ExpressionFactory.Query;
|
||||
using Strata.SqlTools.Query;
|
||||
|
||||
namespace Strata.SqlTools.SqlServer.ExpressionFactory;
|
||||
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
namespace Strata.SqlTools.SqlServer.ExpressionFactory.Query;
|
||||
|
||||
public enum AggregationType
|
||||
{
|
||||
Sum = 0,
|
||||
Count = 1,
|
||||
CountDistinct = 2,
|
||||
Avg = 3,
|
||||
Median = 4,
|
||||
Min = 5,
|
||||
Max = 6
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Strata.SqlTools.SqlServer.ExpressionFactory.Query;
|
||||
|
||||
public class CalculationFilterGroup
|
||||
{
|
||||
// Hereditary logical operation applied to all Filters
|
||||
public LogicalOperator LogicalOperator { get; set; }
|
||||
|
||||
public IEnumerable<CalculationFilter> Filters { get; set; }
|
||||
|
||||
public CalculationFilterGroup()
|
||||
{
|
||||
LogicalOperator = LogicalOperator.And;
|
||||
Filters = new List<CalculationFilter>();
|
||||
}
|
||||
|
||||
[JsonConstructor]
|
||||
public CalculationFilterGroup(IEnumerable<CalculationFilter> filters, LogicalOperator logicalOperator)
|
||||
{
|
||||
Filters = filters;
|
||||
LogicalOperator = logicalOperator;
|
||||
}
|
||||
|
||||
public IEnumerable<CalculationFilter> GetValidFilters()
|
||||
{
|
||||
return Filters?.Where(x => x.IsValid()).ToList() ?? new List<CalculationFilter>();
|
||||
}
|
||||
|
||||
public bool IsValid()
|
||||
{
|
||||
return Filters != null && Filters.Any(x => x.IsValid());
|
||||
}
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Strata.SqlTools.SqlServer.ExpressionFactory.Query;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a filter criteria for querying data with support for various filter types including lists, date ranges, and timeframes.
|
||||
/// Filters can be applied to specific data columns and support different date granularities.
|
||||
/// </summary>
|
||||
public class Filter
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the identifier of the data column to which this filter applies.
|
||||
/// </summary>
|
||||
public int DataColumnId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type of filter being applied (e.g., List, Calendar, Timeframe).
|
||||
/// </summary>
|
||||
public FilterType FilterType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the collection of values to filter by. The interpretation depends on the <see cref="FilterType"/>.
|
||||
/// </summary>
|
||||
public IEnumerable<object> Values { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the collection of filter conditions that define complex filtering logic.
|
||||
/// Only valid conditions are retained.
|
||||
/// </summary>
|
||||
public IEnumerable<FilterCondition> Conditions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the date granularity part for date-based filtering (e.g., Year, Month, Day, FiscalYear).
|
||||
/// </summary>
|
||||
public DatePart DatePart { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether to use NOT IN instead of IN for list-type filters.
|
||||
/// Only applies when <see cref="FilterType"/> is List.
|
||||
/// </summary>
|
||||
public bool ListUseNotIn { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the offset from the current time for timeframe-based filters.
|
||||
/// Used in conjunction with <see cref="DateTimeFrameCount"/> to define relative time periods.
|
||||
/// </summary>
|
||||
public int DateTimeFrameOffset { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the zero-based number of time increments from the offset.
|
||||
/// A value of 0 means current period, -1 means one period backward, and 1 means one period forward.
|
||||
/// The unit (year, month, day, etc.) is determined by the <see cref="DatePart"/> property.
|
||||
/// </summary>
|
||||
public int DateTimeFrameCount { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Filter"/> class with the specified filter criteria.
|
||||
/// </summary>
|
||||
/// <param name="dataColumnId">The identifier of the data column to filter.</param>
|
||||
/// <param name="filterType">The type of filter to apply.</param>
|
||||
/// <param name="values">The collection of values for the filter.</param>
|
||||
/// <param name="conditions">The collection of filter conditions (invalid conditions are automatically removed).</param>
|
||||
/// <param name="datePart">The date granularity for date-based filtering.</param>
|
||||
/// <param name="listUseNotIn">Whether to use NOT IN for list filters; false to use IN.</param>
|
||||
/// <param name="dateTimeFrameOffset">The offset from current time for timeframe filters.</param>
|
||||
/// <param name="dateTimeFrameCount">The number of time increments from the offset (0 = current, negative = past, positive = future).</param>
|
||||
[JsonConstructor]
|
||||
public Filter(int dataColumnId, FilterType filterType, IEnumerable<object> values, IEnumerable<FilterCondition> conditions, DatePart datePart, bool listUseNotIn, int dateTimeFrameOffset, int dateTimeFrameCount)
|
||||
{
|
||||
DataColumnId = dataColumnId;
|
||||
FilterType = filterType;
|
||||
Values = values;
|
||||
Conditions = conditions.Where(x => x.IsValid()).ToList();
|
||||
DatePart = datePart;
|
||||
ListUseNotIn = listUseNotIn;
|
||||
DateTimeFrameOffset = dateTimeFrameOffset;
|
||||
DateTimeFrameCount = dateTimeFrameCount;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether this filter has valid criteria that can be applied.
|
||||
/// A filter is valid if it has values, conditions, or non-default timeframe settings.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// <c>true</c> if the filter has values, conditions, or timeframe settings; otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
public bool IsValid()
|
||||
{
|
||||
return (Values != null && Values.Any()) || (Conditions != null && Conditions.Any()) || (DateTimeFrameCount != default || DateTimeFrameOffset != default);
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
namespace Strata.SqlTools.SqlServer.ExpressionFactory.Query;
|
||||
|
||||
public class FilterCondition
|
||||
{
|
||||
public FilterOperator Operator { get; set; }
|
||||
|
||||
public IEnumerable<object>? Values { get; set; }
|
||||
|
||||
// This is not hereditary to Values; it is used for combination with the next FilterCondition in the set
|
||||
// todo: That could be indexed to ensure accuracy
|
||||
public LogicalOperator LogicalOperator { get; set; }
|
||||
|
||||
public bool IsValid()
|
||||
{
|
||||
return Values != null && Values.Any();
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Strata.SqlTools.SqlServer.ExpressionFactory.Query;
|
||||
|
||||
public class QueryConfig
|
||||
{
|
||||
public IEnumerable<Row> Rows { get; set; }
|
||||
|
||||
public IEnumerable<Value> Values { get; set; }
|
||||
|
||||
public IEnumerable<FilterGroup> FilterGroups { get; }
|
||||
|
||||
public bool WithTotals { get; set; }
|
||||
|
||||
public int RowLimit { get; set; }
|
||||
|
||||
public QueryConfig()
|
||||
{
|
||||
Rows = new List<Row>();
|
||||
Values = new List<Value>();
|
||||
FilterGroups = new List<FilterGroup>();
|
||||
}
|
||||
|
||||
[JsonConstructor]
|
||||
public QueryConfig(IEnumerable<FilterGroup> filterGroups, IEnumerable<Row> rows, IEnumerable<Value> values, bool withTotals, int rowLimit)
|
||||
{
|
||||
FilterGroups = filterGroups.Where(x => x.IsValid()).ToList();
|
||||
Rows = rows;
|
||||
Values = values;
|
||||
WithTotals = withTotals;
|
||||
RowLimit = rowLimit;
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
|
||||
namespace Strata.SqlTools.SqlServer.ExpressionFactory.Query;
|
||||
|
||||
public static class QueryConfigExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets all column ids referenced by this <see cref="QueryConfig"/>
|
||||
/// </summary>
|
||||
/// <param name="queryConfig"></param>
|
||||
/// <returns></returns>
|
||||
public static int[] GetAllColumnIds(this QueryConfig queryConfig)
|
||||
{
|
||||
return queryConfig.Values.SelectMany(value => value.CalculationDataColumnIds)
|
||||
.Union(queryConfig.Values.SelectMany(x => x.FilterGroups.SelectMany(y => y.GetValidFilters().Select(f => f.DataColumnId))))
|
||||
.Union(queryConfig.Rows.Select(row => row.DataColumnId))
|
||||
// FilterGroup.Filters is pre-filtered at construction (see FilterGroup.cs JsonConstructor); no GetValidFilters() equivalent is needed here.
|
||||
.Union(queryConfig.FilterGroups.SelectMany(filterGroup => filterGroup.Filters.Select(filter => filter.DataColumnId)))
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
namespace Strata.SqlTools.SqlServer.ExpressionFactory.Query;
|
||||
|
||||
public class Row : Field
|
||||
{
|
||||
public SortDirection sortDirection { get; set; }
|
||||
|
||||
public Row()
|
||||
{
|
||||
sortDirection = SortDirection.Asc;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
namespace Strata.SqlTools.SqlServer.ExpressionFactory.Query;
|
||||
|
||||
public class SqlResponse
|
||||
{
|
||||
public string SqlQuery { get; }
|
||||
public IEnumerable<KeyValuePair<string, object>> Parameters { get; }
|
||||
|
||||
public SqlResponse(string sqlQuery, IEnumerable<KeyValuePair<string, object>> parameters)
|
||||
{
|
||||
SqlQuery = sqlQuery;
|
||||
Parameters = parameters;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
namespace Strata.SqlTools.SqlServer.ExpressionFactory.Query;
|
||||
|
||||
public class ValueFilter
|
||||
{
|
||||
public int DataColumnId { get; set; }
|
||||
|
||||
public object? FilterValue { get; set; }
|
||||
}
|
||||
@@ -41,6 +41,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Strata.SqlTools.SqlBreakdown\Strata.SqlTools.SqlBreakdown.csproj" />
|
||||
<ProjectReference Include="..\Strata.SqlTools.Query\Strata.SqlTools.Query.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -15,9 +15,12 @@ public class CommandVisitorTests
|
||||
var param1b = InvokeFormatParameterName(visitor1, "p");
|
||||
var param2a = InvokeFormatParameterName(visitor2, "p");
|
||||
|
||||
Assert.That(param1a, Is.EqualTo("$1"), "first visitor's first parameter should be $1");
|
||||
Assert.That(param1b, Is.EqualTo("$2"), "first visitor's second parameter should be $2");
|
||||
Assert.That(param2a, Is.EqualTo("$1"), "second visitor must start at $1, not inherit visitor1's counter");
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(param1a, Is.EqualTo("$1"), "first visitor's first parameter should be $1");
|
||||
Assert.That(param1b, Is.EqualTo("$2"), "first visitor's second parameter should be $2");
|
||||
Assert.That(param2a, Is.EqualTo("$1"), "second visitor must start at $1, not inherit visitor1's counter");
|
||||
});
|
||||
}
|
||||
|
||||
private static string InvokeFormatParameterName(CommandVisitor visitor, string name)
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
using Strata.SqlTools.SqlServer.ExpressionFactory.Query;
|
||||
using Strata.SqlTools.Query;
|
||||
|
||||
namespace Strata.SqlTools.SqlBreakdown.Tests.ExpressionTests;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using Strata.SqlTools.SqlBreakdown.Classes;
|
||||
using Strata.SqlTools.SqlBreakdown.Expressions;
|
||||
using Strata.SqlTools.SqlBreakdown.Expressions.Conditional;
|
||||
using Strata.SqlTools.SqlServer.ExpressionFactory.Query;
|
||||
using Strata.SqlTools.Query;
|
||||
using Strata.SqlTools.SqlBreakdown.Tests.RegisteredTables;
|
||||
using Strata.SqlTools.Visitors.Snowflake;
|
||||
using ExpressionFactoryBase = Strata.SqlTools.SqlServer.ExpressionFactory.ExpressionFactory;
|
||||
|
||||
Reference in New Issue
Block a user