UNPKG

rawsql-ts

Version:

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

262 lines 12.2 kB
import { CommonTable } from "../models/Clause"; import { TokenType } from "../models/Lexeme"; import { SqlTokenizer } from "./SqlTokenizer"; import { SelectQueryParser } from "./SelectQueryParser"; import { SourceAliasExpressionParser } from "./SourceAliasExpressionParser"; import { InsertQueryParser } from "./InsertQueryParser"; import { UpdateQueryParser } from "./UpdateQueryParser"; import { DeleteQueryParser } from "./DeleteQueryParser"; import { MergeQueryParser } from "./MergeQueryParser"; import { InsertQuery } from "../models/InsertQuery"; import { UpdateQuery } from "../models/UpdateQuery"; import { DeleteQuery } from "../models/DeleteQuery"; import { MergeQuery } from "../models/MergeQuery"; import { SelectQueryWithClauseHelper } from "../utils/SelectQueryWithClauseHelper"; import { WithClauseParser } from "./WithClauseParser"; export class CommonTableParser { // Parse SQL string to AST (was: parse) static parse(query) { const tokenizer = new 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 CommonTable definition is complete but there are additional tokens.`); } return result.value; } // Parse from lexeme array (was: parse) static parseFromLexeme(lexemes, index) { let idx = index; // 1. Parse alias and optional column aliases const aliasResult = SourceAliasExpressionParser.parseFromLexeme(lexemes, idx); idx = aliasResult.newIndex; // 2. Collect preceding comments for this CTE this.collectPrecedingComments(lexemes, index, aliasResult); // 3. Parse AS keyword idx = this.parseAsKeyword(lexemes, idx); // 4. Parse optional MATERIALIZED flag const { materialized, newIndex: materializedIndex } = this.parseMaterializedFlag(lexemes, idx); idx = materializedIndex; // 5. Parse inner CTE query with parentheses const { query, trailingComments, newIndex: queryIndex } = this.parseInnerQuery(lexemes, idx); idx = queryIndex; // 6. Create CommonTable instance const value = new CommonTable(query, aliasResult.value, materialized); return { value, newIndex: idx, trailingComments }; } // Collect comments from preceding tokens that should belong to this CTE static collectPrecedingComments(lexemes, index, aliasResult) { var _a; if (!((_a = aliasResult.value) === null || _a === void 0 ? void 0 : _a.table)) return; const cteTable = aliasResult.value.table; for (let i = index - 1; i >= 0; i--) { const token = lexemes[i]; // Handle WITH keyword specially - collect its "after" comments for the first CTE if (token.value.toLowerCase() === 'with') { this.collectWithTokenComments(token, cteTable); break; // Stop at WITH keyword } // Stop looking if we hit a comma (indicates previous CTE) or RECURSIVE keyword if (token.type & TokenType.Comma || token.value.toLowerCase() === 'recursive') { break; } // Collect comments from tokens before the CTE name this.collectTokenComments(token, cteTable); } } // Collect comments from WITH token static collectWithTokenComments(token, cteTable) { let hasPositionedComments = false; if (token.positionedComments && token.positionedComments.length > 0) { for (const posComment of token.positionedComments) { if (posComment.position === 'after' && posComment.comments) { this.addPositionedComment(cteTable, 'before', posComment.comments); hasPositionedComments = true; } } } // Only use legacy comments if no positioned comments were found if (!hasPositionedComments && token.comments && token.comments.length > 0) { this.addPositionedComment(cteTable, 'before', token.comments); } } // Collect comments from a token static collectTokenComments(token, cteTable) { if (token.comments && token.comments.length > 0) { this.addPositionedComment(cteTable, 'before', token.comments); } if (token.positionedComments && token.positionedComments.length > 0) { if (!cteTable.positionedComments) { cteTable.positionedComments = []; } cteTable.positionedComments.unshift(...token.positionedComments); } } // Helper to add positioned comment static addPositionedComment(cteTable, position, comments) { if (!cteTable.positionedComments) { cteTable.positionedComments = []; } cteTable.positionedComments.unshift({ position, comments: [...comments] }); } // Parse AS keyword static parseAsKeyword(lexemes, index) { if (index < lexemes.length && lexemes[index].value !== "as") { throw new Error(`Syntax error at position ${index}: Expected 'AS' keyword after CTE name but found "${lexemes[index].value}".`); } return index + 1; // Skip 'AS' keyword } // Parse optional MATERIALIZED flag static parseMaterializedFlag(lexemes, index) { if (index >= lexemes.length) { return { materialized: null, newIndex: index }; } const currentValue = lexemes[index].value; if (currentValue === "materialized") { return { materialized: true, newIndex: index + 1 }; } else if (currentValue === "not materialized") { return { materialized: false, newIndex: index + 1 }; } return { materialized: null, newIndex: index }; } // Parse inner SELECT query with parentheses static parseInnerQuery(lexemes, index) { let idx = index; if (idx < lexemes.length && lexemes[idx].type !== TokenType.OpenParen) { throw new Error(`Syntax error at position ${idx}: Expected '(' after CTE name but found "${lexemes[idx].value}".`); } // Capture comments from the opening parenthesis for the CTE inner query const cteQueryHeaderComments = this.extractComments(lexemes[idx]); idx++; // Skip opening parenthesis const queryResult = this.parseCteQuery(lexemes, idx); idx = queryResult.newIndex; // Attach comments from the opening parenthesis to the start of the inner query. this.applyCteHeaderComments(queryResult.value, cteQueryHeaderComments); if (idx < lexemes.length && lexemes[idx].type !== TokenType.CloseParen) { throw new Error(`Syntax error at position ${idx}: Expected ')' after CTE query but found "${lexemes[idx].value}".`); } // Capture comments from the closing parenthesis const closingParenComments = this.extractComments(lexemes[idx]); idx++; // Skip closing parenthesis return { query: queryResult.value, trailingComments: closingParenComments.length > 0 ? closingParenComments : null, newIndex: idx }; } static parseCteQuery(lexemes, index) { if (index >= lexemes.length) { throw new Error(`Syntax error at position ${index}: Expected CTE query but found end of input.`); } const firstToken = lexemes[index].value.toLowerCase(); switch (firstToken) { case "select": case "values": return SelectQueryParser.parseFromLexeme(lexemes, index); case "insert into": return InsertQueryParser.parseFromLexeme(lexemes, index); case "update": return UpdateQueryParser.parseFromLexeme(lexemes, index); case "delete from": return DeleteQueryParser.parseFromLexeme(lexemes, index); case "merge into": return MergeQueryParser.parseFromLexeme(lexemes, index); case "with": { // Determine statement type after WITH to route to the correct parser. const commandAfterWith = this.getCommandAfterWith(lexemes, index); switch (commandAfterWith) { case "insert into": return InsertQueryParser.parseFromLexeme(lexemes, index); case "update": return UpdateQueryParser.parseFromLexeme(lexemes, index); case "delete from": return DeleteQueryParser.parseFromLexeme(lexemes, index); case "merge into": return MergeQueryParser.parseFromLexeme(lexemes, index); default: return SelectQueryParser.parseFromLexeme(lexemes, index); } } default: throw new Error(`Syntax error at position ${index}: Expected SELECT, INSERT, UPDATE, DELETE, MERGE, or WITH in CTE query but found "${lexemes[index].value}".`); } } static applyCteHeaderComments(query, headerComments) { var _a, _b, _c; if (headerComments.length === 0) { return; } if (this.isSelectQuery(query)) { if (query.headerComments) { query.headerComments = [...headerComments, ...query.headerComments]; } else { query.headerComments = headerComments; } return; } if (query instanceof InsertQuery) { const withClause = SelectQueryWithClauseHelper.getWithClause(query.selectQuery); const target = withClause !== null && withClause !== void 0 ? withClause : query.insertClause; target.addPositionedComments("before", headerComments); return; } if (query instanceof UpdateQuery) { const target = (_a = query.withClause) !== null && _a !== void 0 ? _a : query.updateClause; target.addPositionedComments("before", headerComments); return; } if (query instanceof DeleteQuery) { const target = (_b = query.withClause) !== null && _b !== void 0 ? _b : query.deleteClause; target.addPositionedComments("before", headerComments); return; } if (query instanceof MergeQuery) { const target = (_c = query.withClause) !== null && _c !== void 0 ? _c : query; target.addPositionedComments("before", headerComments); } } static isSelectQuery(query) { return "__selectQueryType" in query && query.__selectQueryType === "SelectQuery"; } static getCommandAfterWith(lexemes, index) { var _a; try { const withResult = WithClauseParser.parseFromLexeme(lexemes, index); const next = lexemes[withResult.newIndex]; return (_a = next === null || next === void 0 ? void 0 : next.value.toLowerCase()) !== null && _a !== void 0 ? _a : null; } catch (_b) { return null; } } // Extract comments from a lexeme (both positioned and legacy) static extractComments(lexeme) { const comments = []; // Check positioned comments if (lexeme.positionedComments) { for (const posComment of lexeme.positionedComments) { if (posComment.comments) { comments.push(...posComment.comments); } } } // Check legacy comments for backward compatibility if (lexeme.comments && lexeme.comments.length > 0) { comments.push(...lexeme.comments); } return comments; } } //# sourceMappingURL=CommonTableParser.js.map