UNPKG

rawsql-ts

Version:

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

244 lines 11.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.SelectItemParser = exports.SelectClauseParser = void 0; const Clause_1 = require("../models/Clause"); const Lexeme_1 = require("../models/Lexeme"); const ValueComponent_1 = require("../models/ValueComponent"); const SqlTokenizer_1 = require("./SqlTokenizer"); const ValueParser_1 = require("./ValueParser"); const HintClause_1 = require("../models/HintClause"); class SelectClauseParser { // Parse SQL string to AST (was: parse) static parse(query) { const tokenizer = new SqlTokenizer_1.SqlTokenizer(query); // Initialize tokenizer const lexemes = tokenizer.readLexmes(); // Get tokens // Parse const result = this.parseFromLexeme(lexemes, 0); // Error if there are remaining tokens if (result.newIndex < lexemes.length) { throw new Error(`Syntax error: Unexpected token "${lexemes[result.newIndex].value}" at position ${result.newIndex}. The SELECT clause is complete but there are additional tokens.`); } return result.value; } // Parse from lexeme array (was: parse) static parseFromLexeme(lexemes, index) { let idx = index; let distinct = null; // Capture comments from the SELECT token const selectTokenComments = idx < lexemes.length ? lexemes[idx].comments : null; if (lexemes[idx].value !== 'select') { throw new Error(`Syntax error at position ${idx}: Expected 'SELECT' keyword but found "${lexemes[idx].value}". SELECT clauses must start with the SELECT keyword.`); } idx++; // Parse hint clauses (/*+ hint */) after SELECT const hints = []; while (idx < lexemes.length && HintClause_1.HintClause.isHintClause(lexemes[idx].value)) { const hintContent = HintClause_1.HintClause.extractHintContent(lexemes[idx].value); hints.push(new HintClause_1.HintClause(hintContent)); idx++; } if (idx < lexemes.length && lexemes[idx].value === 'distinct') { idx++; distinct = new Clause_1.Distinct(); } else if (idx < lexemes.length && lexemes[idx].value === 'distinct on') { idx++; const argument = ValueParser_1.ValueParser.parseArgument(Lexeme_1.TokenType.OpenParen, Lexeme_1.TokenType.CloseParen, lexemes, idx); distinct = new Clause_1.DistinctOn(argument.value); idx = argument.newIndex; } const items = []; const item = SelectItemParser.parseItem(lexemes, idx); items.push(item.value); idx = item.newIndex; while (idx < lexemes.length && (lexemes[idx].type & Lexeme_1.TokenType.Comma)) { idx++; const item = SelectItemParser.parseItem(lexemes, idx); items.push(item.value); idx = item.newIndex; } if (items.length === 0) { throw new Error(`Syntax error at position ${index}: No select items found. The SELECT clause requires at least one expression to select.`); } else { const clause = new Clause_1.SelectClause(items, distinct, hints); return { value: clause, newIndex: idx }; } } } exports.SelectClauseParser = SelectClauseParser; // Extracted SelectItemParser for parsing individual select items class SelectItemParser { /** * Parses a single select item from a SQL string. * @param query The SQL string representing a select item (e.g. 'id as user_id'). * @returns The parsed SelectItem instance. */ static parse(query) { const tokenizer = new SqlTokenizer_1.SqlTokenizer(query); const lexemes = tokenizer.readLexmes(); const result = this.parseItem(lexemes, 0); if (result.newIndex < lexemes.length) { throw new Error(`Syntax error: Unexpected token "${lexemes[result.newIndex].value}" at position ${result.newIndex}. The select item is complete but there are additional tokens.`); } return result.value; } /** * Parses a single select item from lexemes. * @param lexemes The array of lexemes. * @param index The starting index. * @returns An object containing the SelectItem and the new index. */ static parseItem(lexemes, index) { let idx = index; // Extract positioned comments from the value token directly const valueTokenComments = this.extractValueTokenComments(lexemes, idx); const parsedValue = ValueParser_1.ValueParser.parseFromLexeme(lexemes, idx); const value = parsedValue.value; idx = parsedValue.newIndex; // Parse optional AS keyword and extract comments directly const { asComments, newIndex: asIndex } = this.parseAsKeyword(lexemes, idx); idx = asIndex; if (idx < lexemes.length && (lexemes[idx].type & Lexeme_1.TokenType.Identifier)) { const alias = lexemes[idx].value; const aliasComments = lexemes[idx].comments; // Capture comments from alias token const aliasPositionedComments = lexemes[idx].positionedComments; // Capture positioned comments from alias token idx++; const selectItem = new Clause_1.SelectItem(value, alias); // Apply all comments directly to selectItem (no collection then assignment) this.applyValueTokenComments(selectItem, valueTokenComments); this.applyAsKeywordComments(selectItem, asComments); this.applyAliasComments(selectItem, aliasComments, aliasPositionedComments); return { value: selectItem, newIndex: idx, }; } else if (value instanceof ValueComponent_1.ColumnReference && value.column.name !== "*") { // nameless select item const selectItem = new Clause_1.SelectItem(value, value.column.name); // Apply value token and AS keyword comments directly this.applyValueTokenComments(selectItem, valueTokenComments); this.applyAsKeywordComments(selectItem, asComments); return { value: selectItem, newIndex: idx, }; } // nameless select item const selectItem = new Clause_1.SelectItem(value); // Apply comments directly this.applyValueTokenComments(selectItem, valueTokenComments); this.applyAsKeywordComments(selectItem, asComments); return { value: selectItem, newIndex: idx, }; } /** * Recursively clear positioned comments from all nested components to prevent duplication */ static clearPositionedCommentsRecursively(component) { if (!component || typeof component !== 'object') { return; } // Clear positioned comments from this component if ('positionedComments' in component) { component.positionedComments = null; } // Recursively clear from common nested properties if (component.left) { this.clearPositionedCommentsRecursively(component.left); } if (component.right) { this.clearPositionedCommentsRecursively(component.right); } if (component.qualifiedName) { this.clearPositionedCommentsRecursively(component.qualifiedName); } if (component.table) { this.clearPositionedCommentsRecursively(component.table); } if (component.name) { this.clearPositionedCommentsRecursively(component.name); } if (component.args && Array.isArray(component.args)) { component.args.forEach((arg) => { this.clearPositionedCommentsRecursively(arg); }); } if (component.value) { this.clearPositionedCommentsRecursively(component.value); } } // Extract positioned comments from value token (no collection arrays) static extractValueTokenComments(lexemes, index) { if (index >= lexemes.length) { return { positioned: null, legacy: null }; } const token = lexemes[index]; return { positioned: token.positionedComments && token.positionedComments.length > 0 ? token.positionedComments : null, legacy: null // Value token legacy comments are not typically used }; } // Parse AS keyword and extract its comments directly static parseAsKeyword(lexemes, index) { if (index >= lexemes.length || lexemes[index].value !== 'as') { return { asComments: { positioned: null, legacy: null }, newIndex: index }; } const asToken = lexemes[index]; const asComments = { positioned: asToken.positionedComments && asToken.positionedComments.length > 0 ? asToken.positionedComments : null, legacy: asToken.comments && asToken.comments.length > 0 ? asToken.comments : null }; return { asComments, newIndex: index + 1 }; } // Apply value token comments directly to selectItem static applyValueTokenComments(selectItem, valueTokenComments) { if (valueTokenComments.positioned) { for (const posComment of valueTokenComments.positioned) { selectItem.addPositionedComments(posComment.position, posComment.comments); } this.clearValueTokenComments(selectItem); } } // Apply AS keyword comments directly to selectItem static applyAsKeywordComments(selectItem, asComments) { if (asComments.positioned) { selectItem.asKeywordPositionedComments = asComments.positioned; } else if (asComments.legacy) { selectItem.asKeywordComments = asComments.legacy; } } // Apply alias comments directly to selectItem static applyAliasComments(selectItem, aliasComments, aliasPositionedComments) { if (aliasPositionedComments && aliasPositionedComments.length > 0) { selectItem.aliasPositionedComments = aliasPositionedComments; } else if (aliasComments && aliasComments.length > 0) { selectItem.aliasComments = aliasComments; } } // Clear positioned comments from value to avoid duplication static clearValueTokenComments(selectItem) { // Clear both positioned and legacy comments from the value to avoid duplication if ('positionedComments' in selectItem.value) { selectItem.value.positionedComments = null; } // Also clear positioned comments from nested IdentifierString (in QualifiedName) if (selectItem.value instanceof ValueComponent_1.ColumnReference) { const columnRef = selectItem.value; if (columnRef.qualifiedName && columnRef.qualifiedName.name) { columnRef.qualifiedName.name.positionedComments = null; } } // Clear positioned comments from BinaryExpression children only to avoid duplication if (selectItem.value instanceof ValueComponent_1.BinaryExpression) { this.clearPositionedCommentsRecursively(selectItem.value); } } } exports.SelectItemParser = SelectItemParser; //# sourceMappingURL=SelectClauseParser.js.map