using System.Collections; using System.Runtime.Serialization; using System.Text; using SqlServerUpdateBreakdown = Strata.SqlTools.Breakdowns.SqlServer.UpdateBreakdown; using StatementParser = Strata.SqlTools.Statements.Snowflake.StatementParser; namespace Strata.SqlTools.Breakdowns.Snowflake; /// /// Represents an UPDATE SQL statement breakdown with SET, FROM, and WHERE clauses for Snowflake. /// [Serializable] public class UpdateBreakdown : SqlServerUpdateBreakdown { private static readonly StatementParser SnowflakeParserInstance = new StatementParser(); /// /// Initializes a new instance of the class. /// public UpdateBreakdown() : base() { } /// /// Initializes a new instance of the class. /// /// The table name. /// The SET clause. /// The WHERE clause. /// If true, uses Microsoft T-SQL parsing rules. If false, uses Snowflake parsing rules. Defaults to false. 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; } /// /// Re-establishes invariants after deserialization, since deserialization bypasses the /// constructors that normally initialize the breakdown's clause state (SonarQube rule S5766). /// /// The streaming context for the deserialization operation. [OnDeserialized] private void OnDeserialized(StreamingContext context) => RevalidateBreakdownState(); /// /// Gets the SQL breakdown as a string for Snowflake. /// /// The UPDATE SQL statement. 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 /// /// Parses a Snowflake UPDATE SQL statement into an UpdateBreakdown object. /// /// The UPDATE SQL statement to parse. /// If true, uses Microsoft T-SQL parsing rules. If false, uses Snowflake parsing rules. Defaults to false. /// An UpdateBreakdown object representing the parsed statement. /// Thrown when sql is null or empty. /// Thrown when the SQL statement cannot be parsed. 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; } /// /// Attempts to parse a Snowflake UPDATE SQL statement into an UpdateBreakdown object. /// /// The UPDATE SQL statement to parse. /// When this method returns, contains the parsed UpdateBreakdown if successful, or null if parsing failed. /// If true, uses Microsoft T-SQL parsing rules instead of Snowflake rules. Defaults to false. /// true if the SQL was successfully parsed; otherwise, false. public static bool TryParse(string sql, out UpdateBreakdown result, bool isMicrosoftSql = false) => TryParse(sql, out result, out _, isMicrosoftSql); /// /// Attempts to parse a Snowflake UPDATE SQL statement into an UpdateBreakdown object. /// Handles Snowflake-specific syntax. /// /// The UPDATE SQL statement to parse. /// When this method returns, contains the parsed UpdateBreakdown if successful, or null if parsing failed. /// When this method returns false, contains a message describing why parsing failed. /// If true, uses Microsoft T-SQL parsing rules instead of Snowflake rules. Defaults to false. /// true if the SQL was successfully parsed; otherwise, false. 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(); 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 }