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