Merge pull request 'chore(sonar): bulk-fix production INFO analyzer warnings + skill update' (#12) from chore/sonarqube-info-prod-bulk into main
SonarQube Analysis / sonarqube (push) Successful in 3m10s
SonarQube Analysis / sonarqube (push) Successful in 3m10s
Reviewed-on: #12
This commit was merged in pull request #12.
This commit is contained in:
@@ -38,29 +38,74 @@ Invoke-RestMethod -Uri "$url/api/issues/search?componentKeys=sql-utilities&resol
|
|||||||
|
|
||||||
`SONARQUBE_URL` / `SONARQUBE_TOKEN` are read here; `scan-sonar.ps1` reads the separate `SONAR_TOKEN` / `SONAR_HOST_URL` for the *upload* path — do not conflate them.
|
`SONARQUBE_URL` / `SONARQUBE_TOKEN` are read here; `scan-sonar.ps1` reads the separate `SONAR_TOKEN` / `SONAR_HOST_URL` for the *upload* path — do not conflate them.
|
||||||
|
|
||||||
## B — Cleanup loop
|
## B — Cleanup loop (tiered)
|
||||||
|
|
||||||
```dot
|
```dot
|
||||||
digraph cleanup {
|
digraph cleanup {
|
||||||
query [label="Query open issues + facets"];
|
query [label="Query open issues + facets=rules"];
|
||||||
pick [label="Pick one rule group"];
|
triage [label="Tier each rule:\nmechanical / judgment / API-impact"];
|
||||||
edit [label="Edit code"];
|
t1 [label="Tier 1 — mechanical:\nbulk-fix in one commit"];
|
||||||
build [label="dotnet build -c Release"];
|
t2 [label="Tier 2 — judgment:\none commit per rule, audit diff"];
|
||||||
test [label="dotnet test -c Release --no-build"];
|
t3 [label="Tier 3 — API-impact:\nmanual, accept !breaking or skip"];
|
||||||
commit [label="git commit\nchore(sonar): … (Sxxxx)"];
|
build [label="dotnet build + dotnet test\nafter EACH commit"];
|
||||||
more [label="More groups?" shape=diamond];
|
push [label="git push + open PR"];
|
||||||
scan [label="scan-sonar.ps1"];
|
ci [label="CI runs SonarScanner\non Java 17"];
|
||||||
verify [label="Query API: confirm net-down"];
|
verify [label="Re-query API:\nconfirm rule counts dropped"];
|
||||||
|
|
||||||
query -> pick -> edit -> build -> test -> commit -> more;
|
query -> triage;
|
||||||
more -> pick [label="yes"];
|
triage -> t1 -> build;
|
||||||
more -> scan [label="no"];
|
triage -> t2 -> build;
|
||||||
scan -> verify;
|
triage -> t3 -> build;
|
||||||
|
build -> push -> ci -> verify;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
1. **Query.** Start with `severities=MAJOR,MINOR` + `facets=rules` to see what's worth fixing.
|
**The loop has shifted from per-rule commits to *tiered batches* once a project has more than a handful of issues left.** Use `dotnet format analyzers --diagnostics <ids> --severity info` — it runs all the Roslyn-shipped code fixers for the listed diagnostics, including `CA*`, `IDE*`, and `NUnit*`. (The `csharpsquid:Sxxxx` family from SonarAnalyzer.CSharp usually has no `dotnet format` fixer; those still need manual edits.)
|
||||||
2. **Pick a rule group.** One Sxxxx (or one tightly-related cluster) per commit, easiest first so a late blocker doesn't strand the others.
|
|
||||||
3. **Edit → build → test.** Build must succeed; the warning count for the touched rule must drop. Tests must stay green. If a fix would degrade clarity or break a tested contract, prefer **Won't Fix on the server with a justification** over a forced code change — see [[sonarqube-wontfix-rules]] for the catalog of rules already triaged that way (S1168, S3925, CS8601, S107).
|
### Tier 1 — mechanical (one bulk commit)
|
||||||
4. **Commit.** `chore(sonar): <one-line> (Sxxxx)` — bang (`!`) if it's a breaking rename.
|
|
||||||
5. **After all groups:** `./scan-sonar.ps1` (needs `$env:SONAR_TOKEN`). Wait ~30-60s for the CE task to finish, then re-query the issues endpoint and confirm the open MAJOR/MINOR count fell by the expected number.
|
Rules where the fixer's rewrite is purely syntactic and can't change behavior — pile them into one `dotnet format` invocation, audit the diff for sanity, build, test, single commit:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
dotnet format analyzers Strata.SqlTools.QueryBreakdown.sln `
|
||||||
|
--diagnostics IDE0028 CA1825 CA1834 CA1845 CA1847 CA1860 CA1866 CA1853 CA1830 CA2249 `
|
||||||
|
--severity info --verbosity normal
|
||||||
|
```
|
||||||
|
|
||||||
|
Typical safe rules: `IDE0028`, `CA1825`, `CA1834`, `CA1845`, `CA1847`, `CA1853`, `CA1860`, `CA1866`, `CA2249`, `CA1830`. Some rules in this family report "no associated code fix" — they'll need manual handling separately.
|
||||||
|
|
||||||
|
### Tier 2 — judgment (one commit per rule)
|
||||||
|
|
||||||
|
Rules whose fixer can produce mediocre output or surprise the reader — bulk-fix but **audit before staging**:
|
||||||
|
|
||||||
|
- `CA1510` — `ArgumentNullException.ThrowIfNull` rollup; always safe but voluminous, deserves its own commit.
|
||||||
|
- `CA1822` — make-method-static; the fixer also rewrites internal callers to use type-name form. **Public methods becoming static are a binary-break for external NuGet consumers** — commit with `chore(sonar)!:` and a `BREAKING CHANGE:` footer naming each affected member, or revert those file diffs and apply only the private-helper changes.
|
||||||
|
- `CA1861` — hoists constant array args to `static readonly` fields. The fixer's field names are sometimes cryptic (`stringArray`, `arg2`) or it emits duplicate `separator` fields in the same class. After running, rename ugly fields and dedupe collisions before committing.
|
||||||
|
- `CA1854` — `TryGetValue`-style double-lookup elimination; trivial.
|
||||||
|
- `CA1859` — concrete return type for perf. **No batch fixer**: edit each site manually after checking visibility. Apply only to private/internal/test; on public/protected, either revert or treat as a `!breaking` change (most cases are private helpers, so this is usually fine).
|
||||||
|
|
||||||
|
### Tier 3 — manual (no fixer)
|
||||||
|
|
||||||
|
`CA1806`, `CA1846`, `CA1869`, the `csharpsquid:Sxxxx` family that has no `dotnet format` fixer, and individual public-API rules that need design judgment. Edit by hand, one rule at a time. If a fix would degrade clarity or break a tested contract, prefer **server-side Won't Fix with justification** — see [[sonarqube-wontfix-rules]] for the catalog already triaged that way (S1168, S3925, CS8601, S107).
|
||||||
|
|
||||||
|
### Guardrail
|
||||||
|
|
||||||
|
After every commit (Tier 1, 2, or 3):
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
dotnet build Strata.SqlTools.QueryBreakdown.sln -c Release --nologo
|
||||||
|
dotnet test Strata.SqlTools.QueryBreakdown.sln -c Release --no-build --nologo
|
||||||
|
```
|
||||||
|
|
||||||
|
Build succeeds with no new warnings beyond the baseline. Tests stay green. If either fails, fix or revert before continuing to the next tier — incremental builds can mask warning regressions, so re-run with `--no-incremental` if the warning count looks suspicious.
|
||||||
|
|
||||||
|
### Scan & verify
|
||||||
|
|
||||||
|
**Local `./scan-sonar.ps1` is blocked here** — the system JRE is 8 and the SonarScanner CLI requires Java 17 (`UnsupportedClassVersionError`). CI handles the scan: `.gitea/workflows/sonarqube.yml` runs `actions/setup-java@v4` with `temurin` 17 and uploads on every push and PR. So the path is **push → open PR → CI scans → re-query the API to confirm net-down**:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$h = @{ Authorization = "Basic $b64" }
|
||||||
|
Invoke-RestMethod -Uri "$url/api/issues/search?componentKeys=sql-utilities&resolved=false&facets=rules&ps=500" -Headers $h
|
||||||
|
```
|
||||||
|
|
||||||
|
The targeted rule IDs should each drop to 0 (or near-0 if the fixer left a few cases it couldn't batch-resolve). Commit subject style stays `chore(sonar): … (RuleId)` — bang (`!`) if the change is a deliberate breaking rename or visibility shift.
|
||||||
|
|||||||
@@ -186,9 +186,9 @@ public class QueryCollectionAnalyzer
|
|||||||
var table = query.FromClause?.Clause?.Trim();
|
var table = query.FromClause?.Clause?.Trim();
|
||||||
if (!string.IsNullOrWhiteSpace(table))
|
if (!string.IsNullOrWhiteSpace(table))
|
||||||
{
|
{
|
||||||
if (tableUsage.ContainsKey(table))
|
if (tableUsage.TryGetValue(table, out int value))
|
||||||
{
|
{
|
||||||
tableUsage[table]++;
|
tableUsage[table] = ++value;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -221,9 +221,9 @@ public class QueryCollectionAnalyzer
|
|||||||
foreach (var col in columns)
|
foreach (var col in columns)
|
||||||
{
|
{
|
||||||
var columnName = col.Trim();
|
var columnName = col.Trim();
|
||||||
if (columnUsage.ContainsKey(columnName))
|
if (columnUsage.TryGetValue(columnName, out int value))
|
||||||
{
|
{
|
||||||
columnUsage[columnName]++;
|
columnUsage[columnName] = ++value;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -71,10 +71,7 @@ public class LinqQueryBreakdown : QueryBreakdown
|
|||||||
/// <returns>A LinqQueryBreakdown representing the query structure.</returns>
|
/// <returns>A LinqQueryBreakdown representing the query structure.</returns>
|
||||||
public static LinqQueryBreakdown Analyze<T>(IQueryable<T> query)
|
public static LinqQueryBreakdown Analyze<T>(IQueryable<T> query)
|
||||||
{
|
{
|
||||||
if (query == null)
|
ArgumentNullException.ThrowIfNull(query);
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(query));
|
|
||||||
}
|
|
||||||
|
|
||||||
var breakdown = new LinqQueryBreakdown
|
var breakdown = new LinqQueryBreakdown
|
||||||
{
|
{
|
||||||
@@ -232,10 +229,7 @@ public class LinqQueryBreakdown : QueryBreakdown
|
|||||||
/// <returns>An InsertBreakdown representing the insert operation.</returns>
|
/// <returns>An InsertBreakdown representing the insert operation.</returns>
|
||||||
public static Breakdowns.SqlServer.InsertBreakdown AnalyzeInsert<T>(T entity) where T : class
|
public static Breakdowns.SqlServer.InsertBreakdown AnalyzeInsert<T>(T entity) where T : class
|
||||||
{
|
{
|
||||||
if (entity == null)
|
ArgumentNullException.ThrowIfNull(entity);
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(entity));
|
|
||||||
}
|
|
||||||
|
|
||||||
var breakdown = new Breakdowns.SqlServer.InsertBreakdown();
|
var breakdown = new Breakdowns.SqlServer.InsertBreakdown();
|
||||||
breakdown.TableName.Clause = typeof(T).Name;
|
breakdown.TableName.Clause = typeof(T).Name;
|
||||||
@@ -312,10 +306,7 @@ public class LinqQueryBreakdown : QueryBreakdown
|
|||||||
/// <returns>A DeleteBreakdown representing the delete operation.</returns>
|
/// <returns>A DeleteBreakdown representing the delete operation.</returns>
|
||||||
public static Breakdowns.SqlServer.DeleteBreakdown AnalyzeDelete<T>(Expression<Func<T, bool>> filterExpression) where T : class
|
public static Breakdowns.SqlServer.DeleteBreakdown AnalyzeDelete<T>(Expression<Func<T, bool>> filterExpression) where T : class
|
||||||
{
|
{
|
||||||
if (filterExpression == null)
|
ArgumentNullException.ThrowIfNull(filterExpression);
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(filterExpression));
|
|
||||||
}
|
|
||||||
|
|
||||||
var breakdown = new Breakdowns.SqlServer.DeleteBreakdown();
|
var breakdown = new Breakdowns.SqlServer.DeleteBreakdown();
|
||||||
breakdown.FromClause.Clause = typeof(T).Name;
|
breakdown.FromClause.Clause = typeof(T).Name;
|
||||||
@@ -343,14 +334,8 @@ public class LinqQueryBreakdown : QueryBreakdown
|
|||||||
Expression<Func<T, bool>> filterExpression,
|
Expression<Func<T, bool>> filterExpression,
|
||||||
Expression<Func<T, T>> updateExpression) where T : class
|
Expression<Func<T, T>> updateExpression) where T : class
|
||||||
{
|
{
|
||||||
if (filterExpression == null)
|
ArgumentNullException.ThrowIfNull(filterExpression);
|
||||||
{
|
ArgumentNullException.ThrowIfNull(updateExpression);
|
||||||
throw new ArgumentNullException(nameof(filterExpression));
|
|
||||||
}
|
|
||||||
if (updateExpression == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(updateExpression));
|
|
||||||
}
|
|
||||||
|
|
||||||
var breakdown = new Breakdowns.SqlServer.UpdateBreakdown();
|
var breakdown = new Breakdowns.SqlServer.UpdateBreakdown();
|
||||||
breakdown.TableName.Clause = typeof(T).Name;
|
breakdown.TableName.Clause = typeof(T).Name;
|
||||||
@@ -425,10 +410,7 @@ public class LinqQueryBreakdown : QueryBreakdown
|
|||||||
/// <returns>A string representation of the trace analysis.</returns>
|
/// <returns>A string representation of the trace analysis.</returns>
|
||||||
public static string AnalyzeTrace<T>(IQueryable<T> query, string? executionContext = null) where T : class
|
public static string AnalyzeTrace<T>(IQueryable<T> query, string? executionContext = null) where T : class
|
||||||
{
|
{
|
||||||
if (query == null)
|
ArgumentNullException.ThrowIfNull(query);
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(query));
|
|
||||||
}
|
|
||||||
|
|
||||||
var lines = new List<string>
|
var lines = new List<string>
|
||||||
{
|
{
|
||||||
@@ -580,7 +562,7 @@ public class LinqQueryBreakdown : QueryBreakdown
|
|||||||
/// <returns>True if SELECT contains *; otherwise, false.</returns>
|
/// <returns>True if SELECT contains *; otherwise, false.</returns>
|
||||||
public bool SelectsAllColumns()
|
public bool SelectsAllColumns()
|
||||||
{
|
{
|
||||||
return SelectClause?.Clause?.Contains("*") ?? false;
|
return SelectClause?.Clause?.Contains('*') ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -18,10 +18,7 @@ public static class ReverseConverterExtensions
|
|||||||
/// <returns>A new LinqQueryBreakdown with the same clauses.</returns>
|
/// <returns>A new LinqQueryBreakdown with the same clauses.</returns>
|
||||||
public static LinqQueryBreakdown ToLinqQueryBreakdown(this QueryBreakdown breakdown)
|
public static LinqQueryBreakdown ToLinqQueryBreakdown(this QueryBreakdown breakdown)
|
||||||
{
|
{
|
||||||
if (breakdown == null)
|
ArgumentNullException.ThrowIfNull(breakdown);
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(breakdown));
|
|
||||||
}
|
|
||||||
|
|
||||||
var linq = new LinqQueryBreakdown(
|
var linq = new LinqQueryBreakdown(
|
||||||
breakdown.SelectClause?.Clause ?? "*",
|
breakdown.SelectClause?.Clause ?? "*",
|
||||||
@@ -54,10 +51,7 @@ public static class ReverseConverterExtensions
|
|||||||
/// <returns>A new LinqQueryBreakdown with the same clauses.</returns>
|
/// <returns>A new LinqQueryBreakdown with the same clauses.</returns>
|
||||||
public static LinqQueryBreakdown ToLinqQueryBreakdown(this PostgreSqlBreakdown breakdown)
|
public static LinqQueryBreakdown ToLinqQueryBreakdown(this PostgreSqlBreakdown breakdown)
|
||||||
{
|
{
|
||||||
if (breakdown == null)
|
ArgumentNullException.ThrowIfNull(breakdown);
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(breakdown));
|
|
||||||
}
|
|
||||||
|
|
||||||
var linq = new LinqQueryBreakdown(
|
var linq = new LinqQueryBreakdown(
|
||||||
breakdown.SelectClause?.Clause ?? "*",
|
breakdown.SelectClause?.Clause ?? "*",
|
||||||
@@ -90,10 +84,7 @@ public static class ReverseConverterExtensions
|
|||||||
/// <returns>A new LinqQueryBreakdown with the same clauses.</returns>
|
/// <returns>A new LinqQueryBreakdown with the same clauses.</returns>
|
||||||
public static LinqQueryBreakdown ToLinqQueryBreakdown(this SnowflakeBreakdown breakdown)
|
public static LinqQueryBreakdown ToLinqQueryBreakdown(this SnowflakeBreakdown breakdown)
|
||||||
{
|
{
|
||||||
if (breakdown == null)
|
ArgumentNullException.ThrowIfNull(breakdown);
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(breakdown));
|
|
||||||
}
|
|
||||||
|
|
||||||
var linq = new LinqQueryBreakdown(
|
var linq = new LinqQueryBreakdown(
|
||||||
breakdown.SelectClause?.Clause ?? "*",
|
breakdown.SelectClause?.Clause ?? "*",
|
||||||
@@ -127,10 +118,7 @@ public static class ReverseConverterExtensions
|
|||||||
/// <returns>A new breakdown in the target dialect format.</returns>
|
/// <returns>A new breakdown in the target dialect format.</returns>
|
||||||
public static object ConvertToDialect(this QueryBreakdown breakdown, string targetDialect)
|
public static object ConvertToDialect(this QueryBreakdown breakdown, string targetDialect)
|
||||||
{
|
{
|
||||||
if (breakdown == null)
|
ArgumentNullException.ThrowIfNull(breakdown);
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(breakdown));
|
|
||||||
}
|
|
||||||
|
|
||||||
return targetDialect.ToLowerInvariant() switch
|
return targetDialect.ToLowerInvariant() switch
|
||||||
{
|
{
|
||||||
@@ -150,10 +138,7 @@ public static class ReverseConverterExtensions
|
|||||||
/// <returns>A new breakdown in the target dialect format.</returns>
|
/// <returns>A new breakdown in the target dialect format.</returns>
|
||||||
public static object ConvertToDialect(this PostgreSqlBreakdown breakdown, string targetDialect)
|
public static object ConvertToDialect(this PostgreSqlBreakdown breakdown, string targetDialect)
|
||||||
{
|
{
|
||||||
if (breakdown == null)
|
ArgumentNullException.ThrowIfNull(breakdown);
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(breakdown));
|
|
||||||
}
|
|
||||||
|
|
||||||
return targetDialect.ToLowerInvariant() switch
|
return targetDialect.ToLowerInvariant() switch
|
||||||
{
|
{
|
||||||
@@ -173,10 +158,7 @@ public static class ReverseConverterExtensions
|
|||||||
/// <returns>A new breakdown in the target dialect format.</returns>
|
/// <returns>A new breakdown in the target dialect format.</returns>
|
||||||
public static object ConvertToDialect(this SnowflakeBreakdown breakdown, string targetDialect)
|
public static object ConvertToDialect(this SnowflakeBreakdown breakdown, string targetDialect)
|
||||||
{
|
{
|
||||||
if (breakdown == null)
|
ArgumentNullException.ThrowIfNull(breakdown);
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(breakdown));
|
|
||||||
}
|
|
||||||
|
|
||||||
return targetDialect.ToLowerInvariant() switch
|
return targetDialect.ToLowerInvariant() switch
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -71,10 +71,7 @@ public class QueryValidator
|
|||||||
/// <returns>This validator for method chaining.</returns>
|
/// <returns>This validator for method chaining.</returns>
|
||||||
public QueryValidator Validate(LinqQueryBreakdown breakdown)
|
public QueryValidator Validate(LinqQueryBreakdown breakdown)
|
||||||
{
|
{
|
||||||
if (breakdown == null)
|
ArgumentNullException.ThrowIfNull(breakdown);
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(breakdown));
|
|
||||||
}
|
|
||||||
|
|
||||||
_issues.Clear();
|
_issues.Clear();
|
||||||
|
|
||||||
@@ -225,7 +222,7 @@ public class QueryValidator
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ValidateWhereClause(LinqQueryBreakdown breakdown)
|
private static void ValidateWhereClause(LinqQueryBreakdown breakdown)
|
||||||
{
|
{
|
||||||
// No validation needed - WHERE is optional
|
// No validation needed - WHERE is optional
|
||||||
}
|
}
|
||||||
@@ -245,12 +242,12 @@ public class QueryValidator
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ValidateHavingClause(LinqQueryBreakdown breakdown)
|
private static void ValidateHavingClause(LinqQueryBreakdown breakdown)
|
||||||
{
|
{
|
||||||
// Validation delegated to ValidateGroupByClause
|
// Validation delegated to ValidateGroupByClause
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ValidateOrderByClause(LinqQueryBreakdown breakdown)
|
private static void ValidateOrderByClause(LinqQueryBreakdown breakdown)
|
||||||
{
|
{
|
||||||
// No validation needed - ORDER BY is optional
|
// No validation needed - ORDER BY is optional
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -248,13 +248,13 @@ public class LinqExpressionVisitor : ExpressionVisitor
|
|||||||
{
|
{
|
||||||
if (_isInWhereClause)
|
if (_isInWhereClause)
|
||||||
{
|
{
|
||||||
_whereBuilder.Append("(");
|
_whereBuilder.Append('(');
|
||||||
Visit(node.Left);
|
Visit(node.Left);
|
||||||
|
|
||||||
_whereBuilder.Append($" {GetOperator(node.NodeType)} ");
|
_whereBuilder.Append($" {GetOperator(node.NodeType)} ");
|
||||||
|
|
||||||
Visit(node.Right);
|
Visit(node.Right);
|
||||||
_whereBuilder.Append(")");
|
_whereBuilder.Append(')');
|
||||||
|
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
@@ -323,7 +323,7 @@ public class LinqExpressionVisitor : ExpressionVisitor
|
|||||||
return expression.ToString();
|
return expression.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetFullMemberName(MemberExpression expression)
|
private static string GetFullMemberName(MemberExpression expression)
|
||||||
{
|
{
|
||||||
var parts = new Stack<string>();
|
var parts = new Stack<string>();
|
||||||
var current = expression;
|
var current = expression;
|
||||||
@@ -354,7 +354,7 @@ public class LinqExpressionVisitor : ExpressionVisitor
|
|||||||
return string.Join(".", parts);
|
return string.Join(".", parts);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetOperator(ExpressionType nodeType)
|
private static string GetOperator(ExpressionType nodeType)
|
||||||
{
|
{
|
||||||
return nodeType switch
|
return nodeType switch
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -260,7 +260,7 @@ public class ExpressionGenerator : IVisitor<string>
|
|||||||
return sb.ToString();
|
return sb.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
private int GenerateMermaidNodes(Expression expression, StringBuilder sb, Dictionary<object, int> nodeMap, ref int nodeCounter)
|
private static int GenerateMermaidNodes(Expression expression, StringBuilder sb, Dictionary<object, int> nodeMap, ref int nodeCounter)
|
||||||
{
|
{
|
||||||
var currentNode = nodeCounter++;
|
var currentNode = nodeCounter++;
|
||||||
nodeMap[expression] = currentNode;
|
nodeMap[expression] = currentNode;
|
||||||
@@ -366,7 +366,7 @@ public class ExpressionGenerator : IVisitor<string>
|
|||||||
{
|
{
|
||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
return text.Substring(0, maxLength) + "...";
|
return string.Concat(text.AsSpan(0, maxLength), "...");
|
||||||
}
|
}
|
||||||
|
|
||||||
#region IVisitor Implementation
|
#region IVisitor Implementation
|
||||||
@@ -583,7 +583,7 @@ public class ExpressionGenerator : IVisitor<string>
|
|||||||
{
|
{
|
||||||
var sb = new StringBuilder();
|
var sb = new StringBuilder();
|
||||||
sb.AppendLine($"{Indent()}Function: {function.FunctionName}");
|
sb.AppendLine($"{Indent()}Function: {function.FunctionName}");
|
||||||
if (function.Arguments.Any())
|
if (function.Arguments.Length != 0)
|
||||||
{
|
{
|
||||||
_indentLevel++;
|
_indentLevel++;
|
||||||
sb.AppendLine($"{Indent()}Arguments:");
|
sb.AppendLine($"{Indent()}Arguments:");
|
||||||
@@ -602,7 +602,7 @@ public class ExpressionGenerator : IVisitor<string>
|
|||||||
{
|
{
|
||||||
var sb = new StringBuilder();
|
var sb = new StringBuilder();
|
||||||
sb.AppendLine($"{Indent()}Aggregate Function: {aggregateFunction.FunctionName}");
|
sb.AppendLine($"{Indent()}Aggregate Function: {aggregateFunction.FunctionName}");
|
||||||
if (aggregateFunction.Arguments.Any())
|
if (aggregateFunction.Arguments.Length != 0)
|
||||||
{
|
{
|
||||||
_indentLevel++;
|
_indentLevel++;
|
||||||
sb.AppendLine($"{Indent()}Arguments:");
|
sb.AppendLine($"{Indent()}Arguments:");
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ public class SimpleExpressionGenerator
|
|||||||
return sb.ToString();
|
return sb.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetExpressionDescription(Expression expression)
|
private static string GetExpressionDescription(Expression expression)
|
||||||
{
|
{
|
||||||
var typeName = expression.GetType().Name;
|
var typeName = expression.GetType().Name;
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ public class QueryBreakdownGenerator
|
|||||||
/// <param name="queryBreakdown">The LINQ QueryBreakdown to visualize.</param>
|
/// <param name="queryBreakdown">The LINQ QueryBreakdown to visualize.</param>
|
||||||
/// <param name="title">Optional title for the diagram.</param>
|
/// <param name="title">Optional title for the diagram.</param>
|
||||||
/// <returns>A string containing the Mermaid flowchart showing method calls.</returns>
|
/// <returns>A string containing the Mermaid flowchart showing method calls.</returns>
|
||||||
public string GenerateMethodChainDiagram(LinqQueryBreakdown queryBreakdown, string? title = null)
|
public static string GenerateMethodChainDiagram(LinqQueryBreakdown queryBreakdown, string? title = null)
|
||||||
{
|
{
|
||||||
var sb = new System.Text.StringBuilder();
|
var sb = new System.Text.StringBuilder();
|
||||||
|
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ public class SqlStatementGenerator
|
|||||||
/// <param name="queryBreakdown">The query breakdown containing query information.</param>
|
/// <param name="queryBreakdown">The query breakdown containing query information.</param>
|
||||||
/// <param name="title">Optional title for the diagram.</param>
|
/// <param name="title">Optional title for the diagram.</param>
|
||||||
/// <returns>A string containing the Mermaid diagram markdown.</returns>
|
/// <returns>A string containing the Mermaid diagram markdown.</returns>
|
||||||
public string GenerateLinqPipelineDiagram(IQueryBreakdown queryBreakdown, string? title = null)
|
public static string GenerateLinqPipelineDiagram(IQueryBreakdown queryBreakdown, string? title = null)
|
||||||
{
|
{
|
||||||
var sb = new System.Text.StringBuilder();
|
var sb = new System.Text.StringBuilder();
|
||||||
|
|
||||||
|
|||||||
@@ -177,7 +177,7 @@ public class QueryBreakdownGenerator
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Escapes text for Mermaid diagram labels to prevent syntax errors.
|
/// Escapes text for Mermaid diagram labels to prevent syntax errors.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private string EscapeMermaidText(string text)
|
private static string EscapeMermaidText(string text)
|
||||||
{
|
{
|
||||||
return text
|
return text
|
||||||
.Replace("\"", """)
|
.Replace("\"", """)
|
||||||
@@ -194,14 +194,14 @@ public class QueryBreakdownGenerator
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Truncates text to a maximum length and adds ellipsis if needed.
|
/// Truncates text to a maximum length and adds ellipsis if needed.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private string TruncateText(string text, int maxLength)
|
private static string TruncateText(string text, int maxLength)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(text) || text.Length <= maxLength)
|
if (string.IsNullOrWhiteSpace(text) || text.Length <= maxLength)
|
||||||
{
|
{
|
||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
|
|
||||||
return text.Substring(0, maxLength) + "...";
|
return string.Concat(text.AsSpan(0, maxLength), "...");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ public class SqlStatementGenerator
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Escapes text for Mermaid diagram labels.
|
/// Escapes text for Mermaid diagram labels.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private string EscapeMermaidText(string text)
|
private static string EscapeMermaidText(string text)
|
||||||
{
|
{
|
||||||
return text
|
return text
|
||||||
.Replace("\"", """)
|
.Replace("\"", """)
|
||||||
@@ -115,20 +115,20 @@ public class SqlStatementGenerator
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Truncates text to a maximum length.
|
/// Truncates text to a maximum length.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private string TruncateText(string text, int maxLength)
|
private static string TruncateText(string text, int maxLength)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(text) || text.Length <= maxLength)
|
if (string.IsNullOrWhiteSpace(text) || text.Length <= maxLength)
|
||||||
{
|
{
|
||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
|
|
||||||
return text.Substring(0, maxLength) + "...";
|
return string.Concat(text.AsSpan(0, maxLength), "...");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Cleans table name for use in Mermaid diagrams.
|
/// Cleans table name for use in Mermaid diagrams.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private string CleanTableName(string tableName)
|
private static string CleanTableName(string tableName)
|
||||||
{
|
{
|
||||||
return tableName
|
return tableName
|
||||||
.Replace("[", "")
|
.Replace("[", "")
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ public class StatementExpressionParser : SqlServerStatementExpressionParser
|
|||||||
|
|
||||||
if (reader.TokenType == TokenType.String || reader.TokenType == TokenType.ColumnIdentifier)
|
if (reader.TokenType == TokenType.String || reader.TokenType == TokenType.ColumnIdentifier)
|
||||||
{
|
{
|
||||||
columnBuilder.Append(".").Append(reader.TokenValue);
|
columnBuilder.Append('.').Append(reader.TokenValue);
|
||||||
reader.Read();
|
reader.Read();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -191,7 +191,7 @@ public class StatementExpressionParser : SqlServerStatementExpressionParser
|
|||||||
|
|
||||||
if (reader.TokenType == TokenType.ColumnIdentifier || reader.TokenType == TokenType.String)
|
if (reader.TokenType == TokenType.ColumnIdentifier || reader.TokenType == TokenType.String)
|
||||||
{
|
{
|
||||||
columnBuilder.Append(".").Append(reader.TokenValue);
|
columnBuilder.Append('.').Append(reader.TokenValue);
|
||||||
reader.Read();
|
reader.Read();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -113,9 +113,8 @@ public class StatementParser : SqlServerStatementParser
|
|||||||
// PostgreSQL-specific: Append LIMIT/OFFSET to ORDER BY if present
|
// PostgreSQL-specific: Append LIMIT/OFFSET to ORDER BY if present
|
||||||
var orderByClause = clauses.OrderByClause?.Clause ?? string.Empty;
|
var orderByClause = clauses.OrderByClause?.Clause ?? string.Empty;
|
||||||
|
|
||||||
if (clausePositions.ContainsKey(KeywordLimit))
|
if (clausePositions.TryGetValue(KeywordLimit, out int limitStart))
|
||||||
{
|
{
|
||||||
var limitStart = clausePositions[KeywordLimit];
|
|
||||||
var limitEnd = clausePositions.Values
|
var limitEnd = clausePositions.Values
|
||||||
.Where(v => v > limitStart)
|
.Where(v => v > limitStart)
|
||||||
.Order()
|
.Order()
|
||||||
@@ -127,9 +126,8 @@ public class StatementParser : SqlServerStatementParser
|
|||||||
: $"{orderByClause} {limitClause}";
|
: $"{orderByClause} {limitClause}";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (clausePositions.ContainsKey(KeywordOffset))
|
if (clausePositions.TryGetValue(KeywordOffset, out int offsetStart))
|
||||||
{
|
{
|
||||||
var offsetStart = clausePositions[KeywordOffset];
|
|
||||||
var offsetEnd = clausePositions.Values
|
var offsetEnd = clausePositions.Values
|
||||||
.Where(v => v > offsetStart)
|
.Where(v => v > offsetStart)
|
||||||
.Order()
|
.Order()
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ public class Value
|
|||||||
|
|
||||||
public IEnumerable<CalculationFilterGroup> FilterGroups { get; }
|
public IEnumerable<CalculationFilterGroup> FilterGroups { get; }
|
||||||
|
|
||||||
public Value() : this(string.Empty, string.Empty, new int[0], new string[0], new CalculationFilterGroup[0])
|
public Value() : this(string.Empty, string.Empty, Array.Empty<int>(), Array.Empty<string>(), Array.Empty<CalculationFilterGroup>())
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ public static class Markdown
|
|||||||
return ParseAtomicExpression(text);
|
return ParseAtomicExpression(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Expression? TryParseLogicalOperation(string text)
|
private static BoolExpr? TryParseLogicalOperation(string text)
|
||||||
{
|
{
|
||||||
foreach (var op in LogicalOperators.Keys)
|
foreach (var op in LogicalOperators.Keys)
|
||||||
{
|
{
|
||||||
@@ -130,7 +130,7 @@ public static class Markdown
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Expression? TryParseComparison(string text)
|
private static Comparison? TryParseComparison(string text)
|
||||||
{
|
{
|
||||||
foreach (var op in ComparisonOperators.Keys)
|
foreach (var op in ComparisonOperators.Keys)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ public class RuleSet : IGroup
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="group">The rule group to traverse.</param>
|
/// <param name="group">The rule group to traverse.</param>
|
||||||
/// <returns>An enumerable of all single rules found in the group.</returns>
|
/// <returns>An enumerable of all single rules found in the group.</returns>
|
||||||
private static IEnumerable<SingleRule> GetAllSingleRules(IGroup group)
|
private static List<SingleRule> GetAllSingleRules(IGroup group)
|
||||||
{
|
{
|
||||||
var rules = new List<SingleRule>();
|
var rules = new List<SingleRule>();
|
||||||
foreach (var rule in group.Rules)
|
foreach (var rule in group.Rules)
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ public class ProcedureBreakdown : SqlServerProcedureBreakdown
|
|||||||
|
|
||||||
sb.Append("CALL ");
|
sb.Append("CALL ");
|
||||||
sb.Append(ProcedureName.Clause);
|
sb.Append(ProcedureName.Clause);
|
||||||
sb.Append("(");
|
sb.Append('(');
|
||||||
|
|
||||||
if (IsUsingParameters)
|
if (IsUsingParameters)
|
||||||
{
|
{
|
||||||
@@ -68,7 +68,7 @@ public class ProcedureBreakdown : SqlServerProcedureBreakdown
|
|||||||
sb.Append(string.Join(", ", paramList));
|
sb.Append(string.Join(", ", paramList));
|
||||||
}
|
}
|
||||||
|
|
||||||
sb.Append(")");
|
sb.Append(')');
|
||||||
|
|
||||||
return sb.ToString();
|
return sb.ToString();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ public class QueryBreakdown : SqlServerQueryBreakdown
|
|||||||
|
|
||||||
if (parameterName.StartsWith('@'))
|
if (parameterName.StartsWith('@'))
|
||||||
{
|
{
|
||||||
return ":" + parameterName.Substring(1);
|
return string.Concat(":", parameterName.AsSpan(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add : prefix
|
// Add : prefix
|
||||||
@@ -159,7 +159,7 @@ public class QueryBreakdown : SqlServerQueryBreakdown
|
|||||||
if (paramName.StartsWith(':'))
|
if (paramName.StartsWith(':'))
|
||||||
{
|
{
|
||||||
// Add @param version
|
// Add @param version
|
||||||
var atParam = "@" + paramName.Substring(1);
|
var atParam = string.Concat("@", paramName.AsSpan(1));
|
||||||
if (!Parameters.ContainsKey(atParam))
|
if (!Parameters.ContainsKey(atParam))
|
||||||
{
|
{
|
||||||
Parameters[atParam] = Parameters[paramName];
|
Parameters[atParam] = Parameters[paramName];
|
||||||
@@ -168,7 +168,7 @@ public class QueryBreakdown : SqlServerQueryBreakdown
|
|||||||
else if (paramName.StartsWith('@'))
|
else if (paramName.StartsWith('@'))
|
||||||
{
|
{
|
||||||
// Add :param version
|
// Add :param version
|
||||||
var colonParam = ":" + paramName.Substring(1);
|
var colonParam = string.Concat(":", paramName.AsSpan(1));
|
||||||
if (!Parameters.ContainsKey(colonParam))
|
if (!Parameters.ContainsKey(colonParam))
|
||||||
{
|
{
|
||||||
Parameters[colonParam] = Parameters[paramName];
|
Parameters[colonParam] = Parameters[paramName];
|
||||||
@@ -539,7 +539,7 @@ public class QueryBreakdown : SqlServerQueryBreakdown
|
|||||||
|
|
||||||
if (i > 0)
|
if (i > 0)
|
||||||
{
|
{
|
||||||
sb.Append(",");
|
sb.Append(',');
|
||||||
sb.AppendLine();
|
sb.AppendLine();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ public class QueryBreakdownCollection : SqlServer.QueryBreakdownCollectionBase<Q
|
|||||||
|
|
||||||
if (!trimmed.EndsWith(';'))
|
if (!trimmed.EndsWith(';'))
|
||||||
{
|
{
|
||||||
sb.Append(";");
|
sb.Append(';');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add spacing between statements
|
// Add spacing between statements
|
||||||
@@ -111,7 +111,7 @@ public class QueryBreakdownCollection : SqlServer.QueryBreakdownCollectionBase<Q
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
var specificPattern = stageName.Contains("~")
|
var specificPattern = stageName.Contains('~')
|
||||||
? $@"@~/{System.Text.RegularExpressions.Regex.Escape(stageName.TrimStart('@', '~', '/'))}/"
|
? $@"@~/{System.Text.RegularExpressions.Regex.Escape(stageName.TrimStart('@', '~', '/'))}/"
|
||||||
: $@"@{System.Text.RegularExpressions.Regex.Escape(stageName.TrimStart('@'))}/";
|
: $@"@{System.Text.RegularExpressions.Regex.Escape(stageName.TrimStart('@'))}/";
|
||||||
|
|
||||||
@@ -182,6 +182,15 @@ public class QueryBreakdownCollection : SqlServer.QueryBreakdownCollectionBase<Q
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static readonly string[] SnowflakeFunctionNames =
|
||||||
|
[
|
||||||
|
"PARSE_JSON", "OBJECT_INSERT", "ARRAY_CONSTRUCT", "ARRAY_AGG",
|
||||||
|
"FLATTEN", "GET_PATH", "TRY_PARSE_JSON", "JSON_EXTRACT_PATH_TEXT",
|
||||||
|
"JSON_EXTRACT_PATH_WITH_DEFAULT", "HASHAGGREGATE", "LISTAGG",
|
||||||
|
"APPROX_COUNT_DISTINCT", "APPROX_PERCENTILE", "GREATEST", "LEAST",
|
||||||
|
"NULLIF", "ZEROIFNULL", "STRTOK", "SPLIT_PART", "PIVOT", "UNPIVOT"
|
||||||
|
];
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Filters queries that use Snowflake functions (PARSE_JSON, OBJECT_INSERT, ARRAY, etc.).
|
/// Filters queries that use Snowflake functions (PARSE_JSON, OBJECT_INSERT, ARRAY, etc.).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -191,17 +200,7 @@ public class QueryBreakdownCollection : SqlServer.QueryBreakdownCollectionBase<Q
|
|||||||
return QueryBreakdownList.Where(q =>
|
return QueryBreakdownList.Where(q =>
|
||||||
{
|
{
|
||||||
var sql = q.GetSql().ToUpperInvariant();
|
var sql = q.GetSql().ToUpperInvariant();
|
||||||
|
return SnowflakeFunctionNames.Any(func => sql.Contains(func));
|
||||||
var snowflakeFunctions = new[]
|
|
||||||
{
|
|
||||||
"PARSE_JSON", "OBJECT_INSERT", "ARRAY_CONSTRUCT", "ARRAY_AGG",
|
|
||||||
"FLATTEN", "GET_PATH", "TRY_PARSE_JSON", "JSON_EXTRACT_PATH_TEXT",
|
|
||||||
"JSON_EXTRACT_PATH_WITH_DEFAULT", "HASHAGGREGATE", "LISTAGG",
|
|
||||||
"APPROX_COUNT_DISTINCT", "APPROX_PERCENTILE", "GREATEST", "LEAST",
|
|
||||||
"NULLIF", "ZEROIFNULL", "STRTOK", "SPLIT_PART", "PIVOT", "UNPIVOT"
|
|
||||||
};
|
|
||||||
|
|
||||||
return snowflakeFunctions.Any(func => sql.Contains(func));
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -289,7 +288,7 @@ public class QueryBreakdownCollection : SqlServer.QueryBreakdownCollectionBase<Q
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private static bool UsesStageReference(QueryBreakdown query)
|
private static bool UsesStageReference(QueryBreakdown query)
|
||||||
{
|
{
|
||||||
return query.GetSql().Contains("@") &&
|
return query.GetSql().Contains('@') &&
|
||||||
(query.GetSql().Contains("FROM @") || query.GetSql().Contains(" @"));
|
(query.GetSql().Contains("FROM @") || query.GetSql().Contains(" @"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -119,9 +119,9 @@ public class StatementParser : SqlServerStatementParser
|
|||||||
protected override void PostProcessClauses(SqlClauses clauses, string sql, Dictionary<string, int> clausePositions)
|
protected override void PostProcessClauses(SqlClauses clauses, string sql, Dictionary<string, int> clausePositions)
|
||||||
{
|
{
|
||||||
// Snowflake-specific: Append LIMIT to ORDER BY if present
|
// Snowflake-specific: Append LIMIT to ORDER BY if present
|
||||||
if (clausePositions.ContainsKey(KeywordLimit))
|
if (clausePositions.TryGetValue(KeywordLimit, out int value))
|
||||||
{
|
{
|
||||||
var limitClause = sql.Substring(clausePositions[KeywordLimit]).Trim();
|
var limitClause = sql.Substring(value).Trim();
|
||||||
if (clauses.OrderByClause != null)
|
if (clauses.OrderByClause != null)
|
||||||
{
|
{
|
||||||
clauses.OrderByClause.Clause = string.IsNullOrEmpty(clauses.OrderByClause.Clause)
|
clauses.OrderByClause.Clause = string.IsNullOrEmpty(clauses.OrderByClause.Clause)
|
||||||
|
|||||||
@@ -88,10 +88,7 @@ public class SqlBreakdownCollection : ICollection<ISqlBreakdown>
|
|||||||
/// <exception cref="ArgumentNullException">Thrown when breakdown is null.</exception>
|
/// <exception cref="ArgumentNullException">Thrown when breakdown is null.</exception>
|
||||||
public void Add(ISqlBreakdown breakdown)
|
public void Add(ISqlBreakdown breakdown)
|
||||||
{
|
{
|
||||||
if (breakdown == null)
|
ArgumentNullException.ThrowIfNull(breakdown);
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(breakdown));
|
|
||||||
}
|
|
||||||
|
|
||||||
_breakdowns.Add(breakdown);
|
_breakdowns.Add(breakdown);
|
||||||
}
|
}
|
||||||
@@ -103,10 +100,7 @@ public class SqlBreakdownCollection : ICollection<ISqlBreakdown>
|
|||||||
/// <exception cref="ArgumentNullException">Thrown when breakdowns is null.</exception>
|
/// <exception cref="ArgumentNullException">Thrown when breakdowns is null.</exception>
|
||||||
public void AddRange(IEnumerable<ISqlBreakdown> breakdowns)
|
public void AddRange(IEnumerable<ISqlBreakdown> breakdowns)
|
||||||
{
|
{
|
||||||
if (breakdowns == null)
|
ArgumentNullException.ThrowIfNull(breakdowns);
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(breakdowns));
|
|
||||||
}
|
|
||||||
|
|
||||||
_breakdowns.AddRange(breakdowns);
|
_breakdowns.AddRange(breakdowns);
|
||||||
}
|
}
|
||||||
@@ -142,10 +136,7 @@ public class SqlBreakdownCollection : ICollection<ISqlBreakdown>
|
|||||||
/// <exception cref="ArgumentNullException">Thrown when sqlBatch is null.</exception>
|
/// <exception cref="ArgumentNullException">Thrown when sqlBatch is null.</exception>
|
||||||
public void ParseBatch(string sqlBatch, string? separator = null)
|
public void ParseBatch(string sqlBatch, string? separator = null)
|
||||||
{
|
{
|
||||||
if (sqlBatch == null)
|
ArgumentNullException.ThrowIfNull(sqlBatch);
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(sqlBatch));
|
|
||||||
}
|
|
||||||
|
|
||||||
Clear();
|
Clear();
|
||||||
|
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ public class SqlExpressionClause : SqlClause, ISqlExpressionClause
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="clause">The clause to split.</param>
|
/// <param name="clause">The clause to split.</param>
|
||||||
/// <returns>An enumerable collection of individual items.</returns>
|
/// <returns>An enumerable collection of individual items.</returns>
|
||||||
private static IEnumerable<string> SplitOnComma(string clause)
|
private static List<string> SplitOnComma(string clause)
|
||||||
{
|
{
|
||||||
var items = new List<string>();
|
var items = new List<string>();
|
||||||
var current = new StringBuilder();
|
var current = new StringBuilder();
|
||||||
|
|||||||
@@ -269,20 +269,15 @@ public static class FlatDataUtils
|
|||||||
|
|
||||||
public static object GetValue(this IFlatData data, string key)
|
public static object GetValue(this IFlatData data, string key)
|
||||||
{
|
{
|
||||||
if (data == null)
|
ArgumentNullException.ThrowIfNull(data);
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(data));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!data.ContainsKey(key))
|
if (!data.TryGetValue(key, out object? rawValue))
|
||||||
{
|
{
|
||||||
throw new ArgumentException(
|
throw new ArgumentException(
|
||||||
$"The specified key is not available. Requested key: [{key}] Available keys: [{string.Join(", ", data.Keys)}]",
|
$"The specified key is not available. Requested key: [{key}] Available keys: [{string.Join(", ", data.Keys)}]",
|
||||||
nameof(key));
|
nameof(key));
|
||||||
}
|
}
|
||||||
|
|
||||||
var rawValue = data[key];
|
|
||||||
|
|
||||||
return rawValue;
|
return rawValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ public static class GuidUtils
|
|||||||
return Guid.Empty;
|
return Guid.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (value.Contains("-"))
|
if (value.Contains('-'))
|
||||||
{
|
{
|
||||||
// This function was called on an already valid guid
|
// This function was called on an already valid guid
|
||||||
return new Guid(value);
|
return new Guid(value);
|
||||||
@@ -92,7 +92,7 @@ public static class GuidUtils
|
|||||||
var sb = new System.Text.StringBuilder(newguid);
|
var sb = new System.Text.StringBuilder(newguid);
|
||||||
while (sb.Length < Guid.Empty.ToString().Length)
|
while (sb.Length < Guid.Empty.ToString().Length)
|
||||||
{
|
{
|
||||||
sb.Append(sb.ToString());
|
sb.Append(sb);
|
||||||
}
|
}
|
||||||
newguid = sb.ToString();
|
newguid = sb.ToString();
|
||||||
|
|
||||||
@@ -114,7 +114,7 @@ public static class GuidUtils
|
|||||||
for (int i = 0; i < guidChars.Length; i++)
|
for (int i = 0; i < guidChars.Length; i++)
|
||||||
{
|
{
|
||||||
char chr = guidChars[i];
|
char chr = guidChars[i];
|
||||||
if (valid.IndexOf(chr) == -1 && !((chr == '-') && (i == 8 || i == 13 || i == 18 || i == 23)))
|
if (!valid.Contains(chr) && !((chr == '-') && (i == 8 || i == 13 || i == 18 || i == 23)))
|
||||||
{
|
{
|
||||||
guidChars[i] = valid[Math.Abs(StringUtils.GetHashCode32Bit(chr)) % valid.Length];
|
guidChars[i] = valid[Math.Abs(StringUtils.GetHashCode32Bit(chr)) % valid.Length];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ public static partial class SqlUtils
|
|||||||
public static void AppendAliasColumnWithComma(StringBuilder stringBuilder, string alias, string columnName)
|
public static void AppendAliasColumnWithComma(StringBuilder stringBuilder, string alias, string columnName)
|
||||||
{
|
{
|
||||||
stringBuilder.Append(alias);
|
stringBuilder.Append(alias);
|
||||||
stringBuilder.Append(".");
|
stringBuilder.Append('.');
|
||||||
stringBuilder.Append(columnName);
|
stringBuilder.Append(columnName);
|
||||||
stringBuilder.AppendLine(",");
|
stringBuilder.AppendLine(",");
|
||||||
}
|
}
|
||||||
@@ -58,7 +58,7 @@ public static partial class SqlUtils
|
|||||||
{
|
{
|
||||||
stringBuilder.Append("CAST(");
|
stringBuilder.Append("CAST(");
|
||||||
stringBuilder.Append(alias);
|
stringBuilder.Append(alias);
|
||||||
stringBuilder.Append(".");
|
stringBuilder.Append('.');
|
||||||
stringBuilder.Append(columnName);
|
stringBuilder.Append(columnName);
|
||||||
stringBuilder.Append(" AS ");
|
stringBuilder.Append(" AS ");
|
||||||
stringBuilder.Append(dataType);
|
stringBuilder.Append(dataType);
|
||||||
@@ -94,7 +94,7 @@ public static partial class SqlUtils
|
|||||||
{
|
{
|
||||||
if (i > 0)
|
if (i > 0)
|
||||||
{
|
{
|
||||||
sb.Append(",");
|
sb.Append(',');
|
||||||
}
|
}
|
||||||
|
|
||||||
string trimmed = columnNames[i].Trim();
|
string trimmed = columnNames[i].Trim();
|
||||||
|
|||||||
@@ -42,10 +42,10 @@ public static partial class SqlUtils
|
|||||||
|
|
||||||
foreach (Guid g in guidList)
|
foreach (Guid g in guidList)
|
||||||
{
|
{
|
||||||
sb.Append("'");
|
sb.Append('\'');
|
||||||
sb.Append(g.ToString());
|
sb.Append(g.ToString());
|
||||||
sb.Append("'");
|
sb.Append('\'');
|
||||||
sb.Append(",");
|
sb.Append(',');
|
||||||
}
|
}
|
||||||
|
|
||||||
return sb.ToString().Trim().TrimEnd(',');
|
return sb.ToString().Trim().TrimEnd(',');
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ public static partial class SqlUtils
|
|||||||
double.TryParse(value, out double dblValue);
|
double.TryParse(value, out double dblValue);
|
||||||
|
|
||||||
// Optimize IN if only one value
|
// Optimize IN if only one value
|
||||||
if (operation == FilterOperation.In && !value.Contains(","))
|
if (operation == FilterOperation.In && !value.Contains(','))
|
||||||
{
|
{
|
||||||
operation = FilterOperation.Equal;
|
operation = FilterOperation.Equal;
|
||||||
}
|
}
|
||||||
@@ -144,7 +144,7 @@ public static partial class SqlUtils
|
|||||||
}
|
}
|
||||||
|
|
||||||
int lastCloseParen = sb.ToString().LastIndexOf(')');
|
int lastCloseParen = sb.ToString().LastIndexOf(')');
|
||||||
filter.SqlExpression = sb.ToString().Substring(0, lastCloseParen + 1) + ")";
|
filter.SqlExpression = string.Concat(sb.ToString().AsSpan(0, lastCloseParen + 1), ")");
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case FilterOperation.NotBetween:
|
case FilterOperation.NotBetween:
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ public static partial class SqlUtils
|
|||||||
public const string DATETIME_INSERT_FORMAT = "yyyyMMdd HH:mm:ss";
|
public const string DATETIME_INSERT_FORMAT = "yyyyMMdd HH:mm:ss";
|
||||||
|
|
||||||
private const string DEFAULT_SCHEMA = "dbo";
|
private const string DEFAULT_SCHEMA = "dbo";
|
||||||
|
private static readonly char[] separator = new[] { ',' };
|
||||||
|
|
||||||
#region SQL String Manipulation
|
#region SQL String Manipulation
|
||||||
|
|
||||||
@@ -34,7 +35,7 @@ public static partial class SqlUtils
|
|||||||
public static string StripColumnTableAlias(string sql)
|
public static string StripColumnTableAlias(string sql)
|
||||||
{
|
{
|
||||||
// Break sql into words and remove "abc." from each column
|
// Break sql into words and remove "abc." from each column
|
||||||
string[] commaWords = sql.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
|
string[] commaWords = sql.Split(separator, StringSplitOptions.RemoveEmptyEntries);
|
||||||
var newParts = new List<string>();
|
var newParts = new List<string>();
|
||||||
|
|
||||||
foreach (string commaWord in commaWords)
|
foreach (string commaWord in commaWords)
|
||||||
|
|||||||
@@ -578,7 +578,7 @@ public class QueryBreakdown : SqlBreakdownBase, IQueryBreakdown
|
|||||||
|
|
||||||
if (i > 0)
|
if (i > 0)
|
||||||
{
|
{
|
||||||
sb.Append(",");
|
sb.Append(',');
|
||||||
sb.AppendLine();
|
sb.AppendLine();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,10 +51,7 @@ public abstract class QueryBreakdownCollectionBase<TQuery> : SqlBreakdownCollect
|
|||||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="queryBreakdown"/> is null.</exception>
|
/// <exception cref="ArgumentNullException">Thrown when <paramref name="queryBreakdown"/> is null.</exception>
|
||||||
public void Add(TQuery queryBreakdown)
|
public void Add(TQuery queryBreakdown)
|
||||||
{
|
{
|
||||||
if (queryBreakdown == null)
|
ArgumentNullException.ThrowIfNull(queryBreakdown);
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(queryBreakdown));
|
|
||||||
}
|
|
||||||
|
|
||||||
QueryBreakdownList.Add(queryBreakdown);
|
QueryBreakdownList.Add(queryBreakdown);
|
||||||
base.Add(queryBreakdown);
|
base.Add(queryBreakdown);
|
||||||
@@ -67,10 +64,7 @@ public abstract class QueryBreakdownCollectionBase<TQuery> : SqlBreakdownCollect
|
|||||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="queryBreakdowns"/> is null.</exception>
|
/// <exception cref="ArgumentNullException">Thrown when <paramref name="queryBreakdowns"/> is null.</exception>
|
||||||
public void AddRange(IEnumerable<TQuery> queryBreakdowns)
|
public void AddRange(IEnumerable<TQuery> queryBreakdowns)
|
||||||
{
|
{
|
||||||
if (queryBreakdowns == null)
|
ArgumentNullException.ThrowIfNull(queryBreakdowns);
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(queryBreakdowns));
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var breakdown in queryBreakdowns)
|
foreach (var breakdown in queryBreakdowns)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ public class StatementParser
|
|||||||
public const string KeywordGroupBy = "GROUP BY";
|
public const string KeywordGroupBy = "GROUP BY";
|
||||||
public const string KeywordHaving = "HAVING";
|
public const string KeywordHaving = "HAVING";
|
||||||
public const string KeywordOrderBy = "ORDER BY";
|
public const string KeywordOrderBy = "ORDER BY";
|
||||||
|
private static readonly char[] separator = new[] { '\r', '\n' };
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
@@ -50,7 +51,7 @@ public class StatementParser
|
|||||||
// Replace multiple spaces/tabs with single space, but preserve newlines for comment handling
|
// Replace multiple spaces/tabs with single space, but preserve newlines for comment handling
|
||||||
sql = Regex.Replace(sql, @"[ \t]+", " ", RegexOptions.None, RegexDefaults.MatchTimeout);
|
sql = Regex.Replace(sql, @"[ \t]+", " ", RegexOptions.None, RegexDefaults.MatchTimeout);
|
||||||
// Remove leading/trailing whitespace from each line
|
// Remove leading/trailing whitespace from each line
|
||||||
var lines = sql.Split(new[] { '\r', '\n' }, StringSplitOptions.None);
|
var lines = sql.Split(separator, StringSplitOptions.None);
|
||||||
sql = string.Join("\n", lines.Select(line => line.Trim()));
|
sql = string.Join("\n", lines.Select(line => line.Trim()));
|
||||||
return sql.Trim();
|
return sql.Trim();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ public class QueryBreakdownRepositoryTests
|
|||||||
{
|
{
|
||||||
private DbContext _dbContext = null!;
|
private DbContext _dbContext = null!;
|
||||||
private IQueryBreakdownMapper _mapper = null!;
|
private IQueryBreakdownMapper _mapper = null!;
|
||||||
private IQueryBreakdownRepository _repository = null!;
|
private QueryBreakdownRepository _repository = null!;
|
||||||
|
|
||||||
[SetUp]
|
[SetUp]
|
||||||
public void Setup()
|
public void Setup()
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ namespace Strata.SqlTools.LinqToSql.Tests;
|
|||||||
public class LinqQueryBreakdownTests
|
public class LinqQueryBreakdownTests
|
||||||
{
|
{
|
||||||
private TestDataContext _context = null!;
|
private TestDataContext _context = null!;
|
||||||
|
private static readonly string[] separator = new[] { "AND" };
|
||||||
|
|
||||||
[SetUp]
|
[SetUp]
|
||||||
public void Setup()
|
public void Setup()
|
||||||
@@ -18,7 +19,7 @@ public class LinqQueryBreakdownTests
|
|||||||
public void Analyze_SimpleSelectQuery_ExtractsCorrectClauses()
|
public void Analyze_SimpleSelectQuery_ExtractsCorrectClauses()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var query = from user in _context.Users
|
var query = from user in TestDataContext.Users
|
||||||
select user;
|
select user;
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
@@ -34,7 +35,7 @@ public class LinqQueryBreakdownTests
|
|||||||
public void Analyze_WhereClause_ExtractsCondition()
|
public void Analyze_WhereClause_ExtractsCondition()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var query = from user in _context.Users
|
var query = from user in TestDataContext.Users
|
||||||
where user.Age > 21
|
where user.Age > 21
|
||||||
select user;
|
select user;
|
||||||
|
|
||||||
@@ -51,7 +52,7 @@ public class LinqQueryBreakdownTests
|
|||||||
public void Analyze_SelectWithProjection_ExtractsSelectedFields()
|
public void Analyze_SelectWithProjection_ExtractsSelectedFields()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var query = from user in _context.Users
|
var query = from user in TestDataContext.Users
|
||||||
select new { user.Id, user.Name };
|
select new { user.Id, user.Name };
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
@@ -67,7 +68,7 @@ public class LinqQueryBreakdownTests
|
|||||||
public void Analyze_OrderByClause_ExtractsOrdering()
|
public void Analyze_OrderByClause_ExtractsOrdering()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var query = from user in _context.Users
|
var query = from user in TestDataContext.Users
|
||||||
orderby user.Name
|
orderby user.Name
|
||||||
select user;
|
select user;
|
||||||
|
|
||||||
@@ -83,7 +84,7 @@ public class LinqQueryBreakdownTests
|
|||||||
public void Analyze_MethodSyntax_ExtractsCorrectClauses()
|
public void Analyze_MethodSyntax_ExtractsCorrectClauses()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var query = _context.Users
|
var query = TestDataContext.Users
|
||||||
.Where(u => u.Age > 18)
|
.Where(u => u.Age > 18)
|
||||||
.OrderBy(u => u.Name)
|
.OrderBy(u => u.Name)
|
||||||
.Select(u => new { u.Id, u.Name });
|
.Select(u => new { u.Id, u.Name });
|
||||||
@@ -101,7 +102,7 @@ public class LinqQueryBreakdownTests
|
|||||||
public void Analyze_MethodSyntax_TracksMethodChain()
|
public void Analyze_MethodSyntax_TracksMethodChain()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var query = _context.Users
|
var query = TestDataContext.Users
|
||||||
.Where(u => u.Age > 18)
|
.Where(u => u.Age > 18)
|
||||||
.OrderBy(u => u.Name)
|
.OrderBy(u => u.Name)
|
||||||
.Select(u => u.Name);
|
.Select(u => u.Name);
|
||||||
@@ -120,7 +121,7 @@ public class LinqQueryBreakdownTests
|
|||||||
public void GetQuerySummary_ReturnsFormattedString()
|
public void GetQuerySummary_ReturnsFormattedString()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var query = _context.Users.Where(u => u.Age > 21);
|
var query = TestDataContext.Users.Where(u => u.Age > 21);
|
||||||
var breakdown = LinqQueryBreakdown.Analyze(query);
|
var breakdown = LinqQueryBreakdown.Analyze(query);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
@@ -137,7 +138,7 @@ public class LinqQueryBreakdownTests
|
|||||||
public void GetMethodChain_ReturnsMethodSequence()
|
public void GetMethodChain_ReturnsMethodSequence()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var query = _context.Users.Where(u => u.Age > 18).OrderBy(u => u.Name);
|
var query = TestDataContext.Users.Where(u => u.Age > 18).OrderBy(u => u.Name);
|
||||||
var breakdown = LinqQueryBreakdown.Analyze(query);
|
var breakdown = LinqQueryBreakdown.Analyze(query);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
@@ -153,7 +154,7 @@ public class LinqQueryBreakdownTests
|
|||||||
public void TryAnalyze_ValidQuery_ReturnsTrue()
|
public void TryAnalyze_ValidQuery_ReturnsTrue()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var query = _context.Users.Where(u => u.Age > 21);
|
var query = TestDataContext.Users.Where(u => u.Age > 21);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var success = LinqQueryBreakdown.TryAnalyze(query, out var breakdown, out var error);
|
var success = LinqQueryBreakdown.TryAnalyze(query, out var breakdown, out var error);
|
||||||
@@ -182,7 +183,7 @@ public class LinqQueryBreakdownTests
|
|||||||
public void Analyze_ComplexQuery_HandlesCombinedClauses()
|
public void Analyze_ComplexQuery_HandlesCombinedClauses()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var query = from user in _context.Users
|
var query = from user in TestDataContext.Users
|
||||||
where user.Age > 21 && user.IsActive
|
where user.Age > 21 && user.IsActive
|
||||||
orderby user.Name descending
|
orderby user.Name descending
|
||||||
select new { user.Id, user.Name, user.Email };
|
select new { user.Id, user.Name, user.Email };
|
||||||
@@ -205,7 +206,7 @@ public class LinqQueryBreakdownTests
|
|||||||
public void AnalyzeAndModify_AddWhereClause_GeneratesUpdatedSql()
|
public void AnalyzeAndModify_AddWhereClause_GeneratesUpdatedSql()
|
||||||
{
|
{
|
||||||
// Arrange - Analyze an existing LINQ query
|
// Arrange - Analyze an existing LINQ query
|
||||||
var originalQuery = _context.Users.Where(u => u.Age > 21);
|
var originalQuery = TestDataContext.Users.Where(u => u.Age > 21);
|
||||||
var breakdown = LinqQueryBreakdown.Analyze(originalQuery);
|
var breakdown = LinqQueryBreakdown.Analyze(originalQuery);
|
||||||
|
|
||||||
// Act - Add additional filter using breakdown
|
// Act - Add additional filter using breakdown
|
||||||
@@ -222,7 +223,7 @@ public class LinqQueryBreakdownTests
|
|||||||
public void AnalyzeAndModify_ChangeSelectClause_GeneratesNewProjection()
|
public void AnalyzeAndModify_ChangeSelectClause_GeneratesNewProjection()
|
||||||
{
|
{
|
||||||
// Arrange - Analyze query with projection
|
// Arrange - Analyze query with projection
|
||||||
var originalQuery = _context.Users.Select(u => new { u.Id, u.Name });
|
var originalQuery = TestDataContext.Users.Select(u => new { u.Id, u.Name });
|
||||||
var breakdown = LinqQueryBreakdown.Analyze(originalQuery);
|
var breakdown = LinqQueryBreakdown.Analyze(originalQuery);
|
||||||
|
|
||||||
// Act - Modify the SELECT clause
|
// Act - Modify the SELECT clause
|
||||||
@@ -243,7 +244,7 @@ public class LinqQueryBreakdownTests
|
|||||||
public void AnalyzeAndModify_CloneAndExtend_CreatesIndependentQuery()
|
public void AnalyzeAndModify_CloneAndExtend_CreatesIndependentQuery()
|
||||||
{
|
{
|
||||||
// Arrange - Analyze base query
|
// Arrange - Analyze base query
|
||||||
var baseQuery = _context.Users.Where(u => u.Age > 18);
|
var baseQuery = TestDataContext.Users.Where(u => u.Age > 18);
|
||||||
var baseBreakdown = LinqQueryBreakdown.Analyze(baseQuery);
|
var baseBreakdown = LinqQueryBreakdown.Analyze(baseQuery);
|
||||||
|
|
||||||
// Act - Clone and extend
|
// Act - Clone and extend
|
||||||
@@ -267,10 +268,10 @@ public class LinqQueryBreakdownTests
|
|||||||
public void AnalyzeAndCompose_MultipleQueries_CreatesUnionScenario()
|
public void AnalyzeAndCompose_MultipleQueries_CreatesUnionScenario()
|
||||||
{
|
{
|
||||||
// Arrange - Analyze two different queries
|
// Arrange - Analyze two different queries
|
||||||
var activeUsersQuery = _context.Users.Where(u => u.IsActive);
|
var activeUsersQuery = TestDataContext.Users.Where(u => u.IsActive);
|
||||||
var activeBreakdown = LinqQueryBreakdown.Analyze(activeUsersQuery);
|
var activeBreakdown = LinqQueryBreakdown.Analyze(activeUsersQuery);
|
||||||
|
|
||||||
var recentUsersQuery = _context.Users.Where(u => u.Age < 25);
|
var recentUsersQuery = TestDataContext.Users.Where(u => u.Age < 25);
|
||||||
var recentBreakdown = LinqQueryBreakdown.Analyze(recentUsersQuery);
|
var recentBreakdown = LinqQueryBreakdown.Analyze(recentUsersQuery);
|
||||||
|
|
||||||
// Act - Get SQL for both (could be used in UNION scenario)
|
// Act - Get SQL for both (could be used in UNION scenario)
|
||||||
@@ -287,7 +288,7 @@ public class LinqQueryBreakdownTests
|
|||||||
public void AnalyzeAndBuildFilter_IncrementallyAddConditions_BuildsComplexFilter()
|
public void AnalyzeAndBuildFilter_IncrementallyAddConditions_BuildsComplexFilter()
|
||||||
{
|
{
|
||||||
// Arrange - Start with simple query
|
// Arrange - Start with simple query
|
||||||
var query = _context.Users;
|
var query = TestDataContext.Users;
|
||||||
var breakdown = LinqQueryBreakdown.Analyze(query);
|
var breakdown = LinqQueryBreakdown.Analyze(query);
|
||||||
|
|
||||||
// Act - Incrementally add filter conditions (simulating filter builder UI)
|
// Act - Incrementally add filter conditions (simulating filter builder UI)
|
||||||
@@ -303,7 +304,7 @@ public class LinqQueryBreakdownTests
|
|||||||
Assert.That(filterSql, Does.Contain("IsActive = 1"));
|
Assert.That(filterSql, Does.Contain("IsActive = 1"));
|
||||||
|
|
||||||
// Should have multiple AND conditions
|
// Should have multiple AND conditions
|
||||||
var andCount = filterSql.Split(new[] { "AND" }, StringSplitOptions.None).Length - 1;
|
var andCount = filterSql.Split(separator, StringSplitOptions.None).Length - 1;
|
||||||
Assert.That(andCount, Is.GreaterThanOrEqualTo(2));
|
Assert.That(andCount, Is.GreaterThanOrEqualTo(2));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -311,7 +312,7 @@ public class LinqQueryBreakdownTests
|
|||||||
public void AnalyzeAndPaginate_AddOrderAndLimits_CreatesPaginatedQuery()
|
public void AnalyzeAndPaginate_AddOrderAndLimits_CreatesPaginatedQuery()
|
||||||
{
|
{
|
||||||
// Arrange - Analyze base query
|
// Arrange - Analyze base query
|
||||||
var query = _context.Users.Where(u => u.IsActive);
|
var query = TestDataContext.Users.Where(u => u.IsActive);
|
||||||
var breakdown = LinqQueryBreakdown.Analyze(query);
|
var breakdown = LinqQueryBreakdown.Analyze(query);
|
||||||
|
|
||||||
// Act - Add pagination (ORDER BY required for consistent pagination)
|
// Act - Add pagination (ORDER BY required for consistent pagination)
|
||||||
@@ -335,7 +336,7 @@ public class LinqQueryBreakdownTests
|
|||||||
public void AnalyzeAndGenerateReport_ExtractQueryMetrics_ProvidesAnalytics()
|
public void AnalyzeAndGenerateReport_ExtractQueryMetrics_ProvidesAnalytics()
|
||||||
{
|
{
|
||||||
// Arrange - Complex query to analyze
|
// Arrange - Complex query to analyze
|
||||||
var query = _context.Users
|
var query = TestDataContext.Users
|
||||||
.Where(u => u.Age > 21)
|
.Where(u => u.Age > 21)
|
||||||
.Where(u => u.IsActive)
|
.Where(u => u.IsActive)
|
||||||
.OrderBy(u => u.Name)
|
.OrderBy(u => u.Name)
|
||||||
@@ -364,7 +365,7 @@ public class LinqQueryBreakdownTests
|
|||||||
public void AnalyzeAndOptimize_RemoveSelectStar_ImprovedProjection()
|
public void AnalyzeAndOptimize_RemoveSelectStar_ImprovedProjection()
|
||||||
{
|
{
|
||||||
// Arrange - Analyze query with SELECT *
|
// Arrange - Analyze query with SELECT *
|
||||||
var query = _context.Users.Where(u => u.IsActive);
|
var query = TestDataContext.Users.Where(u => u.IsActive);
|
||||||
var breakdown = LinqQueryBreakdown.Analyze(query);
|
var breakdown = LinqQueryBreakdown.Analyze(query);
|
||||||
|
|
||||||
// Verify it initially has SELECT *
|
// Verify it initially has SELECT *
|
||||||
@@ -390,10 +391,10 @@ public class LinqQueryBreakdownTests
|
|||||||
public void AnalyzeMultipleQueries_CompareAndMerge_CreatesCompositeQuery()
|
public void AnalyzeMultipleQueries_CompareAndMerge_CreatesCompositeQuery()
|
||||||
{
|
{
|
||||||
// Arrange - Analyze two related queries
|
// Arrange - Analyze two related queries
|
||||||
var usersQuery = _context.Users.Where(u => u.Age > 21);
|
var usersQuery = TestDataContext.Users.Where(u => u.Age > 21);
|
||||||
var usersBreakdown = LinqQueryBreakdown.Analyze(usersQuery);
|
var usersBreakdown = LinqQueryBreakdown.Analyze(usersQuery);
|
||||||
|
|
||||||
var activeQuery = _context.Users.Where(u => u.IsActive);
|
var activeQuery = TestDataContext.Users.Where(u => u.IsActive);
|
||||||
var activeBreakdown = LinqQueryBreakdown.Analyze(activeQuery);
|
var activeBreakdown = LinqQueryBreakdown.Analyze(activeQuery);
|
||||||
|
|
||||||
// Act - Merge conditions from both queries
|
// Act - Merge conditions from both queries
|
||||||
@@ -416,7 +417,7 @@ public class LinqQueryBreakdownTests
|
|||||||
public void AnalyzeAndDocument_GenerateQueryDocumentation_CreatesReadableOutput()
|
public void AnalyzeAndDocument_GenerateQueryDocumentation_CreatesReadableOutput()
|
||||||
{
|
{
|
||||||
// Arrange - Analyze a business query
|
// Arrange - Analyze a business query
|
||||||
var query = _context.Orders
|
var query = TestDataContext.Orders
|
||||||
.Where(o => o.Amount > 1000)
|
.Where(o => o.Amount > 1000)
|
||||||
.Where(o => o.OrderDate > DateTime.Now.AddDays(-30))
|
.Where(o => o.OrderDate > DateTime.Now.AddDays(-30))
|
||||||
.OrderBy(o => o.OrderDate);
|
.OrderBy(o => o.OrderDate);
|
||||||
@@ -451,8 +452,8 @@ public class LinqQueryBreakdownTests
|
|||||||
// Test data context and entities
|
// Test data context and entities
|
||||||
public class TestDataContext
|
public class TestDataContext
|
||||||
{
|
{
|
||||||
public IQueryable<User> Users => new List<User>().AsQueryable();
|
public static IQueryable<User> Users => new List<User>().AsQueryable();
|
||||||
public IQueryable<Order> Orders => new List<Order>().AsQueryable();
|
public static IQueryable<Order> Orders => new List<Order>().AsQueryable();
|
||||||
}
|
}
|
||||||
|
|
||||||
public class User
|
public class User
|
||||||
@@ -488,7 +489,7 @@ public class GetQueryTests
|
|||||||
public void GetQuery_LinqBreakdown_ReturnsNullByDefault()
|
public void GetQuery_LinqBreakdown_ReturnsNullByDefault()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var query = _context.Users.Where(u => u.Age > 18);
|
var query = TestDataContext.Users.Where(u => u.Age > 18);
|
||||||
var breakdown = LinqQueryBreakdown.Analyze(query);
|
var breakdown = LinqQueryBreakdown.Analyze(query);
|
||||||
|
|
||||||
// Act - GetQuery returns null because LinqQueryBreakdown needs the original provider
|
// Act - GetQuery returns null because LinqQueryBreakdown needs the original provider
|
||||||
@@ -515,7 +516,7 @@ public class GetQueryTests
|
|||||||
public void GetQuery_MultipleBreakdownTypes_AllReturnNull()
|
public void GetQuery_MultipleBreakdownTypes_AllReturnNull()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var linqBreakdown = LinqQueryBreakdown.Analyze(_context.Users);
|
var linqBreakdown = LinqQueryBreakdown.Analyze(TestDataContext.Users);
|
||||||
var sqlBreakdown = new QueryBreakdown("*", "Users");
|
var sqlBreakdown = new QueryBreakdown("*", "Users");
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
@@ -710,7 +711,7 @@ public class GetQueryTests
|
|||||||
public void AnalyzeTrace_WithValidQuery_ReturnsTraceString()
|
public void AnalyzeTrace_WithValidQuery_ReturnsTraceString()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var query = _context.Users.Where(u => u.Age > 18);
|
var query = TestDataContext.Users.Where(u => u.Age > 18);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var trace = LinqQueryBreakdown.AnalyzeTrace(query);
|
var trace = LinqQueryBreakdown.AnalyzeTrace(query);
|
||||||
@@ -726,7 +727,7 @@ public class GetQueryTests
|
|||||||
public void AnalyzeTrace_WithExecutionContext_IncludesContextInTrace()
|
public void AnalyzeTrace_WithExecutionContext_IncludesContextInTrace()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var query = _context.Users;
|
var query = TestDataContext.Users;
|
||||||
const string context = "Initial Load";
|
const string context = "Initial Load";
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
@@ -754,7 +755,7 @@ public class GetQueryTests
|
|||||||
public void ConvertToSqlServerBreakdown_CopiesAllClauses()
|
public void ConvertToSqlServerBreakdown_CopiesAllClauses()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var query = _context.Users.Where(u => u.Age > 21).OrderBy(u => u.Name);
|
var query = TestDataContext.Users.Where(u => u.Age > 21).OrderBy(u => u.Name);
|
||||||
var breakdown = LinqQueryBreakdown.Analyze(query);
|
var breakdown = LinqQueryBreakdown.Analyze(query);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
@@ -772,7 +773,7 @@ public class GetQueryTests
|
|||||||
public void ConvertToPostgreSqlBreakdown_CopiesAllClauses()
|
public void ConvertToPostgreSqlBreakdown_CopiesAllClauses()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var query = _context.Users
|
var query = TestDataContext.Users
|
||||||
.Where(u => u.IsActive)
|
.Where(u => u.IsActive)
|
||||||
.OrderBy(u => u.Name)
|
.OrderBy(u => u.Name)
|
||||||
.Select(u => new { u.Id, u.Name });
|
.Select(u => new { u.Id, u.Name });
|
||||||
@@ -793,7 +794,7 @@ public class GetQueryTests
|
|||||||
public void ConvertToSnowflakeBreakdown_CopiesAllClauses()
|
public void ConvertToSnowflakeBreakdown_CopiesAllClauses()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var query = _context.Users.Where(u => u.Age > 21).OrderBy(u => u.Name);
|
var query = TestDataContext.Users.Where(u => u.Age > 21).OrderBy(u => u.Name);
|
||||||
var breakdown = LinqQueryBreakdown.Analyze(query);
|
var breakdown = LinqQueryBreakdown.Analyze(query);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
@@ -827,7 +828,7 @@ public class GetQueryTests
|
|||||||
public void ConvertToPostgreSqlBreakdown_WithComplexQuery_CopieAllClauses()
|
public void ConvertToPostgreSqlBreakdown_WithComplexQuery_CopieAllClauses()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var query = _context.Users
|
var query = TestDataContext.Users
|
||||||
.Where(u => u.IsActive && u.Age >= 18)
|
.Where(u => u.IsActive && u.Age >= 18)
|
||||||
.OrderByDescending(u => u.Age);
|
.OrderByDescending(u => u.Age);
|
||||||
|
|
||||||
@@ -848,7 +849,7 @@ public class GetQueryTests
|
|||||||
public void ConvertToSnowflakeBreakdown_PreservesAllClauseInformation()
|
public void ConvertToSnowflakeBreakdown_PreservesAllClauseInformation()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var breakdown = LinqQueryBreakdown.Analyze(_context.Users.OrderBy(u => u.Name));
|
var breakdown = LinqQueryBreakdown.Analyze(TestDataContext.Users.OrderBy(u => u.Name));
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var snowflakeBreakdown = breakdown.ConvertToSnowflakeBreakdown();
|
var snowflakeBreakdown = breakdown.ConvertToSnowflakeBreakdown();
|
||||||
@@ -862,7 +863,7 @@ public class GetQueryTests
|
|||||||
public void ConvertToSqlServerBreakdown_InstancesAreIndependent()
|
public void ConvertToSqlServerBreakdown_InstancesAreIndependent()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var breakdown = LinqQueryBreakdown.Analyze(_context.Users.Where(u => u.Age > 21));
|
var breakdown = LinqQueryBreakdown.Analyze(TestDataContext.Users.Where(u => u.Age > 21));
|
||||||
var sqlServerBreakdown = breakdown.ConvertToSqlServerBreakdown();
|
var sqlServerBreakdown = breakdown.ConvertToSqlServerBreakdown();
|
||||||
|
|
||||||
// Act - Modify the SQL Server breakdown
|
// Act - Modify the SQL Server breakdown
|
||||||
@@ -876,7 +877,7 @@ public class GetQueryTests
|
|||||||
public void ConvertChainMultipleTimes_EachConversionIndependent()
|
public void ConvertChainMultipleTimes_EachConversionIndependent()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var breakdown = LinqQueryBreakdown.Analyze(_context.Users.OrderBy(u => u.Name));
|
var breakdown = LinqQueryBreakdown.Analyze(TestDataContext.Users.OrderBy(u => u.Name));
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var sqlServer1 = breakdown.ConvertToSqlServerBreakdown();
|
var sqlServer1 = breakdown.ConvertToSqlServerBreakdown();
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ public class QueryBreakdownGeneratorTests
|
|||||||
public void GenerateMermaidDiagram_SimpleLinqQuery_GeneratesValidMermaid()
|
public void GenerateMermaidDiagram_SimpleLinqQuery_GeneratesValidMermaid()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var query = _context.Users.Where(u => u.Age > 21);
|
var query = TestDataContext.Users.Where(u => u.Age > 21);
|
||||||
var breakdown = LinqQueryBreakdown.Analyze(query);
|
var breakdown = LinqQueryBreakdown.Analyze(query);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
@@ -39,14 +39,14 @@ public class QueryBreakdownGeneratorTests
|
|||||||
public void GenerateMethodChainDiagram_WithMethodCalls_ShowsChain()
|
public void GenerateMethodChainDiagram_WithMethodCalls_ShowsChain()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var query = _context.Users
|
var query = TestDataContext.Users
|
||||||
.Where(u => u.Age > 18)
|
.Where(u => u.Age > 18)
|
||||||
.OrderBy(u => u.Name)
|
.OrderBy(u => u.Name)
|
||||||
.Select(u => new { u.Id, u.Name });
|
.Select(u => new { u.Id, u.Name });
|
||||||
var breakdown = LinqQueryBreakdown.Analyze(query);
|
var breakdown = LinqQueryBreakdown.Analyze(query);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = _generator.GenerateMethodChainDiagram(breakdown, "Method Chain");
|
var result = QueryBreakdownGenerator.GenerateMethodChainDiagram(breakdown, "Method Chain");
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.That(result, Does.Contain("```mermaid"));
|
Assert.That(result, Does.Contain("```mermaid"));
|
||||||
@@ -66,7 +66,7 @@ public class QueryBreakdownGeneratorTests
|
|||||||
var breakdown = new LinqQueryBreakdown("*", "Users");
|
var breakdown = new LinqQueryBreakdown("*", "Users");
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = _generator.GenerateMethodChainDiagram(breakdown);
|
var result = QueryBreakdownGenerator.GenerateMethodChainDiagram(breakdown);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.That(result, Does.Contain("IQueryable"));
|
Assert.That(result, Does.Contain("IQueryable"));
|
||||||
@@ -77,7 +77,7 @@ public class QueryBreakdownGeneratorTests
|
|||||||
public void GenerateCombinedDiagram_IncludesBothDiagrams()
|
public void GenerateCombinedDiagram_IncludesBothDiagrams()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var query = _context.Users.Where(u => u.Age > 21).OrderBy(u => u.Name);
|
var query = TestDataContext.Users.Where(u => u.Age > 21).OrderBy(u => u.Name);
|
||||||
var breakdown = LinqQueryBreakdown.Analyze(query);
|
var breakdown = LinqQueryBreakdown.Analyze(query);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
@@ -95,7 +95,7 @@ public class QueryBreakdownGeneratorTests
|
|||||||
public void GenerateMermaidDiagram_WithProjection_ShowsSelectedFields()
|
public void GenerateMermaidDiagram_WithProjection_ShowsSelectedFields()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var query = _context.Users.Select(u => new { u.Id, u.Name, u.Email });
|
var query = TestDataContext.Users.Select(u => new { u.Id, u.Name, u.Email });
|
||||||
var breakdown = LinqQueryBreakdown.Analyze(query);
|
var breakdown = LinqQueryBreakdown.Analyze(query);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
@@ -110,7 +110,7 @@ public class QueryBreakdownGeneratorTests
|
|||||||
public void GenerateMermaidDiagram_NullTitle_GeneratesWithoutTitle()
|
public void GenerateMermaidDiagram_NullTitle_GeneratesWithoutTitle()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var query = _context.Users;
|
var query = TestDataContext.Users;
|
||||||
var breakdown = LinqQueryBreakdown.Analyze(query);
|
var breakdown = LinqQueryBreakdown.Analyze(query);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
@@ -125,7 +125,7 @@ public class QueryBreakdownGeneratorTests
|
|||||||
// Test data context
|
// Test data context
|
||||||
public class TestDataContext
|
public class TestDataContext
|
||||||
{
|
{
|
||||||
public IQueryable<User> Users => new List<User>().AsQueryable();
|
public static IQueryable<User> Users => new List<User>().AsQueryable();
|
||||||
}
|
}
|
||||||
|
|
||||||
public class User
|
public class User
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ public class SqlStatementGeneratorTests
|
|||||||
var query = new LinqQueryBreakdown("*", "Users");
|
var query = new LinqQueryBreakdown("*", "Users");
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = _generator.GenerateLinqPipelineDiagram(query, "User Query Pipeline");
|
var result = SqlStatementGenerator.GenerateLinqPipelineDiagram(query, "User Query Pipeline");
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.That(result, Does.Contain("```mermaid"));
|
Assert.That(result, Does.Contain("```mermaid"));
|
||||||
@@ -42,7 +42,7 @@ public class SqlStatementGeneratorTests
|
|||||||
query.AddWhereClause("Age > 21");
|
query.AddWhereClause("Age > 21");
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = _generator.GenerateLinqPipelineDiagram(query);
|
var result = SqlStatementGenerator.GenerateLinqPipelineDiagram(query);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.That(result, Does.Contain("Where Predicate"));
|
Assert.That(result, Does.Contain("Where Predicate"));
|
||||||
@@ -57,7 +57,7 @@ public class SqlStatementGeneratorTests
|
|||||||
var query = new LinqQueryBreakdown("Id, Name, Email", "Users");
|
var query = new LinqQueryBreakdown("Id, Name, Email", "Users");
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = _generator.GenerateLinqPipelineDiagram(query);
|
var result = SqlStatementGenerator.GenerateLinqPipelineDiagram(query);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.That(result, Does.Contain("Select Projection"));
|
Assert.That(result, Does.Contain("Select Projection"));
|
||||||
@@ -72,7 +72,7 @@ public class SqlStatementGeneratorTests
|
|||||||
var query = new LinqQueryBreakdown("*", "Users");
|
var query = new LinqQueryBreakdown("*", "Users");
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = _generator.GenerateLinqPipelineDiagram(query, null);
|
var result = SqlStatementGenerator.GenerateLinqPipelineDiagram(query, null);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.That(result, Does.Not.Contain("###"));
|
Assert.That(result, Does.Not.Contain("###"));
|
||||||
|
|||||||
@@ -513,7 +513,7 @@ public class QueryBreakdownTests
|
|||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
// Find the UserId parameter value
|
// Find the UserId parameter value
|
||||||
var userIdKey = merged.Keys.FirstOrDefault(k => k.Contains("UserId") && !k.Contains("$"));
|
var userIdKey = merged.Keys.FirstOrDefault(k => k.Contains("UserId") && !k.Contains('$'));
|
||||||
Assert.That(userIdKey, Is.Not.Null);
|
Assert.That(userIdKey, Is.Not.Null);
|
||||||
Assert.That(merged[userIdKey], Is.EqualTo(123)); // Main query value, not CTE value
|
Assert.That(merged[userIdKey], Is.EqualTo(123)); // Main query value, not CTE value
|
||||||
}
|
}
|
||||||
@@ -559,9 +559,9 @@ public class QueryBreakdownTests
|
|||||||
var merged = mainQuery.GetMergedParameters();
|
var merged = mainQuery.GetMergedParameters();
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
var dateKey = merged.Keys.FirstOrDefault(k => k.Contains("CreatedDate") && !k.Contains("$"));
|
var dateKey = merged.Keys.FirstOrDefault(k => k.Contains("CreatedDate") && !k.Contains('$'));
|
||||||
var boolKey = merged.Keys.FirstOrDefault(k => k.Contains("IsActive") && !k.Contains("$"));
|
var boolKey = merged.Keys.FirstOrDefault(k => k.Contains("IsActive") && !k.Contains('$'));
|
||||||
var doubleKey = merged.Keys.FirstOrDefault(k => k.Contains("Threshold") && !k.Contains("$"));
|
var doubleKey = merged.Keys.FirstOrDefault(k => k.Contains("Threshold") && !k.Contains('$'));
|
||||||
|
|
||||||
Assert.That(dateKey, Is.Not.Null);
|
Assert.That(dateKey, Is.Not.Null);
|
||||||
Assert.That(merged[dateKey], Is.TypeOf<DateTime>());
|
Assert.That(merged[dateKey], Is.TypeOf<DateTime>());
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ public class TestRuleInput
|
|||||||
|
|
||||||
public string? ImGoingToMakeThisNull { get; set; } = null;
|
public string? ImGoingToMakeThisNull { get; set; } = null;
|
||||||
|
|
||||||
public TestRuleDetail[] Details { get; set; } = { };
|
public TestRuleDetail[] Details { get; set; } = Array.Empty<TestRuleDetail>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public class TestRuleDetail
|
public class TestRuleDetail
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ public class Tests
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
static System.Linq.Expressions.Expression StartsWith(System.Linq.Expressions.Expression expression, string value)
|
static MethodCallExpression StartsWith(System.Linq.Expressions.Expression expression, string value)
|
||||||
{
|
{
|
||||||
return StartsWith(expression, LinqExpression.Constant(value));
|
return StartsWith(expression, LinqExpression.Constant(value));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,6 +98,8 @@ public class QueryBreakdownTests
|
|||||||
Assert.That(sql, Does.Contain("ORDER BY"));
|
Assert.That(sql, Does.Contain("ORDER BY"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static readonly char[] separator = new[] { '\r', '\n' };
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void GetSql_WithSingleWithClause_UsesSnowflakeIndentation()
|
public void GetSql_WithSingleWithClause_UsesSnowflakeIndentation()
|
||||||
{
|
{
|
||||||
@@ -114,7 +116,7 @@ public class QueryBreakdownTests
|
|||||||
Assert.That(sql, Does.Contain("WITH"));
|
Assert.That(sql, Does.Contain("WITH"));
|
||||||
Assert.That(sql, Does.Contain("PRODUCT_SUMMARY AS ("));
|
Assert.That(sql, Does.Contain("PRODUCT_SUMMARY AS ("));
|
||||||
// Verify 4-space Snowflake indentation
|
// Verify 4-space Snowflake indentation
|
||||||
var lines = sql.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
|
var lines = sql.Split(separator, StringSplitOptions.RemoveEmptyEntries);
|
||||||
var indentedLines = lines.Where(l => l.StartsWith(" ")).ToList();
|
var indentedLines = lines.Where(l => l.StartsWith(" ")).ToList();
|
||||||
Assert.That(indentedLines.Count, Is.GreaterThan(0));
|
Assert.That(indentedLines.Count, Is.GreaterThan(0));
|
||||||
}
|
}
|
||||||
@@ -1118,7 +1120,7 @@ public class QueryBreakdownTests
|
|||||||
_ = baseQueryBreakdown.GetSql();
|
_ = baseQueryBreakdown.GetSql();
|
||||||
|
|
||||||
// Assert - Snowflake should use 4-space indent, base uses 5-space
|
// Assert - Snowflake should use 4-space indent, base uses 5-space
|
||||||
var snowflakeLines = snowflakeSql.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
|
var snowflakeLines = snowflakeSql.Split(separator, StringSplitOptions.RemoveEmptyEntries);
|
||||||
var snowflakeIndentedLines = snowflakeLines.Where(l => l.StartsWith(" ") && !l.StartsWith(" ")).ToList();
|
var snowflakeIndentedLines = snowflakeLines.Where(l => l.StartsWith(" ") && !l.StartsWith(" ")).ToList();
|
||||||
Assert.That(snowflakeIndentedLines.Count, Is.GreaterThan(0), "Snowflake should use 4-space indentation");
|
Assert.That(snowflakeIndentedLines.Count, Is.GreaterThan(0), "Snowflake should use 4-space indentation");
|
||||||
}
|
}
|
||||||
@@ -1322,7 +1324,7 @@ public class QueryBreakdownTests
|
|||||||
Assert.That(snowflakeSql, Does.Contain("WHERE"));
|
Assert.That(snowflakeSql, Does.Contain("WHERE"));
|
||||||
Assert.That(snowflakeSql, Does.Contain("ORDER BY"));
|
Assert.That(snowflakeSql, Does.Contain("ORDER BY"));
|
||||||
// Verify Snowflake-style formatting (4-space indentation)
|
// Verify Snowflake-style formatting (4-space indentation)
|
||||||
var lines = snowflakeSql.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
|
var lines = snowflakeSql.Split(separator, StringSplitOptions.RemoveEmptyEntries);
|
||||||
var indentedLines = lines.Where(l => l.StartsWith(" ")).ToList();
|
var indentedLines = lines.Where(l => l.StartsWith(" ")).ToList();
|
||||||
Assert.That(indentedLines.Count, Is.GreaterThan(0), "Should have Snowflake-style indentation");
|
Assert.That(indentedLines.Count, Is.GreaterThan(0), "Should have Snowflake-style indentation");
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-2
@@ -5,6 +5,8 @@ namespace Strata.SqlTools.SqlBreakdown.Tests.ExpressionTests;
|
|||||||
[TestFixture]
|
[TestFixture]
|
||||||
public class ExpressionFactoryFilterTests : ExpressionTestsBase
|
public class ExpressionFactoryFilterTests : ExpressionTestsBase
|
||||||
{
|
{
|
||||||
|
private static readonly string[] values = new[] { "FY2019", "FY2020", "FY2021", "FY2022" };
|
||||||
|
|
||||||
private static IEnumerable<TestCaseData> FilterTestCases()
|
private static IEnumerable<TestCaseData> FilterTestCases()
|
||||||
{
|
{
|
||||||
var dischargeDate = DateTime.Now.Date.AddMonths(1);
|
var dischargeDate = DateTime.Now.Date.AddMonths(1);
|
||||||
@@ -16,7 +18,7 @@ public class ExpressionFactoryFilterTests : ExpressionTestsBase
|
|||||||
).SetName("ListFilterContinuous_{m}");
|
).SetName("ListFilterContinuous_{m}");
|
||||||
|
|
||||||
yield return new TestCaseData(
|
yield return new TestCaseData(
|
||||||
new Filter(4, FilterType.List, new[] { "FY2019", "FY2020", "FY2021", "FY2022" }, Array.Empty<FilterCondition>(), DatePart.FiscalYear, false, 0, 0),
|
new Filter(4, FilterType.List, values, Array.Empty<FilterCondition>(), DatePart.FiscalYear, false, 0, 0),
|
||||||
"(DEPT.DISCHARGE_DATE >= '2018-07-01' AND DEPT.DISCHARGE_DATE < '2019-07-01') OR \n(DEPT.DISCHARGE_DATE >= '2019-07-01' AND DEPT.DISCHARGE_DATE < '2020-07-01') OR \n(DEPT.DISCHARGE_DATE >= '2020-07-01' AND DEPT.DISCHARGE_DATE < '2021-07-01') OR \n(DEPT.DISCHARGE_DATE >= '2021-07-01' AND DEPT.DISCHARGE_DATE < '2022-07-01')"
|
"(DEPT.DISCHARGE_DATE >= '2018-07-01' AND DEPT.DISCHARGE_DATE < '2019-07-01') OR \n(DEPT.DISCHARGE_DATE >= '2019-07-01' AND DEPT.DISCHARGE_DATE < '2020-07-01') OR \n(DEPT.DISCHARGE_DATE >= '2020-07-01' AND DEPT.DISCHARGE_DATE < '2021-07-01') OR \n(DEPT.DISCHARGE_DATE >= '2021-07-01' AND DEPT.DISCHARGE_DATE < '2022-07-01')"
|
||||||
).SetName("DateListFilterFiscalYear_{m}");
|
).SetName("DateListFilterFiscalYear_{m}");
|
||||||
|
|
||||||
@@ -31,7 +33,7 @@ public class ExpressionFactoryFilterTests : ExpressionTestsBase
|
|||||||
).SetName("CalendarFilter_{m}");
|
).SetName("CalendarFilter_{m}");
|
||||||
|
|
||||||
yield return new TestCaseData(
|
yield return new TestCaseData(
|
||||||
new Filter(4, FilterType.Timeframe, new object[] { }, Array.Empty<FilterCondition>(), DatePart.Month, false, 1, 3),
|
new Filter(4, FilterType.Timeframe, Array.Empty<object>(), Array.Empty<FilterCondition>(), DatePart.Month, false, 1, 3),
|
||||||
$"DEPT.DISCHARGE_DATE >= '{startOfCurrentMonth:yyyy-MM-dd}' AND DEPT.DISCHARGE_DATE < '{startOfEndMonth:yyyy-MM-dd}'"
|
$"DEPT.DISCHARGE_DATE >= '{startOfCurrentMonth:yyyy-MM-dd}' AND DEPT.DISCHARGE_DATE < '{startOfEndMonth:yyyy-MM-dd}'"
|
||||||
).SetName("TimeframeFilter_{m}");
|
).SetName("TimeframeFilter_{m}");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ public class ExpressionObjectTests : ExpressionTestsBase
|
|||||||
Assert.That(paramExp.ParameterName, Is.EqualTo("MY_PARAM"));
|
Assert.That(paramExp.ParameterName, Is.EqualTo("MY_PARAM"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static readonly string[] NotInValues = new[] { "value1", "value2", "value3" };
|
||||||
|
|
||||||
private static IEnumerable<TestCaseData> ComparisonExpressionTestCases()
|
private static IEnumerable<TestCaseData> ComparisonExpressionTestCases()
|
||||||
{
|
{
|
||||||
yield return new TestCaseData("GreaterThanOrEqual", 250, typeof(GreaterThanOrEqualToExpression))
|
yield return new TestCaseData("GreaterThanOrEqual", 250, typeof(GreaterThanOrEqualToExpression))
|
||||||
@@ -40,7 +42,7 @@ public class ExpressionObjectTests : ExpressionTestsBase
|
|||||||
yield return new TestCaseData("Equals", "TestDept", typeof(EqualToExpression))
|
yield return new TestCaseData("Equals", "TestDept", typeof(EqualToExpression))
|
||||||
.SetName("Equals_{m}");
|
.SetName("Equals_{m}");
|
||||||
|
|
||||||
yield return new TestCaseData("NotIn", new[] { "value1", "value2", "value3" }, typeof(NotInExpression))
|
yield return new TestCaseData("NotIn", NotInValues, typeof(NotInExpression))
|
||||||
.SetName("NotIn_{m}");
|
.SetName("NotIn_{m}");
|
||||||
|
|
||||||
yield return new TestCaseData("Like", "%pattern%", typeof(LikeExpression))
|
yield return new TestCaseData("Like", "%pattern%", typeof(LikeExpression))
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ public abstract class ExpressionTestsBase
|
|||||||
/// Executes an expression test case with arrange, act, and assert phases.
|
/// Executes an expression test case with arrange, act, and assert phases.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="testCase">The test case to execute.</param>
|
/// <param name="testCase">The test case to execute.</param>
|
||||||
protected void ExecuteExpressionTest(ExpressionTestCase testCase)
|
protected static void ExecuteExpressionTest(ExpressionTestCase testCase)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var visitor = new CommandVisitor();
|
var visitor = new CommandVisitor();
|
||||||
|
|||||||
+3
-1
@@ -243,12 +243,14 @@ public class QueryBreakdownExtensionsTests
|
|||||||
Assert.That(query.WithClauses[1].TableName, Is.EqualTo("recent_orders"));
|
Assert.That(query.WithClauses[1].TableName, Is.EqualTo("recent_orders"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static readonly string[] columns = new[] { "id", "name", "email" };
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void WithCte_WithColumnList_AddsCtesWithColumns()
|
public void WithCte_WithColumnList_AddsCtesWithColumns()
|
||||||
{
|
{
|
||||||
// Arrange & Act
|
// Arrange & Act
|
||||||
var query = new QueryBreakdown()
|
var query = new QueryBreakdown()
|
||||||
.WithCte("active_users", new[] { "id", "name", "email" }, cte => cte
|
.WithCte("active_users", columns, cte => cte
|
||||||
.Select("user_id, user_name, user_email")
|
.Select("user_id, user_name, user_email")
|
||||||
.From("users")
|
.From("users")
|
||||||
.Where("status = 'active'"))
|
.Where("status = 'active'"))
|
||||||
|
|||||||
@@ -174,7 +174,7 @@ public class RecursiveCTETests
|
|||||||
var merged = mainQuery.GetMergedParameters();
|
var merged = mainQuery.GetMergedParameters();
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
var statusKey = merged.Keys.FirstOrDefault(k => k.Contains("Status") && !k.Contains("@") && !k.Contains("$"));
|
var statusKey = merged.Keys.FirstOrDefault(k => k.Contains("Status") && !k.Contains('@') && !k.Contains('$'));
|
||||||
Assert.That(statusKey, Is.Not.Null);
|
Assert.That(statusKey, Is.Not.Null);
|
||||||
Assert.That(merged[statusKey], Is.EqualTo("active"), "Main query parameter should take precedence");
|
Assert.That(merged[statusKey], Is.EqualTo("active"), "Main query parameter should take precedence");
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user