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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
namespace Strata.SqlTools.SqlBreakdown.Enums.SQL;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies SQL aggregate functions.
|
||||
/// </summary>
|
||||
public enum AggregateFunction
|
||||
{
|
||||
/// <summary>
|
||||
/// SUM aggregate function.
|
||||
/// </summary>
|
||||
Sum = 0,
|
||||
|
||||
/// <summary>
|
||||
/// COUNT aggregate function.
|
||||
/// </summary>
|
||||
Count = 1,
|
||||
|
||||
/// <summary>
|
||||
/// AVG (average) aggregate function.
|
||||
/// </summary>
|
||||
Avg = 2,
|
||||
|
||||
/// <summary>
|
||||
/// MIN (minimum) aggregate function.
|
||||
/// </summary>
|
||||
Min = 3,
|
||||
|
||||
/// <summary>
|
||||
/// MAX (maximum) aggregate function.
|
||||
/// </summary>
|
||||
Max = 4,
|
||||
|
||||
/// <summary>
|
||||
/// No aggregation.
|
||||
/// </summary>
|
||||
None = 5
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
namespace Strata.SqlTools.SqlBreakdown.Enums.SQL;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the type of SQL constraint.
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum ConstraintType
|
||||
{
|
||||
/// <summary>
|
||||
/// Default value constraint.
|
||||
/// </summary>
|
||||
DefaultValue = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Primary key constraint.
|
||||
/// </summary>
|
||||
PrimaryKey = 2,
|
||||
|
||||
/// <summary>
|
||||
/// Unique constraint.
|
||||
/// </summary>
|
||||
Unique = 4,
|
||||
|
||||
/// <summary>
|
||||
/// Foreign key constraint.
|
||||
/// </summary>
|
||||
ForeignKey = 8,
|
||||
|
||||
/// <summary>
|
||||
/// Check constraint.
|
||||
/// </summary>
|
||||
Check = 16,
|
||||
|
||||
/// <summary>
|
||||
/// All constraint types.
|
||||
/// </summary>
|
||||
All = 31
|
||||
}
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
namespace Strata.SqlTools.SqlBreakdown.Enums.SQL;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies SQL filter operations for WHERE clauses.
|
||||
/// </summary>
|
||||
public enum FilterOperation
|
||||
{
|
||||
/// <summary>
|
||||
/// Equality comparison (=).
|
||||
/// </summary>
|
||||
Equal = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Inequality comparison (!=).
|
||||
/// </summary>
|
||||
NotEqual = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Greater than comparison (>).
|
||||
/// </summary>
|
||||
GreaterThan = 2,
|
||||
|
||||
/// <summary>
|
||||
/// Greater than or equal to comparison (>=).
|
||||
/// </summary>
|
||||
GreaterThanEqualTo = 3,
|
||||
|
||||
/// <summary>
|
||||
/// Less than comparison (<).
|
||||
/// </summary>
|
||||
LessThan = 4,
|
||||
|
||||
/// <summary>
|
||||
/// Less than or equal to comparison (<=).
|
||||
/// </summary>
|
||||
LessThanEqualTo = 5,
|
||||
|
||||
/// <summary>
|
||||
/// Contains text using LIKE operator.
|
||||
/// </summary>
|
||||
Contains = 6,
|
||||
|
||||
/// <summary>
|
||||
/// Starts with text using LIKE operator.
|
||||
/// </summary>
|
||||
StartsWith = 7,
|
||||
|
||||
/// <summary>
|
||||
/// Ends with text using LIKE operator.
|
||||
/// </summary>
|
||||
EndsWith = 8,
|
||||
|
||||
/// <summary>
|
||||
/// IN operator for multiple values.
|
||||
/// </summary>
|
||||
In = 9,
|
||||
|
||||
/// <summary>
|
||||
/// NOT IN operator for multiple values.
|
||||
/// </summary>
|
||||
NotIn = 10,
|
||||
|
||||
/// <summary>
|
||||
/// Does not contain text using NOT LIKE operator.
|
||||
/// </summary>
|
||||
NotContains = 11,
|
||||
|
||||
/// <summary>
|
||||
/// BETWEEN operator for range comparison.
|
||||
/// </summary>
|
||||
Between = 12,
|
||||
|
||||
/// <summary>
|
||||
/// Numeric equality comparison.
|
||||
/// </summary>
|
||||
EqualNumeric = 13,
|
||||
|
||||
/// <summary>
|
||||
/// Numeric inequality comparison.
|
||||
/// </summary>
|
||||
NotEqualNumeric = 14,
|
||||
|
||||
/// <summary>
|
||||
/// NOT BETWEEN operator for range exclusion.
|
||||
/// </summary>
|
||||
NotBetween = 15,
|
||||
|
||||
/// <summary>
|
||||
/// BETWEEN operator for string range.
|
||||
/// </summary>
|
||||
BetweenStrings = 16,
|
||||
|
||||
/// <summary>
|
||||
/// Exclude filter operation.
|
||||
/// </summary>
|
||||
Exclude = 17,
|
||||
|
||||
/// <summary>
|
||||
/// BETWEEN operator for date range.
|
||||
/// </summary>
|
||||
BetweenDates = 18,
|
||||
|
||||
/// <summary>
|
||||
/// Does not start with text using NOT LIKE operator.
|
||||
/// </summary>
|
||||
DoesNotStartWith = 19,
|
||||
|
||||
/// <summary>
|
||||
/// BETWEEN operator for date filter range.
|
||||
/// </summary>
|
||||
BetweenDateFilter = 20
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace Strata.SqlTools.SqlBreakdown.Enums.SQL;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the type of SQL Server index.
|
||||
/// </summary>
|
||||
public enum IndexType
|
||||
{
|
||||
/// <summary>
|
||||
/// Clustered index - determines the physical order of data in the table.
|
||||
/// </summary>
|
||||
Clustered,
|
||||
|
||||
/// <summary>
|
||||
/// Non-clustered index - separate structure from the data rows.
|
||||
/// </summary>
|
||||
NonClustered
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
namespace Strata.SqlTools.SqlBreakdown.Enums.SQL;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies ordinal position from the end.
|
||||
/// </summary>
|
||||
public enum PositionFromEnd
|
||||
{
|
||||
/// <summary>
|
||||
/// No position specified.
|
||||
/// </summary>
|
||||
None = -1,
|
||||
|
||||
/// <summary>
|
||||
/// Last position.
|
||||
/// </summary>
|
||||
Last = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Second to last position.
|
||||
/// </summary>
|
||||
Second_to_Last = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Third to last position.
|
||||
/// </summary>
|
||||
Third_to_Last = 2,
|
||||
|
||||
/// <summary>
|
||||
/// Fourth to last position.
|
||||
/// </summary>
|
||||
Fourth_to_Last = 3,
|
||||
|
||||
/// <summary>
|
||||
/// Fifth to last position.
|
||||
/// </summary>
|
||||
Fifth_to_Last = 4
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
namespace Strata.SqlTools.SqlBreakdown.Enums.SQL;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies ordinal position from the beginning.
|
||||
/// </summary>
|
||||
public enum PositionFromFront
|
||||
{
|
||||
/// <summary>
|
||||
/// No position specified.
|
||||
/// </summary>
|
||||
None = -1,
|
||||
|
||||
/// <summary>
|
||||
/// First position.
|
||||
/// </summary>
|
||||
First = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Second position.
|
||||
/// </summary>
|
||||
Second = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Third position.
|
||||
/// </summary>
|
||||
Third = 2,
|
||||
|
||||
/// <summary>
|
||||
/// Fourth position.
|
||||
/// </summary>
|
||||
Fourth = 3,
|
||||
|
||||
/// <summary>
|
||||
/// Fifth position.
|
||||
/// </summary>
|
||||
Fifth = 4
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
namespace Strata.SqlTools.SqlBreakdown.Enums.SQL;
|
||||
|
||||
/// <summary>
|
||||
/// Represents simplified data type categories for SQL data types.
|
||||
/// </summary>
|
||||
public enum SimpleDataType
|
||||
{
|
||||
/// <summary>
|
||||
/// String or text data type.
|
||||
/// </summary>
|
||||
String,
|
||||
|
||||
/// <summary>
|
||||
/// Numeric data type (integers, decimals, floats).
|
||||
/// </summary>
|
||||
Numeric,
|
||||
|
||||
/// <summary>
|
||||
/// Date or date/time data type.
|
||||
/// </summary>
|
||||
Date,
|
||||
|
||||
/// <summary>
|
||||
/// Boolean data type.
|
||||
/// </summary>
|
||||
Boolean,
|
||||
|
||||
/// <summary>
|
||||
/// Globally unique identifier (GUID) data type.
|
||||
/// </summary>
|
||||
GUID
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace Strata.SqlTools.SqlBreakdown.Enums.SQL;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the sort direction for ORDER BY clauses.
|
||||
/// </summary>
|
||||
public enum SortDirection
|
||||
{
|
||||
/// <summary>
|
||||
/// Ascending sort order (ASC).
|
||||
/// </summary>
|
||||
Ascending = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Descending sort order (DESC).
|
||||
/// </summary>
|
||||
Descending = 1
|
||||
}
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
namespace Strata.SqlTools.SqlBreakdown.Enums.SQL;
|
||||
|
||||
/// <summary>
|
||||
/// SQL Server data types. Values match the xtype column from syscolumns.
|
||||
/// </summary>
|
||||
public enum SqlDataType
|
||||
{
|
||||
/// <summary>
|
||||
/// Unknown data type.
|
||||
/// </summary>
|
||||
Unknown = 0,
|
||||
|
||||
/// <summary>
|
||||
/// IMAGE data type (xtype = 34).
|
||||
/// </summary>
|
||||
Image = 34,
|
||||
|
||||
/// <summary>
|
||||
/// TEXT data type (xtype = 35).
|
||||
/// </summary>
|
||||
Text = 35,
|
||||
|
||||
/// <summary>
|
||||
/// UNIQUEIDENTIFIER data type (xtype = 36).
|
||||
/// </summary>
|
||||
UniqueIdentifier = 36,
|
||||
|
||||
/// <summary>
|
||||
/// TINYINT data type (xtype = 48).
|
||||
/// </summary>
|
||||
TinyInt = 48,
|
||||
|
||||
/// <summary>
|
||||
/// SMALLINT data type (xtype = 52).
|
||||
/// </summary>
|
||||
SmallInt = 52,
|
||||
|
||||
/// <summary>
|
||||
/// INT data type (xtype = 56).
|
||||
/// </summary>
|
||||
Int = 56,
|
||||
|
||||
/// <summary>
|
||||
/// SMALLDATETIME data type (xtype = 58).
|
||||
/// </summary>
|
||||
SmallDateTime = 58,
|
||||
|
||||
/// <summary>
|
||||
/// REAL data type (xtype = 59).
|
||||
/// </summary>
|
||||
Real = 59,
|
||||
|
||||
/// <summary>
|
||||
/// MONEY data type (xtype = 60).
|
||||
/// </summary>
|
||||
Money = 60,
|
||||
|
||||
/// <summary>
|
||||
/// DATETIME data type (xtype = 61).
|
||||
/// </summary>
|
||||
DateTime = 61,
|
||||
|
||||
/// <summary>
|
||||
/// FLOAT data type (xtype = 62).
|
||||
/// </summary>
|
||||
Float = 62,
|
||||
|
||||
/// <summary>
|
||||
/// NTEXT data type (xtype = 99).
|
||||
/// </summary>
|
||||
NText = 99,
|
||||
|
||||
/// <summary>
|
||||
/// BIT data type (xtype = 104).
|
||||
/// </summary>
|
||||
Bit = 104,
|
||||
|
||||
/// <summary>
|
||||
/// DECIMAL data type (xtype = 106).
|
||||
/// </summary>
|
||||
Decimal = 106,
|
||||
|
||||
/// <summary>
|
||||
/// NUMERIC data type (xtype = 108).
|
||||
/// </summary>
|
||||
Numeric = 108,
|
||||
|
||||
/// <summary>
|
||||
/// SMALLMONEY data type (xtype = 122).
|
||||
/// </summary>
|
||||
SmallMoney = 122,
|
||||
|
||||
/// <summary>
|
||||
/// BIGINT data type (xtype = 127).
|
||||
/// </summary>
|
||||
BigInt = 127,
|
||||
|
||||
/// <summary>
|
||||
/// VARBINARY data type (xtype = 165).
|
||||
/// </summary>
|
||||
VarBinary = 165,
|
||||
|
||||
/// <summary>
|
||||
/// VARCHAR data type (xtype = 167).
|
||||
/// </summary>
|
||||
VarChar = 167,
|
||||
|
||||
/// <summary>
|
||||
/// BINARY data type (xtype = 173).
|
||||
/// </summary>
|
||||
Binary = 173,
|
||||
|
||||
/// <summary>
|
||||
/// CHAR data type (xtype = 175).
|
||||
/// </summary>
|
||||
Char = 175,
|
||||
|
||||
/// <summary>
|
||||
/// TIMESTAMP data type (xtype = 189).
|
||||
/// </summary>
|
||||
Timestamp = 189,
|
||||
|
||||
/// <summary>
|
||||
/// NVARCHAR data type (xtype = 231).
|
||||
/// </summary>
|
||||
NVarChar = 231,
|
||||
|
||||
/// <summary>
|
||||
/// NCHAR data type (xtype = 239).
|
||||
/// </summary>
|
||||
NChar = 239,
|
||||
|
||||
/// <summary>
|
||||
/// XML data type (xtype = 241).
|
||||
/// </summary>
|
||||
XML = 241,
|
||||
|
||||
/// <summary>
|
||||
/// DATE data type (xtype = 40).
|
||||
/// </summary>
|
||||
Date = 40
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace Strata.SqlTools.SqlBreakdown.Enums.SQL;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the type of SQL database.
|
||||
/// </summary>
|
||||
public enum SqlDatabaseType
|
||||
{
|
||||
/// <summary>
|
||||
/// Microsoft SQL Server.
|
||||
/// </summary>
|
||||
MSSQL,
|
||||
/// <summary>
|
||||
/// Snowflake.
|
||||
/// </summary>
|
||||
Snowflake,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
namespace Strata.SqlTools.SqlBreakdown.Enums.SQL;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the foreign key constraint policy for referential actions.
|
||||
/// </summary>
|
||||
public enum SqlForeignKeyPolicy
|
||||
{
|
||||
/// <summary>
|
||||
/// No action is taken when the referenced row is updated or deleted.
|
||||
/// </summary>
|
||||
NoAction,
|
||||
|
||||
/// <summary>
|
||||
/// Cascades the update or delete operation to the dependent rows.
|
||||
/// </summary>
|
||||
Cascade,
|
||||
|
||||
/// <summary>
|
||||
/// Sets the foreign key column to its default value.
|
||||
/// </summary>
|
||||
SetDefault,
|
||||
|
||||
/// <summary>
|
||||
/// Sets the foreign key column to NULL.
|
||||
/// </summary>
|
||||
SetNull
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
namespace Strata.SqlTools.SqlBreakdown.Enums.SQL;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the type of SQL JOIN operation.
|
||||
/// </summary>
|
||||
public enum SqlJoinType
|
||||
{
|
||||
/// <summary>
|
||||
/// LEFT JOIN - returns all rows from the left table and matching rows from the right table.
|
||||
/// </summary>
|
||||
Left,
|
||||
|
||||
/// <summary>
|
||||
/// INNER JOIN - returns only matching rows from both tables.
|
||||
/// </summary>
|
||||
Inner,
|
||||
|
||||
/// <summary>
|
||||
/// RIGHT JOIN - returns all rows from the right table and matching rows from the left table.
|
||||
/// </summary>
|
||||
Right,
|
||||
|
||||
/// <summary>
|
||||
/// CROSS JOIN - returns the Cartesian product of both tables.
|
||||
/// </summary>
|
||||
Cross
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
namespace Strata.SqlTools.SqlBreakdown.Enums.SQL;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the type of SQL Server database object.
|
||||
/// </summary>
|
||||
public enum SqlObjectType
|
||||
{
|
||||
/// <summary>
|
||||
/// Unknown object type.
|
||||
/// </summary>
|
||||
Unknown = 0,
|
||||
|
||||
/// <summary>
|
||||
/// User-defined table.
|
||||
/// </summary>
|
||||
UserTable = 1,
|
||||
|
||||
/// <summary>
|
||||
/// View.
|
||||
/// </summary>
|
||||
View = 2
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
namespace Strata.SqlTools.SqlBreakdown.Enums.SQL;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the type of SQL script operation.
|
||||
/// </summary>
|
||||
public enum SqlScriptType
|
||||
{
|
||||
/// <summary>
|
||||
/// CREATE statement.
|
||||
/// </summary>
|
||||
Create,
|
||||
|
||||
/// <summary>
|
||||
/// DROP statement.
|
||||
/// </summary>
|
||||
Drop,
|
||||
|
||||
/// <summary>
|
||||
/// ALTER statement.
|
||||
/// </summary>
|
||||
Alter,
|
||||
|
||||
/// <summary>
|
||||
/// SELECT statement.
|
||||
/// </summary>
|
||||
Select,
|
||||
|
||||
/// <summary>
|
||||
/// INSERT statement.
|
||||
/// </summary>
|
||||
Insert,
|
||||
|
||||
/// <summary>
|
||||
/// UPDATE statement.
|
||||
/// </summary>
|
||||
Update,
|
||||
|
||||
/// <summary>
|
||||
/// DELETE statement.
|
||||
/// </summary>
|
||||
Delete
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
namespace Strata.SqlTools.SqlBreakdown.Enums.SQL;
|
||||
|
||||
/// <summary>
|
||||
/// SQLite column affinity types. Determines how data is stored and converted.
|
||||
/// </summary>
|
||||
public enum SqliteColumnAffinity
|
||||
{
|
||||
/// <summary>
|
||||
/// A column with TEXT affinity stores all data using storage classes NULL, TEXT or BLOB.
|
||||
/// If numerical data is inserted into a column with TEXT affinity it is converted into text form before being stored.
|
||||
/// </summary>
|
||||
Text = 0,
|
||||
|
||||
/// <summary>
|
||||
/// A column with NUMERIC affinity may contain values using all five storage classes.
|
||||
/// When text data is inserted into a NUMERIC column, the storage class of the text is converted to INTEGER or REAL
|
||||
/// (in order of preference) if such conversion is lossless and reversible.
|
||||
/// </summary>
|
||||
Numeric = 1,
|
||||
|
||||
/// <summary>
|
||||
/// A column that uses INTEGER affinity behaves the same as a column with NUMERIC affinity.
|
||||
/// The difference between INTEGER and NUMERIC affinity is only evident in a CAST expression.
|
||||
/// </summary>
|
||||
Integer = 2,
|
||||
|
||||
/// <summary>
|
||||
/// A column with REAL affinity behaves like a column with NUMERIC affinity except that
|
||||
/// it forces integer values into floating point representation.
|
||||
/// </summary>
|
||||
Real = 3,
|
||||
|
||||
/// <summary>
|
||||
/// Represents a date value. For all intents and purposes, a TEXT Field. Used by biz logic to help determine what values are going to look like.
|
||||
/// Note this is not a true SQLite Column Affinity but rather a Strata specific extension.
|
||||
/// </summary>
|
||||
Date = 4
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
namespace Strata.SqlTools.SqlBreakdown.Enums.SQL;
|
||||
|
||||
public enum TokenType
|
||||
{
|
||||
None,
|
||||
Plus,
|
||||
Minus,
|
||||
Multiply,
|
||||
Divide,
|
||||
Number,
|
||||
String,
|
||||
LeftParenthesis,
|
||||
RightParenthesis,
|
||||
FunctionStart,
|
||||
FunctionEnd,
|
||||
ColumnIdentifier,
|
||||
Parameter,
|
||||
Operator
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
namespace Strata.SqlTools.SqlBreakdown.Enums.SQL;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the type of SQL trigger.
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum TriggerType
|
||||
{
|
||||
/// <summary>
|
||||
/// Trigger fires after the action.
|
||||
/// </summary>
|
||||
After = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Trigger fires instead of the action.
|
||||
/// </summary>
|
||||
InsteadOf = 2,
|
||||
|
||||
/// <summary>
|
||||
/// Both after and instead of triggers.
|
||||
/// </summary>
|
||||
Both = 3
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace Strata.SqlTools.SqlBreakdown.Exceptions;
|
||||
|
||||
/// <summary>
|
||||
/// Exception thrown when a catastrophic failure occurs that should not be recoverable.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class CatastrophicFailureException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CatastrophicFailureException"/> class.
|
||||
/// </summary>
|
||||
public CatastrophicFailureException()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CatastrophicFailureException"/> class with a specified error message.
|
||||
/// </summary>
|
||||
/// <param name="aMessage">The message that describes the error.</param>
|
||||
public CatastrophicFailureException(string aMessage)
|
||||
: base(aMessage)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CatastrophicFailureException"/> class with a specified error message and inner exception.
|
||||
/// </summary>
|
||||
/// <param name="aMessage">The message that describes the error.</param>
|
||||
/// <param name="innerException">The inner exception.</param>
|
||||
public CatastrophicFailureException(string aMessage, Exception innerException)
|
||||
: base(aMessage, innerException)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CatastrophicFailureException"/> class with serialized data.
|
||||
/// </summary>
|
||||
/// <param name="info">The serialization info.</param>
|
||||
/// <param name="context">The streaming context.</param>
|
||||
#pragma warning disable SYSLIB0051 // Type or member is obsolete - Required for ISerializable pattern
|
||||
protected CatastrophicFailureException(SerializationInfo info, StreamingContext context)
|
||||
: base(info, context)
|
||||
{
|
||||
}
|
||||
#pragma warning restore SYSLIB0051
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace Strata.SqlTools.SqlBreakdown.Exceptions;
|
||||
|
||||
public class InvalidSyntaxException : Exception
|
||||
{
|
||||
public InvalidSyntaxException()
|
||||
{
|
||||
}
|
||||
|
||||
public InvalidSyntaxException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public InvalidSyntaxException(string message, Exception innerException) : base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace Strata.SqlTools.SqlBreakdown.Exceptions;
|
||||
|
||||
/// <summary>
|
||||
/// Exception thrown when an enum value is not implemented or handled.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The enum type.</typeparam>
|
||||
[Serializable]
|
||||
public class NotImplementedEnumValueException<T> : Exception
|
||||
{
|
||||
private const string MESSAGE_TEMPLATE = "{0} of type {1} is not implemented.";
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="NotImplementedEnumValueException{T}"/> class.
|
||||
/// </summary>
|
||||
public NotImplementedEnumValueException()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="NotImplementedEnumValueException{T}"/> class with a specific enum value.
|
||||
/// </summary>
|
||||
/// <param name="aValue">The unimplemented enum value.</param>
|
||||
public NotImplementedEnumValueException(T aValue)
|
||||
: base(string.Format(MESSAGE_TEMPLATE, aValue?.ToString(), aValue?.GetType().ToString()))
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="NotImplementedEnumValueException{T}"/> class with a specific enum value and inner exception.
|
||||
/// </summary>
|
||||
/// <param name="aValue">The unimplemented enum value.</param>
|
||||
/// <param name="innerException">The inner exception.</param>
|
||||
public NotImplementedEnumValueException(T aValue, Exception innerException)
|
||||
: base(string.Format(MESSAGE_TEMPLATE, aValue?.ToString(), aValue?.GetType().ToString()), innerException)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="NotImplementedEnumValueException{T}"/> class with serialized data.
|
||||
/// </summary>
|
||||
/// <param name="info">The serialization info.</param>
|
||||
/// <param name="context">The streaming context.</param>
|
||||
#pragma warning disable SYSLIB0051 // Type or member is obsolete - Required for ISerializable pattern
|
||||
protected NotImplementedEnumValueException(SerializationInfo info, StreamingContext context)
|
||||
: base(info, context)
|
||||
{
|
||||
}
|
||||
#pragma warning restore SYSLIB0051
|
||||
}
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
using Strata.SqlTools.SqlBreakdown.Interfaces.Core;
|
||||
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Strata.SqlTools.SqlBreakdown.Expressions.Arithmetic;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for binary arithmetic operations on SQL expressions. Represents operations
|
||||
/// that combine two expressions using an arithmetic operator (+, -, *, /).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Arithmetic expressions are automatically created when using arithmetic operators on
|
||||
/// <see cref="Expression"/> instances. The generated SQL maintains operator precedence
|
||||
/// through the visitor pattern implementation.
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// <code language="csharp">
|
||||
/// var price = new GenericColumnExpression("Price", "Products");
|
||||
/// var quantity = new GenericColumnExpression("Quantity", "Orders");
|
||||
/// var taxRate = Expression.FromObject(0.08m);
|
||||
///
|
||||
/// // Creates MultiplicationExpression and AdditionExpression
|
||||
/// var total = (price * quantity) * (1.0m + taxRate);
|
||||
/// // Generates: (Price * Quantity) * (1.0 + 0.08)
|
||||
/// </code>
|
||||
/// </example>
|
||||
[DebuggerDisplay("{ExpressionA,nq} {ArithmeticOperator,nq} {ExpressionB,nq}")]
|
||||
public abstract class ArithmeticExpression : Expression
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the left-hand expression in the arithmetic operation.
|
||||
/// </summary>
|
||||
public Expression ExpressionA { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the right-hand expression in the arithmetic operation.
|
||||
/// </summary>
|
||||
public Expression ExpressionB { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the arithmetic operator symbol (+, -, *, /).
|
||||
/// </summary>
|
||||
public abstract string ArithmeticOperator { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ArithmeticExpression"/> class.
|
||||
/// </summary>
|
||||
/// <param name="a">The left-hand expression.</param>
|
||||
/// <param name="b">The right-hand expression.</param>
|
||||
/// <remarks>
|
||||
/// Arithmetic expressions are typically created using operators:
|
||||
/// <code language="csharp">
|
||||
/// var price = new GenericColumnExpression("Price", "Products");
|
||||
/// var total = price * 1.1m; // Creates MultiplicationExpression
|
||||
/// </code>
|
||||
/// </remarks>
|
||||
protected ArithmeticExpression(Expression a, Expression b)
|
||||
{
|
||||
ExpressionA = a;
|
||||
ExpressionB = b;
|
||||
}
|
||||
|
||||
public override T Accept<T>(IVisitor<T> visitor)
|
||||
{
|
||||
return visitor.VisitArithmeticExpression(this);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents an addition operation between two numeric expressions.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Automatically created when using the + operator on expressions.
|
||||
/// Generates SQL addition operation (a + b).
|
||||
/// </remarks>
|
||||
public class AdditionExpression : ArithmeticExpression
|
||||
{
|
||||
public override string ArithmeticOperator => "+";
|
||||
|
||||
public AdditionExpression(Expression a, Expression b) : base(a, b)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a subtraction operation between two numeric expressions.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Automatically created when using the - operator on expressions.
|
||||
/// Generates SQL subtraction operation (a - b).
|
||||
/// </remarks>
|
||||
public class SubtractionExpression : ArithmeticExpression
|
||||
{
|
||||
public override string ArithmeticOperator => "-";
|
||||
|
||||
public SubtractionExpression(Expression a, Expression b) : base(a, b)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a multiplication operation between two numeric expressions.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Automatically created when using the * operator on expressions.
|
||||
/// Generates SQL multiplication operation (a * b).
|
||||
/// </remarks>
|
||||
public class MultiplicationExpression : ArithmeticExpression
|
||||
{
|
||||
public override string ArithmeticOperator => "*";
|
||||
|
||||
public MultiplicationExpression(Expression a, Expression b) : base(a, b)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a division operation between two numeric expressions.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Automatically created when using the / operator on expressions.
|
||||
/// Generates SQL division operation (a / b).
|
||||
/// </remarks>
|
||||
public class DivisionExpression : ArithmeticExpression
|
||||
{
|
||||
public override string ArithmeticOperator => "/";
|
||||
|
||||
public DivisionExpression(Expression a, Expression b) : base(a, b)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
using Strata.SqlTools.SqlBreakdown.Classes;
|
||||
using Strata.SqlTools.SqlBreakdown.Interfaces.Core;
|
||||
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Strata.SqlTools.SqlBreakdown.Expressions;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for all column expression types in SQL queries. Provides the fundamental
|
||||
/// column name property and validation for derived column expression classes.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This abstract class serves as the foundation for various column expression types
|
||||
/// including <see cref="GenericColumnExpression"/> and
|
||||
/// <see cref="RegisteredTableColumnExpression"/>.
|
||||
/// </remarks>
|
||||
[DebuggerDisplay("{ColumnName,nq}", Name = "ColumnExpression")]
|
||||
public abstract class ColumnExpression : Expression
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the name of the column.
|
||||
/// </summary>
|
||||
/// <value>The column name as it appears in the database schema.</value>
|
||||
public string ColumnName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ColumnExpression"/> class.
|
||||
/// </summary>
|
||||
/// <param name="columnName">
|
||||
/// The name of the column. Cannot be null or whitespace.
|
||||
/// </param>
|
||||
/// <exception cref="ArgumentException">
|
||||
/// Thrown when <paramref name="columnName"/> is null or whitespace.
|
||||
/// </exception>
|
||||
protected ColumnExpression(string columnName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(columnName))
|
||||
{
|
||||
throw new ArgumentException($"{nameof(columnName)} cannot be null or whitespace", nameof(columnName));
|
||||
}
|
||||
|
||||
ColumnName = columnName;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generic base class for column expressions that are associated with a specific table
|
||||
/// source. Provides column name, table source reference, and implements the visitor
|
||||
/// pattern for SQL generation.
|
||||
/// </summary>
|
||||
/// <typeparam name="TSource">
|
||||
/// The type of table source, must derive from <see cref="SelectSource"/>.
|
||||
/// </typeparam>
|
||||
/// <remarks>
|
||||
/// This generic base class enables type-safe column expressions that maintain a
|
||||
/// reference to their source table, allowing for proper table qualification in
|
||||
/// generated SQL statements. The visitor pattern implementation
|
||||
/// (<see cref="Accept{T}"/>) allows different SQL dialects to generate appropriate
|
||||
/// syntax for the column reference.
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// <code language="csharp">
|
||||
/// // Example with GenericColumnExpression
|
||||
/// var tableSource = new TableSource("Employees");
|
||||
/// var column = new GenericColumnExpression("FirstName", tableSource);
|
||||
/// var visitor = new CommandVisitor();
|
||||
/// string sql = column.Accept(visitor); // Returns: Employees.FirstName
|
||||
/// </code>
|
||||
/// </example>
|
||||
[DebuggerDisplay("{Source,nq}.{ColumnName,nq}")]
|
||||
public abstract class ColumnExpression<TSource> : ColumnExpression
|
||||
where TSource : SelectSource
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the table source that this column belongs to.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The source table containing schema, table name, and alias information.
|
||||
/// </value>
|
||||
public TSource Source { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ColumnExpression{TSource}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="columnName">
|
||||
/// The name of the column. Cannot be null or whitespace.
|
||||
/// </param>
|
||||
/// <param name="source">The table source. Cannot be null.</param>
|
||||
/// <exception cref="ArgumentException">
|
||||
/// Thrown when <paramref name="columnName"/> is null or whitespace.
|
||||
/// </exception>
|
||||
/// <exception cref="ArgumentNullException">
|
||||
/// Thrown when <paramref name="source"/> is null.
|
||||
/// </exception>
|
||||
protected ColumnExpression(string columnName, TSource source) : base(columnName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(columnName))
|
||||
{
|
||||
throw new ArgumentException($"{nameof(columnName)} cannot be null or whitespace", nameof(columnName));
|
||||
}
|
||||
|
||||
Source = source ?? throw new ArgumentNullException(nameof(source));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Accepts a visitor for the visitor pattern, allowing different SQL
|
||||
/// generation strategies.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The return type of the visitor.</typeparam>
|
||||
/// <param name="visitor">
|
||||
/// The visitor instance that will process this expression.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// The result from the visitor's processing of this column expression.
|
||||
/// </returns>
|
||||
public override T Accept<T>(IVisitor<T> visitor)
|
||||
{
|
||||
return visitor.VisitColumnExpression(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
using Strata.SqlTools.SqlBreakdown.Interfaces.Core;
|
||||
|
||||
namespace Strata.SqlTools.SqlBreakdown.Expressions.Conditional;
|
||||
|
||||
/// <summary>
|
||||
/// Tests whether an expression falls within a specified range (inclusive). Generates
|
||||
/// SQL BETWEEN clause.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// BetweenExpression is equivalent to (expr >= lower_bound AND expr <= upper_bound)
|
||||
/// but generates more concise BETWEEN syntax. Works with numeric, date, and string
|
||||
/// expressions.
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// <code language="csharp">
|
||||
/// var price = new GenericColumnExpression("Price", "Products");
|
||||
/// var orderDate = new GenericColumnExpression("OrderDate", "Orders");
|
||||
///
|
||||
/// // Numeric range
|
||||
/// var affordableItems = new BetweenExpression(
|
||||
/// price,
|
||||
/// lowerBound: 10.0m,
|
||||
/// upperBound: 100.0m
|
||||
/// );
|
||||
/// // Generates: Price BETWEEN 10.0 AND 100.0
|
||||
///
|
||||
/// // Date range
|
||||
/// var thisYear = new BetweenExpression(
|
||||
/// orderDate,
|
||||
/// new DateTime(2024, 1, 1),
|
||||
/// new DateTime(2024, 12, 31)
|
||||
/// );
|
||||
/// // Generates: OrderDate BETWEEN '2024-01-01' AND '2024-12-31'
|
||||
/// </code>
|
||||
/// </example>
|
||||
public class BetweenExpression : BooleanExpression
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the expression to test.
|
||||
/// </summary>
|
||||
public Expression Expression { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the lower bound of the range (inclusive).
|
||||
/// </summary>
|
||||
public Expression LowerBound { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the upper bound of the range (inclusive).
|
||||
/// </summary>
|
||||
public Expression UpperBound { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BetweenExpression"/> class.
|
||||
/// </summary>
|
||||
/// <param name="expression">The expression to test.</param>
|
||||
/// <param name="lowerBound">The lower bound (inclusive).</param>
|
||||
/// <param name="upperBound">The upper bound (inclusive).</param>
|
||||
/// <remarks>
|
||||
/// Example:
|
||||
/// <code language="csharp">
|
||||
/// var price = new GenericColumnExpression("Price", "Products");
|
||||
/// var affordable = new BetweenExpression(price, 10.0m, 100.0m);
|
||||
/// // Generates: Price BETWEEN 10.0 AND 100.0
|
||||
/// </code>
|
||||
/// </remarks>
|
||||
public BetweenExpression(Expression expression, Expression lowerBound, Expression upperBound)
|
||||
{
|
||||
Expression = expression;
|
||||
LowerBound = lowerBound;
|
||||
UpperBound = upperBound;
|
||||
}
|
||||
|
||||
public override T Accept<T>(IVisitor<T> visitor)
|
||||
{
|
||||
return visitor.VisitBetweenExpression(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
using Strata.SqlTools.SqlBreakdown.Expressions.Conditional.Comparisons;
|
||||
using Strata.SqlTools.SqlBreakdown.Expressions.Conditional.Logical;
|
||||
using Strata.SqlTools.SqlBreakdown.Expressions.Literals;
|
||||
|
||||
namespace Strata.SqlTools.SqlBreakdown.Expressions.Conditional;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for expressions that evaluate to a boolean (true/false) result. Provides
|
||||
/// logical operators (AND, OR, NOT) for combining boolean conditions.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Boolean expressions are the foundation for WHERE clauses, HAVING clauses, and other
|
||||
/// conditional SQL constructs. This class provides operator overloading for logical
|
||||
/// operations and smart negation that optimizes expression trees.
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// <code language="csharp">
|
||||
/// var price = new GenericColumnExpression("Price", "Products");
|
||||
/// var category = new GenericColumnExpression("Category", "Products");
|
||||
///
|
||||
/// // Comparison operations return BooleanExpression
|
||||
/// BooleanExpression expensive = price > 100;
|
||||
/// BooleanExpression inCategory = category == "Electronics";
|
||||
///
|
||||
/// // Combine with logical operators
|
||||
/// var complexFilter = expensive & inCategory; // AND
|
||||
/// var alternativeFilter = expensive &pipe; inCategory; // OR
|
||||
/// var notExpensive = !expensive; // NOT
|
||||
///
|
||||
/// // Use in query
|
||||
/// query.AddWhereExpression(complexFilter);
|
||||
/// // Generates: WHERE Price > 100 AND Category = 'Electronics'
|
||||
/// </code>
|
||||
/// </example>
|
||||
public abstract class BooleanExpression : Expression
|
||||
{
|
||||
/// <summary>
|
||||
/// Combines two boolean expressions with a logical AND operation.
|
||||
/// </summary>
|
||||
/// <param name="a">The left boolean expression.</param>
|
||||
/// <param name="b">The right boolean expression.</param>
|
||||
/// <returns>
|
||||
/// An <see cref="AndExpression"/> combining both expressions, or the non-null
|
||||
/// expression if one operand is null.
|
||||
/// </returns>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// Thrown when both expressions are null.
|
||||
/// </exception>
|
||||
public static BooleanExpression operator &(BooleanExpression? a, BooleanExpression? b)
|
||||
{
|
||||
if (a is null && b is not null)
|
||||
{
|
||||
return b;
|
||||
}
|
||||
|
||||
if (b is null && a is not null)
|
||||
{
|
||||
return a;
|
||||
}
|
||||
|
||||
if (a is null && b is null)
|
||||
{
|
||||
throw new InvalidOperationException("both expressions cannot be null");
|
||||
}
|
||||
|
||||
return new AndExpression(a!, b!);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Combines two boolean expressions with a logical OR operation.
|
||||
/// </summary>
|
||||
/// <param name="a">The left boolean expression.</param>
|
||||
/// <param name="b">The right boolean expression.</param>
|
||||
/// <returns>
|
||||
/// An <see cref="OrExpression"/> combining both expressions, or the non-null
|
||||
/// expression if one operand is null.
|
||||
/// </returns>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// Thrown when both expressions are null.
|
||||
/// </exception>
|
||||
public static BooleanExpression operator |(BooleanExpression? a, BooleanExpression? b)
|
||||
{
|
||||
if (a is null && b is not null)
|
||||
{
|
||||
return b;
|
||||
}
|
||||
|
||||
if (b is null && a is not null)
|
||||
{
|
||||
return a;
|
||||
}
|
||||
|
||||
if (a is null && b is null)
|
||||
{
|
||||
throw new InvalidOperationException("both expressions cannot be null");
|
||||
}
|
||||
|
||||
return new OrExpression(a!, b!);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Negates a boolean expression, applying logical NOT. Applies intelligent
|
||||
/// optimization to flip comparison operators rather than wrapping in NOT.
|
||||
/// </summary>
|
||||
/// <param name="a">The boolean expression to negate.</param>
|
||||
/// <returns>
|
||||
/// An optimized negated expression. For example, (a == b) becomes (a != b),
|
||||
/// and (IN) becomes (NOT IN), rather than wrapping in a NOT expression.
|
||||
/// </returns>
|
||||
public static BooleanExpression operator !(BooleanExpression a)
|
||||
{
|
||||
return a switch
|
||||
{
|
||||
EqualToExpression equalTo => new NotEqualToExpression(equalTo.ExpressionA, equalTo.ExpressionB),
|
||||
NotEqualToExpression notEqual => new EqualToExpression(notEqual.ExpressionA, notEqual.ExpressionB),
|
||||
InExpression inExpression => new NotInExpression(inExpression.SearchExpression, inExpression.ValuesToCompare),
|
||||
NotInExpression notInExpression => new InExpression(notInExpression.SearchExpression, notInExpression.ValuesToCompare),
|
||||
_ => new NotExpression(a)
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a boolean literal expression representing TRUE.
|
||||
/// </summary>
|
||||
public static BooleanExpression True => new BooleanLiteralExpression(true);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a boolean literal expression representing FALSE.
|
||||
/// </summary>
|
||||
public static BooleanExpression False => new BooleanLiteralExpression(false);
|
||||
}
|
||||
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
using Strata.SqlTools.SqlBreakdown.Interfaces.Core;
|
||||
|
||||
namespace Strata.SqlTools.SqlBreakdown.Expressions.Conditional.Comparisons;
|
||||
|
||||
/// <summary>
|
||||
/// A type of <see cref="BooleanExpression"/> that compares two <see cref="Expression"/>s
|
||||
/// </summary>
|
||||
public abstract class ComparisonOperatorExpression : BooleanExpression
|
||||
{
|
||||
public abstract string Operator { get; }
|
||||
|
||||
public Expression ExpressionA { get; }
|
||||
public Expression ExpressionB { get; }
|
||||
|
||||
protected ComparisonOperatorExpression(Expression a, Expression b)
|
||||
{
|
||||
ExpressionA = a;
|
||||
ExpressionB = b;
|
||||
}
|
||||
|
||||
public override T Accept<T>(IVisitor<T> visitor)
|
||||
{
|
||||
return visitor.VisitComparisonExpression(this);
|
||||
}
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
namespace Strata.SqlTools.SqlBreakdown.Expressions.Conditional.Comparisons;
|
||||
|
||||
/// <summary>
|
||||
/// a is equal to b.
|
||||
/// </summary>
|
||||
public class EqualToExpression : ComparisonOperatorExpression
|
||||
{
|
||||
public override string Operator => "=";
|
||||
|
||||
public EqualToExpression(Expression a, Expression b) : base(a, b)
|
||||
{
|
||||
}
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
namespace Strata.SqlTools.SqlBreakdown.Expressions.Conditional.Comparisons;
|
||||
|
||||
/// <summary>
|
||||
/// a is greater than b.
|
||||
/// </summary>
|
||||
public class GreaterThanExpression : ComparisonOperatorExpression
|
||||
{
|
||||
public override string Operator => ">";
|
||||
|
||||
public GreaterThanExpression(Expression a, Expression b) : base(a, b)
|
||||
{
|
||||
}
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
namespace Strata.SqlTools.SqlBreakdown.Expressions.Conditional.Comparisons;
|
||||
|
||||
/// <summary>
|
||||
/// a is greater than or equal to b.
|
||||
/// </summary>
|
||||
public class GreaterThanOrEqualToExpression : ComparisonOperatorExpression
|
||||
{
|
||||
public override string Operator => ">=";
|
||||
|
||||
public GreaterThanOrEqualToExpression(Expression a, Expression b) : base(a, b)
|
||||
{
|
||||
}
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
namespace Strata.SqlTools.SqlBreakdown.Expressions.Conditional.Comparisons;
|
||||
|
||||
/// <summary>
|
||||
/// a is less than b.
|
||||
/// </summary>
|
||||
public class LessThanExpression : ComparisonOperatorExpression
|
||||
{
|
||||
public override string Operator => "<";
|
||||
|
||||
public LessThanExpression(Expression a, Expression b) : base(a, b)
|
||||
{
|
||||
}
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
namespace Strata.SqlTools.SqlBreakdown.Expressions.Conditional.Comparisons;
|
||||
|
||||
/// <summary>
|
||||
/// a is less than or equal to b.
|
||||
/// </summary>
|
||||
public class LessThanOrEqualToExpression : ComparisonOperatorExpression
|
||||
{
|
||||
public override string Operator => "<=";
|
||||
|
||||
public LessThanOrEqualToExpression(Expression a, Expression b) : base(a, b)
|
||||
{
|
||||
}
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
namespace Strata.SqlTools.SqlBreakdown.Expressions.Conditional.Comparisons;
|
||||
|
||||
/// <summary>
|
||||
/// a is not equal to b.
|
||||
/// </summary>
|
||||
public class NotEqualToExpression : ComparisonOperatorExpression
|
||||
{
|
||||
public override string Operator => "!=";
|
||||
|
||||
public NotEqualToExpression(Expression a, Expression b) : base(a, b)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
using Strata.SqlTools.SqlBreakdown.Interfaces.Core;
|
||||
|
||||
namespace Strata.SqlTools.SqlBreakdown.Expressions.Conditional;
|
||||
|
||||
/// <summary>
|
||||
/// Tests whether an expression matches any value in an explicit list of values.
|
||||
/// Generates SQL IN clause.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// InExpression is equivalent to a series of OR conditions checking equality with each
|
||||
/// value in the list. The SQL visitor generates optimized IN syntax.
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// <code language="csharp">
|
||||
/// var category = new GenericColumnExpression("Category", "Products");
|
||||
///
|
||||
/// // Create IN expression
|
||||
/// var filter = new InExpression(
|
||||
/// category,
|
||||
/// "Electronics",
|
||||
/// "Computers",
|
||||
/// "Software"
|
||||
/// );
|
||||
///
|
||||
/// // Use in query
|
||||
/// query.AddWhereExpression(filter);
|
||||
/// // Generates: WHERE Category IN ('Electronics', 'Computers', 'Software')
|
||||
/// </code>
|
||||
/// </example>
|
||||
public class InExpression : BooleanExpression
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the expression to search for in the value list.
|
||||
/// </summary>
|
||||
public Expression SearchExpression { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the array of values to compare against.
|
||||
/// </summary>
|
||||
public Expression[] ValuesToCompare { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="InExpression"/> class.
|
||||
/// </summary>
|
||||
/// <param name="searchExpression">The expression to search for.</param>
|
||||
/// <param name="valuesToCompare">
|
||||
/// Expressions representing the values to compare against. The search expression
|
||||
/// matches if it equals any of these values.
|
||||
/// </param>
|
||||
/// <exception cref="ArgumentNullException">
|
||||
/// Thrown when <paramref name="valuesToCompare"/> is null.
|
||||
/// </exception>
|
||||
/// <remarks>
|
||||
/// Example:
|
||||
/// <code language="csharp">
|
||||
/// var category = new GenericColumnExpression("Category", "Products");
|
||||
/// var filter = new InExpression(category, "Electronics", "Computers");
|
||||
/// // Generates: Category IN ('Electronics', 'Computers')
|
||||
/// </code>
|
||||
/// </remarks>
|
||||
public InExpression(Expression searchExpression, params Expression[] valuesToCompare)
|
||||
{
|
||||
SearchExpression = searchExpression;
|
||||
ValuesToCompare = valuesToCompare ?? throw new ArgumentNullException(nameof(valuesToCompare));
|
||||
}
|
||||
|
||||
public override T Accept<T>(IVisitor<T> visitor)
|
||||
{
|
||||
return visitor.VisitInExpression(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
using Strata.SqlTools.SqlBreakdown.Interfaces.Core;
|
||||
|
||||
namespace Strata.SqlTools.SqlBreakdown.Expressions.Conditional;
|
||||
|
||||
/// <summary>
|
||||
/// Tests whether a string expression matches a pattern using wildcards. Generates SQL
|
||||
/// LIKE clause.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// LikeExpression enables pattern matching with SQL wildcards (% for any characters,
|
||||
/// _ for single character). Supports case-sensitive and case-insensitive matching
|
||||
/// depending on the SQL dialect and configuration.
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// <code language="csharp">
|
||||
/// var productName = new GenericColumnExpression("ProductName", "Products");
|
||||
///
|
||||
/// // Match names starting with "Apple"
|
||||
/// var appleProducts = new LikeExpression(
|
||||
/// productName,
|
||||
/// "Apple%" // % matches any characters
|
||||
/// );
|
||||
/// // Generates: ProductName LIKE 'Apple%'
|
||||
///
|
||||
/// // Match phone numbers with pattern
|
||||
/// var phone = new GenericColumnExpression("Phone", "Contacts");
|
||||
/// var usPhones = new LikeExpression(
|
||||
/// phone,
|
||||
/// "(___) ___-____" // _ matches single character
|
||||
/// );
|
||||
/// // Generates: Phone LIKE '(___) ___-____'
|
||||
///
|
||||
/// // Case-sensitive matching
|
||||
/// var caseSensitive = new LikeExpression(
|
||||
/// productName,
|
||||
/// "APPLE%",
|
||||
/// caseInsensitive: false
|
||||
/// );
|
||||
/// </code>
|
||||
/// </example>
|
||||
public class LikeExpression : BooleanExpression
|
||||
{
|
||||
/// <summary>
|
||||
/// Subject to match. This is typically a VARCHAR, although some other data types can be used.
|
||||
/// </summary>
|
||||
public Expression Subject { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Pattern to match. This is typically a VARCHAR, although some other data types can be used.
|
||||
/// </summary>
|
||||
public Expression Pattern { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether string-matching is case sensitive or not
|
||||
/// </summary>
|
||||
public bool CaseInsensitive { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for <see cref="LikeExpression"/> with default case-insensitive
|
||||
/// matching.
|
||||
/// </summary>
|
||||
/// <param name="subject">
|
||||
/// Subject to match. This is typically a VARCHAR, although some other data types
|
||||
/// can be used.
|
||||
/// </param>
|
||||
/// <param name="pattern">
|
||||
/// Pattern to match. Use % for any characters, _ for single character.
|
||||
/// </param>
|
||||
/// <remarks>
|
||||
/// Example:
|
||||
/// <code language="csharp">
|
||||
/// var name = new GenericColumnExpression("ProductName", "Products");
|
||||
/// var filter = new LikeExpression(name, "Apple%");
|
||||
/// // Generates: ProductName LIKE 'Apple%'
|
||||
/// </code>
|
||||
/// </remarks>
|
||||
public LikeExpression(Expression subject, Expression pattern) : this(subject, pattern, true)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for <see cref="LikeExpression"/> with configurable case sensitivity.
|
||||
/// </summary>
|
||||
/// <param name="subject">
|
||||
/// Subject to match. This is typically a VARCHAR, although some other data types
|
||||
/// can be used.
|
||||
/// </param>
|
||||
/// <param name="pattern">
|
||||
/// Pattern to match. Use % for any characters, _ for single character.
|
||||
/// </param>
|
||||
/// <param name="caseInsensitive">Whether string-matching is case-insensitive.</param>
|
||||
/// <remarks>
|
||||
/// Example:
|
||||
/// <code language="csharp">
|
||||
/// var name = new GenericColumnExpression("ProductName", "Products");
|
||||
/// var filter = new LikeExpression(name, "APPLE%", caseInsensitive: false);
|
||||
/// // Case-sensitive match
|
||||
/// </code>
|
||||
/// </remarks>
|
||||
public LikeExpression(Expression subject, Expression pattern, bool caseInsensitive)
|
||||
{
|
||||
Subject = subject;
|
||||
Pattern = pattern;
|
||||
CaseInsensitive = caseInsensitive;
|
||||
}
|
||||
|
||||
public override T Accept<T>(IVisitor<T> visitor)
|
||||
{
|
||||
return visitor.VisitLikeExpression(this);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests whether a string expression does NOT match a pattern using wildcards.
|
||||
/// Generates SQL NOT LIKE clause.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// NotLikeExpression is the negation of <see cref="LikeExpression"/>. Matches when
|
||||
/// the subject does not conform to the specified pattern.
|
||||
/// </remarks>
|
||||
public class NotLikeExpression : LikeExpression
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="NotLikeExpression"/> class.
|
||||
/// </summary>
|
||||
/// <param name="subject">Subject to match.</param>
|
||||
/// <param name="pattern">Pattern to match against.</param>
|
||||
/// <remarks>
|
||||
/// Example:
|
||||
/// <code language="csharp">
|
||||
/// var name = new GenericColumnExpression("ProductName", "Products");
|
||||
/// var filter = new NotLikeExpression(name, "Test%");
|
||||
/// // Generates: ProductName NOT LIKE 'Test%'
|
||||
/// </code>
|
||||
/// </remarks>
|
||||
public NotLikeExpression(Expression subject, Expression pattern) : base(subject, pattern)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="NotLikeExpression"/> class.
|
||||
/// </summary>
|
||||
/// <param name="subject">Subject to match.</param>
|
||||
/// <param name="pattern">Pattern to match against.</param>
|
||||
/// <param name="caseInsensitive">Whether matching is case-insensitive.</param>
|
||||
/// <remarks>
|
||||
/// Case-sensitive example:
|
||||
/// <code language="csharp">
|
||||
/// var filter = new NotLikeExpression(name, "TEST%", caseInsensitive: false);
|
||||
/// </code>
|
||||
/// </remarks>
|
||||
public NotLikeExpression(Expression subject, Expression pattern, bool caseInsensitive) : base(subject, pattern, caseInsensitive)
|
||||
{
|
||||
}
|
||||
|
||||
public override T Accept<T>(IVisitor<T> visitor)
|
||||
{
|
||||
return visitor.VisitNotLikeExpression(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
using Strata.SqlTools.SqlBreakdown.Interfaces.Core;
|
||||
|
||||
namespace Strata.SqlTools.SqlBreakdown.Expressions.Conditional.Logical;
|
||||
|
||||
/// <summary>
|
||||
/// Matches both expressions (a and b).
|
||||
/// </summary>
|
||||
public class AndExpression : LogicalOperatorExpression
|
||||
{
|
||||
public BooleanExpression ExpressionA { get; }
|
||||
public BooleanExpression ExpressionB { get; }
|
||||
|
||||
public AndExpression(BooleanExpression a, BooleanExpression b)
|
||||
{
|
||||
ExpressionA = a ?? throw new ArgumentNullException(nameof(a));
|
||||
ExpressionB = b ?? throw new ArgumentNullException(nameof(b));
|
||||
}
|
||||
|
||||
public override T Accept<T>(IVisitor<T> visitor)
|
||||
{
|
||||
return visitor.VisitAndExpression(this);
|
||||
}
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
namespace Strata.SqlTools.SqlBreakdown.Expressions.Conditional.Logical;
|
||||
|
||||
/// <summary>
|
||||
/// Logical operators return the result of a particular Boolean operation on one or two input expressions. They can only be used as a
|
||||
/// predicate (e.g. in the WHERE clause). Input expressions must be <see cref="BooleanExpression"/>.
|
||||
/// </summary>
|
||||
public abstract class LogicalOperatorExpression : BooleanExpression
|
||||
{
|
||||
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using Strata.SqlTools.SqlBreakdown.Interfaces.Core;
|
||||
|
||||
namespace Strata.SqlTools.SqlBreakdown.Expressions.Conditional.Logical;
|
||||
|
||||
/// <summary>
|
||||
/// Logical Expression that returns true if the given expression is NOT matched.
|
||||
/// </summary>
|
||||
public class NotExpression : LogicalOperatorExpression
|
||||
{
|
||||
public BooleanExpression ExpressionA { get; }
|
||||
|
||||
public NotExpression(BooleanExpression a)
|
||||
{
|
||||
ExpressionA = a ?? throw new ArgumentNullException(nameof(a));
|
||||
}
|
||||
|
||||
public override T Accept<T>(IVisitor<T> visitor)
|
||||
{
|
||||
return visitor.VisitNotExpression(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
using Strata.SqlTools.SqlBreakdown.Interfaces.Core;
|
||||
|
||||
namespace Strata.SqlTools.SqlBreakdown.Expressions.Conditional.Logical;
|
||||
|
||||
/// <summary>
|
||||
/// Matches either expression.
|
||||
/// </summary>
|
||||
public class OrExpression : LogicalOperatorExpression
|
||||
{
|
||||
public BooleanExpression ExpressionA { get; }
|
||||
public BooleanExpression ExpressionB { get; }
|
||||
|
||||
public OrExpression(BooleanExpression a, BooleanExpression b)
|
||||
{
|
||||
ExpressionA = a ?? throw new ArgumentNullException(nameof(a));
|
||||
ExpressionB = b ?? throw new ArgumentNullException(nameof(b));
|
||||
}
|
||||
|
||||
public override T Accept<T>(IVisitor<T> visitor)
|
||||
{
|
||||
return visitor.VisitOrExpression(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
using Strata.SqlTools.SqlBreakdown.Interfaces.Core;
|
||||
|
||||
namespace Strata.SqlTools.SqlBreakdown.Expressions.Conditional;
|
||||
|
||||
/// <summary>
|
||||
/// Tests whether an expression does NOT match any value in an explicit list of values.
|
||||
/// Generates SQL NOT IN clause.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// NotInExpression is the negation of <see cref="InExpression"/> and is equivalent to
|
||||
/// a series of AND conditions checking inequality with each value in the list. Often
|
||||
/// created automatically via the NOT operator on InExpression.
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// <code language="csharp">
|
||||
/// var status = new GenericColumnExpression("Status", "Orders");
|
||||
///
|
||||
/// // Create NOT IN expression
|
||||
/// var filter = new NotInExpression(
|
||||
/// status,
|
||||
/// "Cancelled",
|
||||
/// "Rejected",
|
||||
/// "Pending"
|
||||
/// );
|
||||
///
|
||||
/// // Or use negation operator
|
||||
/// var inExpr = new InExpression(status, "Cancelled", "Rejected");
|
||||
/// var notInExpr = !inExpr; // Becomes NotInExpression
|
||||
///
|
||||
/// // Generates: WHERE Status NOT IN ('Cancelled', 'Rejected', 'Pending')
|
||||
/// </code>
|
||||
/// </example>
|
||||
public class NotInExpression : BooleanExpression
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the expression to search for in the value list.
|
||||
/// </summary>
|
||||
public Expression SearchExpression { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the array of values to compare against.
|
||||
/// </summary>
|
||||
public Expression[] ValuesToCompare { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="NotInExpression"/> class.
|
||||
/// </summary>
|
||||
/// <param name="searchExpression">The expression to search for.</param>
|
||||
/// <param name="valuesToCompare">
|
||||
/// Expressions representing the values to compare against. The search expression
|
||||
/// matches if it does NOT equal any of these values.
|
||||
/// </param>
|
||||
/// <exception cref="ArgumentNullException">
|
||||
/// Thrown when <paramref name="valuesToCompare"/> is null.
|
||||
/// </exception>
|
||||
/// <remarks>
|
||||
/// Example:
|
||||
/// <code language="csharp">
|
||||
/// var status = new GenericColumnExpression("Status", "Orders");
|
||||
/// var filter = new NotInExpression(status, "Cancelled", "Rejected");
|
||||
/// // Generates: Status NOT IN ('Cancelled', 'Rejected')
|
||||
/// </code>
|
||||
/// </remarks>
|
||||
public NotInExpression(Expression searchExpression, params Expression[] valuesToCompare)
|
||||
{
|
||||
SearchExpression = searchExpression;
|
||||
ValuesToCompare = valuesToCompare ?? throw new ArgumentNullException(nameof(valuesToCompare));
|
||||
}
|
||||
|
||||
public override T Accept<T>(IVisitor<T> visitor)
|
||||
{
|
||||
return visitor.VisitNotInExpression(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
using Strata.SqlTools.SqlBreakdown.Interfaces.Core;
|
||||
|
||||
using Strata.SqlTools.SqlBreakdown.Expressions.Arithmetic;
|
||||
using Strata.SqlTools.SqlBreakdown.Expressions.Conditional.Comparisons;
|
||||
using Strata.SqlTools.SqlBreakdown.Expressions.Literals;
|
||||
|
||||
namespace Strata.SqlTools.SqlBreakdown.Expressions;
|
||||
|
||||
/// <summary>
|
||||
/// Abstract base class for all SQL expression types. Provides operator overloading for
|
||||
/// building complex SQL expressions using C# operators and implicit conversions for
|
||||
/// common value types.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This class enables type-safe SQL expression building using familiar C# syntax.
|
||||
/// Supports arithmetic operators (+, -, *, /), comparison operators (==, !=, >, <, >=, <=),
|
||||
/// and implicit conversions from common .NET types. All derived expression classes
|
||||
/// implement the visitor pattern through <see cref="Accept{T}"/> for SQL generation.
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// <code language="csharp">
|
||||
/// // Build expressions using operators
|
||||
/// var price = new GenericColumnExpression("Price", "Products");
|
||||
/// var discount = new GenericColumnExpression("Discount", "Products");
|
||||
///
|
||||
/// // Arithmetic operations
|
||||
/// var discountedPrice = price * (1.0m - discount);
|
||||
///
|
||||
/// // Comparison operations
|
||||
/// var affordableItems = price <= 100;
|
||||
/// var expensiveItems = price > 1000;
|
||||
///
|
||||
/// // Implicit conversions from literals
|
||||
/// Expression literalNumber = 42.5m;
|
||||
/// Expression literalString = "Sample";
|
||||
/// Expression literalDate = new DateTime(2024, 1, 1);
|
||||
///
|
||||
/// // Factory method for dynamic values
|
||||
/// Expression valueFromObject = Expression.FromObject(someValue);
|
||||
/// </code>
|
||||
/// </example>
|
||||
public abstract class Expression
|
||||
{
|
||||
/// <summary>
|
||||
/// Determines whether the specified object is equal to the current expression.
|
||||
/// Uses reference equality since the == operator is overloaded for SQL expression building.
|
||||
/// </summary>
|
||||
/// <param name="obj">The object to compare with the current expression.</param>
|
||||
/// <returns>True if the specified object is the same instance; otherwise, false.</returns>
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return ReferenceEquals(this, obj);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the hash code for this expression instance.
|
||||
/// Uses the base implementation for reference-based hashing.
|
||||
/// </summary>
|
||||
/// <returns>A hash code for the current expression.</returns>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Accepts a visitor for the visitor pattern, allowing different SQL generation
|
||||
/// strategies.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The return type of the visitor.</typeparam>
|
||||
/// <param name="visitor">
|
||||
/// The visitor instance that will process this expression.
|
||||
/// </param>
|
||||
/// <returns>The result from the visitor's processing of this expression.</returns>
|
||||
public abstract T Accept<T>(IVisitor<T> visitor);
|
||||
|
||||
/// <summary>
|
||||
/// Creates an appropriate expression from a .NET object value. Automatically converts
|
||||
/// common types to their corresponding literal expressions.
|
||||
/// </summary>
|
||||
/// <param name="value">
|
||||
/// The object to convert. Supported types include numeric types (short, int, long,
|
||||
/// double, decimal), bool, DateTime, DateOnly, DateTimeOffset, and string.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// A literal expression representing the value. Returns
|
||||
/// <see cref="NullLiteralExpression"/> for null values.
|
||||
/// </returns>
|
||||
/// <example>
|
||||
/// <code language="csharp">
|
||||
/// // Numeric types become NumberLiteralExpression
|
||||
/// var num = Expression.FromObject(42);
|
||||
///
|
||||
/// // Boolean values become BooleanLiteralExpression
|
||||
/// var flag = Expression.FromObject(true);
|
||||
///
|
||||
/// // DateTime values become DateTimeLiteralExpression
|
||||
/// var date = Expression.FromObject(DateTime.Now);
|
||||
///
|
||||
/// // Strings become StringLiteralExpression
|
||||
/// var text = Expression.FromObject("example");
|
||||
///
|
||||
/// // Null becomes NullLiteralExpression
|
||||
/// var nullValue = Expression.FromObject(null);
|
||||
/// </code>
|
||||
/// </example>
|
||||
public static Expression FromObject(object? value)
|
||||
{
|
||||
return value switch
|
||||
{
|
||||
null => new NullLiteralExpression(),
|
||||
short s => new NumberLiteralExpression(s),
|
||||
int i => new NumberLiteralExpression(i),
|
||||
long l => new NumberLiteralExpression(l),
|
||||
double d => new NumberLiteralExpression((decimal)d),
|
||||
decimal m => new NumberLiteralExpression(m),
|
||||
bool b => new BooleanLiteralExpression(b),
|
||||
DateOnly d => new DateTimeLiteralExpression(d.ToDateTime(TimeOnly.MinValue, DateTimeKind.Utc)),
|
||||
DateTime dt => new DateTimeLiteralExpression(dt),
|
||||
DateTimeOffset dto => new DateTimeLiteralExpression(dto.UtcDateTime),
|
||||
string s when DateTime.TryParse(s, System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.None, out var dt) => new DateTimeLiteralExpression(dt),
|
||||
_ when value.ToString() == null => new NullLiteralExpression(),
|
||||
_ => new StringLiteralExpression(value.ToString()!)
|
||||
};
|
||||
}
|
||||
|
||||
#region Literal Value Implicit Operators
|
||||
/// <summary>
|
||||
/// Implicitly converts a decimal number to a
|
||||
/// <see cref="NumberLiteralExpression"/>.
|
||||
/// </summary>
|
||||
/// <param name="number">The numeric value.</param>
|
||||
public static implicit operator Expression(decimal number) => new NumberLiteralExpression(number);
|
||||
|
||||
/// <summary>
|
||||
/// Implicitly converts a string to a <see cref="StringLiteralExpression"/>.
|
||||
/// </summary>
|
||||
/// <param name="value">The string value.</param>
|
||||
public static implicit operator Expression(string value) => new StringLiteralExpression(value);
|
||||
|
||||
/// <summary>
|
||||
/// Implicitly converts a DateTime to a <see cref="DateTimeLiteralExpression"/>.
|
||||
/// </summary>
|
||||
/// <param name="dateTime">The DateTime value.</param>
|
||||
public static implicit operator Expression(DateTime dateTime) => new DateTimeLiteralExpression(dateTime);
|
||||
|
||||
/// <summary>
|
||||
/// Implicitly converts a DateOnly to a <see cref="DateTimeLiteralExpression"/>.
|
||||
/// </summary>
|
||||
/// <param name="date">The DateOnly value.</param>
|
||||
public static implicit operator Expression(DateOnly date) => new DateTimeLiteralExpression(date.ToDateTime(TimeOnly.MinValue, DateTimeKind.Utc));
|
||||
#endregion
|
||||
|
||||
#region Comparison Operators
|
||||
/// <summary>
|
||||
/// Creates an equality comparison expression (=).
|
||||
/// </summary>
|
||||
/// <param name="a">The left expression.</param>
|
||||
/// <param name="b">The right expression.</param>
|
||||
/// <returns>An <see cref="EqualToExpression"/>.</returns>
|
||||
public static ComparisonOperatorExpression operator ==(Expression a, Expression b) => new EqualToExpression(a, b);
|
||||
|
||||
/// <summary>
|
||||
/// Creates an inequality comparison expression (!=).
|
||||
/// </summary>
|
||||
/// <param name="a">The left expression.</param>
|
||||
/// <param name="b">The right expression.</param>
|
||||
/// <returns>A <see cref="NotEqualToExpression"/>.</returns>
|
||||
public static ComparisonOperatorExpression operator !=(Expression a, Expression b) => new NotEqualToExpression(a, b);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a greater-than comparison expression (>).
|
||||
/// </summary>
|
||||
/// <param name="a">The left expression.</param>
|
||||
/// <param name="b">The right expression.</param>
|
||||
/// <returns>A <see cref="GreaterThanExpression"/>.</returns>
|
||||
public static ComparisonOperatorExpression operator >(Expression a, Expression b) => new GreaterThanExpression(a, b);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a less-than comparison expression (<).
|
||||
/// </summary>
|
||||
/// <param name="a">The left expression.</param>
|
||||
/// <param name="b">The right expression.</param>
|
||||
/// <returns>A <see cref="LessThanExpression"/>.</returns>
|
||||
public static ComparisonOperatorExpression operator <(Expression a, Expression b) => new LessThanExpression(a, b);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a greater-than-or-equal comparison expression (>=).
|
||||
/// </summary>
|
||||
/// <param name="a">The left expression.</param>
|
||||
/// <param name="b">The right expression.</param>
|
||||
/// <returns>A <see cref="GreaterThanOrEqualToExpression"/>.</returns>
|
||||
public static ComparisonOperatorExpression operator >=(Expression a, Expression b) => new GreaterThanOrEqualToExpression(a, b);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a less-than-or-equal comparison expression (<=).
|
||||
/// </summary>
|
||||
/// <param name="a">The left expression.</param>
|
||||
/// <param name="b">The right expression.</param>
|
||||
/// <returns>A <see cref="LessThanOrEqualToExpression"/>.</returns>
|
||||
public static ComparisonOperatorExpression operator <=(Expression a, Expression b) => new LessThanOrEqualToExpression(a, b);
|
||||
#endregion
|
||||
|
||||
#region Arithmetic Operators
|
||||
/// <summary>
|
||||
/// Creates an addition expression (+).
|
||||
/// </summary>
|
||||
/// <param name="a">The left expression.</param>
|
||||
/// <param name="b">The right expression.</param>
|
||||
/// <returns>An <see cref="AdditionExpression"/>.</returns>
|
||||
public static ArithmeticExpression operator +(Expression a, Expression b) => new AdditionExpression(a, b);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a subtraction expression (-).
|
||||
/// </summary>
|
||||
/// <param name="a">The left expression.</param>
|
||||
/// <param name="b">The right expression.</param>
|
||||
/// <returns>A <see cref="SubtractionExpression"/>.</returns>
|
||||
public static ArithmeticExpression operator -(Expression a, Expression b) => new SubtractionExpression(a, b);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a multiplication expression (*).
|
||||
/// </summary>
|
||||
/// <param name="a">The left expression.</param>
|
||||
/// <param name="b">The right expression.</param>
|
||||
/// <returns>A <see cref="MultiplicationExpression"/>.</returns>
|
||||
public static ArithmeticExpression operator *(Expression a, Expression b) => new MultiplicationExpression(a, b);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a division expression (/).
|
||||
/// </summary>
|
||||
/// <param name="a">The left expression.</param>
|
||||
/// <param name="b">The right expression.</param>
|
||||
/// <returns>A <see cref="DivisionExpression"/>.</returns>
|
||||
public static ArithmeticExpression operator /(Expression a, Expression b) => new DivisionExpression(a, b);
|
||||
#endregion
|
||||
}
|
||||
|
||||
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
using Strata.SqlTools.SqlBreakdown.Interfaces.Core;
|
||||
|
||||
namespace Strata.SqlTools.SqlBreakdown.Expressions.Functions.Aggregate;
|
||||
|
||||
/// <summary>
|
||||
/// <para>
|
||||
/// Aggregate functions operate on values across rows to perform mathematical calculations such as sum, average, counting, minimum/maximum values,
|
||||
/// standard deviation, and estimation, as well as some non-mathematical operations.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// An aggregate function takes multiple rows (actually, zero, one, or more rows) as input and produces a single output. In contrast, scalar functions
|
||||
/// take one row as input and produce one row (one value) as output.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// An aggregate function always returns exactly one row, even when the input contains zero rows. Typically, if the input contained zero rows, the
|
||||
/// output is NULL. However, an aggregate function could return 0, an empty string, or some other value when passed zero rows.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public abstract class AggregateFunctionExpression : FunctionExpression
|
||||
{
|
||||
protected AggregateFunctionExpression(params Expression[] arguments) : base(arguments)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public override T Accept<T>(IVisitor<T> visitor)
|
||||
{
|
||||
return visitor.VisitAggregateFunctionExpression(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
namespace Strata.SqlTools.SqlBreakdown.Expressions.Functions.Aggregate;
|
||||
|
||||
/// <summary>
|
||||
/// Returns the average of non-NULL records. If all records inside a group are NULL, the function returns NULL.
|
||||
/// </summary>
|
||||
public class AverageFunction : AggregateFunctionExpression
|
||||
{
|
||||
public override string FunctionName => "AVG";
|
||||
|
||||
/// <summary>
|
||||
/// constructor for Average aggregate function
|
||||
/// </summary>
|
||||
/// <param name="argument">An expression that evaluates to a numeric data type (INTEGER, FLOAT, DECIMAL, etc.).</param>
|
||||
public AverageFunction(Expression argument) : base(argument)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Strata.SqlTools.SqlBreakdown.Expressions.Functions.Aggregate;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a COUNT aggregate function that counts rows or non-NULL values.
|
||||
/// COUNT(*) counts all rows, while COUNT(column) counts non-NULL values in the column.
|
||||
/// </summary>
|
||||
[DebuggerDisplay("COUNT({Arguments[0]})")]
|
||||
public class CountFunction : AggregateFunctionExpression
|
||||
{
|
||||
public override string FunctionName => "COUNT";
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CountFunction"/> class.
|
||||
/// </summary>
|
||||
/// <param name="argument">The expression to count (column or * for all rows).</param>
|
||||
public CountFunction(Expression argument) : base(argument)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the expression being counted.
|
||||
/// </summary>
|
||||
public Expression Expression => Arguments[0];
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Strata.SqlTools.SqlBreakdown.Expressions.Functions.Aggregate;
|
||||
|
||||
/// <summary>
|
||||
/// Returns the sum of non-NULL records for expr. You can use the DISTINCT keyword to compute the sum of unique non-null
|
||||
/// values. If all records inside a group are NULL, the function returns NULL.
|
||||
/// </summary>
|
||||
[DebuggerDisplay("SUM({Arguments[0]})")]
|
||||
public class SumFunction : AggregateFunctionExpression
|
||||
{
|
||||
public override string FunctionName => "SUM";
|
||||
|
||||
/// <summary>
|
||||
/// constructor for SumFunction
|
||||
/// </summary>
|
||||
/// <param name="argument">An expression that evaluates to a numeric data type (INTEGER, FLOAT, DECIMAL, etc.).</param>
|
||||
public SumFunction(Expression argument) : base(argument)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
using Strata.SqlTools.SqlBreakdown.Interfaces.Core;
|
||||
|
||||
using Strata.SqlTools.SqlBreakdown.Expressions.Conditional;
|
||||
|
||||
namespace Strata.SqlTools.SqlBreakdown.Expressions.Functions.Conditional;
|
||||
|
||||
/// <summary>
|
||||
/// <para>
|
||||
/// Works like a cascading “if-then-else” statement. In the more general form, a series of conditions are evaluated in sequence. When a condition evaluates to TRUE, the evaluation stops and the associated result (after THEN) is returned. If none of the conditions evaluate to TRUE, then the result after the optional ELSE is returned, if present; otherwise NULL is returned.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public class CaseExpression : ConditionalFunctionExpression
|
||||
{
|
||||
private readonly List<(BooleanExpression condition, Expression result)> _conditionResultPairs;
|
||||
|
||||
public IReadOnlyList<(BooleanExpression condition, Expression result)> ConditionResultPairs => _conditionResultPairs;
|
||||
|
||||
public Expression? ElseResultExpression { get; }
|
||||
|
||||
public override string FunctionName => "CASE";
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for <see cref="CaseExpression"/> with only one condition
|
||||
/// </summary>
|
||||
/// <param name="condition1">Condition if true then <paramref name="result1"/> is returned</param>
|
||||
/// <param name="result1">Result returned if <paramref name="condition1"/> is met. The result should be an expression that evaluates to a single value.</param>
|
||||
/// <param name="elseResultExpression">Result returned if no conditions are met. If null, and no matches are found, then the result is NULL.</param>
|
||||
public CaseExpression(BooleanExpression condition1, Expression result1, Expression? elseResultExpression = null)
|
||||
: this(new[] { (condition1, result1) }, elseResultExpression)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public CaseExpression(
|
||||
BooleanExpression condition1, Expression result1,
|
||||
BooleanExpression condition2, Expression result2,
|
||||
Expression? elseResultExpression = null
|
||||
) : this(new[] { (condition1, result1), (condition2, result2) }, elseResultExpression)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public CaseExpression(
|
||||
BooleanExpression condition1, Expression result1,
|
||||
BooleanExpression condition2, Expression result2,
|
||||
BooleanExpression condition3, Expression result3,
|
||||
Expression? elseResultExpression = null
|
||||
) : this(new[] { (condition1, result1), (condition2, result2), (condition3, result3) }, elseResultExpression)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
private CaseExpression(IEnumerable<(BooleanExpression condition, Expression result)> pairs, Expression? elseResultExpression = null)
|
||||
{
|
||||
_conditionResultPairs = pairs.ToList();
|
||||
|
||||
ElseResultExpression = elseResultExpression;
|
||||
}
|
||||
|
||||
public void AddConditionResultPair(BooleanExpression condition, Expression result)
|
||||
{
|
||||
_conditionResultPairs.Add((condition, result));
|
||||
}
|
||||
|
||||
public override T Accept<T>(IVisitor<T> visitor)
|
||||
{
|
||||
return visitor.VisitCaseFunctionExpression(this);
|
||||
}
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
namespace Strata.SqlTools.SqlBreakdown.Expressions.Functions.Conditional;
|
||||
|
||||
/// <summary>
|
||||
/// Conditional expression functions return values based on logical operations using each expression passed to the function.
|
||||
/// </summary>
|
||||
public abstract class ConditionalFunctionExpression : FunctionExpression
|
||||
{
|
||||
protected ConditionalFunctionExpression(params Expression[] arguments) : base(arguments)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
using Strata.SqlTools.SqlBreakdown.Interfaces.Core;
|
||||
|
||||
using Strata.SqlTools.SqlBreakdown.Expressions.Conditional;
|
||||
|
||||
namespace Strata.SqlTools.SqlBreakdown.Expressions.Functions.Conditional;
|
||||
|
||||
/// <summary>
|
||||
/// Single-level if-then-else expression. Similar to <see cref="CaseExpression"/>, but only allows a single condition.
|
||||
/// </summary>
|
||||
public class IfThenElseExpression : ConditionalFunctionExpression
|
||||
{
|
||||
public override string FunctionName => "IFF";
|
||||
|
||||
public BooleanExpression Condition { get; }
|
||||
public Expression ResultIfTrue { get; }
|
||||
public Expression ResultIfFalse { get; }
|
||||
|
||||
public IfThenElseExpression(BooleanExpression condition, Expression resultIfTrue, Expression resultIfFalse) : base(condition, resultIfTrue, resultIfFalse)
|
||||
{
|
||||
Condition = condition;
|
||||
ResultIfTrue = resultIfTrue;
|
||||
ResultIfFalse = resultIfFalse;
|
||||
}
|
||||
|
||||
public override T Accept<T>(IVisitor<T> visitor)
|
||||
{
|
||||
return visitor.VisitFunctionExpression(this);
|
||||
}
|
||||
}
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
using Strata.SqlTools.SqlBreakdown.Interfaces.Core;
|
||||
|
||||
namespace Strata.SqlTools.SqlBreakdown.Expressions.Functions.Conditional;
|
||||
|
||||
public class NullIfExpression : ConditionalFunctionExpression
|
||||
{
|
||||
public Expression A { get; }
|
||||
public override string FunctionName => "NULLIF";
|
||||
|
||||
public NullIfExpression(Expression a) : base(a)
|
||||
{
|
||||
A = a;
|
||||
}
|
||||
|
||||
public override T Accept<T>(IVisitor<T> visitor)
|
||||
{
|
||||
return visitor.VisitFunctionExpression(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
using Strata.SqlTools.SqlBreakdown.Interfaces.Core;
|
||||
|
||||
namespace Strata.SqlTools.SqlBreakdown.Expressions.Functions.DateTime;
|
||||
|
||||
public class DateAddFunction : FunctionExpression
|
||||
{
|
||||
public Expression DateExpression { get; }
|
||||
public string DatePart { get; }
|
||||
public Expression Value { get; }
|
||||
|
||||
public override string FunctionName => "DATEADD";
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for <see cref="DateAddFunction"/>
|
||||
/// </summary>
|
||||
/// <param name="dateExpression">the date, time, or timestamp to which you want to add.</param>
|
||||
/// <param name="datePart">units of time that you want to add</param>
|
||||
/// <param name="value">Number of units of time that you want to add. For example, if you want to add 2 days, this will be 2.</param>
|
||||
public DateAddFunction(Expression dateExpression, string datePart, Expression value) : base(datePart, value, dateExpression)
|
||||
{
|
||||
DateExpression = dateExpression;
|
||||
DatePart = datePart;
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public override T Accept<T>(IVisitor<T> visitor)
|
||||
{
|
||||
return visitor.VisitFunctionExpression(this);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
using Strata.SqlTools.SqlBreakdown.Interfaces.Core;
|
||||
|
||||
namespace Strata.SqlTools.SqlBreakdown.Expressions.Functions.DateTime;
|
||||
|
||||
public class DatePartFunction : FunctionExpression
|
||||
{
|
||||
public Expression DateExpression { get; }
|
||||
public string DatePart { get; }
|
||||
|
||||
public override string FunctionName => "DATE_PART";
|
||||
|
||||
public DatePartFunction(Expression dateExpression, string datePart) : base(datePart, dateExpression)
|
||||
{
|
||||
DateExpression = dateExpression;
|
||||
DatePart = datePart;
|
||||
}
|
||||
|
||||
public override T Accept<T>(IVisitor<T> visitor)
|
||||
{
|
||||
return visitor.VisitFunctionExpression(this);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
using Strata.SqlTools.SqlBreakdown.Interfaces.Core;
|
||||
|
||||
namespace Strata.SqlTools.SqlBreakdown.Expressions.Functions.DateTime;
|
||||
|
||||
public class TruncateDateFunction : FunctionExpression
|
||||
{
|
||||
public Expression DateTimeExpression { get; }
|
||||
public string DatePart { get; }
|
||||
|
||||
public override string FunctionName => "DATE_TRUNC";
|
||||
|
||||
public TruncateDateFunction(Expression dateTimeExpression, string datePart) : base(datePart, dateTimeExpression)
|
||||
{
|
||||
DateTimeExpression = dateTimeExpression;
|
||||
DatePart = datePart;
|
||||
}
|
||||
|
||||
public override T Accept<T>(IVisitor<T> visitor)
|
||||
{
|
||||
return visitor.VisitFunctionExpression(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
namespace Strata.SqlTools.SqlBreakdown.Expressions.Functions;
|
||||
|
||||
/// <summary>
|
||||
/// Abstract base class for SQL function expressions. Represents function calls in SQL
|
||||
/// queries with a function name and zero or more argument expressions.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Function expressions enable type-safe representation of SQL functions such as
|
||||
/// aggregate functions (COUNT, SUM, AVG), date functions (DATEADD, DATEDIFF), string
|
||||
/// functions (SUBSTRING, CONCAT), and more. Derived classes specify the function name
|
||||
/// and implement the visitor pattern for SQL generation.
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// <code language="csharp">
|
||||
/// // Example aggregate function
|
||||
/// public class CountFunction : FunctionExpression
|
||||
/// {
|
||||
/// public override string FunctionName => "COUNT";
|
||||
///
|
||||
/// public CountFunction(Expression column) : base(column) { }
|
||||
/// }
|
||||
///
|
||||
/// // Usage
|
||||
/// var qty = new GenericColumnExpression("Quantity", "Orders");
|
||||
/// var countQty = new CountFunction(qty);
|
||||
/// // Generates: COUNT(Quantity)
|
||||
/// </code>
|
||||
/// </example>
|
||||
public abstract class FunctionExpression : Expression
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the name of the SQL function.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The function name as it should appear in the generated SQL (e.g., "COUNT",
|
||||
/// "SUM", "DATEADD").
|
||||
/// </value>
|
||||
public abstract string FunctionName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the array of expressions that serve as arguments to the function.
|
||||
/// </summary>
|
||||
/// <value>Zero or more expressions representing the function arguments.</value>
|
||||
public Expression[] Arguments { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FunctionExpression"/> class.
|
||||
/// </summary>
|
||||
/// <param name="arguments">
|
||||
/// Variable number of expressions serving as function arguments.
|
||||
/// </param>
|
||||
protected FunctionExpression(params Expression[] arguments)
|
||||
{
|
||||
Arguments = arguments;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
using Strata.SqlTools.SqlBreakdown.Interfaces.Core;
|
||||
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Strata.SqlTools.SqlBreakdown.Expressions.Functions;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a SUBSTRING string function for extracting substrings from text.
|
||||
/// Supports both the SUBSTRING(string, start, length) and SUBSTRING(string, start) syntax.
|
||||
/// </summary>
|
||||
[DebuggerDisplay("SUBSTRING({Arguments[0]}, ...)")]
|
||||
public class SubstringFunction : FunctionExpression
|
||||
{
|
||||
public override string FunctionName => "SUBSTRING";
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SubstringFunction"/> class.
|
||||
/// </summary>
|
||||
/// <param name="arguments">The function arguments (string expression, start position, and optional length).</param>
|
||||
public SubstringFunction(params Expression[] arguments) : base(arguments)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Accepts a visitor for the visitor pattern.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The return type of the visitor.</typeparam>
|
||||
/// <param name="visitor">The visitor to accept.</param>
|
||||
/// <returns>The result of the visitor visit method.</returns>
|
||||
public override T Accept<T>(IVisitor<T> visitor)
|
||||
{
|
||||
// SUBSTRING is a scalar function, not an aggregate function
|
||||
// For now, return a default value since we don't have specific visitor methods for it
|
||||
// This is acceptable for parsing - the function has been recognized and won't throw
|
||||
return default!;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
using Strata.SqlTools.SqlBreakdown.Classes;
|
||||
using Strata.SqlTools.SqlBreakdown.Interfaces.Core;
|
||||
|
||||
namespace Strata.SqlTools.SqlBreakdown.Expressions;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a generic column reference in a SQL query with support for table and
|
||||
/// schema qualification. This class provides a flexible way to reference columns
|
||||
/// without requiring pre-registered table metadata, making it ideal for dynamic
|
||||
/// query construction and ad-hoc queries.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Unlike <see cref="RegisteredTableColumnExpression"/>, which requires a
|
||||
/// pre-registered table source with column IDs, GenericColumnExpression allows
|
||||
/// referencing any column by name with optional table and schema qualification.
|
||||
/// This class supports operator overloading for building complex expressions using
|
||||
/// standard mathematical and comparison operators (+, -, *, /, ==, !=, >, <, >=, <=).
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// <code language="csharp">
|
||||
/// // Simple column reference
|
||||
/// var price = new GenericColumnExpression("Price", "Products");
|
||||
///
|
||||
/// // Column with schema qualification
|
||||
/// var customerId = new GenericColumnExpression("CustomerID", "dbo", "Customers");
|
||||
///
|
||||
/// // Using operator overloads for arithmetic
|
||||
/// var priceWithTax = price * 1.1m;
|
||||
/// var totalPrice = price + 10;
|
||||
///
|
||||
/// // Using comparison operators
|
||||
/// var expensiveItems = price > 100;
|
||||
///
|
||||
/// // Building a complete query
|
||||
/// var query = new QueryBreakdown();
|
||||
/// query.FromClause.Clause = "Products";
|
||||
/// query.AddSelectExpression(price);
|
||||
/// query.AddSelectExpression(priceWithTax, "PriceWithTax");
|
||||
/// query.AddWhereExpression(expensiveItems);
|
||||
/// // Generates: SELECT Price, (Price * 1.1) AS PriceWithTax FROM Products WHERE Price > 100
|
||||
/// </code>
|
||||
/// </example>
|
||||
public class GenericColumnExpression : ColumnExpression<TableSource>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GenericColumnExpression"/> class
|
||||
/// with schema and table qualification.
|
||||
/// </summary>
|
||||
/// <param name="columnName">The name of the column.</param>
|
||||
/// <param name="schema">The schema name (e.g., "dbo").</param>
|
||||
/// <param name="tableName">The table name.</param>
|
||||
/// <remarks>
|
||||
/// Example:
|
||||
/// <code language="csharp">
|
||||
/// var column = new GenericColumnExpression("EmployeeID", "hr", "Employees");
|
||||
/// // Results in: hr.Employees.EmployeeID in the generated SQL
|
||||
/// </code>
|
||||
/// </remarks>
|
||||
public GenericColumnExpression(string columnName, string schema, string tableName) : this(columnName, new TableSource(tableName, schema))
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GenericColumnExpression"/> class
|
||||
/// with table qualification.
|
||||
/// </summary>
|
||||
/// <param name="columnName">The name of the column.</param>
|
||||
/// <param name="tableName">The table name.</param>
|
||||
/// <remarks>
|
||||
/// Example:
|
||||
/// <code language="csharp">
|
||||
/// var column = new GenericColumnExpression("ProductName", "Products");
|
||||
/// // Results in: Products.ProductName in the generated SQL
|
||||
/// </code>
|
||||
/// </remarks>
|
||||
public GenericColumnExpression(string columnName, string tableName) : this(columnName, new TableSource(tableName))
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GenericColumnExpression"/> class
|
||||
/// with a table source.
|
||||
/// </summary>
|
||||
/// <param name="columnName">The name of the column.</param>
|
||||
/// <param name="source">
|
||||
/// The table source containing table and optional schema information.
|
||||
/// </param>
|
||||
/// <remarks>
|
||||
/// Example:
|
||||
/// <code language="csharp">
|
||||
/// var tableSource = new TableSource("Orders", "sales");
|
||||
/// var column = new GenericColumnExpression("OrderDate", tableSource);
|
||||
/// // Results in: sales.Orders.OrderDate in the generated SQL
|
||||
/// </code>
|
||||
/// </remarks>
|
||||
public GenericColumnExpression(string columnName, TableSource source) : base(columnName, source)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,474 @@
|
||||
using Strata.SqlTools.SqlBreakdown.Interfaces.Core;
|
||||
|
||||
using System.Collections;
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Strata.SqlTools.SqlBreakdown.Expressions;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a reference to a property from an input data source, typically used for
|
||||
/// dynamic data binding in query construction. Used when building queries that reference
|
||||
/// external data sources by GUID identifiers.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This expression type is used in scenarios where data comes from registered data
|
||||
/// sources (like data tables or dimensions) that are identified by GUIDs. The
|
||||
/// expression can reference a primary data source and optionally a secondary data source
|
||||
/// for hierarchical or relational lookups.
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// <code language="csharp">
|
||||
/// // Reference a simple property from a data source
|
||||
/// var patientName = new InputPropertyExpression(
|
||||
/// dataSourceGuid: Guid.Parse("41639c8f-fecf-4449-b6e6-53f796c0c3e4"),
|
||||
/// dataKeyLookup: "PatientName"
|
||||
/// );
|
||||
///
|
||||
/// // Reference a property with a secondary data source (dimension lookup)
|
||||
/// var patientType = new InputPropertyExpression(
|
||||
/// dataSourceGuid: Guid.Parse("41639c8f-fecf-4449-b6e6-53f796c0c3e4"),
|
||||
/// secondaryDataSource: Guid.Parse("6ef6b1f7-a50c-4198-8866-140bb82e2dda"),
|
||||
/// dataKeyLookup: "PatientTypeRollupName"
|
||||
/// );
|
||||
/// </code>
|
||||
/// </example>
|
||||
public class InputPropertyExpression : Expression
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the GUID identifier of the primary data source.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The data source GUID, typically representing a data table or primary data entity.
|
||||
/// </value>
|
||||
/// <example>
|
||||
/// 41639c8f-fecf-4449-b6e6-53f796c0c3e4 - the data table id of PES
|
||||
/// </example>
|
||||
public Guid DataSourceGuid { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the optional GUID identifier of a secondary data source.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The secondary data source GUID, typically representing a dimension or lookup table.
|
||||
/// Null if no secondary source is required.
|
||||
/// </value>
|
||||
/// <example>
|
||||
/// 6ef6b1f7-a50c-4198-8866-140bb82e2dda - the dimension id of the Patient Type
|
||||
/// Rollup dimension
|
||||
/// </example>
|
||||
public Guid? SecondaryDataSource { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the key name used to look up the data value.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The property or column name within the data source.
|
||||
/// </value>
|
||||
/// <example>"PatientTypeRollupName"</example>
|
||||
public string DataKeyLookup { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="InputPropertyExpression"/> class
|
||||
/// with a primary data source.
|
||||
/// </summary>
|
||||
/// <param name="dataSourceGuid">The GUID of the primary data source.</param>
|
||||
/// <param name="dataKeyLookup">The key name for data lookup.</param>
|
||||
/// <remarks>
|
||||
/// Example:
|
||||
/// <code language="csharp">
|
||||
/// var property = new InputPropertyExpression(
|
||||
/// Guid.Parse("41639c8f-fecf-4449-b6e6-53f796c0c3e4"),
|
||||
/// "PatientName");
|
||||
/// </code>
|
||||
/// </remarks>
|
||||
public InputPropertyExpression(Guid dataSourceGuid, string dataKeyLookup) : this(dataSourceGuid, null, dataKeyLookup)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="InputPropertyExpression"/> class
|
||||
/// with primary and optional secondary data sources.
|
||||
/// </summary>
|
||||
/// <param name="dataSourceGuid">The GUID of the primary data source.</param>
|
||||
/// <param name="secondaryDataSource">
|
||||
/// The optional GUID of the secondary data source.
|
||||
/// </param>
|
||||
/// <param name="dataKeyLookup">The key name for data lookup.</param>
|
||||
/// <remarks>
|
||||
/// Example with secondary data source:
|
||||
/// <code language="csharp">
|
||||
/// var property = new InputPropertyExpression(
|
||||
/// Guid.Parse("41639c8f-fecf-4449-b6e6-53f796c0c3e4"),
|
||||
/// Guid.Parse("6ef6b1f7-a50c-4198-8866-140bb82e2dda"),
|
||||
/// "PatientTypeRollupName");
|
||||
/// </code>
|
||||
/// </remarks>
|
||||
public InputPropertyExpression(Guid dataSourceGuid, Guid? secondaryDataSource, string dataKeyLookup)
|
||||
{
|
||||
DataSourceGuid = dataSourceGuid;
|
||||
SecondaryDataSource = secondaryDataSource;
|
||||
DataKeyLookup = dataKeyLookup;
|
||||
}
|
||||
|
||||
public override T Accept<T>(IVisitor<T> visitor)
|
||||
{
|
||||
return visitor.VisitInputPropertyExpression(this);
|
||||
}
|
||||
}
|
||||
|
||||
public class CollectionInputPropertyExpression : InputPropertyExpression
|
||||
{
|
||||
public CollectionInputPropertyExpression(Guid dataSourceGuid, Guid secondaryDataSource, string dataKeyLookup)
|
||||
: base(dataSourceGuid, secondaryDataSource, dataKeyLookup)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public InputPropertyExpression? ItemProperty { get; }
|
||||
|
||||
public override T Accept<T>(IVisitor<T> visitor)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
public interface IFlatData : IDictionary<string, object>
|
||||
{
|
||||
string RowIdKey { get; }
|
||||
|
||||
long RowId { get; }
|
||||
}
|
||||
|
||||
public class MyFlatData : Dictionary<string, object>, IFlatData
|
||||
{
|
||||
private readonly Lazy<long> _rowId;
|
||||
|
||||
public MyFlatData() : this(new Dictionary<string, object>())
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public MyFlatData(IDictionary<string, object> rawData) : this(rawData, "RowID")
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public MyFlatData(IDictionary<string, object> rawData, string rowIdKey) : base(rawData)
|
||||
{
|
||||
RowIdKey = rowIdKey;
|
||||
_rowId = new Lazy<long>(() => this.GetValue<long>(RowIdKey), false);
|
||||
}
|
||||
|
||||
public string RowIdKey { get; }
|
||||
|
||||
public long RowId => _rowId.Value;
|
||||
}
|
||||
|
||||
public class FlatData : IFlatData
|
||||
{
|
||||
private readonly IDictionary<string, object> _data;
|
||||
|
||||
private readonly Lazy<long> _rowId;
|
||||
|
||||
public FlatData() : this(new Dictionary<string, object>(), "RowID")
|
||||
{
|
||||
}
|
||||
|
||||
public FlatData(IDictionary<string, object> rawData, string rowIdKey)
|
||||
{
|
||||
_data = rawData;
|
||||
RowIdKey = rowIdKey;
|
||||
_rowId = new Lazy<long>(() => this.GetValue<long>(RowIdKey), false);
|
||||
}
|
||||
|
||||
public string RowIdKey { get; }
|
||||
|
||||
public long RowId => _rowId.Value;
|
||||
|
||||
public IEnumerator<KeyValuePair<string, object>> GetEnumerator()
|
||||
{
|
||||
return _data.GetEnumerator();
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return ((IEnumerable)_data).GetEnumerator();
|
||||
}
|
||||
|
||||
public void Add(KeyValuePair<string, object> item)
|
||||
{
|
||||
_data.Add(item);
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
_data.Clear();
|
||||
}
|
||||
|
||||
public bool Contains(KeyValuePair<string, object> item)
|
||||
{
|
||||
return _data.Contains(item);
|
||||
}
|
||||
|
||||
public void CopyTo(KeyValuePair<string, object>[] array, int arrayIndex)
|
||||
{
|
||||
_data.CopyTo(array, arrayIndex);
|
||||
}
|
||||
|
||||
public bool Remove(KeyValuePair<string, object> item)
|
||||
{
|
||||
return _data.Remove(item);
|
||||
}
|
||||
|
||||
public int Count => _data.Count;
|
||||
|
||||
public bool IsReadOnly { get; }
|
||||
|
||||
public void Add(string key, object value)
|
||||
{
|
||||
_data.Add(key, value);
|
||||
}
|
||||
|
||||
public bool ContainsKey(string key)
|
||||
{
|
||||
return _data.ContainsKey(key);
|
||||
}
|
||||
|
||||
public bool Remove(string key)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public bool TryGetValue(string key, out object value)
|
||||
{
|
||||
return _data.TryGetValue(key, out value!);
|
||||
}
|
||||
|
||||
public IEnumerable<string> Keys => _data.Keys;
|
||||
|
||||
ICollection<object> IDictionary<string, object>.Values => _data.Values;
|
||||
|
||||
ICollection<string> IDictionary<string, object>.Keys => _data.Keys;
|
||||
|
||||
public IEnumerable<object> Values => _data.Values;
|
||||
|
||||
public object this[string key]
|
||||
{
|
||||
get => _data[key];
|
||||
set => _data[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
public static class FlatDataUtils
|
||||
{
|
||||
private static readonly IFormatProvider _culture = new CultureInfo("en-US");
|
||||
|
||||
public static object GetValue(this IFlatData data, string key)
|
||||
{
|
||||
if (data == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(data));
|
||||
}
|
||||
|
||||
if (!data.ContainsKey(key))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
$"The specified key is not available. Requested key: [{key}] Available keys: [{string.Join(", ", data.Keys)}]",
|
||||
nameof(key));
|
||||
}
|
||||
|
||||
var rawValue = data[key];
|
||||
|
||||
return rawValue;
|
||||
}
|
||||
|
||||
public static TValue GetValue<TValue>(this IFlatData data, string key)
|
||||
{
|
||||
var rawValue = data.GetValue(key);
|
||||
|
||||
var convertedValue = (TValue)Convert.ChangeType(rawValue, typeof(TValue), _culture);
|
||||
|
||||
return convertedValue;
|
||||
}
|
||||
|
||||
public static IEnumerable<long> GetRowIds(this IEnumerable<IFlatData> data)
|
||||
{
|
||||
return data.Select(x => x.RowId);
|
||||
}
|
||||
}
|
||||
|
||||
public interface IHierarchicalData
|
||||
{
|
||||
/// <summary>
|
||||
/// The DataSource for this level of the Hierarchy
|
||||
/// </summary>
|
||||
Guid DataSourceGuid { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The Data at this level of the Hierarchy
|
||||
/// </summary>
|
||||
IFlatData Data { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns all Child Data across all DataSources, or an empty collection if there is no Child Data
|
||||
/// </summary>
|
||||
IEnumerable<IHierarchicalData> AllChildData { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Checks if this record has Child Data for a particular DataSource
|
||||
/// </summary>
|
||||
/// <param name="dataSourceGuid">The DataSource to check</param>
|
||||
/// <returns>True if any Child Data exists for the DataSource</returns>
|
||||
bool HasChildData(Guid dataSourceGuid);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the Child Data for the given DataSource
|
||||
/// </summary>
|
||||
/// <param name="dataSourceGuid">DataSource identifier for the child data</param>
|
||||
/// <returns>The Child Data or an empty collection if no child data is set for the DataSource</returns>
|
||||
IEnumerable<IHierarchicalData> GetChildData(Guid dataSourceGuid);
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to add Child Data. Will check if child data exists before adding.
|
||||
/// </summary>
|
||||
/// <param name="datasourceGuid">DataSource identifier for the child data</param>
|
||||
/// <param name="data">The Child Data to Add to the Record</param>
|
||||
/// <returns>False if child data exists and was not overriden</returns>
|
||||
bool TryAddChildData(Guid datasourceGuid, IEnumerable<IHierarchicalData> data);
|
||||
|
||||
/// <summary>
|
||||
/// Add Child Data, will override any existing Child Data for the DataSource
|
||||
/// </summary>
|
||||
/// <param name="datasourceGuid">DataSource identifier for the child data</param>
|
||||
/// <param name="data">The Child Data to Add to the Record</param>
|
||||
void SetChildData(Guid datasourceGuid, IEnumerable<IHierarchicalData> data);
|
||||
}
|
||||
|
||||
public class HierarchicalData : IHierarchicalData
|
||||
{
|
||||
private Dictionary<Guid, List<IHierarchicalData>> _childDataMap;
|
||||
|
||||
[JsonConstructor]
|
||||
public HierarchicalData() : this(Guid.Empty, default!, new List<IHierarchicalData>())
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public HierarchicalData(Guid dataSourceGuid, IFlatData rootData) : this(dataSourceGuid, rootData,
|
||||
new List<IHierarchicalData>())
|
||||
{
|
||||
}
|
||||
|
||||
public HierarchicalData(Guid dataSourceGuid, IFlatData rootData, IEnumerable<IHierarchicalData> childData)
|
||||
{
|
||||
DataSourceGuid = dataSourceGuid;
|
||||
Data = rootData;
|
||||
_childDataMap = childData.GroupBy(x => x.DataSourceGuid).ToDictionary(x => x.Key, x => x.ToList());
|
||||
}
|
||||
|
||||
public Guid DataSourceGuid { get; set; }
|
||||
|
||||
public IFlatData Data { get; set; }
|
||||
|
||||
public IEnumerable<IHierarchicalData> AllChildData
|
||||
{
|
||||
get => _childDataMap.SelectMany(x => x.Value).ToList();
|
||||
set => _childDataMap = value.GroupBy(x => x.DataSourceGuid).ToDictionary(x => x.Key, x => x.ToList());
|
||||
}
|
||||
|
||||
public bool HasChildData(Guid dataSourceGuid)
|
||||
{
|
||||
return _childDataMap.ContainsKey(dataSourceGuid);
|
||||
}
|
||||
|
||||
public IEnumerable<IHierarchicalData> GetChildData(Guid dataSourceGuid)
|
||||
{
|
||||
if (HasChildData(dataSourceGuid))
|
||||
{
|
||||
return _childDataMap[dataSourceGuid];
|
||||
}
|
||||
|
||||
return new List<IHierarchicalData>();
|
||||
}
|
||||
|
||||
public bool TryAddChildData(Guid datasourceGuid, IEnumerable<IHierarchicalData> data)
|
||||
{
|
||||
if (_childDataMap.ContainsKey(datasourceGuid))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_childDataMap[datasourceGuid] = data.ToList();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void SetChildData(Guid datasourceGuid, IEnumerable<IHierarchicalData> data)
|
||||
{
|
||||
_childDataMap[datasourceGuid] = data.ToList();
|
||||
}
|
||||
|
||||
public override string? ToString()
|
||||
{
|
||||
if (Data?.ContainsKey("DimPatientEnEncounterID") ?? false)
|
||||
{
|
||||
return Data["DimPatientEnEncounterID"].ToString();
|
||||
}
|
||||
return base.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
#region Json Converters
|
||||
public class HierarchicalDataConverter : JsonConverter<IHierarchicalData>
|
||||
{
|
||||
public override IHierarchicalData? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
return JsonSerializer.Deserialize<HierarchicalData>(ref reader, options);
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, IHierarchicalData value, JsonSerializerOptions options)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
public class FlatDataConverter : JsonConverter<IFlatData>
|
||||
{
|
||||
public override IFlatData? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
return JsonSerializer.Deserialize<MyFlatData>(ref reader, options);
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, IFlatData value, JsonSerializerOptions options)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
public class ObjectToInferredTypesConverter : JsonConverter<object>
|
||||
{
|
||||
public override object Read(
|
||||
ref Utf8JsonReader reader,
|
||||
Type typeToConvert,
|
||||
JsonSerializerOptions options) => reader.TokenType switch
|
||||
{
|
||||
JsonTokenType.True => true,
|
||||
JsonTokenType.False => false,
|
||||
JsonTokenType.Number when reader.TryGetInt64(out long l) => l,
|
||||
JsonTokenType.Number => reader.GetDouble(),
|
||||
JsonTokenType.String when reader.TryGetDateTime(out DateTime datetime) => datetime,
|
||||
JsonTokenType.String => reader.GetString()!,
|
||||
_ => JsonDocument.ParseValue(ref reader).RootElement.Clone()
|
||||
};
|
||||
|
||||
public override void Write(
|
||||
Utf8JsonWriter writer,
|
||||
object objectToWrite,
|
||||
JsonSerializerOptions options) =>
|
||||
JsonSerializer.Serialize(writer, objectToWrite, objectToWrite.GetType(), options);
|
||||
}
|
||||
#endregion
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
using Strata.SqlTools.SqlBreakdown.Interfaces.Core;
|
||||
|
||||
using Strata.SqlTools.SqlBreakdown.Expressions.Conditional;
|
||||
|
||||
namespace Strata.SqlTools.SqlBreakdown.Expressions.Literals;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a boolean literal value (TRUE or FALSE) in a SQL expression.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// BooleanLiteralExpression stores boolean values and generates TRUE or FALSE keywords
|
||||
/// in SQL. Accessible via <see cref="BooleanExpression.True"/> and
|
||||
/// <see cref="BooleanExpression.False"/> static properties.
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// <code language="csharp">
|
||||
/// // Explicit creation
|
||||
/// var trueValue = new BooleanLiteralExpression(true);
|
||||
/// var falseValue = new BooleanLiteralExpression(false);
|
||||
///
|
||||
/// // Via static properties
|
||||
/// BooleanExpression alwaysTrue = BooleanExpression.True;
|
||||
/// BooleanExpression alwaysFalse = BooleanExpression.False;
|
||||
///
|
||||
/// // In expressions
|
||||
/// var active = new GenericColumnExpression("IsActive", "Users");
|
||||
/// var filter = active == BooleanExpression.True;
|
||||
/// // Generates: IsActive = TRUE
|
||||
/// </code>
|
||||
/// </example>
|
||||
public class BooleanLiteralExpression : BooleanExpression
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the boolean value.
|
||||
/// </summary>
|
||||
/// <value>True or false.</value>
|
||||
public bool Value { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BooleanLiteralExpression"/> class.
|
||||
/// </summary>
|
||||
/// <param name="value">The boolean value (true or false).</param>
|
||||
/// <remarks>
|
||||
/// Example:
|
||||
/// <code language="csharp">
|
||||
/// var trueValue = new BooleanLiteralExpression(true);
|
||||
/// // Or use: BooleanExpression.True or BooleanExpression.False
|
||||
/// </code>
|
||||
/// </remarks>
|
||||
public BooleanLiteralExpression(bool value)
|
||||
{
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public override T Accept<T>(IVisitor<T> visitor)
|
||||
{
|
||||
return visitor.VisitBooleanLiteralExpression(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
using Strata.SqlTools.SqlBreakdown.Interfaces.Core;
|
||||
|
||||
using Strata.SqlTools.SqlBreakdown.Interfaces.Core;
|
||||
|
||||
namespace Strata.SqlTools.SqlBreakdown.Expressions.Literals;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a date/time literal value in a SQL expression.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// DateTimeLiteralExpression stores DateTime values and generates properly formatted
|
||||
/// date/time literals in SQL. The exact format depends on the SQL dialect being
|
||||
/// generated. Automatically created through implicit conversion from DateTime or
|
||||
/// DateOnly values.
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// <code language="csharp">
|
||||
/// // Explicit creation
|
||||
/// var literal = new DateTimeLiteralExpression(new DateTime(2024, 1, 1));
|
||||
///
|
||||
/// // Implicit conversion from DateTime
|
||||
/// Expression startDate = new DateTime(2024, 1, 1);
|
||||
///
|
||||
/// // Implicit conversion from DateOnly
|
||||
/// Expression today = DateOnly.FromDateTime(DateTime.Today);
|
||||
///
|
||||
/// // In expressions
|
||||
/// var orderDate = new GenericColumnExpression("OrderDate", "Orders");
|
||||
/// var recentOrders = orderDate >= new DateTime(2024, 1, 1);
|
||||
/// // Generates: OrderDate >= '2024-01-01' (format varies by SQL dialect)
|
||||
/// </code>
|
||||
/// </example>
|
||||
public class DateTimeLiteralExpression : LiteralValueExpression<DateTime>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DateTimeLiteralExpression"/> class.
|
||||
/// </summary>
|
||||
/// <param name="value">The DateTime value.</param>
|
||||
/// <remarks>
|
||||
/// Example:
|
||||
/// <code language="csharp">
|
||||
/// var literal = new DateTimeLiteralExpression(new DateTime(2024, 1, 1));
|
||||
/// // Or use implicit conversion: Expression date = new DateTime(2024, 1, 1);
|
||||
/// </code>
|
||||
/// </remarks>
|
||||
public DateTimeLiteralExpression(DateTime value) : base(value)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public override T Accept<T>(IVisitor<T> visitor)
|
||||
{
|
||||
return visitor.VisitDateTimeLiteralExpression(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
namespace Strata.SqlTools.SqlBreakdown.Expressions.Literals;
|
||||
|
||||
/// <summary>
|
||||
/// Abstract base class for all literal value expressions in SQL queries. Literal
|
||||
/// expressions represent constant values directly embedded in the SQL.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Literal expressions include numeric values, strings, dates, booleans, and NULL.
|
||||
/// These values are rendered directly in the SQL output rather than being passed as
|
||||
/// parameters. For parameterized values, use <see cref="ParameterExpression"/> instead.
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// <code language="csharp">
|
||||
/// // Literal expressions are typically created via implicit conversions
|
||||
/// Expression num = 42.5m; // NumberLiteralExpression
|
||||
/// Expression text = "example"; // StringLiteralExpression
|
||||
/// Expression date = DateTime.Now; // DateTimeLiteralExpression
|
||||
/// Expression nullValue = Expression.FromObject(null); // NullLiteralExpression
|
||||
/// </code>
|
||||
/// </example>
|
||||
public abstract class LiteralExpression : Expression
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Strata.SqlTools.SqlBreakdown.Expressions.Literals;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for typed literal value expressions. Provides strongly-typed access to
|
||||
/// the literal value through a generic type parameter.
|
||||
/// </summary>
|
||||
/// <typeparam name="TValue">The type of the literal value.</typeparam>
|
||||
/// <remarks>
|
||||
/// This generic base class enables compile-time type safety for literal values while
|
||||
/// maintaining a common structure for all literal expressions.
|
||||
/// </remarks>
|
||||
[DebuggerDisplay("{Value}")]
|
||||
public abstract class LiteralValueExpression<TValue> : LiteralExpression
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the strongly-typed literal value.
|
||||
/// </summary>
|
||||
/// <value>The value of type <typeparamref name="TValue"/>.</value>
|
||||
public TValue Value { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the
|
||||
/// <see cref="LiteralValueExpression{TValue}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="value">The literal value.</param>
|
||||
protected LiteralValueExpression(TValue value)
|
||||
{
|
||||
Value = value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
using Strata.SqlTools.SqlBreakdown.Interfaces.Core;
|
||||
|
||||
namespace Strata.SqlTools.SqlBreakdown.Expressions.Literals;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a NULL literal value in a SQL expression.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// NullLiteralExpression generates the NULL keyword in SQL. Automatically created by
|
||||
/// <see cref="Expression.FromObject"/> when passed a null value. Used in expressions
|
||||
/// that check for NULL values or set columns to NULL.
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// <code language="csharp">
|
||||
/// // Explicit creation
|
||||
/// var nullValue = new NullLiteralExpression();
|
||||
///
|
||||
/// // Via factory method
|
||||
/// Expression nullFromObject = Expression.FromObject(null);
|
||||
///
|
||||
/// // In expressions (NULL checks)
|
||||
/// var email = new GenericColumnExpression("Email", "Users");
|
||||
/// var hasNoEmail = email == new NullLiteralExpression();
|
||||
/// // Generates: Email IS NULL (visitor converts == NULL to IS NULL)
|
||||
/// </code>
|
||||
/// </example>
|
||||
public class NullLiteralExpression : LiteralExpression
|
||||
{
|
||||
public override T Accept<T>(IVisitor<T> visitor)
|
||||
{
|
||||
return visitor.VisitNullLiteralExpression(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
using Strata.SqlTools.SqlBreakdown.Interfaces.Core;
|
||||
|
||||
namespace Strata.SqlTools.SqlBreakdown.Expressions.Literals;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a numeric literal value in a SQL expression.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// NumberLiteralExpression stores decimal values and generates unquoted numeric literals
|
||||
/// in SQL. Automatically created through implicit conversion from decimal values or via
|
||||
/// <see cref="Expression.FromObject"/> for various numeric types.
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// <code language="csharp">
|
||||
/// // Explicit creation
|
||||
/// var literal = new NumberLiteralExpression(42.5m);
|
||||
///
|
||||
/// // Implicit conversion
|
||||
/// Expression price = 99.99m;
|
||||
///
|
||||
/// // In expressions
|
||||
/// var column = new GenericColumnExpression("Price", "Products");
|
||||
/// var discounted = column * 0.8m; // 0.8m becomes NumberLiteralExpression
|
||||
/// // Generates: Price * 0.8
|
||||
/// </code>
|
||||
/// </example>
|
||||
public class NumberLiteralExpression : LiteralValueExpression<decimal>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="NumberLiteralExpression"/> class.
|
||||
/// </summary>
|
||||
/// <param name="value">The numeric value.</param>
|
||||
/// <remarks>
|
||||
/// Example:
|
||||
/// <code language="csharp">
|
||||
/// var literal = new NumberLiteralExpression(42.5m);
|
||||
/// // Or use implicit conversion: Expression price = 99.99m;
|
||||
/// </code>
|
||||
/// </remarks>
|
||||
public NumberLiteralExpression(decimal value) : base(value)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public override T Accept<T>(IVisitor<T> visitor)
|
||||
{
|
||||
return visitor.VisitNumberLiteralExpression(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implicitly converts a <see cref="NumberLiteralExpression"/> to its decimal value.
|
||||
/// </summary>
|
||||
/// <param name="numberExp">The number literal expression.</param>
|
||||
public static implicit operator decimal(NumberLiteralExpression numberExp) => numberExp.Value;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
using Strata.SqlTools.SqlBreakdown.Interfaces.Core;
|
||||
|
||||
namespace Strata.SqlTools.SqlBreakdown.Expressions.Literals;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a parameter placeholder in a SQL expression.
|
||||
/// Supports named parameters (@param, :param) and positional parameters ($1, $2).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// ParameterLiteralExpression stores parameter placeholders used in parameterized SQL queries.
|
||||
/// Common formats include:
|
||||
/// - PostgreSQL positional: $1, $2, $3
|
||||
/// - Named (SQL Server style): @userId, @amount
|
||||
/// - Named (Oracle/PostgreSQL style): :userId, :amount
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// <code language="csharp">
|
||||
/// // Positional parameter
|
||||
/// var param1 = new ParameterLiteralExpression("$1");
|
||||
///
|
||||
/// // Named parameter (SQL Server style)
|
||||
/// var userId = new ParameterLiteralExpression("@userId");
|
||||
///
|
||||
/// // Named parameter (Oracle/PostgreSQL style)
|
||||
/// var userIdColon = new ParameterLiteralExpression(":userId");
|
||||
/// </code>
|
||||
/// </example>
|
||||
public class ParameterLiteralExpression : LiteralValueExpression<string>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ParameterLiteralExpression"/> class.
|
||||
/// </summary>
|
||||
/// <param name="parameterName">The parameter name including its prefix (e.g., "$1", "@userId", ":userId").</param>
|
||||
public ParameterLiteralExpression(string parameterName) : base(parameterName)
|
||||
{
|
||||
}
|
||||
|
||||
public override T Accept<T>(IVisitor<T> visitor)
|
||||
{
|
||||
return visitor.VisitParameterLiteralExpression(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
using Strata.SqlTools.SqlBreakdown.Interfaces.Core;
|
||||
|
||||
namespace Strata.SqlTools.SqlBreakdown.Expressions.Literals;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a string literal value in a SQL expression.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// StringLiteralExpression stores string values and generates properly quoted and
|
||||
/// escaped string literals in SQL. Automatically created through implicit conversion
|
||||
/// from string values or via <see cref="Expression.FromObject"/>.
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// <code language="csharp">
|
||||
/// // Explicit creation
|
||||
/// var literal = new StringLiteralExpression("Electronics");
|
||||
///
|
||||
/// // Implicit conversion
|
||||
/// Expression category = "Electronics";
|
||||
///
|
||||
/// // In expressions
|
||||
/// var categoryCol = new GenericColumnExpression("Category", "Products");
|
||||
/// var filter = categoryCol == "Electronics";
|
||||
/// // Generates: Category = 'Electronics'
|
||||
/// </code>
|
||||
/// </example>
|
||||
public class StringLiteralExpression : LiteralValueExpression<string>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="StringLiteralExpression"/> class.
|
||||
/// </summary>
|
||||
/// <param name="value">The string value.</param>
|
||||
/// <remarks>
|
||||
/// Example:
|
||||
/// <code language="csharp">
|
||||
/// var literal = new StringLiteralExpression("Electronics");
|
||||
/// // Or use implicit conversion: Expression category = "Electronics";
|
||||
/// </code>
|
||||
/// </remarks>
|
||||
public StringLiteralExpression(string value) : base(value)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public override T Accept<T>(IVisitor<T> visitor)
|
||||
{
|
||||
return visitor.VisitStringLiteralExpression(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
using Strata.SqlTools.SqlBreakdown.Interfaces.Core;
|
||||
|
||||
namespace Strata.SqlTools.SqlBreakdown.Expressions.Literals;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a symbolic operator or keyword in a SQL expression.
|
||||
/// Used for database-specific operators that don't have corresponding C# operator overloads.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// SymbolLiteralExpression stores SQL operators and symbols such as PostgreSQL operators (arrow, concat, range, bitwise shift),
|
||||
/// comparison operators (equal, not-equal, less-or-equal, greater-or-equal), and SQL keywords (AS, WHEN, THEN, ELSE, etc)
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// <code language="csharp">
|
||||
/// // PostgreSQL hstore operator
|
||||
/// var arrow = new SymbolLiteralExpression("=>");
|
||||
///
|
||||
/// // Concatenation operator
|
||||
/// var concat = new SymbolLiteralExpression("||");
|
||||
///
|
||||
/// // Range operator
|
||||
/// var range = new SymbolLiteralExpression("..");
|
||||
/// </code>
|
||||
/// </example>
|
||||
public class SymbolLiteralExpression : LiteralValueExpression<string>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SymbolLiteralExpression"/> class.
|
||||
/// </summary>
|
||||
/// <param name="symbol">The symbolic operator or keyword.</param>
|
||||
public SymbolLiteralExpression(string symbol) : base(symbol)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Accepts a visitor to process this expression using the visitor pattern.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The return type of the visitor.</typeparam>
|
||||
/// <param name="visitor">The visitor that will process this expression.</param>
|
||||
/// <returns>The result of the visitor's processing of this expression.</returns>
|
||||
public override T Accept<T>(IVisitor<T> visitor)
|
||||
{
|
||||
return visitor.VisitSymbolLiteralExpression(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
using Strata.SqlTools.SqlBreakdown.Interfaces.Core;
|
||||
|
||||
namespace Strata.SqlTools.SqlBreakdown.Expressions;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a SQL parameter placeholder in a query expression. Parameters are used
|
||||
/// for parameterized queries to prevent SQL injection and improve query plan caching.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// ParameterExpression creates a placeholder that will be replaced with an actual
|
||||
/// value at query execution time. The parameter name should match the key used when
|
||||
/// adding parameter values to the query's parameter collection. The parameter prefix
|
||||
/// (@ or :) is typically managed by the visitor implementation and does not need to
|
||||
/// be included in the parameter name.
|
||||
/// <para>Parameter syntax by SQL dialect:</para>
|
||||
/// <list type="bullet">
|
||||
/// <item><description>SQL Server/T-SQL: @ParameterName</description></item>
|
||||
/// <item><description>Snowflake: :ParameterName</description></item>
|
||||
/// <item><description>Oracle: :ParameterName</description></item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// <code language="csharp">
|
||||
/// // Create parameter expressions
|
||||
/// var userIdParam = new ParameterExpression("UserId");
|
||||
/// var statusParam = new ParameterExpression("Status");
|
||||
///
|
||||
/// // Use in query building
|
||||
/// var query = new QueryBreakdown();
|
||||
/// query.FromClause.Clause = "Users";
|
||||
///
|
||||
/// var userId = new GenericColumnExpression("UserId", "Users");
|
||||
/// var status = new GenericColumnExpression("Status", "Users");
|
||||
///
|
||||
/// query.AddWhereExpression(userId == userIdParam);
|
||||
/// query.AddWhereExpression(status == statusParam, null, "AND");
|
||||
///
|
||||
/// // Add parameter values
|
||||
/// query.AddParameter("UserId", 12345);
|
||||
/// query.AddParameter("Status", "Active");
|
||||
///
|
||||
/// // Generates:
|
||||
/// // SELECT * FROM Users WHERE UserId = @UserId AND Status = @Status
|
||||
/// // With parameters: @UserId = 12345, @Status = 'Active'
|
||||
/// </code>
|
||||
/// </example>
|
||||
public class ParameterExpression : Expression
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the name of the parameter.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The parameter name without prefix. The prefix (@ or :) is added by the SQL visitor
|
||||
/// based on the target SQL dialect.
|
||||
/// </value>
|
||||
public string ParameterName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ParameterExpression"/> class.
|
||||
/// </summary>
|
||||
/// <param name="parameterName">
|
||||
/// The name of the parameter. Should not include the parameter prefix (@ or :).
|
||||
/// The prefix will be added automatically based on the SQL dialect.
|
||||
/// </param>
|
||||
/// <remarks>
|
||||
/// Example:
|
||||
/// <code language="csharp">
|
||||
/// // Correct usage - no prefix
|
||||
/// var param1 = new ParameterExpression("EmployeeId");
|
||||
/// // Will generate @EmployeeId for SQL Server or :EmployeeId for Snowflake
|
||||
/// </code>
|
||||
/// </remarks>
|
||||
public ParameterExpression(string parameterName)
|
||||
{
|
||||
ParameterName = parameterName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Accepts a visitor for the visitor pattern, allowing different SQL
|
||||
/// generation strategies.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The return type of the visitor.</typeparam>
|
||||
/// <param name="visitor">
|
||||
/// The visitor instance that will process this expression.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// The result from the visitor's processing of this parameter expression,
|
||||
/// typically the parameter placeholder string.
|
||||
/// </returns>
|
||||
public override T Accept<T>(IVisitor<T> visitor)
|
||||
{
|
||||
return visitor.VisitParameterExpression(this);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
using Strata.SqlTools.SqlBreakdown.Classes;
|
||||
using Strata.SqlTools.SqlBreakdown.Interfaces.Core;
|
||||
|
||||
namespace Strata.SqlTools.SqlBreakdown.Expressions;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a column expression for pre-registered tables with a unique column
|
||||
/// identifier. This class is used when working with a predefined metadata system
|
||||
/// where columns have registered IDs.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// RegisteredTableColumnExpression is typically used in enterprise systems where
|
||||
/// table and column metadata is registered in a central repository. Each column has
|
||||
/// a unique integer identifier that can be used to retrieve additional metadata such
|
||||
/// as data type, display name, or business logic. Unlike
|
||||
/// <see cref="GenericColumnExpression"/>, this class requires both a column ID and
|
||||
/// a <see cref="RegisteredTableSource"/>, providing stronger type safety and
|
||||
/// metadata integration.
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// <code language="csharp">
|
||||
/// // Create a registered table source
|
||||
/// var tableSource = new RegisteredTableSource(
|
||||
/// tableId: 101,
|
||||
/// schemaName: "dbo",
|
||||
/// tableName: "Employees",
|
||||
/// alias: "EMP"
|
||||
/// );
|
||||
///
|
||||
/// // Create column expressions with IDs
|
||||
/// var employeeId = new RegisteredTableColumnExpression(1, "EmployeeID", tableSource);
|
||||
/// var firstName = new RegisteredTableColumnExpression(2, "FirstName", tableSource);
|
||||
/// var salary = new RegisteredTableColumnExpression(3, "Salary", tableSource);
|
||||
///
|
||||
/// // Use in query building
|
||||
/// var query = new QueryBreakdown();
|
||||
/// query.AddSelectExpression(employeeId);
|
||||
/// query.AddSelectExpression(firstName);
|
||||
/// query.AddWhereExpression(salary > 50000);
|
||||
/// // Generates: SELECT EMP.EmployeeID, EMP.FirstName FROM dbo.Employees EMP WHERE EMP.Salary > 50000
|
||||
/// </code>
|
||||
/// </example>
|
||||
public class RegisteredTableColumnExpression : ColumnExpression<RegisteredTableSource>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the unique identifier for this column in the registered metadata system.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// A positive integer uniquely identifying the column. Must be greater than 0.
|
||||
/// </value>
|
||||
public int ColumnId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RegisteredTableColumnExpression"/> class.
|
||||
/// </summary>
|
||||
/// <param name="columnId">
|
||||
/// The unique identifier for the column. Must be greater than 0.
|
||||
/// </param>
|
||||
/// <param name="columnName">
|
||||
/// The name of the column as it appears in the database schema.
|
||||
/// </param>
|
||||
/// <param name="tableSource">
|
||||
/// The registered table source containing metadata about the parent table.
|
||||
/// </param>
|
||||
/// <exception cref="ArgumentException">
|
||||
/// Thrown when <paramref name="columnId"/> is less than or equal to 0.
|
||||
/// </exception>
|
||||
/// <remarks>
|
||||
/// Example:
|
||||
/// <code language="csharp">
|
||||
/// var table = new RegisteredTableSource(100, "sales", "Orders", "ORD");
|
||||
/// var column = new RegisteredTableColumnExpression(25, "OrderDate", table);
|
||||
/// // Column ID 25 can be used to look up additional metadata about this column
|
||||
/// </code>
|
||||
/// </remarks>
|
||||
public RegisteredTableColumnExpression(int columnId, string columnName, RegisteredTableSource tableSource) : base(columnName, tableSource)
|
||||
{
|
||||
if (columnId <= 0)
|
||||
{
|
||||
throw new ArgumentException($"{nameof(columnId)} must be greater than 0", nameof(columnId));
|
||||
}
|
||||
|
||||
ColumnId = columnId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Strata.SqlTools.SqlBreakdown.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Provides an extended StringBuilder with additional formatting and manipulation capabilities.
|
||||
/// </summary>
|
||||
public class StringBuilderEx
|
||||
{
|
||||
private static readonly Regex AppendFormatExRegex = new Regex(
|
||||
@"\{(?<Index>.*?)(?<Comment>!.*?)?\}",
|
||||
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
private readonly StringBuilder _innerStringBuilder;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the inner StringBuilder instance.
|
||||
/// </summary>
|
||||
public StringBuilder StringBuilder => _innerStringBuilder;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="StringBuilderEx"/> class.
|
||||
/// </summary>
|
||||
public StringBuilderEx()
|
||||
{
|
||||
_innerStringBuilder = new StringBuilder();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="StringBuilderEx"/> class with the specified StringBuilder.
|
||||
/// </summary>
|
||||
/// <param name="stringBuilder">The StringBuilder to wrap.</param>
|
||||
public StringBuilderEx(StringBuilder stringBuilder)
|
||||
{
|
||||
_innerStringBuilder = stringBuilder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends a formatted string with support for commented placeholders (e.g., {0!comment}).
|
||||
/// Comments in placeholders are removed before formatting.
|
||||
/// </summary>
|
||||
/// <param name="format">The format string.</param>
|
||||
/// <param name="values">The values to format.</param>
|
||||
/// <returns>The current instance for method chaining.</returns>
|
||||
public StringBuilderEx AppendFormatEx(string format, params string[] values)
|
||||
{
|
||||
string formatWithoutComments = AppendFormatExRegex.Replace(format, "{${Index}}");
|
||||
StringBuilder.AppendFormat(FixString(formatWithoutComments), values);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends a formatted string to the StringBuilder.
|
||||
/// </summary>
|
||||
/// <param name="format">The format string.</param>
|
||||
/// <param name="values">The values to format.</param>
|
||||
/// <returns>The current instance for method chaining.</returns>
|
||||
public StringBuilderEx AppendFormat(string format, params string[] values)
|
||||
{
|
||||
StringBuilder.AppendFormat(FixString(format), values);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends a formatted string followed by a line terminator to the StringBuilder.
|
||||
/// </summary>
|
||||
/// <param name="format">The format string.</param>
|
||||
/// <param name="values">The values to format.</param>
|
||||
/// <returns>The current instance for method chaining.</returns>
|
||||
public StringBuilderEx AppendFormatLine(string format, params string[] values)
|
||||
{
|
||||
StringBuilder.AppendFormat(FixString(format) + Environment.NewLine, values);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fixes escape sequences in the string by replacing \t with tab and \n with newline.
|
||||
/// </summary>
|
||||
/// <param name="value">The string to fix.</param>
|
||||
/// <returns>The fixed string.</returns>
|
||||
private static string FixString(string value)
|
||||
{
|
||||
string output = value.Replace("\\t", "\t");
|
||||
output = output.Replace("\\n", Environment.NewLine);
|
||||
return output;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends a string to the StringBuilder.
|
||||
/// </summary>
|
||||
/// <param name="value">The string to append.</param>
|
||||
/// <returns>The current instance for method chaining.</returns>
|
||||
public StringBuilderEx Append(string value)
|
||||
{
|
||||
StringBuilder.Append(FixString(value));
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends a string followed by a line terminator to the StringBuilder.
|
||||
/// </summary>
|
||||
/// <param name="value">The string to append.</param>
|
||||
/// <returns>The current instance for method chaining.</returns>
|
||||
public StringBuilderEx AppendLine(string value)
|
||||
{
|
||||
StringBuilder.AppendLine(FixString(value));
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends a line terminator to the StringBuilder.
|
||||
/// </summary>
|
||||
/// <returns>The current instance for method chaining.</returns>
|
||||
public StringBuilderEx AppendLine()
|
||||
{
|
||||
StringBuilder.AppendLine();
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts the StringBuilder content to a string.
|
||||
/// </summary>
|
||||
/// <returns>The string representation of the StringBuilder content.</returns>
|
||||
public override string ToString()
|
||||
{
|
||||
return _innerStringBuilder.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the last character from the StringBuilder.
|
||||
/// </summary>
|
||||
public void Backspace()
|
||||
{
|
||||
if (_innerStringBuilder.Length > 0)
|
||||
{
|
||||
_innerStringBuilder.Remove(_innerStringBuilder.Length - 1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the specified suffix from the end of the StringBuilder if it exists.
|
||||
/// </summary>
|
||||
/// <param name="suffix">The suffix to remove.</param>
|
||||
/// <returns>The current instance for method chaining.</returns>
|
||||
public StringBuilderEx BackspaceIf(string suffix)
|
||||
{
|
||||
string fixedSuffix = FixString(suffix);
|
||||
|
||||
if (_innerStringBuilder.Length < fixedSuffix.Length)
|
||||
{
|
||||
return this;
|
||||
}
|
||||
|
||||
string currentContent = _innerStringBuilder.ToString();
|
||||
if (currentContent.EndsWith(fixedSuffix))
|
||||
{
|
||||
_innerStringBuilder.Remove(_innerStringBuilder.Length - fixedSuffix.Length, fixedSuffix.Length);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the length of the StringBuilder.
|
||||
/// </summary>
|
||||
/// <returns>The length of the StringBuilder content.</returns>
|
||||
public int Length()
|
||||
{
|
||||
return _innerStringBuilder.Length;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Strata.SqlTools.SqlBreakdown.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for string manipulation commonly used in SQL operations.
|
||||
/// </summary>
|
||||
public static class StringExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Trims the specified number of characters from the beginning and end of the string.
|
||||
/// For instance, if aTrimLength is 1 and the string is "(Test)", the return value would be "Test".
|
||||
/// </summary>
|
||||
/// <param name="aString">The string to trim.</param>
|
||||
/// <param name="aTrimLength">The number of characters to trim from each end.</param>
|
||||
/// <returns>The trimmed string.</returns>
|
||||
public static string TakeInner(this string aString, int aTrimLength) =>
|
||||
aString.Substring(aTrimLength, aString.Length - aTrimLength - 1);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the string value (identity function).
|
||||
/// </summary>
|
||||
/// <param name="aString">The string.</param>
|
||||
/// <returns>The same string.</returns>
|
||||
public static string Value(this string aString) => aString;
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the string is null.
|
||||
/// </summary>
|
||||
/// <param name="aString">The string to check.</param>
|
||||
/// <returns>true if the string is null; otherwise, false.</returns>
|
||||
public static bool IsNull(this string aString) => aString is null;
|
||||
|
||||
/// <summary>
|
||||
/// Returns the leftmost characters from a string.
|
||||
/// </summary>
|
||||
/// <param name="aString">The string.</param>
|
||||
/// <param name="aCharCount">The number of characters to return.</param>
|
||||
/// <returns>The leftmost characters.</returns>
|
||||
/// <exception cref="ArgumentException">Thrown when aCharCount is negative.</exception>
|
||||
public static string Left(this string aString, int aCharCount)
|
||||
{
|
||||
if (aCharCount >= aString.Length)
|
||||
{
|
||||
return aString;
|
||||
}
|
||||
|
||||
if (aCharCount < 0)
|
||||
{
|
||||
throw new ArgumentException();
|
||||
}
|
||||
|
||||
if (aCharCount == 0)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return aString.Substring(0, aCharCount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the rightmost characters from a string.
|
||||
/// </summary>
|
||||
/// <param name="aString">The string.</param>
|
||||
/// <param name="aCharCount">The number of characters to return.</param>
|
||||
/// <returns>The rightmost characters.</returns>
|
||||
/// <exception cref="ArgumentException">Thrown when aCharCount is negative.</exception>
|
||||
public static string Right(this string aString, int aCharCount)
|
||||
{
|
||||
if (aCharCount >= aString.Length)
|
||||
{
|
||||
return aString;
|
||||
}
|
||||
|
||||
if (aCharCount < 0)
|
||||
{
|
||||
throw new ArgumentException();
|
||||
}
|
||||
|
||||
if (aCharCount == 0)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return aString.Substring(aString.Length - aCharCount, aCharCount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the string is null or empty.
|
||||
/// </summary>
|
||||
/// <param name="aString">The string to check.</param>
|
||||
/// <returns>true if the string is null or empty; otherwise, false.</returns>
|
||||
public static bool IsNullOrEmpty(this string aString) => string.IsNullOrEmpty(aString);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the string has a value (is not null or empty).
|
||||
/// </summary>
|
||||
/// <param name="aString">The string to check.</param>
|
||||
/// <returns>true if the string has a value; otherwise, false.</returns>
|
||||
public static bool HasValue(this string aString) => !string.IsNullOrEmpty(aString);
|
||||
|
||||
/// <summary>
|
||||
/// Reverses the characters in a string.
|
||||
/// </summary>
|
||||
/// <param name="aString">The string to reverse.</param>
|
||||
/// <returns>The reversed string.</returns>
|
||||
public static string Reverse(this string aString)
|
||||
{
|
||||
var arr = aString.ToArray();
|
||||
Array.Reverse(arr);
|
||||
return new string(arr);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the string is a valid GUID format.
|
||||
/// </summary>
|
||||
/// <param name="aString">The string to check.</param>
|
||||
/// <returns>true if the string is a valid GUID; otherwise, false.</returns>
|
||||
public static bool IsGUID(this string aString)
|
||||
{
|
||||
const string pattern = "^[0-9a-zA-Z]{8}-[0-9a-zA-Z]{4}-[0-9a-zA-Z]{4}-[0-9a-zA-Z]{4}-[0-9a-zA-Z]{12}$";
|
||||
var match = Regex.Match(aString, pattern);
|
||||
return match.Success;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a string to a GUID. If it cannot be converted to a GUID, returns the default value.
|
||||
/// </summary>
|
||||
/// <param name="aString">The string to convert.</param>
|
||||
/// <param name="aDefault">The default GUID to return if conversion fails.</param>
|
||||
/// <returns>The GUID or the default value.</returns>
|
||||
public static Guid ToGUID(this string aString, Guid aDefault) =>
|
||||
aString.IsGUID() ? new Guid(aString) : aDefault;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a string that is valid to be used as a string value in SQL by escaping single quotes.
|
||||
/// </summary>
|
||||
/// <param name="aString">The string to make SQL-safe.</param>
|
||||
/// <returns>The SQL-safe string with single quotes escaped.</returns>
|
||||
public static string GetSQLSafeString(this string aString) => aString.Replace("'", "''");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
namespace Strata.SqlTools.SqlBreakdown.Interfaces.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an object that can generate SQL expressions with parameters.
|
||||
/// </summary>
|
||||
public interface ISql
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the dictionary of parameter names and their values.
|
||||
/// </summary>
|
||||
Dictionary<string, object> ParameterValues { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the SQL expression.
|
||||
/// </summary>
|
||||
string SqlExpression { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets all debug information.
|
||||
/// </summary>
|
||||
/// <returns>A string containing debug information.</returns>
|
||||
string GetAllDebugInfo();
|
||||
|
||||
/// <summary>
|
||||
/// Prints all debug information to the debug output.
|
||||
/// </summary>
|
||||
void PrintAllDebugInfo();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a SQL object that supports appending additional SQL text.
|
||||
/// </summary>
|
||||
public interface ISqlAppendable : ISql
|
||||
{
|
||||
/// <summary>
|
||||
/// Appends a string to the SQL expression.
|
||||
/// </summary>
|
||||
/// <param name="value">The string to append.</param>
|
||||
void Append(string value);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Strata.SqlTools.SqlBreakdown.Interfaces.Core;
|
||||
|
||||
public interface IVisitable
|
||||
{
|
||||
T Accept<T>(IVisitor<T> visitor);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
using Strata.SqlTools.SqlBreakdown.Classes;
|
||||
using Strata.SqlTools.SqlBreakdown.Expressions;
|
||||
using Strata.SqlTools.SqlBreakdown.Expressions.Arithmetic;
|
||||
using Strata.SqlTools.SqlBreakdown.Expressions.Conditional;
|
||||
using Strata.SqlTools.SqlBreakdown.Expressions.Conditional.Comparisons;
|
||||
using Strata.SqlTools.SqlBreakdown.Expressions.Conditional.Logical;
|
||||
using Strata.SqlTools.SqlBreakdown.Expressions.Functions;
|
||||
using Strata.SqlTools.SqlBreakdown.Expressions.Functions.Aggregate;
|
||||
using Strata.SqlTools.SqlBreakdown.Expressions.Functions.Conditional;
|
||||
using Strata.SqlTools.SqlBreakdown.Expressions.Literals;
|
||||
|
||||
namespace Strata.SqlTools.SqlBreakdown.Interfaces.Core;
|
||||
|
||||
public interface IVisitor<T>
|
||||
{
|
||||
// Column
|
||||
T VisitTableSource(TableSource tableSource);
|
||||
T VisitColumnExpression<TSource>(ColumnExpression<TSource> column) where TSource : SelectSource;
|
||||
T VisitSelectClauseColumn(SelectClauseColumn selectClauseColumn);
|
||||
|
||||
T VisitParameterExpression(ParameterExpression parameterExpression);
|
||||
|
||||
// Literal
|
||||
T VisitNumberLiteralExpression(NumberLiteralExpression numberLiteral);
|
||||
T VisitStringLiteralExpression(StringLiteralExpression stringLiteral);
|
||||
T VisitDateTimeLiteralExpression(DateTimeLiteralExpression dateTimeLiteral);
|
||||
T VisitNullLiteralExpression(NullLiteralExpression nullLiteral);
|
||||
T VisitBooleanLiteralExpression(BooleanLiteralExpression booleanLiteral);
|
||||
T VisitParameterLiteralExpression(ParameterLiteralExpression parameterLiteral);
|
||||
T VisitSymbolLiteralExpression(SymbolLiteralExpression symbolLiteral);
|
||||
|
||||
// Conditional/Boolean
|
||||
T VisitComparisonExpression(ComparisonOperatorExpression comparison);
|
||||
T VisitAndExpression(AndExpression logical);
|
||||
T VisitOrExpression(OrExpression logical);
|
||||
T VisitNotExpression(NotExpression logical);
|
||||
T VisitInExpression(InExpression inExpression);
|
||||
T VisitNotInExpression(NotInExpression inExpression);
|
||||
T VisitLikeExpression(LikeExpression likeExpression);
|
||||
T VisitNotLikeExpression(NotLikeExpression notLikeExpression);
|
||||
T VisitBetweenExpression(BetweenExpression betweenExpression);
|
||||
|
||||
// Function
|
||||
T VisitFunctionExpression(FunctionExpression function);
|
||||
T VisitAggregateFunctionExpression(AggregateFunctionExpression aggregateFunction);
|
||||
T VisitCaseFunctionExpression(CaseExpression caseFunction);
|
||||
|
||||
// Arithmetic
|
||||
T VisitArithmeticExpression(ArithmeticExpression arithmeticExpression);
|
||||
|
||||
// RuleEngine
|
||||
T VisitInputPropertyExpression(InputPropertyExpression inputPropertyExpression);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
using System.Collections;
|
||||
|
||||
namespace Strata.SqlTools.SqlBreakdown.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a SQL statement breakdown that can be composed and rendered as SQL.
|
||||
/// </summary>
|
||||
public interface ISqlBreakdown : ICloneable
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the setup clauses to execute before the main statement.
|
||||
/// </summary>
|
||||
List<string> SetupClauses { get; set; }
|
||||
|
||||
/// <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>
|
||||
string? RawSql { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether setup clauses are being used.
|
||||
/// </summary>
|
||||
bool IsUsingSetupClause { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the finish clauses to execute after the main statement.
|
||||
/// </summary>
|
||||
ArrayList FinishClauses { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether finish clauses are being used.
|
||||
/// </summary>
|
||||
bool IsUsingFinishClause { get; }
|
||||
|
||||
/// <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>
|
||||
string GetSql(bool includeSetupFinish = true);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
using Strata.SqlTools.SqlBreakdown.Expressions;
|
||||
|
||||
namespace Strata.SqlTools.SqlBreakdown.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the contract for parsing SQL statements into Expression objects.
|
||||
/// </summary>
|
||||
public interface IStatementExpressionParser
|
||||
{
|
||||
/// <summary>
|
||||
/// Parses a SQL statement into an Expression object.
|
||||
/// </summary>
|
||||
/// <param name="sqlStatement">The SQL statement to parse.</param>
|
||||
/// <returns>The parsed Expression object.</returns>
|
||||
/// <exception cref="FormatException">Thrown when the SQL statement cannot be parsed.</exception>
|
||||
Expression Parse(string sqlStatement);
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to parse a SQL statement into an Expression object.
|
||||
/// </summary>
|
||||
/// <param name="sqlStatement">The SQL statement to parse.</param>
|
||||
/// <param name="result">When this method returns, contains the parsed Expression if successful, or null if parsing failed.</param>
|
||||
/// <returns>true if the SQL was successfully parsed; otherwise, false.</returns>
|
||||
bool TryParse(string sqlStatement, out Expression result);
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to parse a SQL statement into an Expression object.
|
||||
/// </summary>
|
||||
/// <param name="sqlStatement">The SQL statement to parse.</param>
|
||||
/// <param name="result">When this method returns, contains the parsed Expression if successful, or null if parsing failed.</param>
|
||||
/// <param name="errorMessage">When this method returns false, contains a message describing why parsing failed.</param>
|
||||
/// <returns>true if the SQL was successfully parsed; otherwise, false.</returns>
|
||||
bool TryParse(string sqlStatement, out Expression result, out string errorMessage);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
using System.Collections;
|
||||
using Strata.SqlTools.SqlBreakdown.Classes;
|
||||
|
||||
namespace Strata.SqlTools.SqlBreakdown.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for SQL statement parsers.
|
||||
/// Abstracts dialect-specific parsing behavior so that QueryBreakdown can work with different SQL dialects.
|
||||
/// </summary>
|
||||
public interface IStatementParser
|
||||
{
|
||||
/// <summary>
|
||||
/// Normalizes SQL while preserving comments.
|
||||
/// </summary>
|
||||
/// <param name="sql">The SQL statement to normalize.</param>
|
||||
/// <returns>The normalized SQL statement.</returns>
|
||||
string NormalizeSqlPreservingComments(string sql);
|
||||
|
||||
/// <summary>
|
||||
/// Extracts SQL comments from a clause.
|
||||
/// </summary>
|
||||
/// <param name="clause">The SQL clause to extract comments from.</param>
|
||||
/// <param name="comments">When this method returns, contains the extracted comments.</param>
|
||||
/// <returns>The clause without comments.</returns>
|
||||
string ExtractSqlComments(string clause, out List<string> comments);
|
||||
|
||||
/// <summary>
|
||||
/// Extracts setup clauses (e.g., CREATE TABLE, variable declarations) from SQL.
|
||||
/// </summary>
|
||||
/// <param name="sql">The SQL statement to process.</param>
|
||||
/// <param name="setupClauses">When this method returns, contains the extracted setup clauses.</param>
|
||||
/// <returns>The SQL without setup clauses.</returns>
|
||||
string ExtractSetupClauses(string sql, List<string> setupClauses);
|
||||
|
||||
/// <summary>
|
||||
/// Extracts finish clauses (e.g., cleanup statements) from SQL.
|
||||
/// </summary>
|
||||
/// <param name="sql">The SQL statement to process.</param>
|
||||
/// <param name="finishClauses">When this method returns, contains the extracted finish clauses.</param>
|
||||
/// <returns>The SQL without finish clauses.</returns>
|
||||
string ExtractFinishClauses(string sql, ArrayList finishClauses);
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to parse a WITH clause from SQL.
|
||||
/// </summary>
|
||||
/// <param name="sql">The SQL statement to parse.</param>
|
||||
/// <param name="withClause">When this method returns, contains the WITH clause if found.</param>
|
||||
/// <param name="mainQuery">When this method returns, contains the remaining SQL after WITH clause.</param>
|
||||
/// <returns>true if a WITH clause was found and parsed; otherwise, false.</returns>
|
||||
bool TryParseWithClause(string sql, out string? withClause, out string mainQuery);
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to parse a SELECT statement.
|
||||
/// </summary>
|
||||
/// <param name="sql">The SQL statement to parse.</param>
|
||||
/// <param name="clauses">When this method returns, contains the parsed clauses if successful.</param>
|
||||
/// <param name="errorMessage">When this method returns false, contains the error message.</param>
|
||||
/// <returns>true if the SELECT statement was successfully parsed; otherwise, false.</returns>
|
||||
bool TryParseSelectStatement(string sql, out SqlClauses? clauses, out string errorMessage);
|
||||
|
||||
/// <summary>
|
||||
/// Extracts parameters from SQL.
|
||||
/// </summary>
|
||||
/// <param name="parameters">Dictionary to populate with extracted parameters.</param>
|
||||
/// <param name="sql">The SQL to extract parameters from.</param>
|
||||
void ExtractParameters(Dictionary<string, object> parameters, string sql);
|
||||
|
||||
/// <summary>
|
||||
/// Removes SQL comments from a statement.
|
||||
/// </summary>
|
||||
/// <param name="sql">The SQL statement to process.</param>
|
||||
/// <returns>The SQL without comments.</returns>
|
||||
string RemoveSqlComments(string sql);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
using Strata.SqlTools.SqlBreakdown.Enums.SQL;
|
||||
|
||||
namespace Strata.SqlTools.SqlBreakdown.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for SQL statement tokenizers that read and parse SQL statements into tokens.
|
||||
/// </summary>
|
||||
public interface IStatementReader
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the current position in the SQL statement.
|
||||
/// </summary>
|
||||
int Position { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the length of the SQL statement.
|
||||
/// </summary>
|
||||
int Length { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current character being processed.
|
||||
/// </summary>
|
||||
char CurrentCharacter { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type of the current token.
|
||||
/// </summary>
|
||||
TokenType TokenType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the value of the current token.
|
||||
/// </summary>
|
||||
string TokenValue { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Reads the next token from the SQL statement.
|
||||
/// </summary>
|
||||
/// <returns>True if a token was read; false if the end of the statement was reached.</returns>
|
||||
bool Read();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
using System.Collections;
|
||||
using Strata.SqlTools.SqlBreakdown.Classes;
|
||||
|
||||
namespace Strata.SqlTools.SqlBreakdown.Interfaces.QueryEngine;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a SQL query broken down into its component parts (SELECT, FROM, WHERE, etc.).
|
||||
/// </summary>
|
||||
public interface IQueryBreakdown
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the setup clauses executed before the main query (e.g., temp table creation).
|
||||
/// </summary>
|
||||
List<string> SetupClauses { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the query uses setup clauses.
|
||||
/// </summary>
|
||||
bool IsUsingSetupClause { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the finish clauses executed after the main query (e.g., temp table cleanup).
|
||||
/// </summary>
|
||||
ArrayList FinishClauses { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the query uses finish clauses.
|
||||
/// </summary>
|
||||
bool IsUsingFinishClause { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the WITH clauses (Common Table Expressions).
|
||||
/// </summary>
|
||||
IReadOnlyList<IWithClause> WithClauses { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the query uses WITH clauses.
|
||||
/// </summary>
|
||||
bool IsUsingWithClause { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the SELECT clause of the query.
|
||||
/// </summary>
|
||||
ISqlExpressionClause SelectClause { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the FROM clause of the query.
|
||||
/// </summary>
|
||||
ISqlClause FromClause { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the query uses a FROM clause.
|
||||
/// </summary>
|
||||
bool IsUsingFromClause { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the query uses a GROUP BY clause.
|
||||
/// </summary>
|
||||
bool IsUsingGroupByClause { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the GROUP BY clause of the query.
|
||||
/// </summary>
|
||||
ISqlExpressionClause GroupByClause { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the query uses a WHERE clause.
|
||||
/// </summary>
|
||||
bool IsUsingWhereClause { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the WHERE clause of the query.
|
||||
/// </summary>
|
||||
ISqlExpressionClause WhereClause { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Adds a condition to the WHERE clause.
|
||||
/// </summary>
|
||||
/// <param name="sql">The SQL condition to add.</param>
|
||||
void AddWhereClause(string sql);
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the ORDER BY clause of the query.
|
||||
/// </summary>
|
||||
ISqlExpressionClause OrderByClause { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the query uses an ORDER BY clause.
|
||||
/// </summary>
|
||||
bool IsUsingOrderByClause { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the HAVING clause of the query.
|
||||
/// </summary>
|
||||
ISqlExpressionClause HavingClause { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the query uses a HAVING clause.
|
||||
/// </summary>
|
||||
bool IsUsingHavingClause { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Adds a parameter to the query.
|
||||
/// </summary>
|
||||
/// <param name="parameterName">The parameter name.</param>
|
||||
/// <param name="value">The parameter value.</param>
|
||||
void AddParameter(string parameterName, object value);
|
||||
|
||||
/// <summary>
|
||||
/// Adds multiple parameters to the query.
|
||||
/// </summary>
|
||||
/// <param name="queryParams">The parameters to add.</param>
|
||||
void AddParameter(IEnumerable<IQueryParam> queryParams);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of parameters for this query.
|
||||
/// </summary>
|
||||
IEnumerable<IQueryParam> ParameterList { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the complete SQL statement for this query.
|
||||
/// </summary>
|
||||
/// <param name="includeSetupFinish">Whether to include setup and finish clauses.</param>
|
||||
/// <returns>The SQL statement.</returns>
|
||||
string GetSQL(bool includeSetupFinish = true);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the complete SQL statement for this query (preferred method name following naming conventions).
|
||||
/// </summary>
|
||||
/// <param name="includeSetupFinish">Whether to include setup and finish clauses.</param>
|
||||
/// <returns>The SQL statement.</returns>
|
||||
string GetSql(bool includeSetupFinish = true);
|
||||
|
||||
/// <summary>
|
||||
/// Merges another query breakdown into this one.
|
||||
/// </summary>
|
||||
/// <param name="query">The query to merge.</param>
|
||||
void MergeWith(IQueryBreakdown query);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the SQL clauses from this query breakdown.
|
||||
/// </summary>
|
||||
/// <returns>A SqlClauses object containing the current clause properties.</returns>
|
||||
SqlClauses GetClauses();
|
||||
|
||||
/// <summary>
|
||||
/// Applies SQL clauses from a SqlClauses object to this query breakdown.
|
||||
/// Only non-null clauses are applied.
|
||||
/// </summary>
|
||||
/// <param name="clauses">The SQL clauses to apply.</param>
|
||||
void ApplyClauses(SqlClauses? clauses);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a named parameter and its value for a SQL query.
|
||||
/// </summary>
|
||||
public interface IQueryParam
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the parameter name.
|
||||
/// </summary>
|
||||
string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the parameter value.
|
||||
/// </summary>
|
||||
object Value { get; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>latest</LangVersion>
|
||||
|
||||
<!-- NuGet Package Metadata -->
|
||||
<PackageId>Strata.SqlTools</PackageId>
|
||||
<Version>1.0.0</Version>
|
||||
<Authors>Strata Decision Technology</Authors>
|
||||
<Company>Strata Decision Technology</Company>
|
||||
<Product>Strata SQL Utilities - Core</Product>
|
||||
<Description>Core library for SQL utilities providing common interfaces, base classes, expression trees, and utilities for SQL query manipulation. Use Strata.SqlTools.SqlServer or Strata.SqlTools.Snowflake for dialect-specific implementations.</Description>
|
||||
<PackageTags>sql;query-builder;sql-parser;database;core;abstractions</PackageTags>
|
||||
<PackageProjectUrl>https://github.com/stratadecision/sql-builder</PackageProjectUrl>
|
||||
<RepositoryUrl>https://github.com/stratadecision/sql-builder</RepositoryUrl>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<PackageReleaseNotes>Initial release with core SQL utilities, interfaces, and base classes for building SQL dialect-specific implementations.</PackageReleaseNotes>
|
||||
<Copyright>Copyright © Strata Decision Technology 2024-2026</Copyright>
|
||||
|
||||
<!-- Build Configuration -->
|
||||
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
|
||||
<IncludeSymbols>true</IncludeSymbols>
|
||||
<SymbolPackageFormat>symbols.nupkg</SymbolPackageFormat>
|
||||
<EmbedUntrackedSources>true</EmbedUntrackedSources>
|
||||
<ContinuousIntegrationBuild Condition="'$(CI)' == 'true'">true</ContinuousIntegrationBuild>
|
||||
|
||||
<!-- Code Analysis -->
|
||||
<EnableNETAnalyzers>true</EnableNETAnalyzers>
|
||||
<AnalysisLevel>latest</AnalysisLevel>
|
||||
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
|
||||
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="..\..\README.md" Pack="true" PackagePath="\" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Remove="Strata.SqlTools\**" />
|
||||
<EmbeddedResource Remove="Strata.SqlTools\**" />
|
||||
<None Remove="Strata.SqlTools\**" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,495 @@
|
||||
using System.Collections;
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
|
||||
namespace Strata.SqlTools.SqlBreakdown.Utilities;
|
||||
|
||||
/// <summary>
|
||||
/// Provides utility methods for working with arrays, lists, and CSV conversions.
|
||||
/// </summary>
|
||||
public static class ArrayUtils
|
||||
{
|
||||
private static readonly char ENCODED_LIST_DELIMITER = ',';
|
||||
private static readonly char ILLEGAL_CHARACTER = (char)8;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the type of quotes to use when converting lists to strings.
|
||||
/// </summary>
|
||||
public enum QuoteType
|
||||
{
|
||||
/// <summary>
|
||||
/// No quotes.
|
||||
/// </summary>
|
||||
None,
|
||||
|
||||
/// <summary>
|
||||
/// Single quotes.
|
||||
/// </summary>
|
||||
Single,
|
||||
|
||||
/// <summary>
|
||||
/// Double quotes.
|
||||
/// </summary>
|
||||
Double
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns an array of the specified type.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of array elements.</typeparam>
|
||||
/// <param name="values">The values to include in the array.</param>
|
||||
/// <returns>An array containing the specified values.</returns>
|
||||
public static T[] NewArray<T>(params T[] values)
|
||||
{
|
||||
return values;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a comma-separated value string to a GUID array.
|
||||
/// </summary>
|
||||
/// <param name="csv">The comma-separated GUID values.</param>
|
||||
/// <returns>An array of GUIDs parsed from the CSV string.</returns>
|
||||
public static Guid[] GetGuidArrayFromCsv(string csv)
|
||||
{
|
||||
string[] sArray = csv.Split(',');
|
||||
var gList = new List<Guid>();
|
||||
|
||||
foreach (string s in sArray)
|
||||
{
|
||||
gList.Add(GuidUtils.GetGuid(s));
|
||||
}
|
||||
|
||||
return gList.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a comma-separated value string to an ArrayList.
|
||||
/// </summary>
|
||||
/// <param name="csv">The comma-separated values.</param>
|
||||
/// <returns>An ArrayList containing the parsed values.</returns>
|
||||
public static ArrayList GetArrayListFromCsv(string csv)
|
||||
{
|
||||
try
|
||||
{
|
||||
var list = new ArrayList();
|
||||
string temp = csv;
|
||||
|
||||
while (temp.Length > 0)
|
||||
{
|
||||
string item;
|
||||
int commaIndex = temp.IndexOf(',');
|
||||
|
||||
if (commaIndex > 0)
|
||||
{
|
||||
item = temp.Substring(0, commaIndex);
|
||||
temp = temp.Substring(commaIndex + 1).TrimStart();
|
||||
}
|
||||
else
|
||||
{
|
||||
item = temp;
|
||||
temp = string.Empty;
|
||||
}
|
||||
|
||||
list.Add(item);
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
catch
|
||||
{
|
||||
if (Debugger.IsAttached)
|
||||
{
|
||||
Debugger.Break();
|
||||
}
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a comma-separated value string to a generic list of GUIDs.
|
||||
/// </summary>
|
||||
/// <param name="csv">The comma-separated GUID values.</param>
|
||||
/// <returns>A list of GUIDs parsed from the CSV string.</returns>
|
||||
public static List<Guid> GetGuidListFromCsv(string csv)
|
||||
{
|
||||
if (string.IsNullOrEmpty(csv))
|
||||
{
|
||||
return new List<Guid>();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
string[] array = csv.Split(',');
|
||||
var list = new List<Guid>();
|
||||
|
||||
foreach (string s in array)
|
||||
{
|
||||
string tmp = s.Trim();
|
||||
if (Guid.TryParse(tmp, out Guid tempGuid))
|
||||
{
|
||||
list.Add(tempGuid);
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
catch
|
||||
{
|
||||
if (Debugger.IsAttached)
|
||||
{
|
||||
Debugger.Break();
|
||||
}
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a comma-separated value string to a generic list of strings.
|
||||
/// </summary>
|
||||
/// <param name="csv">The comma-separated values.</param>
|
||||
/// <returns>A list of strings parsed from the CSV string.</returns>
|
||||
public static List<string> GetGenericListFromCsv(string csv)
|
||||
{
|
||||
var list = new List<string>();
|
||||
|
||||
if (!string.IsNullOrEmpty(csv))
|
||||
{
|
||||
list.AddRange(csv.Split(',').Select(obj => obj.Trim()));
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a comma-separated value string to a generic list of integers.
|
||||
/// </summary>
|
||||
/// <param name="csv">The comma-separated integer values.</param>
|
||||
/// <returns>A list of integers parsed from the CSV string.</returns>
|
||||
public static List<int> GetIntegerListFromCsv(string csv)
|
||||
{
|
||||
var list = new List<int>();
|
||||
|
||||
foreach (string strInt in csv.Split(','))
|
||||
{
|
||||
if (int.TryParse(strInt, out int tempInt))
|
||||
{
|
||||
list.Add(tempInt);
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts an ArrayList to a comma-separated value string.
|
||||
/// </summary>
|
||||
/// <param name="list">The ArrayList to convert.</param>
|
||||
/// <returns>A comma-separated string representation of the list.</returns>
|
||||
public static string GetCsvFromArrayList(ArrayList list)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
bool isFirst = true;
|
||||
|
||||
for (int i = 0; i < list.Count; i++)
|
||||
{
|
||||
if (!isFirst)
|
||||
{
|
||||
sb.Append(',');
|
||||
}
|
||||
sb.Append(list[i]?.ToString());
|
||||
isFirst = false;
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a generic list of strings to a comma-separated value string.
|
||||
/// </summary>
|
||||
/// <param name="list">The list to convert.</param>
|
||||
/// <returns>A comma-separated string representation of the list.</returns>
|
||||
public static string GetCsvFromGenericList(List<string> list)
|
||||
{
|
||||
return string.Join(",", list);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a generic list of strings to a comma-separated value string with optional quotes.
|
||||
/// </summary>
|
||||
/// <param name="list">The list to convert.</param>
|
||||
/// <param name="quoteType">The type of quotes to use around each value.</param>
|
||||
/// <returns>A comma-separated string representation of the list with quotes.</returns>
|
||||
public static string GetCsvFromGenericList(List<string> list, QuoteType quoteType)
|
||||
{
|
||||
if (quoteType == QuoteType.None)
|
||||
{
|
||||
return GetCsvFromGenericList(list);
|
||||
}
|
||||
|
||||
var sb = new StringBuilder();
|
||||
bool isFirst = true;
|
||||
|
||||
foreach (string istring in list)
|
||||
{
|
||||
if (!isFirst)
|
||||
{
|
||||
sb.Append(',');
|
||||
}
|
||||
|
||||
switch (quoteType)
|
||||
{
|
||||
case QuoteType.Double:
|
||||
sb.Append('"');
|
||||
sb.Append(istring);
|
||||
sb.Append('"');
|
||||
break;
|
||||
case QuoteType.Single:
|
||||
sb.Append('\'');
|
||||
sb.Append(istring);
|
||||
sb.Append('\'');
|
||||
break;
|
||||
default:
|
||||
if (Debugger.IsAttached)
|
||||
{
|
||||
Debugger.Break();
|
||||
}
|
||||
sb.Append(istring);
|
||||
break;
|
||||
}
|
||||
|
||||
isFirst = false;
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a generic list of GUIDs to a comma-separated value string with optional quotes.
|
||||
/// </summary>
|
||||
/// <param name="list">The list of GUIDs to convert.</param>
|
||||
/// <param name="quoteType">The type of quotes to use around each GUID.</param>
|
||||
/// <returns>A comma-separated string representation of the GUID list.</returns>
|
||||
public static string GetCsvFromGenericListOfGuids(IList<Guid> list, QuoteType quoteType = QuoteType.None)
|
||||
{
|
||||
if (list == null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var sb = new StringBuilder();
|
||||
bool isFirst = true;
|
||||
|
||||
foreach (Guid g in list)
|
||||
{
|
||||
if (!isFirst)
|
||||
{
|
||||
sb.Append(',');
|
||||
}
|
||||
|
||||
switch (quoteType)
|
||||
{
|
||||
case QuoteType.None:
|
||||
sb.Append(g.ToString());
|
||||
break;
|
||||
case QuoteType.Double:
|
||||
sb.Append('"');
|
||||
sb.Append(g.ToString());
|
||||
sb.Append('"');
|
||||
break;
|
||||
case QuoteType.Single:
|
||||
sb.Append('\'');
|
||||
sb.Append(g.ToString());
|
||||
sb.Append('\'');
|
||||
break;
|
||||
default:
|
||||
if (Debugger.IsAttached)
|
||||
{
|
||||
Debugger.Break();
|
||||
}
|
||||
sb.Append(g.ToString());
|
||||
break;
|
||||
}
|
||||
|
||||
isFirst = false;
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Encodes a list of strings into a single string with special delimiter handling.
|
||||
/// </summary>
|
||||
/// <param name="list">The list of strings to encode.</param>
|
||||
/// <returns>An encoded string representation of the list.</returns>
|
||||
public static string Encode(List<string> list)
|
||||
{
|
||||
var result = new StringBuilder();
|
||||
|
||||
for (int i = 0; i < list.Count - 1; i++)
|
||||
{
|
||||
result.Append(EncodeString(list[i]));
|
||||
result.Append(ENCODED_LIST_DELIMITER);
|
||||
}
|
||||
|
||||
if (list.Count > 0)
|
||||
{
|
||||
result.Append(EncodeString(list[list.Count - 1]));
|
||||
}
|
||||
|
||||
return result.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Encodes a single string by replacing delimiters with illegal characters.
|
||||
/// </summary>
|
||||
/// <param name="stringToEncode">The string to encode.</param>
|
||||
/// <returns>The encoded string.</returns>
|
||||
private static string EncodeString(string stringToEncode)
|
||||
{
|
||||
return stringToEncode.Replace(ENCODED_LIST_DELIMITER, ILLEGAL_CHARACTER);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts an enumerable collection of strings to a GUID array.
|
||||
/// </summary>
|
||||
/// <param name="strings">The collection of string representations of GUIDs.</param>
|
||||
/// <returns>An array of GUIDs.</returns>
|
||||
public static Guid[] ConvertToGuidArray(IEnumerable strings)
|
||||
{
|
||||
var guids = new List<Guid>();
|
||||
|
||||
foreach (object obj in strings)
|
||||
{
|
||||
guids.Add(new Guid(obj.ToString()!));
|
||||
}
|
||||
|
||||
return guids.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts an enumerable collection of strings to a GUID list.
|
||||
/// </summary>
|
||||
/// <param name="strings">The collection of string representations of GUIDs.</param>
|
||||
/// <returns>A list of GUIDs.</returns>
|
||||
public static List<Guid> ConvertToGuidList(IEnumerable strings)
|
||||
{
|
||||
var guids = new List<Guid>();
|
||||
|
||||
foreach (object obj in strings)
|
||||
{
|
||||
guids.Add(new Guid(obj.ToString()!));
|
||||
}
|
||||
|
||||
return guids;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a GUID list string to a generic list of GUIDs.
|
||||
/// </summary>
|
||||
/// <param name="guidListAsString">The comma-separated GUID string.</param>
|
||||
/// <returns>A list of GUIDs, or an empty list if the input is empty.</returns>
|
||||
public static List<Guid> ConvertGuidStringToList(string guidListAsString)
|
||||
{
|
||||
if (string.IsNullOrEmpty(guidListAsString))
|
||||
{
|
||||
return new List<Guid>();
|
||||
}
|
||||
|
||||
var list = new List<Guid>();
|
||||
string[] guidStringArray = guidListAsString.Split(',');
|
||||
|
||||
foreach (string guidString in guidStringArray)
|
||||
{
|
||||
list.Add(new Guid(guidString));
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a generic enumerable of GUIDs to a string representation.
|
||||
/// </summary>
|
||||
/// <param name="guidList">The list of GUIDs to convert.</param>
|
||||
/// <param name="surroundWithChar">Optional character to surround each GUID with.</param>
|
||||
/// <returns>A string representation of the GUID list.</returns>
|
||||
public static string ConvertGuidListToString(IEnumerable<Guid> guidList, string surroundWithChar = "")
|
||||
{
|
||||
if (guidList == null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var guidString = new StringBuilder();
|
||||
bool isFirst = true;
|
||||
|
||||
foreach (Guid guid in guidList)
|
||||
{
|
||||
if (!isFirst)
|
||||
{
|
||||
guidString.Append(',');
|
||||
}
|
||||
|
||||
isFirst = false;
|
||||
guidString.Append(surroundWithChar);
|
||||
guidString.Append(guid.ToString());
|
||||
guidString.Append(surroundWithChar);
|
||||
}
|
||||
|
||||
return guidString.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a generic enumerable of GUIDs to a list of string representations.
|
||||
/// </summary>
|
||||
/// <param name="guidList">The list of GUIDs to convert.</param>
|
||||
/// <returns>A list of string representations of the GUIDs.</returns>
|
||||
public static List<string> ConvertGuidListToStringList(IEnumerable<Guid> guidList)
|
||||
{
|
||||
var retList = new List<string>();
|
||||
|
||||
foreach (Guid guidObj in guidList)
|
||||
{
|
||||
retList.Add(guidObj.ToString());
|
||||
}
|
||||
|
||||
return retList;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the intersection of two GUID lists (GUIDs present in both lists).
|
||||
/// </summary>
|
||||
/// <param name="guidList1">The first GUID list.</param>
|
||||
/// <param name="guidList2">The second GUID list.</param>
|
||||
/// <returns>A list containing GUIDs that are present in both input lists.</returns>
|
||||
public static List<Guid> GetGuidListsIntersection(List<Guid> guidList1, List<Guid> guidList2)
|
||||
{
|
||||
var finalList = new List<Guid>();
|
||||
var foundGuids = new Dictionary<Guid, bool>();
|
||||
|
||||
if (guidList1 != null)
|
||||
{
|
||||
foreach (Guid g in guidList1.Where(g => !foundGuids.ContainsKey(g)))
|
||||
{
|
||||
foundGuids.Add(g, true);
|
||||
}
|
||||
}
|
||||
|
||||
if (guidList2 != null)
|
||||
{
|
||||
finalList.AddRange(guidList2.Where(g => foundGuids.ContainsKey(g)));
|
||||
}
|
||||
|
||||
return finalList;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the first non-null object from the provided parameters.
|
||||
/// </summary>
|
||||
/// <param name="values">The objects to evaluate.</param>
|
||||
/// <returns>The first non-null object, or null if all are null.</returns>
|
||||
public static object? Coalesce(params object[] values)
|
||||
{
|
||||
return values.FirstOrDefault(y => y != null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,301 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Strata.SqlTools.SqlBreakdown.Utilities;
|
||||
|
||||
/// <summary>
|
||||
/// Provides utility methods for working with GUIDs including encoding, decoding, validation, and parsing operations.
|
||||
/// </summary>
|
||||
public static class GuidUtils
|
||||
{
|
||||
#region Constants
|
||||
|
||||
private const string GUID_STRING = @"(\{){0,1}[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12}(\}){0,1}";
|
||||
|
||||
#endregion
|
||||
|
||||
#region Encoding / Decoding
|
||||
|
||||
/// <summary>
|
||||
/// Removes hyphens from the GUID and prefixes it with a "g".
|
||||
/// </summary>
|
||||
/// <param name="guid">The GUID to translate.</param>
|
||||
/// <returns>A string representation of the GUID with hyphens removed and prefixed with "g".</returns>
|
||||
public static string TranslateGuid(Guid guid)
|
||||
{
|
||||
string newguid = guid.ToString();
|
||||
newguid = newguid.Replace("-", string.Empty);
|
||||
return "g" + newguid;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reconstructs a GUID from a string created by <see cref="TranslateGuid"/>.
|
||||
/// </summary>
|
||||
/// <param name="value">The translated GUID string.</param>
|
||||
/// <returns>The reconstructed GUID, or <see cref="Guid.Empty"/> if the input is invalid.</returns>
|
||||
public static Guid UnTranslateGuid(string value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
return Guid.Empty;
|
||||
}
|
||||
|
||||
if (value.Contains("-"))
|
||||
{
|
||||
// This function was called on an already valid guid
|
||||
return new Guid(value);
|
||||
}
|
||||
|
||||
if (!value.StartsWith("g", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Guid.Empty;
|
||||
}
|
||||
|
||||
if (value.Length != 33)
|
||||
{
|
||||
return Guid.Empty;
|
||||
}
|
||||
|
||||
string newguid = value.Substring(1); // Get rid of trailing g
|
||||
|
||||
newguid = newguid.Insert(8, "-");
|
||||
newguid = newguid.Insert(13, "-");
|
||||
newguid = newguid.Insert(18, "-");
|
||||
newguid = newguid.Insert(23, "-");
|
||||
|
||||
return GetGuid(newguid);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Takes any string and converts it into a GUID by truncating to 32 characters and translating the character codes of invalid characters.
|
||||
/// Results are consistent for any given string, but are not guaranteed to be unique compared to results for other inputs.
|
||||
/// </summary>
|
||||
/// <param name="text">The text to convert to a GUID.</param>
|
||||
/// <returns>A GUID representation of the input text.</returns>
|
||||
#pragma warning disable S3776 // Cognitive Complexity of methods should not be too high - GUID translation logic inherently complex
|
||||
public static Guid TranslateTextToGuid(string text)
|
||||
#pragma warning restore S3776
|
||||
{
|
||||
string newguid = text.ToUpperInvariant();
|
||||
|
||||
if (string.IsNullOrEmpty(newguid))
|
||||
{
|
||||
if (Debugger.IsAttached)
|
||||
{
|
||||
Debugger.Break();
|
||||
}
|
||||
return Guid.Empty;
|
||||
}
|
||||
|
||||
// Make sure the string is the correct length
|
||||
var sb = new System.Text.StringBuilder(newguid);
|
||||
while (sb.Length < Guid.Empty.ToString().Length)
|
||||
{
|
||||
sb.Append(sb.ToString());
|
||||
}
|
||||
newguid = sb.ToString();
|
||||
|
||||
if (newguid.Length > Guid.Empty.ToString().Length - 4)
|
||||
{
|
||||
newguid = newguid.Substring(newguid.Length - Guid.Empty.ToString().Length + 4);
|
||||
}
|
||||
|
||||
// Insert dashes
|
||||
newguid = newguid.Insert(8, "-");
|
||||
newguid = newguid.Insert(13, "-");
|
||||
newguid = newguid.Insert(18, "-");
|
||||
newguid = newguid.Insert(23, "-");
|
||||
|
||||
// Convert invalid characters
|
||||
const string valid = "0123456789ABCDEF";
|
||||
char[] guidChars = newguid.ToCharArray();
|
||||
|
||||
for (int i = 0; i < guidChars.Length; i++)
|
||||
{
|
||||
char chr = guidChars[i];
|
||||
if (valid.IndexOf(chr) == -1 && !((chr == '-') && (i == 8 || i == 13 || i == 18 || i == 23)))
|
||||
{
|
||||
guidChars[i] = valid[Math.Abs(StringUtils.GetHashCode32Bit(chr)) % valid.Length];
|
||||
}
|
||||
}
|
||||
|
||||
newguid = new string(guidChars);
|
||||
|
||||
// Here goes nothing
|
||||
try
|
||||
{
|
||||
return new Guid(newguid);
|
||||
}
|
||||
catch
|
||||
{
|
||||
if (Debugger.IsAttached)
|
||||
{
|
||||
Debugger.Break();
|
||||
}
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts hyphens in the GUID to underscores, and prefixes it with a "G". This result is safe to use in a SQL expression.
|
||||
/// </summary>
|
||||
/// <param name="guid">The GUID to convert.</param>
|
||||
/// <returns>A SQL-safe column name representation of the GUID.</returns>
|
||||
public static string GetSqlColumnSafeGuid(Guid guid)
|
||||
{
|
||||
if (!_guidToColumnCache.TryGetValue(guid, out string? answer))
|
||||
{
|
||||
answer = "G" + guid.ToString().ToUpperInvariant().Replace("-", "_");
|
||||
_guidToColumnCache.TryAdd(guid, answer);
|
||||
}
|
||||
|
||||
return answer;
|
||||
}
|
||||
|
||||
private static readonly ConcurrentDictionary<Guid, string> _guidToColumnCache = new ConcurrentDictionary<Guid, string>();
|
||||
|
||||
/// <summary>
|
||||
/// Reconstructs a GUID from a SQL-safe column name created by <see cref="GetSqlColumnSafeGuid"/>.
|
||||
/// </summary>
|
||||
/// <param name="columnName">The SQL-safe column name.</param>
|
||||
/// <returns>The reconstructed GUID.</returns>
|
||||
public static Guid GetGuidFromSqlColumnSafeGuid(string columnName)
|
||||
{
|
||||
string guidStr = columnName.Substring(1); // Remove G
|
||||
guidStr = guidStr.Replace("_", "-");
|
||||
return GetGuid(guidStr);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Validation
|
||||
|
||||
/// <summary>
|
||||
/// Tests if the given string is a GUID.
|
||||
/// </summary>
|
||||
/// <param name="value">The string to test.</param>
|
||||
/// <returns><c>true</c> if the string is a valid GUID; otherwise, <c>false</c>.</returns>
|
||||
public static bool IsGuid(string value)
|
||||
{
|
||||
try
|
||||
{
|
||||
return Guid.TryParse(value, out _);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if the given object is a GUID, or another type that can be cast to a GUID.
|
||||
/// </summary>
|
||||
/// <param name="value">The object to test.</param>
|
||||
/// <returns><c>true</c> if the object is or can be converted to a valid GUID; otherwise, <c>false</c>.</returns>
|
||||
public static bool IsGuid(object value)
|
||||
{
|
||||
if (value is Guid)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (value is string strValue)
|
||||
{
|
||||
return IsGuid(strValue);
|
||||
}
|
||||
|
||||
return IsGuid(value?.ToString()!);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds the first GUID in the given string using regex pattern matching.
|
||||
/// </summary>
|
||||
/// <param name="value">The string to search.</param>
|
||||
/// <returns>The first GUID found, or <see cref="Guid.Empty"/> if no GUID is found.</returns>
|
||||
public static Guid FindFirstGuid(string value)
|
||||
{
|
||||
Match match = FindFirstGuidRegex.Match(value);
|
||||
if (match.Success)
|
||||
{
|
||||
return new Guid(match.Value);
|
||||
}
|
||||
|
||||
return Guid.Empty;
|
||||
}
|
||||
|
||||
private static readonly Regex FindFirstGuidRegex = new Regex(
|
||||
"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}",
|
||||
RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Parsing
|
||||
|
||||
/// <summary>
|
||||
/// Returns a new GUID from the specified string value.
|
||||
/// </summary>
|
||||
/// <param name="value">GUID as a string.</param>
|
||||
/// <param name="defaultValue">Default value to return if the GUID fails to parse.</param>
|
||||
/// <returns>A GUID parsed from the string, or the default value if parsing fails.</returns>
|
||||
public static Guid GetGuid(string value, Guid defaultValue)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
if (Guid.TryParse(value, out Guid result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a new GUID from the specified string value, or <see cref="Guid.Empty"/> if the string doesn't contain a GUID.
|
||||
/// </summary>
|
||||
/// <param name="value">GUID as a string.</param>
|
||||
/// <returns>A GUID parsed from the string, or <see cref="Guid.Empty"/> if parsing fails.</returns>
|
||||
public static Guid GetGuid(string value)
|
||||
{
|
||||
return GetGuid(value, Guid.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a new GUID from the specified object value.
|
||||
/// </summary>
|
||||
/// <param name="value">GUID as an object.</param>
|
||||
/// <returns>A GUID parsed from the object, or <see cref="Guid.Empty"/> if the object is null or parsing fails.</returns>
|
||||
public static Guid GetGuid(object value)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
return Guid.Empty;
|
||||
}
|
||||
|
||||
return GetGuid(value.ToString()!, Guid.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all GUIDs that are present in the input string.
|
||||
/// </summary>
|
||||
/// <param name="value">Any string with GUIDs.</param>
|
||||
/// <returns>A list of GUID strings found in the input.</returns>
|
||||
public static List<string> GetGuids(string value)
|
||||
{
|
||||
MatchCollection matches = Regex.Matches(value, GUID_STRING);
|
||||
return matches.Cast<Match>().Select(x => x.Value).ToList();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
using Strata.SqlTools.SqlBreakdown.Enums.SQL;
|
||||
using Strata.SqlTools.SqlBreakdown.Exceptions;
|
||||
|
||||
namespace Strata.SqlTools.SqlBreakdown.Utilities;
|
||||
|
||||
/// <summary>
|
||||
/// SQL aggregation function helper methods.
|
||||
/// </summary>
|
||||
public static partial class SqlUtils
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the default aggregation type for a given SQL data type.
|
||||
/// </summary>
|
||||
/// <param name="dataType">The SQL data type.</param>
|
||||
/// <returns>The default aggregate function.</returns>
|
||||
public static AggregateFunction GetDefaultAggregationType(SqlDataType dataType)
|
||||
{
|
||||
if (dataType == SqlDataType.Bit)
|
||||
{
|
||||
return AggregateFunction.Count;
|
||||
}
|
||||
|
||||
return AggregateFunction.Max;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if an aggregation function is valid for a given SQL data type.
|
||||
/// </summary>
|
||||
/// <param name="aggregationType">The aggregate function.</param>
|
||||
/// <param name="dataType">The SQL data type.</param>
|
||||
/// <returns><c>true</c> if the aggregation is valid; otherwise, <c>false</c>.</returns>
|
||||
public static bool IsAggregationValid(AggregateFunction aggregationType, SqlDataType dataType)
|
||||
{
|
||||
switch (aggregationType)
|
||||
{
|
||||
case AggregateFunction.None:
|
||||
case AggregateFunction.Count:
|
||||
return true;
|
||||
|
||||
case AggregateFunction.Avg:
|
||||
case AggregateFunction.Sum:
|
||||
// SUM and AVG only work with numeric datatypes
|
||||
return IsNumericType(dataType);
|
||||
|
||||
case AggregateFunction.Max:
|
||||
case AggregateFunction.Min:
|
||||
// MAX and MIN work with Numeric, Character and DateTime columns
|
||||
return IsNumericType(dataType) || IsDateTimeType(dataType) || IsCharacterType(dataType);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the SQL string for an aggregate function.
|
||||
/// </summary>
|
||||
/// <param name="aggregateFunction">The aggregate function.</param>
|
||||
/// <returns>The SQL aggregate function name.</returns>
|
||||
public static string GetAggregateFunctionSql(AggregateFunction aggregateFunction)
|
||||
=> aggregateFunction switch
|
||||
{
|
||||
AggregateFunction.Avg => "AVG",
|
||||
AggregateFunction.Count => "COUNT",
|
||||
AggregateFunction.Max => "MAX",
|
||||
AggregateFunction.Min => "MIN",
|
||||
AggregateFunction.Sum => "SUM",
|
||||
AggregateFunction.None => string.Empty,
|
||||
_ => throw new NotImplementedEnumValueException<AggregateFunction>(aggregateFunction)
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a value that will not affect the aggregation (typically NULL).
|
||||
/// </summary>
|
||||
/// <param name="aggregateFunction">The aggregate function.</param>
|
||||
/// <returns>A SQL null value string.</returns>
|
||||
public static string GetAggregateNullValue(AggregateFunction aggregateFunction)
|
||||
=> "NULL";
|
||||
}
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
using System.Text;
|
||||
|
||||
namespace Strata.SqlTools.SqlBreakdown.Utilities;
|
||||
|
||||
/// <summary>
|
||||
/// SQL column and alias helper methods.
|
||||
/// </summary>
|
||||
public static partial class SqlUtils
|
||||
{
|
||||
/// <summary>
|
||||
/// Appends a column expression with alias and comma to a StringBuilder.
|
||||
/// </summary>
|
||||
/// <param name="stringBuilder">The StringBuilder to append to.</param>
|
||||
/// <param name="alias">The table alias.</param>
|
||||
/// <param name="columnName">The column name.</param>
|
||||
public static void AppendAliasColumnWithComma(StringBuilder stringBuilder, string alias, string columnName)
|
||||
{
|
||||
stringBuilder.Append(alias);
|
||||
stringBuilder.Append(".");
|
||||
stringBuilder.Append(columnName);
|
||||
stringBuilder.AppendLine(",");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a column expression with alias and comma.
|
||||
/// </summary>
|
||||
/// <param name="alias">The table alias.</param>
|
||||
/// <param name="columnName">The column name.</param>
|
||||
/// <returns>A formatted column expression string.</returns>
|
||||
public static string GetAliasColumnWithComma(string alias, string columnName)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
AppendAliasColumnWithComma(sb, alias, columnName);
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends a CAST expression with alias and comma to a StringBuilder.
|
||||
/// </summary>
|
||||
/// <param name="stringBuilder">The StringBuilder to append to.</param>
|
||||
/// <param name="alias">The table alias.</param>
|
||||
/// <param name="columnName">The column name.</param>
|
||||
/// <param name="dataType">The target data type for CAST.</param>
|
||||
public static void AppendAliasColumnWithCommaCasting(StringBuilder stringBuilder, string alias, string columnName, string dataType)
|
||||
{
|
||||
AppendAliasColumnWithCommaCasting(stringBuilder, alias, columnName, columnName, dataType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends a CAST expression with alias, new column name, and comma to a StringBuilder.
|
||||
/// </summary>
|
||||
/// <param name="stringBuilder">The StringBuilder to append to.</param>
|
||||
/// <param name="alias">The table alias.</param>
|
||||
/// <param name="columnName">The column name.</param>
|
||||
/// <param name="newColumnName">The alias for the result column.</param>
|
||||
/// <param name="dataType">The target data type for CAST.</param>
|
||||
public static void AppendAliasColumnWithCommaCasting(StringBuilder stringBuilder, string alias, string columnName, string newColumnName, string dataType)
|
||||
{
|
||||
stringBuilder.Append("CAST(");
|
||||
stringBuilder.Append(alias);
|
||||
stringBuilder.Append(".");
|
||||
stringBuilder.Append(columnName);
|
||||
stringBuilder.Append(" AS ");
|
||||
stringBuilder.Append(dataType);
|
||||
stringBuilder.Append(") AS ");
|
||||
stringBuilder.Append(newColumnName);
|
||||
stringBuilder.AppendLine(",");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a CAST expression with alias and comma.
|
||||
/// </summary>
|
||||
/// <param name="alias">The table alias.</param>
|
||||
/// <param name="columnName">The column name.</param>
|
||||
/// <param name="dataType">The target data type for CAST.</param>
|
||||
/// <returns>A formatted CAST expression string.</returns>
|
||||
public static string GetAliasColumnWithCommaCasting(string alias, string columnName, string dataType)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
AppendAliasColumnWithCommaCasting(sb, alias, columnName, dataType);
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a list of column names to a SQL-safe bracketed column list.
|
||||
/// </summary>
|
||||
/// <param name="columnNames">The list of column names.</param>
|
||||
/// <returns>A comma-separated string of bracketed column names.</returns>
|
||||
public static string GetSqlSafeColumnList(List<string> columnNames)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
for (int i = 0; i < columnNames.Count; i++)
|
||||
{
|
||||
if (i > 0)
|
||||
{
|
||||
sb.Append(",");
|
||||
}
|
||||
|
||||
string trimmed = columnNames[i].Trim();
|
||||
if (trimmed.StartsWith('['))
|
||||
{
|
||||
sb.Append(columnNames[i]);
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.Append($"[{columnNames[i]}]");
|
||||
}
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
using Strata.SqlTools.SqlBreakdown.Enums.SQL;
|
||||
|
||||
namespace Strata.SqlTools.SqlBreakdown.Utilities;
|
||||
|
||||
/// <summary>
|
||||
/// SQL data type checking helper methods.
|
||||
/// </summary>
|
||||
public static partial class SqlUtils
|
||||
{
|
||||
/// <summary>
|
||||
/// Determines if a SQL data type is a character type.
|
||||
/// </summary>
|
||||
/// <param name="dataType">The SQL data type.</param>
|
||||
/// <returns><c>true</c> if the data type is a character type; otherwise, <c>false</c>.</returns>
|
||||
public static bool IsCharacterType(SqlDataType dataType)
|
||||
=> dataType switch
|
||||
{
|
||||
SqlDataType.Char or SqlDataType.NChar or SqlDataType.Text or SqlDataType.NText or SqlDataType.VarChar or SqlDataType.NVarChar => true,
|
||||
_ => false
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Determines if a SQL data type is a date/time type.
|
||||
/// </summary>
|
||||
/// <param name="dataType">The SQL data type.</param>
|
||||
/// <returns><c>true</c> if the data type is a date/time type; otherwise, <c>false</c>.</returns>
|
||||
public static bool IsDateTimeType(SqlDataType dataType)
|
||||
=> dataType switch
|
||||
{
|
||||
SqlDataType.DateTime or SqlDataType.SmallDateTime or SqlDataType.Date => true,
|
||||
_ => false
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Determines if a SQL data type is a numeric type.
|
||||
/// </summary>
|
||||
/// <param name="dataType">The SQL data type.</param>
|
||||
/// <returns><c>true</c> if the data type is a numeric type; otherwise, <c>false</c>.</returns>
|
||||
public static bool IsNumericType(SqlDataType dataType)
|
||||
=> dataType switch
|
||||
{
|
||||
SqlDataType.TinyInt or SqlDataType.SmallInt or SqlDataType.Int or SqlDataType.BigInt or SqlDataType.Decimal or SqlDataType.Money or SqlDataType.SmallMoney or SqlDataType.Float or SqlDataType.Real or SqlDataType.Numeric => true,
|
||||
_ => false
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Test if the specified type can be cast to a string for SQL-side comparisons.
|
||||
/// E.g. this is safe for Integers, but not for Doubles since "3.0" != "3".
|
||||
/// </summary>
|
||||
/// <param name="dataType">The .NET data type.</param>
|
||||
/// <returns><c>true</c> if the type can be safely compared as a string; otherwise, <c>false</c>.</returns>
|
||||
public static bool IsStringComparableType(Type dataType)
|
||||
{
|
||||
if (dataType == typeof(float) || dataType == typeof(double) || dataType == typeof(decimal))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the full SQL column type name including precision, scale, and max length.
|
||||
/// </summary>
|
||||
/// <param name="columnType">The SQL data type.</param>
|
||||
/// <param name="precision">The precision for decimal types.</param>
|
||||
/// <param name="scale">The scale for decimal types.</param>
|
||||
/// <param name="maxLength">The maximum length for variable-length types.</param>
|
||||
/// <returns>The full SQL type name.</returns>
|
||||
public static string GetColumnTypeFullName(SqlDataType columnType, int precision, int scale, int maxLength)
|
||||
=> columnType switch
|
||||
{
|
||||
SqlDataType.BigInt => "bigint",
|
||||
SqlDataType.Binary => "binary",
|
||||
SqlDataType.Bit => "bit",
|
||||
SqlDataType.Char => $"char({(maxLength == -1 ? "max" : maxLength.ToString())})",
|
||||
SqlDataType.Date => "date",
|
||||
SqlDataType.DateTime => "datetime",
|
||||
SqlDataType.Decimal => $"decimal({precision},{scale})",
|
||||
SqlDataType.Float => "float",
|
||||
SqlDataType.Image => "image",
|
||||
SqlDataType.Int => "int",
|
||||
SqlDataType.Money => "money",
|
||||
SqlDataType.NChar => $"nchar({(maxLength == -1 ? "max" : (maxLength / 2).ToString())})",
|
||||
SqlDataType.NText => "ntext",
|
||||
SqlDataType.NVarChar => $"nvarchar({(maxLength == -1 ? "max" : (maxLength / 2).ToString())})",
|
||||
SqlDataType.Numeric => "numeric",
|
||||
SqlDataType.Real => "real",
|
||||
SqlDataType.SmallDateTime => "smalldatetime",
|
||||
SqlDataType.SmallInt => "smallint",
|
||||
SqlDataType.SmallMoney => "smallmoney",
|
||||
SqlDataType.Text => "text",
|
||||
SqlDataType.Timestamp => "timestamp",
|
||||
SqlDataType.TinyInt => "tinyint",
|
||||
SqlDataType.UniqueIdentifier => "uniqueidentifier",
|
||||
SqlDataType.VarBinary => "varbinary",
|
||||
SqlDataType.VarChar => $"varchar({(maxLength == -1 ? "max" : maxLength.ToString())})",
|
||||
SqlDataType.XML => "xml",
|
||||
_ => throw new NotImplementedException()
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
using System.Text;
|
||||
using Strata.SqlTools.SqlBreakdown.Classes;
|
||||
using Strata.SqlTools.SqlBreakdown.Enums.SQL;
|
||||
using Strata.SqlTools.SqlBreakdown.Interfaces.Core;
|
||||
|
||||
namespace Strata.SqlTools.SqlBreakdown.Utilities;
|
||||
|
||||
/// <summary>
|
||||
/// SQL filtering helper methods.
|
||||
/// </summary>
|
||||
public static partial class SqlUtils
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a filter value string for date range filtering.
|
||||
/// </summary>
|
||||
/// <param name="startDate">The start date.</param>
|
||||
/// <param name="endDate">The end date.</param>
|
||||
/// <returns>A comma-separated string of dates.</returns>
|
||||
public static string GetFilterValueForBetweenDates(DateTime startDate, DateTime endDate)
|
||||
{
|
||||
if (startDate == DateTime.MinValue && endDate == DateTime.MinValue)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
DateTime effectiveEnd = endDate == DateTime.MinValue ? DateTime.MaxValue : endDate;
|
||||
return $"{startDate},{effectiveEnd}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a filter value string for short date/time range filtering.
|
||||
/// </summary>
|
||||
/// <param name="startDate">The start date.</param>
|
||||
/// <param name="endDate">The end date.</param>
|
||||
/// <returns>A comma-separated string of formatted dates.</returns>
|
||||
public static string GetFilterValueForBetweenShortDateTimes(DateTime startDate, DateTime endDate)
|
||||
{
|
||||
if (startDate == DateTime.MinValue && endDate == DateTime.MinValue)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
string strStartDate = startDate.ToShortDateString();
|
||||
string strEndDate = endDate == DateTime.MinValue ? DateTime.MaxValue.ToString("g") : endDate.ToString("g");
|
||||
|
||||
return $"{strStartDate},{strEndDate}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a SQL filter for multiple columns using LIKE operator.
|
||||
/// </summary>
|
||||
/// <param name="columnNameList">The list of column names to search.</param>
|
||||
/// <param name="value">The value to search for.</param>
|
||||
/// <returns>An ISQL filter object.</returns>
|
||||
public static ISql GetMultiFilterSql(List<string> columnNameList, string value)
|
||||
{
|
||||
var sqlString = new StringBuilder();
|
||||
var filter = new SqlFilter();
|
||||
|
||||
for (int i = 0; i < columnNameList.Count; i++)
|
||||
{
|
||||
sqlString.Append($"{columnNameList[i]} like '%{value}%'");
|
||||
if (i < columnNameList.Count - 1)
|
||||
{
|
||||
sqlString.Append(" or ");
|
||||
}
|
||||
}
|
||||
|
||||
filter.SqlExpression = sqlString.ToString();
|
||||
return filter;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a friendly name for a filter operation.
|
||||
/// </summary>
|
||||
/// <param name="operation">The filter operation.</param>
|
||||
/// <returns>A user-friendly name for the operation.</returns>
|
||||
public static string GetFilterOperatorFriendlyName(FilterOperation operation)
|
||||
=> operation switch
|
||||
{
|
||||
FilterOperation.GreaterThan => "Greater Than",
|
||||
FilterOperation.GreaterThanEqualTo => "Greater Than or Equals To",
|
||||
FilterOperation.LessThan => "Less Than",
|
||||
FilterOperation.LessThanEqualTo => "Less Than or Equals To",
|
||||
FilterOperation.Equal => "Equals",
|
||||
FilterOperation.NotEqual => "Not Equals",
|
||||
FilterOperation.EndsWith => "Ends with",
|
||||
FilterOperation.StartsWith => "Starts with",
|
||||
FilterOperation.NotContains => "Not contains",
|
||||
FilterOperation.NotIn => "Not in",
|
||||
FilterOperation.NotBetween => "Not Between",
|
||||
FilterOperation.Exclude => "Exclude",
|
||||
_ => operation.ToString()
|
||||
};
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user