UNPKG

rawsql-ts

Version:

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

738 lines 31.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.CreateTableParser = void 0; const SqlTokenizer_1 = require("./SqlTokenizer"); const SelectQueryParser_1 = require("./SelectQueryParser"); const CreateTableQuery_1 = require("../models/CreateTableQuery"); const Lexeme_1 = require("../models/Lexeme"); const FullNameParser_1 = require("./FullNameParser"); const FunctionExpressionParser_1 = require("./FunctionExpressionParser"); const ValueParser_1 = require("./ValueParser"); const ValueComponent_1 = require("../models/ValueComponent"); const ParserStringUtils_1 = require("../utils/ParserStringUtils"); /** * Parses CREATE TABLE statements (DDL or AS SELECT) into CreateTableQuery models. */ class CreateTableParser { /** * Parse SQL string to CreateTableQuery AST. */ static parse(query) { const tokenizer = new SqlTokenizer_1.SqlTokenizer(query); const lexemes = tokenizer.readLexemes(); const result = this.parseFromLexeme(lexemes, 0); if (result.newIndex < lexemes.length) { throw new Error(`Syntax error: Unexpected token "${lexemes[result.newIndex].value}" at position ${result.newIndex}. The CREATE TABLE statement is complete but there are additional tokens.`); } return result.value; } /** * Parse from lexeme array (for internal use and tests). */ static parseFromLexeme(lexemes, index) { var _a; let idx = index; // Guard against unexpected end of input before parsing begins. if (idx >= lexemes.length) { throw new Error(`[CreateTableParser] Unexpected end of input at position ${idx}.`); } const commandLexeme = lexemes[idx]; // Capture comments that precede the CREATE TABLE keyword so they can be re-applied later. const leadingCreateComments = this.popLexemeComments(commandLexeme, 'before'); const commandToken = commandLexeme.value.toLowerCase(); const isTemporary = commandToken === "create temporary table"; if (commandToken !== "create table" && !isTemporary) { throw new Error(`[CreateTableParser] Syntax error at position ${idx}: expected 'CREATE TABLE' but found '${lexemes[idx].value}'.`); } idx++; // Handle optional IF NOT EXISTS clause. const tokenAt = (offset) => { var _a; return (_a = lexemes[idx + offset]) === null || _a === void 0 ? void 0 : _a.value.toLowerCase(); }; let ifNotExists = false; if (tokenAt(0) === "if not exists") { idx++; ifNotExists = true; } // Parse qualified table name. const tableNameResult = this.parseQualifiedName(lexemes, idx); idx = tableNameResult.newIndex; const tableName = tableNameResult.name; const tableNamespaces = tableNameResult.namespaces; // Place captured comments from the identifier on the CreateTableQuery after instantiation. const positionedComments = tableName.positionedComments ? [...tableName.positionedComments] : null; const legacyComments = tableName.comments ? [...tableName.comments] : null; let columns = []; let tableConstraints = []; let tableOptions = null; let asSelectQuery; let withDataOption = null; // Parse DDL column definitions when present. if (((_a = lexemes[idx]) === null || _a === void 0 ? void 0 : _a.type) === Lexeme_1.TokenType.OpenParen) { ({ columns, tableConstraints, newIndex: idx } = this.parseDefinitionList(lexemes, idx)); } // Capture trailing table options that appear before an AS SELECT clause. if (idx < lexemes.length) { const nextValue = tokenAt(0); if (!this.isSelectKeyword(nextValue, lexemes[idx + 1])) { const optionsEnd = this.findClauseBoundary(lexemes, idx); if (optionsEnd > idx) { tableOptions = new ValueComponent_1.RawString((0, ParserStringUtils_1.joinLexemeValues)(lexemes, idx, optionsEnd)); idx = optionsEnd; } } } // Parse optional AS SELECT / SELECT clause. const nextToken = tokenAt(0); if (nextToken === "as") { idx++; const selectResult = SelectQueryParser_1.SelectQueryParser.parseFromLexeme(lexemes, idx); asSelectQuery = selectResult.value; idx = selectResult.newIndex; } else if (nextToken === "select" || nextToken === "with" || nextToken === "values") { const selectResult = SelectQueryParser_1.SelectQueryParser.parseFromLexeme(lexemes, idx); asSelectQuery = selectResult.value; idx = selectResult.newIndex; } if (asSelectQuery) { // Allow optional PostgreSQL-style WITH [NO] DATA clause after AS SELECT bodies. const withResult = this.parseWithDataOption(lexemes, idx); if (withResult) { withDataOption = withResult.value; idx = withResult.newIndex; } } const query = new CreateTableQuery_1.CreateTableQuery({ tableName: tableName.name, namespaces: tableNamespaces, isTemporary, ifNotExists, columns, tableConstraints, tableOptions, asSelectQuery, withDataOption }); // Re-attach positioned comments captured on the identifier. if (positionedComments) { query.tableName.positionedComments = positionedComments.map(pc => ({ position: pc.position, comments: [...pc.comments] })); } if (legacyComments) { query.tableName.comments = [...legacyComments]; } if (leadingCreateComments.length > 0) { // Keep top-level comments aligned with the CREATE TABLE statement. query.addPositionedComments('before', leadingCreateComments); } return { value: query, newIndex: idx }; } static parseQualifiedName(lexemes, index) { const { namespaces, name, newIndex } = FullNameParser_1.FullNameParser.parseFromLexeme(lexemes, index); return { namespaces: namespaces ? [...namespaces] : null, name, newIndex }; } static parseDefinitionList(lexemes, index) { var _a, _b; let idx = index; const columns = []; const constraints = []; const openParenLexeme = lexemes[idx]; // Convert comments placed immediately after '(' into leading comments for the first entry. let pendingLeading = this.toOptionalComments(this.popLexemeComments(openParenLexeme, 'after')); // Skip opening parenthesis. idx++; // Parse individual column or constraint entries until closing parenthesis. while (idx < lexemes.length) { const lexeme = lexemes[idx]; if (lexeme.type === Lexeme_1.TokenType.CloseParen) { // When comments sit right before ')', keep them with the preceding entry. const closingLeading = this.popLexemeComments(lexeme, 'before'); if (closingLeading.length > 0) { const target = (_a = (constraints.length > 0 ? constraints[constraints.length - 1] : columns[columns.length - 1])) !== null && _a !== void 0 ? _a : null; if (target) { target.addPositionedComments('after', closingLeading); } } idx++; break; } const tokenValue = lexeme.value.toLowerCase(); const isConstraint = this.TABLE_CONSTRAINT_STARTERS.has(tokenValue); const entryResult = isConstraint ? this.parseTableConstraint(lexemes, idx) : this.parseColumnDefinition(lexemes, idx); let entry = entryResult.value; if (pendingLeading && pendingLeading.length > 0) { // Reattach comments that belonged between comma/parenthesis and the entry itself. entry.addPositionedComments('before', pendingLeading); pendingLeading = null; } const trailingComments = this.popLexemeComments(lexemes[Math.max(entryResult.newIndex - 1, idx)], 'after'); if (trailingComments.length > 0) { // Preserve inline comments that appeared after the entry tokens. entry.addPositionedComments('after', trailingComments); } if (isConstraint) { constraints.push(entry); } else { columns.push(entry); } idx = entryResult.newIndex; // Consume delimiter comma between definitions. if (idx < lexemes.length && (lexemes[idx].type & Lexeme_1.TokenType.Comma)) { const commaLexeme = lexemes[idx]; const commaTrailing = this.popLexemeComments(commaLexeme, 'after'); pendingLeading = this.toOptionalComments(commaTrailing); idx++; continue; } // Break when encountering the closing parenthesis. if (idx < lexemes.length && lexemes[idx].type === Lexeme_1.TokenType.CloseParen) { const closingLexeme = lexemes[idx]; const closingLeading = this.popLexemeComments(closingLexeme, 'before'); if (closingLeading.length > 0) { const target = (_b = (constraints.length > 0 ? constraints[constraints.length - 1] : columns[columns.length - 1])) !== null && _b !== void 0 ? _b : null; if (target) { target.addPositionedComments('after', closingLeading); } } idx++; break; } pendingLeading = null; } return { columns, tableConstraints: constraints, newIndex: idx }; } static parseColumnDefinition(lexemes, index) { let idx = index; // Parse the column name as a qualified identifier. const columnNameResult = this.parseQualifiedName(lexemes, idx); idx = columnNameResult.newIndex; if (columnNameResult.namespaces && columnNameResult.namespaces.length > 0) { const qualified = [...columnNameResult.namespaces, columnNameResult.name.name].join("."); throw new Error(`[CreateTableParser] Column name '${qualified}' must not include a schema or namespace qualifier.`); } const columnName = columnNameResult.name; // Parse optional data type immediately following the column name. let dataType; if (idx < lexemes.length && !this.isColumnConstraintStart(lexemes[idx]) && !this.isColumnTerminator(lexemes[idx])) { const typeResult = this.parseColumnType(lexemes, idx); dataType = typeResult.value; idx = typeResult.newIndex; } // Collect column constraints until termination. const constraints = []; while (idx < lexemes.length && !this.isColumnTerminator(lexemes[idx])) { const constraintResult = this.parseColumnConstraint(lexemes, idx); constraints.push(constraintResult.value); idx = constraintResult.newIndex; } const columnDef = new CreateTableQuery_1.TableColumnDefinition({ name: columnName, dataType, constraints }); return { value: columnDef, newIndex: idx }; } static parseColumnType(lexemes, index) { try { const result = FunctionExpressionParser_1.FunctionExpressionParser.parseTypeValue(lexemes, index); return { value: result.value, newIndex: result.newIndex }; } catch { const typeEnd = this.findFirstConstraintIndex(lexemes, index); const rawText = (0, ParserStringUtils_1.joinLexemeValues)(lexemes, index, typeEnd); return { value: new ValueComponent_1.RawString(rawText), newIndex: typeEnd }; } } static parseColumnConstraint(lexemes, index) { var _a; let idx = index; let constraintName; // Handle optional CONSTRAINT <name> prefix. if (((_a = lexemes[idx]) === null || _a === void 0 ? void 0 : _a.value.toLowerCase()) === "constraint") { idx++; const nameResult = this.parseQualifiedName(lexemes, idx); constraintName = nameResult.name; idx = nameResult.newIndex; } const token = lexemes[idx]; if (!token) { throw new Error(`[CreateTableParser] Expected constraint definition at index ${idx}.`); } const value = token.value.toLowerCase(); // Parse NOT NULL / NULL constraints. if (value === "not null" || value === "null") { idx++; return { value: new CreateTableQuery_1.ColumnConstraintDefinition({ kind: value === "not null" ? "not-null" : "null", constraintName }), newIndex: idx }; } // Parse DEFAULT constraint with arbitrary expressions. if (value === "default") { idx++; const exprResult = ValueParser_1.ValueParser.parseFromLexeme(lexemes, idx); idx = exprResult.newIndex; return { value: new CreateTableQuery_1.ColumnConstraintDefinition({ kind: "default", constraintName, defaultValue: exprResult.value }), newIndex: idx }; } // Parse PRIMARY KEY constraint. if (value === "primary key") { idx++; return { value: new CreateTableQuery_1.ColumnConstraintDefinition({ kind: "primary-key", constraintName }), newIndex: idx }; } // Parse UNIQUE / UNIQUE KEY constraint. if (value === "unique" || value === "unique key") { idx++; return { value: new CreateTableQuery_1.ColumnConstraintDefinition({ kind: "unique", constraintName }), newIndex: idx }; } // Parse REFERENCES clause. if (value === "references") { const referenceResult = this.parseReferenceDefinition(lexemes, idx); idx = referenceResult.newIndex; return { value: new CreateTableQuery_1.ColumnConstraintDefinition({ kind: "references", constraintName, reference: referenceResult.value }), newIndex: idx }; } // Parse CHECK constraint. if (value === "check") { idx++; const checkExpression = this.parseParenExpression(lexemes, idx); idx = checkExpression.newIndex; return { value: new CreateTableQuery_1.ColumnConstraintDefinition({ kind: "check", constraintName, checkExpression: checkExpression.value }), newIndex: idx }; } // Parse identity-style generated clauses. if (value.startsWith("generated")) { const clauseEnd = this.findFirstConstraintIndex(lexemes, idx + 1); const text = (0, ParserStringUtils_1.joinLexemeValues)(lexemes, idx, clauseEnd); idx = clauseEnd; const kind = value.startsWith("generated always") ? "generated-always-identity" : "generated-by-default-identity"; return { value: new CreateTableQuery_1.ColumnConstraintDefinition({ kind, constraintName, rawClause: new ValueComponent_1.RawString(text) }), newIndex: idx }; } // Fallback to raw clause capture for unsupported constraints. const rawEnd = this.findFirstConstraintIndex(lexemes, idx + 1); const rawText = (0, ParserStringUtils_1.joinLexemeValues)(lexemes, idx, rawEnd); return { value: new CreateTableQuery_1.ColumnConstraintDefinition({ kind: "raw", constraintName, rawClause: new ValueComponent_1.RawString(rawText) }), newIndex: rawEnd }; } static parseTableConstraint(lexemes, index) { var _a; let idx = index; let constraintName; // Capture optional CONSTRAINT <name> prefix. if (((_a = lexemes[idx]) === null || _a === void 0 ? void 0 : _a.value.toLowerCase()) === "constraint") { idx++; const nameResult = this.parseQualifiedName(lexemes, idx); constraintName = nameResult.name; idx = nameResult.newIndex; } const token = lexemes[idx]; if (!token) { throw new Error(`[CreateTableParser] Expected table constraint at index ${idx}.`); } const value = token.value.toLowerCase(); if (value === "primary key") { idx++; const { identifiers, newIndex } = this.parseIdentifierList(lexemes, idx); idx = newIndex; return { value: new CreateTableQuery_1.TableConstraintDefinition({ kind: "primary-key", constraintName, columns: identifiers }), newIndex: idx }; } if (value === "unique" || value === "unique key") { idx++; let inlineKeyName; if (idx < lexemes.length && lexemes[idx].value !== "(" && !(lexemes[idx].type & Lexeme_1.TokenType.Command)) { const inlineNameResult = this.parseQualifiedName(lexemes, idx); inlineKeyName = inlineNameResult.name; idx = inlineNameResult.newIndex; } const { identifiers, newIndex } = this.parseIdentifierList(lexemes, idx); idx = newIndex; return { value: new CreateTableQuery_1.TableConstraintDefinition({ kind: "unique", constraintName: constraintName !== null && constraintName !== void 0 ? constraintName : inlineKeyName, columns: identifiers }), newIndex: idx }; } if (value === "foreign key") { idx++; let inlineKeyName; if (idx < lexemes.length && lexemes[idx].value !== "(" && !(lexemes[idx].type & Lexeme_1.TokenType.Command)) { const inlineNameResult = this.parseQualifiedName(lexemes, idx); inlineKeyName = inlineNameResult.name; idx = inlineNameResult.newIndex; } const { identifiers, newIndex } = this.parseIdentifierList(lexemes, idx); idx = newIndex; const referenceResult = this.parseReferenceDefinition(lexemes, idx); idx = referenceResult.newIndex; return { value: new CreateTableQuery_1.TableConstraintDefinition({ kind: "foreign-key", constraintName: constraintName !== null && constraintName !== void 0 ? constraintName : inlineKeyName, columns: identifiers, reference: referenceResult.value, deferrable: referenceResult.value.deferrable, initially: referenceResult.value.initially }), newIndex: idx }; } if (value === "check") { idx++; const checkExpression = this.parseParenExpression(lexemes, idx); idx = checkExpression.newIndex; return { value: new CreateTableQuery_1.TableConstraintDefinition({ kind: "check", constraintName, checkExpression: checkExpression.value }), newIndex: idx }; } // Fallback to capturing the raw text when the constraint is not recognized. const rawEnd = this.findFirstConstraintIndex(lexemes, idx + 1); const rawText = (0, ParserStringUtils_1.joinLexemeValues)(lexemes, idx, rawEnd); return { value: new CreateTableQuery_1.TableConstraintDefinition({ kind: "raw", constraintName, rawClause: new ValueComponent_1.RawString(rawText) }), newIndex: rawEnd }; } static parseIdentifierList(lexemes, index) { var _a, _b, _c; let idx = index; const identifiers = []; if (((_a = lexemes[idx]) === null || _a === void 0 ? void 0 : _a.type) !== Lexeme_1.TokenType.OpenParen) { throw new Error(`[CreateTableParser] Expected '(' to start identifier list at index ${idx}.`); } idx++; while (idx < lexemes.length) { const nameResult = this.parseQualifiedName(lexemes, idx); identifiers.push(nameResult.name); idx = nameResult.newIndex; if (((_b = lexemes[idx]) === null || _b === void 0 ? void 0 : _b.type) === Lexeme_1.TokenType.Comma) { idx++; continue; } if (((_c = lexemes[idx]) === null || _c === void 0 ? void 0 : _c.type) === Lexeme_1.TokenType.CloseParen) { idx++; break; } } return { identifiers, newIndex: idx }; } static parseReferenceDefinition(lexemes, index) { var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k; let idx = index; if (((_a = lexemes[idx]) === null || _a === void 0 ? void 0 : _a.value.toLowerCase()) !== "references") { throw new Error(`[CreateTableParser] Expected REFERENCES clause at index ${idx}.`); } idx++; const tableNameResult = this.parseQualifiedName(lexemes, idx); idx = tableNameResult.newIndex; const targetTable = new ValueComponent_1.QualifiedName(tableNameResult.namespaces, tableNameResult.name); // Parse optional column list in the REFERENCES clause. let columns = null; if (((_b = lexemes[idx]) === null || _b === void 0 ? void 0 : _b.type) === Lexeme_1.TokenType.OpenParen) { const listResult = this.parseIdentifierList(lexemes, idx); columns = listResult.identifiers; idx = listResult.newIndex; } let matchType = null; let onDelete = null; let onUpdate = null; let deferrable = null; let initially = null; // Parse optional trailing reference options. while (idx < lexemes.length) { const current = lexemes[idx].value.toLowerCase(); if (this.MATCH_KEYWORDS.has(current)) { matchType = this.MATCH_KEYWORDS.get(current); idx++; continue; } if (current === "match") { idx++; const descriptor = (_d = (_c = lexemes[idx]) === null || _c === void 0 ? void 0 : _c.value.toLowerCase()) !== null && _d !== void 0 ? _d : ""; matchType = descriptor; idx++; continue; } if (current === "on delete") { idx++; const action = (_f = (_e = lexemes[idx]) === null || _e === void 0 ? void 0 : _e.value.toLowerCase()) !== null && _f !== void 0 ? _f : ""; onDelete = (_g = this.REFERENTIAL_ACTIONS.get(action)) !== null && _g !== void 0 ? _g : null; idx++; continue; } if (current === "on update") { idx++; const action = (_j = (_h = lexemes[idx]) === null || _h === void 0 ? void 0 : _h.value.toLowerCase()) !== null && _j !== void 0 ? _j : ""; onUpdate = (_k = this.REFERENTIAL_ACTIONS.get(action)) !== null && _k !== void 0 ? _k : null; idx++; continue; } if (this.DEFERRABILITY_KEYWORDS.has(current)) { deferrable = this.DEFERRABILITY_KEYWORDS.get(current); idx++; continue; } if (this.INITIALLY_KEYWORDS.has(current)) { initially = this.INITIALLY_KEYWORDS.get(current); idx++; continue; } break; } return { value: new CreateTableQuery_1.ReferenceDefinition({ targetTable, columns, matchType, onDelete, onUpdate, deferrable, initially }), newIndex: idx }; } static parseParenExpression(lexemes, index) { var _a, _b; let idx = index; if (((_a = lexemes[idx]) === null || _a === void 0 ? void 0 : _a.type) !== Lexeme_1.TokenType.OpenParen) { throw new Error(`[CreateTableParser] Expected '(' introducing expression at index ${idx}.`); } idx++; const expressionResult = ValueParser_1.ValueParser.parseFromLexeme(lexemes, idx); idx = expressionResult.newIndex; if (((_b = lexemes[idx]) === null || _b === void 0 ? void 0 : _b.type) !== Lexeme_1.TokenType.CloseParen) { throw new Error(`[CreateTableParser] Expected ')' terminating expression at index ${idx}.`); } idx++; return { value: expressionResult.value, newIndex: idx }; } static isColumnConstraintStart(lexeme) { if (!lexeme) { return false; } const lower = lexeme.value.toLowerCase(); return this.COLUMN_CONSTRAINT_STARTERS.has(lower); } static isColumnTerminator(lexeme) { if (!lexeme) { return true; } if (lexeme.type & (Lexeme_1.TokenType.Comma | Lexeme_1.TokenType.CloseParen)) { return true; } return false; } static isSelectKeyword(value, nextLexeme) { if (!value) { return false; } if (value === 'with' && (nextLexeme === null || nextLexeme === void 0 ? void 0 : nextLexeme.type) === Lexeme_1.TokenType.OpenParen) { return false; } return value === "as" || value === "select" || value === "with" || value === "values"; } static findClauseBoundary(lexemes, index) { let idx = index; while (idx < lexemes.length) { const lower = lexemes[idx].value.toLowerCase(); if (this.isSelectKeyword(lower, lexemes[idx + 1])) { break; } idx++; } return idx; } static findFirstConstraintIndex(lexemes, index) { let idx = index; while (idx < lexemes.length && !this.isColumnConstraintStart(lexemes[idx]) && !this.isColumnTerminator(lexemes[idx])) { idx++; } return idx; } static parseWithDataOption(lexemes, index) { // Detect PostgreSQL-style WITH [NO] DATA phrases that follow CREATE TABLE ... AS SELECT. const current = lexemes[index]; if (!current) { return null; } const value = current.value.toLowerCase(); if (value === "with data") { return { value: "with-data", newIndex: index + 1 }; } if (value === "with no data") { return { value: "with-no-data", newIndex: index + 1 }; } if (value !== "with") { return null; } const next = lexemes[index + 1]; const nextValue = next === null || next === void 0 ? void 0 : next.value.toLowerCase(); if (nextValue === "data") { return { value: "with-data", newIndex: index + 2 }; } if (nextValue === "data") { return { value: "with-data", newIndex: index + 2 }; } if (nextValue === "no data") { return { value: "with-no-data", newIndex: index + 2 }; } if (nextValue === "no") { const following = lexemes[index + 2]; if (following && following.value.toLowerCase() === "data") { return { value: "with-no-data", newIndex: index + 3 }; } } return null; } static popLexemeComments(lexeme, position) { if (!lexeme) { return []; } let collected = []; // Extract positioned comments first so they are not reused downstream. if (lexeme.positionedComments && lexeme.positionedComments.length > 0) { const matchIndex = lexeme.positionedComments.findIndex(pc => pc.position === position); if (matchIndex >= 0) { collected = [...lexeme.positionedComments[matchIndex].comments]; const remaining = lexeme.positionedComments.filter((_, idx) => idx !== matchIndex); lexeme.positionedComments = remaining.length > 0 ? remaining : undefined; } } if (collected.length > 0) { return collected; } if (lexeme.comments && lexeme.comments.length > 0) { const legacy = [...lexeme.comments]; lexeme.comments = null; return legacy; } return []; } static toOptionalComments(comments) { return comments.length > 0 ? comments : null; } } exports.CreateTableParser = CreateTableParser; CreateTableParser.TABLE_CONSTRAINT_STARTERS = new Set([ "constraint", "primary key", "unique", "unique key", "foreign key", "check" ]); CreateTableParser.COLUMN_CONSTRAINT_STARTERS = new Set([ "constraint", "not null", "null", "default", "primary key", "unique", "unique key", "references", "check", "generated always", "generated always as identity", "generated by default", "generated by default as identity" ]); CreateTableParser.MATCH_KEYWORDS = new Map([ ["match full", "full"], ["match partial", "partial"], ["match simple", "simple"] ]); CreateTableParser.REFERENTIAL_ACTIONS = new Map([ ["cascade", "cascade"], ["restrict", "restrict"], ["no action", "no action"], ["set null", "set null"], ["set default", "set default"] ]); CreateTableParser.DEFERRABILITY_KEYWORDS = new Map([ ["deferrable", "deferrable"], ["not deferrable", "not deferrable"] ]); CreateTableParser.INITIALLY_KEYWORDS = new Map([ ["initially immediate", "immediate"], ["initially deferred", "deferred"] ]); //# sourceMappingURL=CreateTableParser.js.map