chore: initial git load of code space

This commit is contained in:
Thom Lamb
2026-05-12 08:52:33 -05:00
parent 9abada692f
commit 5e467bcc9c
384 changed files with 65960 additions and 2 deletions
@@ -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))
};
}
}
+137
View File
@@ -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;
}
}