Merge pull request 'Chore/sonarqube src dedup' (#20) from chore/sonarqube-src-dedup into main
SonarQube Analysis / sonarqube (push) Successful in 4m47s
SonarQube Analysis / sonarqube (push) Successful in 4m47s
Reviewed-on: #20
This commit was merged in pull request #20.
This commit is contained in:
@@ -19,29 +19,7 @@ public static class ReverseConverterExtensions
|
||||
public static LinqQueryBreakdown ToLinqQueryBreakdown(this QueryBreakdown breakdown)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(breakdown);
|
||||
|
||||
var linq = new LinqQueryBreakdown(
|
||||
breakdown.SelectClause?.Clause ?? "*",
|
||||
breakdown.FromClause?.Clause ?? string.Empty,
|
||||
breakdown.WhereClause?.Clause ?? string.Empty
|
||||
);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(breakdown.GroupByClause?.Clause))
|
||||
{
|
||||
linq.GroupByClause.Clause = breakdown.GroupByClause.Clause;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(breakdown.HavingClause?.Clause))
|
||||
{
|
||||
linq.HavingClause.Clause = breakdown.HavingClause.Clause;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(breakdown.OrderByClause?.Clause))
|
||||
{
|
||||
linq.OrderByClause.Clause = breakdown.OrderByClause.Clause;
|
||||
}
|
||||
|
||||
return linq;
|
||||
return BuildLinqBreakdownFrom(breakdown);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -52,29 +30,7 @@ public static class ReverseConverterExtensions
|
||||
public static LinqQueryBreakdown ToLinqQueryBreakdown(this PostgreSqlBreakdown breakdown)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(breakdown);
|
||||
|
||||
var linq = new LinqQueryBreakdown(
|
||||
breakdown.SelectClause?.Clause ?? "*",
|
||||
breakdown.FromClause?.Clause ?? string.Empty,
|
||||
breakdown.WhereClause?.Clause ?? string.Empty
|
||||
);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(breakdown.GroupByClause?.Clause))
|
||||
{
|
||||
linq.GroupByClause.Clause = breakdown.GroupByClause.Clause;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(breakdown.HavingClause?.Clause))
|
||||
{
|
||||
linq.HavingClause.Clause = breakdown.HavingClause.Clause;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(breakdown.OrderByClause?.Clause))
|
||||
{
|
||||
linq.OrderByClause.Clause = breakdown.OrderByClause.Clause;
|
||||
}
|
||||
|
||||
return linq;
|
||||
return BuildLinqBreakdownFrom(breakdown);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -85,7 +41,13 @@ public static class ReverseConverterExtensions
|
||||
public static LinqQueryBreakdown ToLinqQueryBreakdown(this SnowflakeBreakdown breakdown)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(breakdown);
|
||||
return BuildLinqBreakdownFrom(breakdown);
|
||||
}
|
||||
|
||||
// Shared body — PostgreSql/Snowflake QueryBreakdown derive from SqlServer.QueryBreakdown,
|
||||
// so all three public overloads can flow through this single helper.
|
||||
private static LinqQueryBreakdown BuildLinqBreakdownFrom(QueryBreakdown breakdown)
|
||||
{
|
||||
var linq = new LinqQueryBreakdown(
|
||||
breakdown.SelectClause?.Clause ?? "*",
|
||||
breakdown.FromClause?.Clause ?? string.Empty,
|
||||
|
||||
@@ -361,13 +361,7 @@ public class ExpressionGenerator : IVisitor<string>
|
||||
}
|
||||
|
||||
private static string TruncateText(string text, int maxLength)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text) || text.Length <= maxLength)
|
||||
{
|
||||
return text;
|
||||
}
|
||||
return string.Concat(text.AsSpan(0, maxLength), "...");
|
||||
}
|
||||
=> Internal.MarkdownTextHelpers.TruncateText(text, maxLength);
|
||||
|
||||
#region IVisitor Implementation
|
||||
|
||||
@@ -485,37 +479,23 @@ public class ExpressionGenerator : IVisitor<string>
|
||||
}
|
||||
|
||||
public string VisitInExpression(InExpression inExpression)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine($"{Indent()}IN:");
|
||||
_indentLevel++;
|
||||
sb.AppendLine($"{Indent()}Search Expression:");
|
||||
_indentLevel++;
|
||||
sb.AppendLine(inExpression.SearchExpression.Accept(this));
|
||||
_indentLevel--;
|
||||
sb.AppendLine($"{Indent()}Values:");
|
||||
_indentLevel++;
|
||||
foreach (var value in inExpression.ValuesToCompare)
|
||||
{
|
||||
sb.AppendLine(value.Accept(this));
|
||||
}
|
||||
_indentLevel--;
|
||||
_indentLevel--;
|
||||
return sb.ToString();
|
||||
}
|
||||
=> RenderInList("IN", inExpression.SearchExpression, inExpression.ValuesToCompare);
|
||||
|
||||
public string VisitNotInExpression(NotInExpression inExpression)
|
||||
=> RenderInList("NOT IN", inExpression.SearchExpression, inExpression.ValuesToCompare);
|
||||
|
||||
private string RenderInList(string label, Expression searchExpression, IEnumerable<Expression> values)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine($"{Indent()}NOT IN:");
|
||||
sb.AppendLine($"{Indent()}{label}:");
|
||||
_indentLevel++;
|
||||
sb.AppendLine($"{Indent()}Search Expression:");
|
||||
_indentLevel++;
|
||||
sb.AppendLine(inExpression.SearchExpression.Accept(this));
|
||||
sb.AppendLine(searchExpression.Accept(this));
|
||||
_indentLevel--;
|
||||
sb.AppendLine($"{Indent()}Values:");
|
||||
_indentLevel++;
|
||||
foreach (var value in inExpression.ValuesToCompare)
|
||||
foreach (var value in values)
|
||||
{
|
||||
sb.AppendLine(value.Accept(this));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
namespace Strata.SqlTools.Markdown.Internal;
|
||||
|
||||
/// <summary>
|
||||
/// Small text utilities shared by the Markdown generators.
|
||||
/// Kept <c>internal</c> so they don't widen the public surface of this assembly.
|
||||
/// </summary>
|
||||
internal static class MarkdownTextHelpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Truncates a string to <paramref name="maxLength"/> characters, appending an ellipsis when truncated.
|
||||
/// Returns the input unchanged when null/whitespace or already short enough.
|
||||
/// </summary>
|
||||
public static string TruncateText(string text, int maxLength)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text) || text.Length <= maxLength)
|
||||
{
|
||||
return text;
|
||||
}
|
||||
return string.Concat(text.AsSpan(0, maxLength), "...");
|
||||
}
|
||||
}
|
||||
@@ -191,17 +191,7 @@ public static class QueryBreakdownGenerator
|
||||
.Replace(">", ">");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Truncates text to a maximum length and adds ellipsis if needed.
|
||||
/// </summary>
|
||||
private static string TruncateText(string text, int maxLength)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text) || text.Length <= maxLength)
|
||||
{
|
||||
return text;
|
||||
}
|
||||
|
||||
return string.Concat(text.AsSpan(0, maxLength), "...");
|
||||
}
|
||||
=> Internal.MarkdownTextHelpers.TruncateText(text, maxLength);
|
||||
}
|
||||
|
||||
|
||||
@@ -112,18 +112,8 @@ public static class SqlStatementGenerator
|
||||
.Replace("\r", "");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Truncates text to a maximum length.
|
||||
/// </summary>
|
||||
private static string TruncateText(string text, int maxLength)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text) || text.Length <= maxLength)
|
||||
{
|
||||
return text;
|
||||
}
|
||||
|
||||
return string.Concat(text.AsSpan(0, maxLength), "...");
|
||||
}
|
||||
=> Internal.MarkdownTextHelpers.TruncateText(text, maxLength);
|
||||
|
||||
/// <summary>
|
||||
/// Cleans table name for use in Mermaid diagrams.
|
||||
|
||||
@@ -121,39 +121,7 @@ public class StatementExpressionParser : SqlServerStatementExpressionParser
|
||||
// Check if this is a qualified column name (e.g., users.id)
|
||||
if (reader.TokenType == TokenType.Operator && reader.TokenValue == ".")
|
||||
{
|
||||
// Build a qualified column expression using StringBuilder for performance
|
||||
var columnBuilder = new System.Text.StringBuilder(startingToken);
|
||||
while (reader.TokenType == TokenType.Operator && reader.TokenValue == ".")
|
||||
{
|
||||
reader.Read(); // Skip the dot
|
||||
|
||||
if (reader.TokenType == TokenType.String || reader.TokenType == TokenType.ColumnIdentifier)
|
||||
{
|
||||
columnBuilder.Append('.').Append(reader.TokenValue);
|
||||
reader.Read();
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidSyntaxException(
|
||||
$"Invalid syntax at position {reader.Position}. Expected column identifier after dot.");
|
||||
}
|
||||
}
|
||||
|
||||
var columnToken = columnBuilder.ToString();
|
||||
|
||||
// Return a column expression for the qualified name
|
||||
var dataColumnId = GetColumnIdFromToken(columnToken);
|
||||
var tableSource = new RegisteredTableSource(1001, "FW", "DEPARTMENT", "DEPT");
|
||||
return dataColumnId switch
|
||||
{
|
||||
1 => new RegisteredTableColumnExpression(dataColumnId, "DEPARTMENT_ID", tableSource),
|
||||
2 => new RegisteredTableColumnExpression(dataColumnId, "NAME", tableSource),
|
||||
3 => new RegisteredTableColumnExpression(dataColumnId, "REVENUE", tableSource),
|
||||
4 => new RegisteredTableColumnExpression(dataColumnId, "DISCHARGE_DATE", tableSource),
|
||||
586883 => new RegisteredTableColumnExpression(dataColumnId, "FIXED_COST", tableSource),
|
||||
586664 => new RegisteredTableColumnExpression(dataColumnId, "VARIABLE_COST", tableSource),
|
||||
_ => new RegisteredTableColumnExpression(dataColumnId, GetDefaultColumnName(columnToken), tableSource)
|
||||
};
|
||||
return BuildQualifiedColumnExpression(new System.Text.StringBuilder(startingToken), reader);
|
||||
}
|
||||
|
||||
// Not a qualified column, treat as a string expression
|
||||
@@ -182,9 +150,14 @@ public class StatementExpressionParser : SqlServerStatementExpressionParser
|
||||
{
|
||||
var columnBuilder = new System.Text.StringBuilder(reader.TokenValue);
|
||||
reader.Read();
|
||||
return BuildQualifiedColumnExpression(columnBuilder, reader);
|
||||
}
|
||||
|
||||
// Handle qualified names: table.column, "Table"."Column", etc.
|
||||
// Keep reading while we see dot-separated identifiers
|
||||
// Consumes dot-separated identifier segments from the reader, appending each to the seeded builder,
|
||||
// then maps the resulting qualified name to a RegisteredTableColumnExpression. Shared between
|
||||
// HandleStringToken (qualified column path) and GrabColumnExpression (entry-point path).
|
||||
private RegisteredTableColumnExpression BuildQualifiedColumnExpression(System.Text.StringBuilder columnBuilder, IStatementReader reader)
|
||||
{
|
||||
while (reader.TokenType == TokenType.Operator && reader.TokenValue == ".")
|
||||
{
|
||||
reader.Read(); // Skip the dot
|
||||
@@ -202,8 +175,6 @@ public class StatementExpressionParser : SqlServerStatementExpressionParser
|
||||
}
|
||||
|
||||
var columnToken = columnBuilder.ToString();
|
||||
|
||||
// Use base implementation to get the column expression
|
||||
var dataColumnId = GetColumnIdFromToken(columnToken);
|
||||
var tableSource = new RegisteredTableSource(1001, "FW", "DEPARTMENT", "DEPT");
|
||||
return dataColumnId switch
|
||||
|
||||
@@ -84,23 +84,7 @@ public class StatementParser : SqlServerStatementParser
|
||||
/// <param name="position">Current position in the SQL string.</param>
|
||||
/// <returns>Token and new position after the token.</returns>
|
||||
protected override ((TokenType type, string value, int position) token, int newPosition) HandleDoubleQuote(string sql, int position)
|
||||
{
|
||||
// PostgreSQL: double-quote is an identifier (like [brackets] in T-SQL)
|
||||
int start = position;
|
||||
position++; // Skip opening quote
|
||||
var identifier = new StringBuilder();
|
||||
while (position < sql.Length && sql[position] != '"')
|
||||
{
|
||||
identifier.Append(sql[position]);
|
||||
position++;
|
||||
}
|
||||
if (position < sql.Length)
|
||||
{
|
||||
position++; // Skip closing quote
|
||||
}
|
||||
|
||||
return ((TokenType.ColumnIdentifier, identifier.ToString(), start), position);
|
||||
}
|
||||
=> HandleDoubleQuoteAsIdentifier(sql, position);
|
||||
|
||||
/// <summary>
|
||||
/// Post-processes extracted clauses to handle PostgreSQL-specific LIMIT and OFFSET clauses.
|
||||
|
||||
@@ -111,12 +111,6 @@ public class DeleteBreakdown : SqlServerDeleteBreakdown
|
||||
|
||||
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)
|
||||
{
|
||||
@@ -138,25 +132,13 @@ public class DeleteBreakdown : SqlServerDeleteBreakdown
|
||||
return true;
|
||||
}
|
||||
|
||||
var parser = SnowflakeParserInstance;
|
||||
sql = parser.NormalizeSqlPreservingComments(sql);
|
||||
|
||||
// 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, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout))
|
||||
if (!Strata.SqlTools.Statements.SqlServer.ParsePreparation.TryRunPrelude(
|
||||
sql, SnowflakeParserInstance, @"^\s*DELETE\b", "DELETE",
|
||||
out sql, out var setupClauses, out var finishClauses, out errorMessage))
|
||||
{
|
||||
errorMessage = "SQL statement must start with DELETE.";
|
||||
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);
|
||||
|
||||
// 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+(.*))?$",
|
||||
|
||||
@@ -115,12 +115,6 @@ public class InsertBreakdown : SqlServerInsertBreakdown
|
||||
|
||||
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)
|
||||
{
|
||||
@@ -142,25 +136,13 @@ public class InsertBreakdown : SqlServerInsertBreakdown
|
||||
return true;
|
||||
}
|
||||
|
||||
var parser = SnowflakeParserInstance;
|
||||
sql = parser.NormalizeSqlPreservingComments(sql);
|
||||
|
||||
// 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, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout))
|
||||
if (!Strata.SqlTools.Statements.SqlServer.ParsePreparation.TryRunPrelude(
|
||||
sql, SnowflakeParserInstance, @"^\s*INSERT\s+INTO\b", "INSERT INTO",
|
||||
out sql, out var setupClauses, out var finishClauses, out errorMessage))
|
||||
{
|
||||
errorMessage = "SQL statement must start with INSERT INTO.";
|
||||
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 INSERT statement using regex
|
||||
var insertMatch = System.Text.RegularExpressions.Regex.Match(sql,
|
||||
@"INSERT\s+INTO\s+([^\(\s]+)\s*\(([^\)]*)\)\s*VALUES\s*\(([^\)]*)\)",
|
||||
|
||||
@@ -138,37 +138,19 @@ public class ProcedureBreakdown : SqlServerProcedureBreakdown
|
||||
result = null!;
|
||||
errorMessage = null!;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(sql))
|
||||
{
|
||||
errorMessage = "SQL statement cannot be null or empty.";
|
||||
return false;
|
||||
}
|
||||
|
||||
// If Microsoft SQL mode, delegate to base class
|
||||
if (isMicrosoftSql)
|
||||
{
|
||||
return TryParseMicrosoftSql(sql, out result, out errorMessage);
|
||||
}
|
||||
|
||||
var parser = SnowflakeParserInstance;
|
||||
sql = parser.NormalizeSqlPreservingComments(sql);
|
||||
|
||||
// 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, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout))
|
||||
if (!Strata.SqlTools.Statements.SqlServer.ParsePreparation.TryRunPrelude(
|
||||
sql, SnowflakeParserInstance, @"^\s*(CALL|EXEC|EXECUTE)\b", "CALL, EXEC, or EXECUTE",
|
||||
out sql, out var setupClauses, out var finishClauses, out errorMessage))
|
||||
{
|
||||
errorMessage = "SQL statement must start with CALL, EXEC, or EXECUTE.";
|
||||
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 CALL statement - match procedure name and parameters
|
||||
// Pattern: CALL procedureName(param => value, ...)
|
||||
var callMatch = System.Text.RegularExpressions.Regex.Match(sql,
|
||||
|
||||
@@ -231,28 +231,7 @@ public class QueryBreakdown : SqlServerQueryBreakdown
|
||||
var visitor = isMicrosoftSql
|
||||
? (IVisitor<string>)new SqlServerCommandVisitor()
|
||||
: new CommandVisitor();
|
||||
var sql = expression.Accept(visitor);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(WhereClause.Clause))
|
||||
{
|
||||
WhereClause.Clause = sql;
|
||||
}
|
||||
else
|
||||
{
|
||||
WhereClause.Clause = $"{WhereClause.Clause} {operation} {sql}";
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(comment))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(WhereClause.Comment))
|
||||
{
|
||||
WhereClause.Comment = comment;
|
||||
}
|
||||
else
|
||||
{
|
||||
WhereClause.Comment = $"{WhereClause.Comment} {comment}";
|
||||
}
|
||||
}
|
||||
AppendToClause(WhereClause, expression.Accept(visitor), operation, comment);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -297,29 +276,9 @@ public class QueryBreakdown : SqlServerQueryBreakdown
|
||||
|
||||
// Extract comments from the incoming SQL
|
||||
var cleanSql = parser.ExtractSqlComments(sql, out var comments);
|
||||
var commentText = comments.Count > 0 ? string.Join(" ", comments) : null;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(WhereClause.Clause))
|
||||
{
|
||||
WhereClause.Clause = cleanSql.Trim();
|
||||
}
|
||||
else
|
||||
{
|
||||
WhereClause.Clause = $"{WhereClause.Clause} {operation} {cleanSql.Trim()}";
|
||||
}
|
||||
|
||||
// Merge comments
|
||||
if (comments.Count > 0)
|
||||
{
|
||||
var newComment = string.Join(" ", comments);
|
||||
if (string.IsNullOrWhiteSpace(WhereClause.Comment))
|
||||
{
|
||||
WhereClause.Comment = newComment;
|
||||
}
|
||||
else
|
||||
{
|
||||
WhereClause.Comment = $"{WhereClause.Comment} {newComment}";
|
||||
}
|
||||
}
|
||||
AppendToClause(WhereClause, cleanSql.Trim(), operation, commentText);
|
||||
|
||||
// Extract and add parameters from the WHERE clause using appropriate parser
|
||||
ExtractAndAddParametersWithParser(cleanSql, parser);
|
||||
@@ -476,28 +435,7 @@ public class QueryBreakdown : SqlServerQueryBreakdown
|
||||
var visitor = isMicrosoftSql
|
||||
? (IVisitor<string>)new SqlServerCommandVisitor()
|
||||
: new CommandVisitor();
|
||||
var sql = expression.Accept(visitor);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(HavingClause.Clause))
|
||||
{
|
||||
HavingClause.Clause = sql;
|
||||
}
|
||||
else
|
||||
{
|
||||
HavingClause.Clause = $"{HavingClause.Clause} {operation} {sql}";
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(comment))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(HavingClause.Comment))
|
||||
{
|
||||
HavingClause.Comment = comment;
|
||||
}
|
||||
else
|
||||
{
|
||||
HavingClause.Comment = $"{HavingClause.Comment} {comment}";
|
||||
}
|
||||
}
|
||||
AppendToClause(HavingClause, expression.Accept(visitor), operation, comment);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -125,12 +125,6 @@ public class UpdateBreakdown : SqlServerUpdateBreakdown
|
||||
|
||||
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)
|
||||
{
|
||||
@@ -154,24 +148,13 @@ public class UpdateBreakdown : SqlServerUpdateBreakdown
|
||||
}
|
||||
|
||||
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))
|
||||
if (!Strata.SqlTools.Statements.SqlServer.ParsePreparation.TryRunPrelude(
|
||||
sql, parser, @"^\s*UPDATE\b", "UPDATE",
|
||||
out sql, out var setupClauses, out var finishClauses, out errorMessage))
|
||||
{
|
||||
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+(.*))?$",
|
||||
|
||||
@@ -91,23 +91,7 @@ public class StatementParser : SqlServerStatementParser
|
||||
/// <param name="position">Current position in the SQL string.</param>
|
||||
/// <returns>Token and new position after the token.</returns>
|
||||
protected override ((TokenType type, string value, int position) token, int newPosition) HandleDoubleQuote(string sql, int position)
|
||||
{
|
||||
// Snowflake: double-quote is an identifier (like [brackets])
|
||||
int start = position;
|
||||
position++; // Skip opening quote
|
||||
var identifier = new StringBuilder();
|
||||
while (position < sql.Length && sql[position] != '"')
|
||||
{
|
||||
identifier.Append(sql[position]);
|
||||
position++;
|
||||
}
|
||||
if (position < sql.Length)
|
||||
{
|
||||
position++; // Skip closing quote
|
||||
}
|
||||
|
||||
return ((TokenType.ColumnIdentifier, identifier.ToString(), start), position);
|
||||
}
|
||||
=> HandleDoubleQuoteAsIdentifier(sql, position);
|
||||
|
||||
/// <summary>
|
||||
/// Post-processes extracted clauses to handle Snowflake-specific LIMIT clause.
|
||||
|
||||
@@ -53,6 +53,42 @@ public abstract class SqlBreakdownBase : ISqlBreakdown
|
||||
/// <returns>The SQL query string.</returns>
|
||||
protected abstract string GetSqlBreakdown();
|
||||
|
||||
/// <summary>
|
||||
/// Appends a SQL fragment to an <see cref="ISqlClause"/> with the given logical operation
|
||||
/// (e.g. <c>"and"</c> / <c>"or"</c>), and optionally merges an associated comment.
|
||||
/// If the target clause is empty, the fragment is set as the clause; otherwise it is
|
||||
/// joined with the operation.
|
||||
/// </summary>
|
||||
/// <param name="clause">The clause being built up (e.g. <c>WhereClause</c>, <c>HavingClause</c>).</param>
|
||||
/// <param name="sql">The already-generated SQL fragment to append.</param>
|
||||
/// <param name="operation">The logical operation used to join with the existing content.</param>
|
||||
/// <param name="comment">Optional comment to merge into <c>clause.Comment</c>.</param>
|
||||
protected static void AppendToClause(ISqlClause clause, string sql, string operation, string? comment)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(clause.Clause))
|
||||
{
|
||||
clause.Clause = sql;
|
||||
}
|
||||
else
|
||||
{
|
||||
clause.Clause = $"{clause.Clause} {operation} {sql}";
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(comment))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(clause.Comment))
|
||||
{
|
||||
clause.Comment = comment;
|
||||
}
|
||||
else
|
||||
{
|
||||
clause.Comment = $"{clause.Comment} {comment}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a LINQ to SQL query of the specified type based on this breakdown.
|
||||
/// </summary>
|
||||
|
||||
@@ -146,31 +146,14 @@ public class DeleteBreakdown : SqlBreakdownBase
|
||||
result = null!;
|
||||
errorMessage = null!;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(sql))
|
||||
{
|
||||
errorMessage = "SQL statement cannot be null or empty.";
|
||||
return false;
|
||||
}
|
||||
|
||||
var parser = new StatementParser();
|
||||
sql = parser.NormalizeSqlPreservingComments(sql);
|
||||
|
||||
// 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, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout))
|
||||
if (!Strata.SqlTools.Statements.SqlServer.ParsePreparation.TryRunPrelude(
|
||||
sql, parser, @"^\s*DELETE\b", "DELETE",
|
||||
out sql, out var setupClauses, out var finishClauses, out errorMessage))
|
||||
{
|
||||
errorMessage = "SQL statement must start with DELETE.";
|
||||
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 DELETE statement using regex
|
||||
// Pattern: DELETE [table_alias] FROM table WHERE condition
|
||||
var deleteMatch = System.Text.RegularExpressions.Regex.Match(sql,
|
||||
|
||||
@@ -145,31 +145,13 @@ public class InsertBreakdown : SqlBreakdownBase
|
||||
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(sql))
|
||||
if (!Strata.SqlTools.Statements.SqlServer.ParsePreparation.TryRunPrelude(
|
||||
sql, new StatementParser(), @"^\s*INSERT\s+INTO\b", "INSERT INTO",
|
||||
out sql, out var setupClauses, out var finishClauses, out errorMessage))
|
||||
{
|
||||
errorMessage = "SQL statement cannot be null or empty.";
|
||||
return false;
|
||||
}
|
||||
|
||||
var parser = new StatementParser();
|
||||
sql = parser.NormalizeSqlPreservingComments(sql);
|
||||
|
||||
// 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, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout))
|
||||
{
|
||||
errorMessage = "SQL statement must start with INSERT INTO.";
|
||||
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 INSERT statement using regex
|
||||
// Pattern: INSERT INTO table (columns) VALUES (values)
|
||||
var insertMatch = System.Text.RegularExpressions.Regex.Match(sql,
|
||||
|
||||
@@ -156,31 +156,13 @@ public class ProcedureBreakdown : SqlBreakdownBase
|
||||
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(sql))
|
||||
if (!ParsePreparation.TryRunPrelude(
|
||||
sql, new StatementParser(), @"^\s*(EXEC|EXECUTE)\b", "EXEC or EXECUTE",
|
||||
out sql, out var setupClauses, out var finishClauses, out errorMessage))
|
||||
{
|
||||
errorMessage = "SQL statement cannot be null or empty.";
|
||||
return false;
|
||||
}
|
||||
|
||||
var parser = new StatementParser();
|
||||
sql = parser.NormalizeSqlPreservingComments(sql);
|
||||
|
||||
// Check if it's an EXEC or EXECUTE statement
|
||||
var sqlTrimmed = sql.TrimStart();
|
||||
if (!Regex.IsMatch(sqlTrimmed, @"^\s*(EXEC|EXECUTE)\b",
|
||||
RegexOptions.IgnoreCase, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout))
|
||||
{
|
||||
errorMessage = "SQL statement must start with EXEC or EXECUTE.";
|
||||
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 EXEC statement - match procedure name and parameters
|
||||
// Pattern: EXEC[UTE] procedureName [@param = value, ...]
|
||||
var execMatch = Regex.Match(sql,
|
||||
|
||||
@@ -822,29 +822,9 @@ public class QueryBreakdown : SqlBreakdownBase, IQueryBreakdown
|
||||
|
||||
// Extract comments from the incoming SQL
|
||||
var cleanSql = Parser.ExtractSqlComments(sql, out var comments);
|
||||
var commentText = comments.Count > 0 ? string.Join(" ", comments) : null;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(WhereClause.Clause))
|
||||
{
|
||||
WhereClause.Clause = cleanSql.Trim();
|
||||
}
|
||||
else
|
||||
{
|
||||
WhereClause.Clause = $"{WhereClause.Clause} {operation} {cleanSql.Trim()}";
|
||||
}
|
||||
|
||||
// Merge comments
|
||||
if (comments.Count > 0)
|
||||
{
|
||||
var newComment = string.Join(" ", comments);
|
||||
if (string.IsNullOrWhiteSpace(WhereClause.Comment))
|
||||
{
|
||||
WhereClause.Comment = newComment;
|
||||
}
|
||||
else
|
||||
{
|
||||
WhereClause.Comment = $"{WhereClause.Comment} {newComment}";
|
||||
}
|
||||
}
|
||||
AppendToClause(WhereClause, cleanSql.Trim(), operation, commentText);
|
||||
|
||||
// Extract and add parameters from the WHERE clause
|
||||
ExtractAndAddParameters(cleanSql);
|
||||
@@ -901,28 +881,7 @@ public class QueryBreakdown : SqlBreakdownBase, IQueryBreakdown
|
||||
}
|
||||
|
||||
var visitor = new CommandVisitor();
|
||||
var sql = expression.Accept(visitor);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(WhereClause.Clause))
|
||||
{
|
||||
WhereClause.Clause = sql;
|
||||
}
|
||||
else
|
||||
{
|
||||
WhereClause.Clause = $"{WhereClause.Clause} {operation} {sql}";
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(comment))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(WhereClause.Comment))
|
||||
{
|
||||
WhereClause.Comment = comment;
|
||||
}
|
||||
else
|
||||
{
|
||||
WhereClause.Comment = $"{WhereClause.Comment} {comment}";
|
||||
}
|
||||
}
|
||||
AppendToClause(WhereClause, expression.Accept(visitor), operation, comment);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -1013,28 +972,7 @@ public class QueryBreakdown : SqlBreakdownBase, IQueryBreakdown
|
||||
}
|
||||
|
||||
var visitor = new CommandVisitor();
|
||||
var sql = expression.Accept(visitor);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(HavingClause.Clause))
|
||||
{
|
||||
HavingClause.Clause = sql;
|
||||
}
|
||||
else
|
||||
{
|
||||
HavingClause.Clause = $"{HavingClause.Clause} {operation} {sql}";
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(comment))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(HavingClause.Comment))
|
||||
{
|
||||
HavingClause.Comment = comment;
|
||||
}
|
||||
else
|
||||
{
|
||||
HavingClause.Comment = $"{HavingClause.Comment} {comment}";
|
||||
}
|
||||
}
|
||||
AppendToClause(HavingClause, expression.Accept(visitor), operation, comment);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -152,31 +152,14 @@ public class UpdateBreakdown : SqlBreakdownBase
|
||||
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(sql))
|
||||
{
|
||||
errorMessage = "SQL statement cannot be null or empty.";
|
||||
return false;
|
||||
}
|
||||
|
||||
var parser = new StatementParser();
|
||||
sql = parser.NormalizeSqlPreservingComments(sql);
|
||||
|
||||
// Check if it's an UPDATE statement
|
||||
var sqlTrimmed = sql.TrimStart();
|
||||
if (!Regex.IsMatch(sqlTrimmed, @"^\s*UPDATE\b",
|
||||
RegexOptions.IgnoreCase, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout))
|
||||
if (!ParsePreparation.TryRunPrelude(
|
||||
sql, parser, @"^\s*UPDATE\b", "UPDATE",
|
||||
out sql, out var setupClauses, out var finishClauses, out errorMessage))
|
||||
{
|
||||
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
|
||||
// Pattern: UPDATE table SET column=value [FROM table] [WHERE condition]
|
||||
var updateMatch = Regex.Match(sql,
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
using System.Collections;
|
||||
using System.Text.RegularExpressions;
|
||||
using Strata.SqlTools.SqlBreakdown.Utilities;
|
||||
|
||||
namespace Strata.SqlTools.Statements.SqlServer;
|
||||
|
||||
/// <summary>
|
||||
/// Shared parse-prelude scaffolding for <c>TryParse</c> implementations on
|
||||
/// dialect-specific breakdown classes (Insert/Update/Delete/Procedure).
|
||||
/// Each breakdown's prelude was historically 17–30 lines of identical code:
|
||||
/// null check, comment-preserving normalize, statement-prefix regex check,
|
||||
/// then setup/finish clause extraction. This helper performs all of it.
|
||||
/// </summary>
|
||||
public static class ParsePreparation
|
||||
{
|
||||
/// <summary>
|
||||
/// Runs the standard <c>TryParse</c> prelude:
|
||||
/// validates that <paramref name="sql"/> is non-empty, normalizes it with
|
||||
/// <see cref="StatementParser.NormalizeSqlPreservingComments"/>, requires the
|
||||
/// trimmed statement to match <paramref name="prefixRegex"/>, and pulls out
|
||||
/// the setup/finish clauses.
|
||||
/// </summary>
|
||||
/// <param name="sql">The raw SQL being parsed.</param>
|
||||
/// <param name="parser">The dialect's <see cref="StatementParser"/> (or subclass).</param>
|
||||
/// <param name="prefixRegex">A regex anchored at the start that the trimmed SQL must match (e.g. <c>@"^\s*INSERT\s+INTO\b"</c>).</param>
|
||||
/// <param name="prefixDescription">Human-readable name of the expected prefix used in the error message (e.g. <c>"INSERT INTO"</c>).</param>
|
||||
/// <param name="normalizedSql">On success, the SQL with comments preserved and setup/finish clauses removed.</param>
|
||||
/// <param name="setupClauses">On success, the list of setup-clause strings extracted from the front of the SQL.</param>
|
||||
/// <param name="finishClauses">On success, the finish clauses extracted from the back of the SQL.</param>
|
||||
/// <param name="errorMessage">On failure, a human-readable explanation suitable for surfacing through the caller's <c>out string errorMessage</c>.</param>
|
||||
/// <returns><c>true</c> if the prelude completed and <paramref name="normalizedSql"/> is ready for dialect-specific matching; <c>false</c> otherwise.</returns>
|
||||
public static bool TryRunPrelude(
|
||||
string sql,
|
||||
StatementParser parser,
|
||||
string prefixRegex,
|
||||
string prefixDescription,
|
||||
out string normalizedSql,
|
||||
out List<string> setupClauses,
|
||||
out ArrayList finishClauses,
|
||||
out string errorMessage)
|
||||
{
|
||||
normalizedSql = null!;
|
||||
setupClauses = null!;
|
||||
finishClauses = null!;
|
||||
errorMessage = null!;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(sql))
|
||||
{
|
||||
errorMessage = "SQL statement cannot be null or empty.";
|
||||
return false;
|
||||
}
|
||||
|
||||
normalizedSql = parser.NormalizeSqlPreservingComments(sql);
|
||||
|
||||
if (!Regex.IsMatch(normalizedSql.TrimStart(), prefixRegex, RegexOptions.IgnoreCase, RegexDefaults.MatchTimeout))
|
||||
{
|
||||
errorMessage = $"SQL statement must start with {prefixDescription}.";
|
||||
return false;
|
||||
}
|
||||
|
||||
setupClauses = new List<string>();
|
||||
normalizedSql = parser.ExtractSetupClauses(normalizedSql, setupClauses);
|
||||
|
||||
finishClauses = new ArrayList();
|
||||
normalizedSql = parser.ExtractFinishClauses(normalizedSql, finishClauses);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -649,6 +649,32 @@ public class StatementParser
|
||||
return ((TokenType.String, str.ToString(), start), position);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper used by dialects (PostgreSQL, Snowflake) where double-quoted text is an
|
||||
/// identifier rather than a string literal. Reads from the opening quote at
|
||||
/// <paramref name="position"/> and returns the inner text as a
|
||||
/// <see cref="TokenType.ColumnIdentifier"/> token.
|
||||
/// </summary>
|
||||
/// <param name="sql">The SQL statement being tokenized.</param>
|
||||
/// <param name="position">The current position in the SQL string (must point at the opening <c>"</c>).</param>
|
||||
/// <returns>The identifier token and the new position past the closing <c>"</c>.</returns>
|
||||
protected static ((TokenType type, string value, int position) token, int newPosition) HandleDoubleQuoteAsIdentifier(string sql, int position)
|
||||
{
|
||||
int start = position;
|
||||
position++; // Skip opening quote
|
||||
var identifier = new StringBuilder();
|
||||
while (position < sql.Length && sql[position] != '"')
|
||||
{
|
||||
identifier.Append(sql[position]);
|
||||
position++;
|
||||
}
|
||||
if (position < sql.Length)
|
||||
{
|
||||
position++; // Skip closing quote
|
||||
}
|
||||
return ((TokenType.ColumnIdentifier, identifier.ToString(), start), position);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Processes a list of tokens to find SQL keywords at the top level (outside parentheses).
|
||||
/// </summary>
|
||||
|
||||
Reference in New Issue
Block a user