using System.Text;
using Strata.SqlTools.SqlBreakdown.Interfaces;
using Strata.SqlTools.SqlBreakdown.Interfaces.QueryEngine;
namespace Strata.SqlTools.SqlBreakdown.Classes;
///
/// Manages a collection of multiple SQL breakdown objects and provides parsing for batch SQL statements.
///
///
/// 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.
///
public class SqlBreakdownCollection : ICollection
{
private readonly List _breakdowns;
private string _separator = "GO";
///
/// Initializes a new instance of the class.
///
public SqlBreakdownCollection()
{
_breakdowns = new List();
}
///
/// Initializes a new instance of the class with initial breakdowns.
///
/// The initial collection of SQL breakdowns.
public SqlBreakdownCollection(IEnumerable breakdowns)
{
_breakdowns = new List(breakdowns ?? Enumerable.Empty());
}
///
/// Gets the collection of SQL breakdowns.
///
public IReadOnlyList Breakdowns => _breakdowns.AsReadOnly();
///
/// Gets the collection of raw SQL statements from all breakdowns.
///
public IReadOnlyList RawStatements => _breakdowns
.Where(b => !string.IsNullOrWhiteSpace(b.RawSql))
.Select(b => b.RawSql!)
.ToList()
.AsReadOnly();
///
/// Gets the count of SQL breakdowns in the collection.
///
public int Count => _breakdowns.Count;
///
/// Gets a value indicating whether the collection is empty.
///
public bool IsEmpty => _breakdowns.Count == 0;
///
/// Gets a value indicating whether the collection is read-only.
///
///
/// This collection is not read-only; items can be added and removed.
///
public bool IsReadOnly => false;
///
/// 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.
///
///
/// 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.
///
public string Separator
{
get => _separator;
set => _separator = string.IsNullOrWhiteSpace(value) ? "GO" : value;
}
///
/// Adds a single SQL breakdown to the collection.
///
/// The breakdown to add.
/// Thrown when breakdown is null.
public void Add(ISqlBreakdown breakdown)
{
if (breakdown == null)
{
throw new ArgumentNullException(nameof(breakdown));
}
_breakdowns.Add(breakdown);
}
///
/// Adds multiple SQL breakdowns to the collection.
///
/// The breakdowns to add.
/// Thrown when breakdowns is null.
public void AddRange(IEnumerable breakdowns)
{
if (breakdowns == null)
{
throw new ArgumentNullException(nameof(breakdowns));
}
_breakdowns.AddRange(breakdowns);
}
///
/// Removes a SQL breakdown from the collection.
///
/// The breakdown to remove.
/// True if the breakdown was removed; otherwise, false.
public bool Remove(ISqlBreakdown breakdown)
{
return _breakdowns.Remove(breakdown);
}
///
/// Removes all SQL breakdowns from the collection.
///
public void Clear()
{
_breakdowns.Clear();
}
///
/// Parses a batch SQL statement containing multiple queries and populates the collection.
///
///
/// 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.
///
/// The batch SQL statement to parse.
/// Optional separator to use for splitting. If null, uses the Separator property.
/// Thrown when sqlBatch is null.
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);
}
}
}
///
/// Gets the combined SQL from all breakdowns in the collection.
///
/// Whether to include setup and finish clauses for each breakdown.
/// The separator to use between SQL statements. If null, uses the Separator property.
/// The combined SQL string from all breakdowns.
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();
}
///
/// Gets the raw SQL statements as a batch (joined with the configured separator).
///
/// The separator to use between SQL statements. If null, uses the Separator property.
/// The combined raw SQL statements.
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);
}
///
/// Determines whether the collection contains a specific breakdown.
///
/// The breakdown to locate.
/// True if the breakdown is found; otherwise, false.
public bool Contains(ISqlBreakdown item)
{
return _breakdowns.Contains(item);
}
///
/// Copies the elements of the collection to an array, starting at a particular array index.
///
/// The destination array.
/// The zero-based index in the array at which copying begins.
/// Thrown when array is null.
/// Thrown when arrayIndex is out of range.
/// Thrown when there is not enough space in the array.
public void CopyTo(ISqlBreakdown[] array, int arrayIndex)
{
_breakdowns.CopyTo(array, arrayIndex);
}
///
/// Gets a breakdown at the specified index.
///
/// The index of the breakdown.
/// The breakdown at the specified index.
/// Thrown when index is out of range.
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];
}
///
/// Gets the first breakdown matching the given predicate.
///
/// The predicate to match.
/// The first matching breakdown, or null if not found.
public ISqlBreakdown? FirstOrDefault(Func predicate)
{
return _breakdowns.FirstOrDefault(predicate ?? throw new ArgumentNullException(nameof(predicate)));
}
///
/// Returns an enumerator that iterates through the breakdown collection.
///
/// An enumerator for the collection.
public IEnumerator GetEnumerator()
{
return _breakdowns.GetEnumerator();
}
///
/// Returns an enumerator that iterates through the breakdown collection.
///
/// An enumerator for the collection.
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
///
/// Gets the raw statement at the specified index.
///
/// The index of the raw statement.
/// The raw statement at the specified index.
/// Thrown when index is out of range.
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];
}
///
/// Returns the combined SQL string representation of all breakdowns.
///
/// The combined SQL string.
public override string ToString()
{
return GetCombinedSql();
}
///
/// Splits SQL batch by the specified separator character/string.
///
/// The SQL batch to split.
/// The separator to use for splitting (e.g., "GO" or ";").
/// An array of SQL statements.
private static string[] SplitBySeparators(string sqlBatch, string separator)
{
var statements = new List();
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();
}
///
/// Gets all unique parameters across all breakdowns in the collection.
///
///
/// This aggregates parameters from all QueryBreakdown objects in the collection.
/// Parameters are uniquely identified by name (case-insensitive comparison).
///
/// A collection of unique parameters from all breakdowns.
public IEnumerable<(string Name, object? Value)> GetAllUniqueParameters()
{
var parameterDict = new Dictionary(StringComparer.OrdinalIgnoreCase);
foreach (var breakdown in _breakdowns.OfType())
{
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));
}
///
/// Gets a dictionary of all unique parameter names and their values across all breakdowns.
///
///
/// This is useful for parameterized query execution where you need all parameters in one place.
/// Parameters are uniquely identified by name (case-insensitive comparison).
///
/// A dictionary mapping parameter names to their values.
public Dictionary GetParameterDictionary()
{
var parameters = new Dictionary(StringComparer.OrdinalIgnoreCase);
foreach (var breakdown in _breakdowns.OfType())
{
foreach (var param in breakdown.ParameterList)
{
parameters[param.Name] = param.Value;
}
}
return parameters;
}
///
/// Gets parameter names used in queries that have a specific value.
///
/// The parameter value to search for.
/// Parameter names that have the specified value.
public IEnumerable GetParametersWithValue(object? value)
{
return GetAllUniqueParameters()
.Where(p => (p.Value == null && value == null) ||
(p.Value != null && p.Value.Equals(value)))
.Select(p => p.Name);
}
///
/// Checks if a parameter with the specified name exists in any breakdown.
///
/// The parameter name to check.
/// True if the parameter exists; otherwise, false.
public bool HasParameter(string parameterName)
{
if (string.IsNullOrWhiteSpace(parameterName))
{
return false;
}
return _breakdowns.OfType()
.SelectMany(b => b.ParameterList)
.Any(p => p.Name.Equals(parameterName, StringComparison.OrdinalIgnoreCase));
}
///
/// Gets the value of a parameter by name from the first breakdown that contains it.
///
/// The parameter name to retrieve.
/// The parameter value, or null if not found.
/// True if the parameter was found; otherwise, false.
public bool TryGetParameterValue(string parameterName, out object? value)
{
value = null;
if (string.IsNullOrWhiteSpace(parameterName))
{
return false;
}
var param = _breakdowns.OfType()
.SelectMany(b => b.ParameterList)
.FirstOrDefault(p => p.Name.Equals(parameterName, StringComparison.OrdinalIgnoreCase));
if (param != null)
{
value = param.Value;
return true;
}
return false;
}
///
/// Gets the count of unique parameters across all breakdowns.
///
/// The number of unique parameters.
public int GetParameterCount()
{
return GetAllUniqueParameters().Count();
}
///
/// Gets all parameter names used in the collection (case-insensitive unique list).
///
/// An enumerable of unique parameter names.
public IEnumerable GetParameterNames()
{
return GetAllUniqueParameters().Select(p => p.Name).Distinct(StringComparer.OrdinalIgnoreCase);
}
}