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,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&lt;ISqlBreakdown&gt; 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));
}
}