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;
///
/// Represents a LINQ to SQL query breakdown, analyzing IQueryable expressions
/// and converting them to SQL Server QueryBreakdown format.
///
///
/// 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.
///
public class LinqQueryBreakdown : QueryBreakdown
{
///
/// Gets or sets the original LINQ expression that was analyzed.
///
public Expression? OriginalExpression { get; set; }
///
/// Gets or sets the type of the entity being queried.
///
public Type? EntityType { get; set; }
///
/// Gets or sets whether this query uses LINQ method syntax.
///
public bool IsMethodSyntax { get; set; } = true;
///
/// Gets or sets the list of LINQ method calls in the query chain.
///
public List MethodCallChain { get; set; } = [];
///
/// Initializes a new instance of the class.
///
public LinqQueryBreakdown() : base()
{
}
///
/// Initializes a new instance of the class with SELECT and FROM clauses.
///
/// The SELECT clause.
/// The FROM clause (table name or data source).
public LinqQueryBreakdown(string selectClause, string fromClause) : base(selectClause, fromClause)
{
}
///
/// Initializes a new instance of the class with SELECT, FROM, and WHERE clauses.
///
/// The SELECT clause.
/// The FROM clause (table name or data source).
/// The WHERE clause.
public LinqQueryBreakdown(string selectClause, string fromClause, string whereClause)
: base(selectClause, fromClause, whereClause)
{
}
///
/// Analyzes an IQueryable LINQ query and creates a LinqQueryBreakdown.
///
/// The entity type being queried.
/// The IQueryable query to analyze.
/// A LinqQueryBreakdown representing the query structure.
public static LinqQueryBreakdown Analyze(IQueryable query)
{
ArgumentNullException.ThrowIfNull(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;
}
///
/// Tries to analyze an IQueryable LINQ query and create a LinqQueryBreakdown.
///
/// The entity type being queried.
/// The IQueryable query to analyze.
/// The resulting LinqQueryBreakdown if successful.
/// Error message if analysis fails.
/// True if analysis succeeded; otherwise, false.
public static bool TryAnalyze(IQueryable 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;
}
}
///
/// Gets a summary of the LINQ query structure.
///
/// A string describing the query composition.
public string GetQuerySummary()
{
var parts = new List();
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);
}
///
/// Gets the LINQ method call chain as a string.
///
/// A string representing the method chain.
public string GetMethodChain()
{
if (MethodCallChain.Count == 0)
{
return "No method calls";
}
return string.Join(" -> ", MethodCallChain);
}
///
/// Gets a LINQ to SQL query of the specified type based on this breakdown.
///
/// The entity type for the query.
/// An IQueryable of the specified type reconstructed from the breakdown, or null if the type doesn't match the original entity type.
///
/// 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.
///
public override IQueryable? GetQuery() 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
try
{
// If the original expression can be converted to IQueryable, use it
// Otherwise, we cannot safely reconstruct without the original query provider
if (OriginalExpression is Expression && EntityType == typeof(T))
{
// We have the expression, but we don't have the provider to create IQueryable
// 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;
}
}
///
/// Analyzes an INSERT operation for the given entity.
///
/// The entity type being inserted.
/// The entity instance being inserted.
/// An InsertBreakdown representing the insert operation.
public static Breakdowns.SqlServer.InsertBreakdown AnalyzeInsert(T entity) where T : class
{
ArgumentNullException.ThrowIfNull(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();
var valuesList = new List();
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;
}
///
/// Analyzes an INSERT operation for multiple entities.
///
/// The entity type being inserted.
/// The entities being inserted.
/// An InsertBreakdown representing the bulk insert operation.
public static Breakdowns.SqlServer.InsertBreakdown AnalyzeInsertRange(IEnumerable entities) where T : class
{
var entitiesList = entities?.ToList() ?? [];
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 the entity type to get column names
var properties = typeof(T).GetProperties();
var columnNames = new List();
foreach (var prop in properties)
{
columnNames.Add(prop.Name);
}
breakdown.InsertIntoClause.Clause = string.Join(", ", columnNames);
// Add values for each entity
var allValues = new List();
foreach (var entity in entitiesList)
{
var rowValues = new List();
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;
}
///
/// Analyzes a DELETE operation based on a filter expression.
///
/// The entity type being deleted.
/// The filter expression defining which entities to delete.
/// A DeleteBreakdown representing the delete operation.
public static Breakdowns.SqlServer.DeleteBreakdown AnalyzeDelete(Expression> filterExpression) where T : class
{
ArgumentNullException.ThrowIfNull(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;
}
///
/// Analyzes an UPDATE operation based on filter and update expressions.
///
/// The entity type being updated.
/// The filter expression defining which entities to update.
/// The update expression defining what to update.
/// An UpdateBreakdown representing the update operation.
public static Breakdowns.SqlServer.UpdateBreakdown AnalyzeUpdate(
Expression> filterExpression,
Expression> updateExpression) where T : class
{
ArgumentNullException.ThrowIfNull(filterExpression);
ArgumentNullException.ThrowIfNull(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();
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;
}
///
/// Analyzes a procedure call breakdown.
///
/// The name of the stored procedure.
/// The procedure parameters.
/// A ProcedureBreakdown representing the procedure call.
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;
}
///
/// Analyzes a query execution trace context.
///
/// The entity type being traced.
/// The query being traced.
/// Additional execution context.
/// A string representation of the trace analysis.
public static string AnalyzeTrace(IQueryable query, string? executionContext = null) where T : class
{
ArgumentNullException.ThrowIfNull(query);
var lines = new List
{
$"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);
}
///
/// Converts this LINQ breakdown to a SQL Server QueryBreakdown.
///
/// A SQL Server QueryBreakdown with the same clauses as this breakdown.
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;
}
///
/// Converts this LINQ breakdown to a PostgreSQL QueryBreakdown.
///
/// A PostgreSQL QueryBreakdown with the same clauses as this breakdown.
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;
}
///
/// Converts this LINQ breakdown to a Snowflake QueryBreakdown.
///
/// A Snowflake QueryBreakdown with the same clauses as this breakdown.
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
///
/// Generates SQL Server T-SQL from this breakdown.
///
/// SQL Server formatted SQL statement.
public string ToSqlServerSql()
{
return ConvertToSqlServerBreakdown().GetSql();
}
///
/// Generates PostgreSQL SQL from this breakdown.
///
/// PostgreSQL formatted SQL statement.
public string ToPostgreSqlSql()
{
return ConvertToPostgreSqlBreakdown().GetSql();
}
///
/// Generates Snowflake SQL from this breakdown.
///
/// Snowflake formatted SQL statement.
public string ToSnowflakeSql()
{
return ConvertToSnowflakeBreakdown().GetSql();
}
#endregion
#region Query Analysis and Validation
///
/// Determines if this query has a WHERE clause for safe modification operations.
///
/// True if WHERE clause exists; otherwise, false.
public bool HasWhereClause()
{
return !string.IsNullOrWhiteSpace(WhereClause?.Clause);
}
///
/// Determines if this query has GROUP BY clause.
///
/// True if GROUP BY clause exists; otherwise, false.
public bool HasGroupByClause()
{
return !string.IsNullOrWhiteSpace(GroupByClause?.Clause);
}
///
/// Determines if this query selects all columns (SELECT *).
///
/// True if SELECT contains *; otherwise, false.
public bool SelectsAllColumns()
{
return SelectClause?.Clause?.Contains('*') ?? false;
}
///
/// Gets query complexity estimate based on clause count.
///
/// Complexity level: Simple, Moderate, or Complex.
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"
};
}
///
/// Gets a detailed natural language explanation of what this query does.
///
/// Human-readable query explanation.
public string GetDetailedExplanation()
{
var lines = new List();
// 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
}