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
JavaScript
;
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