Files
sql-utilities/src/Strata.SqlTools.PostgreSql/Visitors/CommandVisitor.cs
T
Thom LambandClaude Opus 4.7 acfe29ee98 fix(pgsql): make CommandVisitor parameter index instance-scoped
Resolves SonarQube S2696 in CommandVisitor.cs.

The static _parameterIndex field was mutated from an instance method,
causing every new CommandVisitor to inherit the previous instance's
counter and never reset. Parameter indices now restart at $1 per
visitor, which is the intended PostgreSQL behavior.

Adds CommandVisitorTests.TwoVisitors_HaveIndependentParameterIndices to
lock in the contract via reflection (FormatParameterName is protected).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 16:21:17 -05:00

63 lines
2.7 KiB
C#

using Strata.SqlTools.SqlBreakdown.Expressions.Conditional;
using SqlServerCommandVisitor = Strata.SqlTools.Visitors.SqlServer.CommandVisitor;
namespace Strata.SqlTools.Visitors.PostgreSql;
/// <summary>
/// Implements the visitor pattern to convert SQL expression objects into PostgreSQL-compatible SQL command strings.
/// Inherits from SqlServer.CommandVisitor and overrides only the dialect-specific formatting methods.
/// </summary>
public class CommandVisitor : SqlServerCommandVisitor
{
private int _parameterIndex = 1;
/// <summary>
/// Formats an identifier for PostgreSQL using double-quote quoting.
/// </summary>
/// <param name="identifier">The identifier to format.</param>
/// <returns>The quoted identifier.</returns>
protected override string FormatIdentifier(string identifier) => $"\"{identifier}\"";
/// <summary>
/// Formats a parameter name for PostgreSQL using positional parameter syntax.
/// Parameters in PostgreSQL are referenced as $1, $2, $3, etc.
/// </summary>
/// <param name="parameterName">The parameter name to format.</param>
/// <returns>A SQL string in the format "$position" where position is a number.</returns>
protected override string FormatParameterName(string parameterName)
{
// PostgreSQL uses positional parameters: $1, $2, $3, etc.
return $"${_parameterIndex++}";
}
/// <summary>
/// Formats a boolean literal for PostgreSQL using TRUE/FALSE keywords.
/// </summary>
/// <param name="value">The boolean value to format.</param>
/// <returns>The string "true" or "false" in lowercase.</returns>
protected override string FormatBooleanLiteral(bool value) => value ? "true" : "false";
/// <summary>
/// Formats a string literal for PostgreSQL with proper escaping of single quotes.
/// </summary>
/// <param name="value">The string value to format.</param>
/// <returns>A SQL string literal enclosed in single quotes with escaped quotes.</returns>
protected override string FormatStringLiteral(string value)
{
// PostgreSQL: escape single quotes by doubling them
var escaped = value.Replace("'", "''");
return $"'{escaped}'";
}
/// <summary>
/// Formats a case-insensitive LIKE expression for PostgreSQL using ILIKE keyword.
/// </summary>
/// <param name="likeExpression">The LIKE expression to format.</param>
/// <returns>A SQL string in the format "expression ILIKE pattern".</returns>
protected override string FormatCaseInsensitiveLike(LikeExpression likeExpression)
{
return $"{likeExpression.Subject.Accept(this)} ILIKE {likeExpression.Pattern.Accept(this)}";
}
}