chore: initial git load of code space
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
namespace Strata.SqlTools.SqlBreakdown.Classes;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the contract for SQL clauses with text and optional comments.
|
||||
/// </summary>
|
||||
public interface ISqlClause
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the SQL clause text (without comments).
|
||||
/// </summary>
|
||||
string? Clause { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets any comments associated with this clause.
|
||||
/// </summary>
|
||||
string? Comment { get; set; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
using Strata.SqlTools.SqlBreakdown.Expressions;
|
||||
using Strata.SqlTools.SqlBreakdown.Interfaces;
|
||||
|
||||
namespace Strata.SqlTools.SqlBreakdown.Classes;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the contract for SQL clauses that can be parsed into Expression objects.
|
||||
/// </summary>
|
||||
public interface ISqlExpressionClause : ISqlClause
|
||||
{
|
||||
/// <summary>
|
||||
/// Parses the clause into one or more Expression objects.
|
||||
/// </summary>
|
||||
/// <param name="parser">The statement expression parser to use for parsing.</param>
|
||||
/// <returns>An enumerable collection of parsed Expression objects.</returns>
|
||||
IEnumerable<Expression> GetExpressions(IStatementExpressionParser parser);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
namespace Strata.SqlTools.SqlBreakdown.Classes;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the contract for a WITH clause (Common Table Expression).
|
||||
/// </summary>
|
||||
public interface IWithClause : ISqlClause
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the table name for the CTE.
|
||||
/// </summary>
|
||||
string TableName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the parsed SQL clauses representing the CTE query.
|
||||
/// </summary>
|
||||
SqlClauses? Sql { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the query breakdown representing the CTE.
|
||||
/// This provides access to the full query structure including parameters.
|
||||
/// </summary>
|
||||
Interfaces.QueryEngine.IQueryBreakdown? Query { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this CTE is recursive.
|
||||
/// Recursive CTEs require a UNION ALL pattern with an anchor member and recursive member.
|
||||
/// </summary>
|
||||
bool IsRecursive { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the recursive query (UNION ALL part) for recursive CTEs.
|
||||
/// This query represents the recursive member that joins back to the CTE.
|
||||
/// Only applicable when IsRecursive is true.
|
||||
/// </summary>
|
||||
Interfaces.QueryEngine.IQueryBreakdown? RecursiveQuery { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the explicit column list for the CTE.
|
||||
/// When specified, defines column names for the CTE that can differ from the underlying query columns.
|
||||
/// Example: WITH users (id, name, email) AS (...)
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Column list rules:
|
||||
/// <list type="bullet">
|
||||
/// <item><description>Must match the number of columns in the CTE's SELECT clause</description></item>
|
||||
/// <item><description>Column names override the names from the underlying query</description></item>
|
||||
/// <item><description>Required for recursive CTEs to define consistent column names across anchor and recursive members</description></item>
|
||||
/// <item><description>Column names should follow SQL identifier rules (alphanumeric, underscores, no special characters)</description></item>
|
||||
/// <item><description>Case sensitivity depends on database collation settings</description></item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
List<string>? ColumnList { get; set; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
using Strata.SqlTools.SqlBreakdown.Interfaces.QueryEngine;
|
||||
|
||||
namespace Strata.SqlTools.SqlBreakdown.Classes;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a query parameter with a name and value.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public sealed class QueryParam : IQueryParam
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="QueryParam"/> class.
|
||||
/// </summary>
|
||||
/// <param name="name">The parameter name.</param>
|
||||
/// <param name="value">The parameter value.</param>
|
||||
public QueryParam(string name, object value)
|
||||
{
|
||||
Name = name;
|
||||
Value = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the parameter name.
|
||||
/// </summary>
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the parameter value.
|
||||
/// </summary>
|
||||
public object Value { get; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
using System.Collections;
|
||||
using Strata.SqlTools.SqlBreakdown.Interfaces;
|
||||
|
||||
namespace Strata.SqlTools.SqlBreakdown.Classes;
|
||||
|
||||
/// <summary>
|
||||
/// A simple ISqlBreakdown implementation that holds only raw SQL without parsing into clauses.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This class is primarily used for batch SQL parsing where raw statements need to be stored
|
||||
/// without detailed clause breakdown. Actual clause parsing can be performed separately.
|
||||
/// </remarks>
|
||||
[Serializable]
|
||||
public class RawSqlBreakdown : ISqlBreakdown
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the raw SQL statement.
|
||||
/// </summary>
|
||||
public string? RawSql { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the setup clauses to execute before the main statement.
|
||||
/// </summary>
|
||||
public List<string> SetupClauses { get; set; } = new List<string>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether setup clauses are being used.
|
||||
/// </summary>
|
||||
public bool IsUsingSetupClause => SetupClauses.Count > 0;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the finish clauses to execute after the main statement.
|
||||
/// </summary>
|
||||
public ArrayList FinishClauses { get; set; } = new ArrayList();
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether finish clauses are being used.
|
||||
/// </summary>
|
||||
public bool IsUsingFinishClause => FinishClauses.Count > 0;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the complete SQL statement including optional setup and finish clauses.
|
||||
/// </summary>
|
||||
/// <param name="includeSetupFinish">Whether to include setup and finish clauses.</param>
|
||||
/// <returns>The complete SQL statement string.</returns>
|
||||
public string GetSql(bool includeSetupFinish = true)
|
||||
{
|
||||
if (!includeSetupFinish)
|
||||
{
|
||||
return RawSql ?? string.Empty;
|
||||
}
|
||||
|
||||
var sql = RawSql ?? string.Empty;
|
||||
|
||||
if (IsUsingSetupClause || IsUsingFinishClause)
|
||||
{
|
||||
var lines = new List<string>();
|
||||
|
||||
if (IsUsingSetupClause)
|
||||
{
|
||||
lines.AddRange(SetupClauses);
|
||||
}
|
||||
|
||||
lines.Add(sql);
|
||||
|
||||
if (IsUsingFinishClause)
|
||||
{
|
||||
lines.AddRange(FinishClauses.Cast<string>());
|
||||
}
|
||||
|
||||
return string.Join(Environment.NewLine, lines);
|
||||
}
|
||||
|
||||
return sql;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a deep copy of this breakdown.
|
||||
/// </summary>
|
||||
/// <returns>A new RawSqlBreakdown with copied data.</returns>
|
||||
public object Clone()
|
||||
{
|
||||
return new RawSqlBreakdown
|
||||
{
|
||||
RawSql = RawSql,
|
||||
SetupClauses = new List<string>(SetupClauses),
|
||||
FinishClauses = new ArrayList(FinishClauses)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
using Strata.SqlTools.SqlBreakdown.Expressions;
|
||||
using Strata.SqlTools.SqlBreakdown.Expressions.Literals;
|
||||
using Strata.SqlTools.SqlBreakdown.Interfaces.Core;
|
||||
|
||||
namespace Strata.SqlTools.SqlBreakdown.Classes;
|
||||
|
||||
public class SelectClauseColumn : IVisitable
|
||||
{
|
||||
public string? Alias { get; }
|
||||
public Expression Expression { get; }
|
||||
|
||||
public SelectClauseColumn(Expression expression, string alias)
|
||||
{
|
||||
Expression = expression;
|
||||
Alias = string.IsNullOrWhiteSpace(alias) ? null : alias;
|
||||
}
|
||||
|
||||
public T Accept<T>(IVisitor<T> visitor)
|
||||
{
|
||||
return visitor.VisitSelectClauseColumn(this);
|
||||
}
|
||||
|
||||
public static SelectClauseColumn Null(string alias) => new(new NullLiteralExpression(), alias);
|
||||
public static SelectClauseColumn Number(decimal number, string alias) => new(new NumberLiteralExpression(number), alias);
|
||||
public static SelectClauseColumn String(string value, string alias) => new(new StringLiteralExpression(value), alias);
|
||||
public static SelectClauseColumn DateTime(DateTime dateTime, string alias) => new(new DateTimeLiteralExpression(dateTime), alias);
|
||||
|
||||
public static SelectClauseColumn TableColumn(int columnId, string columnName, RegisteredTableSource tableSource, string alias) =>
|
||||
new(new RegisteredTableColumnExpression(columnId, columnName, tableSource), alias);
|
||||
|
||||
public static SelectClauseColumn TableColumn(RegisteredTableColumnExpression column, string alias) => new(column, alias);
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
using System.Diagnostics;
|
||||
using Strata.SqlTools.SqlBreakdown.Interfaces.Core;
|
||||
|
||||
namespace Strata.SqlTools.SqlBreakdown.Classes;
|
||||
|
||||
#pragma warning disable S1694 // An abstract class should have both abstract and concrete methods
|
||||
public abstract class SelectSource : IVisitable
|
||||
{
|
||||
public abstract T Accept<T>(IVisitor<T> visitor);
|
||||
}
|
||||
|
||||
[DebuggerDisplay("{TableName}")]
|
||||
public class TableSource : SelectSource
|
||||
{
|
||||
public string TableName { get; }
|
||||
|
||||
public string? Schema { get; }
|
||||
|
||||
public string? Alias { get; }
|
||||
|
||||
public TableSource(string tableName) : this(tableName, null)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
#pragma warning disable S3427 // Method overloads with default parameter values should not overlap
|
||||
public TableSource(string tableName, string? schema = null, string? alias = null)
|
||||
{
|
||||
TableName = string.IsNullOrWhiteSpace(tableName) ? throw new ArgumentNullException(nameof(tableName)) : tableName;
|
||||
Schema = string.IsNullOrWhiteSpace(schema) ? null : schema;
|
||||
Alias = string.IsNullOrWhiteSpace(alias) ? null : alias;
|
||||
}
|
||||
#pragma warning restore S3427
|
||||
|
||||
public override T Accept<T>(IVisitor<T> visitor)
|
||||
{
|
||||
return visitor.VisitTableSource(this);
|
||||
}
|
||||
}
|
||||
|
||||
public class RegisteredTableSource : TableSource
|
||||
{
|
||||
public int TableId { get; }
|
||||
|
||||
public RegisteredTableSource(int tableId, string tableSchema, string tableName)
|
||||
: this(tableId, tableSchema, tableName, null)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public RegisteredTableSource(int tableId, string tableSchema, string tableName, string? alias) : base(tableName, tableSchema, alias)
|
||||
{
|
||||
if (tableId <= 0)
|
||||
{
|
||||
throw new ArgumentException("tableId must be greater than 0", nameof(tableId));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(tableSchema))
|
||||
{
|
||||
throw new ArgumentException("tableName cannot be null or whitespace", nameof(tableSchema));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(tableName))
|
||||
{
|
||||
throw new ArgumentException("tableName cannot be null or whitespace", nameof(tableName));
|
||||
}
|
||||
|
||||
TableId = tableId;
|
||||
}
|
||||
}
|
||||
#pragma warning restore S1694
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
using System.Collections;
|
||||
using System.Text;
|
||||
using Strata.SqlTools.SqlBreakdown.Interfaces;
|
||||
|
||||
namespace Strata.SqlTools.SqlBreakdown.Classes;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for SQL query breakdowns that provides common setup/finish clause handling and cloning.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public abstract class SqlBreakdownBase : ISqlBreakdown
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SqlBreakdownBase"/> class.
|
||||
/// </summary>
|
||||
protected SqlBreakdownBase()
|
||||
{
|
||||
SetupClauses = new List<string>();
|
||||
FinishClauses = new ArrayList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the setup clauses to execute before the main query.
|
||||
/// </summary>
|
||||
public List<string> SetupClauses { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether setup clauses are being used.
|
||||
/// </summary>
|
||||
public bool IsUsingSetupClause => SetupClauses.Count > 0;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the raw/original SQL statement before parsing and breakdown.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This property stores the original SQL text that was parsed to create this breakdown.
|
||||
/// It's useful for auditing, logging, and batch statement retrieval.
|
||||
/// </remarks>
|
||||
public string? RawSql { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the finish clauses to execute after the main query.
|
||||
/// </summary>
|
||||
public ArrayList FinishClauses { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether finish clauses are being used.
|
||||
/// </summary>
|
||||
public bool IsUsingFinishClause => FinishClauses.Count > 0;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the SQL breakdown as a string. Must be implemented by derived classes.
|
||||
/// </summary>
|
||||
/// <returns>The SQL query string.</returns>
|
||||
protected abstract string GetSqlBreakdown();
|
||||
|
||||
/// <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, or null if the breakdown cannot be converted to a LINQ query.</returns>
|
||||
/// <remarks>
|
||||
/// This method allows derived breakdown classes to reconstruct or generate LINQ queries
|
||||
/// from the analyzed components (SELECT, WHERE, ORDER BY, etc.).
|
||||
/// The default implementation returns null.
|
||||
/// </remarks>
|
||||
public virtual IQueryable<T>? GetQuery<T>() where T : class => null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the complete SQL query including optional setup and finish clauses.
|
||||
/// </summary>
|
||||
/// <param name="includeSetupFinish">Whether to include setup and finish clauses.</param>
|
||||
/// <returns>The complete SQL query string.</returns>
|
||||
public string GetSql(bool includeSetupFinish = true)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
if (includeSetupFinish)
|
||||
{
|
||||
// Write out any setup clauses
|
||||
sb.AppendLine();
|
||||
foreach (string setup in SetupClauses)
|
||||
{
|
||||
sb.AppendLine(setup);
|
||||
}
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
sb.Append(GetSqlBreakdown());
|
||||
|
||||
if (includeSetupFinish)
|
||||
{
|
||||
// Write out any finish clauses
|
||||
sb.AppendLine();
|
||||
sb.AppendLine();
|
||||
foreach (string finish in FinishClauses)
|
||||
{
|
||||
sb.AppendLine(finish);
|
||||
}
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the SQL query as a string.
|
||||
/// </summary>
|
||||
/// <returns>The SQL query string.</returns>
|
||||
public override string ToString()
|
||||
=> GetSql();
|
||||
|
||||
/// <summary>
|
||||
/// Creates a deep clone of this object.
|
||||
/// </summary>
|
||||
/// <returns>A cloned instance.</returns>
|
||||
public object Clone()
|
||||
{
|
||||
// Create a shallow copy
|
||||
var clone = (SqlBreakdownBase)MemberwiseClone();
|
||||
|
||||
// Deep copy the SetupClauses list
|
||||
clone.SetupClauses = new List<string>(SetupClauses);
|
||||
|
||||
// Deep copy the FinishClauses ArrayList
|
||||
clone.FinishClauses = new ArrayList(FinishClauses);
|
||||
|
||||
return clone;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,492 @@
|
||||
using System.Text;
|
||||
using Strata.SqlTools.SqlBreakdown.Interfaces;
|
||||
using Strata.SqlTools.SqlBreakdown.Interfaces.QueryEngine;
|
||||
|
||||
namespace Strata.SqlTools.SqlBreakdown.Classes;
|
||||
|
||||
/// <summary>
|
||||
/// Manages a collection of multiple SQL breakdown objects and provides parsing for batch SQL statements.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This class handles SQL statements containing multiple queries separated by GO statements or semicolons,
|
||||
/// allowing efficient management and retrieval of multiple SQL breakdowns as a unified collection.
|
||||
/// Implements ICollection<ISqlBreakdown> to provide standard collection semantics and LINQ support.
|
||||
/// </remarks>
|
||||
[Serializable]
|
||||
public class SqlBreakdownCollection : ICollection<ISqlBreakdown>
|
||||
{
|
||||
private readonly List<ISqlBreakdown> _breakdowns;
|
||||
private string _separator = "GO";
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SqlBreakdownCollection"/> class.
|
||||
/// </summary>
|
||||
public SqlBreakdownCollection()
|
||||
{
|
||||
_breakdowns = new List<ISqlBreakdown>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SqlBreakdownCollection"/> class with initial breakdowns.
|
||||
/// </summary>
|
||||
/// <param name="breakdowns">The initial collection of SQL breakdowns.</param>
|
||||
public SqlBreakdownCollection(IEnumerable<ISqlBreakdown> breakdowns)
|
||||
{
|
||||
_breakdowns = new List<ISqlBreakdown>(breakdowns ?? Enumerable.Empty<ISqlBreakdown>());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the collection of SQL breakdowns.
|
||||
/// </summary>
|
||||
public IReadOnlyList<ISqlBreakdown> Breakdowns => _breakdowns.AsReadOnly();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the collection of raw SQL statements from all breakdowns.
|
||||
/// </summary>
|
||||
public IReadOnlyList<string> RawStatements => _breakdowns
|
||||
.Where(b => !string.IsNullOrWhiteSpace(b.RawSql))
|
||||
.Select(b => b.RawSql!)
|
||||
.ToList()
|
||||
.AsReadOnly();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the count of SQL breakdowns in the collection.
|
||||
/// </summary>
|
||||
public int Count => _breakdowns.Count;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the collection is empty.
|
||||
/// </summary>
|
||||
public bool IsEmpty => _breakdowns.Count == 0;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the collection is read-only.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This collection is not read-only; items can be added and removed.
|
||||
/// </remarks>
|
||||
public bool IsReadOnly => false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the separator used for splitting and combining SQL statements.
|
||||
/// The default separator is "GO" (common in SQL Server and T-SQL).
|
||||
/// You can set this to ";" or other separators based on your SQL dialect.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When set, this separator will be used as the default for ParseBatch, GetCombinedSql, and GetBatchSql operations.
|
||||
/// Individual method calls can still override this default by providing an explicit separator argument.
|
||||
/// </remarks>
|
||||
public string Separator
|
||||
{
|
||||
get => _separator;
|
||||
set => _separator = string.IsNullOrWhiteSpace(value) ? "GO" : value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a single SQL breakdown to the collection.
|
||||
/// </summary>
|
||||
/// <param name="breakdown">The breakdown to add.</param>
|
||||
/// <exception cref="ArgumentNullException">Thrown when breakdown is null.</exception>
|
||||
public void Add(ISqlBreakdown breakdown)
|
||||
{
|
||||
if (breakdown == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(breakdown));
|
||||
}
|
||||
|
||||
_breakdowns.Add(breakdown);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds multiple SQL breakdowns to the collection.
|
||||
/// </summary>
|
||||
/// <param name="breakdowns">The breakdowns to add.</param>
|
||||
/// <exception cref="ArgumentNullException">Thrown when breakdowns is null.</exception>
|
||||
public void AddRange(IEnumerable<ISqlBreakdown> breakdowns)
|
||||
{
|
||||
if (breakdowns == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(breakdowns));
|
||||
}
|
||||
|
||||
_breakdowns.AddRange(breakdowns);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a SQL breakdown from the collection.
|
||||
/// </summary>
|
||||
/// <param name="breakdown">The breakdown to remove.</param>
|
||||
/// <returns>True if the breakdown was removed; otherwise, false.</returns>
|
||||
public bool Remove(ISqlBreakdown breakdown)
|
||||
{
|
||||
return _breakdowns.Remove(breakdown);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes all SQL breakdowns from the collection.
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
{
|
||||
_breakdowns.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a batch SQL statement containing multiple queries and populates the collection.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Splits the SQL batch by the configured separator (default: GO) or provided separator argument.
|
||||
/// The GO statement is a batch separator commonly used in SQL Server and T-SQL.
|
||||
/// Each parsed statement is stored as a RawSqlBreakdown in the collection.
|
||||
/// </remarks>
|
||||
/// <param name="sqlBatch">The batch SQL statement to parse.</param>
|
||||
/// <param name="separator">Optional separator to use for splitting. If null, uses the Separator property.</param>
|
||||
/// <exception cref="ArgumentNullException">Thrown when sqlBatch is null.</exception>
|
||||
public void ParseBatch(string sqlBatch, string? separator = null)
|
||||
{
|
||||
if (sqlBatch == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(sqlBatch));
|
||||
}
|
||||
|
||||
Clear();
|
||||
|
||||
var effectiveSeparator = separator ?? _separator;
|
||||
|
||||
// Split by configured separator
|
||||
var statements = SplitBySeparators(sqlBatch, effectiveSeparator);
|
||||
|
||||
// Create RawSqlBreakdown objects for each statement
|
||||
foreach (var statement in statements)
|
||||
{
|
||||
var trimmed = statement.Trim();
|
||||
if (!string.IsNullOrWhiteSpace(trimmed))
|
||||
{
|
||||
var breakdown = new RawSqlBreakdown { RawSql = trimmed };
|
||||
_breakdowns.Add(breakdown);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the combined SQL from all breakdowns in the collection.
|
||||
/// </summary>
|
||||
/// <param name="includeSetupFinish">Whether to include setup and finish clauses for each breakdown.</param>
|
||||
/// <param name="separator">The separator to use between SQL statements. If null, uses the Separator property.</param>
|
||||
/// <returns>The combined SQL string from all breakdowns.</returns>
|
||||
public string GetCombinedSql(bool includeSetupFinish = true, string? separator = null)
|
||||
{
|
||||
if (_breakdowns.Count == 0)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var effectiveSeparator = separator ?? _separator;
|
||||
var sb = new StringBuilder();
|
||||
|
||||
for (int i = 0; i < _breakdowns.Count; i++)
|
||||
{
|
||||
var sql = _breakdowns[i].GetSql(includeSetupFinish);
|
||||
sb.Append(sql);
|
||||
|
||||
// Add separator between statements (but not after the last one)
|
||||
if (i < _breakdowns.Count - 1)
|
||||
{
|
||||
sb.AppendLine();
|
||||
sb.AppendLine(effectiveSeparator);
|
||||
sb.AppendLine();
|
||||
}
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the raw SQL statements as a batch (joined with the configured separator).
|
||||
/// </summary>
|
||||
/// <param name="separator">The separator to use between SQL statements. If null, uses the Separator property.</param>
|
||||
/// <returns>The combined raw SQL statements.</returns>
|
||||
public string GetBatchSql(string? separator = null)
|
||||
{
|
||||
var rawStatements = RawStatements;
|
||||
if (rawStatements.Count == 0)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var effectiveSeparator = separator ?? _separator;
|
||||
return string.Join($"{Environment.NewLine}{effectiveSeparator}{Environment.NewLine}", rawStatements);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the collection contains a specific breakdown.
|
||||
/// </summary>
|
||||
/// <param name="item">The breakdown to locate.</param>
|
||||
/// <returns>True if the breakdown is found; otherwise, false.</returns>
|
||||
public bool Contains(ISqlBreakdown item)
|
||||
{
|
||||
return _breakdowns.Contains(item);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies the elements of the collection to an array, starting at a particular array index.
|
||||
/// </summary>
|
||||
/// <param name="array">The destination array.</param>
|
||||
/// <param name="arrayIndex">The zero-based index in the array at which copying begins.</param>
|
||||
/// <exception cref="ArgumentNullException">Thrown when array is null.</exception>
|
||||
/// <exception cref="ArgumentOutOfRangeException">Thrown when arrayIndex is out of range.</exception>
|
||||
/// <exception cref="ArgumentException">Thrown when there is not enough space in the array.</exception>
|
||||
public void CopyTo(ISqlBreakdown[] array, int arrayIndex)
|
||||
{
|
||||
_breakdowns.CopyTo(array, arrayIndex);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a breakdown at the specified index.
|
||||
/// </summary>
|
||||
/// <param name="index">The index of the breakdown.</param>
|
||||
/// <returns>The breakdown at the specified index.</returns>
|
||||
/// <exception cref="ArgumentOutOfRangeException">Thrown when index is out of range.</exception>
|
||||
public ISqlBreakdown GetAt(int index)
|
||||
{
|
||||
if (index < 0 || index >= _breakdowns.Count)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(index), $"Index {index} is out of range for collection with {_breakdowns.Count} items.");
|
||||
}
|
||||
|
||||
return _breakdowns[index];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the first breakdown matching the given predicate.
|
||||
/// </summary>
|
||||
/// <param name="predicate">The predicate to match.</param>
|
||||
/// <returns>The first matching breakdown, or null if not found.</returns>
|
||||
public ISqlBreakdown? FirstOrDefault(Func<ISqlBreakdown, bool> predicate)
|
||||
{
|
||||
return _breakdowns.FirstOrDefault(predicate ?? throw new ArgumentNullException(nameof(predicate)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns an enumerator that iterates through the breakdown collection.
|
||||
/// </summary>
|
||||
/// <returns>An enumerator for the collection.</returns>
|
||||
public IEnumerator<ISqlBreakdown> GetEnumerator()
|
||||
{
|
||||
return _breakdowns.GetEnumerator();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns an enumerator that iterates through the breakdown collection.
|
||||
/// </summary>
|
||||
/// <returns>An enumerator for the collection.</returns>
|
||||
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
|
||||
{
|
||||
return GetEnumerator();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the raw statement at the specified index.
|
||||
/// </summary>
|
||||
/// <param name="index">The index of the raw statement.</param>
|
||||
/// <returns>The raw statement at the specified index.</returns>
|
||||
/// <exception cref="ArgumentOutOfRangeException">Thrown when index is out of range.</exception>
|
||||
public string GetRawStatementAt(int index)
|
||||
{
|
||||
var rawStatements = RawStatements;
|
||||
if (index < 0 || index >= rawStatements.Count)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(index), $"Index {index} is out of range for raw statements collection with {rawStatements.Count} items.");
|
||||
}
|
||||
|
||||
return rawStatements[index];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the combined SQL string representation of all breakdowns.
|
||||
/// </summary>
|
||||
/// <returns>The combined SQL string.</returns>
|
||||
public override string ToString()
|
||||
{
|
||||
return GetCombinedSql();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Splits SQL batch by the specified separator character/string.
|
||||
/// </summary>
|
||||
/// <param name="sqlBatch">The SQL batch to split.</param>
|
||||
/// <param name="separator">The separator to use for splitting (e.g., "GO" or ";").</param>
|
||||
/// <returns>An array of SQL statements.</returns>
|
||||
private static string[] SplitBySeparators(string sqlBatch, string separator)
|
||||
{
|
||||
var statements = new List<string>();
|
||||
var currentStatement = new StringBuilder();
|
||||
|
||||
using (var reader = new StringReader(sqlBatch))
|
||||
{
|
||||
string? line;
|
||||
while ((line = reader.ReadLine()) != null)
|
||||
{
|
||||
// Check if line is a separator statement (case-insensitive for "GO", exact for others, possibly with whitespace)
|
||||
var trimmedLine = line.Trim();
|
||||
var isSeparator = separator.Equals("GO", StringComparison.OrdinalIgnoreCase)
|
||||
? trimmedLine.Equals("GO", StringComparison.OrdinalIgnoreCase)
|
||||
: trimmedLine.Equals(separator);
|
||||
|
||||
if (isSeparator)
|
||||
{
|
||||
// Save the current statement if it's not empty
|
||||
var statement = currentStatement.ToString().Trim();
|
||||
if (!string.IsNullOrWhiteSpace(statement))
|
||||
{
|
||||
statements.Add(statement);
|
||||
}
|
||||
|
||||
currentStatement.Clear();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Add line to current statement
|
||||
if (currentStatement.Length > 0)
|
||||
{
|
||||
currentStatement.AppendLine();
|
||||
}
|
||||
|
||||
currentStatement.Append(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add the final statement if it's not empty
|
||||
var finalStatement = currentStatement.ToString().Trim();
|
||||
if (!string.IsNullOrWhiteSpace(finalStatement))
|
||||
{
|
||||
statements.Add(finalStatement);
|
||||
}
|
||||
|
||||
return statements.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all unique parameters across all breakdowns in the collection.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This aggregates parameters from all QueryBreakdown objects in the collection.
|
||||
/// Parameters are uniquely identified by name (case-insensitive comparison).
|
||||
/// </remarks>
|
||||
/// <returns>A collection of unique parameters from all breakdowns.</returns>
|
||||
public IEnumerable<(string Name, object? Value)> GetAllUniqueParameters()
|
||||
{
|
||||
var parameterDict = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var breakdown in _breakdowns.OfType<IQueryBreakdown>())
|
||||
{
|
||||
foreach (var param in breakdown.ParameterList)
|
||||
{
|
||||
// Add or update parameter (later occurrences override earlier ones)
|
||||
parameterDict[param.Name] = param.Value;
|
||||
}
|
||||
}
|
||||
|
||||
return parameterDict.Select(kvp => (kvp.Key, kvp.Value));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a dictionary of all unique parameter names and their values across all breakdowns.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is useful for parameterized query execution where you need all parameters in one place.
|
||||
/// Parameters are uniquely identified by name (case-insensitive comparison).
|
||||
/// </remarks>
|
||||
/// <returns>A dictionary mapping parameter names to their values.</returns>
|
||||
public Dictionary<string, object?> GetParameterDictionary()
|
||||
{
|
||||
var parameters = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var breakdown in _breakdowns.OfType<IQueryBreakdown>())
|
||||
{
|
||||
foreach (var param in breakdown.ParameterList)
|
||||
{
|
||||
parameters[param.Name] = param.Value;
|
||||
}
|
||||
}
|
||||
|
||||
return parameters;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets parameter names used in queries that have a specific value.
|
||||
/// </summary>
|
||||
/// <param name="value">The parameter value to search for.</param>
|
||||
/// <returns>Parameter names that have the specified value.</returns>
|
||||
public IEnumerable<string> GetParametersWithValue(object? value)
|
||||
{
|
||||
return GetAllUniqueParameters()
|
||||
.Where(p => (p.Value == null && value == null) ||
|
||||
(p.Value != null && p.Value.Equals(value)))
|
||||
.Select(p => p.Name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a parameter with the specified name exists in any breakdown.
|
||||
/// </summary>
|
||||
/// <param name="parameterName">The parameter name to check.</param>
|
||||
/// <returns>True if the parameter exists; otherwise, false.</returns>
|
||||
public bool HasParameter(string parameterName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(parameterName))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return _breakdowns.OfType<IQueryBreakdown>()
|
||||
.SelectMany(b => b.ParameterList)
|
||||
.Any(p => p.Name.Equals(parameterName, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the value of a parameter by name from the first breakdown that contains it.
|
||||
/// </summary>
|
||||
/// <param name="parameterName">The parameter name to retrieve.</param>
|
||||
/// <param name="value">The parameter value, or null if not found.</param>
|
||||
/// <returns>True if the parameter was found; otherwise, false.</returns>
|
||||
public bool TryGetParameterValue(string parameterName, out object? value)
|
||||
{
|
||||
value = null;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(parameterName))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var param = _breakdowns.OfType<IQueryBreakdown>()
|
||||
.SelectMany(b => b.ParameterList)
|
||||
.FirstOrDefault(p => p.Name.Equals(parameterName, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (param != null)
|
||||
{
|
||||
value = param.Value;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the count of unique parameters across all breakdowns.
|
||||
/// </summary>
|
||||
/// <returns>The number of unique parameters.</returns>
|
||||
public int GetParameterCount()
|
||||
{
|
||||
return GetAllUniqueParameters().Count();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all parameter names used in the collection (case-insensitive unique list).
|
||||
/// </summary>
|
||||
/// <returns>An enumerable of unique parameter names.</returns>
|
||||
public IEnumerable<string> GetParameterNames()
|
||||
{
|
||||
return GetAllUniqueParameters().Select(p => p.Name).Distinct(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
namespace Strata.SqlTools.SqlBreakdown.Classes;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a SQL clause with its text and any associated comments.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The Comment property can be used to preserve SQL comments that were associated with this clause
|
||||
/// during parsing. This allows for round-trip parsing where comments are not lost.
|
||||
/// </remarks>
|
||||
public class SqlClause : ISqlClause
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the SQL clause text (without comments).
|
||||
/// </summary>
|
||||
public string? Clause { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets any comments associated with this clause.
|
||||
/// </summary>
|
||||
public string? Comment { get; set; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
namespace Strata.SqlTools.SqlBreakdown.Classes;
|
||||
|
||||
/// <summary>
|
||||
/// Helper structure to hold parsed SQL clauses.
|
||||
/// </summary>
|
||||
public sealed class SqlClauses
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the SELECT clause. Can be parsed into Expression objects.
|
||||
/// </summary>
|
||||
public ISqlExpressionClause? SelectClause { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the FROM clause.
|
||||
/// </summary>
|
||||
public ISqlClause? FromClause { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the WHERE clause. Can be parsed into Expression objects.
|
||||
/// </summary>
|
||||
public ISqlExpressionClause? WhereClause { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the GROUP BY clause. Can be parsed into Expression objects.
|
||||
/// </summary>
|
||||
public ISqlExpressionClause? GroupByClause { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the HAVING clause. Can be parsed into Expression objects.
|
||||
/// </summary>
|
||||
public ISqlExpressionClause? HavingClause { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the ORDER BY clause. Can be parsed into Expression objects.
|
||||
/// </summary>
|
||||
public ISqlExpressionClause? OrderByClause { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a copy of this SqlClauses object.
|
||||
/// </summary>
|
||||
/// <returns>A new SqlClauses object with the same clause references.</returns>
|
||||
public SqlClauses Copy()
|
||||
{
|
||||
return new SqlClauses
|
||||
{
|
||||
SelectClause = SelectClause,
|
||||
FromClause = FromClause,
|
||||
WhereClause = WhereClause,
|
||||
GroupByClause = GroupByClause,
|
||||
HavingClause = HavingClause,
|
||||
OrderByClause = OrderByClause
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
using System.Text;
|
||||
using Strata.SqlTools.SqlBreakdown.Expressions;
|
||||
using Strata.SqlTools.SqlBreakdown.Interfaces;
|
||||
|
||||
namespace Strata.SqlTools.SqlBreakdown.Classes;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a SQL clause that can be parsed into Expression objects.
|
||||
/// Used for SELECT and WHERE clauses that contain expressions.
|
||||
/// </summary>
|
||||
public class SqlExpressionClause : SqlClause, ISqlExpressionClause
|
||||
{
|
||||
private readonly bool _splitOnComma;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SqlExpressionClause"/> class.
|
||||
/// </summary>
|
||||
/// <param name="splitOnComma">
|
||||
/// If true, splits the clause on commas (for SELECT clauses with multiple columns).
|
||||
/// If false, treats the entire clause as a single expression (for WHERE clauses).
|
||||
/// </param>
|
||||
public SqlExpressionClause(bool splitOnComma = false)
|
||||
{
|
||||
_splitOnComma = splitOnComma;
|
||||
Clause = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses the clause into one or more Expression objects.
|
||||
/// </summary>
|
||||
/// <param name="parser">The statement expression parser to use for parsing.</param>
|
||||
/// <returns>An enumerable collection of parsed Expression objects.</returns>
|
||||
/// <exception cref="FormatException">Thrown when the clause cannot be parsed.</exception>
|
||||
public IEnumerable<Expression> GetExpressions(IStatementExpressionParser parser)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(Clause))
|
||||
{
|
||||
return Enumerable.Empty<Expression>();
|
||||
}
|
||||
|
||||
var expressions = new List<Expression>();
|
||||
|
||||
if (_splitOnComma)
|
||||
{
|
||||
// Split SELECT clause by commas (respecting parentheses and quoted strings)
|
||||
var items = SplitOnComma(Clause);
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(item))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var expression = parser.Parse(item.Trim());
|
||||
expressions.Add(expression);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new FormatException($"Failed to parse expression '{item}': {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Parse entire clause as single expression (for WHERE, HAVING, etc.)
|
||||
try
|
||||
{
|
||||
var expression = parser.Parse(Clause);
|
||||
expressions.Add(expression);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new FormatException($"Failed to parse expression: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
return expressions;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Splits a clause by commas while respecting parentheses, quotes, and brackets.
|
||||
/// </summary>
|
||||
/// <param name="clause">The clause to split.</param>
|
||||
/// <returns>An enumerable collection of individual items.</returns>
|
||||
private static IEnumerable<string> SplitOnComma(string clause)
|
||||
{
|
||||
var items = new List<string>();
|
||||
var current = new StringBuilder();
|
||||
var parenDepth = 0;
|
||||
var inSingleQuote = false;
|
||||
var inDoubleQuote = false;
|
||||
var inBracket = false;
|
||||
|
||||
for (int i = 0; i < clause.Length; i++)
|
||||
{
|
||||
var ch = clause[i];
|
||||
|
||||
// Handle escape sequences
|
||||
if (i < clause.Length - 1 && ch == '\\')
|
||||
{
|
||||
current.Append(ch);
|
||||
current.Append(clause[++i]);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Toggle quote states
|
||||
if (ch == '\'' && !inDoubleQuote && !inBracket)
|
||||
{
|
||||
inSingleQuote = !inSingleQuote;
|
||||
}
|
||||
else if (ch == '"' && !inSingleQuote && !inBracket)
|
||||
{
|
||||
inDoubleQuote = !inDoubleQuote;
|
||||
}
|
||||
else if (ch == '[' && !inSingleQuote && !inDoubleQuote)
|
||||
{
|
||||
inBracket = true;
|
||||
}
|
||||
else if (ch == ']' && inBracket && !inSingleQuote && !inDoubleQuote)
|
||||
{
|
||||
inBracket = false;
|
||||
}
|
||||
|
||||
// Track parenthesis depth
|
||||
if (!inSingleQuote && !inDoubleQuote && !inBracket)
|
||||
{
|
||||
if (ch == '(')
|
||||
{
|
||||
parenDepth++;
|
||||
}
|
||||
else if (ch == ')')
|
||||
{
|
||||
parenDepth--;
|
||||
}
|
||||
}
|
||||
|
||||
// Split on comma only when not inside quotes, brackets, or parentheses
|
||||
if (ch == ',' && !inSingleQuote && !inDoubleQuote && !inBracket && parenDepth == 0)
|
||||
{
|
||||
items.Add(current.ToString());
|
||||
current.Clear();
|
||||
}
|
||||
else
|
||||
{
|
||||
current.Append(ch);
|
||||
}
|
||||
}
|
||||
|
||||
// Add the last item
|
||||
if (current.Length > 0)
|
||||
{
|
||||
items.Add(current.ToString());
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
using System.Text;
|
||||
using Strata.SqlTools.SqlBreakdown.Interfaces.Core;
|
||||
|
||||
namespace Strata.SqlTools.SqlBreakdown.Classes;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a SQL filter with an expression and parameters.
|
||||
/// Implements SQL appendable and SQL interfaces for query building.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class SqlFilter : ISqlAppendable
|
||||
{
|
||||
private readonly StringBuilder _sqlExpression;
|
||||
private readonly Dictionary<string, object> _parameterValues;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SqlFilter"/> class.
|
||||
/// </summary>
|
||||
public SqlFilter()
|
||||
{
|
||||
_sqlExpression = new StringBuilder();
|
||||
_parameterValues = new Dictionary<string, object>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SqlFilter"/> class with an expression and parameters.
|
||||
/// </summary>
|
||||
/// <param name="expression">The SQL filter expression.</param>
|
||||
/// <param name="parameterNameValue">Parameter name-value pairs (must be even number of elements).</param>
|
||||
public SqlFilter(string expression, params object[] parameterNameValue)
|
||||
{
|
||||
_sqlExpression = new StringBuilder(expression);
|
||||
_parameterValues = new Dictionary<string, object>();
|
||||
|
||||
if (parameterNameValue.Length % 2 != 0)
|
||||
{
|
||||
throw new InvalidOperationException("The paramarray should have even #s");
|
||||
}
|
||||
|
||||
for (int i = 0; i < parameterNameValue.Length; i += 2)
|
||||
{
|
||||
_parameterValues.Add(parameterNameValue[i].ToString()!, parameterNameValue[i + 1]);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SqlFilter"/> class by copying another filter.
|
||||
/// </summary>
|
||||
/// <param name="filter">The filter to copy.</param>
|
||||
public SqlFilter(SqlFilter filter)
|
||||
{
|
||||
_sqlExpression = new StringBuilder(filter.SqlExpression);
|
||||
_parameterValues = new Dictionary<string, object>();
|
||||
|
||||
foreach (var key in filter.ParameterValues.Keys)
|
||||
{
|
||||
_parameterValues.Add(key, filter.ParameterValues[key]);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the SQL expression.
|
||||
/// </summary>
|
||||
public string SqlExpression
|
||||
{
|
||||
get => _sqlExpression.ToString();
|
||||
set => _sqlExpression.Clear().Append(value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the parameter values dictionary.
|
||||
/// </summary>
|
||||
public Dictionary<string, object> ParameterValues => _parameterValues;
|
||||
|
||||
/// <summary>
|
||||
/// Adds a parameter to the filter.
|
||||
/// </summary>
|
||||
/// <param name="name">The parameter name.</param>
|
||||
/// <param name="value">The parameter value.</param>
|
||||
public void AddParameter(string name, object value)
|
||||
{
|
||||
_parameterValues.Add(name, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Combines this filter with another filter using AND logic.
|
||||
/// </summary>
|
||||
/// <param name="filter">The filter to AND with this one.</param>
|
||||
/// <exception cref="InvalidOperationException">Thrown when parameters conflict.</exception>
|
||||
public void AndAnotherFilter(ISql filter)
|
||||
{
|
||||
if (string.IsNullOrEmpty(SqlExpression))
|
||||
{
|
||||
SqlExpression = filter.SqlExpression;
|
||||
}
|
||||
else
|
||||
{
|
||||
SqlExpression = $"({SqlExpression}) AND ({filter.SqlExpression})";
|
||||
}
|
||||
|
||||
foreach (var kvPair in filter.ParameterValues)
|
||||
{
|
||||
if (!ParameterValues.TryGetValue(kvPair.Key, out object? existing))
|
||||
{
|
||||
// Add if new param
|
||||
ParameterValues.Add(kvPair.Key, kvPair.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!existing.Equals(kvPair.Value))
|
||||
{
|
||||
throw new InvalidOperationException("Combining two filters that use the same parameter but have different values is illegal.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends a string to the SQL expression.
|
||||
/// </summary>
|
||||
/// <param name="value">The string to append.</param>
|
||||
public void Append(string value)
|
||||
{
|
||||
_sqlExpression.Append(value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all debug information including the SQL and parameters.
|
||||
/// </summary>
|
||||
/// <returns>A debug information string.</returns>
|
||||
public string GetAllDebugInfo()
|
||||
{
|
||||
var sb = new System.Text.StringBuilder();
|
||||
|
||||
// Add parameter declarations
|
||||
foreach (var param in ParameterValues)
|
||||
{
|
||||
sb.AppendLine($"DECLARE {param.Key} nvarchar(max)");
|
||||
if (param.Value == null)
|
||||
{
|
||||
sb.AppendLine($"SET {param.Key} = null");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine($"SET {param.Key} = '{param.Value}'");
|
||||
}
|
||||
}
|
||||
|
||||
sb.AppendLine();
|
||||
sb.Append(SqlExpression);
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prints all debug information to the debug output.
|
||||
/// </summary>
|
||||
public void PrintAllDebugInfo()
|
||||
{
|
||||
System.Diagnostics.Debug.Print(GetAllDebugInfo());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
using Strata.SqlTools.SqlBreakdown.Extensions;
|
||||
|
||||
namespace Strata.SqlTools.SqlBreakdown.Classes;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a SQL FROM clause with optional JOIN clauses.
|
||||
/// </summary>
|
||||
public class SqlFrom
|
||||
{
|
||||
private readonly SqlTable _firstTable;
|
||||
private readonly List<SqlJoin> _joins;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SqlFrom"/> class.
|
||||
/// </summary>
|
||||
/// <param name="tableExpression">The primary table expression.</param>
|
||||
/// <param name="tableAlias">The primary table alias.</param>
|
||||
public SqlFrom(string tableExpression, string tableAlias)
|
||||
{
|
||||
_firstTable = new SqlTable(tableExpression, tableAlias);
|
||||
_joins = new List<SqlJoin>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a JOIN to the FROM clause.
|
||||
/// </summary>
|
||||
/// <param name="table2Expression">The table expression to join.</param>
|
||||
/// <param name="table2Alias">The alias for the joined table.</param>
|
||||
/// <param name="table1Column">The column from the primary table.</param>
|
||||
/// <param name="table2Column">The column from the joined table.</param>
|
||||
public void Join(string table2Expression, string table2Alias, string table1Column, string table2Column)
|
||||
{
|
||||
var theJoin = new SqlJoin(table2Expression, table2Alias, table1Column, table2Column);
|
||||
Join(theJoin);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a pre-constructed JOIN to the FROM clause.
|
||||
/// </summary>
|
||||
/// <param name="join">The join to add.</param>
|
||||
public void Join(SqlJoin join)
|
||||
{
|
||||
_joins.Add(join);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the SQL FROM clause as a string.
|
||||
/// </summary>
|
||||
/// <returns>The FROM clause with all JOINs.</returns>
|
||||
public override string ToString()
|
||||
{
|
||||
var output = new StringBuilderEx();
|
||||
output.AppendFormat($"\t{_firstTable.TableExpression} {_firstTable.TableAlias}\n");
|
||||
|
||||
foreach (var join in _joins)
|
||||
{
|
||||
output.AppendFormat($"\tINNER JOIN {join.TableExpression} {join.TableAlias} ON {join.GetJoinOn(_firstTable.TableAlias)}\n");
|
||||
}
|
||||
|
||||
return output.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
namespace Strata.SqlTools.SqlBreakdown.Classes;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a SQL JOIN clause with table information and join conditions.
|
||||
/// </summary>
|
||||
public class SqlJoin
|
||||
{
|
||||
private readonly string _joinOn;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SqlJoin"/> class.
|
||||
/// </summary>
|
||||
/// <param name="tableExpression">The table expression.</param>
|
||||
/// <param name="tableAlias">The table alias.</param>
|
||||
/// <param name="table1Column">The column from the first table.</param>
|
||||
/// <param name="table2Column">The column from the second table.</param>
|
||||
public SqlJoin(string tableExpression, string tableAlias, string table1Column, string table2Column)
|
||||
{
|
||||
TableExpression = tableExpression;
|
||||
TableAlias = tableAlias;
|
||||
_joinOn = $"{{0}}.{table1Column} = {{1}}.{table2Column}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the table expression.
|
||||
/// </summary>
|
||||
public string TableExpression { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the table alias.
|
||||
/// </summary>
|
||||
public string TableAlias { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the JOIN ON clause formatted with the table aliases.
|
||||
/// </summary>
|
||||
/// <param name="table1Alias">The alias of the first table.</param>
|
||||
/// <returns>The formatted JOIN ON clause.</returns>
|
||||
public string GetJoinOn(string table1Alias)
|
||||
=> string.Format(_joinOn, table1Alias, TableAlias);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
namespace Strata.SqlTools.SqlBreakdown.Classes;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a SQL table with its expression and alias.
|
||||
/// </summary>
|
||||
public class SqlTable
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SqlTable"/> class.
|
||||
/// </summary>
|
||||
/// <param name="tableExpression">The table expression.</param>
|
||||
/// <param name="tableAlias">The table alias.</param>
|
||||
public SqlTable(string tableExpression, string tableAlias)
|
||||
{
|
||||
TableExpression = tableExpression;
|
||||
TableAlias = tableAlias;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the table expression.
|
||||
/// </summary>
|
||||
public string TableExpression { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the table alias.
|
||||
/// </summary>
|
||||
public string TableAlias { get; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
using Strata.SqlTools.SqlBreakdown.Enums.SQL;
|
||||
|
||||
namespace Strata.SqlTools.SqlBreakdown.Classes;
|
||||
|
||||
public class Token
|
||||
{
|
||||
public TokenType Type { get; }
|
||||
public string Value { get; }
|
||||
|
||||
public Token(TokenType type, string value)
|
||||
{
|
||||
Type = type;
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public static Token None()
|
||||
{
|
||||
return new Token(TokenType.None, "");
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
using Strata.SqlTools.SqlBreakdown.Interfaces.QueryEngine;
|
||||
|
||||
namespace Strata.SqlTools.SqlBreakdown.Classes;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a WITH clause (Common Table Expression) with its structure and parsed query.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// The Clause property inherited from SqlClause contains the full CTE definition text for round-trip parsing.
|
||||
/// The TableName property identifies the CTE, while Sql contains the parsed query structure.
|
||||
/// The Query property provides access to the full query breakdown including parameters.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>Recursive CTE Limitations and Requirements:</b>
|
||||
/// <list type="bullet">
|
||||
/// <item><description>Must have IsRecursive = true</description></item>
|
||||
/// <item><description>Must provide a RecursiveQuery (the UNION ALL recursive member)</description></item>
|
||||
/// <item><description>The Query property represents the anchor member (non-recursive base case)</description></item>
|
||||
/// <item><description>Both anchor and recursive members must return the same number of columns with compatible types</description></item>
|
||||
/// <item><description>ColumnList is recommended but not required; helps ensure column consistency</description></item>
|
||||
/// <item><description>RecursiveQuery typically references the CTE's TableName in its FROM clause</description></item>
|
||||
/// <item><description>Always include a termination condition in the recursive query's WHERE clause to prevent infinite loops</description></item>
|
||||
/// <item><description>Parameters are inherited from the ancestor query; main query parameters override CTE parameters</description></item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public class WithClause : SqlClause, IWithClause
|
||||
{
|
||||
private SqlClauses? _sql;
|
||||
private IQueryBreakdown? _query;
|
||||
private IQueryBreakdown? _recursiveQuery;
|
||||
private List<string>? _columnList;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the table name for the CTE.
|
||||
/// </summary>
|
||||
public string TableName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the parsed SQL clauses representing the CTE query.
|
||||
/// When getting, if Query is not null, returns SqlClauses constructed from the Query's properties.
|
||||
/// When setting, if Query is not null, applies the clauses to the Query for validation/restructuring.
|
||||
/// Otherwise stores the value for later use.
|
||||
/// </summary>
|
||||
public SqlClauses? Sql
|
||||
{
|
||||
get => Query?.GetClauses() ?? _sql;
|
||||
set
|
||||
{
|
||||
_sql = value;
|
||||
|
||||
// If Query is already set and we're setting new Sql clauses, apply them to the Query
|
||||
if (_query != null && value != null)
|
||||
{
|
||||
_query.ApplyClauses(value);
|
||||
|
||||
// Clear the stored value since it's now in the Query
|
||||
_sql = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the query breakdown representing the CTE.
|
||||
/// This provides access to the full query structure including parameters.
|
||||
/// When setting, if Sql clauses were previously set, they will be applied to the Query.
|
||||
/// </summary>
|
||||
public IQueryBreakdown? Query
|
||||
{
|
||||
get => _query;
|
||||
set
|
||||
{
|
||||
_query = value;
|
||||
|
||||
// If we have stored SQL clauses and a new Query is being set, apply the clauses to it
|
||||
if (_query != null && _sql != null)
|
||||
{
|
||||
_query.ApplyClauses(_sql);
|
||||
|
||||
// Clear the stored SQL clauses since they're now part of the Query
|
||||
_sql = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this CTE is recursive.
|
||||
/// Recursive CTEs require a UNION ALL pattern with an anchor member and recursive member.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When set to true, you must also provide a RecursiveQuery. The Query property represents the
|
||||
/// anchor member (base case), while RecursiveQuery represents the recursive member that typically
|
||||
/// references the CTE's own TableName. Always ensure the recursive query has a proper termination
|
||||
/// condition to avoid infinite recursion.
|
||||
/// </remarks>
|
||||
public bool IsRecursive { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the recursive query (UNION ALL part) for recursive CTEs.
|
||||
/// This query represents the recursive member that joins back to the CTE.
|
||||
/// Only applicable when IsRecursive is true.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// The recursive member typically:
|
||||
/// <list type="bullet">
|
||||
/// <item><description>References the CTE's TableName in its FROM clause</description></item>
|
||||
/// <item><description>Includes a JOIN or WHERE condition that advances the recursion</description></item>
|
||||
/// <item><description>Has a termination condition (e.g., depth limit, no more rows to process)</description></item>
|
||||
/// <item><description>Returns the same column count and compatible types as the anchor member</description></item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Example recursive scenario: traversing an organizational hierarchy where employees reference their managers.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public IQueryBreakdown? RecursiveQuery
|
||||
{
|
||||
get => _recursiveQuery;
|
||||
set => _recursiveQuery = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the explicit column list for the CTE.
|
||||
/// When specified, defines column names for the CTE that can differ from the underlying query columns.
|
||||
/// Example: WITH users (id, name, email) AS (...)
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Particularly useful for:
|
||||
/// <list type="bullet">
|
||||
/// <item><description>Recursive CTEs where consistent column naming is critical</description></item>
|
||||
/// <item><description>CTEs with complex expressions where column aliases may not be clear</description></item>
|
||||
/// <item><description>Providing meaningful column names for external consumers of the CTE</description></item>
|
||||
/// </list>
|
||||
/// The number of column names must match the number of columns in the SELECT clause.
|
||||
/// </remarks>
|
||||
public List<string>? ColumnList
|
||||
{
|
||||
get => _columnList;
|
||||
set => _columnList = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="WithClause"/> class.
|
||||
/// </summary>
|
||||
public WithClause()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="WithClause"/> class with a table name and query.
|
||||
/// </summary>
|
||||
/// <param name="tableName">The table name for the CTE.</param>
|
||||
/// <param name="query">The query breakdown for the CTE.</param>
|
||||
public WithClause(string tableName, IQueryBreakdown query)
|
||||
{
|
||||
TableName = tableName ?? throw new ArgumentNullException(nameof(tableName));
|
||||
Query = query ?? throw new ArgumentNullException(nameof(query));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="WithClause"/> class with a table name and SQL clauses.
|
||||
/// </summary>
|
||||
/// <param name="tableName">The table name for the CTE.</param>
|
||||
/// <param name="sql">The parsed SQL clauses for the CTE.</param>
|
||||
public WithClause(string tableName, SqlClauses sql)
|
||||
{
|
||||
TableName = tableName ?? throw new ArgumentNullException(nameof(tableName));
|
||||
Sql = sql ?? throw new ArgumentNullException(nameof(sql));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user