SonarQube Analysis / sonarqube (pull_request) Successful in 2m47s
Manual sweep of all 42 IDE0028 sites flagged by SonarQube — `dotnet format
analyzers --diagnostics IDE0028` declined to fix these (no .editorconfig
opt-in for `dotnet_style_prefer_collection_expression`), so applied by
hand. The repo already targets `<LangVersion>latest</LangVersion>` on
net8.0, so C# 12 collection expressions are available.
Pattern: `new List<T>()` / `new Dictionary<K,V>()` / `new ArrayList()` /
`new()` -> `[]` for empty; `new List<T> { ... }` -> `[...]` for literal.
24 files touched in src/{EFCore, LinqToSql, Query, Snowflake, SqlBreakdown,
SqlServer}; tests untouched (no IDE0028 sites in test code).
Build clean (35 warnings unchanged from baseline, 0 errors). All tests
remain green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
282 lines
9.8 KiB
C#
282 lines
9.8 KiB
C#
using Strata.SqlTools.Breakdowns.LinqToSql;
|
|
using Strata.SqlTools.Comparers.LinqToSql;
|
|
|
|
namespace Strata.SqlTools.Analyzers.LinqToSql;
|
|
|
|
/// <summary>
|
|
/// Statistics about a collection of analyzed queries.
|
|
/// </summary>
|
|
public record QueryCollectionStatistics(
|
|
int TotalQueries,
|
|
int UniqueQueries,
|
|
List<LinqQueryBreakdown> DuplicateQueries,
|
|
Dictionary<string, int> TableUsageFrequency,
|
|
Dictionary<string, int> ColumnSelectionFrequency,
|
|
int QueriesWithoutWhere,
|
|
int QueriesWithoutOrderBy,
|
|
int QueriesWithSelectAll,
|
|
double AverageComplexity,
|
|
int ComplexQueriesCount
|
|
)
|
|
{
|
|
/// <summary>
|
|
/// Gets the deduplication rate (unique queries / total queries).
|
|
/// </summary>
|
|
public double DeduplicationRate => TotalQueries == 0 ? 0.0 : (double)UniqueQueries / TotalQueries;
|
|
|
|
/// <summary>
|
|
/// Gets a formatted statistics report.
|
|
/// </summary>
|
|
public string GetReport()
|
|
{
|
|
var report = new System.Text.StringBuilder();
|
|
report.AppendLine("Query Collection Analysis Report");
|
|
report.AppendLine("================================");
|
|
report.AppendLine($"Total Queries: {TotalQueries}");
|
|
report.AppendLine($"Unique Queries: {UniqueQueries} ({DeduplicationRate * 100:F1}%)");
|
|
report.AppendLine($"Duplicate Queries: {DuplicateQueries.Count}");
|
|
report.AppendLine();
|
|
|
|
report.AppendLine("Query Characteristics:");
|
|
report.AppendLine($" Queries without WHERE: {QueriesWithoutWhere}");
|
|
report.AppendLine($" Queries without ORDER BY: {QueriesWithoutOrderBy}");
|
|
report.AppendLine($" Queries with SELECT *: {QueriesWithSelectAll}");
|
|
report.AppendLine();
|
|
|
|
report.AppendLine("Complexity Analysis:");
|
|
report.AppendLine($" Average Complexity Level: {AverageComplexity:F2}");
|
|
report.AppendLine($" Complex Queries: {ComplexQueriesCount}");
|
|
report.AppendLine();
|
|
|
|
if (TableUsageFrequency.Count > 0)
|
|
{
|
|
report.AppendLine("Most Frequently Used Tables:");
|
|
foreach (var kvp in TableUsageFrequency.OrderByDescending(x => x.Value).Take(5))
|
|
{
|
|
report.AppendLine($" {kvp.Key}: {kvp.Value} times");
|
|
}
|
|
report.AppendLine();
|
|
}
|
|
|
|
if (ColumnSelectionFrequency.Count > 0)
|
|
{
|
|
report.AppendLine("Most Frequently Selected Columns:");
|
|
foreach (var kvp in ColumnSelectionFrequency.OrderByDescending(x => x.Value).Take(5))
|
|
{
|
|
report.AppendLine($" {kvp.Key}: {kvp.Value} times");
|
|
}
|
|
}
|
|
|
|
return report.ToString();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Analyzes a collection of LinqQueryBreakdown queries for patterns, duplicates, and statistics.
|
|
/// </summary>
|
|
public class QueryCollectionAnalyzer
|
|
{
|
|
private readonly List<LinqQueryBreakdown> _queries;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="QueryCollectionAnalyzer"/> class.
|
|
/// </summary>
|
|
/// <param name="queries">The queries to analyze.</param>
|
|
public QueryCollectionAnalyzer(IEnumerable<LinqQueryBreakdown> queries)
|
|
{
|
|
_queries = queries?.ToList() ?? throw new ArgumentNullException(nameof(queries));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Analyzes the query collection and returns comprehensive statistics.
|
|
/// </summary>
|
|
/// <returns>Statistics about the query collection.</returns>
|
|
public QueryCollectionStatistics Analyze()
|
|
{
|
|
if (_queries.Count == 0)
|
|
{
|
|
return new QueryCollectionStatistics(
|
|
0, 0, [],
|
|
[],
|
|
[],
|
|
0, 0, 0, 0.0, 0);
|
|
}
|
|
|
|
var duplicates = FindDuplicates();
|
|
var uniqueCount = _queries.Count - duplicates.Count;
|
|
var tableUsage = AnalyzeTableUsage();
|
|
var columnUsage = AnalyzeColumnUsage();
|
|
var queriesWithoutWhere = _queries.Count(q => string.IsNullOrWhiteSpace(q.WhereClause?.Clause));
|
|
var queriesWithoutOrderBy = _queries.Count(q => string.IsNullOrWhiteSpace(q.OrderByClause?.Clause));
|
|
var queriesWithSelectAll = _queries.Count(q =>
|
|
q.SelectClause?.Clause?.Trim() == "*");
|
|
var complexityScores = _queries.Select(q => GetComplexityScore(q)).ToList();
|
|
var avgComplexity = complexityScores.Average();
|
|
var complexQueries = complexityScores.Count(c => c >= 7);
|
|
|
|
return new QueryCollectionStatistics(
|
|
_queries.Count,
|
|
uniqueCount,
|
|
duplicates,
|
|
tableUsage,
|
|
columnUsage,
|
|
queriesWithoutWhere,
|
|
queriesWithoutOrderBy,
|
|
queriesWithSelectAll,
|
|
avgComplexity,
|
|
complexQueries);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Finds duplicate queries in the collection.
|
|
/// </summary>
|
|
/// <returns>List of queries that are identical to another query in the collection.</returns>
|
|
public List<LinqQueryBreakdown> FindDuplicates()
|
|
{
|
|
var duplicates = new List<LinqQueryBreakdown>();
|
|
|
|
for (int i = 0; i < _queries.Count; i++)
|
|
{
|
|
for (int j = i + 1; j < _queries.Count; j++)
|
|
{
|
|
if (QueryComparator.AreQueriesIdentical(_queries[i], _queries[j]) && !duplicates.Contains(_queries[j]))
|
|
{
|
|
duplicates.Add(_queries[j]);
|
|
}
|
|
}
|
|
}
|
|
|
|
return duplicates;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Finds similar queries that are not identical but have high similarity.
|
|
/// </summary>
|
|
/// <param name="minimumSimilarity">Minimum similarity score (0.0-1.0).</param>
|
|
/// <returns>Pairs of similar queries and their similarity scores.</returns>
|
|
public List<(LinqQueryBreakdown Query1, LinqQueryBreakdown Query2, double Similarity)> FindSimilarQueries(double minimumSimilarity = 0.75)
|
|
{
|
|
var similarPairs = new List<(LinqQueryBreakdown, LinqQueryBreakdown, double)>();
|
|
|
|
for (int i = 0; i < _queries.Count; i++)
|
|
{
|
|
for (int j = i + 1; j < _queries.Count; j++)
|
|
{
|
|
var similarity = QueryComparator.GetSimilarity(_queries[i], _queries[j]);
|
|
if (similarity >= minimumSimilarity && similarity < 1.0)
|
|
{
|
|
similarPairs.Add((_queries[i], _queries[j], similarity));
|
|
}
|
|
}
|
|
}
|
|
|
|
return similarPairs.OrderByDescending(x => x.Item3).ToList();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Analyzes table usage frequency across all queries.
|
|
/// </summary>
|
|
/// <returns>Dictionary of table names and their usage counts.</returns>
|
|
private Dictionary<string, int> AnalyzeTableUsage()
|
|
{
|
|
var tableUsage = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
foreach (var query in _queries)
|
|
{
|
|
var table = query.FromClause?.Clause?.Trim();
|
|
if (!string.IsNullOrWhiteSpace(table))
|
|
{
|
|
if (tableUsage.TryGetValue(table, out int value))
|
|
{
|
|
tableUsage[table] = ++value;
|
|
}
|
|
else
|
|
{
|
|
tableUsage[table] = 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
return tableUsage;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Analyzes column selection frequency across all queries.
|
|
/// </summary>
|
|
/// <returns>Dictionary of column names and their selection frequency.</returns>
|
|
private Dictionary<string, int> AnalyzeColumnUsage()
|
|
{
|
|
var columnUsage = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
foreach (var query in _queries)
|
|
{
|
|
var selectClause = query.SelectClause?.Clause;
|
|
if (string.IsNullOrWhiteSpace(selectClause) || selectClause.Trim() == "*")
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Split columns and count them
|
|
var columns = selectClause.Split(',');
|
|
foreach (var col in columns)
|
|
{
|
|
var columnName = col.Trim();
|
|
if (columnUsage.TryGetValue(columnName, out int value))
|
|
{
|
|
columnUsage[columnName] = ++value;
|
|
}
|
|
else
|
|
{
|
|
columnUsage[columnName] = 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
return columnUsage;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calculates a complexity score for a query (0-10).
|
|
/// </summary>
|
|
private static int GetComplexityScore(LinqQueryBreakdown query)
|
|
{
|
|
int score = 1; // Base score for having a query
|
|
|
|
if (!string.IsNullOrWhiteSpace(query.WhereClause?.Clause))
|
|
{ score += 2; }
|
|
if (!string.IsNullOrWhiteSpace(query.GroupByClause?.Clause))
|
|
{ score += 2; }
|
|
if (!string.IsNullOrWhiteSpace(query.HavingClause?.Clause))
|
|
{ score += 2; }
|
|
if (!string.IsNullOrWhiteSpace(query.OrderByClause?.Clause))
|
|
{ score += 1; }
|
|
|
|
// Bonus points for complex WHERE conditions
|
|
var whereClause = query.WhereClause?.Clause ?? string.Empty;
|
|
var complexityIndicators = new[] { " AND ", " OR ", "IN (", "BETWEEN", "LIKE" };
|
|
var complexParts = complexityIndicators.Count(ind => whereClause.Contains(ind, StringComparison.OrdinalIgnoreCase));
|
|
score += Math.Min(complexParts, 2); // Cap at +2
|
|
|
|
return Math.Min(score, 10); // Cap at 10
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a new analyzer for the given queries.
|
|
/// </summary>
|
|
/// <param name="queries">The queries to analyze.</param>
|
|
/// <returns>A new QueryCollectionAnalyzer instance.</returns>
|
|
public static QueryCollectionAnalyzer Analyze(IEnumerable<LinqQueryBreakdown> queries)
|
|
{
|
|
return new QueryCollectionAnalyzer(queries);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a summary report for the query collection.
|
|
/// </summary>
|
|
/// <returns>A formatted analysis report.</returns>
|
|
public string GetReport()
|
|
{
|
|
return Analyze().GetReport();
|
|
}
|
|
}
|