chore: initial git load of code space
This commit is contained in:
@@ -0,0 +1,281 @@
|
||||
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, new List<LinqQueryBreakdown>(),
|
||||
new Dictionary<string, int>(),
|
||||
new Dictionary<string, int>(),
|
||||
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.ContainsKey(table))
|
||||
{
|
||||
tableUsage[table]++;
|
||||
}
|
||||
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.ContainsKey(columnName))
|
||||
{
|
||||
columnUsage[columnName]++;
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,675 @@
|
||||
using System.Linq.Expressions;
|
||||
using Strata.SqlTools.Breakdowns.SqlServer;
|
||||
using PostgreSqlBreakdown = Strata.SqlTools.Breakdowns.PostgreSql.QueryBreakdown;
|
||||
using SnowflakeBreakdown = Strata.SqlTools.Breakdowns.Snowflake.QueryBreakdown;
|
||||
|
||||
namespace Strata.SqlTools.Breakdowns.LinqToSql;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a LINQ to SQL query breakdown, analyzing IQueryable expressions
|
||||
/// and converting them to SQL Server QueryBreakdown format.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This class analyzes LINQ expression trees to extract query components such as
|
||||
/// SELECT, WHERE, JOIN, GROUP BY, and ORDER BY clauses, making them accessible
|
||||
/// through the QueryBreakdown interface.
|
||||
/// </remarks>
|
||||
[Serializable]
|
||||
public class LinqQueryBreakdown : QueryBreakdown
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the original LINQ expression that was analyzed.
|
||||
/// </summary>
|
||||
public Expression? OriginalExpression { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the type of the entity being queried.
|
||||
/// </summary>
|
||||
public Type? EntityType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether this query uses LINQ method syntax.
|
||||
/// </summary>
|
||||
public bool IsMethodSyntax { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of LINQ method calls in the query chain.
|
||||
/// </summary>
|
||||
public List<string> MethodCallChain { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LinqQueryBreakdown"/> class.
|
||||
/// </summary>
|
||||
public LinqQueryBreakdown() : base()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LinqQueryBreakdown"/> class with SELECT and FROM clauses.
|
||||
/// </summary>
|
||||
/// <param name="selectClause">The SELECT clause.</param>
|
||||
/// <param name="fromClause">The FROM clause (table name or data source).</param>
|
||||
public LinqQueryBreakdown(string selectClause, string fromClause) : base(selectClause, fromClause)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LinqQueryBreakdown"/> class with SELECT, FROM, and WHERE clauses.
|
||||
/// </summary>
|
||||
/// <param name="selectClause">The SELECT clause.</param>
|
||||
/// <param name="fromClause">The FROM clause (table name or data source).</param>
|
||||
/// <param name="whereClause">The WHERE clause.</param>
|
||||
public LinqQueryBreakdown(string selectClause, string fromClause, string whereClause)
|
||||
: base(selectClause, fromClause, whereClause)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Analyzes an IQueryable LINQ query and creates a LinqQueryBreakdown.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The entity type being queried.</typeparam>
|
||||
/// <param name="query">The IQueryable query to analyze.</param>
|
||||
/// <returns>A LinqQueryBreakdown representing the query structure.</returns>
|
||||
public static LinqQueryBreakdown Analyze<T>(IQueryable<T> query)
|
||||
{
|
||||
if (query == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(query));
|
||||
}
|
||||
|
||||
var breakdown = new LinqQueryBreakdown
|
||||
{
|
||||
OriginalExpression = query.Expression,
|
||||
EntityType = typeof(T)
|
||||
};
|
||||
|
||||
var visitor = new Visitors.LinqToSql.LinqExpressionVisitor();
|
||||
visitor.Visit(query.Expression);
|
||||
|
||||
// Extract components from visitor
|
||||
breakdown.SelectClause.Clause = visitor.SelectClause ?? "*";
|
||||
breakdown.FromClause.Clause = visitor.FromClause ?? typeof(T).Name;
|
||||
|
||||
if (!string.IsNullOrEmpty(visitor.WhereClause))
|
||||
{
|
||||
breakdown.WhereClause.Clause = visitor.WhereClause;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(visitor.OrderByClause))
|
||||
{
|
||||
breakdown.OrderByClause.Clause = visitor.OrderByClause;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(visitor.GroupByClause))
|
||||
{
|
||||
breakdown.GroupByClause.Clause = visitor.GroupByClause;
|
||||
}
|
||||
|
||||
breakdown.MethodCallChain = visitor.MethodCallChain;
|
||||
|
||||
return breakdown;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to analyze an IQueryable LINQ query and create a LinqQueryBreakdown.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The entity type being queried.</typeparam>
|
||||
/// <param name="query">The IQueryable query to analyze.</param>
|
||||
/// <param name="result">The resulting LinqQueryBreakdown if successful.</param>
|
||||
/// <param name="errorMessage">Error message if analysis fails.</param>
|
||||
/// <returns>True if analysis succeeded; otherwise, false.</returns>
|
||||
public static bool TryAnalyze<T>(IQueryable<T> query, out LinqQueryBreakdown result, out string errorMessage)
|
||||
{
|
||||
result = new LinqQueryBreakdown();
|
||||
errorMessage = string.Empty;
|
||||
|
||||
try
|
||||
{
|
||||
result = Analyze(query);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errorMessage = ex.Message;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a summary of the LINQ query structure.
|
||||
/// </summary>
|
||||
/// <returns>A string describing the query composition.</returns>
|
||||
public string GetQuerySummary()
|
||||
{
|
||||
var parts = new List<string>();
|
||||
|
||||
if (!string.IsNullOrEmpty(SelectClause?.Clause))
|
||||
{
|
||||
parts.Add($"SELECT {SelectClause.Clause}");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(FromClause?.Clause))
|
||||
{
|
||||
parts.Add($"FROM {FromClause.Clause}");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(WhereClause?.Clause))
|
||||
{
|
||||
parts.Add($"WHERE {WhereClause.Clause}");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(GroupByClause?.Clause))
|
||||
{
|
||||
parts.Add($"GROUP BY {GroupByClause.Clause}");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(OrderByClause?.Clause))
|
||||
{
|
||||
parts.Add($"ORDER BY {OrderByClause.Clause}");
|
||||
}
|
||||
|
||||
return string.Join(" ", parts);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the LINQ method call chain as a string.
|
||||
/// </summary>
|
||||
/// <returns>A string representing the method chain.</returns>
|
||||
public string GetMethodChain()
|
||||
{
|
||||
if (MethodCallChain.Count == 0)
|
||||
{
|
||||
return "No method calls";
|
||||
}
|
||||
|
||||
return string.Join(" -> ", MethodCallChain);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a LINQ to SQL query of the specified type based on this breakdown.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The entity type for the query.</typeparam>
|
||||
/// <returns>An IQueryable of the specified type reconstructed from the breakdown, or null if the type doesn't match the original entity type.</returns>
|
||||
/// <remarks>
|
||||
/// This method attempts to reconstruct a LINQ query from the analyzed components (WHERE, ORDER BY, etc.).
|
||||
/// If a data source (IQueryable) is available in the breakdown's OriginalExpression, it will be used.
|
||||
/// Otherwise, returns null to indicate the query cannot be reconstructed without the original data source.
|
||||
/// </remarks>
|
||||
public override IQueryable<T>? GetQuery<T>() where T : class
|
||||
{
|
||||
// If we don't have the original expression, we cannot reconstruct the LINQ query
|
||||
if (OriginalExpression == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// The original expression is the full LINQ query that was analyzed
|
||||
// To use it, we need it to be an IQueryable<T>
|
||||
try
|
||||
{
|
||||
// If the original expression can be converted to IQueryable<T>, use it
|
||||
// Otherwise, we cannot safely reconstruct without the original query provider
|
||||
if (OriginalExpression is Expression expr && EntityType == typeof(T))
|
||||
{
|
||||
// We have the expression, but we don't have the provider to create IQueryable<T>
|
||||
// The breakdown analysis is one-way; reconstruction requires the original provider
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// If any error occurs during reconstruction, return null
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Analyzes an INSERT operation for the given entity.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The entity type being inserted.</typeparam>
|
||||
/// <param name="entity">The entity instance being inserted.</param>
|
||||
/// <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));
|
||||
}
|
||||
|
||||
var breakdown = new Breakdowns.SqlServer.InsertBreakdown();
|
||||
breakdown.TableName.Clause = typeof(T).Name;
|
||||
|
||||
// Extract property names and values from entity
|
||||
var properties = typeof(T).GetProperties();
|
||||
var columnNames = new List<string>();
|
||||
var valuesList = new List<string>();
|
||||
|
||||
foreach (var prop in properties)
|
||||
{
|
||||
var value = prop.GetValue(entity);
|
||||
columnNames.Add(prop.Name);
|
||||
valuesList.Add(value?.ToString() ?? "NULL");
|
||||
}
|
||||
|
||||
breakdown.InsertIntoClause.Clause = string.Join(", ", columnNames);
|
||||
breakdown.ValuesClause.Clause = string.Join(", ", valuesList);
|
||||
|
||||
return breakdown;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Analyzes an INSERT operation for multiple entities.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The entity type being inserted.</typeparam>
|
||||
/// <param name="entities">The entities being inserted.</param>
|
||||
/// <returns>An InsertBreakdown representing the bulk insert operation.</returns>
|
||||
public static Breakdowns.SqlServer.InsertBreakdown AnalyzeInsertRange<T>(IEnumerable<T> entities) where T : class
|
||||
{
|
||||
var entitiesList = entities?.ToList() ?? new List<T>();
|
||||
if (entitiesList.Count == 0)
|
||||
{
|
||||
throw new ArgumentException("Must provide at least one entity to insert.", nameof(entities));
|
||||
}
|
||||
|
||||
var breakdown = new Breakdowns.SqlServer.InsertBreakdown();
|
||||
breakdown.TableName.Clause = typeof(T).Name;
|
||||
|
||||
// Use first entity to get column names
|
||||
var firstEntity = entitiesList.First();
|
||||
var properties = typeof(T).GetProperties();
|
||||
var columnNames = new List<string>();
|
||||
|
||||
foreach (var prop in properties)
|
||||
{
|
||||
columnNames.Add(prop.Name);
|
||||
}
|
||||
|
||||
breakdown.InsertIntoClause.Clause = string.Join(", ", columnNames);
|
||||
|
||||
// Add values for each entity
|
||||
var allValues = new List<string>();
|
||||
foreach (var entity in entitiesList)
|
||||
{
|
||||
var rowValues = new List<string>();
|
||||
foreach (var prop in properties)
|
||||
{
|
||||
var value = prop.GetValue(entity);
|
||||
rowValues.Add(value?.ToString() ?? "NULL");
|
||||
}
|
||||
allValues.Add($"({string.Join(", ", rowValues)})");
|
||||
}
|
||||
|
||||
breakdown.ValuesClause.Clause = string.Join(", ", allValues);
|
||||
|
||||
return breakdown;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Analyzes a DELETE operation based on a filter expression.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The entity type being deleted.</typeparam>
|
||||
/// <param name="filterExpression">The filter expression defining which entities to delete.</param>
|
||||
/// <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));
|
||||
}
|
||||
|
||||
var breakdown = new Breakdowns.SqlServer.DeleteBreakdown();
|
||||
breakdown.FromClause.Clause = typeof(T).Name;
|
||||
|
||||
// Analyze the filter expression to extract WHERE clause
|
||||
var visitor = new Visitors.LinqToSql.LinqExpressionVisitor();
|
||||
visitor.Visit(filterExpression);
|
||||
|
||||
if (!string.IsNullOrEmpty(visitor.WhereClause))
|
||||
{
|
||||
breakdown.WhereClause.Clause = visitor.WhereClause;
|
||||
}
|
||||
|
||||
return breakdown;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Analyzes an UPDATE operation based on filter and update expressions.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The entity type being updated.</typeparam>
|
||||
/// <param name="filterExpression">The filter expression defining which entities to update.</param>
|
||||
/// <param name="updateExpression">The update expression defining what to update.</param>
|
||||
/// <returns>An UpdateBreakdown representing the update operation.</returns>
|
||||
public static Breakdowns.SqlServer.UpdateBreakdown AnalyzeUpdate<T>(
|
||||
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));
|
||||
}
|
||||
|
||||
var breakdown = new Breakdowns.SqlServer.UpdateBreakdown();
|
||||
breakdown.TableName.Clause = typeof(T).Name;
|
||||
|
||||
// Analyze filter expression for WHERE clause
|
||||
var filterVisitor = new Visitors.LinqToSql.LinqExpressionVisitor();
|
||||
filterVisitor.Visit(filterExpression);
|
||||
|
||||
if (!string.IsNullOrEmpty(filterVisitor.WhereClause))
|
||||
{
|
||||
breakdown.WhereClause.Clause = filterVisitor.WhereClause;
|
||||
}
|
||||
|
||||
// For the SET clause, we collect property assignments
|
||||
var setClauseParts = new List<string>();
|
||||
if (updateExpression.Body is System.Linq.Expressions.NewExpression newExpr)
|
||||
{
|
||||
for (int i = 0; i < newExpr.Arguments.Count; i++)
|
||||
{
|
||||
var arg = newExpr.Arguments[i];
|
||||
var member = newExpr.Members?[i];
|
||||
if (member != null)
|
||||
{
|
||||
setClauseParts.Add($"{member.Name} = {arg}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (setClauseParts.Count > 0)
|
||||
{
|
||||
breakdown.SetClause.Clause = string.Join(", ", setClauseParts);
|
||||
}
|
||||
|
||||
return breakdown;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Analyzes a procedure call breakdown.
|
||||
/// </summary>
|
||||
/// <param name="procedureName">The name of the stored procedure.</param>
|
||||
/// <param name="parameters">The procedure parameters.</param>
|
||||
/// <returns>A ProcedureBreakdown representing the procedure call.</returns>
|
||||
public static Breakdowns.SqlServer.ProcedureBreakdown AnalyzeProcedure(string procedureName, params object[] parameters)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(procedureName))
|
||||
{
|
||||
throw new ArgumentException("Procedure name cannot be null or empty.", nameof(procedureName));
|
||||
}
|
||||
|
||||
var breakdown = new Breakdowns.SqlServer.ProcedureBreakdown();
|
||||
breakdown.ProcedureName.Clause = procedureName;
|
||||
|
||||
if (parameters != null && parameters.Length > 0)
|
||||
{
|
||||
for (int i = 0; i < parameters.Length; i++)
|
||||
{
|
||||
var paramName = $"@param{i}";
|
||||
var paramValue = parameters[i]?.ToString() ?? "NULL";
|
||||
breakdown.Parameters.Add(paramName, paramValue);
|
||||
}
|
||||
}
|
||||
|
||||
return breakdown;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Analyzes a query execution trace context.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The entity type being traced.</typeparam>
|
||||
/// <param name="query">The query being traced.</param>
|
||||
/// <param name="executionContext">Additional execution context.</param>
|
||||
/// <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));
|
||||
}
|
||||
|
||||
var lines = new List<string>
|
||||
{
|
||||
$"Trace Context for {typeof(T).Name}",
|
||||
$"Entity Type: {typeof(T).FullName}",
|
||||
$"Query Provider: {query.Provider?.GetType().Name ?? "Unknown"}",
|
||||
$"Expression: {query.Expression}"
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(executionContext))
|
||||
{
|
||||
lines.Add($"Execution Context: {executionContext}");
|
||||
}
|
||||
|
||||
lines.Add($"Timestamp: {DateTime.UtcNow:O}");
|
||||
|
||||
return string.Join(Environment.NewLine, lines);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts this LINQ breakdown to a SQL Server QueryBreakdown.
|
||||
/// </summary>
|
||||
/// <returns>A SQL Server QueryBreakdown with the same clauses as this breakdown.</returns>
|
||||
public Strata.SqlTools.Breakdowns.SqlServer.QueryBreakdown ConvertToSqlServerBreakdown()
|
||||
{
|
||||
var sqlServerBreakdown = new Strata.SqlTools.Breakdowns.SqlServer.QueryBreakdown();
|
||||
|
||||
// Copy all clause information from this breakdown
|
||||
sqlServerBreakdown.SelectClause.Clause = SelectClause?.Clause;
|
||||
sqlServerBreakdown.SelectClause.Comment = SelectClause?.Comment;
|
||||
sqlServerBreakdown.FromClause.Clause = FromClause?.Clause;
|
||||
sqlServerBreakdown.FromClause.Comment = FromClause?.Comment;
|
||||
sqlServerBreakdown.WhereClause.Clause = WhereClause?.Clause;
|
||||
sqlServerBreakdown.WhereClause.Comment = WhereClause?.Comment;
|
||||
sqlServerBreakdown.GroupByClause.Clause = GroupByClause?.Clause;
|
||||
sqlServerBreakdown.GroupByClause.Comment = GroupByClause?.Comment;
|
||||
sqlServerBreakdown.HavingClause.Clause = HavingClause?.Clause;
|
||||
sqlServerBreakdown.HavingClause.Comment = HavingClause?.Comment;
|
||||
sqlServerBreakdown.OrderByClause.Clause = OrderByClause?.Clause;
|
||||
sqlServerBreakdown.OrderByClause.Comment = OrderByClause?.Comment;
|
||||
|
||||
return sqlServerBreakdown;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts this LINQ breakdown to a PostgreSQL QueryBreakdown.
|
||||
/// </summary>
|
||||
/// <returns>A PostgreSQL QueryBreakdown with the same clauses as this breakdown.</returns>
|
||||
public PostgreSqlBreakdown ConvertToPostgreSqlBreakdown()
|
||||
{
|
||||
var postgresBreakdown = new PostgreSqlBreakdown();
|
||||
|
||||
// Copy all clause information from this breakdown
|
||||
postgresBreakdown.SelectClause.Clause = SelectClause?.Clause;
|
||||
postgresBreakdown.SelectClause.Comment = SelectClause?.Comment;
|
||||
postgresBreakdown.FromClause.Clause = FromClause?.Clause;
|
||||
postgresBreakdown.FromClause.Comment = FromClause?.Comment;
|
||||
postgresBreakdown.WhereClause.Clause = WhereClause?.Clause;
|
||||
postgresBreakdown.WhereClause.Comment = WhereClause?.Comment;
|
||||
postgresBreakdown.GroupByClause.Clause = GroupByClause?.Clause;
|
||||
postgresBreakdown.GroupByClause.Comment = GroupByClause?.Comment;
|
||||
postgresBreakdown.HavingClause.Clause = HavingClause?.Clause;
|
||||
postgresBreakdown.HavingClause.Comment = HavingClause?.Comment;
|
||||
postgresBreakdown.OrderByClause.Clause = OrderByClause?.Clause;
|
||||
postgresBreakdown.OrderByClause.Comment = OrderByClause?.Comment;
|
||||
|
||||
return postgresBreakdown;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts this LINQ breakdown to a Snowflake QueryBreakdown.
|
||||
/// </summary>
|
||||
/// <returns>A Snowflake QueryBreakdown with the same clauses as this breakdown.</returns>
|
||||
public SnowflakeBreakdown ConvertToSnowflakeBreakdown()
|
||||
{
|
||||
var snowflakeBreakdown = new SnowflakeBreakdown();
|
||||
|
||||
// Copy all clause information from this breakdown
|
||||
snowflakeBreakdown.SelectClause.Clause = SelectClause?.Clause;
|
||||
snowflakeBreakdown.SelectClause.Comment = SelectClause?.Comment;
|
||||
snowflakeBreakdown.FromClause.Clause = FromClause?.Clause;
|
||||
snowflakeBreakdown.FromClause.Comment = FromClause?.Comment;
|
||||
snowflakeBreakdown.WhereClause.Clause = WhereClause?.Clause;
|
||||
snowflakeBreakdown.WhereClause.Comment = WhereClause?.Comment;
|
||||
snowflakeBreakdown.GroupByClause.Clause = GroupByClause?.Clause;
|
||||
snowflakeBreakdown.GroupByClause.Comment = GroupByClause?.Comment;
|
||||
snowflakeBreakdown.HavingClause.Clause = HavingClause?.Clause;
|
||||
snowflakeBreakdown.HavingClause.Comment = HavingClause?.Comment;
|
||||
snowflakeBreakdown.OrderByClause.Clause = OrderByClause?.Clause;
|
||||
snowflakeBreakdown.OrderByClause.Comment = OrderByClause?.Comment;
|
||||
|
||||
return snowflakeBreakdown;
|
||||
}
|
||||
|
||||
#region Dialect-Specific SQL Generation
|
||||
|
||||
/// <summary>
|
||||
/// Generates SQL Server T-SQL from this breakdown.
|
||||
/// </summary>
|
||||
/// <returns>SQL Server formatted SQL statement.</returns>
|
||||
public string ToSqlServerSql()
|
||||
{
|
||||
return ConvertToSqlServerBreakdown().GetSql();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates PostgreSQL SQL from this breakdown.
|
||||
/// </summary>
|
||||
/// <returns>PostgreSQL formatted SQL statement.</returns>
|
||||
public string ToPostgreSqlSql()
|
||||
{
|
||||
return ConvertToPostgreSqlBreakdown().GetSql();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates Snowflake SQL from this breakdown.
|
||||
/// </summary>
|
||||
/// <returns>Snowflake formatted SQL statement.</returns>
|
||||
public string ToSnowflakeSql()
|
||||
{
|
||||
return ConvertToSnowflakeBreakdown().GetSql();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Query Analysis and Validation
|
||||
|
||||
/// <summary>
|
||||
/// Determines if this query has a WHERE clause for safe modification operations.
|
||||
/// </summary>
|
||||
/// <returns>True if WHERE clause exists; otherwise, false.</returns>
|
||||
public bool HasWhereClause()
|
||||
{
|
||||
return !string.IsNullOrWhiteSpace(WhereClause?.Clause);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if this query has GROUP BY clause.
|
||||
/// </summary>
|
||||
/// <returns>True if GROUP BY clause exists; otherwise, false.</returns>
|
||||
public bool HasGroupByClause()
|
||||
{
|
||||
return !string.IsNullOrWhiteSpace(GroupByClause?.Clause);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if this query selects all columns (SELECT *).
|
||||
/// </summary>
|
||||
/// <returns>True if SELECT contains *; otherwise, false.</returns>
|
||||
public bool SelectsAllColumns()
|
||||
{
|
||||
return SelectClause?.Clause?.Contains("*") ?? false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets query complexity estimate based on clause count.
|
||||
/// </summary>
|
||||
/// <returns>Complexity level: Simple, Moderate, or Complex.</returns>
|
||||
public string GetComplexityLevel()
|
||||
{
|
||||
var clauseCount = 0;
|
||||
if (HasWhereClause())
|
||||
{
|
||||
clauseCount++;
|
||||
}
|
||||
if (HasGroupByClause())
|
||||
{
|
||||
clauseCount++;
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(HavingClause?.Clause))
|
||||
{
|
||||
clauseCount++;
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(OrderByClause?.Clause))
|
||||
{
|
||||
clauseCount++;
|
||||
}
|
||||
|
||||
return clauseCount switch
|
||||
{
|
||||
0 => "Simple",
|
||||
1 or 2 => "Moderate",
|
||||
_ => "Complex"
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a detailed natural language explanation of what this query does.
|
||||
/// </summary>
|
||||
/// <returns>Human-readable query explanation.</returns>
|
||||
public string GetDetailedExplanation()
|
||||
{
|
||||
var lines = new List<string>();
|
||||
|
||||
// Basic query structure
|
||||
if (!string.IsNullOrWhiteSpace(SelectClause?.Clause))
|
||||
{
|
||||
var what = SelectsAllColumns() ? "all columns" : "specific columns";
|
||||
lines.Add($"This query selects {what}");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(FromClause?.Clause))
|
||||
{
|
||||
lines.Add($"from the {FromClause.Clause} table");
|
||||
}
|
||||
|
||||
// Filtering
|
||||
if (HasWhereClause())
|
||||
{
|
||||
lines.Add($"where {WhereClause.Clause}");
|
||||
}
|
||||
|
||||
// Grouping
|
||||
if (HasGroupByClause())
|
||||
{
|
||||
lines.Add($"grouped by {GroupByClause.Clause}");
|
||||
}
|
||||
|
||||
// Filtering grouped results
|
||||
if (!string.IsNullOrWhiteSpace(HavingClause?.Clause))
|
||||
{
|
||||
lines.Add($"with groups filtered where {HavingClause.Clause}");
|
||||
}
|
||||
|
||||
// Sorting
|
||||
if (!string.IsNullOrWhiteSpace(OrderByClause?.Clause))
|
||||
{
|
||||
lines.Add($"sorted by {OrderByClause.Clause}");
|
||||
}
|
||||
|
||||
// Complexity note
|
||||
var complexity = GetComplexityLevel();
|
||||
if (complexity != "Simple")
|
||||
{
|
||||
lines.Add($"(Complexity: {complexity})");
|
||||
}
|
||||
|
||||
return string.Join(" ", lines);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
using Strata.SqlTools.Breakdowns.LinqToSql;
|
||||
|
||||
namespace Strata.SqlTools.Builders.LinqToSql;
|
||||
|
||||
/// <summary>
|
||||
/// Fluent builder for constructing LinqQueryBreakdown instances programmatically.
|
||||
/// Useful for scenarios where you don't have a live IQueryable to analyze.
|
||||
/// </summary>
|
||||
public class LinqQueryBreakdownBuilder
|
||||
{
|
||||
private readonly LinqQueryBreakdown _breakdown;
|
||||
private readonly List<string> _selectColumns = new();
|
||||
private string? _fromTable;
|
||||
private string? _whereClause;
|
||||
private string? _groupByClause;
|
||||
private string? _havingClause;
|
||||
private string? _orderByClause;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LinqQueryBreakdownBuilder"/> class.
|
||||
/// </summary>
|
||||
public LinqQueryBreakdownBuilder()
|
||||
{
|
||||
_breakdown = new LinqQueryBreakdown();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the SELECT columns for the query.
|
||||
/// </summary>
|
||||
/// <param name="columns">Column names to select.</param>
|
||||
/// <returns>This builder for method chaining.</returns>
|
||||
public LinqQueryBreakdownBuilder SelectColumns(params string[] columns)
|
||||
{
|
||||
if (columns.Length == 0)
|
||||
{
|
||||
_selectColumns.Add("*");
|
||||
}
|
||||
else
|
||||
{
|
||||
_selectColumns.AddRange(columns);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the SELECT to all columns (*).
|
||||
/// </summary>
|
||||
/// <returns>This builder for method chaining.</returns>
|
||||
public LinqQueryBreakdownBuilder SelectAll()
|
||||
{
|
||||
_selectColumns.Clear();
|
||||
_selectColumns.Add("*");
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the FROM table for the query.
|
||||
/// </summary>
|
||||
/// <param name="tableName">The table name.</param>
|
||||
/// <returns>This builder for method chaining.</returns>
|
||||
public LinqQueryBreakdownBuilder FromTable(string tableName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(tableName))
|
||||
{
|
||||
throw new ArgumentException("Table name cannot be null or empty.", nameof(tableName));
|
||||
}
|
||||
_fromTable = tableName;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the WHERE clause for the query.
|
||||
/// </summary>
|
||||
/// <param name="condition">The WHERE condition.</param>
|
||||
/// <returns>This builder for method chaining.</returns>
|
||||
public LinqQueryBreakdownBuilder Where(string condition)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(condition))
|
||||
{
|
||||
_whereClause = condition;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the GROUP BY clause for the query.
|
||||
/// </summary>
|
||||
/// <param name="columns">The columns to group by.</param>
|
||||
/// <returns>This builder for method chaining.</returns>
|
||||
public LinqQueryBreakdownBuilder GroupBy(params string[] columns)
|
||||
{
|
||||
if (columns.Length > 0)
|
||||
{
|
||||
_groupByClause = string.Join(", ", columns);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the HAVING clause for the query.
|
||||
/// </summary>
|
||||
/// <param name="condition">The HAVING condition.</param>
|
||||
/// <returns>This builder for method chaining.</returns>
|
||||
public LinqQueryBreakdownBuilder Having(string condition)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(condition))
|
||||
{
|
||||
_havingClause = condition;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the ORDER BY clause for the query.
|
||||
/// </summary>
|
||||
/// <param name="orderSpecification">The ORDER BY specification (e.g., "Name ASC, Age DESC").</param>
|
||||
/// <returns>This builder for method chaining.</returns>
|
||||
public LinqQueryBreakdownBuilder OrderBy(string orderSpecification)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(orderSpecification))
|
||||
{
|
||||
_orderByClause = orderSpecification;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds an ORDER BY clause in ascending order.
|
||||
/// </summary>
|
||||
/// <param name="column">The column to order by.</param>
|
||||
/// <returns>This builder for method chaining.</returns>
|
||||
public LinqQueryBreakdownBuilder OrderByAscending(string column)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(column))
|
||||
{
|
||||
throw new ArgumentException("Column name cannot be null or empty.", nameof(column));
|
||||
}
|
||||
_orderByClause = $"{column} ASC";
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds an ORDER BY clause in descending order.
|
||||
/// </summary>
|
||||
/// <param name="column">The column to order by.</param>
|
||||
/// <returns>This builder for method chaining.</returns>
|
||||
public LinqQueryBreakdownBuilder OrderByDescending(string column)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(column))
|
||||
{
|
||||
throw new ArgumentException("Column name cannot be null or empty.", nameof(column));
|
||||
}
|
||||
_orderByClause = $"{column} DESC";
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds and returns the LinqQueryBreakdown instance.
|
||||
/// </summary>
|
||||
/// <returns>A new LinqQueryBreakdown with the configured clauses.</returns>
|
||||
/// <exception cref="InvalidOperationException">Thrown when required clauses are missing.</exception>
|
||||
public LinqQueryBreakdown Build()
|
||||
{
|
||||
if (_selectColumns.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException("At least one SELECT column must be specified.");
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(_fromTable))
|
||||
{
|
||||
throw new InvalidOperationException("FROM table must be specified.");
|
||||
}
|
||||
|
||||
var breakdown = new LinqQueryBreakdown(
|
||||
string.Join(", ", _selectColumns),
|
||||
_fromTable,
|
||||
_whereClause ?? string.Empty
|
||||
);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(_groupByClause))
|
||||
{
|
||||
breakdown.GroupByClause.Clause = _groupByClause;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(_havingClause))
|
||||
{
|
||||
breakdown.HavingClause.Clause = _havingClause;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(_orderByClause))
|
||||
{
|
||||
breakdown.OrderByClause.Clause = _orderByClause;
|
||||
}
|
||||
|
||||
return breakdown;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a SQL Server formatted preview of the query being built.
|
||||
/// </summary>
|
||||
/// <returns>Preview SQL statement.</returns>
|
||||
public string PreviewSql()
|
||||
{
|
||||
try
|
||||
{
|
||||
return Build().ToSqlServerSql();
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
return "-- Incomplete query (missing required clauses)";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new builder with the default state.
|
||||
/// </summary>
|
||||
/// <returns>A new LinqQueryBreakdownBuilder instance.</returns>
|
||||
public static LinqQueryBreakdownBuilder Create()
|
||||
{
|
||||
return new LinqQueryBreakdownBuilder();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a builder with a table already specified.
|
||||
/// </summary>
|
||||
/// <param name="tableName">The table to select from.</param>
|
||||
/// <returns>A new builder with the table set.</returns>
|
||||
public static LinqQueryBreakdownBuilder CreateForTable(string tableName)
|
||||
{
|
||||
return new LinqQueryBreakdownBuilder().FromTable(tableName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
using Strata.SqlTools.Breakdowns.LinqToSql;
|
||||
|
||||
namespace Strata.SqlTools.Comparers.LinqToSql;
|
||||
|
||||
/// <summary>
|
||||
/// Result of comparing two LinqQueryBreakdown instances.
|
||||
/// </summary>
|
||||
public record QueryComparisonResult(
|
||||
bool AreEquivalent,
|
||||
double SimilarityScore, // 0.0 to 1.0
|
||||
List<string> Differences,
|
||||
bool HaveSameSelectColumns,
|
||||
bool HaveSameFromTable,
|
||||
bool HaveSameWhereClause,
|
||||
bool HaveSameGroupBy,
|
||||
bool HaveSameHaving,
|
||||
bool HaveSameOrderBy
|
||||
)
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a formatted comparison report.
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compares LinqQueryBreakdown instances to detect similarity, equivalence, and duplicates.
|
||||
/// </summary>
|
||||
public class QueryComparator
|
||||
{
|
||||
private readonly LinqQueryBreakdown _query1;
|
||||
private readonly LinqQueryBreakdown _query2;
|
||||
private QueryComparisonResult? _result;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="QueryComparator"/> class.
|
||||
/// </summary>
|
||||
/// <param name="query1">The first query to compare.</param>
|
||||
/// <param name="query2">The second query to compare.</param>
|
||||
public QueryComparator(LinqQueryBreakdown query1, LinqQueryBreakdown query2)
|
||||
{
|
||||
_query1 = query1 ?? throw new ArgumentNullException(nameof(query1));
|
||||
_query2 = query2 ?? throw new ArgumentNullException(nameof(query2));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the comparison result, calculating it if needed.
|
||||
/// </summary>
|
||||
public QueryComparisonResult Result =>
|
||||
_result ??= PerformComparison();
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the queries are equivalent (same structure).
|
||||
/// </summary>
|
||||
public bool AreEquivalent => Result.AreEquivalent;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the similarity score from 0.0 (completely different) to 1.0 (identical).
|
||||
/// </summary>
|
||||
public double SimilarityScore => Result.SimilarityScore;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of differences found between the queries.
|
||||
/// </summary>
|
||||
public List<string> Differences => Result.Differences;
|
||||
|
||||
/// <summary>
|
||||
/// Performs the actual comparison between the two queries.
|
||||
/// </summary>
|
||||
/// <returns>The comparison result.</returns>
|
||||
private QueryComparisonResult PerformComparison()
|
||||
{
|
||||
var differences = new List<string>();
|
||||
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+", " ")
|
||||
.ToUpperInvariant();
|
||||
}
|
||||
|
||||
private static bool StringEquals(string? str1, string? str2)
|
||||
{
|
||||
return string.Equals(str1, str2, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new comparator for two queries.
|
||||
/// </summary>
|
||||
/// <param name="query1">The first query.</param>
|
||||
/// <param name="query2">The second query.</param>
|
||||
/// <returns>A new QueryComparator instance.</returns>
|
||||
public static QueryComparator Compare(LinqQueryBreakdown query1, LinqQueryBreakdown query2)
|
||||
{
|
||||
return new QueryComparator(query1, query2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if two queries are equivalent.
|
||||
/// </summary>
|
||||
/// <param name="query1">The first query.</param>
|
||||
/// <param name="query2">The second query.</param>
|
||||
/// <returns>True if the queries are equivalent; otherwise, false.</returns>
|
||||
public static bool AreQueriesEquivalent(LinqQueryBreakdown query1, LinqQueryBreakdown query2)
|
||||
{
|
||||
return new QueryComparator(query1, query2).AreEquivalent;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if two queries are identical (same text after normalization).
|
||||
/// </summary>
|
||||
/// <param name="query1">The first query.</param>
|
||||
/// <param name="query2">The second query.</param>
|
||||
/// <returns>True if the queries are identical; otherwise, false.</returns>
|
||||
public static bool AreQueriesIdentical(LinqQueryBreakdown query1, LinqQueryBreakdown query2)
|
||||
{
|
||||
var comparator = new QueryComparator(query1, query2);
|
||||
return comparator.SimilarityScore >= 1.0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the similarity score between two queries (0.0 to 1.0).
|
||||
/// </summary>
|
||||
/// <param name="query1">The first query.</param>
|
||||
/// <param name="query2">The second query.</param>
|
||||
/// <returns>A similarity score from 0.0 (completely different) to 1.0 (identical).</returns>
|
||||
public static double GetSimilarity(LinqQueryBreakdown query1, LinqQueryBreakdown query2)
|
||||
{
|
||||
return new QueryComparator(query1, query2).SimilarityScore;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
using Strata.SqlTools.Breakdowns.LinqToSql;
|
||||
using Strata.SqlTools.Breakdowns.SqlServer;
|
||||
using PostgreSqlBreakdown = Strata.SqlTools.Breakdowns.PostgreSql.QueryBreakdown;
|
||||
using SnowflakeBreakdown = Strata.SqlTools.Breakdowns.Snowflake.QueryBreakdown;
|
||||
|
||||
namespace Strata.SqlTools.Converters.LinqToSql;
|
||||
|
||||
/// <summary>
|
||||
/// Converts dialect-specific QueryBreakdown instances back to the generic LinqQueryBreakdown format.
|
||||
/// Enables parsing from any dialect and converting between all supported dialects.
|
||||
/// </summary>
|
||||
public static class ReverseConverterExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts a SQL Server QueryBreakdown to a LinqQueryBreakdown.
|
||||
/// </summary>
|
||||
/// <param name="breakdown">The SQL Server breakdown to convert.</param>
|
||||
/// <returns>A new LinqQueryBreakdown with the same clauses.</returns>
|
||||
public static LinqQueryBreakdown ToLinqQueryBreakdown(this QueryBreakdown breakdown)
|
||||
{
|
||||
if (breakdown == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(breakdown));
|
||||
}
|
||||
|
||||
var linq = new LinqQueryBreakdown(
|
||||
breakdown.SelectClause?.Clause ?? "*",
|
||||
breakdown.FromClause?.Clause ?? string.Empty,
|
||||
breakdown.WhereClause?.Clause ?? string.Empty
|
||||
);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(breakdown.GroupByClause?.Clause))
|
||||
{
|
||||
linq.GroupByClause.Clause = breakdown.GroupByClause.Clause;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(breakdown.HavingClause?.Clause))
|
||||
{
|
||||
linq.HavingClause.Clause = breakdown.HavingClause.Clause;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(breakdown.OrderByClause?.Clause))
|
||||
{
|
||||
linq.OrderByClause.Clause = breakdown.OrderByClause.Clause;
|
||||
}
|
||||
|
||||
return linq;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a PostgreSQL QueryBreakdown to a LinqQueryBreakdown.
|
||||
/// </summary>
|
||||
/// <param name="breakdown">The PostgreSQL breakdown to convert.</param>
|
||||
/// <returns>A new LinqQueryBreakdown with the same clauses.</returns>
|
||||
public static LinqQueryBreakdown ToLinqQueryBreakdown(this PostgreSqlBreakdown breakdown)
|
||||
{
|
||||
if (breakdown == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(breakdown));
|
||||
}
|
||||
|
||||
var linq = new LinqQueryBreakdown(
|
||||
breakdown.SelectClause?.Clause ?? "*",
|
||||
breakdown.FromClause?.Clause ?? string.Empty,
|
||||
breakdown.WhereClause?.Clause ?? string.Empty
|
||||
);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(breakdown.GroupByClause?.Clause))
|
||||
{
|
||||
linq.GroupByClause.Clause = breakdown.GroupByClause.Clause;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(breakdown.HavingClause?.Clause))
|
||||
{
|
||||
linq.HavingClause.Clause = breakdown.HavingClause.Clause;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(breakdown.OrderByClause?.Clause))
|
||||
{
|
||||
linq.OrderByClause.Clause = breakdown.OrderByClause.Clause;
|
||||
}
|
||||
|
||||
return linq;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a Snowflake QueryBreakdown to a LinqQueryBreakdown.
|
||||
/// </summary>
|
||||
/// <param name="breakdown">The Snowflake breakdown to convert.</param>
|
||||
/// <returns>A new LinqQueryBreakdown with the same clauses.</returns>
|
||||
public static LinqQueryBreakdown ToLinqQueryBreakdown(this SnowflakeBreakdown breakdown)
|
||||
{
|
||||
if (breakdown == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(breakdown));
|
||||
}
|
||||
|
||||
var linq = new LinqQueryBreakdown(
|
||||
breakdown.SelectClause?.Clause ?? "*",
|
||||
breakdown.FromClause?.Clause ?? string.Empty,
|
||||
breakdown.WhereClause?.Clause ?? string.Empty
|
||||
);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(breakdown.GroupByClause?.Clause))
|
||||
{
|
||||
linq.GroupByClause.Clause = breakdown.GroupByClause.Clause;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(breakdown.HavingClause?.Clause))
|
||||
{
|
||||
linq.HavingClause.Clause = breakdown.HavingClause.Clause;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(breakdown.OrderByClause?.Clause))
|
||||
{
|
||||
linq.OrderByClause.Clause = breakdown.OrderByClause.Clause;
|
||||
}
|
||||
|
||||
return linq;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a SQL Server QueryBreakdown to a different dialect.
|
||||
/// </summary>
|
||||
/// <param name="breakdown">The SQL Server breakdown to convert.</param>
|
||||
/// <param name="targetDialect">The target dialect: "postgresql", "snowflake", or "linq".</param>
|
||||
/// <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));
|
||||
}
|
||||
|
||||
return targetDialect.ToLowerInvariant() switch
|
||||
{
|
||||
"postgresql" or "postgres" => breakdown.ToLinqQueryBreakdown().ConvertToPostgreSqlBreakdown(),
|
||||
"snowflake" => breakdown.ToLinqQueryBreakdown().ConvertToSnowflakeBreakdown(),
|
||||
"linq" => breakdown.ToLinqQueryBreakdown(),
|
||||
"sqlserver" or "sql_server" => breakdown,
|
||||
_ => throw new ArgumentException($"Unknown target dialect: {targetDialect}", nameof(targetDialect))
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a PostgreSQL QueryBreakdown to a different dialect.
|
||||
/// </summary>
|
||||
/// <param name="breakdown">The PostgreSQL breakdown to convert.</param>
|
||||
/// <param name="targetDialect">The target dialect: "sqlserver", "snowflake", or "linq".</param>
|
||||
/// <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));
|
||||
}
|
||||
|
||||
return targetDialect.ToLowerInvariant() switch
|
||||
{
|
||||
"sqlserver" or "sql_server" => breakdown.ToLinqQueryBreakdown().ConvertToSqlServerBreakdown(),
|
||||
"snowflake" => breakdown.ToLinqQueryBreakdown().ConvertToSnowflakeBreakdown(),
|
||||
"linq" => breakdown.ToLinqQueryBreakdown(),
|
||||
"postgresql" or "postgres" => breakdown,
|
||||
_ => throw new ArgumentException($"Unknown target dialect: {targetDialect}", nameof(targetDialect))
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a Snowflake QueryBreakdown to a different dialect.
|
||||
/// </summary>
|
||||
/// <param name="breakdown">The Snowflake breakdown to convert.</param>
|
||||
/// <param name="targetDialect">The target dialect: "sqlserver", "postgresql", or "linq".</param>
|
||||
/// <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));
|
||||
}
|
||||
|
||||
return targetDialect.ToLowerInvariant() switch
|
||||
{
|
||||
"sqlserver" or "sql_server" => breakdown.ToLinqQueryBreakdown().ConvertToSqlServerBreakdown(),
|
||||
"postgresql" or "postgres" => breakdown.ToLinqQueryBreakdown().ConvertToPostgreSqlBreakdown(),
|
||||
"linq" => breakdown.ToLinqQueryBreakdown(),
|
||||
"snowflake" => breakdown,
|
||||
_ => throw new ArgumentException($"Unknown target dialect: {targetDialect}", nameof(targetDialect))
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
# Strata.SqlTools.LinqToSql
|
||||
|
||||
LINQ to SQL support for Strata SQL Utilities, providing query breakdown and analysis capabilities for LINQ to SQL queries.
|
||||
|
||||
## Overview
|
||||
|
||||
This library extends Strata.SqlTools to work with LINQ to SQL queries, allowing you to:
|
||||
|
||||
- Analyze LINQ query expressions
|
||||
- Break down LINQ queries into their component parts
|
||||
- Convert LINQ expressions to QueryBreakdown objects
|
||||
- Generate SQL representations from LINQ queries
|
||||
- Visualize LINQ query structure
|
||||
|
||||
## Features
|
||||
|
||||
### LINQ Query Analysis
|
||||
- Extract SELECT, WHERE, JOIN, GROUP BY, and ORDER BY operations from LINQ expressions
|
||||
- Identify data sources and table references
|
||||
- Analyze query composition and complexity
|
||||
|
||||
### QueryBreakdown Integration
|
||||
- Convert LINQ `IQueryable<T>` to `QueryBreakdown` objects
|
||||
- Support for common LINQ methods: `Where`, `Select`, `OrderBy`, `GroupBy`, `Join`, etc.
|
||||
- Parameter extraction and analysis
|
||||
|
||||
### Expression Visitors
|
||||
- Custom expression visitors for LINQ expression trees
|
||||
- Support for method call expressions, lambda expressions, and member access
|
||||
- Handles both query syntax and method syntax
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
dotnet add package Strata.SqlTools.LinqToSql
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Query Breakdown
|
||||
|
||||
```csharp
|
||||
using Strata.SqlTools.Breakdowns.LinqToSql;
|
||||
using System.Linq;
|
||||
|
||||
// Your LINQ to SQL query
|
||||
var query = from user in context.Users
|
||||
where user.Age > 21
|
||||
orderby user.Name
|
||||
select new { user.Id, user.Name, user.Email };
|
||||
|
||||
// Analyze the query
|
||||
var breakdown = LinqQueryBreakdown.Analyze(query);
|
||||
|
||||
// Access breakdown components
|
||||
Console.WriteLine($"Select: {breakdown.SelectClause}");
|
||||
Console.WriteLine($"From: {breakdown.FromClause}");
|
||||
Console.WriteLine($"Where: {breakdown.WhereClause}");
|
||||
Console.WriteLine($"OrderBy: {breakdown.OrderByClause}");
|
||||
```
|
||||
|
||||
### Expression Analysis
|
||||
|
||||
```csharp
|
||||
using Strata.SqlTools.Visitors.LinqToSql;
|
||||
|
||||
// Analyze a specific expression
|
||||
Expression<Func<User, bool>> predicate = u => u.Age > 21 && u.Status == "Active";
|
||||
|
||||
var visitor = new LinqExpressionVisitor();
|
||||
visitor.Visit(predicate);
|
||||
|
||||
// Get analysis results
|
||||
var conditions = visitor.GetConditions();
|
||||
var parameters = visitor.GetParameters();
|
||||
```
|
||||
|
||||
### SQL Generation
|
||||
|
||||
```csharp
|
||||
// Generate SQL from LINQ query
|
||||
var breakdown = LinqQueryBreakdown.Analyze(query);
|
||||
string sql = breakdown.GetSql();
|
||||
|
||||
Console.WriteLine(sql);
|
||||
// Output: SELECT u.Id, u.Name, u.Email FROM Users u WHERE u.Age > 21 ORDER BY u.Name
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
### Key Components
|
||||
|
||||
- **LinqQueryBreakdown**: Main class for analyzing LINQ queries and converting them to breakdown format
|
||||
- **LinqExpressionVisitor**: Expression visitor for traversing LINQ expression trees
|
||||
- **LinqToSqlConverter**: Converts LINQ expressions to SQL Server QueryBreakdown objects
|
||||
|
||||
### Supported LINQ Methods
|
||||
|
||||
- `Where` → WHERE clause
|
||||
- `Select` → SELECT clause
|
||||
- `OrderBy`, `OrderByDescending`, `ThenBy`, `ThenByDescending` → ORDER BY clause
|
||||
- `GroupBy` → GROUP BY clause
|
||||
- `Join`, `GroupJoin` → JOIN clauses
|
||||
- `First`, `FirstOrDefault`, `Single`, `SingleOrDefault` → TOP 1
|
||||
- `Take`, `Skip` → TOP n / OFFSET-FETCH
|
||||
- `Distinct` → DISTINCT
|
||||
- `Count`, `Sum`, `Average`, `Min`, `Max` → Aggregate functions
|
||||
|
||||
## Limitations
|
||||
|
||||
- LINQ to SQL translates to SQL Server T-SQL dialect
|
||||
- Complex expressions may not be fully analyzed
|
||||
- Some LINQ features may not have direct SQL equivalents
|
||||
- Requires the query to be `IQueryable<T>` (not `IEnumerable<T>`)
|
||||
|
||||
## Integration with Markdown
|
||||
|
||||
Use with `Strata.SqlTools.Markdown` to generate visual diagrams:
|
||||
|
||||
```csharp
|
||||
using Strata.SqlTools.Markdown.LinqToSql;
|
||||
|
||||
var breakdown = LinqQueryBreakdown.Analyze(query);
|
||||
var generator = new QueryBreakdownGenerator();
|
||||
|
||||
string mermaidDiagram = generator.GenerateMermaidDiagram(breakdown, "User Query");
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- [Strata.SqlTools](../Strata.SqlTools/README.md) - Core SQL utilities
|
||||
- [Strata.SqlTools.SqlServer](../Strata.SqlTools.SqlServer/README.md) - SQL Server support
|
||||
- [Strata.SqlTools.Markdown](../Strata.SqlTools.Markdown/README.md) - Markdown generation
|
||||
|
||||
## License
|
||||
|
||||
MIT License - Copyright © Strata Decision Technology 2024-2026
|
||||
@@ -0,0 +1,51 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>latest</LangVersion>
|
||||
|
||||
<!-- NuGet Package Metadata -->
|
||||
<PackageId>Strata.SqlTools.LinqToSql</PackageId>
|
||||
<Version>1.0.0</Version>
|
||||
<Authors>Strata Decision Technology</Authors>
|
||||
<Company>Strata Decision Technology</Company>
|
||||
<Product>Strata SQL Utilities - LINQ to SQL</Product>
|
||||
<Description>LINQ to SQL specific implementations for Strata.SqlTools, including LINQ expression analysis, query breakdown, and SQL generation from LINQ queries. Provides tools to analyze and visualize LINQ to SQL query structures.</Description>
|
||||
<PackageTags>linq;linq-to-sql;sql;query-builder;expression-trees;database;dotnet</PackageTags>
|
||||
<PackageProjectUrl>https://github.com/stratadecision/sql-builder</PackageProjectUrl>
|
||||
<RepositoryUrl>https://github.com/stratadecision/sql-builder</RepositoryUrl>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<PackageReleaseNotes>Initial release with LINQ to SQL query analysis, breakdown, and visualization support.</PackageReleaseNotes>
|
||||
<Copyright>Copyright © Strata Decision Technology 2024-2026</Copyright>
|
||||
|
||||
<!-- Build Configuration -->
|
||||
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
|
||||
<IncludeSymbols>true</IncludeSymbols>
|
||||
<SymbolPackageFormat>symbols.nupkg</SymbolPackageFormat>
|
||||
<EmbedUntrackedSources>true</EmbedUntrackedSources>
|
||||
<ContinuousIntegrationBuild Condition="'$(CI)' == 'true'">true</ContinuousIntegrationBuild>
|
||||
|
||||
<!-- Code Analysis -->
|
||||
<EnableNETAnalyzers>true</EnableNETAnalyzers>
|
||||
<AnalysisLevel>latest</AnalysisLevel>
|
||||
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="..\..\README.md" Pack="true" PackagePath="\" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Strata.SqlTools.SqlBreakdown\Strata.SqlTools.SqlBreakdown.csproj" />
|
||||
<ProjectReference Include="..\Strata.SqlTools.SqlServer\Strata.SqlTools.SqlServer.csproj" />
|
||||
<ProjectReference Include="..\Strata.SqlTools.PostgreSql\Strata.SqlTools.PostgreSql.csproj" />
|
||||
<ProjectReference Include="..\Strata.SqlTools.Snowflake\Strata.SqlTools.Snowflake.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- No additional package references needed - works with System.Linq.Expressions from .NET -->
|
||||
|
||||
</Project>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,398 @@
|
||||
using System.Linq.Expressions;
|
||||
using System.Text;
|
||||
|
||||
namespace Strata.SqlTools.Visitors.LinqToSql;
|
||||
|
||||
/// <summary>
|
||||
/// Expression visitor for analyzing LINQ to SQL expression trees.
|
||||
/// Extracts query components such as SELECT, WHERE, JOIN, GROUP BY, and ORDER BY.
|
||||
/// </summary>
|
||||
public class LinqExpressionVisitor : ExpressionVisitor
|
||||
{
|
||||
private readonly StringBuilder _whereBuilder = new();
|
||||
private readonly StringBuilder _orderByBuilder = new();
|
||||
private readonly List<string> _methodCalls = new();
|
||||
private bool _isInWhereClause;
|
||||
#pragma warning disable IDE0052, S4487
|
||||
private bool _isInSelectClause;
|
||||
private bool _isInOrderByClause;
|
||||
private bool _isInGroupByClause;
|
||||
private string? _tableName;
|
||||
#pragma warning restore IDE0052, S4487
|
||||
|
||||
/// <summary>
|
||||
/// Gets the SELECT clause extracted from the expression.
|
||||
/// </summary>
|
||||
public string? SelectClause { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the FROM clause (table name) extracted from the expression.
|
||||
/// </summary>
|
||||
public string? FromClause { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the WHERE clause extracted from the expression.
|
||||
/// </summary>
|
||||
public string? WhereClause { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the ORDER BY clause extracted from the expression.
|
||||
/// </summary>
|
||||
public string? OrderByClause { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the GROUP BY clause extracted from the expression.
|
||||
/// </summary>
|
||||
public string? GroupByClause { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of LINQ method calls in the query chain.
|
||||
/// </summary>
|
||||
public List<string> MethodCallChain => _methodCalls;
|
||||
|
||||
/// <summary>
|
||||
/// Visits a method call expression.
|
||||
/// </summary>
|
||||
protected override Expression VisitMethodCall(MethodCallExpression node)
|
||||
{
|
||||
var methodName = node.Method.Name;
|
||||
_methodCalls.Add(methodName);
|
||||
|
||||
switch (methodName)
|
||||
{
|
||||
case "Where":
|
||||
VisitWhereMethod(node);
|
||||
break;
|
||||
case "Select":
|
||||
VisitSelectMethod(node);
|
||||
break;
|
||||
case "OrderBy":
|
||||
case "OrderByDescending":
|
||||
case "ThenBy":
|
||||
case "ThenByDescending":
|
||||
VisitOrderByMethod(node);
|
||||
break;
|
||||
case "GroupBy":
|
||||
VisitGroupByMethod(node);
|
||||
break;
|
||||
case "Join":
|
||||
case "GroupJoin":
|
||||
VisitJoinMethod(node);
|
||||
break;
|
||||
case "Take":
|
||||
case "Skip":
|
||||
VisitTakeSkipMethod(node);
|
||||
break;
|
||||
default:
|
||||
// Visit the source expression
|
||||
Visit(node.Arguments[0]);
|
||||
break;
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Visits a constant expression to extract the table name.
|
||||
/// </summary>
|
||||
protected override Expression VisitConstant(ConstantExpression node)
|
||||
{
|
||||
// Handle WHERE clause constants
|
||||
if (_isInWhereClause)
|
||||
{
|
||||
if (node.Value is string)
|
||||
{
|
||||
_whereBuilder.Append($"'{node.Value}'");
|
||||
}
|
||||
else if (node.Value != null)
|
||||
{
|
||||
_whereBuilder.Append(node.Value.ToString());
|
||||
}
|
||||
else
|
||||
{
|
||||
_whereBuilder.Append("NULL");
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
// Handle table name extraction
|
||||
if (node.Type.IsGenericType)
|
||||
{
|
||||
var genericType = node.Type.GetGenericTypeDefinition();
|
||||
if (genericType.Name.Contains("Table") || genericType.Name.Contains("Query"))
|
||||
{
|
||||
var entityType = node.Type.GetGenericArguments().FirstOrDefault();
|
||||
if (entityType != null)
|
||||
{
|
||||
_tableName = entityType.Name;
|
||||
FromClause = _tableName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return base.VisitConstant(node);
|
||||
}
|
||||
|
||||
private void VisitWhereMethod(MethodCallExpression node)
|
||||
{
|
||||
// Visit the source
|
||||
Visit(node.Arguments[0]);
|
||||
|
||||
// Extract the predicate
|
||||
if (node.Arguments.Count > 1)
|
||||
{
|
||||
var lambda = StripQuotes(node.Arguments[1]) as LambdaExpression;
|
||||
if (lambda != null)
|
||||
{
|
||||
_isInWhereClause = true;
|
||||
Visit(lambda.Body);
|
||||
_isInWhereClause = false;
|
||||
|
||||
if (_whereBuilder.Length > 0)
|
||||
{
|
||||
WhereClause = _whereBuilder.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void VisitSelectMethod(MethodCallExpression node)
|
||||
{
|
||||
// Visit the source
|
||||
Visit(node.Arguments[0]);
|
||||
|
||||
// Extract the selector
|
||||
if (node.Arguments.Count > 1)
|
||||
{
|
||||
var lambda = StripQuotes(node.Arguments[1]) as LambdaExpression;
|
||||
if (lambda != null)
|
||||
{
|
||||
_isInSelectClause = true;
|
||||
var selectExpression = ExtractSelectExpression(lambda.Body);
|
||||
_isInSelectClause = false;
|
||||
|
||||
if (!string.IsNullOrEmpty(selectExpression))
|
||||
{
|
||||
SelectClause = selectExpression;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void VisitOrderByMethod(MethodCallExpression node)
|
||||
{
|
||||
// Visit the source
|
||||
Visit(node.Arguments[0]);
|
||||
|
||||
// Extract the key selector
|
||||
if (node.Arguments.Count > 1)
|
||||
{
|
||||
var lambda = StripQuotes(node.Arguments[1]) as LambdaExpression;
|
||||
if (lambda != null)
|
||||
{
|
||||
_isInOrderByClause = true;
|
||||
var orderByExpression = ExtractMemberName(lambda.Body);
|
||||
_isInOrderByClause = false;
|
||||
|
||||
if (!string.IsNullOrEmpty(orderByExpression))
|
||||
{
|
||||
var direction = node.Method.Name.Contains("Descending") ? " DESC" : " ASC";
|
||||
|
||||
if (_orderByBuilder.Length > 0)
|
||||
{
|
||||
_orderByBuilder.Append(", ");
|
||||
}
|
||||
_orderByBuilder.Append(orderByExpression + direction);
|
||||
OrderByClause = _orderByBuilder.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void VisitGroupByMethod(MethodCallExpression node)
|
||||
{
|
||||
// Visit the source
|
||||
Visit(node.Arguments[0]);
|
||||
|
||||
// Extract the key selector
|
||||
if (node.Arguments.Count > 1)
|
||||
{
|
||||
var lambda = StripQuotes(node.Arguments[1]) as LambdaExpression;
|
||||
if (lambda != null)
|
||||
{
|
||||
_isInGroupByClause = true;
|
||||
var groupByExpression = ExtractMemberName(lambda.Body);
|
||||
_isInGroupByClause = false;
|
||||
|
||||
if (!string.IsNullOrEmpty(groupByExpression))
|
||||
{
|
||||
GroupByClause = groupByExpression;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void VisitJoinMethod(MethodCallExpression node)
|
||||
{
|
||||
// Visit the source
|
||||
Visit(node.Arguments[0]);
|
||||
|
||||
// For joins, we'd need more complex logic to extract full join information
|
||||
// This is a simplified version
|
||||
_methodCalls.Add($"{node.Method.Name} (complex join analysis not fully implemented)");
|
||||
}
|
||||
|
||||
private void VisitTakeSkipMethod(MethodCallExpression node)
|
||||
{
|
||||
// Visit the source
|
||||
Visit(node.Arguments[0]);
|
||||
|
||||
// Extract the count
|
||||
if (node.Arguments.Count > 1 && node.Arguments[1] is ConstantExpression constant)
|
||||
{
|
||||
_methodCalls.Add($"{node.Method.Name}({constant.Value})");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Visits a binary expression (e.g., comparisons, logical operations).
|
||||
/// </summary>
|
||||
protected override Expression VisitBinary(BinaryExpression node)
|
||||
{
|
||||
if (_isInWhereClause)
|
||||
{
|
||||
_whereBuilder.Append("(");
|
||||
Visit(node.Left);
|
||||
|
||||
_whereBuilder.Append($" {GetOperator(node.NodeType)} ");
|
||||
|
||||
Visit(node.Right);
|
||||
_whereBuilder.Append(")");
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
return base.VisitBinary(node);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Visits a member access expression.
|
||||
/// </summary>
|
||||
protected override Expression VisitMember(MemberExpression node)
|
||||
{
|
||||
if (_isInWhereClause)
|
||||
{
|
||||
var memberName = GetFullMemberName(node);
|
||||
_whereBuilder.Append(memberName);
|
||||
return node;
|
||||
}
|
||||
|
||||
return base.VisitMember(node);
|
||||
}
|
||||
|
||||
private string ExtractSelectExpression(Expression expression)
|
||||
{
|
||||
if (expression is NewExpression newExpr)
|
||||
{
|
||||
var members = new List<string>();
|
||||
for (int i = 0; i < newExpr.Arguments.Count; i++)
|
||||
{
|
||||
var memberName = ExtractMemberName(newExpr.Arguments[i]);
|
||||
var alias = newExpr.Members?[i].Name;
|
||||
|
||||
if (!string.IsNullOrEmpty(alias) && alias != memberName)
|
||||
{
|
||||
members.Add($"{memberName} AS {alias}");
|
||||
}
|
||||
else
|
||||
{
|
||||
members.Add(memberName);
|
||||
}
|
||||
}
|
||||
return string.Join(", ", members);
|
||||
}
|
||||
|
||||
var name = ExtractMemberName(expression);
|
||||
return string.IsNullOrEmpty(name) ? "*" : name;
|
||||
}
|
||||
|
||||
private string ExtractMemberName(Expression expression)
|
||||
{
|
||||
if (expression is MemberExpression member)
|
||||
{
|
||||
return GetFullMemberName(member);
|
||||
}
|
||||
|
||||
if (expression is ParameterExpression param)
|
||||
{
|
||||
return "*";
|
||||
}
|
||||
|
||||
if (expression is MethodCallExpression methodCall)
|
||||
{
|
||||
return $"{methodCall.Method.Name}(...)";
|
||||
}
|
||||
|
||||
return expression.ToString();
|
||||
}
|
||||
|
||||
private string GetFullMemberName(MemberExpression expression)
|
||||
{
|
||||
var parts = new Stack<string>();
|
||||
var current = expression;
|
||||
|
||||
while (current != null)
|
||||
{
|
||||
parts.Push(current.Member.Name);
|
||||
|
||||
if (current.Expression is MemberExpression memberExpr)
|
||||
{
|
||||
current = memberExpr;
|
||||
}
|
||||
else if (current.Expression is ParameterExpression paramExpr)
|
||||
{
|
||||
// Use parameter name as table alias if it's not the default
|
||||
if (paramExpr.Name != null && paramExpr.Name.Length == 1)
|
||||
{
|
||||
parts.Push(paramExpr.Name);
|
||||
}
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return string.Join(".", parts);
|
||||
}
|
||||
|
||||
private string GetOperator(ExpressionType nodeType)
|
||||
{
|
||||
return nodeType switch
|
||||
{
|
||||
ExpressionType.Equal => "=",
|
||||
ExpressionType.NotEqual => "!=",
|
||||
ExpressionType.GreaterThan => ">",
|
||||
ExpressionType.GreaterThanOrEqual => ">=",
|
||||
ExpressionType.LessThan => "<",
|
||||
ExpressionType.LessThanOrEqual => "<=",
|
||||
ExpressionType.AndAlso => "AND",
|
||||
ExpressionType.OrElse => "OR",
|
||||
ExpressionType.Add => "+",
|
||||
ExpressionType.Subtract => "-",
|
||||
ExpressionType.Multiply => "*",
|
||||
ExpressionType.Divide => "/",
|
||||
_ => nodeType.ToString()
|
||||
};
|
||||
}
|
||||
|
||||
private static Expression StripQuotes(Expression expression)
|
||||
{
|
||||
while (expression.NodeType == ExpressionType.Quote)
|
||||
{
|
||||
expression = ((UnaryExpression)expression).Operand;
|
||||
}
|
||||
return expression;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user