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.
|
||||
|
||||
## B — Cleanup loop
|
||||
## B — Cleanup loop (tiered)
|
||||
|
||||
```dot
|
||||
digraph cleanup {
|
||||
query [label="Query open issues + facets"];
|
||||
pick [label="Pick one rule group"];
|
||||
edit [label="Edit code"];
|
||||
build [label="dotnet build -c Release"];
|
||||
test [label="dotnet test -c Release --no-build"];
|
||||
commit [label="git commit\nchore(sonar): … (Sxxxx)"];
|
||||
more [label="More groups?" shape=diamond];
|
||||
scan [label="scan-sonar.ps1"];
|
||||
verify [label="Query API: confirm net-down"];
|
||||
query [label="Query open issues + facets=rules"];
|
||||
triage [label="Tier each rule:\nmechanical / judgment / API-impact"];
|
||||
t1 [label="Tier 1 — mechanical:\nbulk-fix in one commit"];
|
||||
t2 [label="Tier 2 — judgment:\none commit per rule, audit diff"];
|
||||
t3 [label="Tier 3 — API-impact:\nmanual, accept !breaking or skip"];
|
||||
build [label="dotnet build + dotnet test\nafter EACH commit"];
|
||||
push [label="git push + open PR"];
|
||||
ci [label="CI runs SonarScanner\non Java 17"];
|
||||
verify [label="Re-query API:\nconfirm rule counts dropped"];
|
||||
|
||||
query -> pick -> edit -> build -> test -> commit -> more;
|
||||
more -> pick [label="yes"];
|
||||
more -> scan [label="no"];
|
||||
scan -> verify;
|
||||
query -> triage;
|
||||
triage -> t1 -> build;
|
||||
triage -> t2 -> build;
|
||||
triage -> t3 -> build;
|
||||
build -> push -> ci -> verify;
|
||||
}
|
||||
```
|
||||
|
||||
1. **Query.** Start with `severities=MAJOR,MINOR` + `facets=rules` to see what's worth fixing.
|
||||
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).
|
||||
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.
|
||||
**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.)
|
||||
|
||||
### Tier 1 — mechanical (one bulk commit)
|
||||
|
||||
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();
|
||||
if (!string.IsNullOrWhiteSpace(table))
|
||||
{
|
||||
if (tableUsage.ContainsKey(table))
|
||||
if (tableUsage.TryGetValue(table, out int value))
|
||||
{
|
||||
tableUsage[table]++;
|
||||
tableUsage[table] = ++value;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -221,9 +221,9 @@ public class QueryCollectionAnalyzer
|
||||
foreach (var col in columns)
|
||||
{
|
||||
var columnName = col.Trim();
|
||||
if (columnUsage.ContainsKey(columnName))
|
||||
if (columnUsage.TryGetValue(columnName, out int value))
|
||||
{
|
||||
columnUsage[columnName]++;
|
||||
columnUsage[columnName] = ++value;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -71,10 +71,7 @@ public class LinqQueryBreakdown : QueryBreakdown
|
||||
/// <returns>A LinqQueryBreakdown representing the query structure.</returns>
|
||||
public static LinqQueryBreakdown Analyze<T>(IQueryable<T> query)
|
||||
{
|
||||
if (query == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(query));
|
||||
}
|
||||
ArgumentNullException.ThrowIfNull(query);
|
||||
|
||||
var breakdown = new LinqQueryBreakdown
|
||||
{
|
||||
@@ -232,10 +229,7 @@ public class LinqQueryBreakdown : QueryBreakdown
|
||||
/// <returns>An InsertBreakdown representing the insert operation.</returns>
|
||||
public static Breakdowns.SqlServer.InsertBreakdown AnalyzeInsert<T>(T entity) where T : class
|
||||
{
|
||||
if (entity == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(entity));
|
||||
}
|
||||
ArgumentNullException.ThrowIfNull(entity);
|
||||
|
||||
var breakdown = new Breakdowns.SqlServer.InsertBreakdown();
|
||||
breakdown.TableName.Clause = typeof(T).Name;
|
||||
@@ -312,10 +306,7 @@ public class LinqQueryBreakdown : QueryBreakdown
|
||||
/// <returns>A DeleteBreakdown representing the delete operation.</returns>
|
||||
public static Breakdowns.SqlServer.DeleteBreakdown AnalyzeDelete<T>(Expression<Func<T, bool>> filterExpression) where T : class
|
||||
{
|
||||
if (filterExpression == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(filterExpression));
|
||||
}
|
||||
ArgumentNullException.ThrowIfNull(filterExpression);
|
||||
|
||||
var breakdown = new Breakdowns.SqlServer.DeleteBreakdown();
|
||||
breakdown.FromClause.Clause = typeof(T).Name;
|
||||
@@ -343,14 +334,8 @@ public class LinqQueryBreakdown : QueryBreakdown
|
||||
Expression<Func<T, bool>> filterExpression,
|
||||
Expression<Func<T, T>> updateExpression) where T : class
|
||||
{
|
||||
if (filterExpression == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(filterExpression));
|
||||
}
|
||||
if (updateExpression == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(updateExpression));
|
||||
}
|
||||
ArgumentNullException.ThrowIfNull(filterExpression);
|
||||
ArgumentNullException.ThrowIfNull(updateExpression);
|
||||
|
||||
var breakdown = new Breakdowns.SqlServer.UpdateBreakdown();
|
||||
breakdown.TableName.Clause = typeof(T).Name;
|
||||
@@ -425,10 +410,7 @@ public class LinqQueryBreakdown : QueryBreakdown
|
||||
/// <returns>A string representation of the trace analysis.</returns>
|
||||
public static string AnalyzeTrace<T>(IQueryable<T> query, string? executionContext = null) where T : class
|
||||
{
|
||||
if (query == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(query));
|
||||
}
|
||||
ArgumentNullException.ThrowIfNull(query);
|
||||
|
||||
var lines = new List<string>
|
||||
{
|
||||
@@ -580,7 +562,7 @@ public class LinqQueryBreakdown : QueryBreakdown
|
||||
/// <returns>True if SELECT contains *; otherwise, false.</returns>
|
||||
public bool SelectsAllColumns()
|
||||
{
|
||||
return SelectClause?.Clause?.Contains("*") ?? false;
|
||||
return SelectClause?.Clause?.Contains('*') ?? false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -18,10 +18,7 @@ public static class ReverseConverterExtensions
|
||||
/// <returns>A new LinqQueryBreakdown with the same clauses.</returns>
|
||||
public static LinqQueryBreakdown ToLinqQueryBreakdown(this QueryBreakdown breakdown)
|
||||
{
|
||||
if (breakdown == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(breakdown));
|
||||
}
|
||||
ArgumentNullException.ThrowIfNull(breakdown);
|
||||
|
||||
var linq = new LinqQueryBreakdown(
|
||||
breakdown.SelectClause?.Clause ?? "*",
|
||||
@@ -54,10 +51,7 @@ public static class ReverseConverterExtensions
|
||||
/// <returns>A new LinqQueryBreakdown with the same clauses.</returns>
|
||||
public static LinqQueryBreakdown ToLinqQueryBreakdown(this PostgreSqlBreakdown breakdown)
|
||||
{
|
||||
if (breakdown == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(breakdown));
|
||||
}
|
||||
ArgumentNullException.ThrowIfNull(breakdown);
|
||||
|
||||
var linq = new LinqQueryBreakdown(
|
||||
breakdown.SelectClause?.Clause ?? "*",
|
||||
@@ -90,10 +84,7 @@ public static class ReverseConverterExtensions
|
||||
/// <returns>A new LinqQueryBreakdown with the same clauses.</returns>
|
||||
public static LinqQueryBreakdown ToLinqQueryBreakdown(this SnowflakeBreakdown breakdown)
|
||||
{
|
||||
if (breakdown == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(breakdown));
|
||||
}
|
||||
ArgumentNullException.ThrowIfNull(breakdown);
|
||||
|
||||
var linq = new LinqQueryBreakdown(
|
||||
breakdown.SelectClause?.Clause ?? "*",
|
||||
@@ -127,10 +118,7 @@ public static class ReverseConverterExtensions
|
||||
/// <returns>A new breakdown in the target dialect format.</returns>
|
||||
public static object ConvertToDialect(this QueryBreakdown breakdown, string targetDialect)
|
||||
{
|
||||
if (breakdown == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(breakdown));
|
||||
}
|
||||
ArgumentNullException.ThrowIfNull(breakdown);
|
||||
|
||||
return targetDialect.ToLowerInvariant() switch
|
||||
{
|
||||
@@ -150,10 +138,7 @@ public static class ReverseConverterExtensions
|
||||
/// <returns>A new breakdown in the target dialect format.</returns>
|
||||
public static object ConvertToDialect(this PostgreSqlBreakdown breakdown, string targetDialect)
|
||||
{
|
||||
if (breakdown == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(breakdown));
|
||||
}
|
||||
ArgumentNullException.ThrowIfNull(breakdown);
|
||||
|
||||
return targetDialect.ToLowerInvariant() switch
|
||||
{
|
||||
@@ -173,10 +158,7 @@ public static class ReverseConverterExtensions
|
||||
/// <returns>A new breakdown in the target dialect format.</returns>
|
||||
public static object ConvertToDialect(this SnowflakeBreakdown breakdown, string targetDialect)
|
||||
{
|
||||
if (breakdown == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(breakdown));
|
||||
}
|
||||
ArgumentNullException.ThrowIfNull(breakdown);
|
||||
|
||||
return targetDialect.ToLowerInvariant() switch
|
||||
{
|
||||
|
||||
@@ -71,10 +71,7 @@ public class QueryValidator
|
||||
/// <returns>This validator for method chaining.</returns>
|
||||
public QueryValidator Validate(LinqQueryBreakdown breakdown)
|
||||
{
|
||||
if (breakdown == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(breakdown));
|
||||
}
|
||||
ArgumentNullException.ThrowIfNull(breakdown);
|
||||
|
||||
_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
|
||||
}
|
||||
@@ -245,12 +242,12 @@ public class QueryValidator
|
||||
}
|
||||
}
|
||||
|
||||
private void ValidateHavingClause(LinqQueryBreakdown breakdown)
|
||||
private static void ValidateHavingClause(LinqQueryBreakdown breakdown)
|
||||
{
|
||||
// Validation delegated to ValidateGroupByClause
|
||||
}
|
||||
|
||||
private void ValidateOrderByClause(LinqQueryBreakdown breakdown)
|
||||
private static void ValidateOrderByClause(LinqQueryBreakdown breakdown)
|
||||
{
|
||||
// No validation needed - ORDER BY is optional
|
||||
}
|
||||
|
||||
@@ -248,13 +248,13 @@ public class LinqExpressionVisitor : ExpressionVisitor
|
||||
{
|
||||
if (_isInWhereClause)
|
||||
{
|
||||
_whereBuilder.Append("(");
|
||||
_whereBuilder.Append('(');
|
||||
Visit(node.Left);
|
||||
|
||||
_whereBuilder.Append($" {GetOperator(node.NodeType)} ");
|
||||
|
||||
Visit(node.Right);
|
||||
_whereBuilder.Append(")");
|
||||
_whereBuilder.Append(')');
|
||||
|
||||
return node;
|
||||
}
|
||||
@@ -323,7 +323,7 @@ public class LinqExpressionVisitor : ExpressionVisitor
|
||||
return expression.ToString();
|
||||
}
|
||||
|
||||
private string GetFullMemberName(MemberExpression expression)
|
||||
private static string GetFullMemberName(MemberExpression expression)
|
||||
{
|
||||
var parts = new Stack<string>();
|
||||
var current = expression;
|
||||
@@ -354,7 +354,7 @@ public class LinqExpressionVisitor : ExpressionVisitor
|
||||
return string.Join(".", parts);
|
||||
}
|
||||
|
||||
private string GetOperator(ExpressionType nodeType)
|
||||
private static string GetOperator(ExpressionType nodeType)
|
||||
{
|
||||
return nodeType switch
|
||||
{
|
||||
|
||||
@@ -260,7 +260,7 @@ public class ExpressionGenerator : IVisitor<string>
|
||||
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++;
|
||||
nodeMap[expression] = currentNode;
|
||||
@@ -366,7 +366,7 @@ public class ExpressionGenerator : IVisitor<string>
|
||||
{
|
||||
return text;
|
||||
}
|
||||
return text.Substring(0, maxLength) + "...";
|
||||
return string.Concat(text.AsSpan(0, maxLength), "...");
|
||||
}
|
||||
|
||||
#region IVisitor Implementation
|
||||
@@ -583,7 +583,7 @@ public class ExpressionGenerator : IVisitor<string>
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine($"{Indent()}Function: {function.FunctionName}");
|
||||
if (function.Arguments.Any())
|
||||
if (function.Arguments.Length != 0)
|
||||
{
|
||||
_indentLevel++;
|
||||
sb.AppendLine($"{Indent()}Arguments:");
|
||||
@@ -602,7 +602,7 @@ public class ExpressionGenerator : IVisitor<string>
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine($"{Indent()}Aggregate Function: {aggregateFunction.FunctionName}");
|
||||
if (aggregateFunction.Arguments.Any())
|
||||
if (aggregateFunction.Arguments.Length != 0)
|
||||
{
|
||||
_indentLevel++;
|
||||
sb.AppendLine($"{Indent()}Arguments:");
|
||||
|
||||
@@ -106,7 +106,7 @@ public class SimpleExpressionGenerator
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private string GetExpressionDescription(Expression expression)
|
||||
private static string GetExpressionDescription(Expression expression)
|
||||
{
|
||||
var typeName = expression.GetType().Name;
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ public class QueryBreakdownGenerator
|
||||
/// <param name="queryBreakdown">The LINQ QueryBreakdown to visualize.</param>
|
||||
/// <param name="title">Optional title for the diagram.</param>
|
||||
/// <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();
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ public class SqlStatementGenerator
|
||||
/// <param name="queryBreakdown">The query breakdown containing query information.</param>
|
||||
/// <param name="title">Optional title for the diagram.</param>
|
||||
/// <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();
|
||||
|
||||
|
||||
@@ -177,7 +177,7 @@ public class QueryBreakdownGenerator
|
||||
/// <summary>
|
||||
/// Escapes text for Mermaid diagram labels to prevent syntax errors.
|
||||
/// </summary>
|
||||
private string EscapeMermaidText(string text)
|
||||
private static string EscapeMermaidText(string text)
|
||||
{
|
||||
return text
|
||||
.Replace("\"", """)
|
||||
@@ -194,14 +194,14 @@ public class QueryBreakdownGenerator
|
||||
/// <summary>
|
||||
/// Truncates text to a maximum length and adds ellipsis if needed.
|
||||
/// </summary>
|
||||
private string TruncateText(string text, int maxLength)
|
||||
private static string TruncateText(string text, int maxLength)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text) || text.Length <= maxLength)
|
||||
{
|
||||
return text;
|
||||
}
|
||||
|
||||
return text.Substring(0, maxLength) + "...";
|
||||
return string.Concat(text.AsSpan(0, maxLength), "...");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -104,7 +104,7 @@ public class SqlStatementGenerator
|
||||
/// <summary>
|
||||
/// Escapes text for Mermaid diagram labels.
|
||||
/// </summary>
|
||||
private string EscapeMermaidText(string text)
|
||||
private static string EscapeMermaidText(string text)
|
||||
{
|
||||
return text
|
||||
.Replace("\"", """)
|
||||
@@ -115,20 +115,20 @@ public class SqlStatementGenerator
|
||||
/// <summary>
|
||||
/// Truncates text to a maximum length.
|
||||
/// </summary>
|
||||
private string TruncateText(string text, int maxLength)
|
||||
private static string TruncateText(string text, int maxLength)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text) || text.Length <= maxLength)
|
||||
{
|
||||
return text;
|
||||
}
|
||||
|
||||
return text.Substring(0, maxLength) + "...";
|
||||
return string.Concat(text.AsSpan(0, maxLength), "...");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cleans table name for use in Mermaid diagrams.
|
||||
/// </summary>
|
||||
private string CleanTableName(string tableName)
|
||||
private static string CleanTableName(string tableName)
|
||||
{
|
||||
return tableName
|
||||
.Replace("[", "")
|
||||
|
||||
@@ -129,7 +129,7 @@ public class StatementExpressionParser : SqlServerStatementExpressionParser
|
||||
|
||||
if (reader.TokenType == TokenType.String || reader.TokenType == TokenType.ColumnIdentifier)
|
||||
{
|
||||
columnBuilder.Append(".").Append(reader.TokenValue);
|
||||
columnBuilder.Append('.').Append(reader.TokenValue);
|
||||
reader.Read();
|
||||
}
|
||||
else
|
||||
@@ -191,7 +191,7 @@ public class StatementExpressionParser : SqlServerStatementExpressionParser
|
||||
|
||||
if (reader.TokenType == TokenType.ColumnIdentifier || reader.TokenType == TokenType.String)
|
||||
{
|
||||
columnBuilder.Append(".").Append(reader.TokenValue);
|
||||
columnBuilder.Append('.').Append(reader.TokenValue);
|
||||
reader.Read();
|
||||
}
|
||||
else
|
||||
|
||||
@@ -113,9 +113,8 @@ public class StatementParser : SqlServerStatementParser
|
||||
// PostgreSQL-specific: Append LIMIT/OFFSET to ORDER BY if present
|
||||
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
|
||||
.Where(v => v > limitStart)
|
||||
.Order()
|
||||
@@ -127,9 +126,8 @@ public class StatementParser : SqlServerStatementParser
|
||||
: $"{orderByClause} {limitClause}";
|
||||
}
|
||||
|
||||
if (clausePositions.ContainsKey(KeywordOffset))
|
||||
if (clausePositions.TryGetValue(KeywordOffset, out int offsetStart))
|
||||
{
|
||||
var offsetStart = clausePositions[KeywordOffset];
|
||||
var offsetEnd = clausePositions.Values
|
||||
.Where(v => v > offsetStart)
|
||||
.Order()
|
||||
|
||||
@@ -11,7 +11,7 @@ public class Value
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
private static Expression? TryParseLogicalOperation(string text)
|
||||
private static BoolExpr? TryParseLogicalOperation(string text)
|
||||
{
|
||||
foreach (var op in LogicalOperators.Keys)
|
||||
{
|
||||
@@ -130,7 +130,7 @@ public static class Markdown
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Expression? TryParseComparison(string text)
|
||||
private static Comparison? TryParseComparison(string text)
|
||||
{
|
||||
foreach (var op in ComparisonOperators.Keys)
|
||||
{
|
||||
|
||||
@@ -50,7 +50,7 @@ public class RuleSet : IGroup
|
||||
/// </summary>
|
||||
/// <param name="group">The rule group to traverse.</param>
|
||||
/// <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>();
|
||||
foreach (var rule in group.Rules)
|
||||
|
||||
@@ -55,7 +55,7 @@ public class ProcedureBreakdown : SqlServerProcedureBreakdown
|
||||
|
||||
sb.Append("CALL ");
|
||||
sb.Append(ProcedureName.Clause);
|
||||
sb.Append("(");
|
||||
sb.Append('(');
|
||||
|
||||
if (IsUsingParameters)
|
||||
{
|
||||
@@ -68,7 +68,7 @@ public class ProcedureBreakdown : SqlServerProcedureBreakdown
|
||||
sb.Append(string.Join(", ", paramList));
|
||||
}
|
||||
|
||||
sb.Append(")");
|
||||
sb.Append(')');
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
@@ -141,7 +141,7 @@ public class QueryBreakdown : SqlServerQueryBreakdown
|
||||
|
||||
if (parameterName.StartsWith('@'))
|
||||
{
|
||||
return ":" + parameterName.Substring(1);
|
||||
return string.Concat(":", parameterName.AsSpan(1));
|
||||
}
|
||||
|
||||
// Add : prefix
|
||||
@@ -159,7 +159,7 @@ public class QueryBreakdown : SqlServerQueryBreakdown
|
||||
if (paramName.StartsWith(':'))
|
||||
{
|
||||
// Add @param version
|
||||
var atParam = "@" + paramName.Substring(1);
|
||||
var atParam = string.Concat("@", paramName.AsSpan(1));
|
||||
if (!Parameters.ContainsKey(atParam))
|
||||
{
|
||||
Parameters[atParam] = Parameters[paramName];
|
||||
@@ -168,7 +168,7 @@ public class QueryBreakdown : SqlServerQueryBreakdown
|
||||
else if (paramName.StartsWith('@'))
|
||||
{
|
||||
// Add :param version
|
||||
var colonParam = ":" + paramName.Substring(1);
|
||||
var colonParam = string.Concat(":", paramName.AsSpan(1));
|
||||
if (!Parameters.ContainsKey(colonParam))
|
||||
{
|
||||
Parameters[colonParam] = Parameters[paramName];
|
||||
@@ -539,7 +539,7 @@ public class QueryBreakdown : SqlServerQueryBreakdown
|
||||
|
||||
if (i > 0)
|
||||
{
|
||||
sb.Append(",");
|
||||
sb.Append(',');
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ public class QueryBreakdownCollection : SqlServer.QueryBreakdownCollectionBase<Q
|
||||
|
||||
if (!trimmed.EndsWith(';'))
|
||||
{
|
||||
sb.Append(";");
|
||||
sb.Append(';');
|
||||
}
|
||||
|
||||
// Add spacing between statements
|
||||
@@ -111,7 +111,7 @@ public class QueryBreakdownCollection : SqlServer.QueryBreakdownCollectionBase<Q
|
||||
return true;
|
||||
}
|
||||
|
||||
var specificPattern = stageName.Contains("~")
|
||||
var specificPattern = stageName.Contains('~')
|
||||
? $@"@~/{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>
|
||||
/// Filters queries that use Snowflake functions (PARSE_JSON, OBJECT_INSERT, ARRAY, etc.).
|
||||
/// </summary>
|
||||
@@ -191,17 +200,7 @@ public class QueryBreakdownCollection : SqlServer.QueryBreakdownCollectionBase<Q
|
||||
return QueryBreakdownList.Where(q =>
|
||||
{
|
||||
var sql = q.GetSql().ToUpperInvariant();
|
||||
|
||||
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));
|
||||
return SnowflakeFunctionNames.Any(func => sql.Contains(func));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -289,7 +288,7 @@ public class QueryBreakdownCollection : SqlServer.QueryBreakdownCollectionBase<Q
|
||||
/// </summary>
|
||||
private static bool UsesStageReference(QueryBreakdown query)
|
||||
{
|
||||
return query.GetSql().Contains("@") &&
|
||||
return 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)
|
||||
{
|
||||
// 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)
|
||||
{
|
||||
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>
|
||||
public void Add(ISqlBreakdown breakdown)
|
||||
{
|
||||
if (breakdown == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(breakdown));
|
||||
}
|
||||
ArgumentNullException.ThrowIfNull(breakdown);
|
||||
|
||||
_breakdowns.Add(breakdown);
|
||||
}
|
||||
@@ -103,10 +100,7 @@ public class SqlBreakdownCollection : ICollection<ISqlBreakdown>
|
||||
/// <exception cref="ArgumentNullException">Thrown when breakdowns is null.</exception>
|
||||
public void AddRange(IEnumerable<ISqlBreakdown> breakdowns)
|
||||
{
|
||||
if (breakdowns == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(breakdowns));
|
||||
}
|
||||
ArgumentNullException.ThrowIfNull(breakdowns);
|
||||
|
||||
_breakdowns.AddRange(breakdowns);
|
||||
}
|
||||
@@ -142,10 +136,7 @@ public class SqlBreakdownCollection : ICollection<ISqlBreakdown>
|
||||
/// <exception cref="ArgumentNullException">Thrown when sqlBatch is null.</exception>
|
||||
public void ParseBatch(string sqlBatch, string? separator = null)
|
||||
{
|
||||
if (sqlBatch == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(sqlBatch));
|
||||
}
|
||||
ArgumentNullException.ThrowIfNull(sqlBatch);
|
||||
|
||||
Clear();
|
||||
|
||||
|
||||
@@ -85,7 +85,7 @@ public class SqlExpressionClause : SqlClause, ISqlExpressionClause
|
||||
/// </summary>
|
||||
/// <param name="clause">The clause to split.</param>
|
||||
/// <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 current = new StringBuilder();
|
||||
|
||||
@@ -269,20 +269,15 @@ public static class FlatDataUtils
|
||||
|
||||
public static object GetValue(this IFlatData data, string key)
|
||||
{
|
||||
if (data == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(data));
|
||||
}
|
||||
ArgumentNullException.ThrowIfNull(data);
|
||||
|
||||
if (!data.ContainsKey(key))
|
||||
if (!data.TryGetValue(key, out object? rawValue))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
$"The specified key is not available. Requested key: [{key}] Available keys: [{string.Join(", ", data.Keys)}]",
|
||||
nameof(key));
|
||||
}
|
||||
|
||||
var rawValue = data[key];
|
||||
|
||||
return rawValue;
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ public static class GuidUtils
|
||||
return Guid.Empty;
|
||||
}
|
||||
|
||||
if (value.Contains("-"))
|
||||
if (value.Contains('-'))
|
||||
{
|
||||
// This function was called on an already valid guid
|
||||
return new Guid(value);
|
||||
@@ -92,7 +92,7 @@ public static class GuidUtils
|
||||
var sb = new System.Text.StringBuilder(newguid);
|
||||
while (sb.Length < Guid.Empty.ToString().Length)
|
||||
{
|
||||
sb.Append(sb.ToString());
|
||||
sb.Append(sb);
|
||||
}
|
||||
newguid = sb.ToString();
|
||||
|
||||
@@ -114,7 +114,7 @@ public static class GuidUtils
|
||||
for (int i = 0; i < guidChars.Length; 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];
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ public static partial class SqlUtils
|
||||
public static void AppendAliasColumnWithComma(StringBuilder stringBuilder, string alias, string columnName)
|
||||
{
|
||||
stringBuilder.Append(alias);
|
||||
stringBuilder.Append(".");
|
||||
stringBuilder.Append('.');
|
||||
stringBuilder.Append(columnName);
|
||||
stringBuilder.AppendLine(",");
|
||||
}
|
||||
@@ -58,7 +58,7 @@ public static partial class SqlUtils
|
||||
{
|
||||
stringBuilder.Append("CAST(");
|
||||
stringBuilder.Append(alias);
|
||||
stringBuilder.Append(".");
|
||||
stringBuilder.Append('.');
|
||||
stringBuilder.Append(columnName);
|
||||
stringBuilder.Append(" AS ");
|
||||
stringBuilder.Append(dataType);
|
||||
@@ -94,7 +94,7 @@ public static partial class SqlUtils
|
||||
{
|
||||
if (i > 0)
|
||||
{
|
||||
sb.Append(",");
|
||||
sb.Append(',');
|
||||
}
|
||||
|
||||
string trimmed = columnNames[i].Trim();
|
||||
|
||||
@@ -42,10 +42,10 @@ public static partial class SqlUtils
|
||||
|
||||
foreach (Guid g in guidList)
|
||||
{
|
||||
sb.Append("'");
|
||||
sb.Append('\'');
|
||||
sb.Append(g.ToString());
|
||||
sb.Append("'");
|
||||
sb.Append(",");
|
||||
sb.Append('\'');
|
||||
sb.Append(',');
|
||||
}
|
||||
|
||||
return sb.ToString().Trim().TrimEnd(',');
|
||||
|
||||
@@ -27,7 +27,7 @@ public static partial class SqlUtils
|
||||
double.TryParse(value, out double dblValue);
|
||||
|
||||
// Optimize IN if only one value
|
||||
if (operation == FilterOperation.In && !value.Contains(","))
|
||||
if (operation == FilterOperation.In && !value.Contains(','))
|
||||
{
|
||||
operation = FilterOperation.Equal;
|
||||
}
|
||||
@@ -144,7 +144,7 @@ public static partial class SqlUtils
|
||||
}
|
||||
|
||||
int lastCloseParen = sb.ToString().LastIndexOf(')');
|
||||
filter.SqlExpression = sb.ToString().Substring(0, lastCloseParen + 1) + ")";
|
||||
filter.SqlExpression = string.Concat(sb.ToString().AsSpan(0, lastCloseParen + 1), ")");
|
||||
break;
|
||||
|
||||
case FilterOperation.NotBetween:
|
||||
|
||||
@@ -23,6 +23,7 @@ public static partial class SqlUtils
|
||||
public const string DATETIME_INSERT_FORMAT = "yyyyMMdd HH:mm:ss";
|
||||
|
||||
private const string DEFAULT_SCHEMA = "dbo";
|
||||
private static readonly char[] separator = new[] { ',' };
|
||||
|
||||
#region SQL String Manipulation
|
||||
|
||||
@@ -34,7 +35,7 @@ public static partial class SqlUtils
|
||||
public static string StripColumnTableAlias(string sql)
|
||||
{
|
||||
// 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>();
|
||||
|
||||
foreach (string commaWord in commaWords)
|
||||
|
||||
@@ -578,7 +578,7 @@ public class QueryBreakdown : SqlBreakdownBase, IQueryBreakdown
|
||||
|
||||
if (i > 0)
|
||||
{
|
||||
sb.Append(",");
|
||||
sb.Append(',');
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
|
||||
@@ -51,10 +51,7 @@ public abstract class QueryBreakdownCollectionBase<TQuery> : SqlBreakdownCollect
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="queryBreakdown"/> is null.</exception>
|
||||
public void Add(TQuery queryBreakdown)
|
||||
{
|
||||
if (queryBreakdown == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(queryBreakdown));
|
||||
}
|
||||
ArgumentNullException.ThrowIfNull(queryBreakdown);
|
||||
|
||||
QueryBreakdownList.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>
|
||||
public void AddRange(IEnumerable<TQuery> queryBreakdowns)
|
||||
{
|
||||
if (queryBreakdowns == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(queryBreakdowns));
|
||||
}
|
||||
ArgumentNullException.ThrowIfNull(queryBreakdowns);
|
||||
|
||||
foreach (var breakdown in queryBreakdowns)
|
||||
{
|
||||
|
||||
@@ -21,6 +21,7 @@ public class StatementParser
|
||||
public const string KeywordGroupBy = "GROUP BY";
|
||||
public const string KeywordHaving = "HAVING";
|
||||
public const string KeywordOrderBy = "ORDER BY";
|
||||
private static readonly char[] separator = new[] { '\r', '\n' };
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -50,7 +51,7 @@ public class StatementParser
|
||||
// Replace multiple spaces/tabs with single space, but preserve newlines for comment handling
|
||||
sql = Regex.Replace(sql, @"[ \t]+", " ", RegexOptions.None, RegexDefaults.MatchTimeout);
|
||||
// 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()));
|
||||
return sql.Trim();
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ public class QueryBreakdownRepositoryTests
|
||||
{
|
||||
private DbContext _dbContext = null!;
|
||||
private IQueryBreakdownMapper _mapper = null!;
|
||||
private IQueryBreakdownRepository _repository = null!;
|
||||
private QueryBreakdownRepository _repository = null!;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
|
||||
@@ -7,6 +7,7 @@ namespace Strata.SqlTools.LinqToSql.Tests;
|
||||
public class LinqQueryBreakdownTests
|
||||
{
|
||||
private TestDataContext _context = null!;
|
||||
private static readonly string[] separator = new[] { "AND" };
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
@@ -18,7 +19,7 @@ public class LinqQueryBreakdownTests
|
||||
public void Analyze_SimpleSelectQuery_ExtractsCorrectClauses()
|
||||
{
|
||||
// Arrange
|
||||
var query = from user in _context.Users
|
||||
var query = from user in TestDataContext.Users
|
||||
select user;
|
||||
|
||||
// Act
|
||||
@@ -34,7 +35,7 @@ public class LinqQueryBreakdownTests
|
||||
public void Analyze_WhereClause_ExtractsCondition()
|
||||
{
|
||||
// Arrange
|
||||
var query = from user in _context.Users
|
||||
var query = from user in TestDataContext.Users
|
||||
where user.Age > 21
|
||||
select user;
|
||||
|
||||
@@ -51,7 +52,7 @@ public class LinqQueryBreakdownTests
|
||||
public void Analyze_SelectWithProjection_ExtractsSelectedFields()
|
||||
{
|
||||
// Arrange
|
||||
var query = from user in _context.Users
|
||||
var query = from user in TestDataContext.Users
|
||||
select new { user.Id, user.Name };
|
||||
|
||||
// Act
|
||||
@@ -67,7 +68,7 @@ public class LinqQueryBreakdownTests
|
||||
public void Analyze_OrderByClause_ExtractsOrdering()
|
||||
{
|
||||
// Arrange
|
||||
var query = from user in _context.Users
|
||||
var query = from user in TestDataContext.Users
|
||||
orderby user.Name
|
||||
select user;
|
||||
|
||||
@@ -83,7 +84,7 @@ public class LinqQueryBreakdownTests
|
||||
public void Analyze_MethodSyntax_ExtractsCorrectClauses()
|
||||
{
|
||||
// Arrange
|
||||
var query = _context.Users
|
||||
var query = TestDataContext.Users
|
||||
.Where(u => u.Age > 18)
|
||||
.OrderBy(u => u.Name)
|
||||
.Select(u => new { u.Id, u.Name });
|
||||
@@ -101,7 +102,7 @@ public class LinqQueryBreakdownTests
|
||||
public void Analyze_MethodSyntax_TracksMethodChain()
|
||||
{
|
||||
// Arrange
|
||||
var query = _context.Users
|
||||
var query = TestDataContext.Users
|
||||
.Where(u => u.Age > 18)
|
||||
.OrderBy(u => u.Name)
|
||||
.Select(u => u.Name);
|
||||
@@ -120,7 +121,7 @@ public class LinqQueryBreakdownTests
|
||||
public void GetQuerySummary_ReturnsFormattedString()
|
||||
{
|
||||
// Arrange
|
||||
var query = _context.Users.Where(u => u.Age > 21);
|
||||
var query = TestDataContext.Users.Where(u => u.Age > 21);
|
||||
var breakdown = LinqQueryBreakdown.Analyze(query);
|
||||
|
||||
// Act
|
||||
@@ -137,7 +138,7 @@ public class LinqQueryBreakdownTests
|
||||
public void GetMethodChain_ReturnsMethodSequence()
|
||||
{
|
||||
// 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);
|
||||
|
||||
// Act
|
||||
@@ -153,7 +154,7 @@ public class LinqQueryBreakdownTests
|
||||
public void TryAnalyze_ValidQuery_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
var query = _context.Users.Where(u => u.Age > 21);
|
||||
var query = TestDataContext.Users.Where(u => u.Age > 21);
|
||||
|
||||
// Act
|
||||
var success = LinqQueryBreakdown.TryAnalyze(query, out var breakdown, out var error);
|
||||
@@ -182,7 +183,7 @@ public class LinqQueryBreakdownTests
|
||||
public void Analyze_ComplexQuery_HandlesCombinedClauses()
|
||||
{
|
||||
// Arrange
|
||||
var query = from user in _context.Users
|
||||
var query = from user in TestDataContext.Users
|
||||
where user.Age > 21 && user.IsActive
|
||||
orderby user.Name descending
|
||||
select new { user.Id, user.Name, user.Email };
|
||||
@@ -205,7 +206,7 @@ public class LinqQueryBreakdownTests
|
||||
public void AnalyzeAndModify_AddWhereClause_GeneratesUpdatedSql()
|
||||
{
|
||||
// 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);
|
||||
|
||||
// Act - Add additional filter using breakdown
|
||||
@@ -222,7 +223,7 @@ public class LinqQueryBreakdownTests
|
||||
public void AnalyzeAndModify_ChangeSelectClause_GeneratesNewProjection()
|
||||
{
|
||||
// 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);
|
||||
|
||||
// Act - Modify the SELECT clause
|
||||
@@ -243,7 +244,7 @@ public class LinqQueryBreakdownTests
|
||||
public void AnalyzeAndModify_CloneAndExtend_CreatesIndependentQuery()
|
||||
{
|
||||
// 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);
|
||||
|
||||
// Act - Clone and extend
|
||||
@@ -267,10 +268,10 @@ public class LinqQueryBreakdownTests
|
||||
public void AnalyzeAndCompose_MultipleQueries_CreatesUnionScenario()
|
||||
{
|
||||
// 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 recentUsersQuery = _context.Users.Where(u => u.Age < 25);
|
||||
var recentUsersQuery = TestDataContext.Users.Where(u => u.Age < 25);
|
||||
var recentBreakdown = LinqQueryBreakdown.Analyze(recentUsersQuery);
|
||||
|
||||
// Act - Get SQL for both (could be used in UNION scenario)
|
||||
@@ -287,7 +288,7 @@ public class LinqQueryBreakdownTests
|
||||
public void AnalyzeAndBuildFilter_IncrementallyAddConditions_BuildsComplexFilter()
|
||||
{
|
||||
// Arrange - Start with simple query
|
||||
var query = _context.Users;
|
||||
var query = TestDataContext.Users;
|
||||
var breakdown = LinqQueryBreakdown.Analyze(query);
|
||||
|
||||
// Act - Incrementally add filter conditions (simulating filter builder UI)
|
||||
@@ -303,7 +304,7 @@ public class LinqQueryBreakdownTests
|
||||
Assert.That(filterSql, Does.Contain("IsActive = 1"));
|
||||
|
||||
// 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));
|
||||
}
|
||||
|
||||
@@ -311,7 +312,7 @@ public class LinqQueryBreakdownTests
|
||||
public void AnalyzeAndPaginate_AddOrderAndLimits_CreatesPaginatedQuery()
|
||||
{
|
||||
// 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);
|
||||
|
||||
// Act - Add pagination (ORDER BY required for consistent pagination)
|
||||
@@ -335,7 +336,7 @@ public class LinqQueryBreakdownTests
|
||||
public void AnalyzeAndGenerateReport_ExtractQueryMetrics_ProvidesAnalytics()
|
||||
{
|
||||
// Arrange - Complex query to analyze
|
||||
var query = _context.Users
|
||||
var query = TestDataContext.Users
|
||||
.Where(u => u.Age > 21)
|
||||
.Where(u => u.IsActive)
|
||||
.OrderBy(u => u.Name)
|
||||
@@ -364,7 +365,7 @@ public class LinqQueryBreakdownTests
|
||||
public void AnalyzeAndOptimize_RemoveSelectStar_ImprovedProjection()
|
||||
{
|
||||
// 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);
|
||||
|
||||
// Verify it initially has SELECT *
|
||||
@@ -390,10 +391,10 @@ public class LinqQueryBreakdownTests
|
||||
public void AnalyzeMultipleQueries_CompareAndMerge_CreatesCompositeQuery()
|
||||
{
|
||||
// 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 activeQuery = _context.Users.Where(u => u.IsActive);
|
||||
var activeQuery = TestDataContext.Users.Where(u => u.IsActive);
|
||||
var activeBreakdown = LinqQueryBreakdown.Analyze(activeQuery);
|
||||
|
||||
// Act - Merge conditions from both queries
|
||||
@@ -416,7 +417,7 @@ public class LinqQueryBreakdownTests
|
||||
public void AnalyzeAndDocument_GenerateQueryDocumentation_CreatesReadableOutput()
|
||||
{
|
||||
// Arrange - Analyze a business query
|
||||
var query = _context.Orders
|
||||
var query = TestDataContext.Orders
|
||||
.Where(o => o.Amount > 1000)
|
||||
.Where(o => o.OrderDate > DateTime.Now.AddDays(-30))
|
||||
.OrderBy(o => o.OrderDate);
|
||||
@@ -451,8 +452,8 @@ public class LinqQueryBreakdownTests
|
||||
// Test data context and entities
|
||||
public class TestDataContext
|
||||
{
|
||||
public IQueryable<User> Users => new List<User>().AsQueryable();
|
||||
public IQueryable<Order> Orders => new List<Order>().AsQueryable();
|
||||
public static IQueryable<User> Users => new List<User>().AsQueryable();
|
||||
public static IQueryable<Order> Orders => new List<Order>().AsQueryable();
|
||||
}
|
||||
|
||||
public class User
|
||||
@@ -488,7 +489,7 @@ public class GetQueryTests
|
||||
public void GetQuery_LinqBreakdown_ReturnsNullByDefault()
|
||||
{
|
||||
// Arrange
|
||||
var query = _context.Users.Where(u => u.Age > 18);
|
||||
var query = TestDataContext.Users.Where(u => u.Age > 18);
|
||||
var breakdown = LinqQueryBreakdown.Analyze(query);
|
||||
|
||||
// Act - GetQuery returns null because LinqQueryBreakdown needs the original provider
|
||||
@@ -515,7 +516,7 @@ public class GetQueryTests
|
||||
public void GetQuery_MultipleBreakdownTypes_AllReturnNull()
|
||||
{
|
||||
// Arrange
|
||||
var linqBreakdown = LinqQueryBreakdown.Analyze(_context.Users);
|
||||
var linqBreakdown = LinqQueryBreakdown.Analyze(TestDataContext.Users);
|
||||
var sqlBreakdown = new QueryBreakdown("*", "Users");
|
||||
|
||||
// Act
|
||||
@@ -710,7 +711,7 @@ public class GetQueryTests
|
||||
public void AnalyzeTrace_WithValidQuery_ReturnsTraceString()
|
||||
{
|
||||
// Arrange
|
||||
var query = _context.Users.Where(u => u.Age > 18);
|
||||
var query = TestDataContext.Users.Where(u => u.Age > 18);
|
||||
|
||||
// Act
|
||||
var trace = LinqQueryBreakdown.AnalyzeTrace(query);
|
||||
@@ -726,7 +727,7 @@ public class GetQueryTests
|
||||
public void AnalyzeTrace_WithExecutionContext_IncludesContextInTrace()
|
||||
{
|
||||
// Arrange
|
||||
var query = _context.Users;
|
||||
var query = TestDataContext.Users;
|
||||
const string context = "Initial Load";
|
||||
|
||||
// Act
|
||||
@@ -754,7 +755,7 @@ public class GetQueryTests
|
||||
public void ConvertToSqlServerBreakdown_CopiesAllClauses()
|
||||
{
|
||||
// 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);
|
||||
|
||||
// Act
|
||||
@@ -772,7 +773,7 @@ public class GetQueryTests
|
||||
public void ConvertToPostgreSqlBreakdown_CopiesAllClauses()
|
||||
{
|
||||
// Arrange
|
||||
var query = _context.Users
|
||||
var query = TestDataContext.Users
|
||||
.Where(u => u.IsActive)
|
||||
.OrderBy(u => u.Name)
|
||||
.Select(u => new { u.Id, u.Name });
|
||||
@@ -793,7 +794,7 @@ public class GetQueryTests
|
||||
public void ConvertToSnowflakeBreakdown_CopiesAllClauses()
|
||||
{
|
||||
// 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);
|
||||
|
||||
// Act
|
||||
@@ -827,7 +828,7 @@ public class GetQueryTests
|
||||
public void ConvertToPostgreSqlBreakdown_WithComplexQuery_CopieAllClauses()
|
||||
{
|
||||
// Arrange
|
||||
var query = _context.Users
|
||||
var query = TestDataContext.Users
|
||||
.Where(u => u.IsActive && u.Age >= 18)
|
||||
.OrderByDescending(u => u.Age);
|
||||
|
||||
@@ -848,7 +849,7 @@ public class GetQueryTests
|
||||
public void ConvertToSnowflakeBreakdown_PreservesAllClauseInformation()
|
||||
{
|
||||
// Arrange
|
||||
var breakdown = LinqQueryBreakdown.Analyze(_context.Users.OrderBy(u => u.Name));
|
||||
var breakdown = LinqQueryBreakdown.Analyze(TestDataContext.Users.OrderBy(u => u.Name));
|
||||
|
||||
// Act
|
||||
var snowflakeBreakdown = breakdown.ConvertToSnowflakeBreakdown();
|
||||
@@ -862,7 +863,7 @@ public class GetQueryTests
|
||||
public void ConvertToSqlServerBreakdown_InstancesAreIndependent()
|
||||
{
|
||||
// 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();
|
||||
|
||||
// Act - Modify the SQL Server breakdown
|
||||
@@ -876,7 +877,7 @@ public class GetQueryTests
|
||||
public void ConvertChainMultipleTimes_EachConversionIndependent()
|
||||
{
|
||||
// Arrange
|
||||
var breakdown = LinqQueryBreakdown.Analyze(_context.Users.OrderBy(u => u.Name));
|
||||
var breakdown = LinqQueryBreakdown.Analyze(TestDataContext.Users.OrderBy(u => u.Name));
|
||||
|
||||
// Act
|
||||
var sqlServer1 = breakdown.ConvertToSqlServerBreakdown();
|
||||
|
||||
@@ -20,7 +20,7 @@ public class QueryBreakdownGeneratorTests
|
||||
public void GenerateMermaidDiagram_SimpleLinqQuery_GeneratesValidMermaid()
|
||||
{
|
||||
// Arrange
|
||||
var query = _context.Users.Where(u => u.Age > 21);
|
||||
var query = TestDataContext.Users.Where(u => u.Age > 21);
|
||||
var breakdown = LinqQueryBreakdown.Analyze(query);
|
||||
|
||||
// Act
|
||||
@@ -39,14 +39,14 @@ public class QueryBreakdownGeneratorTests
|
||||
public void GenerateMethodChainDiagram_WithMethodCalls_ShowsChain()
|
||||
{
|
||||
// Arrange
|
||||
var query = _context.Users
|
||||
var query = TestDataContext.Users
|
||||
.Where(u => u.Age > 18)
|
||||
.OrderBy(u => u.Name)
|
||||
.Select(u => new { u.Id, u.Name });
|
||||
var breakdown = LinqQueryBreakdown.Analyze(query);
|
||||
|
||||
// Act
|
||||
var result = _generator.GenerateMethodChainDiagram(breakdown, "Method Chain");
|
||||
var result = QueryBreakdownGenerator.GenerateMethodChainDiagram(breakdown, "Method Chain");
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Does.Contain("```mermaid"));
|
||||
@@ -66,7 +66,7 @@ public class QueryBreakdownGeneratorTests
|
||||
var breakdown = new LinqQueryBreakdown("*", "Users");
|
||||
|
||||
// Act
|
||||
var result = _generator.GenerateMethodChainDiagram(breakdown);
|
||||
var result = QueryBreakdownGenerator.GenerateMethodChainDiagram(breakdown);
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Does.Contain("IQueryable"));
|
||||
@@ -77,7 +77,7 @@ public class QueryBreakdownGeneratorTests
|
||||
public void GenerateCombinedDiagram_IncludesBothDiagrams()
|
||||
{
|
||||
// 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);
|
||||
|
||||
// Act
|
||||
@@ -95,7 +95,7 @@ public class QueryBreakdownGeneratorTests
|
||||
public void GenerateMermaidDiagram_WithProjection_ShowsSelectedFields()
|
||||
{
|
||||
// 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);
|
||||
|
||||
// Act
|
||||
@@ -110,7 +110,7 @@ public class QueryBreakdownGeneratorTests
|
||||
public void GenerateMermaidDiagram_NullTitle_GeneratesWithoutTitle()
|
||||
{
|
||||
// Arrange
|
||||
var query = _context.Users;
|
||||
var query = TestDataContext.Users;
|
||||
var breakdown = LinqQueryBreakdown.Analyze(query);
|
||||
|
||||
// Act
|
||||
@@ -125,7 +125,7 @@ public class QueryBreakdownGeneratorTests
|
||||
// Test data context
|
||||
public class TestDataContext
|
||||
{
|
||||
public IQueryable<User> Users => new List<User>().AsQueryable();
|
||||
public static IQueryable<User> Users => new List<User>().AsQueryable();
|
||||
}
|
||||
|
||||
public class User
|
||||
|
||||
@@ -21,7 +21,7 @@ public class SqlStatementGeneratorTests
|
||||
var query = new LinqQueryBreakdown("*", "Users");
|
||||
|
||||
// Act
|
||||
var result = _generator.GenerateLinqPipelineDiagram(query, "User Query Pipeline");
|
||||
var result = SqlStatementGenerator.GenerateLinqPipelineDiagram(query, "User Query Pipeline");
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Does.Contain("```mermaid"));
|
||||
@@ -42,7 +42,7 @@ public class SqlStatementGeneratorTests
|
||||
query.AddWhereClause("Age > 21");
|
||||
|
||||
// Act
|
||||
var result = _generator.GenerateLinqPipelineDiagram(query);
|
||||
var result = SqlStatementGenerator.GenerateLinqPipelineDiagram(query);
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Does.Contain("Where Predicate"));
|
||||
@@ -57,7 +57,7 @@ public class SqlStatementGeneratorTests
|
||||
var query = new LinqQueryBreakdown("Id, Name, Email", "Users");
|
||||
|
||||
// Act
|
||||
var result = _generator.GenerateLinqPipelineDiagram(query);
|
||||
var result = SqlStatementGenerator.GenerateLinqPipelineDiagram(query);
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Does.Contain("Select Projection"));
|
||||
@@ -72,7 +72,7 @@ public class SqlStatementGeneratorTests
|
||||
var query = new LinqQueryBreakdown("*", "Users");
|
||||
|
||||
// Act
|
||||
var result = _generator.GenerateLinqPipelineDiagram(query, null);
|
||||
var result = SqlStatementGenerator.GenerateLinqPipelineDiagram(query, null);
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Does.Not.Contain("###"));
|
||||
|
||||
@@ -513,7 +513,7 @@ public class QueryBreakdownTests
|
||||
|
||||
// Assert
|
||||
// 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(merged[userIdKey], Is.EqualTo(123)); // Main query value, not CTE value
|
||||
}
|
||||
@@ -559,9 +559,9 @@ public class QueryBreakdownTests
|
||||
var merged = mainQuery.GetMergedParameters();
|
||||
|
||||
// Assert
|
||||
var dateKey = merged.Keys.FirstOrDefault(k => k.Contains("CreatedDate") && !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 dateKey = merged.Keys.FirstOrDefault(k => k.Contains("CreatedDate") && !k.Contains('$'));
|
||||
var boolKey = merged.Keys.FirstOrDefault(k => k.Contains("IsActive") && !k.Contains('$'));
|
||||
var doubleKey = merged.Keys.FirstOrDefault(k => k.Contains("Threshold") && !k.Contains('$'));
|
||||
|
||||
Assert.That(dateKey, Is.Not.Null);
|
||||
Assert.That(merged[dateKey], Is.TypeOf<DateTime>());
|
||||
|
||||
@@ -133,7 +133,7 @@ public class TestRuleInput
|
||||
|
||||
public string? ImGoingToMakeThisNull { get; set; } = null;
|
||||
|
||||
public TestRuleDetail[] Details { get; set; } = { };
|
||||
public TestRuleDetail[] Details { get; set; } = Array.Empty<TestRuleDetail>();
|
||||
}
|
||||
|
||||
public class TestRuleDetail
|
||||
|
||||
@@ -148,7 +148,7 @@ public class Tests
|
||||
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));
|
||||
}
|
||||
|
||||
@@ -98,6 +98,8 @@ public class QueryBreakdownTests
|
||||
Assert.That(sql, Does.Contain("ORDER BY"));
|
||||
}
|
||||
|
||||
private static readonly char[] separator = new[] { '\r', '\n' };
|
||||
|
||||
[Test]
|
||||
public void GetSql_WithSingleWithClause_UsesSnowflakeIndentation()
|
||||
{
|
||||
@@ -114,7 +116,7 @@ public class QueryBreakdownTests
|
||||
Assert.That(sql, Does.Contain("WITH"));
|
||||
Assert.That(sql, Does.Contain("PRODUCT_SUMMARY AS ("));
|
||||
// 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();
|
||||
Assert.That(indentedLines.Count, Is.GreaterThan(0));
|
||||
}
|
||||
@@ -1118,7 +1120,7 @@ public class QueryBreakdownTests
|
||||
_ = baseQueryBreakdown.GetSql();
|
||||
|
||||
// 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();
|
||||
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("ORDER BY"));
|
||||
// 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();
|
||||
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]
|
||||
public class ExpressionFactoryFilterTests : ExpressionTestsBase
|
||||
{
|
||||
private static readonly string[] values = new[] { "FY2019", "FY2020", "FY2021", "FY2022" };
|
||||
|
||||
private static IEnumerable<TestCaseData> FilterTestCases()
|
||||
{
|
||||
var dischargeDate = DateTime.Now.Date.AddMonths(1);
|
||||
@@ -16,7 +18,7 @@ public class ExpressionFactoryFilterTests : ExpressionTestsBase
|
||||
).SetName("ListFilterContinuous_{m}");
|
||||
|
||||
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')"
|
||||
).SetName("DateListFilterFiscalYear_{m}");
|
||||
|
||||
@@ -31,7 +33,7 @@ public class ExpressionFactoryFilterTests : ExpressionTestsBase
|
||||
).SetName("CalendarFilter_{m}");
|
||||
|
||||
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}'"
|
||||
).SetName("TimeframeFilter_{m}");
|
||||
}
|
||||
|
||||
@@ -29,6 +29,8 @@ public class ExpressionObjectTests : ExpressionTestsBase
|
||||
Assert.That(paramExp.ParameterName, Is.EqualTo("MY_PARAM"));
|
||||
}
|
||||
|
||||
private static readonly string[] NotInValues = new[] { "value1", "value2", "value3" };
|
||||
|
||||
private static IEnumerable<TestCaseData> ComparisonExpressionTestCases()
|
||||
{
|
||||
yield return new TestCaseData("GreaterThanOrEqual", 250, typeof(GreaterThanOrEqualToExpression))
|
||||
@@ -40,7 +42,7 @@ public class ExpressionObjectTests : ExpressionTestsBase
|
||||
yield return new TestCaseData("Equals", "TestDept", typeof(EqualToExpression))
|
||||
.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}");
|
||||
|
||||
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.
|
||||
/// </summary>
|
||||
/// <param name="testCase">The test case to execute.</param>
|
||||
protected void ExecuteExpressionTest(ExpressionTestCase testCase)
|
||||
protected static void ExecuteExpressionTest(ExpressionTestCase testCase)
|
||||
{
|
||||
// Arrange
|
||||
var visitor = new CommandVisitor();
|
||||
|
||||
+3
-1
@@ -243,12 +243,14 @@ public class QueryBreakdownExtensionsTests
|
||||
Assert.That(query.WithClauses[1].TableName, Is.EqualTo("recent_orders"));
|
||||
}
|
||||
|
||||
private static readonly string[] columns = new[] { "id", "name", "email" };
|
||||
|
||||
[Test]
|
||||
public void WithCte_WithColumnList_AddsCtesWithColumns()
|
||||
{
|
||||
// Arrange & Act
|
||||
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")
|
||||
.From("users")
|
||||
.Where("status = 'active'"))
|
||||
|
||||
@@ -174,7 +174,7 @@ public class RecursiveCTETests
|
||||
var merged = mainQuery.GetMergedParameters();
|
||||
|
||||
// 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(merged[statusKey], Is.EqualTo("active"), "Main query parameter should take precedence");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user