chore: initial git load of code space

This commit is contained in:
Thom Lamb
2026-05-12 08:52:33 -05:00
parent 9abada692f
commit 5e467bcc9c
384 changed files with 65960 additions and 2 deletions
@@ -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 &gt;=, &pipe;&pipe;, etc.
/// </summary>
/// <param name="symbolLiteral">The symbol literal expression to convert.</param>
/// <returns>
/// The symbolic operator string (e.g., "&gt;=", "&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., =, !=, &gt;, &lt;, &gt;=, &lt;=).
/// </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)}";
}
}