493 lines
18 KiB
C#
493 lines
18 KiB
C#
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);
|
|
}
|
|
}
|
|
|