UNPKG

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
"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