SonarQube Analysis / sonarqube (pull_request) Successful in 2m47s
Manual sweep of all 42 IDE0028 sites flagged by SonarQube — `dotnet format
analyzers --diagnostics IDE0028` declined to fix these (no .editorconfig
opt-in for `dotnet_style_prefer_collection_expression`), so applied by
hand. The repo already targets `<LangVersion>latest</LangVersion>` on
net8.0, so C# 12 collection expressions are available.
Pattern: `new List<T>()` / `new Dictionary<K,V>()` / `new ArrayList()` /
`new()` -> `[]` for empty; `new List<T> { ... }` -> `[...]` for literal.
24 files touched in src/{EFCore, LinqToSql, Query, Snowflake, SqlBreakdown,
SqlServer}; tests untouched (no IDE0028 sites in test code).
Build clean (35 warnings unchanged from baseline, 0 errors). All tests
remain green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
656 lines
23 KiB
C#
656 lines
23 KiB
C#
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>
|
|
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; } = [];
|
|
|
|
/// <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)
|
|
{
|
|
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;
|
|
}
|
|
|
|
/// <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 && 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
|
|
{
|
|
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<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() ?? [];
|
|
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<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
|
|
{
|
|
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;
|
|
}
|
|
|
|
/// <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
|
|
{
|
|
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<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
|
|
{
|
|
ArgumentNullException.ThrowIfNull(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
|
|
}
|