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)}"; } }