chore: initial git load of code space
This commit is contained in:
@@ -0,0 +1,219 @@
|
||||
using System.Collections;
|
||||
using System.Text;
|
||||
using Strata.SqlTools.SqlBreakdown.Classes;
|
||||
using Strata.SqlTools.Statements.SqlServer;
|
||||
|
||||
namespace Strata.SqlTools.Breakdowns.SqlServer;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a DELETE SQL statement breakdown with FROM and WHERE clauses for SQL Server.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class DeleteBreakdown : SqlBreakdownBase
|
||||
{
|
||||
protected readonly StatementParser Parser;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DeleteBreakdown"/> class.
|
||||
/// </summary>
|
||||
public DeleteBreakdown()
|
||||
{
|
||||
Parser = new StatementParser();
|
||||
FromClause = new SqlClause();
|
||||
WhereClause = new SqlClause();
|
||||
DeleteClause = new SqlClause();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DeleteBreakdown"/> class.
|
||||
/// </summary>
|
||||
/// <param name="fromClause">The FROM clause.</param>
|
||||
/// <param name="whereClause">The WHERE clause.</param>
|
||||
public DeleteBreakdown(string fromClause, string whereClause) : this()
|
||||
{
|
||||
var cleanFrom = Parser.ExtractSqlComments(fromClause, out var fromComments);
|
||||
FromClause.Clause = cleanFrom.Trim();
|
||||
FromClause.Comment = fromComments.Count > 0 ? string.Join(" ", fromComments) : null;
|
||||
|
||||
var cleanWhere = Parser.ExtractSqlComments(whereClause, out var whereComments);
|
||||
WhereClause.Clause = cleanWhere.Trim();
|
||||
WhereClause.Comment = whereComments.Count > 0 ? string.Join(" ", whereComments) : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the FROM clause.
|
||||
/// </summary>
|
||||
public SqlClause FromClause { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the DELETE clause (optional, for DELETE with alias).
|
||||
/// </summary>
|
||||
public SqlClause DeleteClause { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether a WHERE clause is being used.
|
||||
/// </summary>
|
||||
public bool IsUsingWhereClause => !string.IsNullOrWhiteSpace(WhereClause.Clause);
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the WHERE clause.
|
||||
/// </summary>
|
||||
public SqlClause WhereClause { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the SQL breakdown as a string.
|
||||
/// </summary>
|
||||
/// <returns>The DELETE SQL statement.</returns>
|
||||
protected override string GetSqlBreakdown()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
sb.AppendLine("DELETE ");
|
||||
if (!string.IsNullOrWhiteSpace(DeleteClause.Clause))
|
||||
{
|
||||
sb.AppendLine($" {DeleteClause.Clause}");
|
||||
}
|
||||
|
||||
sb.AppendLine("FROM ");
|
||||
sb.AppendLine($" {FromClause.Clause}");
|
||||
|
||||
if (IsUsingWhereClause)
|
||||
{
|
||||
sb.AppendLine("WHERE ");
|
||||
sb.AppendLine($" {WhereClause.Clause}");
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
#region Parse Methods
|
||||
|
||||
/// <summary>
|
||||
/// Parses a DELETE SQL statement into a DeleteBreakdown object.
|
||||
/// </summary>
|
||||
/// <param name="sql">The DELETE SQL statement to parse.</param>
|
||||
/// <returns>A DeleteBreakdown object representing the parsed statement.</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown when sql is null or empty.</exception>
|
||||
/// <exception cref="FormatException">Thrown when the SQL statement cannot be parsed.</exception>
|
||||
public static DeleteBreakdown Parse(string sql)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(sql))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(sql), "SQL statement cannot be null or empty.");
|
||||
}
|
||||
|
||||
if (!TryParse(sql, out var result, out var error))
|
||||
{
|
||||
throw new FormatException($"Failed to parse DELETE statement: {error}");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to parse a DELETE SQL statement into a DeleteBreakdown object.
|
||||
/// </summary>
|
||||
/// <param name="sql">The DELETE SQL statement to parse.</param>
|
||||
/// <param name="result">When this method returns, contains the parsed DeleteBreakdown if successful, or null if parsing failed.</param>
|
||||
/// <returns>true if the SQL was successfully parsed; otherwise, false.</returns>
|
||||
public static bool TryParse(string sql, out DeleteBreakdown result)
|
||||
=> TryParse(sql, out result, out _);
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to parse a DELETE SQL statement into a DeleteBreakdown object.
|
||||
/// </summary>
|
||||
/// <param name="sql">The DELETE SQL statement to parse.</param>
|
||||
/// <param name="result">When this method returns, contains the parsed DeleteBreakdown if successful, or null if parsing failed.</param>
|
||||
/// <param name="errorMessage">When this method returns false, contains a message describing why parsing failed.</param>
|
||||
/// <returns>true if the SQL was successfully parsed; otherwise, false.</returns>
|
||||
public static bool TryParse(string sql, out DeleteBreakdown result, out string errorMessage)
|
||||
{
|
||||
result = null!;
|
||||
errorMessage = null!;
|
||||
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(sql))
|
||||
{
|
||||
errorMessage = "SQL statement cannot be null or empty.";
|
||||
return false;
|
||||
}
|
||||
|
||||
var parser = new StatementParser();
|
||||
sql = parser.NormalizeSqlPreservingComments(sql);
|
||||
|
||||
// Check if it's a DELETE statement
|
||||
var sqlTrimmed = sql.TrimStart();
|
||||
if (!System.Text.RegularExpressions.Regex.IsMatch(sqlTrimmed, @"^\s*DELETE\b",
|
||||
System.Text.RegularExpressions.RegexOptions.IgnoreCase))
|
||||
{
|
||||
errorMessage = "SQL statement must start with DELETE.";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Extract setup and finish clauses
|
||||
var setupClauses = new List<string>();
|
||||
sql = parser.ExtractSetupClauses(sql, setupClauses);
|
||||
|
||||
var finishClauses = new ArrayList();
|
||||
sql = parser.ExtractFinishClauses(sql, finishClauses);
|
||||
|
||||
// Parse DELETE statement using regex
|
||||
// Pattern: DELETE [table_alias] FROM table WHERE condition
|
||||
var deleteMatch = System.Text.RegularExpressions.Regex.Match(sql,
|
||||
@"DELETE\s+(.*?)\s+FROM\s+(.*?)(?:\s+WHERE\s+(.*))?$",
|
||||
System.Text.RegularExpressions.RegexOptions.IgnoreCase | System.Text.RegularExpressions.RegexOptions.Singleline);
|
||||
|
||||
if (!deleteMatch.Success)
|
||||
{
|
||||
// Try simpler pattern: DELETE FROM table WHERE condition
|
||||
deleteMatch = System.Text.RegularExpressions.Regex.Match(sql,
|
||||
@"DELETE\s+FROM\s+(.*?)(?:\s+WHERE\s+(.*))?$",
|
||||
System.Text.RegularExpressions.RegexOptions.IgnoreCase | System.Text.RegularExpressions.RegexOptions.Singleline);
|
||||
|
||||
if (!deleteMatch.Success)
|
||||
{
|
||||
errorMessage = "Could not parse DELETE statement. Expected format: DELETE [alias] FROM table [WHERE condition]";
|
||||
return false;
|
||||
}
|
||||
|
||||
var fromClause = deleteMatch.Groups[1].Value.Trim();
|
||||
var whereClause = deleteMatch.Groups.Count > 2 ? deleteMatch.Groups[2].Value.Trim() : string.Empty;
|
||||
|
||||
result = new DeleteBreakdown(fromClause, whereClause)
|
||||
{
|
||||
SetupClauses = setupClauses,
|
||||
FinishClauses = finishClauses
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
var deleteClause = deleteMatch.Groups[1].Value.Trim();
|
||||
var fromClause = deleteMatch.Groups[2].Value.Trim();
|
||||
var whereClause = deleteMatch.Groups.Count > 3 ? deleteMatch.Groups[3].Value.Trim() : string.Empty;
|
||||
|
||||
result = new DeleteBreakdown(fromClause, whereClause)
|
||||
{
|
||||
SetupClauses = setupClauses,
|
||||
FinishClauses = finishClauses
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(deleteClause))
|
||||
{
|
||||
var cleanDelete = parser.ExtractSqlComments(deleteClause, out var deleteComments);
|
||||
result.DeleteClause.Clause = cleanDelete.Trim();
|
||||
result.DeleteClause.Comment = deleteComments.Count > 0 ? string.Join(" ", deleteComments) : null;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errorMessage = $"Unexpected error during parsing: {ex.Message}";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
using System.Collections;
|
||||
using System.Text;
|
||||
using Strata.SqlTools.SqlBreakdown.Classes;
|
||||
using Strata.SqlTools.Statements.SqlServer;
|
||||
using Strata.SqlTools.SqlBreakdown.Utilities;
|
||||
|
||||
namespace Strata.SqlTools.Breakdowns.SqlServer;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an INSERT SQL statement breakdown with column and value clauses for SQL Server.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class InsertBreakdown : SqlBreakdownBase
|
||||
{
|
||||
protected readonly StatementParser Parser;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="InsertBreakdown"/> class.
|
||||
/// </summary>
|
||||
public InsertBreakdown()
|
||||
{
|
||||
Parser = new StatementParser();
|
||||
TableName = new SqlClause();
|
||||
InsertIntoClause = new SqlClause();
|
||||
ValuesClause = new SqlClause();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="InsertBreakdown"/> class.
|
||||
/// </summary>
|
||||
/// <param name="tableName">The table name.</param>
|
||||
/// <param name="insertIntoClause">The column list for the INSERT.</param>
|
||||
/// <param name="valuesClause">The values clause.</param>
|
||||
public InsertBreakdown(string tableName, string insertIntoClause, string valuesClause) : this()
|
||||
{
|
||||
var cleanTable = Parser.ExtractSqlComments(tableName, out var tableComments);
|
||||
TableName.Clause = cleanTable.Trim();
|
||||
TableName.Comment = tableComments.Count > 0 ? string.Join(" ", tableComments) : null;
|
||||
|
||||
var cleanInsert = Parser.ExtractSqlComments(insertIntoClause, out var insertComments);
|
||||
InsertIntoClause.Clause = cleanInsert.Trim();
|
||||
InsertIntoClause.Comment = insertComments.Count > 0 ? string.Join(" ", insertComments) : null;
|
||||
|
||||
var cleanValues = Parser.ExtractSqlComments(valuesClause, out var valuesComments);
|
||||
ValuesClause.Clause = cleanValues.Trim();
|
||||
ValuesClause.Comment = valuesComments.Count > 0 ? string.Join(" ", valuesComments) : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="InsertBreakdown"/> class from a list of column names.
|
||||
/// </summary>
|
||||
/// <param name="tableName">The table name.</param>
|
||||
/// <param name="columnNames">The list of column names.</param>
|
||||
public InsertBreakdown(string tableName, List<string> columnNames) : this()
|
||||
{
|
||||
TableName.Clause = tableName;
|
||||
InsertIntoClause.Clause = SqlUtils.GetSqlSafeColumnList(columnNames);
|
||||
|
||||
// Generate parameter names for values (SQL Server uses @parameter syntax)
|
||||
var valuesList = new List<string>();
|
||||
foreach (string item in columnNames)
|
||||
{
|
||||
valuesList.Add($"@{item}");
|
||||
}
|
||||
ValuesClause.Clause = string.Join(",", valuesList);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the INSERT INTO clause (column list).
|
||||
/// </summary>
|
||||
public SqlClause InsertIntoClause { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the VALUES clause.
|
||||
/// </summary>
|
||||
public SqlClause ValuesClause { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the table name.
|
||||
/// </summary>
|
||||
public SqlClause TableName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the SQL breakdown as a string.
|
||||
/// </summary>
|
||||
/// <returns>The INSERT SQL statement.</returns>
|
||||
protected override string GetSqlBreakdown()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
sb.AppendLine("INSERT INTO ");
|
||||
sb.Append($" {TableName.Clause} (");
|
||||
sb.Append(InsertIntoClause.Clause);
|
||||
sb.AppendLine(")");
|
||||
sb.Append(" VALUES (");
|
||||
sb.Append(ValuesClause.Clause);
|
||||
sb.AppendLine(")");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
#region Parse Methods
|
||||
|
||||
/// <summary>
|
||||
/// Parses an INSERT SQL statement into an InsertBreakdown object.
|
||||
/// </summary>
|
||||
/// <param name="sql">The INSERT SQL statement to parse.</param>
|
||||
/// <returns>An InsertBreakdown object representing the parsed statement.</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown when sql is null or empty.</exception>
|
||||
/// <exception cref="FormatException">Thrown when the SQL statement cannot be parsed.</exception>
|
||||
public static InsertBreakdown Parse(string sql)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(sql))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(sql), "SQL statement cannot be null or empty.");
|
||||
}
|
||||
|
||||
if (!TryParse(sql, out var result, out var error))
|
||||
{
|
||||
throw new FormatException($"Failed to parse INSERT statement: {error}");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to parse an INSERT SQL statement into an InsertBreakdown object.
|
||||
/// </summary>
|
||||
/// <param name="sql">The INSERT SQL statement to parse.</param>
|
||||
/// <param name="result">When this method returns, contains the parsed InsertBreakdown if successful, or null if parsing failed.</param>
|
||||
/// <returns>true if the SQL was successfully parsed; otherwise, false.</returns>
|
||||
public static bool TryParse(string sql, out InsertBreakdown result)
|
||||
=> TryParse(sql, out result, out _);
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to parse an INSERT SQL statement into an InsertBreakdown object.
|
||||
/// </summary>
|
||||
/// <param name="sql">The INSERT SQL statement to parse.</param>
|
||||
/// <param name="result">When this method returns, contains the parsed InsertBreakdown if successful, or null if parsing failed.</param>
|
||||
/// <param name="errorMessage">When this method returns false, contains a message describing why parsing failed.</param>
|
||||
/// <returns>true if the SQL was successfully parsed; otherwise, false.</returns>
|
||||
public static bool TryParse(string sql, out InsertBreakdown result, out string errorMessage)
|
||||
{
|
||||
result = null!;
|
||||
errorMessage = null!;
|
||||
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(sql))
|
||||
{
|
||||
errorMessage = "SQL statement cannot be null or empty.";
|
||||
return false;
|
||||
}
|
||||
|
||||
var parser = new StatementParser();
|
||||
sql = parser.NormalizeSqlPreservingComments(sql);
|
||||
|
||||
// Check if it's an INSERT statement
|
||||
var sqlTrimmed = sql.TrimStart();
|
||||
if (!System.Text.RegularExpressions.Regex.IsMatch(sqlTrimmed, @"^\s*INSERT\s+INTO\b",
|
||||
System.Text.RegularExpressions.RegexOptions.IgnoreCase))
|
||||
{
|
||||
errorMessage = "SQL statement must start with INSERT INTO.";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Extract setup and finish clauses
|
||||
var setupClauses = new List<string>();
|
||||
sql = parser.ExtractSetupClauses(sql, setupClauses);
|
||||
|
||||
var finishClauses = new ArrayList();
|
||||
sql = parser.ExtractFinishClauses(sql, finishClauses);
|
||||
|
||||
// Parse INSERT statement using regex
|
||||
// Pattern: INSERT INTO table (columns) VALUES (values)
|
||||
var insertMatch = System.Text.RegularExpressions.Regex.Match(sql,
|
||||
@"INSERT\s+INTO\s+([^\(\s]+)\s*\(([^\)]*)\)\s*VALUES\s*\(([^\)]*)\)",
|
||||
System.Text.RegularExpressions.RegexOptions.IgnoreCase | System.Text.RegularExpressions.RegexOptions.Singleline);
|
||||
|
||||
if (!insertMatch.Success)
|
||||
{
|
||||
errorMessage = "Could not parse INSERT statement. Expected format: INSERT INTO table (columns) VALUES (values)";
|
||||
return false;
|
||||
}
|
||||
|
||||
var tableName = insertMatch.Groups[1].Value.Trim();
|
||||
var columnsClause = insertMatch.Groups[2].Value.Trim();
|
||||
var valuesClause = insertMatch.Groups[3].Value.Trim();
|
||||
|
||||
result = new InsertBreakdown(tableName, columnsClause, valuesClause)
|
||||
{
|
||||
SetupClauses = setupClauses,
|
||||
FinishClauses = finishClauses
|
||||
};
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errorMessage = $"Unexpected error during parsing: {ex.Message}";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
using System.Collections;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using Strata.SqlTools.SqlBreakdown.Classes;
|
||||
using Strata.SqlTools.Statements.SqlServer;
|
||||
|
||||
namespace Strata.SqlTools.Breakdowns.SqlServer;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a SQL Server stored procedure call breakdown with procedure name and parameters.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class ProcedureBreakdown : SqlBreakdownBase
|
||||
{
|
||||
protected readonly StatementParser Parser;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ProcedureBreakdown"/> class.
|
||||
/// </summary>
|
||||
public ProcedureBreakdown()
|
||||
{
|
||||
Parser = new StatementParser();
|
||||
ProcedureName = new SqlClause();
|
||||
Parameters = new Dictionary<string, string>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ProcedureBreakdown"/> class.
|
||||
/// </summary>
|
||||
/// <param name="procedureName">The stored procedure name.</param>
|
||||
public ProcedureBreakdown(string procedureName) : this()
|
||||
{
|
||||
var cleanName = Parser.ExtractSqlComments(procedureName, out var nameComments);
|
||||
ProcedureName.Clause = cleanName.Trim();
|
||||
ProcedureName.Comment = nameComments.Count > 0 ? string.Join(" ", nameComments) : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ProcedureBreakdown"/> class.
|
||||
/// </summary>
|
||||
/// <param name="procedureName">The stored procedure name.</param>
|
||||
/// <param name="parameters">The parameters dictionary (parameter name -> value expression).</param>
|
||||
public ProcedureBreakdown(string procedureName, Dictionary<string, string> parameters) : this(procedureName)
|
||||
{
|
||||
Parameters = parameters ?? new Dictionary<string, string>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the stored procedure name.
|
||||
/// </summary>
|
||||
public SqlClause ProcedureName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the parameters dictionary (parameter name -> value expression).
|
||||
/// </summary>
|
||||
public Dictionary<string, string> Parameters { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether parameters are being used.
|
||||
/// </summary>
|
||||
#pragma warning disable S2325 // Methods and properties that don't access instance data should be static
|
||||
public bool IsUsingParameters => Parameters.Count > 0;
|
||||
#pragma warning restore S2325
|
||||
|
||||
/// <summary>
|
||||
/// Adds a parameter to the stored procedure call.
|
||||
/// </summary>
|
||||
/// <param name="parameterName">The parameter name (with or without @).</param>
|
||||
/// <param name="valueExpression">The value expression or literal.</param>
|
||||
#pragma warning disable S2325 // Methods and properties that don't access instance data should be static
|
||||
public void AddParameter(string parameterName, string valueExpression)
|
||||
#pragma warning restore S2325
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(parameterName))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(parameterName));
|
||||
}
|
||||
|
||||
// Ensure parameter name starts with @
|
||||
if (!parameterName.StartsWith('@'))
|
||||
{
|
||||
parameterName = "@" + parameterName;
|
||||
}
|
||||
|
||||
Parameters[parameterName] = valueExpression;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the SQL breakdown as a string.
|
||||
/// </summary>
|
||||
/// <returns>The EXECUTE/EXEC SQL statement.</returns>
|
||||
protected override string GetSqlBreakdown()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
sb.Append("EXEC ");
|
||||
sb.Append(ProcedureName.Clause);
|
||||
|
||||
if (IsUsingParameters)
|
||||
{
|
||||
sb.AppendLine();
|
||||
var paramList = new List<string>();
|
||||
foreach (var param in Parameters)
|
||||
{
|
||||
paramList.Add($" {param.Key} = {param.Value}");
|
||||
}
|
||||
sb.Append(string.Join($",{Environment.NewLine}", paramList));
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
#region Parse Methods
|
||||
|
||||
/// <summary>
|
||||
/// Parses an EXEC/EXECUTE SQL statement into a ProcedureBreakdown object.
|
||||
/// </summary>
|
||||
/// <param name="sql">The EXEC/EXECUTE SQL statement to parse.</param>
|
||||
/// <returns>A ProcedureBreakdown object representing the parsed statement.</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown when sql is null or empty.</exception>
|
||||
/// <exception cref="FormatException">Thrown when the SQL statement cannot be parsed.</exception>
|
||||
public static ProcedureBreakdown Parse(string sql)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(sql))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(sql), "SQL statement cannot be null or empty.");
|
||||
}
|
||||
|
||||
if (!TryParse(sql, out var result, out var error))
|
||||
{
|
||||
throw new FormatException($"Failed to parse EXEC statement: {error}");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to parse an EXEC/EXECUTE SQL statement into a ProcedureBreakdown object.
|
||||
/// </summary>
|
||||
/// <param name="sql">The EXEC/EXECUTE SQL statement to parse.</param>
|
||||
/// <param name="result">When this method returns, contains the parsed ProcedureBreakdown if successful, or null if parsing failed.</param>
|
||||
/// <returns>true if the SQL was successfully parsed; otherwise, false.</returns>
|
||||
public static bool TryParse(string sql, out ProcedureBreakdown result)
|
||||
=> TryParse(sql, out result, out _);
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to parse an EXEC/EXECUTE SQL statement into a ProcedureBreakdown object.
|
||||
/// </summary>
|
||||
/// <param name="sql">The EXEC/EXECUTE SQL statement to parse.</param>
|
||||
/// <param name="result">When this method returns, contains the parsed ProcedureBreakdown if successful, or null if parsing failed.</param>
|
||||
/// <param name="errorMessage">When this method returns false, contains a message describing why parsing failed.</param>
|
||||
/// <returns>true if the SQL was successfully parsed; otherwise, false.</returns>
|
||||
public static bool TryParse(string sql, out ProcedureBreakdown result, out string errorMessage)
|
||||
{
|
||||
result = null!;
|
||||
errorMessage = null!;
|
||||
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(sql))
|
||||
{
|
||||
errorMessage = "SQL statement cannot be null or empty.";
|
||||
return false;
|
||||
}
|
||||
|
||||
var parser = new StatementParser();
|
||||
sql = parser.NormalizeSqlPreservingComments(sql);
|
||||
|
||||
// Check if it's an EXEC or EXECUTE statement
|
||||
var sqlTrimmed = sql.TrimStart();
|
||||
if (!Regex.IsMatch(sqlTrimmed, @"^\s*(EXEC|EXECUTE)\b",
|
||||
RegexOptions.IgnoreCase))
|
||||
{
|
||||
errorMessage = "SQL statement must start with EXEC or EXECUTE.";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Extract setup and finish clauses
|
||||
var setupClauses = new List<string>();
|
||||
sql = parser.ExtractSetupClauses(sql, setupClauses);
|
||||
|
||||
var finishClauses = new ArrayList();
|
||||
sql = parser.ExtractFinishClauses(sql, finishClauses);
|
||||
|
||||
// Parse EXEC statement - match procedure name and parameters
|
||||
// Pattern: EXEC[UTE] procedureName [@param = value, ...]
|
||||
var execMatch = Regex.Match(sql,
|
||||
@"(?:EXEC|EXECUTE)\s+([^\s@,]+)(?:\s+(.*))?$",
|
||||
RegexOptions.IgnoreCase | RegexOptions.Singleline);
|
||||
|
||||
if (!execMatch.Success)
|
||||
{
|
||||
errorMessage = "Could not parse EXEC statement. Expected format: EXEC procedureName [@param = value, ...]";
|
||||
return false;
|
||||
}
|
||||
|
||||
var procedureName = execMatch.Groups[1].Value.Trim();
|
||||
var parametersText = execMatch.Groups.Count > 2 ? execMatch.Groups[2].Value.Trim() : string.Empty;
|
||||
|
||||
var parameters = new Dictionary<string, string>();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(parametersText))
|
||||
{
|
||||
// Parse parameters - handle both @param = value and positional parameters
|
||||
var paramMatches = Regex.Matches(parametersText,
|
||||
@"(@\w+)\s*=\s*([^,]+)(?:,|$)",
|
||||
RegexOptions.IgnoreCase);
|
||||
|
||||
parameters = paramMatches
|
||||
.Cast<System.Text.RegularExpressions.Match>()
|
||||
.ToDictionary(
|
||||
paramMatch => paramMatch.Groups[1].Value.Trim(),
|
||||
paramMatch => paramMatch.Groups[2].Value.Trim()
|
||||
);
|
||||
}
|
||||
|
||||
result = new ProcedureBreakdown(procedureName, parameters)
|
||||
{
|
||||
SetupClauses = setupClauses,
|
||||
FinishClauses = finishClauses
|
||||
};
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errorMessage = $"Unexpected error during parsing: {ex.Message}";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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"}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
using System.Data;
|
||||
using System.Data.SqlClient;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Strata.SqlTools.Breakdowns.SqlServer;
|
||||
|
||||
/// <summary>
|
||||
/// A trace listener that writes trace messages to a SQL Server database.
|
||||
/// </summary>
|
||||
public class TraceListener : System.Diagnostics.TraceListener
|
||||
{
|
||||
private readonly string _serverName;
|
||||
private readonly string _traceDbConnectionString;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TraceListener"/> class.
|
||||
/// </summary>
|
||||
/// <param name="serverName">The server name for logging.</param>
|
||||
/// <param name="traceDbConnectionString">The connection string to the trace database.</param>
|
||||
public TraceListener(string serverName, string traceDbConnectionString)
|
||||
{
|
||||
_serverName = serverName;
|
||||
_traceDbConnectionString = traceDbConnectionString;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a message to the trace database.
|
||||
/// </summary>
|
||||
/// <param name="message">The message to write.</param>
|
||||
public override void Write(string? message)
|
||||
{
|
||||
WriteTrace(message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a message followed by a line terminator to the trace database.
|
||||
/// </summary>
|
||||
/// <param name="message">The message to write.</param>
|
||||
public override void WriteLine(string? message)
|
||||
{
|
||||
WriteTrace(message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a trace message to the database.
|
||||
/// </summary>
|
||||
/// <param name="message">The message to write.</param>
|
||||
private void WriteTrace(string? message)
|
||||
{
|
||||
using var sqlConnection = new SqlConnection();
|
||||
|
||||
try
|
||||
{
|
||||
sqlConnection.ConnectionString = _traceDbConnectionString;
|
||||
sqlConnection.Open();
|
||||
|
||||
using var command = sqlConnection.CreateCommand();
|
||||
command.CommandType = CommandType.Text;
|
||||
command.CommandText = "INSERT INTO Trace ([SERVER], [MESSAGE]) VALUES(@SERVER, @MESSAGE)";
|
||||
command.Parameters.Add(new SqlParameter("@SERVER", _serverName));
|
||||
command.Parameters.Add(new SqlParameter("@MESSAGE", message));
|
||||
command.ExecuteNonQuery();
|
||||
}
|
||||
catch (SqlException)
|
||||
{
|
||||
if (Debugger.IsAttached)
|
||||
{
|
||||
Debugger.Break();
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
if (Debugger.IsAttached)
|
||||
{
|
||||
Debugger.Break();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
using System.Collections;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using Strata.SqlTools.SqlBreakdown.Classes;
|
||||
using Strata.SqlTools.Statements.SqlServer;
|
||||
|
||||
namespace Strata.SqlTools.Breakdowns.SqlServer;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an UPDATE SQL statement breakdown with SET, FROM, and WHERE clauses for SQL Server.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class UpdateBreakdown : SqlBreakdownBase
|
||||
{
|
||||
protected readonly StatementParser Parser;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UpdateBreakdown"/> class.
|
||||
/// </summary>
|
||||
public UpdateBreakdown()
|
||||
{
|
||||
Parser = new StatementParser();
|
||||
TableName = new SqlClause();
|
||||
SetClause = new SqlClause();
|
||||
FromClause = new SqlClause();
|
||||
WhereClause = new SqlClause();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UpdateBreakdown"/> class.
|
||||
/// </summary>
|
||||
/// <param name="tableName">The table name.</param>
|
||||
/// <param name="setClause">The SET clause.</param>
|
||||
/// <param name="whereClause">The WHERE clause.</param>
|
||||
public UpdateBreakdown(string tableName, string setClause, string whereClause) : this()
|
||||
{
|
||||
var cleanTable = Parser.ExtractSqlComments(tableName, out var tableComments);
|
||||
TableName.Clause = cleanTable.Trim();
|
||||
TableName.Comment = tableComments.Count > 0 ? string.Join(" ", tableComments) : null;
|
||||
|
||||
var cleanSet = Parser.ExtractSqlComments(setClause, out var setComments);
|
||||
SetClause.Clause = cleanSet.Trim();
|
||||
SetClause.Comment = setComments.Count > 0 ? string.Join(" ", setComments) : null;
|
||||
|
||||
var cleanWhere = Parser.ExtractSqlComments(whereClause, out var whereComments);
|
||||
WhereClause.Clause = cleanWhere.Trim();
|
||||
WhereClause.Comment = whereComments.Count > 0 ? string.Join(" ", whereComments) : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the SET clause.
|
||||
/// </summary>
|
||||
public SqlClause SetClause { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the table name.
|
||||
/// </summary>
|
||||
public SqlClause TableName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether a FROM clause is being used.
|
||||
/// </summary>
|
||||
public bool IsUsingFromClause => !string.IsNullOrWhiteSpace(FromClause.Clause);
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the FROM clause (for UPDATE with JOIN).
|
||||
/// </summary>
|
||||
public SqlClause FromClause { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether a WHERE clause is being used.
|
||||
/// </summary>
|
||||
public bool IsUsingWhereClause => !string.IsNullOrWhiteSpace(WhereClause.Clause);
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the WHERE clause.
|
||||
/// </summary>
|
||||
public SqlClause WhereClause { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the SQL breakdown as a string.
|
||||
/// </summary>
|
||||
/// <returns>The UPDATE SQL statement.</returns>
|
||||
protected override string GetSqlBreakdown()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
sb.AppendLine("UPDATE ");
|
||||
sb.AppendLine($" {TableName.Clause}");
|
||||
|
||||
sb.AppendLine("SET ");
|
||||
sb.AppendLine($" {SetClause.Clause}");
|
||||
|
||||
if (IsUsingFromClause)
|
||||
{
|
||||
sb.AppendLine("FROM ");
|
||||
sb.AppendLine($" {FromClause.Clause}");
|
||||
}
|
||||
|
||||
if (IsUsingWhereClause)
|
||||
{
|
||||
sb.AppendLine("WHERE ");
|
||||
sb.AppendLine($" {WhereClause.Clause}");
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
#region Parse Methods
|
||||
|
||||
/// <summary>
|
||||
/// Parses an UPDATE SQL statement into an UpdateBreakdown object.
|
||||
/// </summary>
|
||||
/// <param name="sql">The UPDATE SQL statement to parse.</param>
|
||||
/// <returns>An UpdateBreakdown object representing the parsed statement.</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown when sql is null or empty.</exception>
|
||||
/// <exception cref="FormatException">Thrown when the SQL statement cannot be parsed.</exception>
|
||||
public static UpdateBreakdown Parse(string sql)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(sql))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(sql), "SQL statement cannot be null or empty.");
|
||||
}
|
||||
|
||||
if (!TryParse(sql, out var result, out var error))
|
||||
{
|
||||
throw new FormatException($"Failed to parse UPDATE statement: {error}");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to parse an UPDATE SQL statement into an UpdateBreakdown object.
|
||||
/// </summary>
|
||||
/// <param name="sql">The UPDATE SQL statement to parse.</param>
|
||||
/// <param name="result">When this method returns, contains the parsed UpdateBreakdown if successful, or null if parsing failed.</param>
|
||||
/// <returns>true if the SQL was successfully parsed; otherwise, false.</returns>
|
||||
public static bool TryParse(string sql, out UpdateBreakdown result)
|
||||
=> TryParse(sql, out result, out _);
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to parse an UPDATE SQL statement into an UpdateBreakdown object.
|
||||
/// </summary>
|
||||
/// <param name="sql">The UPDATE SQL statement to parse.</param>
|
||||
/// <param name="result">When this method returns, contains the parsed UpdateBreakdown if successful, or null if parsing failed.</param>
|
||||
/// <param name="errorMessage">When this method returns false, contains a message describing why parsing failed.</param>
|
||||
/// <returns>true if the SQL was successfully parsed; otherwise, false.</returns>
|
||||
public static bool TryParse(string sql, out UpdateBreakdown result, out string errorMessage)
|
||||
{
|
||||
result = null!;
|
||||
errorMessage = null!;
|
||||
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(sql))
|
||||
{
|
||||
errorMessage = "SQL statement cannot be null or empty.";
|
||||
return false;
|
||||
}
|
||||
|
||||
var parser = new StatementParser();
|
||||
sql = parser.NormalizeSqlPreservingComments(sql);
|
||||
|
||||
// Check if it's an UPDATE statement
|
||||
var sqlTrimmed = sql.TrimStart();
|
||||
if (!Regex.IsMatch(sqlTrimmed, @"^\s*UPDATE\b",
|
||||
RegexOptions.IgnoreCase))
|
||||
{
|
||||
errorMessage = "SQL statement must start with UPDATE.";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Extract setup and finish clauses
|
||||
var setupClauses = new List<string>();
|
||||
sql = parser.ExtractSetupClauses(sql, setupClauses);
|
||||
|
||||
var finishClauses = new ArrayList();
|
||||
sql = parser.ExtractFinishClauses(sql, finishClauses);
|
||||
|
||||
// Parse UPDATE statement - handle both with and without FROM clause
|
||||
// Pattern: UPDATE table SET column=value [FROM table] [WHERE condition]
|
||||
var updateMatch = Regex.Match(sql,
|
||||
@"UPDATE\s+([^\s]+)\s+SET\s+(.*?)(?:\s+FROM\s+(.*?))?(?:\s+WHERE\s+(.*))?$",
|
||||
RegexOptions.IgnoreCase | RegexOptions.Singleline);
|
||||
|
||||
if (!updateMatch.Success)
|
||||
{
|
||||
errorMessage = "Could not parse UPDATE statement. Expected format: UPDATE table SET column=value [FROM table] [WHERE condition]";
|
||||
return false;
|
||||
}
|
||||
|
||||
var tableName = updateMatch.Groups[1].Value.Trim();
|
||||
var setClause = updateMatch.Groups[2].Value.Trim();
|
||||
var fromClause = updateMatch.Groups.Count > 3 ? updateMatch.Groups[3].Value.Trim() : string.Empty;
|
||||
var whereClause = updateMatch.Groups.Count > 4 ? updateMatch.Groups[4].Value.Trim() : string.Empty;
|
||||
|
||||
result = new UpdateBreakdown(tableName, setClause, whereClause)
|
||||
{
|
||||
SetupClauses = setupClauses,
|
||||
FinishClauses = finishClauses
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(fromClause))
|
||||
{
|
||||
var cleanFrom = parser.ExtractSqlComments(fromClause, out var fromComments);
|
||||
result.FromClause.Clause = cleanFrom.Trim();
|
||||
result.FromClause.Comment = fromComments.Count > 0 ? string.Join(" ", fromComments) : null;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errorMessage = $"Unexpected error during parsing: {ex.Message}";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
Reference in New Issue
Block a user