rawsql-ts
Version:
[beta]High-performance SQL parser and AST analyzer written in TypeScript. Provides fast parsing and advanced transformation capabilities.
165 lines • 7.15 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.WithClauseParser = void 0;
const Clause_1 = require("../models/Clause");
const Lexeme_1 = require("../models/Lexeme");
const SqlTokenizer_1 = require("./SqlTokenizer");
const CommonTableParser_1 = require("./CommonTableParser");
/**
* Parser for SQL WITH clauses (Common Table Expressions - CTEs).
* Parses only the WITH clause portion of SQL, not the entire query.
*
* **Note**: For most use cases, use `SelectQueryParser` which provides more comprehensive SQL parsing.
* This parser should only be used for the special case where you need to analyze only the WITH clause portion.
*
* @example
* ```typescript
* // Parses only the WITH clause, not the following SELECT
* const sql = "WITH recursive_cte AS (SELECT 1 as n UNION SELECT n+1 FROM recursive_cte WHERE n < 10)";
* const withClause = WithClauseParser.parse(sql);
* console.log(withClause.recursive); // true
* console.log(withClause.tables.length); // 1
* ```
*/
class WithClauseParser {
/**
* Parses a SQL string containing only a WITH clause into a WithClause AST.
* The input should contain only the WITH clause, not the subsequent main query.
*
* @param query - The SQL string containing only the WITH clause
* @returns The parsed WithClause object
* @throws Error if the syntax is invalid or there are unexpected tokens after the WITH clause
*
* @example
* ```typescript
* // Correct: Only the WITH clause
* const sql = "WITH users_data AS (SELECT id, name FROM users)";
* const withClause = WithClauseParser.parse(sql);
*
* // Error: Contains SELECT after WITH clause
* // const badSql = "WITH users_data AS (SELECT id, name FROM users) SELECT * FROM users_data";
* ```
*/
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 WITH clause is complete but there are additional tokens.`);
}
return result.value;
}
/**
* Parses a WITH clause from an array of lexemes starting at the specified index.
*
* @param lexemes - Array of lexemes to parse from
* @param index - Starting index in the lexemes array
* @returns Object containing the parsed WithClause and the new index position
* @throws Error if the syntax is invalid or WITH keyword is not found
*
* @example
* ```typescript
* const tokenizer = new SqlTokenizer("WITH cte AS (SELECT 1)");
* const lexemes = tokenizer.readLexmes();
* const result = WithClauseParser.parseFromLexeme(lexemes, 0);
* console.log(result.value.tables.length); // 1
* console.log(result.newIndex); // position after the WITH clause
* ```
*/
static parseFromLexeme(lexemes, index) {
let idx = index;
// Extract header comments from WITH token
const headerComments = this.extractWithTokenHeaderComments(lexemes, idx);
// Parse WITH keyword
idx = this.parseWithKeyword(lexemes, idx);
// Parse optional RECURSIVE keyword
const { recursive, newIndex: recursiveIndex } = this.parseRecursiveFlag(lexemes, idx);
idx = recursiveIndex;
// Parse all CTEs
const { tables, trailingComments, newIndex: ctesIndex } = this.parseAllCommonTables(lexemes, idx);
idx = ctesIndex;
// Create WITH clause and apply trailing comments directly
const withClause = new Clause_1.WithClause(recursive, tables);
if (trailingComments.length > 0) {
withClause.trailingComments = trailingComments;
}
return {
value: withClause,
newIndex: idx,
headerComments: headerComments
};
}
// Extract header comments from WITH token
static extractWithTokenHeaderComments(lexemes, index) {
if (index >= lexemes.length)
return null;
const withToken = lexemes[index];
let headerComments = null;
// Extract positioned comments: only "before" comments are header comments
if (withToken.positionedComments && withToken.positionedComments.length > 0) {
for (const posComment of withToken.positionedComments) {
if (posComment.position === 'before' && posComment.comments) {
if (!headerComments)
headerComments = [];
headerComments.push(...posComment.comments);
}
}
}
// Fallback to legacy comments if no positioned comments (for backward compatibility)
if (!headerComments && withToken.comments && withToken.comments.length > 0) {
headerComments = [...withToken.comments];
}
return headerComments;
}
// Parse WITH keyword
static parseWithKeyword(lexemes, index) {
if (index < lexemes.length && lexemes[index].value.toLowerCase() === "with") {
return index + 1;
}
else {
throw new Error(`Syntax error at position ${index}: Expected WITH keyword.`);
}
}
// Parse optional RECURSIVE keyword
static parseRecursiveFlag(lexemes, index) {
const recursive = index < lexemes.length && lexemes[index].value.toLowerCase() === "recursive";
return {
recursive,
newIndex: recursive ? index + 1 : index
};
}
// Parse all common table expressions
static parseAllCommonTables(lexemes, index) {
let idx = index;
const tables = [];
const allTrailingComments = [];
// Parse first CTE (required)
const firstCte = CommonTableParser_1.CommonTableParser.parseFromLexeme(lexemes, idx);
tables.push(firstCte.value);
idx = firstCte.newIndex;
// Collect trailing comments from first CTE directly
if (firstCte.trailingComments) {
allTrailingComments.push(...firstCte.trailingComments);
}
// Parse additional CTEs (optional)
while (idx < lexemes.length && (lexemes[idx].type & Lexeme_1.TokenType.Comma)) {
idx++; // Skip comma
const cteResult = CommonTableParser_1.CommonTableParser.parseFromLexeme(lexemes, idx);
tables.push(cteResult.value);
idx = cteResult.newIndex;
// Collect trailing comments from this CTE directly
if (cteResult.trailingComments) {
allTrailingComments.push(...cteResult.trailingComments);
}
}
return {
tables,
trailingComments: allTrailingComments,
newIndex: idx
};
}
}
exports.WithClauseParser = WithClauseParser;
//# sourceMappingURL=WithClauseParser.js.map