Files
sql-utilities/src/Strata.SqlTools.SqlBreakdown/Classes/SqlBreakdownCollection.cs
T
Thom Lamb 2d9148547f
SonarQube Analysis / sonarqube (pull_request) Successful in 4m42s
fix(serialization): Remove legacy BinaryFormatter support
The `[Serializable]` attribute and corresponding `[OnDeserialized]` methods have been removed from various breakdown classes. This eliminates reliance on `BinaryFormatter`, which is a deprecated and insecure serialization mechanism in modern .NET.

This change also resolves SonarQube rule S5766 warnings by removing the context in which they apply, leading to cleaner and more secure code.
2026-05-20 17:44:21 -05:00

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