chore: initial git load of code space
This commit is contained in:
@@ -0,0 +1,409 @@
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public class CommandVisitor : IVisitor<string>
|
||||
{
|
||||
#region Dialect-Specific Formatting (Template Method Pattern)
|
||||
|
||||
/// <summary>
|
||||
/// Formats an identifier (table name, column name, alias) according to the SQL dialect.
|
||||
/// SQL Server uses square brackets. Override for other dialects.
|
||||
/// </summary>
|
||||
/// <param name="identifier">The identifier to format.</param>
|
||||
/// <returns>The formatted identifier.</returns>
|
||||
protected virtual string FormatIdentifier(string identifier) => $"[{identifier}]";
|
||||
|
||||
/// <summary>
|
||||
/// Formats a parameter name according to the SQL dialect.
|
||||
/// SQL Server uses @ prefix. Override for other dialects (e.g., : for Oracle/Snowflake).
|
||||
/// </summary>
|
||||
/// <param name="parameterName">The parameter name to format.</param>
|
||||
/// <returns>The formatted parameter reference.</returns>
|
||||
protected virtual string FormatParameterName(string parameterName) => $"@{parameterName}";
|
||||
|
||||
/// <summary>
|
||||
/// Formats a boolean literal according to the SQL dialect.
|
||||
/// SQL Server uses bit values (1/0). Override for dialects with TRUE/FALSE keywords.
|
||||
/// </summary>
|
||||
/// <param name="value">The boolean value to format.</param>
|
||||
/// <returns>The formatted boolean literal.</returns>
|
||||
protected virtual string FormatBooleanLiteral(bool value) => value ? "1" : "0";
|
||||
|
||||
/// <summary>
|
||||
/// Formats a string literal according to the SQL dialect, including escaping.
|
||||
/// SQL Server escapes single quotes by doubling them. Override for other escaping rules.
|
||||
/// </summary>
|
||||
/// <param name="value">The string value to format.</param>
|
||||
/// <returns>The formatted string literal with quotes.</returns>
|
||||
protected virtual string FormatStringLiteral(string value) => $"'{value.Replace("'", "''")}'";
|
||||
|
||||
/// <summary>
|
||||
/// Formats a case-insensitive LIKE expression according to the SQL dialect.
|
||||
/// SQL Server uses UPPER() wrapper. Override for dialects with ILIKE or other mechanisms.
|
||||
/// </summary>
|
||||
/// <param name="likeExpression">The LIKE expression to format.</param>
|
||||
/// <returns>The formatted case-insensitive LIKE expression.</returns>
|
||||
protected virtual string FormatCaseInsensitiveLike(LikeExpression likeExpression)
|
||||
{
|
||||
return $"UPPER({likeExpression.Subject.Accept(this)}) LIKE UPPER({likeExpression.Pattern.Accept(this)})";
|
||||
}
|
||||
|
||||
#endregion
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="tableSource">The table source expression to convert.</param>
|
||||
/// <returns>A SQL string representing the table identifier with SQL Server bracket notation.</returns>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Visits a column expression and generates a fully qualified column reference.
|
||||
/// </summary>
|
||||
/// <typeparam name="TSource">The type of the source (e.g., TableSource).</typeparam>
|
||||
/// <param name="column">The column expression to convert.</param>
|
||||
/// <returns>A SQL string in the format "[source].[columnName]".</returns>
|
||||
public virtual string VisitColumnExpression<TSource>(ColumnExpression<TSource> column) where TSource : SelectSource
|
||||
{
|
||||
var sourceName = column.Source.Accept(this);
|
||||
return $"{sourceName}.{FormatIdentifier(column.ColumnName)}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Visits a SELECT clause column and generates the column expression with optional alias.
|
||||
/// </summary>
|
||||
/// <param name="selectClauseColumn">The SELECT clause column to convert.</param>
|
||||
/// <returns>A SQL string representing the column expression, with "AS [alias]" appended if an alias is specified.</returns>
|
||||
public virtual string VisitSelectClauseColumn(SelectClauseColumn selectClauseColumn)
|
||||
{
|
||||
var expr = selectClauseColumn.Expression.Accept(this);
|
||||
return !string.IsNullOrWhiteSpace(selectClauseColumn.Alias)
|
||||
? $"{expr} AS {FormatIdentifier(selectClauseColumn.Alias)}"
|
||||
: expr;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Visits a parameter expression and generates a T-SQL parameter reference.
|
||||
/// </summary>
|
||||
/// <param name="parameterExpression">The parameter expression to convert.</param>
|
||||
/// <returns>A SQL string in the format "@parameterName".</returns>
|
||||
public virtual string VisitParameterExpression(ParameterExpression parameterExpression)
|
||||
{
|
||||
return FormatParameterName(parameterExpression.ParameterName);
|
||||
}
|
||||
|
||||
#region Literal Expressions
|
||||
|
||||
/// <summary>
|
||||
/// Visits a numeric literal expression and converts it to a SQL number literal.
|
||||
/// </summary>
|
||||
/// <param name="numberLiteral">The number literal expression to convert.</param>
|
||||
/// <returns>A SQL string representing the numeric value.</returns>
|
||||
public string VisitNumberLiteralExpression(NumberLiteralExpression numberLiteral) =>
|
||||
$"{numberLiteral.Value}";
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="stringLiteral">The string literal expression to convert.</param>
|
||||
/// <returns>A SQL string literal enclosed in single quotes with escaped quotes.</returns>
|
||||
public virtual string VisitStringLiteralExpression(StringLiteralExpression stringLiteral) =>
|
||||
FormatStringLiteral(stringLiteral.Value);
|
||||
|
||||
/// <summary>
|
||||
/// 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).
|
||||
/// </summary>
|
||||
/// <param name="dateTimeLiteral">The DateTime literal expression to convert.</param>
|
||||
/// <returns>A SQL datetime literal string enclosed in single quotes.</returns>
|
||||
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}'";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Visits a NULL literal expression and returns the SQL NULL keyword.
|
||||
/// </summary>
|
||||
/// <param name="nullLiteral">The NULL literal expression to convert.</param>
|
||||
/// <returns>The string "NULL".</returns>
|
||||
public string VisitNullLiteralExpression(NullLiteralExpression nullLiteral)
|
||||
=> "NULL";
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="booleanLiteral">The boolean literal expression to convert.</param>
|
||||
/// <returns>The string "1" for true or "0" for false.</returns>
|
||||
public virtual string VisitBooleanLiteralExpression(BooleanLiteralExpression booleanLiteral) =>
|
||||
FormatBooleanLiteral(booleanLiteral.Value);
|
||||
|
||||
/// <summary>
|
||||
/// Visits a parameter literal expression and returns the parameter placeholder as-is.
|
||||
/// Supports positional ($1, $2), named with @, and named with : format.
|
||||
/// </summary>
|
||||
/// <param name="parameterLiteral">The parameter literal expression to convert.</param>
|
||||
/// <returns>The parameter placeholder string (e.g., "$1", "@userId", ":userId").</returns>
|
||||
public virtual string VisitParameterLiteralExpression(ParameterLiteralExpression parameterLiteral) =>
|
||||
parameterLiteral.Value;
|
||||
|
||||
|
||||
#pragma warning disable CS1570 // XML comment has badly formed XML
|
||||
/// <summary>
|
||||
/// Visits a symbol literal expression and returns the symbolic operator as-is.
|
||||
/// Used for database-specific operators like PostgreSQL's >=, &pipe;&pipe;, etc.
|
||||
/// </summary>
|
||||
/// <param name="symbolLiteral">The symbol literal expression to convert.</param>
|
||||
/// <returns>
|
||||
/// The symbolic operator string (e.g., ">=", "&pipe;&pipe;", "..").
|
||||
/// </returns>
|
||||
public virtual string VisitSymbolLiteralExpression(SymbolLiteralExpression symbolLiteral) =>
|
||||
symbolLiteral.Value;
|
||||
#pragma warning restore CS1570 // XML comment has badly formed XML
|
||||
|
||||
#endregion
|
||||
|
||||
#region Boolean Expressions
|
||||
|
||||
/// <summary>
|
||||
/// Visits a comparison expression and generates SQL comparison syntax (e.g., =, !=, >, <, >=, <=).
|
||||
/// </summary>
|
||||
/// <param name="comparison">The comparison expression to convert.</param>
|
||||
/// <returns>A SQL string in the format "expressionA operator expressionB".</returns>
|
||||
public string VisitComparisonExpression(ComparisonOperatorExpression comparison)
|
||||
{
|
||||
return $"{comparison.ExpressionA.Accept(this)} {comparison.Operator} {comparison.ExpressionB.Accept(this)}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Visits an AND logical expression and generates SQL AND syntax.
|
||||
/// Automatically wraps OR and NOT expressions in parentheses for correct precedence.
|
||||
/// </summary>
|
||||
/// <param name="logical">The AND expression to convert.</param>
|
||||
/// <returns>A SQL string in the format "expressionA AND expressionB" with appropriate parentheses.</returns>
|
||||
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}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="logical">The OR expression to convert.</param>
|
||||
/// <returns>A SQL string in the format "expressionA OR \nexpressionB" with appropriate parentheses.</returns>
|
||||
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}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Visits a NOT logical expression and generates SQL NOT syntax.
|
||||
/// Automatically wraps AND and OR expressions in parentheses for correct precedence.
|
||||
/// </summary>
|
||||
/// <param name="logical">The NOT expression to convert.</param>
|
||||
/// <returns>A SQL string in the format "NOT expression" with appropriate parentheses.</returns>
|
||||
public string VisitNotExpression(NotExpression logical)
|
||||
{
|
||||
var aExpSql = WrapInParenthesis(logical.ExpressionA, exp => exp is AndExpression or OrExpression);
|
||||
|
||||
return $"NOT {aExpSql}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Visits an IN expression and generates SQL IN syntax for testing membership in a set of values.
|
||||
/// </summary>
|
||||
/// <param name="inExpression">The IN expression to convert.</param>
|
||||
/// <returns>A SQL string in the format "expression IN (value1, value2, ...)".</returns>
|
||||
public string VisitInExpression(InExpression inExpression)
|
||||
{
|
||||
return $"{inExpression.SearchExpression.Accept(this)} IN ({string.Join(", ", inExpression.ValuesToCompare.Select(v => v.Accept(this)))})";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Visits a NOT IN expression and generates SQL NOT IN syntax for testing non-membership in a set of values.
|
||||
/// </summary>
|
||||
/// <param name="inExpression">The NOT IN expression to convert.</param>
|
||||
/// <returns>A SQL string in the format "expression NOT IN (value1, value2, ...)".</returns>
|
||||
public string VisitNotInExpression(NotInExpression inExpression)
|
||||
{
|
||||
return $"{inExpression.SearchExpression.Accept(this)} NOT IN ({string.Join(", ", inExpression.ValuesToCompare.Select(v => v.Accept(this)))})";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="likeExpression">The LIKE expression to convert.</param>
|
||||
/// <returns>A SQL string in the format "expression LIKE pattern" or "UPPER(expression) LIKE UPPER(pattern)".</returns>
|
||||
public virtual string VisitLikeExpression(LikeExpression likeExpression)
|
||||
{
|
||||
if (likeExpression.CaseInsensitive)
|
||||
{
|
||||
return FormatCaseInsensitiveLike(likeExpression);
|
||||
}
|
||||
|
||||
return $"{likeExpression.Subject.Accept(this)} LIKE {likeExpression.Pattern.Accept(this)}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Visits a NOT LIKE expression and generates SQL NOT LIKE syntax.
|
||||
/// </summary>
|
||||
/// <param name="notLikeExpression">The NOT LIKE expression to convert.</param>
|
||||
/// <returns>A SQL string in the format "NOT (expression LIKE pattern)" or "NOT (UPPER(expression) LIKE UPPER(pattern))".</returns>
|
||||
public string VisitNotLikeExpression(NotLikeExpression notLikeExpression) =>
|
||||
$"NOT ({VisitLikeExpression(notLikeExpression)})";
|
||||
|
||||
/// <summary>
|
||||
/// Visits a BETWEEN expression and generates SQL BETWEEN syntax for range testing.
|
||||
/// </summary>
|
||||
/// <param name="betweenExpression">The BETWEEN expression to convert.</param>
|
||||
/// <returns>A SQL string in the format "expression BETWEEN lowerBound AND upperBound".</returns>
|
||||
public string VisitBetweenExpression(BetweenExpression betweenExpression)
|
||||
{
|
||||
return $"{betweenExpression.Expression.Accept(this)} BETWEEN {betweenExpression.LowerBound.Accept(this)} AND {betweenExpression.UpperBound.Accept(this)}";
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Function Expressions
|
||||
|
||||
/// <summary>
|
||||
/// Visits an aggregate function expression (e.g., SUM, COUNT, AVG) and generates SQL aggregate function syntax.
|
||||
/// </summary>
|
||||
/// <param name="aggregateFunction">The aggregate function expression to convert.</param>
|
||||
/// <returns>A SQL string representing the aggregate function call.</returns>
|
||||
public string VisitAggregateFunctionExpression(AggregateFunctionExpression aggregateFunction) =>
|
||||
VisitFunctionExpression(aggregateFunction);
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="caseFunction">The CASE expression to convert.</param>
|
||||
/// <returns>A multi-line SQL string representing the CASE statement.</returns>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Visits a generic function expression and generates SQL function call syntax.
|
||||
/// This is the base implementation for all function expressions.
|
||||
/// </summary>
|
||||
/// <param name="function">The function expression to convert.</param>
|
||||
/// <returns>A SQL string in the format "functionName(arg1, arg2, ...)".</returns>
|
||||
public virtual string VisitFunctionExpression(FunctionExpression function)
|
||||
{
|
||||
return $"{function.FunctionName}({string.Join(", ", function.Arguments.Select(e => e.Accept(this)))})";
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Visits an arithmetic expression and generates SQL arithmetic operation syntax (+, -, *, /).
|
||||
/// Automatically wraps sub-expressions in parentheses when needed to maintain correct operator precedence.
|
||||
/// </summary>
|
||||
/// <param name="arithmeticExpression">The arithmetic expression to convert.</param>
|
||||
/// <returns>A SQL string representing the arithmetic operation with appropriate parentheses.</returns>
|
||||
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}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Visits an input property expression. This method is not implemented as input properties
|
||||
/// are typically not directly converted to SQL.
|
||||
/// </summary>
|
||||
/// <param name="inputPropertyExpression">The input property expression.</param>
|
||||
/// <returns>Throws NotImplementedException.</returns>
|
||||
/// <exception cref="NotImplementedException">This method is not implemented.</exception>
|
||||
public string VisitInputPropertyExpression(InputPropertyExpression inputPropertyExpression)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether an arithmetic sub-expression should be wrapped in parentheses
|
||||
/// to maintain correct operator precedence (multiplication/division have higher precedence than addition/subtraction).
|
||||
/// </summary>
|
||||
/// <param name="arithmeticExpression">The parent arithmetic expression.</param>
|
||||
/// <param name="other">The sub-expression to evaluate.</param>
|
||||
/// <returns>True if the sub-expression should be wrapped in parentheses; otherwise, false.</returns>
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wraps an expression in parentheses if the provided predicate returns true.
|
||||
/// This is used to ensure correct operator precedence in generated SQL.
|
||||
/// </summary>
|
||||
/// <param name="expression">The expression to potentially wrap.</param>
|
||||
/// <param name="shouldWrap">A predicate function that determines if wrapping is needed.</param>
|
||||
/// <returns>The expression SQL with or without parentheses.</returns>
|
||||
private string WrapInParenthesis(Expression expression, Func<Expression, bool> shouldWrap)
|
||||
{
|
||||
return shouldWrap(expression)
|
||||
? $"({expression.Accept(this)})"
|
||||
: $"{expression.Accept(this)}";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user