Files
sql-utilities/src/Strata.SqlTools.Snowflake/Breakdowns/UpdateBreakdown.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

215 lines
9.1 KiB
C#

using System.Collections;
using System.Text;
using SqlServerUpdateBreakdown = Strata.SqlTools.Breakdowns.SqlServer.UpdateBreakdown;
using StatementParser = Strata.SqlTools.Statements.Snowflake.StatementParser;
namespace Strata.SqlTools.Breakdowns.Snowflake;
/// <summary>
/// Represents an UPDATE SQL statement breakdown with SET, FROM, and WHERE clauses for Snowflake.
/// </summary>
public class UpdateBreakdown : SqlServerUpdateBreakdown
{
private static readonly StatementParser SnowflakeParserInstance = new StatementParser();
/// <summary>
/// Initializes a new instance of the <see cref="UpdateBreakdown"/> class.
/// </summary>
public UpdateBreakdown() : base()
{
}
/// <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>
/// <param name="isMicrosoftSql">If true, uses Microsoft T-SQL parsing rules. If false, uses Snowflake parsing rules. Defaults to false.</param>
public UpdateBreakdown(string tableName, string setClause, string whereClause, bool isMicrosoftSql = false)
: base()
{
var parser = isMicrosoftSql ? Parser : SnowflakeParserInstance;
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 the SQL breakdown as a string for Snowflake.
/// </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)
{
// Snowflake supports FROM clause in UPDATE
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 Snowflake UPDATE SQL statement into an UpdateBreakdown object.
/// </summary>
/// <param name="sql">The UPDATE SQL statement to parse.</param>
/// <param name="isMicrosoftSql">If true, uses Microsoft T-SQL parsing rules. If false, uses Snowflake parsing rules. Defaults to false.</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, bool isMicrosoftSql = false)
{
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, isMicrosoftSql))
{
throw new FormatException($"Failed to parse {(isMicrosoftSql ? "T-SQL" : "Snowflake SQL")} UPDATE statement: {error}");
}
return result;
}
/// <summary>
/// Attempts to parse a Snowflake 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="isMicrosoftSql">If true, uses Microsoft T-SQL parsing rules instead of Snowflake rules. Defaults to false.</param>
/// <returns>true if the SQL was successfully parsed; otherwise, false.</returns>
public static bool TryParse(string sql, out UpdateBreakdown result, bool isMicrosoftSql = false)
=> TryParse(sql, out result, out _, isMicrosoftSql);
/// <summary>
/// Attempts to parse a Snowflake UPDATE SQL statement into an UpdateBreakdown object.
/// Handles Snowflake-specific syntax.
/// </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>
/// <param name="isMicrosoftSql">If true, uses Microsoft T-SQL parsing rules instead of Snowflake rules. Defaults to false.</param>
/// <returns>true if the SQL was successfully parsed; otherwise, false.</returns>
public static bool TryParse(string sql, out UpdateBreakdown result, out string errorMessage, bool isMicrosoftSql = false)
{
result = null!;
errorMessage = null!;
try
{
if (string.IsNullOrWhiteSpace(sql))
{
errorMessage = "SQL statement cannot be null or empty.";
return false;
}
// If Microsoft SQL mode, delegate to base class
if (isMicrosoftSql)
{
if (!SqlServerUpdateBreakdown.TryParse(sql, out var baseResult, out errorMessage))
{
return false;
}
// Convert to Snowflake UpdateBreakdown
result = new UpdateBreakdown
{
TableName = baseResult.TableName,
SetClause = baseResult.SetClause,
FromClause = baseResult.FromClause,
WhereClause = baseResult.WhereClause,
SetupClauses = baseResult.SetupClauses,
FinishClauses = baseResult.FinishClauses
};
return true;
}
var parser = SnowflakeParserInstance;
sql = parser.NormalizeSqlPreservingComments(sql);
// Check if it's an UPDATE statement
var sqlTrimmed = sql.TrimStart();
if (!System.Text.RegularExpressions.Regex.IsMatch(sqlTrimmed, @"^\s*UPDATE\b",
System.Text.RegularExpressions.RegexOptions.IgnoreCase, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout))
{
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
var updateMatch = System.Text.RegularExpressions.Regex.Match(sql,
@"UPDATE\s+([^\s]+)\s+SET\s+(.*?)(?:\s+FROM\s+(.*?))?(?:\s+WHERE\s+(.*))?$",
System.Text.RegularExpressions.RegexOptions.IgnoreCase | System.Text.RegularExpressions.RegexOptions.Singleline, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout);
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, isMicrosoftSql: false)
{
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
}