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;
parser.PopulateClauseWithComments(selectClause, SelectClause);
parser.PopulateClauseWithComments(fromClause, FromClause);
}
///
/// 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();
}