using System.Text;
using Strata.SqlTools.SqlBreakdown.Classes;
using Strata.SqlTools.SqlBreakdown.Enums.SQL;
using Strata.SqlTools.SqlBreakdown.Exceptions;
using SqlServerStatementReader = Strata.SqlTools.Statements.SqlServer.StatementReader;
namespace Strata.SqlTools.Statements.PostgreSql;
///
/// PostgreSQL-specific tokenizer class that reads a string representation of a PostgreSQL SQL statement
/// and parses out each part as a token. Handles PostgreSQL's double-quoted identifiers, schema-qualified names,
/// single-quoted string literals, positional parameters, and PostgreSQL naming conventions.
///
public class StatementReader : SqlServerStatementReader
{
public StatementReader(string sqlStatement) : base(sqlStatement)
{
}
///
/// Handles PostgreSQL-specific characters: double-quotes (") for delimited identifiers,
/// single quotes (') for string literals, dollar sign ($) for positional parameters,
/// colon (:) for named parameters, and at-sign (@) for named parameters.
///
/// True if the character was handled; false otherwise.
///
/// Attempts to handle additional PostgreSQL-specific characters that the base reader doesn't handle.
///
/// True if the character was handled; false otherwise.
#pragma warning disable S3776 // Cognitive Complexity - Refactoring this would reduce clarity
protected override bool TryHandleAdditionalCharacter()
{
if (CurrentCharacter == '"')
{
// PostgreSQL uses double quotes for delimited identifiers (case-sensitive)
MovePosition();
var quotedIdentifier = GrabStringValue();
_currentToken = new Token(TokenType.ColumnIdentifier, quotedIdentifier);
if (CurrentCharacter != '"')
{
throw new InvalidSyntaxException(
$"Invalid syntax at position {Position}. Expected closing double quote.");
}
MovePosition();
return true;
}
if (CurrentCharacter == '\'')
{
// PostgreSQL uses single quotes for string literals
MovePosition();
var stringLiteral = GrabStringLiteral();
_currentToken = new Token(TokenType.String, stringLiteral);
if (CurrentCharacter != '\'')
{
throw new InvalidSyntaxException(
$"Invalid syntax at position {Position}. Expected closing single quote.");
}
MovePosition();
return true;
}
if (CurrentCharacter == '$')
{
// PostgreSQL positional parameters: $1, $2, etc.
MovePosition();
if (char.IsDigit(CurrentCharacter))
{
var paramNumber = GrabNumberValue();
_currentToken = new Token(TokenType.Parameter, $"${paramNumber}");
return true;
}
throw new InvalidSyntaxException(
$"Invalid syntax at position {Position}. Expected digit after $.");
}
if (CurrentCharacter == ':')
{
// PostgreSQL colon-prefixed named parameters: :userId
MovePosition();
if (char.IsLetter(CurrentCharacter) || CurrentCharacter == '_')
{
var paramName = GrabStringValue();
_currentToken = new Token(TokenType.Parameter, $":{paramName}");
return true;
}
throw new InvalidSyntaxException(
$"Invalid syntax at position {Position}. Expected identifier after :.");
}
if (CurrentCharacter == '@')
{
// PostgreSQL at-sign named parameters: @userId (also SQL Server compatible)
MovePosition();
if (char.IsLetter(CurrentCharacter) || CurrentCharacter == '_')
{
var paramName = GrabStringValue();
_currentToken = new Token(TokenType.Parameter, $"@{paramName}");
return true;
}
throw new InvalidSyntaxException(
$"Invalid syntax at position {Position}. Expected identifier after @.");
}
if (CurrentCharacter == '=')
{
// =, => (PostgreSQL hstore + other operations)
MovePosition();
if (TryMatchTwoCharOperator('>', "=>")) return true;
_currentToken = new Token(TokenType.Operator, "=");
return true;
}
if (CurrentCharacter == '|')
{
// |, ||
MovePosition();
if (TryMatchTwoCharOperator('|', "||")) return true;
_currentToken = new Token(TokenType.Operator, "|");
return true;
}
if (CurrentCharacter == '<')
{
// <, <=, <>, <<
MovePosition();
if (TryMatchTwoCharOperator('=', "<=")) return true;
if (TryMatchTwoCharOperator('>', "<>")) return true;
if (TryMatchTwoCharOperator('<', "<<")) return true;
_currentToken = new Token(TokenType.Operator, "<");
return true;
}
if (CurrentCharacter == '>')
{
// >, >=, >>
MovePosition();
if (TryMatchTwoCharOperator('=', ">=")) return true;
if (TryMatchTwoCharOperator('>', ">>")) return true;
_currentToken = new Token(TokenType.Operator, ">");
return true;
}
if (CurrentCharacter == '.')
{
// Handle .. range operator (used in arrays and ranges)
// and single . for column qualification (table.column)
if (Position + 1 < Length && _sqlStatement[Position + 1] == '.')
{
MovePosition();
MovePosition();
_currentToken = new Token(TokenType.Operator, "..");
return true;
}
// Single . is used for column qualification (table.column)
// Return it as an Operator token
MovePosition();
_currentToken = new Token(TokenType.Operator, ".");
return true;
}
return false;
}
#pragma warning restore S3776
///
/// Handles PostgreSQL-specific identifier prefixes: underscores (_) can start identifiers.
///
/// True if the character was handled; false otherwise.
protected override bool TryHandleIdentifierPrefix()
{
if (CurrentCharacter == '_')
{
var underscoreIdentifier = GrabStringValue();
_currentToken = new Token(TokenType.ColumnIdentifier, underscoreIdentifier);
return true;
}
return false;
}
///
/// Grabs a string literal value between single quotes, handling PostgreSQL's escaped quotes ('').
///
/// The string literal value without the surrounding quotes.
private string GrabStringLiteral()
{
var stringValue = new StringBuilder();
while (CurrentCharacter != '\'' && CurrentCharacter != char.MinValue)
{
stringValue.Append(CurrentCharacter);
MovePosition();
// Handle escaped single quotes ('')
if (CurrentCharacter == '\'')
{
var nextPos = Position + 1;
if (nextPos < Length && _sqlStatement[nextPos] == '\'')
{
// Double single-quote is an escape
stringValue.Append('\'');
MovePosition(); // Skip first quote
MovePosition(); // Skip second quote
}
}
}
return stringValue.ToString();
}
///
/// If the position is currently sitting on , advances past it,
/// emits as the current Operator token, and returns
/// true. Otherwise leaves position untouched and returns false. Used by the
/// multi-character operator dispatch (e.g. </<=/<>/<<).
///
private bool TryMatchTwoCharOperator(char nextChar, string twoCharOperator)
{
if (CurrentCharacter != nextChar)
{
return false;
}
MovePosition();
_currentToken = new Token(TokenType.Operator, twoCharOperator);
return true;
}
}