Files
sql-utilities/src/Strata.SqlTools.SqlBreakdown/Classes/SqlBreakdownCollection.cs
T

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&lt;ISqlBreakdown&gt; to provide standard collection semantics and LINQ support.
/// </remarks>
[Serializable]
public class SqlBreakdownCollection : ICollection<ISqlBreakdown>
{
private readonly List<ISqlBreakdown> _breakdowns;
private string _separator = "GO";
/// <summary>
/// Initializes a new instance of the <see cref="SqlBreakdownCollection"/> class.
/// </summary>
public SqlBreakdownCollection()
{
_breakdowns = new List<ISqlBreakdown>();
}
/// <summary>
/// Initializes a new instance of the <see cref="SqlBreakdownCollection"/> class with initial breakdowns.
/// </summary>
/// <param name="breakdowns">The initial collection of SQL breakdowns.</param>
public SqlBreakdownCollection(IEnumerable<ISqlBreakdown> breakdowns)
{
_breakdowns = new List<ISqlBreakdown>(breakdowns ?? Enumerable.Empty<ISqlBreakdown>());
}
/// <summary>
/// Gets the collection of SQL breakdowns.
/// </summary>
public IReadOnlyList<ISqlBreakdown> Breakdowns => _breakdowns.AsReadOnly();
/// <summary>
/// Gets the collection of raw SQL statements from all breakdowns.
/// </summary>
public IReadOnlyList<string> RawStatements => _breakdowns
.Where(b => !string.IsNullOrWhiteSpace(b.RawSql))
.Select(b => b.RawSql!)
.ToList()
.AsReadOnly();
/// <summary>
/// Gets the count of SQL breakdowns in the collection.
/// </summary>
public int Count => _breakdowns.Count;
/// <summary>
/// Gets a value indicating whether the collection is empty.
/// </summary>
public bool IsEmpty => _breakdowns.Count == 0;
/// <summary>
/// Gets a value indicating whether the collection is read-only.
/// </summary>
/// <remarks>
/// This collection is not read-only; items can be added and removed.
/// </remarks>
public bool IsReadOnly => false;
/// <summary>
/// Gets or sets the separator used for splitting and combining SQL statements.
/// The default separator is "GO" (common in SQL Server and T-SQL).
/// You can set this to ";" or other separators based on your SQL dialect.
/// </summary>
/// <remarks>
/// When set, this separator will be used as the default for ParseBatch, GetCombinedSql, and GetBatchSql operations.
/// Individual method calls can still override this default by providing an explicit separator argument.
/// </remarks>
public string Separator
{
get => _separator;
set => _separator = string.IsNullOrWhiteSpace(value) ? "GO" : value;
}
/// <summary>
/// Adds a single SQL breakdown to the collection.
/// </summary>
/// <param name="breakdown">The breakdown to add.</param>
/// <exception cref="ArgumentNullException">Thrown when breakdown is null.</exception>
public void Add(ISqlBreakdown breakdown)
{
if (breakdown == null)
{
throw new ArgumentNullException(nameof(breakdown));
}
_breakdowns.Add(breakdown);
}
/// <summary>
/// Adds multiple SQL breakdowns to the collection.
/// </summary>
/// <param name="breakdowns">The breakdowns to add.</param>
/// <exception cref="ArgumentNullException">Thrown when breakdowns is null.</exception>
public void AddRange(IEnumerable<ISqlBreakdown> breakdowns)
{
if (breakdowns == null)
{
throw new ArgumentNullException(nameof(breakdowns));
}
_breakdowns.AddRange(breakdowns);
}
/// <summary>
/// Removes a SQL breakdown from the collection.
/// </summary>
/// <param name="breakdown">The breakdown to remove.</param>
/// <returns>True if the breakdown was removed; otherwise, false.</returns>
public bool Remove(ISqlBreakdown breakdown)
{
return _breakdowns.Remove(breakdown);
}
/// <summary>
/// Removes all SQL breakdowns from the collection.
/// </summary>
public void Clear()
{
_breakdowns.Clear();
}
/// <summary>
/// Parses a batch SQL statement containing multiple queries and populates the collection.
/// </summary>
/// <remarks>
/// Splits the SQL batch by the configured separator (default: GO) or provided separator argument.
/// The GO statement is a batch separator commonly used in SQL Server and T-SQL.
/// Each parsed statement is stored as a RawSqlBreakdown in the collection.
/// </remarks>
/// <param name="sqlBatch">The batch SQL statement to parse.</param>
/// <param name="separator">Optional separator to use for splitting. If null, uses the Separator property.</param>
/// <exception cref="ArgumentNullException">Thrown when sqlBatch is null.</exception>
public void ParseBatch(string sqlBatch, string? separator = null)
{
if (sqlBatch == null)
{
throw new ArgumentNullException(nameof(sqlBatch));
}
Clear();
var effectiveSeparator = separator ?? _separator;
// Split by configured separator
var statements = SplitBySeparators(sqlBatch, effectiveSeparator);
// Create RawSqlBreakdown objects for each statement
foreach (var statement in statements)
{
var trimmed = statement.Trim();
if (!string.IsNullOrWhiteSpace(trimmed))
{
var breakdown = new RawSqlBreakdown { RawSql = trimmed };
_breakdowns.Add(breakdown);
}
}
}
/// <summary>
/// Gets the combined SQL from all breakdowns in the collection.
/// </summary>
/// <param name="includeSetupFinish">Whether to include setup and finish clauses for each breakdown.</param>
/// <param name="separator">The separator to use between SQL statements. If null, uses the Separator property.</param>
/// <returns>The combined SQL string from all breakdowns.</returns>
public string GetCombinedSql(bool includeSetupFinish = true, string? separator = null)
{
if (_breakdowns.Count == 0)
{
return string.Empty;
}
var effectiveSeparator = separator ?? _separator;
var sb = new StringBuilder();
for (int i = 0; i < _breakdowns.Count; i++)
{
var sql = _breakdowns[i].GetSql(includeSetupFinish);
sb.Append(sql);
// Add separator between statements (but not after the last one)
if (i < _breakdowns.Count - 1)
{
sb.AppendLine();
sb.AppendLine(effectiveSeparator);
sb.AppendLine();
}
}
return sb.ToString();
}
/// <summary>
/// Gets the raw SQL statements as a batch (joined with the configured separator).
/// </summary>
/// <param name="separator">The separator to use between SQL statements. If null, uses the Separator property.</param>
/// <returns>The combined raw SQL statements.</returns>
public string GetBatchSql(string? separator = null)
{
var rawStatements = RawStatements;
if (rawStatements.Count == 0)
{
return string.Empty;
}
var effectiveSeparator = separator ?? _separator;
return string.Join($"{Environment.NewLine}{effectiveSeparator}{Environment.NewLine}", rawStatements);
}
/// <summary>
/// Determines whether the collection contains a specific breakdown.
/// </summary>
/// <param name="item">The breakdown to locate.</param>
/// <returns>True if the breakdown is found; otherwise, false.</returns>
public bool Contains(ISqlBreakdown item)
{
return _breakdowns.Contains(item);
}
/// <summary>
/// Copies the elements of the collection to an array, starting at a particular array index.
/// </summary>
/// <param name="array">The destination array.</param>
/// <param name="arrayIndex">The zero-based index in the array at which copying begins.</param>
/// <exception cref="ArgumentNullException">Thrown when array is null.</exception>
/// <exception cref="ArgumentOutOfRangeException">Thrown when arrayIndex is out of range.</exception>
/// <exception cref="ArgumentException">Thrown when there is not enough space in the array.</exception>
public void CopyTo(ISqlBreakdown[] array, int arrayIndex)
{
_breakdowns.CopyTo(array, arrayIndex);
}
/// <summary>
/// Gets a breakdown at the specified index.
/// </summary>
/// <param name="index">The index of the breakdown.</param>
/// <returns>The breakdown at the specified index.</returns>
/// <exception cref="ArgumentOutOfRangeException">Thrown when index is out of range.</exception>
public ISqlBreakdown GetAt(int index)
{
if (index < 0 || index >= _breakdowns.Count)
{
throw new ArgumentOutOfRangeException(nameof(index), $"Index {index} is out of range for collection with {_breakdowns.Count} items.");
}
return _breakdowns[index];
}
/// <summary>
/// Gets the first breakdown matching the given predicate.
/// </summary>
/// <param name="predicate">The predicate to match.</param>
/// <returns>The first matching breakdown, or null if not found.</returns>
public ISqlBreakdown? FirstOrDefault(Func<ISqlBreakdown, bool> predicate)
{
return _breakdowns.FirstOrDefault(predicate ?? throw new ArgumentNullException(nameof(predicate)));
}
/// <summary>
/// Returns an enumerator that iterates through the breakdown collection.
/// </summary>
/// <returns>An enumerator for the collection.</returns>
public IEnumerator<ISqlBreakdown> GetEnumerator()
{
return _breakdowns.GetEnumerator();
}
/// <summary>
/// Returns an enumerator that iterates through the breakdown collection.
/// </summary>
/// <returns>An enumerator for the collection.</returns>
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
/// <summary>
/// Gets the raw statement at the specified index.
/// </summary>
/// <param name="index">The index of the raw statement.</param>
/// <returns>The raw statement at the specified index.</returns>
/// <exception cref="ArgumentOutOfRangeException">Thrown when index is out of range.</exception>
public string GetRawStatementAt(int index)
{
var rawStatements = RawStatements;
if (index < 0 || index >= rawStatements.Count)
{
throw new ArgumentOutOfRangeException(nameof(index), $"Index {index} is out of range for raw statements collection with {rawStatements.Count} items.");
}
return rawStatements[index];
}
/// <summary>
/// Returns the combined SQL string representation of all breakdowns.
/// </summary>
/// <returns>The combined SQL string.</returns>
public override string ToString()
{
return GetCombinedSql();
}
/// <summary>
/// Splits SQL batch by the specified separator character/string.
/// </summary>
/// <param name="sqlBatch">The SQL batch to split.</param>
/// <param name="separator">The separator to use for splitting (e.g., "GO" or ";").</param>
/// <returns>An array of SQL statements.</returns>
private static string[] SplitBySeparators(string sqlBatch, string separator)
{
var statements = new List<string>();
var currentStatement = new StringBuilder();
using (var reader = new StringReader(sqlBatch))
{
string? line;
while ((line = reader.ReadLine()) != null)
{
// Check if line is a separator statement (case-insensitive for "GO", exact for others, possibly with whitespace)
var trimmedLine = line.Trim();
var isSeparator = separator.Equals("GO", StringComparison.OrdinalIgnoreCase)
? trimmedLine.Equals("GO", StringComparison.OrdinalIgnoreCase)
: trimmedLine.Equals(separator);
if (isSeparator)
{
// Save the current statement if it's not empty
var statement = currentStatement.ToString().Trim();
if (!string.IsNullOrWhiteSpace(statement))
{
statements.Add(statement);
}
currentStatement.Clear();
}
else
{
// Add line to current statement
if (currentStatement.Length > 0)
{
currentStatement.AppendLine();
}
currentStatement.Append(line);
}
}
}
// Add the final statement if it's not empty
var finalStatement = currentStatement.ToString().Trim();
if (!string.IsNullOrWhiteSpace(finalStatement))
{
statements.Add(finalStatement);
}
return statements.ToArray();
}
/// <summary>
/// Gets all unique parameters across all breakdowns in the collection.
/// </summary>
/// <remarks>
/// This aggregates parameters from all QueryBreakdown objects in the collection.
/// Parameters are uniquely identified by name (case-insensitive comparison).
/// </remarks>
/// <returns>A collection of unique parameters from all breakdowns.</returns>
public IEnumerable<(string Name, object? Value)> GetAllUniqueParameters()
{
var parameterDict = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
foreach (var breakdown in _breakdowns.OfType<IQueryBreakdown>())
{
foreach (var param in breakdown.ParameterList)
{
// Add or update parameter (later occurrences override earlier ones)
parameterDict[param.Name] = param.Value;
}
}
return parameterDict.Select(kvp => (kvp.Key, kvp.Value));
}
/// <summary>
/// Gets a dictionary of all unique parameter names and their values across all breakdowns.
/// </summary>
/// <remarks>
/// This is useful for parameterized query execution where you need all parameters in one place.
/// Parameters are uniquely identified by name (case-insensitive comparison).
/// </remarks>
/// <returns>A dictionary mapping parameter names to their values.</returns>
public Dictionary<string, object?> GetParameterDictionary()
{
var parameters = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
foreach (var breakdown in _breakdowns.OfType<IQueryBreakdown>())
{
foreach (var param in breakdown.ParameterList)
{
parameters[param.Name] = param.Value;
}
}
return parameters;
}
/// <summary>
/// Gets parameter names used in queries that have a specific value.
/// </summary>
/// <param name="value">The parameter value to search for.</param>
/// <returns>Parameter names that have the specified value.</returns>
public IEnumerable<string> GetParametersWithValue(object? value)
{
return GetAllUniqueParameters()
.Where(p => (p.Value == null && value == null) ||
(p.Value != null && p.Value.Equals(value)))
.Select(p => p.Name);
}
/// <summary>
/// Checks if a parameter with the specified name exists in any breakdown.
/// </summary>
/// <param name="parameterName">The parameter name to check.</param>
/// <returns>True if the parameter exists; otherwise, false.</returns>
public bool HasParameter(string parameterName)
{
if (string.IsNullOrWhiteSpace(parameterName))
{
return false;
}
return _breakdowns.OfType<IQueryBreakdown>()
.SelectMany(b => b.ParameterList)
.Any(p => p.Name.Equals(parameterName, StringComparison.OrdinalIgnoreCase));
}
/// <summary>
/// Gets the value of a parameter by name from the first breakdown that contains it.
/// </summary>
/// <param name="parameterName">The parameter name to retrieve.</param>
/// <param name="value">The parameter value, or null if not found.</param>
/// <returns>True if the parameter was found; otherwise, false.</returns>
public bool TryGetParameterValue(string parameterName, out object? value)
{
value = null;
if (string.IsNullOrWhiteSpace(parameterName))
{
return false;
}
var param = _breakdowns.OfType<IQueryBreakdown>()
.SelectMany(b => b.ParameterList)
.FirstOrDefault(p => p.Name.Equals(parameterName, StringComparison.OrdinalIgnoreCase));
if (param != null)
{
value = param.Value;
return true;
}
return false;
}
/// <summary>
/// Gets the count of unique parameters across all breakdowns.
/// </summary>
/// <returns>The number of unique parameters.</returns>
public int GetParameterCount()
{
return GetAllUniqueParameters().Count();
}
/// <summary>
/// Gets all parameter names used in the collection (case-insensitive unique list).
/// </summary>
/// <returns>An enumerable of unique parameter names.</returns>
public IEnumerable<string> GetParameterNames()
{
return GetAllUniqueParameters().Select(p => p.Name).Distinct(StringComparer.OrdinalIgnoreCase);
}
}