The `QueryBreakdown(string select, string from, ...)` constructors on
PostgreSql.QueryBreakdown and Snowflake.QueryBreakdown each ran the
same six-line pattern twice (once per clause): call
`parser.ExtractSqlComments`, set the clause to the trimmed result,
join the comment list into the Comment property.
New `StatementParser.PopulateClauseWithComments(rawText, target)`
instance method does both halves. Each ctor now reads:
parser.PopulateClauseWithComments(selectClause, SelectClause);
parser.PopulateClauseWithComments(fromClause, FromClause);
Same behavior; the helper is a pure refactor of existing semantics.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
279 lines
12 KiB
C#
279 lines
12 KiB
C#
using System.Collections;
|
|
using Strata.SqlTools.SqlBreakdown.Expressions;
|
|
using Strata.SqlTools.SqlBreakdown.Interfaces.Core;
|
|
using CommandVisitor = Strata.SqlTools.Visitors.PostgreSql.CommandVisitor;
|
|
using SqlClause = Strata.SqlTools.SqlBreakdown.Classes.SqlClause;
|
|
using SqlExpressionClause = Strata.SqlTools.SqlBreakdown.Classes.SqlExpressionClause;
|
|
using SqlServerCommandVisitor = Strata.SqlTools.Visitors.SqlServer.CommandVisitor;
|
|
using SqlServerQueryBreakdown = Strata.SqlTools.Breakdowns.SqlServer.QueryBreakdown;
|
|
using StatementParser = Strata.SqlTools.Statements.PostgreSql.StatementParser;
|
|
|
|
namespace Strata.SqlTools.Breakdowns.PostgreSql;
|
|
|
|
/// <summary>
|
|
/// Represents a PostgreSQL query breakdown with all clauses, following PostgreSQL SQL standards.
|
|
/// Handles positional parameters using $1, $2, ... syntax for parameterized queries.
|
|
/// </summary>
|
|
public class QueryBreakdown : SqlServerQueryBreakdown
|
|
{
|
|
private const string ExpressionNullErrorMessage = "Expression cannot be null.";
|
|
private static readonly StatementParser PostgreSqlParserInstance = new StatementParser();
|
|
private int _parameterIndex = 1;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="QueryBreakdown"/> class.
|
|
/// </summary>
|
|
public QueryBreakdown() : base()
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="QueryBreakdown"/> class with SELECT and FROM clauses.
|
|
/// </summary>
|
|
/// <param name="selectClause">The SELECT clause.</param>
|
|
/// <param name="fromClause">The FROM clause.</param>
|
|
/// <param name="isMicrosoftSql">If true, uses Microsoft T-SQL parsing rules. If false, uses PostgreSQL parsing rules. Defaults to false.</param>
|
|
public QueryBreakdown(string selectClause, string fromClause, bool isMicrosoftSql = false) : base()
|
|
{
|
|
var parser = isMicrosoftSql ? Parser : PostgreSqlParserInstance;
|
|
parser.PopulateClauseWithComments(selectClause, SelectClause);
|
|
parser.PopulateClauseWithComments(fromClause, FromClause);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="QueryBreakdown"/> class with SELECT, FROM, and WHERE clauses.
|
|
/// </summary>
|
|
/// <param name="selectClause">The SELECT clause.</param>
|
|
/// <param name="fromClause">The FROM clause.</param>
|
|
/// <param name="whereClause">The WHERE clause.</param>
|
|
/// <param name="isMicrosoftSql">If true, uses Microsoft T-SQL parsing rules. If false, uses PostgreSQL parsing rules. Defaults to false.</param>
|
|
public QueryBreakdown(string selectClause, string fromClause, string whereClause, bool isMicrosoftSql = false)
|
|
: this(selectClause, fromClause, isMicrosoftSql)
|
|
{
|
|
if (!string.IsNullOrEmpty(whereClause))
|
|
{
|
|
var parser = isMicrosoftSql ? Parser : PostgreSqlParserInstance;
|
|
|
|
var cleanWhere = parser.ExtractSqlComments(whereClause, out var whereComments);
|
|
WhereClause.Clause = cleanWhere.Trim();
|
|
WhereClause.Comment = whereComments.Count > 0 ? string.Join(" ", whereComments) : null;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="QueryBreakdown"/> class with SELECT, FROM, WHERE, and ORDER BY clauses.
|
|
/// </summary>
|
|
/// <param name="selectClause">The SELECT clause.</param>
|
|
/// <param name="fromClause">The FROM clause.</param>
|
|
/// <param name="whereClause">The WHERE clause.</param>
|
|
/// <param name="orderByClause">The ORDER BY clause.</param>
|
|
/// <param name="isMicrosoftSql">If true, uses Microsoft T-SQL parsing rules. If false, uses PostgreSQL parsing rules. Defaults to false.</param>
|
|
public QueryBreakdown(string selectClause, string fromClause, string whereClause, string orderByClause, bool isMicrosoftSql = false)
|
|
: this(selectClause, fromClause, whereClause, isMicrosoftSql)
|
|
{
|
|
if (!string.IsNullOrEmpty(orderByClause))
|
|
{
|
|
var parser = isMicrosoftSql ? Parser : PostgreSqlParserInstance;
|
|
|
|
var cleanOrderBy = parser.ExtractSqlComments(orderByClause, out var orderByComments);
|
|
OrderByClause.Clause = cleanOrderBy.Trim();
|
|
OrderByClause.Comment = orderByComments.Count > 0 ? string.Join(" ", orderByComments) : null;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds a parameter to the query using PostgreSQL's positional parameter format ($1, $2, ...).
|
|
/// </summary>
|
|
/// <param name="parameterName">The parameter name (can be any name; PostgreSQL uses positions).</param>
|
|
/// <param name="value">The parameter value.</param>
|
|
public new void AddParameter(string parameterName, object value)
|
|
{
|
|
// For PostgreSQL, we track the parameter position and store by name
|
|
var cleanName = parameterName.TrimStart('@', ':');
|
|
|
|
// Use base class internal list
|
|
base.AddParameter(cleanName, value);
|
|
|
|
// Store with PostgreSQL position syntax for reference
|
|
Parameters[$"${_parameterIndex}"] = value;
|
|
Parameters[cleanName] = value;
|
|
Parameters[$"@{cleanName}"] = value;
|
|
|
|
_parameterIndex++;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the value of a parameter using PostgreSQL's positional format.
|
|
/// </summary>
|
|
/// <param name="parameterName">The parameter name (can be any name; PostgreSQL uses positions).</param>
|
|
/// <param name="value">The parameter value.</param>
|
|
public new void SetParameterValue(string parameterName, object value)
|
|
{
|
|
var cleanName = parameterName.TrimStart('@', ':');
|
|
Parameters[cleanName] = value;
|
|
Parameters[$"@{cleanName}"] = value;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds an expression to the SELECT clause.
|
|
/// </summary>
|
|
/// <param name="expression">The expression to add.</param>
|
|
/// <param name="comment">Optional comment to add with the expression.</param>
|
|
/// <param name="isMicrosoftSql">If true, uses Microsoft T-SQL formatting. If false, uses PostgreSQL formatting. Defaults to false.</param>
|
|
public void AddSelectExpression(Expression expression, string? comment = null, bool isMicrosoftSql = false)
|
|
{
|
|
if (expression is null)
|
|
{
|
|
throw new ArgumentNullException(nameof(expression), ExpressionNullErrorMessage);
|
|
}
|
|
|
|
var visitor = isMicrosoftSql
|
|
? (IVisitor<string>)new SqlServerCommandVisitor()
|
|
: new CommandVisitor();
|
|
var sql = expression.Accept(visitor);
|
|
|
|
if (string.IsNullOrWhiteSpace(SelectClause.Clause))
|
|
{
|
|
SelectClause.Clause = sql;
|
|
}
|
|
else
|
|
{
|
|
SelectClause.Clause += ", " + sql;
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(comment))
|
|
{
|
|
SelectClause.Comment = string.IsNullOrEmpty(SelectClause.Comment)
|
|
? comment
|
|
: $"{SelectClause.Comment} {comment}";
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds an expression to the WHERE clause.
|
|
/// </summary>
|
|
/// <param name="expression">The expression to add.</param>
|
|
/// <param name="comment">Optional comment to add with the expression.</param>
|
|
/// <param name="isMicrosoftSql">If true, uses Microsoft T-SQL formatting. If false, uses PostgreSQL formatting. Defaults to false.</param>
|
|
public void AddWhereExpression(Expression expression, string? comment = null, bool isMicrosoftSql = false)
|
|
{
|
|
if (expression is null)
|
|
{
|
|
throw new ArgumentNullException(nameof(expression), ExpressionNullErrorMessage);
|
|
}
|
|
|
|
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 += " AND " + sql;
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(comment))
|
|
{
|
|
WhereClause.Comment = string.IsNullOrEmpty(WhereClause.Comment)
|
|
? comment
|
|
: $"{WhereClause.Comment} {comment}";
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses a PostgreSQL SELECT statement and populates the query breakdown.
|
|
/// </summary>
|
|
/// <param name="sql">The SQL statement to parse.</param>
|
|
/// <returns>A new QueryBreakdown instance with parsed components.</returns>
|
|
public static new QueryBreakdown Parse(string sql)
|
|
{
|
|
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))
|
|
{
|
|
throw new FormatException($"Failed to parse SQL statement: {error}");
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Attempts to parse a PostgreSQL SELECT statement.
|
|
/// </summary>
|
|
/// <param name="sql">The SQL statement to parse.</param>
|
|
/// <param name="result">The resulting QueryBreakdown if successful.</param>
|
|
/// <param name="errorMessage">The error message if parsing fails.</param>
|
|
/// <returns>True if parsing succeeded; false otherwise.</returns>
|
|
public static bool TryParse(string sql, out QueryBreakdown result, out string errorMessage)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(sql))
|
|
{
|
|
result = new QueryBreakdown();
|
|
errorMessage = "SQL statement cannot be null or empty.";
|
|
return false;
|
|
}
|
|
|
|
try
|
|
{
|
|
var parser = PostgreSqlParserInstance;
|
|
var setupClauses = new List<string>();
|
|
sql = parser.ExtractSetupClauses(sql, setupClauses);
|
|
|
|
var finishClauses = new ArrayList();
|
|
sql = parser.ExtractFinishClauses(sql, finishClauses);
|
|
|
|
if (!parser.TryParseSelectStatement(sql, out var clauses, out errorMessage))
|
|
{
|
|
result = new QueryBreakdown();
|
|
return false;
|
|
}
|
|
|
|
result = new QueryBreakdown
|
|
{
|
|
SelectClause = clauses?.SelectClause ?? new SqlExpressionClause(splitOnComma: true),
|
|
FromClause = clauses?.FromClause ?? new SqlClause(),
|
|
WhereClause = clauses?.WhereClause ?? new SqlExpressionClause(splitOnComma: false),
|
|
GroupByClause = clauses?.GroupByClause ?? new SqlExpressionClause(splitOnComma: true),
|
|
HavingClause = clauses?.HavingClause ?? new SqlExpressionClause(splitOnComma: false),
|
|
OrderByClause = clauses?.OrderByClause ?? new SqlExpressionClause(splitOnComma: true),
|
|
SetupClauses = setupClauses,
|
|
FinishClauses = finishClauses,
|
|
RawSql = sql
|
|
};
|
|
|
|
// Extract parameters using PostgreSQL parser
|
|
parser.ExtractParameters(result.Parameters, sql);
|
|
|
|
errorMessage = string.Empty;
|
|
return true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
result = new QueryBreakdown();
|
|
errorMessage = ex.Message;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a LINQ to SQL query of the specified type based on this breakdown.
|
|
/// </summary>
|
|
/// <typeparam name="T">The entity type for the query.</typeparam>
|
|
/// <returns>An empty queryable by default, as QueryBreakdown operates on SQL. Override in derived classes to provide LINQ query reconstruction.</returns>
|
|
/// <remarks>
|
|
/// This PostgreSQL-specific implementation returns an empty queryable since PostgreSQL QueryBreakdown
|
|
/// represents parsed SQL statements and has no built-in way to create LINQ queries.
|
|
/// Derived classes can override this method to reconstruct LINQ queries from the analyzed components.
|
|
/// </remarks>
|
|
public override IQueryable<T> GetQuery<T>() where T : class => Enumerable.Empty<T>().AsQueryable();
|
|
}
|
|
|
|
|