using Strata.SqlTools.Breakdowns.LinqToSql;
namespace Strata.SqlTools.Comparers.LinqToSql;
///
/// Result of comparing two LinqQueryBreakdown instances.
///
public record QueryComparisonResult(
bool AreEquivalent,
double SimilarityScore, // 0.0 to 1.0
List Differences,
bool HaveSameSelectColumns,
bool HaveSameFromTable,
bool HaveSameWhereClause,
bool HaveSameGroupBy,
bool HaveSameHaving,
bool HaveSameOrderBy
)
{
///
/// Gets a formatted comparison report.
///
public string GetReport()
{
var report = new System.Text.StringBuilder();
report.AppendLine($"Query Comparison Report");
report.AppendLine($"Similarity: {(SimilarityScore * 100):F1}%");
report.AppendLine($"Equivalent: {(AreEquivalent ? "Yes" : "No")}");
report.AppendLine();
if (Differences.Count == 0)
{
report.AppendLine("✓ Queries are identical");
return report.ToString();
}
report.AppendLine("Differences:");
foreach (var diff in Differences)
{
report.AppendLine($" • {diff}");
}
return report.ToString();
}
}
///
/// Compares LinqQueryBreakdown instances to detect similarity, equivalence, and duplicates.
///
public class QueryComparator
{
private readonly LinqQueryBreakdown _query1;
private readonly LinqQueryBreakdown _query2;
private QueryComparisonResult? _result;
///
/// Initializes a new instance of the class.
///
/// The first query to compare.
/// The second query to compare.
public QueryComparator(LinqQueryBreakdown query1, LinqQueryBreakdown query2)
{
_query1 = query1 ?? throw new ArgumentNullException(nameof(query1));
_query2 = query2 ?? throw new ArgumentNullException(nameof(query2));
}
///
/// Gets the comparison result, calculating it if needed.
///
public QueryComparisonResult Result =>
_result ??= PerformComparison();
///
/// Gets a value indicating whether the queries are equivalent (same structure).
///
public bool AreEquivalent => Result.AreEquivalent;
///
/// Gets the similarity score from 0.0 (completely different) to 1.0 (identical).
///
public double SimilarityScore => Result.SimilarityScore;
///
/// Gets the list of differences found between the queries.
///
public List Differences => Result.Differences;
///
/// Performs the actual comparison between the two queries.
///
/// The comparison result.
private QueryComparisonResult PerformComparison()
{
var differences = new List();
var scoreComponents = 0;
var scoreMatches = 0;
// Compare SELECT clause
var selectMatch = CompareSelectClauses(_query1, _query2);
if (!selectMatch)
{
differences.Add("SELECT clauses differ");
}
scoreComponents++;
if (selectMatch)
{ scoreMatches++; }
// Compare FROM clause
var fromMatch = CompareFromClauses(_query1, _query2);
if (!fromMatch)
{
differences.Add("FROM clauses differ");
}
scoreComponents++;
if (fromMatch)
{ scoreMatches++; }
// Compare WHERE clause
var whereMatch = CompareWhereClauses(_query1, _query2);
if (!whereMatch)
{
differences.Add("WHERE clauses differ");
}
scoreComponents++;
if (whereMatch)
{ scoreMatches++; }
// Compare GROUP BY clause
var groupByMatch = CompareGroupByClauses(_query1, _query2);
if (!groupByMatch)
{
differences.Add("GROUP BY clauses differ");
}
scoreComponents++;
if (groupByMatch)
{ scoreMatches++; }
// Compare HAVING clause
var havingMatch = CompareHavingClauses(_query1, _query2);
if (!havingMatch)
{
differences.Add("HAVING clauses differ");
}
scoreComponents++;
if (havingMatch)
{ scoreMatches++; }
// Compare ORDER BY clause
var orderByMatch = CompareOrderByClauses(_query1, _query2);
if (!orderByMatch)
{
differences.Add("ORDER BY clauses differ");
}
scoreComponents++;
if (orderByMatch)
{ scoreMatches++; }
var similarityScore = scoreComponents > 0 ? (double)scoreMatches / scoreComponents : 0.0;
var areEquivalent = differences.Count == 0;
return new QueryComparisonResult(
areEquivalent,
similarityScore,
differences,
selectMatch,
fromMatch,
whereMatch,
groupByMatch,
havingMatch,
orderByMatch);
}
private static bool CompareSelectClauses(LinqQueryBreakdown query1, LinqQueryBreakdown query2)
{
var select1 = NormalizeClause(query1.SelectClause?.Clause ?? string.Empty);
var select2 = NormalizeClause(query2.SelectClause?.Clause ?? string.Empty);
return StringEquals(select1, select2);
}
private static bool CompareFromClauses(LinqQueryBreakdown query1, LinqQueryBreakdown query2)
{
var from1 = NormalizeClause(query1.FromClause?.Clause ?? string.Empty);
var from2 = NormalizeClause(query2.FromClause?.Clause ?? string.Empty);
return StringEquals(from1, from2);
}
private static bool CompareWhereClauses(LinqQueryBreakdown query1, LinqQueryBreakdown query2)
{
var where1 = NormalizeClause(query1.WhereClause?.Clause ?? string.Empty);
var where2 = NormalizeClause(query2.WhereClause?.Clause ?? string.Empty);
return StringEquals(where1, where2);
}
private static bool CompareGroupByClauses(LinqQueryBreakdown query1, LinqQueryBreakdown query2)
{
var groupBy1 = NormalizeClause(query1.GroupByClause?.Clause ?? string.Empty);
var groupBy2 = NormalizeClause(query2.GroupByClause?.Clause ?? string.Empty);
return StringEquals(groupBy1, groupBy2);
}
private static bool CompareHavingClauses(LinqQueryBreakdown query1, LinqQueryBreakdown query2)
{
var having1 = NormalizeClause(query1.HavingClause?.Clause ?? string.Empty);
var having2 = NormalizeClause(query2.HavingClause?.Clause ?? string.Empty);
return StringEquals(having1, having2);
}
private static bool CompareOrderByClauses(LinqQueryBreakdown query1, LinqQueryBreakdown query2)
{
var orderBy1 = NormalizeClause(query1.OrderByClause?.Clause ?? string.Empty);
var orderBy2 = NormalizeClause(query2.OrderByClause?.Clause ?? string.Empty);
return StringEquals(orderBy1, orderBy2);
}
private static string NormalizeClause(string clause)
{
if (string.IsNullOrWhiteSpace(clause))
{
return string.Empty;
}
// Normalize whitespace and case
return System.Text.RegularExpressions.Regex
.Replace(clause.Trim(), @"\s+", " ", System.Text.RegularExpressions.RegexOptions.None, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout)
.ToUpperInvariant();
}
private static bool StringEquals(string? str1, string? str2)
{
return string.Equals(str1, str2, StringComparison.Ordinal);
}
///
/// Creates a new comparator for two queries.
///
/// The first query.
/// The second query.
/// A new QueryComparator instance.
public static QueryComparator Compare(LinqQueryBreakdown query1, LinqQueryBreakdown query2)
{
return new QueryComparator(query1, query2);
}
///
/// Checks if two queries are equivalent.
///
/// The first query.
/// The second query.
/// True if the queries are equivalent; otherwise, false.
public static bool AreQueriesEquivalent(LinqQueryBreakdown query1, LinqQueryBreakdown query2)
{
return new QueryComparator(query1, query2).AreEquivalent;
}
///
/// Checks if two queries are identical (same text after normalization).
///
/// The first query.
/// The second query.
/// True if the queries are identical; otherwise, false.
public static bool AreQueriesIdentical(LinqQueryBreakdown query1, LinqQueryBreakdown query2)
{
var comparator = new QueryComparator(query1, query2);
return comparator.SimilarityScore >= 1.0;
}
///
/// Gets the similarity score between two queries (0.0 to 1.0).
///
/// The first query.
/// The second query.
/// A similarity score from 0.0 (completely different) to 1.0 (identical).
public static double GetSimilarity(LinqQueryBreakdown query1, LinqQueryBreakdown query2)
{
return new QueryComparator(query1, query2).SimilarityScore;
}
}