Merge pull request 'fix(security): Resolve SonarQube security hotspots' (#5) from fix/sonarqube-security-hotspots into main
SonarQube Analysis / sonarqube (push) Successful in 3m54s
SonarQube Analysis / sonarqube (push) Successful in 3m54s
Reviewed-on: #5
This commit was merged in pull request #5.
This commit is contained in:
@@ -221,7 +221,7 @@ public class QueryComparator
|
||||
|
||||
// Normalize whitespace and case
|
||||
return System.Text.RegularExpressions.Regex
|
||||
.Replace(clause.Trim(), @"\s+", " ")
|
||||
.Replace(clause.Trim(), @"\s+", " ", System.Text.RegularExpressions.RegexOptions.None, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout)
|
||||
.ToUpperInvariant();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Collections;
|
||||
using System.Runtime.Serialization;
|
||||
using Strata.SqlTools.SqlBreakdown.Expressions;
|
||||
using Strata.SqlTools.SqlBreakdown.Interfaces.Core;
|
||||
using CommandVisitor = Strata.SqlTools.Visitors.PostgreSql.CommandVisitor;
|
||||
@@ -88,6 +89,14 @@ public class QueryBreakdown : SqlServerQueryBreakdown
|
||||
}
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// Adds a parameter to the query using PostgreSQL's positional parameter format ($1, $2, ...).
|
||||
/// </summary>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Runtime.Serialization;
|
||||
using System.Text;
|
||||
using Strata.SqlTools.SqlBreakdown.Classes;
|
||||
using Strata.SqlTools.SqlBreakdown.Interfaces;
|
||||
@@ -35,6 +36,20 @@ public class QueryBreakdownCollection : SqlBreakdownCollection
|
||||
_queryBreakdowns = queryBreakdowns?.ToList() ?? new List<QueryBreakdown>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates that the backing list survived deserialization, since deserialization bypasses
|
||||
/// the constructors that normally initialize it (SonarQube rule S5766).
|
||||
/// </summary>
|
||||
/// <param name="context">The streaming context for the deserialization operation.</param>
|
||||
[OnDeserialized]
|
||||
private void OnDeserialized(StreamingContext context)
|
||||
{
|
||||
if (_queryBreakdowns is null)
|
||||
{
|
||||
throw new SerializationException("Deserialized QueryBreakdownCollection is missing its backing list.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the collection of QueryBreakdown objects.
|
||||
/// </summary>
|
||||
|
||||
@@ -2,18 +2,24 @@ using System.Text.Json.Serialization;
|
||||
|
||||
namespace Strata.SqlTools.Query;
|
||||
|
||||
[method: JsonConstructor]
|
||||
public class CalculationFilterGroup(IEnumerable<CalculationFilter> filters, LogicalOperator logicalOperator)
|
||||
public class CalculationFilterGroup
|
||||
{
|
||||
// Hereditary logical operation applied to all Filters
|
||||
public LogicalOperator LogicalOperator { get; set; } = logicalOperator;
|
||||
|
||||
public IEnumerable<CalculationFilter> Filters { get; set; } = filters;
|
||||
[JsonConstructor]
|
||||
public CalculationFilterGroup(IEnumerable<CalculationFilter> filters, LogicalOperator logicalOperator)
|
||||
{
|
||||
Filters = filters;
|
||||
LogicalOperator = logicalOperator;
|
||||
}
|
||||
|
||||
public CalculationFilterGroup() : this([], LogicalOperator.And)
|
||||
{
|
||||
}
|
||||
|
||||
// Hereditary logical operation applied to all Filters
|
||||
public LogicalOperator LogicalOperator { get; set; }
|
||||
|
||||
public IEnumerable<CalculationFilter> Filters { get; set; }
|
||||
|
||||
public IEnumerable<CalculationFilter> GetValidFilters()
|
||||
{
|
||||
return Filters?.Where(x => x.IsValid()).ToList() ?? [];
|
||||
|
||||
@@ -8,6 +8,12 @@ namespace Strata.SqlTools.Rules.Rule.Expression;
|
||||
/// </summary>
|
||||
public static class Markdown
|
||||
{
|
||||
/// <summary>
|
||||
/// Match timeout applied to all regular expressions to guard against catastrophic
|
||||
/// backtracking / ReDoS denial-of-service attacks (SonarQube rule S6444).
|
||||
/// </summary>
|
||||
private static readonly TimeSpan RegexTimeout = TimeSpan.FromSeconds(1);
|
||||
|
||||
private static readonly Dictionary<string, Func<Expression, Expression, BoolExpr>> LogicalOperators = new()
|
||||
{
|
||||
{ "\\land", (left, right) => new And((BoolExpr)left, (BoolExpr)right) },
|
||||
@@ -70,12 +76,12 @@ public static class Markdown
|
||||
private static string StripMarkdownDelimiters(string text)
|
||||
{
|
||||
// Remove $...$ or $$...$$ delimiters
|
||||
text = Regex.Replace(text, @"^\$\$?\s*", "");
|
||||
text = Regex.Replace(text, @"\s*\$\$?$", "");
|
||||
text = Regex.Replace(text, @"^\$\$?\s*", "", RegexOptions.None, RegexTimeout);
|
||||
text = Regex.Replace(text, @"\s*\$\$?$", "", RegexOptions.None, RegexTimeout);
|
||||
|
||||
// Remove ```math...``` code fence
|
||||
text = Regex.Replace(text, @"^```math\s*", "", RegexOptions.Multiline);
|
||||
text = Regex.Replace(text, @"\s*```$", "", RegexOptions.Multiline);
|
||||
text = Regex.Replace(text, @"^```math\s*", "", RegexOptions.Multiline, RegexTimeout);
|
||||
text = Regex.Replace(text, @"\s*```$", "", RegexOptions.Multiline, RegexTimeout);
|
||||
|
||||
return text.Trim();
|
||||
}
|
||||
@@ -186,18 +192,18 @@ public static class Markdown
|
||||
private static Expression? TryParseProperty(string text)
|
||||
{
|
||||
// Parse property access (e.g., x.PropertyName or \text{x.PropertyName})
|
||||
var propertyMatch = Regex.Match(text, @"^([a-zA-Z_][a-zA-Z0-9_]*)\.([a-zA-Z_][a-zA-Z0-9_]*)$");
|
||||
var propertyMatch = Regex.Match(text, @"^([a-zA-Z_][a-zA-Z0-9_]*)\.([a-zA-Z_][a-zA-Z0-9_]*)$", RegexOptions.None, RegexTimeout);
|
||||
if (propertyMatch.Success)
|
||||
{
|
||||
return new Property(propertyMatch.Groups[1].Value, propertyMatch.Groups[2].Value);
|
||||
}
|
||||
|
||||
// Parse \text{...} property access
|
||||
var textMatch = Regex.Match(text, @"^\\text\{([^}]+)\}$");
|
||||
var textMatch = Regex.Match(text, @"^\\text\{([^}]+)\}$", RegexOptions.None, RegexTimeout);
|
||||
if (textMatch.Success)
|
||||
{
|
||||
var textContent = textMatch.Groups[1].Value;
|
||||
var propMatch = Regex.Match(textContent, @"^([a-zA-Z_][a-zA-Z0-9_]*)\.([a-zA-Z_][a-zA-Z0-9_]*)$");
|
||||
var propMatch = Regex.Match(textContent, @"^([a-zA-Z_][a-zA-Z0-9_]*)\.([a-zA-Z_][a-zA-Z0-9_]*)$", RegexOptions.None, RegexTimeout);
|
||||
if (propMatch.Success)
|
||||
{
|
||||
return new Property(propMatch.Groups[1].Value, propMatch.Groups[2].Value);
|
||||
@@ -215,7 +221,7 @@ public static class Markdown
|
||||
}
|
||||
|
||||
// Single property name
|
||||
if (Regex.IsMatch(textContent, @"^[a-zA-Z_][a-zA-Z0-9_]*$"))
|
||||
if (Regex.IsMatch(textContent, @"^[a-zA-Z_][a-zA-Z0-9_]*$", RegexOptions.None, RegexTimeout))
|
||||
{
|
||||
return new Property(textContent);
|
||||
}
|
||||
@@ -236,7 +242,7 @@ public static class Markdown
|
||||
}
|
||||
|
||||
// Parse simple property without parameter
|
||||
if (Regex.IsMatch(text, @"^[a-zA-Z_][a-zA-Z0-9_]*$"))
|
||||
if (Regex.IsMatch(text, @"^[a-zA-Z_][a-zA-Z0-9_]*$", RegexOptions.None, RegexTimeout))
|
||||
{
|
||||
return new Property(text);
|
||||
}
|
||||
@@ -247,7 +253,7 @@ public static class Markdown
|
||||
private static Expression? TryParseLiteral(string text)
|
||||
{
|
||||
// Parse string literals (quoted)
|
||||
var stringMatch = Regex.Match(text, @"^[""'](.+?)[""']$");
|
||||
var stringMatch = Regex.Match(text, @"^[""'](.+?)[""']$", RegexOptions.None, RegexTimeout);
|
||||
if (stringMatch.Success)
|
||||
{
|
||||
return new StringLiteral(stringMatch.Groups[1].Value);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Collections;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Text;
|
||||
using SqlServerDeleteBreakdown = Strata.SqlTools.Breakdowns.SqlServer.DeleteBreakdown;
|
||||
using StatementParser = Strata.SqlTools.Statements.Snowflake.StatementParser;
|
||||
@@ -40,6 +41,14 @@ public class DeleteBreakdown : SqlServerDeleteBreakdown
|
||||
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>
|
||||
@@ -145,7 +154,7 @@ public class DeleteBreakdown : SqlServerDeleteBreakdown
|
||||
// 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))
|
||||
System.Text.RegularExpressions.RegexOptions.IgnoreCase, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout))
|
||||
{
|
||||
errorMessage = "SQL statement must start with DELETE.";
|
||||
return false;
|
||||
@@ -161,7 +170,7 @@ public class DeleteBreakdown : SqlServerDeleteBreakdown
|
||||
// Snowflake uses simpler DELETE syntax: DELETE FROM table WHERE condition
|
||||
var deleteMatch = System.Text.RegularExpressions.Regex.Match(sql,
|
||||
@"DELETE\s+FROM\s+(.*?)(?:\s+WHERE\s+(.*))?$",
|
||||
System.Text.RegularExpressions.RegexOptions.IgnoreCase | System.Text.RegularExpressions.RegexOptions.Singleline);
|
||||
System.Text.RegularExpressions.RegexOptions.IgnoreCase | System.Text.RegularExpressions.RegexOptions.Singleline, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout);
|
||||
|
||||
if (!deleteMatch.Success)
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Collections;
|
||||
using System.Runtime.Serialization;
|
||||
using Strata.SqlTools.SqlBreakdown.Utilities;
|
||||
using SqlServerInsertBreakdown = Strata.SqlTools.Breakdowns.SqlServer.InsertBreakdown;
|
||||
using StatementParser = Strata.SqlTools.Statements.Snowflake.StatementParser;
|
||||
@@ -65,6 +66,14 @@ public class InsertBreakdown : SqlServerInsertBreakdown
|
||||
ValuesClause.Clause = string.Join(",", valuesList);
|
||||
}
|
||||
|
||||
/// <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();
|
||||
|
||||
#region Parse Methods
|
||||
|
||||
/// <summary>
|
||||
@@ -149,7 +158,7 @@ public class InsertBreakdown : SqlServerInsertBreakdown
|
||||
// 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))
|
||||
System.Text.RegularExpressions.RegexOptions.IgnoreCase, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout))
|
||||
{
|
||||
errorMessage = "SQL statement must start with INSERT INTO.";
|
||||
return false;
|
||||
@@ -165,7 +174,7 @@ public class InsertBreakdown : SqlServerInsertBreakdown
|
||||
// Parse INSERT statement using regex
|
||||
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);
|
||||
System.Text.RegularExpressions.RegexOptions.IgnoreCase | System.Text.RegularExpressions.RegexOptions.Singleline, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout);
|
||||
|
||||
if (!insertMatch.Success)
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Collections;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Text;
|
||||
using SqlServerProcedureBreakdown = Strata.SqlTools.Breakdowns.SqlServer.ProcedureBreakdown;
|
||||
using StatementParser = Strata.SqlTools.Statements.Snowflake.StatementParser;
|
||||
@@ -46,6 +47,15 @@ public class ProcedureBreakdown : SqlServerProcedureBreakdown
|
||||
Parameters = parameters ?? new Dictionary<string, string>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Re-establishes invariants after deserialization, since deserialization bypasses the
|
||||
/// constructors that normally initialize the procedure name and parameter collection
|
||||
/// (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>
|
||||
@@ -157,7 +167,7 @@ public class ProcedureBreakdown : SqlServerProcedureBreakdown
|
||||
// Check if it's a CALL statement (Snowflake syntax) or EXEC (for compatibility)
|
||||
var sqlTrimmed = sql.TrimStart();
|
||||
if (!System.Text.RegularExpressions.Regex.IsMatch(sqlTrimmed, @"^\s*(CALL|EXEC|EXECUTE)\b",
|
||||
System.Text.RegularExpressions.RegexOptions.IgnoreCase))
|
||||
System.Text.RegularExpressions.RegexOptions.IgnoreCase, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout))
|
||||
{
|
||||
errorMessage = "SQL statement must start with CALL, EXEC, or EXECUTE.";
|
||||
return false;
|
||||
@@ -174,7 +184,7 @@ public class ProcedureBreakdown : SqlServerProcedureBreakdown
|
||||
// Pattern: CALL procedureName(param => value, ...)
|
||||
var callMatch = System.Text.RegularExpressions.Regex.Match(sql,
|
||||
@"(?:CALL|EXEC|EXECUTE)\s+([^\s\(]+)(?:\s*\((.*?)\))?$",
|
||||
System.Text.RegularExpressions.RegexOptions.IgnoreCase | System.Text.RegularExpressions.RegexOptions.Singleline);
|
||||
System.Text.RegularExpressions.RegexOptions.IgnoreCase | System.Text.RegularExpressions.RegexOptions.Singleline, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout);
|
||||
|
||||
if (!callMatch.Success)
|
||||
{
|
||||
@@ -192,7 +202,7 @@ public class ProcedureBreakdown : SqlServerProcedureBreakdown
|
||||
// Parse parameters - Snowflake uses param => value syntax
|
||||
var paramMatches = System.Text.RegularExpressions.Regex.Matches(parametersText,
|
||||
@"(\w+)\s*=>\s*([^,]+)(?:,|$)",
|
||||
System.Text.RegularExpressions.RegexOptions.IgnoreCase);
|
||||
System.Text.RegularExpressions.RegexOptions.IgnoreCase, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout);
|
||||
|
||||
foreach (System.Text.RegularExpressions.Match paramMatch in paramMatches)
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Collections;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Text;
|
||||
using Strata.SqlTools.SqlBreakdown.Classes;
|
||||
using Strata.SqlTools.SqlBreakdown.Expressions;
|
||||
@@ -85,6 +86,14 @@ public class QueryBreakdown : SqlServerQueryBreakdown
|
||||
OrderByClause.Comment = orderByComments.Count > 0 ? string.Join(" ", orderByComments) : 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>
|
||||
/// Adds a parameter to the query using Snowflake's :param format.
|
||||
/// Also adds @param format for compatibility.
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Runtime.Serialization;
|
||||
using System.Text;
|
||||
using Strata.SqlTools.SqlBreakdown.Classes;
|
||||
using Strata.SqlTools.SqlBreakdown.Interfaces;
|
||||
@@ -36,6 +37,20 @@ public class QueryBreakdownCollection : SqlBreakdownCollection
|
||||
_queryBreakdowns = new List<QueryBreakdown>(queryBreakdowns ?? Enumerable.Empty<QueryBreakdown>());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates that the backing list survived deserialization, since deserialization bypasses
|
||||
/// the constructors that normally initialize it (SonarQube rule S5766).
|
||||
/// </summary>
|
||||
/// <param name="context">The streaming context for the deserialization operation.</param>
|
||||
[OnDeserialized]
|
||||
private void OnDeserialized(StreamingContext context)
|
||||
{
|
||||
if (_queryBreakdowns is null)
|
||||
{
|
||||
throw new SerializationException("Deserialized QueryBreakdownCollection is missing its backing list.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the collection of QueryBreakdown objects.
|
||||
/// </summary>
|
||||
@@ -170,7 +185,7 @@ public class QueryBreakdownCollection : SqlBreakdownCollection
|
||||
// This avoids false positives from @parameter syntax
|
||||
var stagePattern = @"@[\w~]+/";
|
||||
|
||||
if (!System.Text.RegularExpressions.Regex.IsMatch(sql, stagePattern))
|
||||
if (!System.Text.RegularExpressions.Regex.IsMatch(sql, stagePattern, System.Text.RegularExpressions.RegexOptions.None, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -184,7 +199,7 @@ public class QueryBreakdownCollection : SqlBreakdownCollection
|
||||
? $@"@~/{System.Text.RegularExpressions.Regex.Escape(stageName.TrimStart('@', '~', '/'))}/"
|
||||
: $@"@{System.Text.RegularExpressions.Regex.Escape(stageName.TrimStart('@'))}/";
|
||||
|
||||
return System.Text.RegularExpressions.Regex.IsMatch(sql, specificPattern, System.Text.RegularExpressions.RegexOptions.IgnoreCase);
|
||||
return System.Text.RegularExpressions.Regex.IsMatch(sql, specificPattern, System.Text.RegularExpressions.RegexOptions.IgnoreCase, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Collections;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Text;
|
||||
using SqlServerUpdateBreakdown = Strata.SqlTools.Breakdowns.SqlServer.UpdateBreakdown;
|
||||
using StatementParser = Strata.SqlTools.Statements.Snowflake.StatementParser;
|
||||
@@ -45,6 +46,14 @@ public class UpdateBreakdown : SqlServerUpdateBreakdown
|
||||
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>
|
||||
@@ -160,7 +169,7 @@ public class UpdateBreakdown : SqlServerUpdateBreakdown
|
||||
// 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))
|
||||
System.Text.RegularExpressions.RegexOptions.IgnoreCase, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout))
|
||||
{
|
||||
errorMessage = "SQL statement must start with UPDATE.";
|
||||
return false;
|
||||
@@ -176,7 +185,7 @@ public class UpdateBreakdown : SqlServerUpdateBreakdown
|
||||
// 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);
|
||||
System.Text.RegularExpressions.RegexOptions.IgnoreCase | System.Text.RegularExpressions.RegexOptions.Singleline, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout);
|
||||
|
||||
if (!updateMatch.Success)
|
||||
{
|
||||
|
||||
@@ -48,6 +48,18 @@ public abstract class SqlBreakdownBase : ISqlBreakdown
|
||||
/// </summary>
|
||||
public bool IsUsingFinishClause => FinishClauses.Count > 0;
|
||||
|
||||
/// <summary>
|
||||
/// Re-establishes the invariants normally guaranteed by the constructors after the object
|
||||
/// is reconstructed by deserialization. Deserialization bypasses constructors, so the
|
||||
/// collection state must be re-validated to avoid a partially-initialized object
|
||||
/// (SonarQube rule S5766).
|
||||
/// </summary>
|
||||
protected void RevalidateBreakdownState()
|
||||
{
|
||||
SetupClauses ??= new List<string>();
|
||||
FinishClauses ??= new ArrayList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the SQL breakdown as a string. Must be implemented by derived classes.
|
||||
/// </summary>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Runtime.Serialization;
|
||||
using System.Text;
|
||||
using Strata.SqlTools.SqlBreakdown.Interfaces;
|
||||
using Strata.SqlTools.SqlBreakdown.Interfaces.QueryEngine;
|
||||
@@ -35,6 +36,20 @@ public class SqlBreakdownCollection : ICollection<ISqlBreakdown>
|
||||
_breakdowns = new List<ISqlBreakdown>(breakdowns ?? Enumerable.Empty<ISqlBreakdown>());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates that the backing list survived deserialization, since deserialization bypasses
|
||||
/// the constructors that normally initialize it (SonarQube rule S5766).
|
||||
/// </summary>
|
||||
/// <param name="context">The streaming context for the deserialization operation.</param>
|
||||
[OnDeserialized]
|
||||
private void OnDeserialized(StreamingContext context)
|
||||
{
|
||||
if (_breakdowns is null)
|
||||
{
|
||||
throw new SerializationException("Deserialized SqlBreakdownCollection is missing its backing list.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the collection of SQL breakdowns.
|
||||
/// </summary>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Runtime.Serialization;
|
||||
using System.Text;
|
||||
using Strata.SqlTools.SqlBreakdown.Interfaces.Core;
|
||||
|
||||
@@ -58,6 +59,21 @@ public class SqlFilter : ISqlAppendable
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates that the expression and parameter state survived deserialization, since
|
||||
/// deserialization bypasses the constructors that normally initialize them and enforce the
|
||||
/// even parameter-name/value pairing (SonarQube rule S5766).
|
||||
/// </summary>
|
||||
/// <param name="context">The streaming context for the deserialization operation.</param>
|
||||
[OnDeserialized]
|
||||
private void OnDeserialized(StreamingContext context)
|
||||
{
|
||||
if (_sqlExpression is null || _parameterValues is null)
|
||||
{
|
||||
throw new SerializationException("Deserialized SqlFilter is missing its expression or parameter state.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the SQL expression.
|
||||
/// </summary>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using Strata.SqlTools.SqlBreakdown.Utilities;
|
||||
|
||||
namespace Strata.SqlTools.SqlBreakdown.Extensions;
|
||||
|
||||
@@ -10,7 +11,7 @@ public class StringBuilderEx
|
||||
{
|
||||
private static readonly Regex AppendFormatExRegex = new Regex(
|
||||
@"\{(?<Index>.*?)(?<Comment>!.*?)?\}",
|
||||
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
RegexOptions.Compiled | RegexOptions.IgnoreCase, RegexDefaults.MatchTimeout);
|
||||
|
||||
private readonly StringBuilder _innerStringBuilder;
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using Strata.SqlTools.SqlBreakdown.Utilities;
|
||||
|
||||
namespace Strata.SqlTools.SqlBreakdown.Extensions;
|
||||
|
||||
@@ -119,7 +120,7 @@ public static class StringExtensions
|
||||
public static bool IsGUID(this string aString)
|
||||
{
|
||||
const string pattern = "^[0-9a-zA-Z]{8}-[0-9a-zA-Z]{4}-[0-9a-zA-Z]{4}-[0-9a-zA-Z]{4}-[0-9a-zA-Z]{12}$";
|
||||
var match = Regex.Match(aString, pattern);
|
||||
var match = Regex.Match(aString, pattern, RegexOptions.None, RegexDefaults.MatchTimeout);
|
||||
return match.Success;
|
||||
}
|
||||
|
||||
|
||||
@@ -226,7 +226,7 @@ public static class GuidUtils
|
||||
|
||||
private static readonly Regex FindFirstGuidRegex = new Regex(
|
||||
"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}",
|
||||
RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
RegexOptions.IgnoreCase | RegexOptions.Compiled, RegexDefaults.MatchTimeout);
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -292,7 +292,7 @@ public static class GuidUtils
|
||||
/// <returns>A list of GUID strings found in the input.</returns>
|
||||
public static List<string> GetGuids(string value)
|
||||
{
|
||||
MatchCollection matches = Regex.Matches(value, GUID_STRING);
|
||||
MatchCollection matches = Regex.Matches(value, GUID_STRING, RegexOptions.None, RegexDefaults.MatchTimeout);
|
||||
return matches.Cast<Match>().Select(x => x.Value).ToList();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace Strata.SqlTools.SqlBreakdown.Utilities;
|
||||
|
||||
/// <summary>
|
||||
/// Shared defaults for <see cref="System.Text.RegularExpressions.Regex"/> usage across the SQL tools.
|
||||
/// </summary>
|
||||
public static class RegexDefaults
|
||||
{
|
||||
/// <summary>
|
||||
/// Default match timeout applied to regular expressions to guard against catastrophic
|
||||
/// backtracking / ReDoS denial-of-service attacks (SonarQube rule S6444). Regular expressions
|
||||
/// in this library process arbitrary SQL text, so every pattern is given a bounded execution time.
|
||||
/// </summary>
|
||||
public static readonly TimeSpan MatchTimeout = TimeSpan.FromSeconds(1);
|
||||
}
|
||||
@@ -115,7 +115,7 @@ public static partial class SqlUtils
|
||||
public static string GetSqlFriendlyName(string str, string replacement = "")
|
||||
{
|
||||
// Invalid characters --> . , ; ' ` : / \ * | ? " & % $ ! + = ( ) [ ] { } - ~ ^
|
||||
return new Regex(@"[\.,;'`:/\\*\|?""&%\$!\+=\(\)\[\]{}\-~\ \^]").Replace(str, replacement);
|
||||
return new Regex(@"[\.,;'`:/\\*\|?""&%\$!\+=\(\)\[\]{}\-~\ \^]", RegexOptions.None, RegexDefaults.MatchTimeout).Replace(str, replacement);
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -382,37 +382,37 @@ public static partial class SqlUtils
|
||||
{
|
||||
string msg = ex.Message;
|
||||
|
||||
Match match = Regex.Match(msg, @".*?Violation of UNIQUE KEY constraint (.*?) Cannot insert duplicate key in object (.*?)");
|
||||
Match match = Regex.Match(msg, @".*?Violation of UNIQUE KEY constraint (.*?) Cannot insert duplicate key in object (.*?)", RegexOptions.None, RegexDefaults.MatchTimeout);
|
||||
if (match.Success)
|
||||
{
|
||||
return WellKnownSqlError.UniqueKeyViolation;
|
||||
}
|
||||
|
||||
match = Regex.Match(msg, @".*Cannot insert duplicate key row in object.*", RegexOptions.IgnoreCase);
|
||||
match = Regex.Match(msg, @".*Cannot insert duplicate key row in object.*", RegexOptions.IgnoreCase, RegexDefaults.MatchTimeout);
|
||||
if (match.Success)
|
||||
{
|
||||
return WellKnownSqlError.UniqueKeyViolation;
|
||||
}
|
||||
|
||||
match = Regex.Match(msg, @".*Update or insert of view or function (.*?) failed because it contains a derived or constant field.*");
|
||||
match = Regex.Match(msg, @".*Update or insert of view or function (.*?) failed because it contains a derived or constant field.*", RegexOptions.None, RegexDefaults.MatchTimeout);
|
||||
if (match.Success)
|
||||
{
|
||||
return WellKnownSqlError.UpdateViewMultipleTables;
|
||||
}
|
||||
|
||||
match = Regex.Match(msg, @".*There is already an object named (.*?) in the database.*");
|
||||
match = Regex.Match(msg, @".*There is already an object named (.*?) in the database.*", RegexOptions.None, RegexDefaults.MatchTimeout);
|
||||
if (match.Success)
|
||||
{
|
||||
return WellKnownSqlError.ObjectAlreadyExists;
|
||||
}
|
||||
|
||||
match = Regex.Match(msg, @".*Cannot truncate table (.*?) because it is being referenced by a FOREIGN KEY constraint.*");
|
||||
match = Regex.Match(msg, @".*Cannot truncate table (.*?) because it is being referenced by a FOREIGN KEY constraint.*", RegexOptions.None, RegexDefaults.MatchTimeout);
|
||||
if (match.Success)
|
||||
{
|
||||
return WellKnownSqlError.TruncateTableForeignKeyReferenceError;
|
||||
}
|
||||
|
||||
match = Regex.Match(msg, @".*Could not truncate object (.*?) because it is not a table.*");
|
||||
match = Regex.Match(msg, @".*Could not truncate object (.*?) because it is not a table.*", RegexOptions.None, RegexDefaults.MatchTimeout);
|
||||
if (match.Success)
|
||||
{
|
||||
return WellKnownSqlError.TruncateTableNotATableError;
|
||||
|
||||
@@ -145,7 +145,7 @@ public class DeleteBreakdown : SqlBreakdownBase
|
||||
// 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))
|
||||
System.Text.RegularExpressions.RegexOptions.IgnoreCase, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout))
|
||||
{
|
||||
errorMessage = "SQL statement must start with DELETE.";
|
||||
return false;
|
||||
@@ -162,14 +162,14 @@ public class DeleteBreakdown : SqlBreakdownBase
|
||||
// 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);
|
||||
System.Text.RegularExpressions.RegexOptions.IgnoreCase | System.Text.RegularExpressions.RegexOptions.Singleline, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout);
|
||||
|
||||
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);
|
||||
System.Text.RegularExpressions.RegexOptions.IgnoreCase | System.Text.RegularExpressions.RegexOptions.Singleline, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout);
|
||||
|
||||
if (!deleteMatch.Success)
|
||||
{
|
||||
|
||||
@@ -158,7 +158,7 @@ public class InsertBreakdown : SqlBreakdownBase
|
||||
// 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))
|
||||
System.Text.RegularExpressions.RegexOptions.IgnoreCase, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout))
|
||||
{
|
||||
errorMessage = "SQL statement must start with INSERT INTO.";
|
||||
return false;
|
||||
@@ -175,7 +175,7 @@ public class InsertBreakdown : SqlBreakdownBase
|
||||
// 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);
|
||||
System.Text.RegularExpressions.RegexOptions.IgnoreCase | System.Text.RegularExpressions.RegexOptions.Singleline, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout);
|
||||
|
||||
if (!insertMatch.Success)
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Collections;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using Strata.SqlTools.SqlBreakdown.Classes;
|
||||
@@ -45,6 +46,20 @@ public class ProcedureBreakdown : SqlBreakdownBase
|
||||
Parameters = parameters ?? new Dictionary<string, string>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Re-establishes invariants after deserialization, since deserialization bypasses the
|
||||
/// constructors that normally initialize the procedure name and parameter collection
|
||||
/// (SonarQube rule S5766).
|
||||
/// </summary>
|
||||
/// <param name="context">The streaming context for the deserialization operation.</param>
|
||||
[OnDeserialized]
|
||||
private void OnDeserialized(StreamingContext context)
|
||||
{
|
||||
RevalidateBreakdownState();
|
||||
ProcedureName ??= new SqlClause();
|
||||
Parameters ??= new Dictionary<string, string>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the stored procedure name.
|
||||
/// </summary>
|
||||
@@ -169,7 +184,7 @@ public class ProcedureBreakdown : SqlBreakdownBase
|
||||
// Check if it's an EXEC or EXECUTE statement
|
||||
var sqlTrimmed = sql.TrimStart();
|
||||
if (!Regex.IsMatch(sqlTrimmed, @"^\s*(EXEC|EXECUTE)\b",
|
||||
RegexOptions.IgnoreCase))
|
||||
RegexOptions.IgnoreCase, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout))
|
||||
{
|
||||
errorMessage = "SQL statement must start with EXEC or EXECUTE.";
|
||||
return false;
|
||||
@@ -186,7 +201,7 @@ public class ProcedureBreakdown : SqlBreakdownBase
|
||||
// Pattern: EXEC[UTE] procedureName [@param = value, ...]
|
||||
var execMatch = Regex.Match(sql,
|
||||
@"(?:EXEC|EXECUTE)\s+([^\s@,]+)(?:\s+(.*))?$",
|
||||
RegexOptions.IgnoreCase | RegexOptions.Singleline);
|
||||
RegexOptions.IgnoreCase | RegexOptions.Singleline, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout);
|
||||
|
||||
if (!execMatch.Success)
|
||||
{
|
||||
@@ -204,7 +219,7 @@ public class ProcedureBreakdown : SqlBreakdownBase
|
||||
// Parse parameters - handle both @param = value and positional parameters
|
||||
var paramMatches = Regex.Matches(parametersText,
|
||||
@"(@\w+)\s*=\s*([^,]+)(?:,|$)",
|
||||
RegexOptions.IgnoreCase);
|
||||
RegexOptions.IgnoreCase, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout);
|
||||
|
||||
parameters = paramMatches
|
||||
.Cast<System.Text.RegularExpressions.Match>()
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Collections;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Text;
|
||||
using Strata.SqlTools.SqlBreakdown.Classes;
|
||||
using Strata.SqlTools.SqlBreakdown.Expressions;
|
||||
@@ -107,6 +108,28 @@ public class QueryBreakdown : SqlBreakdownBase, IQueryBreakdown
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Re-establishes invariants after deserialization, since deserialization bypasses the
|
||||
/// constructors that normally initialize the parameter, WITH-clause, and clause backing
|
||||
/// fields (SonarQube rule S5766).
|
||||
/// </summary>
|
||||
/// <param name="context">The streaming context for the deserialization operation.</param>
|
||||
[OnDeserialized]
|
||||
private void OnDeserialized(StreamingContext context)
|
||||
{
|
||||
RevalidateBreakdownState();
|
||||
|
||||
_parameterList ??= new List<IQueryParam>();
|
||||
_withClauses ??= new List<IWithClause>();
|
||||
_selectClause ??= new SqlExpressionClause(splitOnComma: true);
|
||||
_fromClause ??= new SqlClause();
|
||||
_whereClause ??= new SqlExpressionClause(splitOnComma: false);
|
||||
_groupByClause ??= new SqlExpressionClause(splitOnComma: true);
|
||||
_havingClause ??= new SqlExpressionClause(splitOnComma: false);
|
||||
_orderByClause ??= new SqlExpressionClause(splitOnComma: true);
|
||||
_clausesCacheDirty = true;
|
||||
}
|
||||
|
||||
#region Properties
|
||||
|
||||
/// <summary>
|
||||
@@ -1308,7 +1331,7 @@ public class QueryBreakdown : SqlBreakdownBase, IQueryBreakdown
|
||||
{
|
||||
// Try to extract position from error message
|
||||
var match = System.Text.RegularExpressions.Regex.Match(error, @"position[:\s]+(\d+)",
|
||||
System.Text.RegularExpressions.RegexOptions.IgnoreCase);
|
||||
System.Text.RegularExpressions.RegexOptions.IgnoreCase, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout);
|
||||
if (match.Success && int.TryParse(match.Groups[1].Value, out var parsedPos))
|
||||
{
|
||||
position = parsedPos;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Runtime.Serialization;
|
||||
using System.Text;
|
||||
using Strata.SqlTools.SqlBreakdown.Classes;
|
||||
using Strata.SqlTools.SqlBreakdown.Interfaces;
|
||||
@@ -33,6 +34,20 @@ public class QueryBreakdownCollection : SqlBreakdownCollection
|
||||
_queryBreakdowns = new List<QueryBreakdown>(queryBreakdowns ?? Enumerable.Empty<QueryBreakdown>());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates that the backing list survived deserialization, since deserialization bypasses
|
||||
/// the constructors that normally initialize it (SonarQube rule S5766).
|
||||
/// </summary>
|
||||
/// <param name="context">The streaming context for the deserialization operation.</param>
|
||||
[OnDeserialized]
|
||||
private void OnDeserialized(StreamingContext context)
|
||||
{
|
||||
if (_queryBreakdowns is null)
|
||||
{
|
||||
throw new SerializationException("Deserialized QueryBreakdownCollection is missing its backing list.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the collection of QueryBreakdown objects.
|
||||
/// </summary>
|
||||
|
||||
@@ -165,7 +165,7 @@ public class UpdateBreakdown : SqlBreakdownBase
|
||||
// Check if it's an UPDATE statement
|
||||
var sqlTrimmed = sql.TrimStart();
|
||||
if (!Regex.IsMatch(sqlTrimmed, @"^\s*UPDATE\b",
|
||||
RegexOptions.IgnoreCase))
|
||||
RegexOptions.IgnoreCase, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout))
|
||||
{
|
||||
errorMessage = "SQL statement must start with UPDATE.";
|
||||
return false;
|
||||
@@ -182,7 +182,7 @@ public class UpdateBreakdown : SqlBreakdownBase
|
||||
// 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);
|
||||
RegexOptions.IgnoreCase | RegexOptions.Singleline, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout);
|
||||
|
||||
if (!updateMatch.Success)
|
||||
{
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using Strata.SqlTools.SqlBreakdown.Classes;
|
||||
using Strata.SqlTools.SqlBreakdown.Enums.SQL;
|
||||
using Strata.SqlTools.SqlBreakdown.Utilities;
|
||||
|
||||
namespace Strata.SqlTools.Statements.SqlServer;
|
||||
|
||||
@@ -35,7 +36,7 @@ public class StatementParser
|
||||
// Remove SQL comments before processing
|
||||
sql = RemoveSqlComments(sql);
|
||||
// Replace multiple whitespace/newlines with single space
|
||||
sql = Regex.Replace(sql, @"\s+", " ");
|
||||
sql = Regex.Replace(sql, @"\s+", " ", RegexOptions.None, RegexDefaults.MatchTimeout);
|
||||
return sql.Trim();
|
||||
}
|
||||
|
||||
@@ -47,7 +48,7 @@ public class StatementParser
|
||||
public virtual string NormalizeSqlPreservingComments(string sql)
|
||||
{
|
||||
// Replace multiple spaces/tabs with single space, but preserve newlines for comment handling
|
||||
sql = Regex.Replace(sql, @"[ \t]+", " ");
|
||||
sql = Regex.Replace(sql, @"[ \t]+", " ", RegexOptions.None, RegexDefaults.MatchTimeout);
|
||||
// Remove leading/trailing whitespace from each line
|
||||
var lines = sql.Split(new[] { '\r', '\n' }, StringSplitOptions.None);
|
||||
sql = string.Join("\n", lines.Select(line => line.Trim()));
|
||||
@@ -238,7 +239,7 @@ public class StatementParser
|
||||
|
||||
// Simple extraction: look for statements before the main SELECT
|
||||
var selectIndex = Regex.Match(
|
||||
sql, @"\bSELECT\b", RegexOptions.IgnoreCase).Index;
|
||||
sql, @"\bSELECT\b", RegexOptions.IgnoreCase, RegexDefaults.MatchTimeout).Index;
|
||||
|
||||
if (selectIndex > 0)
|
||||
{
|
||||
@@ -275,7 +276,7 @@ public class StatementParser
|
||||
// Look for DROP TABLE or other cleanup statements after the main query
|
||||
var finishPattern = GetFinishClausePattern();
|
||||
var match = Regex.Match(
|
||||
sql, finishPattern, RegexOptions.IgnoreCase);
|
||||
sql, finishPattern, RegexOptions.IgnoreCase, RegexDefaults.MatchTimeout);
|
||||
|
||||
if (match.Success)
|
||||
{
|
||||
@@ -302,7 +303,7 @@ public class StatementParser
|
||||
|
||||
// Check if SQL starts with WITH
|
||||
var withMatch = Regex.Match(
|
||||
sql, @"^\s*WITH\b", RegexOptions.IgnoreCase);
|
||||
sql, @"^\s*WITH\b", RegexOptions.IgnoreCase, RegexDefaults.MatchTimeout);
|
||||
|
||||
if (!withMatch.Success)
|
||||
{
|
||||
@@ -330,7 +331,7 @@ public class StatementParser
|
||||
// Check if we're at a SELECT keyword at top level
|
||||
var selectMatch = Regex.Match(
|
||||
sql.Substring(i), @"^\s*SELECT\b",
|
||||
RegexOptions.IgnoreCase);
|
||||
RegexOptions.IgnoreCase, RegexDefaults.MatchTimeout);
|
||||
|
||||
if (selectMatch.Success && IsTopLevelKeyword(sql, i + selectMatch.Index))
|
||||
{
|
||||
@@ -398,7 +399,7 @@ public class StatementParser
|
||||
}
|
||||
|
||||
if (!Regex.IsMatch(sqlTrimmed, @"^\s*(WITH|SELECT)\b",
|
||||
RegexOptions.IgnoreCase))
|
||||
RegexOptions.IgnoreCase, RegexDefaults.MatchTimeout))
|
||||
{
|
||||
errorMessage = "SQL statement must start with WITH or SELECT.";
|
||||
clauses = null;
|
||||
@@ -830,7 +831,7 @@ public class StatementParser
|
||||
/// <param name="paramPattern">The regex pattern to match parameter names.</param>
|
||||
protected virtual void ExtractParameters(Dictionary<string, object> parameters, string sql, string paramPattern)
|
||||
{
|
||||
var matches = Regex.Matches(sql, paramPattern);
|
||||
var matches = Regex.Matches(sql, paramPattern, RegexOptions.None, RegexDefaults.MatchTimeout);
|
||||
|
||||
var paramNames = matches.Cast<Match>()
|
||||
.Select(match => match.Value)
|
||||
|
||||
Reference in New Issue
Block a user