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

Reviewed-on: #12
This commit was merged in pull request #12.
This commit is contained in:
2026-05-26 16:49:02 -05:00
45 changed files with 229 additions and 235 deletions
+64 -19
View File
@@ -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("\"", "&quot;")
@@ -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("\"", "&quot;")
@@ -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()
+1 -1
View File
@@ -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)
{
+1 -1
View File
@@ -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");
}
@@ -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();
@@ -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");
}