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; /// /// Represents a PostgreSQL query breakdown with all clauses, following PostgreSQL SQL standards. /// Handles positional parameters using $1, $2, ... syntax for parameterized queries. /// public class QueryBreakdown : SqlServerQueryBreakdown { private const string ExpressionNullErrorMessage = "Expression cannot be null."; private static readonly StatementParser PostgreSqlParserInstance = new StatementParser(); private int _parameterIndex = 1; /// /// Initializes a new instance of the class. /// public QueryBreakdown() : base() { } /// /// Initializes a new instance of the class with SELECT and FROM clauses. /// /// The SELECT clause. /// The FROM clause. /// If true, uses Microsoft T-SQL parsing rules. If false, uses PostgreSQL parsing rules. Defaults to false. public QueryBreakdown(string selectClause, string fromClause, bool isMicrosoftSql = false) : base() { var parser = isMicrosoftSql ? Parser : PostgreSqlParserInstance; var cleanSelect = parser.ExtractSqlComments(selectClause, out var selectComments); SelectClause.Clause = cleanSelect.Trim(); SelectClause.Comment = selectComments.Count > 0 ? string.Join(" ", selectComments) : null; var cleanFrom = parser.ExtractSqlComments(fromClause, out var fromComments); FromClause.Clause = cleanFrom.Trim(); FromClause.Comment = fromComments.Count > 0 ? string.Join(" ", fromComments) : null; } /// /// Initializes a new instance of the class with SELECT, FROM, and WHERE clauses. /// /// The SELECT clause. /// The FROM clause. /// The WHERE clause. /// If true, uses Microsoft T-SQL parsing rules. If false, uses PostgreSQL parsing rules. Defaults to false. 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; } } /// /// Initializes a new instance of the class with SELECT, FROM, WHERE, and ORDER BY clauses. /// /// The SELECT clause. /// The FROM clause. /// The WHERE clause. /// The ORDER BY clause. /// If true, uses Microsoft T-SQL parsing rules. If false, uses PostgreSQL parsing rules. Defaults to false. 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; } } /// /// Adds a parameter to the query using PostgreSQL's positional parameter format ($1, $2, ...). /// /// The parameter name (can be any name; PostgreSQL uses positions). /// The parameter value. 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++; } /// /// Sets the value of a parameter using PostgreSQL's positional format. /// /// The parameter name (can be any name; PostgreSQL uses positions). /// The parameter value. public new void SetParameterValue(string parameterName, object value) { var cleanName = parameterName.TrimStart('@', ':'); Parameters[cleanName] = value; Parameters[$"@{cleanName}"] = value; } /// /// Adds an expression to the SELECT clause. /// /// The expression to add. /// Optional comment to add with the expression. /// If true, uses Microsoft T-SQL formatting. If false, uses PostgreSQL formatting. Defaults to false. 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)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}"; } } /// /// Adds an expression to the WHERE clause. /// /// The expression to add. /// Optional comment to add with the expression. /// If true, uses Microsoft T-SQL formatting. If false, uses PostgreSQL formatting. Defaults to false. 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)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}"; } } /// /// Parses a PostgreSQL SELECT statement and populates the query breakdown. /// /// The SQL statement to parse. /// A new QueryBreakdown instance with parsed components. 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; } /// /// Attempts to parse a PostgreSQL SELECT statement. /// /// The SQL statement to parse. /// The resulting QueryBreakdown if successful. /// The error message if parsing fails. /// True if parsing succeeded; false otherwise. 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(); 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; } } /// /// Gets a LINQ to SQL query of the specified type based on this breakdown. /// /// The entity type for the query. /// An empty queryable by default, as QueryBreakdown operates on SQL. Override in derived classes to provide LINQ query reconstruction. /// /// 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. /// public override IQueryable GetQuery() where T : class => Enumerable.Empty().AsQueryable(); }