UNPKG

rawsql-ts

Version:

High-performance SQL parser and AST analyzer written in TypeScript. Provides fast parsing and advanced transformation capabilities.

190 lines 8.23 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ParameterTokenReader = void 0; const BaseTokenReader_1 = require("./BaseTokenReader"); const Lexeme_1 = require("../models/Lexeme"); const charLookupTable_1 = require("../utils/charLookupTable"); /** * Reads SQL parameter tokens (@param, :param, $param, ?, ${param}) */ class ParameterTokenReader extends BaseTokenReader_1.BaseTokenReader { constructor(input) { super(input); } /** * Try to read a parameter token */ tryRead(previous) { if (this.isEndOfInput()) { return null; } // parameter with suffix (${param}) - check this first if (this.canRead(1) && this.input[this.position] === '$' && this.input[this.position + 1] === '{') { this.position += 2; // Skip ${ const start = this.position; while (this.canRead() && this.input[this.position] !== '}') { this.position++; } if (this.isEndOfInput()) { throw new Error(`Unexpected end of input. Expected closing '}' for parameter at position ${start}`); } const identifier = this.input.slice(start, this.position); if (identifier.length === 0) { throw new Error('Empty parameter name is not allowed: found ${} at position ' + (start - 2)); } this.position++; // Skip } return this.createLexeme(Lexeme_1.TokenType.Parameter, '${' + identifier + '}'); } const char = this.input[this.position]; // named parameter (@param, :param, $param) if (charLookupTable_1.CharLookupTable.isNamedParameterPrefix(char)) { // However, do not recognize as a parameter if the next character is an operator symbol // To avoid postgres `::` if (this.canRead(1) && charLookupTable_1.CharLookupTable.isOperatorSymbol(this.input[this.position + 1])) { return null; } // Don't treat `:` as parameter prefix in array slice context [1:2] if (char === ':' && this.isInArraySliceContext()) { return null; } // Special handling for PostgreSQL dollar-quoted strings ($$ or $tag$) if (char === '$' && this.isDollarQuotedString()) { return null; // Let LiteralTokenReader handle it as dollar-quoted string } // Special handling for SQL Server MONEY literals ($123.45) // Only treat as MONEY if it contains decimal point or comma (not just $123) if (char === '$' && this.canRead(1) && charLookupTable_1.CharLookupTable.isDigit(this.input[this.position + 1])) { // Look ahead to see if this looks like a MONEY literal (has . or ,) let pos = this.position + 1; let hasDecimalOrComma = false; while (pos < this.input.length && (charLookupTable_1.CharLookupTable.isDigit(this.input[pos]) || this.input[pos] === ',' || this.input[pos] === '.')) { if (this.input[pos] === '.' || this.input[pos] === ',') { hasDecimalOrComma = true; break; } pos++; } if (hasDecimalOrComma) { return null; // Let LiteralTokenReader handle it as MONEY } // Otherwise, treat as parameter (e.g., $1, $123) } this.position++; // Read the identifier part after the prefix const start = this.position; while (this.canRead() && !charLookupTable_1.CharLookupTable.isDelimiter(this.input[this.position])) { this.position++; } const identifier = this.input.slice(start, this.position); return this.createLexeme(Lexeme_1.TokenType.Parameter, char + identifier); } // nameless parameter (?) // However, do not recognize as a parameter if it could be a JSON operator if (char === '?') { // Check for JSON operators ?| and ?& if (this.canRead(1)) { const nextChar = this.input[this.position + 1]; if (nextChar === '|' || nextChar === '&') { return null; // Let OperatorTokenReader handle it } } // If previous token is an identifier or literal, ? might be a JSON operator if (previous && (previous.type & Lexeme_1.TokenType.Identifier || previous.type & Lexeme_1.TokenType.Literal)) { return null; // Let OperatorTokenReader handle it } this.position++; return this.createLexeme(Lexeme_1.TokenType.Parameter, char); } return null; } /** * Check if we're in an array slice context where : should be treated as an operator * Look backwards for an opening bracket that suggests array access */ isInArraySliceContext() { // Look backwards from current position to find opening bracket let pos = this.position - 1; let bracketDepth = 0; let parenDepth = 0; while (pos >= 0) { const char = this.input[pos]; if (char === ']') { bracketDepth++; } else if (char === '[') { bracketDepth--; if (bracketDepth < 0) { // Found unmatched opening bracket, check if it's array access context // Array access context: after identifier, closing paren, or closing bracket if (pos > 0) { const prevChar = this.input[pos - 1]; // If previous char could end an expression (identifier, paren, bracket) if (/[a-zA-Z0-9_)\]]/.test(prevChar)) { return true; } } // Also check if we're at start of input with brackets if (pos === 0) { return false; // Standalone [expr] is not array access } break; } } else if (char === ')') { parenDepth++; } else if (char === '(') { parenDepth--; // Continue searching even through parentheses as they might be function calls // in array slice context like arr[func(x):func(y)] } pos--; } return false; } /** * Check if the current position starts a PostgreSQL dollar-quoted string * Patterns: $$ or $tag$ */ isDollarQuotedString() { if (!this.canRead(1)) { return false; } // Check for $$ pattern if (this.input[this.position + 1] === '$') { return true; } // Check for $tag$ pattern // Look for the closing $ after the tag let pos = this.position + 1; while (pos < this.input.length) { const char = this.input[pos]; if (char === '$') { // Found closing $ - this looks like $tag$ pattern return true; } // Tag can contain letters, digits, underscores if (!this.isAlphanumeric(char) && char !== '_') { // Invalid character for tag, not a dollar-quoted string return false; } pos++; } // No closing $ found return false; } /** * Check if character is alphanumeric (letter or digit) */ isAlphanumeric(char) { if (char.length !== 1) return false; const code = char.charCodeAt(0); // Check if digit (0-9) or letter (a-z, A-Z) return (code >= 48 && code <= 57) || // 0-9 (code >= 65 && code <= 90) || // A-Z (code >= 97 && code <= 122); // a-z } } exports.ParameterTokenReader = ParameterTokenReader; //# sourceMappingURL=ParameterTokenReader.js.map