rawsql-ts
Version:
[beta]High-performance SQL parser and AST analyzer written in TypeScript. Provides fast parsing and advanced transformation capabilities.
440 lines • 22.8 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.FunctionExpressionParser = void 0;
const Lexeme_1 = require("../models/Lexeme");
const ValueComponent_1 = require("../models/ValueComponent");
const OverExpressionParser_1 = require("./OverExpressionParser");
const ValueParser_1 = require("./ValueParser");
const FullNameParser_1 = require("./FullNameParser");
const SelectQueryParser_1 = require("./SelectQueryParser");
const OrderByClauseParser_1 = require("./OrderByClauseParser");
const ParseError_1 = require("./ParseError");
class FunctionExpressionParser {
/**
* Parse ARRAY expressions - handles both ARRAY[...] (literal) and ARRAY(...) (query) syntax
* @param lexemes Array of lexemes to parse
* @param index Current parsing index
* @returns Parsed array expression and new index
*/
static parseArrayExpression(lexemes, index) {
let idx = index;
// Check if this is array literal (ARRAY[...]) or function call (ARRAY(...))
if (idx + 1 < lexemes.length && (lexemes[idx + 1].type & Lexeme_1.TokenType.OpenBracket)) {
idx++;
const arg = ValueParser_1.ValueParser.parseArgument(Lexeme_1.TokenType.OpenBracket, Lexeme_1.TokenType.CloseBracket, lexemes, idx);
idx = arg.newIndex;
const value = new ValueComponent_1.ArrayExpression(arg.value);
return { value, newIndex: idx };
}
else if (idx + 1 < lexemes.length && (lexemes[idx + 1].type & Lexeme_1.TokenType.OpenParen)) {
idx++;
idx++; // Skip the opening parenthesis
const arg = SelectQueryParser_1.SelectQueryParser.parseFromLexeme(lexemes, idx);
idx = arg.newIndex;
idx++; // Skip the closing parenthesis
const value = new ValueComponent_1.ArrayQueryExpression(arg.value);
return { value, newIndex: idx };
}
throw new Error(`Invalid ARRAY syntax at index ${idx}, expected ARRAY[... or ARRAY(...)`);
}
static parseFromLexeme(lexemes, index) {
let idx = index;
const current = lexemes[idx];
if (current.value === "array") {
return this.parseArrayExpression(lexemes, idx);
}
else if (current.value === "substring" || current.value === "overlay") {
return this.parseKeywordFunction(lexemes, idx, [
{ key: "from", required: false },
{ key: "for", required: false }
]);
}
else if (current.value === "cast") {
return this.parseKeywordFunction(lexemes, idx, [
{ key: "as", required: true }
]);
}
else if (current.value === "trim") {
return this.parseKeywordFunction(lexemes, idx, [
{ key: "from", required: false }
]);
}
return this.parseFunctionCall(lexemes, idx);
}
static tryParseBinaryExpression(lexemes, index, left, allowAndOperator = true, allowOrOperator = true) {
let idx = index;
// If the next element is an operator, process it as a binary expression
if (idx < lexemes.length && (lexemes[idx].type & Lexeme_1.TokenType.Operator)) {
const operator = lexemes[idx].value.toLowerCase();
if (!allowAndOperator && operator === "and") {
// Handle special case for "and" operator
return null;
}
if (!allowOrOperator && operator === "or") {
// Handle special case for "or" operator
return null;
}
idx++;
// between
if (operator === "between") {
return this.parseBetweenExpression(lexemes, idx, left, false);
}
else if (operator === "not between") {
return this.parseBetweenExpression(lexemes, idx, left, true);
}
// ::
if (operator === "::") {
const typeValue = this.parseTypeValue(lexemes, idx);
idx = typeValue.newIndex;
const exp = new ValueComponent_1.CastExpression(left, typeValue.value);
return { value: exp, newIndex: idx };
}
// Get the right-hand side value
const rightResult = ValueParser_1.ValueParser.parseFromLexeme(lexemes, idx);
idx = rightResult.newIndex;
// Create binary expression
const value = new ValueComponent_1.BinaryExpression(left, operator, rightResult.value);
return { value, newIndex: idx };
}
return null;
}
static parseBetweenExpression(lexemes, index, value, negated) {
let idx = index;
const lower = ValueParser_1.ValueParser.parseFromLexeme(lexemes, idx, false);
idx = lower.newIndex;
if (idx < lexemes.length && (lexemes[idx].type & Lexeme_1.TokenType.Operator) && lexemes[idx].value !== "and") {
throw new Error(`Expected 'and' after 'between' at index ${idx}`);
}
idx++;
// Parse upper bound with restricted scope - stop at logical operators
const upper = this.parseBetweenUpperBound(lexemes, idx);
idx = upper.newIndex;
const result = new ValueComponent_1.BetweenExpression(value, lower.value, upper.value, negated);
return { value: result, newIndex: idx };
}
/**
* Parse the upper bound of a BETWEEN expression with logical operator precedence
* This stops parsing when it encounters AND/OR operators at the same level
*/
static parseBetweenUpperBound(lexemes, index) {
// Parse with higher precedence than AND/OR to ensure BETWEEN binds tighter
// Use precedence 3 (higher than AND=2, OR=1) as minimum to stop at logical operators
return ValueParser_1.ValueParser.parseFromLexeme(lexemes, index, false, false);
}
static parseFunctionCall(lexemes, index) {
let idx = index;
// Parse namespaced function name (e.g., myschema.myfunc, dbo.util.myfunc)
// Use FullNameParser to get namespaces and function name
const fullNameResult = FullNameParser_1.FullNameParser.parseFromLexeme(lexemes, idx);
const namespaces = fullNameResult.namespaces;
const name = fullNameResult.name;
idx = fullNameResult.newIndex;
if (idx < lexemes.length && (lexemes[idx].type & Lexeme_1.TokenType.OpenParen)) {
// Check if this is an aggregate function that supports internal ORDER BY
const functionName = name.name.toLowerCase();
let arg;
let closingComments = null;
let internalOrderBy = null;
if (this.AGGREGATE_FUNCTIONS_WITH_ORDER_BY.has(functionName)) {
// Use special aggregate function argument parser with comment capture
const result = this.parseAggregateArguments(lexemes, idx);
arg = { value: result.arguments, newIndex: result.newIndex };
internalOrderBy = result.orderByClause;
closingComments = result.closingComments;
}
else {
// General argument parsing with comment capture
const argWithComments = this.parseArgumentWithComments(lexemes, idx);
arg = { value: argWithComments.value, newIndex: argWithComments.newIndex };
closingComments = argWithComments.closingComments;
}
idx = arg.newIndex;
// Check for WITHIN GROUP clause
let withinGroup = null;
if (idx < lexemes.length && lexemes[idx].value === "within group") {
const withinGroupResult = this.parseWithinGroupClause(lexemes, idx);
withinGroup = withinGroupResult.value;
idx = withinGroupResult.newIndex;
}
// Check for WITH ORDINALITY clause
let withOrdinality = false;
if (idx < lexemes.length && lexemes[idx].value === "with ordinality") {
withOrdinality = true;
idx++; // Skip single "with ordinality" token
}
if (idx < lexemes.length && lexemes[idx].value === "over") {
const over = OverExpressionParser_1.OverExpressionParser.parseFromLexeme(lexemes, idx);
idx = over.newIndex;
const value = new ValueComponent_1.FunctionCall(namespaces, name.name, arg.value, over.value, withinGroup, withOrdinality, internalOrderBy);
// Set closing comments if available
if (closingComments && closingComments.length > 0) {
value.comments = closingComments;
}
return { value, newIndex: idx };
}
else {
const value = new ValueComponent_1.FunctionCall(namespaces, name.name, arg.value, null, withinGroup, withOrdinality, internalOrderBy);
// Set closing comments if available
if (closingComments && closingComments.length > 0) {
value.comments = closingComments;
}
return { value, newIndex: idx };
}
}
else {
throw ParseError_1.ParseError.fromUnparsedLexemes(lexemes, idx, `Expected opening parenthesis after function name '${name.name}'.`);
}
}
static parseKeywordFunction(lexemes, index, keywords) {
let idx = index;
// Parse function name and namespaces at the beginning for consistent usage
const fullNameResult = FullNameParser_1.FullNameParser.parseFromLexeme(lexemes, idx);
const namespaces = fullNameResult.namespaces;
const name = fullNameResult.name;
idx = fullNameResult.newIndex;
if (idx < lexemes.length && (lexemes[idx].type & Lexeme_1.TokenType.OpenParen)) {
idx++;
const input = ValueParser_1.ValueParser.parseFromLexeme(lexemes, idx);
let arg = input.value;
idx = input.newIndex;
// Delegate to the standard function parser if parsing by comma
if (idx < lexemes.length && (lexemes[idx].type & Lexeme_1.TokenType.Comma)) {
return this.parseFunctionCall(lexemes, index);
}
// Check for required/optional keywords in function arguments
for (const { key, required } of keywords) {
if (idx < lexemes.length && (lexemes[idx].type & Lexeme_1.TokenType.Command) && lexemes[idx].value === key) {
idx++;
if (idx < lexemes.length && (lexemes[idx].type & Lexeme_1.TokenType.Type)) {
const typeValue = this.parseTypeValue(lexemes, idx);
arg = new ValueComponent_1.BinaryExpression(arg, key, typeValue.value);
idx = typeValue.newIndex;
}
else {
const right = ValueParser_1.ValueParser.parseFromLexeme(lexemes, idx);
arg = new ValueComponent_1.BinaryExpression(arg, key, right.value);
idx = right.newIndex;
}
}
else if (required) {
throw ParseError_1.ParseError.fromUnparsedLexemes(lexemes, idx, `Keyword '${key}' is required for ${name.name} function.`);
}
}
if (idx < lexemes.length && (lexemes[idx].type & Lexeme_1.TokenType.CloseParen)) {
idx++;
// Check for WITHIN GROUP clause
let withinGroup = null;
if (idx < lexemes.length && lexemes[idx].value === "within group") {
const withinGroupResult = this.parseWithinGroupClause(lexemes, idx);
withinGroup = withinGroupResult.value;
idx = withinGroupResult.newIndex;
}
// Check for WITH ORDINALITY clause
let withOrdinality = false;
if (idx < lexemes.length && lexemes[idx].value === "with ordinality") {
withOrdinality = true;
idx++; // Skip single "with ordinality" token
}
// Use the previously parsed namespaces and function name for consistency
if (idx < lexemes.length && lexemes[idx].value === "over") {
idx++;
const over = OverExpressionParser_1.OverExpressionParser.parseFromLexeme(lexemes, idx);
idx = over.newIndex;
const value = new ValueComponent_1.FunctionCall(namespaces, name.name, arg, over.value, withinGroup, withOrdinality, null);
return { value, newIndex: idx };
}
else {
const value = new ValueComponent_1.FunctionCall(namespaces, name.name, arg, null, withinGroup, withOrdinality, null);
return { value, newIndex: idx };
}
}
else {
throw ParseError_1.ParseError.fromUnparsedLexemes(lexemes, idx, `Missing closing parenthesis for function '${name.name}'.`);
}
}
else {
throw ParseError_1.ParseError.fromUnparsedLexemes(lexemes, idx, `Missing opening parenthesis for function '${name.name}'.`);
}
}
static parseTypeValue(lexemes, index) {
let idx = index;
const { namespaces, name, newIndex } = FullNameParser_1.FullNameParser.parseFromLexeme(lexemes, idx);
idx = newIndex;
if (idx < lexemes.length && (lexemes[idx].type & Lexeme_1.TokenType.OpenParen)) {
const arg = ValueParser_1.ValueParser.parseArgument(Lexeme_1.TokenType.OpenParen, Lexeme_1.TokenType.CloseParen, lexemes, idx);
idx = arg.newIndex;
const value = new ValueComponent_1.TypeValue(namespaces, new ValueComponent_1.RawString(name.name), arg.value);
return { value, newIndex: idx };
}
else {
const value = new ValueComponent_1.TypeValue(namespaces, new ValueComponent_1.RawString(name.name));
return { value, newIndex: idx };
}
}
/**
* Parse WITHIN GROUP (ORDER BY ...) clause
* @param lexemes Array of lexemes to parse
* @param index Current parsing index (should point to "WITHIN GROUP")
* @returns Parsed OrderByClause and new index
*/
static parseWithinGroupClause(lexemes, index) {
let idx = index;
// Expect "WITHIN GROUP" (now a single token)
if (idx >= lexemes.length || lexemes[idx].value !== "within group") {
throw new Error(`Expected 'WITHIN GROUP' at index ${idx}`);
}
idx++;
// Expect "("
if (idx >= lexemes.length || !(lexemes[idx].type & Lexeme_1.TokenType.OpenParen)) {
throw new Error(`Expected '(' after 'WITHIN GROUP' at index ${idx}`);
}
idx++;
// Parse ORDER BY clause
const orderByResult = OrderByClauseParser_1.OrderByClauseParser.parseFromLexeme(lexemes, idx);
idx = orderByResult.newIndex;
// Expect ")"
if (idx >= lexemes.length || !(lexemes[idx].type & Lexeme_1.TokenType.CloseParen)) {
throw new Error(`Expected ')' after WITHIN GROUP ORDER BY clause at index ${idx}`);
}
idx++;
return { value: orderByResult.value, newIndex: idx };
}
/**
* Parse arguments for aggregate functions that support internal ORDER BY
* Handles patterns like: string_agg(expr, separator ORDER BY sort_expr)
* @param lexemes Array of lexemes to parse
* @param index Current parsing index (should point to opening parenthesis)
* @returns Parsed arguments, ORDER BY clause, closing parenthesis comments, and new index
*/
static parseAggregateArguments(lexemes, index) {
let idx = index;
const args = [];
let orderByClause = null;
// Check for opening parenthesis
if (idx >= lexemes.length || !(lexemes[idx].type & Lexeme_1.TokenType.OpenParen)) {
throw ParseError_1.ParseError.fromUnparsedLexemes(lexemes, idx, `Expected opening parenthesis.`);
}
idx++;
// Handle empty arguments
if (idx < lexemes.length && (lexemes[idx].type & Lexeme_1.TokenType.CloseParen)) {
const closingComments = lexemes[idx].comments;
idx++;
return { arguments: new ValueComponent_1.ValueList([]), orderByClause: null, closingComments, newIndex: idx };
}
// Handle wildcard case
if (idx < lexemes.length && lexemes[idx].value === "*") {
const wildcard = new ValueComponent_1.ColumnReference(null, "*");
idx++;
if (idx < lexemes.length && (lexemes[idx].type & Lexeme_1.TokenType.CloseParen)) {
const closingComments = lexemes[idx].comments;
idx++;
return { arguments: wildcard, orderByClause: null, closingComments, newIndex: idx };
}
else {
throw ParseError_1.ParseError.fromUnparsedLexemes(lexemes, idx, `Expected closing parenthesis after wildcard '*'.`);
}
}
// Parse first argument
const firstArg = ValueParser_1.ValueParser.parseFromLexeme(lexemes, idx);
idx = firstArg.newIndex;
args.push(firstArg.value);
// Parse additional arguments separated by comma, or ORDER BY
while (idx < lexemes.length &&
((lexemes[idx].type & Lexeme_1.TokenType.Comma) || lexemes[idx].value === "order by")) {
// Check if current token is ORDER BY (without comma)
if (lexemes[idx].value === "order by") {
// Parse ORDER BY clause
const orderByResult = OrderByClauseParser_1.OrderByClauseParser.parseFromLexeme(lexemes, idx);
idx = orderByResult.newIndex;
orderByClause = orderByResult.value;
break; // ORDER BY should be the last element in aggregate functions
}
if (lexemes[idx].type & Lexeme_1.TokenType.Comma) {
idx++; // Skip comma
// Check if next token after comma is ORDER BY
if (idx < lexemes.length && lexemes[idx].value === "order by") {
// Parse ORDER BY clause
const orderByResult = OrderByClauseParser_1.OrderByClauseParser.parseFromLexeme(lexemes, idx);
idx = orderByResult.newIndex;
orderByClause = orderByResult.value;
break; // ORDER BY should be the last element in aggregate functions
}
// Parse regular argument after comma
const argResult = ValueParser_1.ValueParser.parseFromLexeme(lexemes, idx);
idx = argResult.newIndex;
args.push(argResult.value);
}
}
// Check for closing parenthesis and capture comments
if (idx >= lexemes.length || !(lexemes[idx].type & Lexeme_1.TokenType.CloseParen)) {
throw ParseError_1.ParseError.fromUnparsedLexemes(lexemes, idx, `Expected closing parenthesis.`);
}
const closingComments = lexemes[idx].comments;
idx++;
// Return single argument if only one, otherwise return ValueList
const argumentsValue = args.length === 1 ? args[0] : new ValueComponent_1.ValueList(args);
return { arguments: argumentsValue, orderByClause, closingComments, newIndex: idx };
}
/**
* Parse function arguments and capture closing parenthesis comments
* @param lexemes Array of lexemes to parse
* @param index Current parsing index (should point to opening parenthesis)
* @returns Parsed arguments, closing parenthesis comments, and new index
*/
static parseArgumentWithComments(lexemes, index) {
let idx = index;
// Check for opening parenthesis
if (idx >= lexemes.length || !(lexemes[idx].type & Lexeme_1.TokenType.OpenParen)) {
throw ParseError_1.ParseError.fromUnparsedLexemes(lexemes, idx, `Expected opening parenthesis.`);
}
idx++; // Skip opening parenthesis
const args = [];
// Check for empty parentheses
if (idx < lexemes.length && (lexemes[idx].type & Lexeme_1.TokenType.CloseParen)) {
const closingComments = lexemes[idx].comments;
idx++; // Skip closing parenthesis
return { value: new ValueComponent_1.ValueList([]), closingComments, newIndex: idx };
}
// Handle wildcard case: count(*)
if (idx < lexemes.length && lexemes[idx].value === "*") {
const wildcard = new ValueComponent_1.ColumnReference(null, "*");
idx++;
// Check for closing parenthesis
if (idx >= lexemes.length || !(lexemes[idx].type & Lexeme_1.TokenType.CloseParen)) {
throw ParseError_1.ParseError.fromUnparsedLexemes(lexemes, idx, `Expected closing parenthesis after wildcard '*'.`);
}
const closingComments = lexemes[idx].comments;
idx++;
return { value: wildcard, closingComments, newIndex: idx };
}
// Parse regular arguments
const result = ValueParser_1.ValueParser.parseFromLexeme(lexemes, idx);
idx = result.newIndex;
args.push(result.value);
// Continue reading if the next element is a comma
while (idx < lexemes.length && (lexemes[idx].type & Lexeme_1.TokenType.Comma)) {
idx++;
const argResult = ValueParser_1.ValueParser.parseFromLexeme(lexemes, idx);
idx = argResult.newIndex;
args.push(argResult.value);
}
// Check for closing parenthesis
if (idx >= lexemes.length || !(lexemes[idx].type & Lexeme_1.TokenType.CloseParen)) {
throw ParseError_1.ParseError.fromUnparsedLexemes(lexemes, idx, `Expected closing parenthesis.`);
}
const closingComments = lexemes[idx].comments;
idx++;
// Return single argument if only one, otherwise return ValueList
const argumentsValue = args.length === 1 ? args[0] : new ValueComponent_1.ValueList(args);
return { value: argumentsValue, closingComments, newIndex: idx };
}
}
exports.FunctionExpressionParser = FunctionExpressionParser;
/**
* Aggregate functions that support internal ORDER BY clause
*/
FunctionExpressionParser.AGGREGATE_FUNCTIONS_WITH_ORDER_BY = new Set([
'string_agg', 'array_agg', 'json_agg', 'jsonb_agg',
'json_object_agg', 'jsonb_object_agg', 'xmlagg'
]);
//# sourceMappingURL=FunctionExpressionParser.js.map