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); } }