Files
sql-utilities/src/Strata.SqlTools.Snowflake/Breakdowns/UpdateBreakdown.cs
T
Thom LambandClaude Opus 4.7 507620cc04 refactor(dedup): redundant Snowflake override + shared Insert regex helper
Two follow-ups to the prior dedup pass:

- **Delete `Snowflake.UpdateBreakdown.GetSqlBreakdown`**: it was a
  byte-for-byte copy of the SqlServer base's `GetSqlBreakdown` (modulo
  one explanatory comment). Snowflake's UPDATE syntax — including the
  FROM clause — is identical at the formatter level, so the override
  was pure inheritance noise. Now inherits.

- **Extract `ParsePreparation.TryMatchInsertSql`**: the regex match +
  group extraction + failure message at the end of
  `SqlServer.InsertBreakdown.TryParse` and
  `Snowflake.InsertBreakdown.TryParse` was duplicated. Hoist the
  shared piece next to `TryRunPrelude` on `ParsePreparation`. Both
  callers continue to construct their own `InsertBreakdown` instance
  (the constructor signatures differ slightly between dialects).

All 1180 tests stay green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 17:05:00 -05:00

171 lines
7.8 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;
}
// GetSqlBreakdown() inherited from SqlServer.UpdateBreakdown — Snowflake's UPDATE syntax
// (including the optional FROM clause) is identical at the formatter level, so no override needed.
#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 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;
if (!Strata.SqlTools.Statements.SqlServer.ParsePreparation.TryRunPrelude(
sql, parser, @"^\s*UPDATE\b", "UPDATE",
out sql, out var setupClauses, out var finishClauses, out errorMessage))
{
return false;
}
// 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
}