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; } }