chore: initial git load of code space

This commit is contained in:
Thom Lamb
2026-05-12 08:52:33 -05:00
parent 9abada692f
commit 5e467bcc9c
384 changed files with 65960 additions and 2 deletions
@@ -0,0 +1,698 @@
using System.Text;
using Strata.SqlTools.SqlBreakdown.Classes;
using Strata.SqlTools.SqlBreakdown.Interfaces;
namespace Strata.SqlTools.Breakdowns.SqlServer;
/// <summary>
/// SQL Server-specific collection for managing multiple QueryBreakdown objects.
/// </summary>
/// <remarks>
/// This class extends SqlBreakdownCollection with SQL Server-specific functionality,
/// including support for T-SQL features like batches (GO), temporary tables, stored procedures, and CTEs.
/// </remarks>
[Serializable]
public class QueryBreakdownCollection : SqlBreakdownCollection
{
private readonly List<QueryBreakdown> _queryBreakdowns;
/// <summary>
/// Initializes a new instance of the <see cref="QueryBreakdownCollection"/> class.
/// </summary>
public QueryBreakdownCollection() : base()
{
_queryBreakdowns = new List<QueryBreakdown>();
}
/// <summary>
/// Initializes a new instance of the <see cref="QueryBreakdownCollection"/> class with initial query breakdowns.
/// </summary>
/// <param name="queryBreakdowns">The initial collection of query breakdowns.</param>
public QueryBreakdownCollection(IEnumerable<QueryBreakdown> queryBreakdowns) : base(queryBreakdowns?.Cast<ISqlBreakdown>() ?? Enumerable.Empty<ISqlBreakdown>())
{
_queryBreakdowns = new List<QueryBreakdown>(queryBreakdowns ?? Enumerable.Empty<QueryBreakdown>());
}
/// <summary>
/// Gets the collection of QueryBreakdown objects.
/// </summary>
public IReadOnlyList<QueryBreakdown> QueryBreakdowns => _queryBreakdowns.AsReadOnly();
/// <summary>
/// Adds a QueryBreakdown to the collection.
/// </summary>
/// <param name="queryBreakdown">The query breakdown to add.</param>
/// <exception cref="ArgumentNullException">Thrown when queryBreakdown is null.</exception>
public void Add(QueryBreakdown queryBreakdown)
{
if (queryBreakdown == null)
{
throw new ArgumentNullException(nameof(queryBreakdown));
}
_queryBreakdowns.Add(queryBreakdown);
base.Add(queryBreakdown);
}
/// <summary>
/// Adds multiple QueryBreakdowns to the collection.
/// </summary>
/// <param name="queryBreakdowns">The query breakdowns to add.</param>
/// <exception cref="ArgumentNullException">Thrown when queryBreakdowns is null.</exception>
public void AddRange(IEnumerable<QueryBreakdown> queryBreakdowns)
{
if (queryBreakdowns == null)
{
throw new ArgumentNullException(nameof(queryBreakdowns));
}
foreach (var breakdown in queryBreakdowns)
{
Add(breakdown);
}
}
/// <summary>
/// Removes a QueryBreakdown from the collection.
/// </summary>
/// <param name="queryBreakdown">The query breakdown to remove.</param>
/// <returns>True if removed; otherwise, false.</returns>
public bool Remove(QueryBreakdown queryBreakdown)
{
var removed = _queryBreakdowns.Remove(queryBreakdown);
if (removed)
{
base.Remove(queryBreakdown);
}
return removed;
}
/// <summary>
/// Clears all query breakdowns from the collection.
/// </summary>
public new void Clear()
{
_queryBreakdowns.Clear();
base.Clear();
}
/// <summary>
/// Gets the SQL Server T-SQL batch representation with proper batch handling.
/// </summary>
/// <remarks>
/// Generates T-SQL with proper GO separators and optional transaction support.
/// </remarks>
/// <param name="includeSetupFinish">Whether to include setup and finish clauses.</param>
/// <param name="includeTransaction">Whether to wrap in BEGIN TRANSACTION / COMMIT.</param>
/// <returns>The formatted T-SQL batch.</returns>
public string GetSqlServerBatch(bool includeSetupFinish = true, bool includeTransaction = false)
{
var sb = new StringBuilder();
// Add transaction wrapper if requested
if (includeTransaction)
{
sb.AppendLine("BEGIN TRANSACTION;");
sb.AppendLine();
}
// Add all queries with GO separators
if (_queryBreakdowns.Count > 0)
{
for (int i = 0; i < _queryBreakdowns.Count; i++)
{
var query = _queryBreakdowns[i];
sb.Append(query.GetSql(includeSetupFinish));
// Add GO separator between queries (not after last)
if (i < _queryBreakdowns.Count - 1)
{
sb.AppendLine();
sb.AppendLine("GO");
sb.AppendLine();
}
}
}
// Close transaction if opened
if (includeTransaction)
{
sb.AppendLine();
sb.AppendLine("COMMIT TRANSACTION;");
}
return sb.ToString();
}
/// <summary>
/// Filters query breakdowns where the SELECT clause contains specific text.
/// </summary>
/// <param name="selectContains">The text to find in the SELECT clause.</param>
/// <returns>Filtered query breakdowns.</returns>
public IEnumerable<QueryBreakdown> WhereSelectContains(string selectContains)
{
if (string.IsNullOrWhiteSpace(selectContains))
{
throw new ArgumentNullException(nameof(selectContains));
}
return _queryBreakdowns.Where(q =>
q.SelectClause?.Clause?.Contains(selectContains, StringComparison.OrdinalIgnoreCase) ?? false);
}
/// <summary>
/// Filters query breakdowns where the FROM clause contains specific text.
/// </summary>
/// <param name="tableNameContains">The table name or pattern to find.</param>
/// <returns>Filtered query breakdowns.</returns>
public IEnumerable<QueryBreakdown> WhereTableContains(string tableNameContains)
{
if (string.IsNullOrWhiteSpace(tableNameContains))
{
throw new ArgumentNullException(nameof(tableNameContains));
}
return _queryBreakdowns.Where(q =>
q.FromClause?.Clause?.Contains(tableNameContains, StringComparison.OrdinalIgnoreCase) ?? false);
}
/// <summary>
/// Filters query breakdowns that have a WHERE clause.
/// </summary>
/// <returns>Query breakdowns with WHERE clauses.</returns>
public IEnumerable<QueryBreakdown> WhereHaveWhereClause()
{
return _queryBreakdowns.Where(q =>
!string.IsNullOrWhiteSpace(q.WhereClause?.Clause));
}
/// <summary>
/// Filters query breakdowns that do NOT have a WHERE clause.
/// </summary>
/// <remarks>
/// This is useful for identifying potentially risky queries that affect all rows.
/// </remarks>
/// <returns>Query breakdowns without WHERE clauses.</returns>
public IEnumerable<QueryBreakdown> WhereHaveNoWhereClause()
{
return _queryBreakdowns.Where(q =>
string.IsNullOrWhiteSpace(q.WhereClause?.Clause));
}
/// <summary>
/// Filters query breakdowns that have a GROUP BY clause.
/// </summary>
/// <returns>Query breakdowns with GROUP BY clauses.</returns>
public IEnumerable<QueryBreakdown> WhereHaveGroupByClause()
{
return _queryBreakdowns.Where(q =>
!string.IsNullOrWhiteSpace(q.GroupByClause?.Clause));
}
/// <summary>
/// Filters query breakdowns that have an ORDER BY clause.
/// </summary>
/// <returns>Query breakdowns with ORDER BY clauses.</returns>
public IEnumerable<QueryBreakdown> WhereHaveOrderByClause()
{
return _queryBreakdowns.Where(q =>
!string.IsNullOrWhiteSpace(q.OrderByClause?.Clause));
}
/// <summary>
/// Filters query breakdowns that have WITH clauses (CTEs).
/// </summary>
/// <returns>Query breakdowns with CTE definitions.</returns>
public IEnumerable<QueryBreakdown> WhereHaveCommonTableExpressions()
{
return _queryBreakdowns.Where(q => q.WithClauses.Count > 0);
}
/// <summary>
/// Filters query breakdowns that reference JOIN operations.
/// </summary>
/// <returns>Query breakdowns with JOINs.</returns>
public IEnumerable<QueryBreakdown> WhereHaveJoins()
{
return _queryBreakdowns.Where(q => q.GetSql().Contains("JOIN", StringComparison.OrdinalIgnoreCase));
}
/// <summary>
/// Filters query breakdowns by parameter usage.
/// </summary>
/// <param name="parameterName">The parameter name to search for.</param>
/// <returns>Query breakdowns using the specified parameter.</returns>
public IEnumerable<QueryBreakdown> WhereUseParameter(string parameterName)
{
if (string.IsNullOrWhiteSpace(parameterName))
{
throw new ArgumentNullException(nameof(parameterName));
}
return _queryBreakdowns.Where(q =>
q.ParameterList.Any(p => p.Name == parameterName));
}
/// <summary>
/// Gets the total number of columns selected across all queries.
/// </summary>
/// <returns>Total column count.</returns>
public int GetTotalSelectedColumns()
{
return _queryBreakdowns.Sum(q =>
!string.IsNullOrWhiteSpace(q.SelectClause?.Clause)
? q.SelectClause.Clause.Split(',').Length
: 0);
}
/// <summary>
/// Gets all unique table names referenced across all queries.
/// </summary>
/// <remarks>
/// This provides a quick overview of which tables are being queried.
/// Note: This is a best-effort extraction and may not capture all table references,
/// especially in complex subqueries or with aliasing.
/// </remarks>
/// <returns>List of unique table names.</returns>
public IEnumerable<string> GetUniqueTableReferences()
{
var tables = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var tableNames = _queryBreakdowns
.Where(q => !string.IsNullOrWhiteSpace(q.FromClause?.Clause))
.SelectMany(q => ExtractTableNames(q.FromClause!.Clause!));
foreach (var table in tableNames)
{
tables.Add(table);
}
return tables;
}
/// <summary>
/// Gets a summary of all queries including their types and basic composition.
/// </summary>
/// <returns>Summary information for each query.</returns>
public IEnumerable<QuerySummary> GetQuerySummaries()
{
return _queryBreakdowns.Select((q, index) => new QuerySummary
{
Index = index,
HasSelectClause = !string.IsNullOrWhiteSpace(q.SelectClause?.Clause),
HasFromClause = !string.IsNullOrWhiteSpace(q.FromClause?.Clause),
HasWhereClause = !string.IsNullOrWhiteSpace(q.WhereClause?.Clause),
HasGroupByClause = !string.IsNullOrWhiteSpace(q.GroupByClause?.Clause),
HasHavingClause = !string.IsNullOrWhiteSpace(q.HavingClause?.Clause),
HasOrderByClause = !string.IsNullOrWhiteSpace(q.OrderByClause?.Clause),
HasJoins = false,
HasCTE = q.WithClauses.Count > 0,
ColumnCount = !string.IsNullOrWhiteSpace(q.SelectClause?.Clause) ? q.SelectClause.Clause.Split(',').Length : 0,
ParameterCount = q.ParameterList.Count(),
JoinCount = 0
});
}
/// <summary>
/// Helper method to extract table names from a FROM clause.
/// </summary>
private static IEnumerable<string> ExtractTableNames(string fromClause)
{
if (string.IsNullOrWhiteSpace(fromClause))
{
yield break;
}
// Simple extraction: split by comma and clean up aliases
var parts = fromClause.Split(',');
foreach (var part in parts)
{
var trimmed = part.Trim();
// Remove alias (assuming format: table AS alias or table alias)
var tokens = trimmed.Split(new[] { " AS ", " " }, StringSplitOptions.RemoveEmptyEntries);
if (tokens.Length > 0)
{
var tableName = tokens[0].Trim();
if (!string.IsNullOrWhiteSpace(tableName))
{
yield return tableName;
}
}
}
}
/// <summary>
/// Synchronizes parameters across all queries in the collection.
/// </summary>
/// <remarks>
/// This ensures all queries share the same parameter values based on parameter name.
/// Later parameter values override earlier ones if there are conflicts.
/// Only synchronizes parameters that the query already defines to avoid adding unused parameters.
/// </remarks>
public void SynchronizeParameters()
{
// Get all unique parameter names across all queries
var allParameterNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var query in _queryBreakdowns)
{
foreach (var paramName in query.Parameters.Keys)
{
allParameterNames.Add(paramName);
}
}
// For each parameter, use the last query's value and sync to all queries that have it
foreach (var paramName in allParameterNames)
{
object? lastValue = null;
bool parameterFound = false;
// Find the last query that has this parameter and get its value
for (int i = _queryBreakdowns.Count - 1; i >= 0; i--)
{
if (_queryBreakdowns[i].Parameters.ContainsKey(paramName))
{
lastValue = _queryBreakdowns[i].Parameters[paramName];
parameterFound = true;
break;
}
}
// Synchronize the parameter value to all queries that have it
if (parameterFound)
{
foreach (var query in _queryBreakdowns.Where(q => q.Parameters.ContainsKey(paramName)))
{
query.Parameters[paramName] = lastValue!;
}
}
}
}
/// <summary>
/// Adds a parameter to all queries in the collection.
/// </summary>
/// <param name="parameterName">The parameter name.</param>
/// <param name="value">The parameter value.</param>
public void AddParameterToAll(string parameterName, object? value)
{
if (string.IsNullOrWhiteSpace(parameterName))
{
throw new ArgumentNullException(nameof(parameterName));
}
foreach (var query in _queryBreakdowns)
{
query.Parameters[parameterName] = value!;
}
}
/// <summary>
/// Gets all unique parameters from all queries in the collection as a combined dictionary.
/// </summary>
/// <returns>A dictionary containing all unique parameters across all queries.</returns>
protected Dictionary<string, object> GetCombinedParameterDictionary()
{
var combinedParams = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
foreach (var query in _queryBreakdowns)
{
// Add from ParameterList (parsed parameters)
foreach (var param in query.ParameterList)
{
combinedParams[param.Name] = param.Value;
}
// Add/override from Parameters dictionary (manually added parameters)
foreach (var param in query.Parameters)
{
combinedParams[param.Key] = param.Value;
}
}
return combinedParams;
}
/// <summary>
/// Gets all unique parameters from all queries in the collection.
/// </summary>
/// <returns>A collection of unique QueryParam objects.</returns>
/// <summary>
/// Gets all T-SQL parameters as a formatted string suitable for SQL Server.
/// </summary>
/// <param name="includeDataTypes">Whether to include estimated data types (uses generic approach).</param>
/// <returns>A formatted string of parameters.</returns>
public string GetParametersAsString(bool includeDataTypes = false)
{
var parameters = GetCombinedParameterDictionary();
if (parameters.Count == 0)
{
return string.Empty;
}
var sb = new StringBuilder();
bool first = true;
foreach (var param in parameters)
{
if (!first)
{
sb.Append(", ");
}
sb.Append($"@{param.Key}");
if (includeDataTypes)
{
var dataType = GetSqlDataType(param.Value);
sb.Append($" {dataType}");
}
sb.Append($" = {FormatParameterValue(param.Value)}");
first = false;
}
return sb.ToString();
}
/// <summary>
/// Gets a report of parameter usage across all queries.
/// </summary>
/// <returns>Parameter usage information.</returns>
public IEnumerable<ParameterUsageReport> GetParameterUsageReport()
{
// Collect all unique parameter names from both ParameterList and Parameters dictionary
var allParamNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var query in _queryBreakdowns)
{
// Add from ParameterList (parsed parameters)
foreach (var param in query.ParameterList)
{
allParamNames.Add(param.Name);
}
// Add from Parameters dictionary (manually added parameters)
foreach (var paramName in query.Parameters.Keys)
{
allParamNames.Add(paramName);
}
}
foreach (var paramName in allParamNames)
{
var queriesUsing = 0;
object? lastValue = null;
foreach (var query in _queryBreakdowns)
{
// Check ParameterList first (parsed)
var param = query.ParameterList.FirstOrDefault(p => p.Name.Equals(paramName, StringComparison.OrdinalIgnoreCase));
if (param != null)
{
queriesUsing++;
lastValue = param.Value;
}
// Also check Parameters dictionary (manually added)
else if (query.Parameters.TryGetValue(paramName, out var dictValue))
{
queriesUsing++;
lastValue = dictValue;
}
}
yield return new ParameterUsageReport
{
ParameterName = paramName,
Value = lastValue,
UsedInQueryCount = queriesUsing,
TotalQueries = _queryBreakdowns.Count
};
}
}
/// <summary>
/// Helper method to get SQL Server data type from a .NET object.
/// </summary>
private static string GetSqlDataType(object? value)
{
return value switch
{
null => "SQL_VARIANT",
bool => "BIT",
byte => "TINYINT",
short => "SMALLINT",
int => "INT",
long => "BIGINT",
float => "REAL",
double => "FLOAT",
decimal => "DECIMAL(18, 2)",
string => "NVARCHAR(MAX)",
DateTime => "DATETIME2",
_ => "SQL_VARIANT"
};
}
/// <summary>
/// Helper method to format a parameter value for SQL output.
/// </summary>
private static string FormatParameterValue(object? value)
{
return value switch
{
null => "NULL",
bool b => b ? "1" : "0",
string s => $"'{s.Replace("'", "''")}'",
DateTime dt => $"'{dt:yyyy-MM-dd HH:mm:ss}'",
byte or short or int or long or float or double or decimal => value.ToString() ?? "NULL",
_ => throw new ArgumentException($"Unsupported parameter type: {value.GetType().Name}. Only primitive types, strings, and DateTime are supported.")
};
}
}
/// <summary>
/// Summary information about a query for quick analysis.
/// </summary>
public class QuerySummary
{
/// <summary>
/// Gets or sets the index of the query in the collection.
/// </summary>
public int Index { get; set; }
/// <summary>
/// Gets or sets whether the query has a SELECT clause.
/// </summary>
public bool HasSelectClause { get; set; }
/// <summary>
/// Gets or sets whether the query has a FROM clause.
/// </summary>
public bool HasFromClause { get; set; }
/// <summary>
/// Gets or sets whether the query has a WHERE clause.
/// </summary>
public bool HasWhereClause { get; set; }
/// <summary>
/// Gets or sets whether the query has a GROUP BY clause.
/// </summary>
public bool HasGroupByClause { get; set; }
/// <summary>
/// Gets or sets whether the query has a HAVING clause.
/// </summary>
public bool HasHavingClause { get; set; }
/// <summary>
/// Gets or sets whether the query has an ORDER BY clause.
/// </summary>
public bool HasOrderByClause { get; set; }
/// <summary>
/// Gets or sets whether the query has JOIN clauses.
/// </summary>
public bool HasJoins { get; set; }
/// <summary>
/// Gets or sets whether the query has Common Table Expressions (CTEs).
/// </summary>
public bool HasCTE { get; set; }
/// <summary>
/// Gets or sets the number of columns in the SELECT clause.
/// </summary>
public int ColumnCount { get; set; }
/// <summary>
/// Gets or sets the number of parameters used.
/// </summary>
public int ParameterCount { get; set; }
/// <summary>
/// Gets or sets the number of JOIN clauses.
/// </summary>
public int JoinCount { get; set; }
/// <summary>
/// Returns a string representation of the query summary.
/// </summary>
public override string ToString()
{
var sb = new StringBuilder();
sb.AppendLine($"Query #{Index}");
sb.AppendLine($" SELECT: {(HasSelectClause ? "Yes" : "No")} ({ColumnCount} columns)");
sb.AppendLine($" FROM: {(HasFromClause ? "Yes" : "No")}");
sb.AppendLine($" WHERE: {(HasWhereClause ? "Yes" : "No")}");
sb.AppendLine($" GROUP BY: {(HasGroupByClause ? "Yes" : "No")}");
sb.AppendLine($" HAVING: {(HasHavingClause ? "Yes" : "No")}");
sb.AppendLine($" ORDER BY: {(HasOrderByClause ? "Yes" : "No")}");
sb.AppendLine($" JOINs: {(HasJoins ? "Yes" : "No")} ({JoinCount} joins)");
sb.AppendLine($" CTEs: {(HasCTE ? "Yes" : "No")}");
sb.Append($" Parameters: {ParameterCount}");
return sb.ToString();
}
}
/// <summary>
/// Report of parameter usage across queries in a collection.
/// </summary>
public class ParameterUsageReport
{
/// <summary>
/// Gets or sets the parameter name.
/// </summary>
public string ParameterName { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the parameter value.
/// </summary>
public object? Value { get; set; }
/// <summary>
/// Gets or sets the number of queries using this parameter.
/// </summary>
public int UsedInQueryCount { get; set; }
/// <summary>
/// Gets or sets the total number of queries in the collection.
/// </summary>
public int TotalQueries { get; set; }
/// <summary>
/// Gets whether the parameter is used in all queries.
/// </summary>
public bool IsUsedInAllQueries => UsedInQueryCount == TotalQueries;
/// <summary>
/// Returns a string representation of the parameter usage report.
/// </summary>
public override string ToString()
{
var usagePercentage = TotalQueries > 0 ? (UsedInQueryCount / (decimal)TotalQueries * 100) : 0;
return $"@{ParameterName}: {UsedInQueryCount}/{TotalQueries} queries ({usagePercentage:F1}%) - Value: {Value?.ToString() ?? "NULL"}";
}
}