chore: initial git load of code space
This commit is contained in:
@@ -0,0 +1,311 @@
|
||||
using System.Collections.Immutable;
|
||||
using Strata.SqlTools.Breakdowns.LinqToSql;
|
||||
|
||||
namespace Strata.SqlTools.Validators.LinqToSql;
|
||||
|
||||
/// <summary>
|
||||
/// Severity level for validation issues.
|
||||
/// </summary>
|
||||
public enum ValidationSeverity
|
||||
{
|
||||
/// <summary>Informational message, no action required.</summary>
|
||||
Info = 0,
|
||||
|
||||
/// <summary>Warning - potential issue that should be reviewed.</summary>
|
||||
Warning = 1,
|
||||
|
||||
/// <summary>Error - definite issue that should be fixed.</summary>
|
||||
Error = 2
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a single validation issue found in a query.
|
||||
/// </summary>
|
||||
public record QueryValidationIssue(
|
||||
ValidationSeverity Severity,
|
||||
string Code,
|
||||
string Message,
|
||||
string? Details = null
|
||||
)
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a formatted string representation of the validation issue.
|
||||
/// </summary>
|
||||
public override string ToString()
|
||||
{
|
||||
var result = $"[{Severity}] {Code}: {Message}";
|
||||
if (!string.IsNullOrWhiteSpace(Details))
|
||||
{
|
||||
result += $" - {Details}";
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates LinqQueryBreakdown instances and detects common anti-patterns.
|
||||
/// </summary>
|
||||
public class QueryValidator
|
||||
{
|
||||
private readonly List<QueryValidationIssue> _issues = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of validation issues found.
|
||||
/// </summary>
|
||||
public IReadOnlyList<QueryValidationIssue> Issues => _issues.AsReadOnly();
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether any errors were found.
|
||||
/// </summary>
|
||||
public bool HasErrors => _issues.Any(i => i.Severity == ValidationSeverity.Error);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether any warnings were found.
|
||||
/// </summary>
|
||||
public bool HasWarnings => _issues.Any(i => i.Severity == ValidationSeverity.Warning);
|
||||
|
||||
/// <summary>
|
||||
/// Validates a LinqQueryBreakdown instance and returns the result.
|
||||
/// </summary>
|
||||
/// <param name="breakdown">The breakdown to validate.</param>
|
||||
/// <returns>This validator for method chaining.</returns>
|
||||
public QueryValidator Validate(LinqQueryBreakdown breakdown)
|
||||
{
|
||||
if (breakdown == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(breakdown));
|
||||
}
|
||||
|
||||
_issues.Clear();
|
||||
|
||||
ValidateSelectClause(breakdown);
|
||||
ValidateFromClause(breakdown);
|
||||
ValidateWhereClause(breakdown);
|
||||
ValidateGroupByClause(breakdown);
|
||||
ValidateHavingClause(breakdown);
|
||||
ValidateOrderByClause(breakdown);
|
||||
ValidateCommonAntiPatterns(breakdown);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a custom validation issue.
|
||||
/// </summary>
|
||||
/// <param name="severity">The severity level.</param>
|
||||
/// <param name="code">The issue code (e.g., "RULE_001").</param>
|
||||
/// <param name="message">The issue message.</param>
|
||||
/// <param name="details">Optional detailed information.</param>
|
||||
/// <returns>This validator for method chaining.</returns>
|
||||
public QueryValidator AddIssue(
|
||||
ValidationSeverity severity,
|
||||
string code,
|
||||
string message,
|
||||
string? details = null)
|
||||
{
|
||||
_issues.Add(new QueryValidationIssue(severity, code, message, details));
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears all validation issues.
|
||||
/// </summary>
|
||||
/// <returns>This validator for method chaining.</returns>
|
||||
public QueryValidator Clear()
|
||||
{
|
||||
_issues.Clear();
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets validation issues by severity level.
|
||||
/// </summary>
|
||||
/// <param name="severity">The severity to filter by.</param>
|
||||
/// <returns>Issues matching the severity level.</returns>
|
||||
public IReadOnlyList<QueryValidationIssue> GetIssuesBySeverity(ValidationSeverity severity)
|
||||
{
|
||||
return _issues.Where(i => i.Severity == severity).ToList().AsReadOnly();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a formatted validation report.
|
||||
/// </summary>
|
||||
/// <returns>A formatted string containing all validation issues.</returns>
|
||||
public string GetReport()
|
||||
{
|
||||
if (_issues.Count == 0)
|
||||
{
|
||||
return "✓ No validation issues found.";
|
||||
}
|
||||
|
||||
var report = new System.Text.StringBuilder();
|
||||
report.AppendLine($"Validation Report ({_issues.Count} issue{(_issues.Count != 1 ? "s" : "")}:");
|
||||
report.AppendLine();
|
||||
|
||||
var errors = GetIssuesBySeverity(ValidationSeverity.Error);
|
||||
if (errors.Count > 0)
|
||||
{
|
||||
report.AppendLine("ERRORS:");
|
||||
foreach (var issue in errors)
|
||||
{
|
||||
report.AppendLine($" • {issue}");
|
||||
}
|
||||
report.AppendLine();
|
||||
}
|
||||
|
||||
var warnings = GetIssuesBySeverity(ValidationSeverity.Warning);
|
||||
if (warnings.Count > 0)
|
||||
{
|
||||
report.AppendLine("WARNINGS:");
|
||||
foreach (var issue in warnings)
|
||||
{
|
||||
report.AppendLine($" • {issue}");
|
||||
}
|
||||
report.AppendLine();
|
||||
}
|
||||
|
||||
var infos = GetIssuesBySeverity(ValidationSeverity.Info);
|
||||
if (infos.Count > 0)
|
||||
{
|
||||
report.AppendLine("INFO:");
|
||||
foreach (var issue in infos)
|
||||
{
|
||||
report.AppendLine($" • {issue}");
|
||||
}
|
||||
}
|
||||
|
||||
return report.ToString();
|
||||
}
|
||||
|
||||
private void ValidateSelectClause(LinqQueryBreakdown breakdown)
|
||||
{
|
||||
if (breakdown.SelectClause == null || string.IsNullOrWhiteSpace(breakdown.SelectClause.Clause))
|
||||
{
|
||||
_issues.Add(new QueryValidationIssue(
|
||||
ValidationSeverity.Error,
|
||||
"SELECT_MISSING",
|
||||
"SELECT clause is missing or empty",
|
||||
"Every query must specify which columns to select."));
|
||||
return;
|
||||
}
|
||||
|
||||
var selectClause = breakdown.SelectClause.Clause;
|
||||
|
||||
// Check for SELECT *
|
||||
if (selectClause.Trim() == "*")
|
||||
{
|
||||
_issues.Add(new QueryValidationIssue(
|
||||
ValidationSeverity.Warning,
|
||||
"SELECT_ALL_COLUMNS",
|
||||
"Query selects all columns with SELECT *",
|
||||
"Consider being explicit about which columns you need to avoid returning unnecessary data."));
|
||||
}
|
||||
|
||||
// Check for excessive columns
|
||||
var columnCount = selectClause.Split(',').Length;
|
||||
if (columnCount > 20)
|
||||
{
|
||||
_issues.Add(new QueryValidationIssue(
|
||||
ValidationSeverity.Warning,
|
||||
"SELECT_TOO_MANY",
|
||||
$"Query selects {columnCount} columns",
|
||||
"Consider narrowing the selection to reduce data transfer and improve performance."));
|
||||
}
|
||||
}
|
||||
|
||||
private void ValidateFromClause(LinqQueryBreakdown breakdown)
|
||||
{
|
||||
if (breakdown.FromClause == null || string.IsNullOrWhiteSpace(breakdown.FromClause.Clause))
|
||||
{
|
||||
_issues.Add(new QueryValidationIssue(
|
||||
ValidationSeverity.Error,
|
||||
"FROM_MISSING",
|
||||
"FROM clause is missing",
|
||||
"Every query must specify a source table."));
|
||||
}
|
||||
}
|
||||
|
||||
private void ValidateWhereClause(LinqQueryBreakdown breakdown)
|
||||
{
|
||||
// No validation needed - WHERE is optional
|
||||
}
|
||||
|
||||
private void ValidateGroupByClause(LinqQueryBreakdown breakdown)
|
||||
{
|
||||
var hasGroupBy = !string.IsNullOrWhiteSpace(breakdown.GroupByClause?.Clause);
|
||||
var hasHaving = !string.IsNullOrWhiteSpace(breakdown.HavingClause?.Clause);
|
||||
|
||||
if (hasHaving && !hasGroupBy)
|
||||
{
|
||||
_issues.Add(new QueryValidationIssue(
|
||||
ValidationSeverity.Error,
|
||||
"HAVING_WITHOUT_GROUPBY",
|
||||
"HAVING clause found without GROUP BY",
|
||||
"HAVING must be used with GROUP BY to filter aggregated results."));
|
||||
}
|
||||
}
|
||||
|
||||
private void ValidateHavingClause(LinqQueryBreakdown breakdown)
|
||||
{
|
||||
// Validation delegated to ValidateGroupByClause
|
||||
}
|
||||
|
||||
private void ValidateOrderByClause(LinqQueryBreakdown breakdown)
|
||||
{
|
||||
// No validation needed - ORDER BY is optional
|
||||
}
|
||||
|
||||
private void ValidateCommonAntiPatterns(LinqQueryBreakdown breakdown)
|
||||
{
|
||||
// Check for DELETE/UPDATE without WHERE (dangerous!)
|
||||
// Note: This is primarily for LINQ operations, but we can flag it for awareness
|
||||
if (string.IsNullOrWhiteSpace(breakdown.WhereClause?.Clause))
|
||||
{
|
||||
_issues.Add(new QueryValidationIssue(
|
||||
ValidationSeverity.Warning,
|
||||
"NO_WHERE_CLAUSE",
|
||||
"Query has no WHERE clause",
|
||||
"Consider whether this is intentional. Queries without WHERE clauses affect all rows."));
|
||||
}
|
||||
|
||||
// Check for missing ORDER BY on large results
|
||||
var hasOrderBy = !string.IsNullOrWhiteSpace(breakdown.OrderByClause?.Clause);
|
||||
var hasGroupBy = !string.IsNullOrWhiteSpace(breakdown.GroupByClause?.Clause);
|
||||
|
||||
if (!hasOrderBy && !hasGroupBy)
|
||||
{
|
||||
_issues.Add(new QueryValidationIssue(
|
||||
ValidationSeverity.Info,
|
||||
"NO_ORDER_BY",
|
||||
"Query has no ORDER BY clause",
|
||||
"Consider adding ORDER BY to ensure consistent result ordering, especially for pagination scenarios."));
|
||||
}
|
||||
|
||||
// Check for SELECT without FROM (invalid in most SQL dialects except for SELECT constants)
|
||||
var selectClause = breakdown.SelectClause?.Clause ?? string.Empty;
|
||||
if (!string.IsNullOrWhiteSpace(selectClause) &&
|
||||
string.IsNullOrWhiteSpace(breakdown.FromClause?.Clause))
|
||||
{
|
||||
// This is already caught by ValidateFromClause
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of QueryValidator.
|
||||
/// </summary>
|
||||
/// <returns>A new QueryValidator instance.</returns>
|
||||
public static QueryValidator Create()
|
||||
{
|
||||
return new QueryValidator();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates a breakdown and returns a new validator with the results.
|
||||
/// </summary>
|
||||
/// <param name="breakdown">The breakdown to validate.</param>
|
||||
/// <returns>A new validator containing the validation results.</returns>
|
||||
public static QueryValidator ValidateQuery(LinqQueryBreakdown breakdown)
|
||||
{
|
||||
return new QueryValidator().Validate(breakdown);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user