using System.Text;
using Strata.SqlTools.SqlBreakdown.Classes;
using Strata.SqlTools.SqlBreakdown.Expressions;
using Strata.SqlTools.SqlBreakdown.Expressions.Arithmetic;
using Strata.SqlTools.SqlBreakdown.Expressions.Conditional;
using Strata.SqlTools.SqlBreakdown.Expressions.Conditional.Comparisons;
using Strata.SqlTools.SqlBreakdown.Expressions.Conditional.Logical;
using Strata.SqlTools.SqlBreakdown.Expressions.Functions;
using Strata.SqlTools.SqlBreakdown.Expressions.Functions.Aggregate;
using Strata.SqlTools.SqlBreakdown.Expressions.Functions.Conditional;
using Strata.SqlTools.SqlBreakdown.Expressions.Literals;
using Strata.SqlTools.SqlBreakdown.Interfaces.Core;
namespace Strata.SqlTools.Visitors.SqlServer;
///
/// Implements the visitor pattern to convert SQL expression objects into T-SQL (Microsoft SQL Server) compatible SQL command strings.
/// This class traverses the expression tree and generates appropriate SQL syntax for SQL Server database.
/// Can be inherited to support other SQL dialects by overriding dialect-specific formatting methods.
///
public class CommandVisitor : IVisitor
{
#region Dialect-Specific Formatting (Template Method Pattern)
///
/// Formats an identifier (table name, column name, alias) according to the SQL dialect.
/// SQL Server uses square brackets. Override for other dialects.
///
/// The identifier to format.
/// The formatted identifier.
protected virtual string FormatIdentifier(string identifier) => $"[{identifier}]";
///
/// Formats a parameter name according to the SQL dialect.
/// SQL Server uses @ prefix. Override for other dialects (e.g., : for Oracle/Snowflake).
///
/// The parameter name to format.
/// The formatted parameter reference.
protected virtual string FormatParameterName(string parameterName) => $"@{parameterName}";
///
/// Formats a boolean literal according to the SQL dialect.
/// SQL Server uses bit values (1/0). Override for dialects with TRUE/FALSE keywords.
///
/// The boolean value to format.
/// The formatted boolean literal.
protected virtual string FormatBooleanLiteral(bool value) => value ? "1" : "0";
///
/// Formats a string literal according to the SQL dialect, including escaping.
/// SQL Server escapes single quotes by doubling them. Override for other escaping rules.
///
/// The string value to format.
/// The formatted string literal with quotes.
protected virtual string FormatStringLiteral(string value) => $"'{value.Replace("'", "''")}'";
///
/// Formats a case-insensitive LIKE expression according to the SQL dialect.
/// SQL Server uses UPPER() wrapper. Override for dialects with ILIKE or other mechanisms.
///
/// The LIKE expression to format.
/// The formatted case-insensitive LIKE expression.
protected virtual string FormatCaseInsensitiveLike(LikeExpression likeExpression)
{
return $"UPPER({likeExpression.Subject.Accept(this)}) LIKE UPPER({likeExpression.Pattern.Accept(this)})";
}
#endregion
///
/// Visits a table source expression and generates the appropriate SQL identifier.
/// Returns the alias if present, otherwise returns the fully qualified table name ([schema].[table]) or just the table name.
///
/// The table source expression to convert.
/// A SQL string representing the table identifier with SQL Server bracket notation.
public virtual string VisitTableSource(TableSource tableSource)
{
if (!string.IsNullOrWhiteSpace(tableSource.Alias))
{
return FormatIdentifier(tableSource.Alias);
}
if (!string.IsNullOrWhiteSpace(tableSource.Schema))
{
return $"{FormatIdentifier(tableSource.Schema)}.{FormatIdentifier(tableSource.TableName)}";
}
return FormatIdentifier(tableSource.TableName);
}
///
/// Visits a column expression and generates a fully qualified column reference.
///
/// The type of the source (e.g., TableSource).
/// The column expression to convert.
/// A SQL string in the format "[source].[columnName]".
public virtual string VisitColumnExpression(ColumnExpression column) where TSource : SelectSource
{
var sourceName = column.Source.Accept(this);
return $"{sourceName}.{FormatIdentifier(column.ColumnName)}";
}
///
/// Visits a SELECT clause column and generates the column expression with optional alias.
///
/// The SELECT clause column to convert.
/// A SQL string representing the column expression, with "AS [alias]" appended if an alias is specified.
public virtual string VisitSelectClauseColumn(SelectClauseColumn selectClauseColumn)
{
var expr = selectClauseColumn.Expression.Accept(this);
return !string.IsNullOrWhiteSpace(selectClauseColumn.Alias)
? $"{expr} AS {FormatIdentifier(selectClauseColumn.Alias)}"
: expr;
}
///
/// Visits a parameter expression and generates a T-SQL parameter reference.
///
/// The parameter expression to convert.
/// A SQL string in the format "@parameterName".
public virtual string VisitParameterExpression(ParameterExpression parameterExpression)
{
return FormatParameterName(parameterExpression.ParameterName);
}
#region Literal Expressions
///
/// Visits a numeric literal expression and converts it to a SQL number literal.
///
/// The number literal expression to convert.
/// A SQL string representing the numeric value.
public string VisitNumberLiteralExpression(NumberLiteralExpression numberLiteral) =>
$"{numberLiteral.Value}";
///
/// Visits a string literal expression and converts it to a SQL string literal with single quotes.
/// Escapes single quotes within the string by doubling them.
///
/// The string literal expression to convert.
/// A SQL string literal enclosed in single quotes with escaped quotes.
public virtual string VisitStringLiteralExpression(StringLiteralExpression stringLiteral) =>
FormatStringLiteral(stringLiteral.Value);
///
/// Visits a DateTime literal expression and converts it to a SQL date or datetime literal.
/// If the time component is zero, only the date is included (yyyy-MM-dd).
/// Otherwise, the full datetime with milliseconds is included (yyyy-MM-dd HH:mm:ss.fff).
///
/// The DateTime literal expression to convert.
/// A SQL datetime literal string enclosed in single quotes.
public string VisitDateTimeLiteralExpression(DateTimeLiteralExpression dateTimeLiteral)
{
return dateTimeLiteral.Value.TimeOfDay == TimeSpan.Zero
? $"'{dateTimeLiteral.Value:yyyy-MM-dd}'"
: $"'{dateTimeLiteral.Value:yyyy-MM-dd HH:mm:ss.fff}'";
}
///
/// Visits a NULL literal expression and returns the SQL NULL keyword.
///
/// The NULL literal expression to convert.
/// The string "NULL".
public string VisitNullLiteralExpression(NullLiteralExpression nullLiteral)
=> "NULL";
///
/// Visits a boolean literal expression and converts it to T-SQL boolean representation (1 or 0).
/// SQL Server does not have a native BOOLEAN type, so bit values are used.
///
/// The boolean literal expression to convert.
/// The string "1" for true or "0" for false.
public virtual string VisitBooleanLiteralExpression(BooleanLiteralExpression booleanLiteral) =>
FormatBooleanLiteral(booleanLiteral.Value);
///
/// Visits a parameter literal expression and returns the parameter placeholder as-is.
/// Supports positional ($1, $2), named with @, and named with : format.
///
/// The parameter literal expression to convert.
/// The parameter placeholder string (e.g., "$1", "@userId", ":userId").
public virtual string VisitParameterLiteralExpression(ParameterLiteralExpression parameterLiteral) =>
parameterLiteral.Value;
#pragma warning disable CS1570 // XML comment has badly formed XML
///
/// Visits a symbol literal expression and returns the symbolic operator as-is.
/// Used for database-specific operators like PostgreSQL's >=, &pipe;&pipe;, etc.
///
/// The symbol literal expression to convert.
///
/// The symbolic operator string (e.g., ">=", "&pipe;&pipe;", "..").
///
public virtual string VisitSymbolLiteralExpression(SymbolLiteralExpression symbolLiteral) =>
symbolLiteral.Value;
#pragma warning restore CS1570 // XML comment has badly formed XML
#endregion
#region Boolean Expressions
///
/// Visits a comparison expression and generates SQL comparison syntax (e.g., =, !=, >, <, >=, <=).
///
/// The comparison expression to convert.
/// A SQL string in the format "expressionA operator expressionB".
public string VisitComparisonExpression(ComparisonOperatorExpression comparison)
{
return $"{comparison.ExpressionA.Accept(this)} {comparison.Operator} {comparison.ExpressionB.Accept(this)}";
}
///
/// Visits an AND logical expression and generates SQL AND syntax.
/// Automatically wraps OR and NOT expressions in parentheses for correct precedence.
///
/// The AND expression to convert.
/// A SQL string in the format "expressionA AND expressionB" with appropriate parentheses.
public string VisitAndExpression(AndExpression logical)
{
var aExpSql = WrapInParenthesis(logical.ExpressionA, exp => exp is OrExpression or NotExpression);
var bExpSql = WrapInParenthesis(logical.ExpressionB, exp => exp is OrExpression or NotExpression);
return $"{aExpSql} AND {bExpSql}";
}
///
/// Visits an OR logical expression and generates SQL OR syntax.
/// Automatically wraps AND and NOT expressions in parentheses for correct precedence.
/// The second expression is placed on a new line for readability.
///
/// The OR expression to convert.
/// A SQL string in the format "expressionA OR \nexpressionB" with appropriate parentheses.
public string VisitOrExpression(OrExpression logical)
{
var aExpSql = WrapInParenthesis(logical.ExpressionA, exp => exp is AndExpression or NotExpression);
var bExpSql = WrapInParenthesis(logical.ExpressionB, exp => exp is AndExpression or NotExpression);
return $"{aExpSql} OR \n{bExpSql}";
}
///
/// Visits a NOT logical expression and generates SQL NOT syntax.
/// Automatically wraps AND and OR expressions in parentheses for correct precedence.
///
/// The NOT expression to convert.
/// A SQL string in the format "NOT expression" with appropriate parentheses.
public string VisitNotExpression(NotExpression logical)
{
var aExpSql = WrapInParenthesis(logical.ExpressionA, exp => exp is AndExpression or OrExpression);
return $"NOT {aExpSql}";
}
///
/// Visits an IN expression and generates SQL IN syntax for testing membership in a set of values.
///
/// The IN expression to convert.
/// A SQL string in the format "expression IN (value1, value2, ...)".
public string VisitInExpression(InExpression inExpression)
{
return $"{inExpression.SearchExpression.Accept(this)} IN ({string.Join(", ", inExpression.ValuesToCompare.Select(v => v.Accept(this)))})";
}
///
/// Visits a NOT IN expression and generates SQL NOT IN syntax for testing non-membership in a set of values.
///
/// The NOT IN expression to convert.
/// A SQL string in the format "expression NOT IN (value1, value2, ...)".
public string VisitNotInExpression(NotInExpression inExpression)
{
return $"{inExpression.SearchExpression.Accept(this)} NOT IN ({string.Join(", ", inExpression.ValuesToCompare.Select(v => v.Accept(this)))})";
}
///
/// Visits a LIKE expression and generates SQL LIKE syntax for pattern matching.
/// For case-insensitive matching, wraps both the subject and pattern in UPPER() function calls.
///
/// The LIKE expression to convert.
/// A SQL string in the format "expression LIKE pattern" or "UPPER(expression) LIKE UPPER(pattern)".
public virtual string VisitLikeExpression(LikeExpression likeExpression)
{
if (likeExpression.CaseInsensitive)
{
return FormatCaseInsensitiveLike(likeExpression);
}
return $"{likeExpression.Subject.Accept(this)} LIKE {likeExpression.Pattern.Accept(this)}";
}
///
/// Visits a NOT LIKE expression and generates SQL NOT LIKE syntax.
///
/// The NOT LIKE expression to convert.
/// A SQL string in the format "NOT (expression LIKE pattern)" or "NOT (UPPER(expression) LIKE UPPER(pattern))".
public string VisitNotLikeExpression(NotLikeExpression notLikeExpression) =>
$"NOT ({VisitLikeExpression(notLikeExpression)})";
///
/// Visits a BETWEEN expression and generates SQL BETWEEN syntax for range testing.
///
/// The BETWEEN expression to convert.
/// A SQL string in the format "expression BETWEEN lowerBound AND upperBound".
public string VisitBetweenExpression(BetweenExpression betweenExpression)
{
return $"{betweenExpression.Expression.Accept(this)} BETWEEN {betweenExpression.LowerBound.Accept(this)} AND {betweenExpression.UpperBound.Accept(this)}";
}
#endregion
#region Function Expressions
///
/// Visits an aggregate function expression (e.g., SUM, COUNT, AVG) and generates SQL aggregate function syntax.
///
/// The aggregate function expression to convert.
/// A SQL string representing the aggregate function call.
public string VisitAggregateFunctionExpression(AggregateFunctionExpression aggregateFunction) =>
VisitFunctionExpression(aggregateFunction);
///
/// Visits a CASE expression and generates SQL CASE statement syntax with WHEN/THEN/ELSE clauses.
/// Each condition-result pair is placed on a new line for readability.
///
/// The CASE expression to convert.
/// A multi-line SQL string representing the CASE statement.
public string VisitCaseFunctionExpression(CaseExpression caseFunction)
{
var sb = new StringBuilder("CASE\n");
sb.AppendJoin("\n", caseFunction.ConditionResultPairs.Select(p => $" WHEN {p.condition.Accept(this)} THEN {p.result.Accept(this)}"));
if (caseFunction.ElseResultExpression is not null)
{
sb.Append($"\n ELSE {caseFunction.ElseResultExpression.Accept(this)}");
}
sb.Append("\nEND");
return sb.ToString();
}
///
/// Visits a generic function expression and generates SQL function call syntax.
/// This is the base implementation for all function expressions.
///
/// The function expression to convert.
/// A SQL string in the format "functionName(arg1, arg2, ...)".
public virtual string VisitFunctionExpression(FunctionExpression function)
{
return $"{function.FunctionName}({string.Join(", ", function.Arguments.Select(e => e.Accept(this)))})";
}
#endregion
///
/// Visits an arithmetic expression and generates SQL arithmetic operation syntax (+, -, *, /).
/// Automatically wraps sub-expressions in parentheses when needed to maintain correct operator precedence.
///
/// The arithmetic expression to convert.
/// A SQL string representing the arithmetic operation with appropriate parentheses.
public string VisitArithmeticExpression(ArithmeticExpression arithmeticExpression)
{
var aExpSql = WrapInParenthesis(arithmeticExpression.ExpressionA, expr => ShouldWrapArithmetic(arithmeticExpression, expr));
var bExpSql = WrapInParenthesis(arithmeticExpression.ExpressionB, expr => ShouldWrapArithmetic(arithmeticExpression, expr));
return $"{aExpSql} {arithmeticExpression.ArithmeticOperator} {bExpSql}";
}
///
/// Visits an input property expression. This method is not implemented as input properties
/// are typically not directly converted to SQL.
///
/// The input property expression.
/// Throws NotImplementedException.
/// This method is not implemented.
public string VisitInputPropertyExpression(InputPropertyExpression inputPropertyExpression)
{
throw new NotImplementedException();
}
///
/// Determines whether an arithmetic sub-expression should be wrapped in parentheses
/// to maintain correct operator precedence (multiplication/division have higher precedence than addition/subtraction).
///
/// The parent arithmetic expression.
/// The sub-expression to evaluate.
/// True if the sub-expression should be wrapped in parentheses; otherwise, false.
private static bool ShouldWrapArithmetic(ArithmeticExpression arithmeticExpression, Expression other)
{
return other switch
{
AdditionExpression or SubtractionExpression => arithmeticExpression is not (AdditionExpression or SubtractionExpression),
MultiplicationExpression or DivisionExpression => arithmeticExpression is not (MultiplicationExpression or DivisionExpression),
_ => false
};
}
///
/// Wraps an expression in parentheses if the provided predicate returns true.
/// This is used to ensure correct operator precedence in generated SQL.
///
/// The expression to potentially wrap.
/// A predicate function that determines if wrapping is needed.
/// The expression SQL with or without parentheses.
private string WrapInParenthesis(Expression expression, Func shouldWrap)
{
return shouldWrap(expression)
? $"({expression.Accept(this)})"
: $"{expression.Accept(this)}";
}
}