SonarQube Analysis / sonarqube (pull_request) Successful in 3m9s
Introduce a default regex match timeout across the library to prevent potential ReDoS attacks (SonarQube rule S6444). Implement `[OnDeserialized]` methods to re-establish object invariants and validate state after deserialization, addressing SonarQube rule S5766.
225 lines
9.6 KiB
C#
225 lines
9.6 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// Represents an UPDATE SQL statement breakdown with SET, FROM, and WHERE clauses for Snowflake.
|
|
/// </summary>
|
|
[Serializable]
|
|
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>
|
|
/// Re-establishes invariants after deserialization, since deserialization bypasses the
|
|
/// constructors that normally initialize the breakdown's clause state (SonarQube rule S5766).
|
|
/// </summary>
|
|
/// <param name="context">The streaming context for the deserialization operation.</param>
|
|
[OnDeserialized]
|
|
private void OnDeserialized(StreamingContext context) => RevalidateBreakdownState();
|
|
|
|
/// <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
|
|
}
|