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; } = new(); /// /// 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) { 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; } /// /// 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 { 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(); 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() ?? new List(); 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 { 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; } /// /// 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 { 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(); 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 { if (query == null) { throw new ArgumentNullException(nameof(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 }