UNPKG

rawsql-ts

Version:

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

189 lines 8.55 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ExplainStatementParser = void 0; const SqlTokenizer_1 = require("./SqlTokenizer"); const DDLStatements_1 = require("../models/DDLStatements"); const ValueComponent_1 = require("../models/ValueComponent"); const Lexeme_1 = require("../models/Lexeme"); const ValueParser_1 = require("./ValueParser"); /** * Parses EXPLAIN statements including legacy shorthand flags and option lists. */ class ExplainStatementParser { static parse(sql, parseNested) { const tokenizer = new SqlTokenizer_1.SqlTokenizer(sql); const lexemes = tokenizer.readLexemes(); const result = this.parseFromLexeme(lexemes, 0, parseNested); if (result.newIndex < lexemes.length) { throw new Error(`[ExplainStatementParser] Unexpected token "${lexemes[result.newIndex].value}" after EXPLAIN statement.`); } return result.value; } static parseFromLexeme(lexemes, index, parseNested) { let idx = index; const first = lexemes[idx]; if (!first || first.value.toLowerCase() !== "explain") { throw new Error(`[ExplainStatementParser] Expected EXPLAIN at index ${idx}.`); } idx++; const options = []; // Collect legacy shorthand flags (ANALYZE, VERBOSE) before option list. while (this.isLegacyFlag(lexemes[idx])) { const flagLexeme = lexemes[idx]; const optionName = new ValueComponent_1.IdentifierString(flagLexeme.value.toLowerCase()); if (flagLexeme.comments && flagLexeme.comments.length > 0) { optionName.comments = [...flagLexeme.comments]; } const option = new DDLStatements_1.ExplainOption({ name: optionName, value: new ValueComponent_1.RawString("true"), }); options.push(option); idx++; } // Parse optional parenthesised option list. if (lexemes[idx] && (lexemes[idx].type & Lexeme_1.TokenType.OpenParen) !== 0) { const optionResult = this.parseOptionList(lexemes, idx); options.push(...optionResult.options); idx = optionResult.newIndex; if (optionResult.trailingComments && optionResult.trailingComments.length > 0) { // Promote comments that follow the option list so they attach to the nested statement. this.attachTrailingCommentsToNextLexeme(lexemes, idx, optionResult.trailingComments); } } if (idx >= lexemes.length) { throw new Error("[ExplainStatementParser] EXPLAIN must be followed by a statement to analyze."); } const nested = parseNested(lexemes, idx); const statement = nested.value; idx = nested.newIndex; const explain = new DDLStatements_1.ExplainStatement({ options: options.length > 0 ? options : null, statement, }); return { value: explain, newIndex: idx }; } static parseOptionList(lexemes, index) { let idx = index; if (!(lexemes[idx].type & Lexeme_1.TokenType.OpenParen)) { throw new Error(`[ExplainStatementParser] Expected '(' to start option list at index ${idx}.`); } idx++; // consume '(' const options = []; let trailingComments = null; while (idx < lexemes.length) { const token = lexemes[idx]; if (!token) { throw new Error("[ExplainStatementParser] Unterminated option list."); } if (token.type & Lexeme_1.TokenType.CloseParen) { idx++; // consume ')' break; } if (!this.canStartOptionName(token)) { throw new Error(`[ExplainStatementParser] Expected option name inside EXPLAIN option list at index ${idx}, found "${token.value}".`); } const nameComponent = new ValueComponent_1.IdentifierString(token.value.toLowerCase()); if (token.comments && token.comments.length > 0) { nameComponent.comments = [...token.comments]; } idx++; let value = null; // Optional equals sign before value. if (lexemes[idx] && (lexemes[idx].type & Lexeme_1.TokenType.Operator) && lexemes[idx].value === "=") { idx++; } // Consume explicit value when present (anything other than comma/close paren). if (lexemes[idx] && !(lexemes[idx].type & Lexeme_1.TokenType.Comma) && !(lexemes[idx].type & Lexeme_1.TokenType.CloseParen)) { const parsedValue = ValueParser_1.ValueParser.parseFromLexeme(lexemes, idx); value = parsedValue.value; idx = parsedValue.newIndex; } if (!value) { value = new ValueComponent_1.RawString("true"); } options.push(new DDLStatements_1.ExplainOption({ name: nameComponent, value })); if (!lexemes[idx]) { throw new Error("[ExplainStatementParser] Unterminated option list."); } if (lexemes[idx].type & Lexeme_1.TokenType.Comma) { idx++; continue; } if (lexemes[idx].type & Lexeme_1.TokenType.CloseParen) { trailingComments = this.extractAfterComments(lexemes[idx]); idx++; break; } throw new Error(`[ExplainStatementParser] Expected ',' or ')' after EXPLAIN option at index ${idx}, found "${lexemes[idx].value}".`); } return { options, newIndex: idx, trailingComments }; } static isLegacyFlag(lexeme) { if (!lexeme) { return false; } if (!(lexeme.type & (Lexeme_1.TokenType.Identifier | Lexeme_1.TokenType.Command | Lexeme_1.TokenType.Function | Lexeme_1.TokenType.Type))) { return false; } const keyword = lexeme.value.toLowerCase(); return keyword === "analyze" || keyword === "verbose"; } static canStartOptionName(lexeme) { return (lexeme.type & (Lexeme_1.TokenType.Identifier | Lexeme_1.TokenType.Command | Lexeme_1.TokenType.Function | Lexeme_1.TokenType.Type)) !== 0; } static extractAfterComments(token) { let collected = null; if (token.positionedComments && token.positionedComments.length > 0) { // Split out 'after' comments so they can be reassigned to the following token. const retained = []; for (const posComment of token.positionedComments) { if (posComment.position === 'after' && posComment.comments && posComment.comments.length > 0) { if (!collected) { collected = []; } collected.push(...posComment.comments); continue; } retained.push(posComment); } token.positionedComments = retained.length > 0 ? retained : undefined; } if (token.comments && token.comments.length > 0) { // Fall back to legacy comment storage when positioned comments are absent. if (!collected) { collected = []; } collected.push(...token.comments); token.comments = null; } return collected; } static attachTrailingCommentsToNextLexeme(lexemes, index, comments) { if (!comments || comments.length === 0) { return; } const target = lexemes[index]; if (!target) { return; } if (!target.positionedComments) { // Initialise positioned comments to allow inserting 'before' entries. target.positionedComments = []; } const beforeEntry = target.positionedComments.find(pos => pos.position === 'before'); if (beforeEntry) { // Append to existing leading comments on the target lexeme. beforeEntry.comments.push(...comments); } else { // Seed a new leading comment entry on the next lexeme. target.positionedComments.push({ position: 'before', comments: [...comments], }); } } } exports.ExplainStatementParser = ExplainStatementParser; //# sourceMappingURL=ExplainStatementParser.js.map