UNPKG

rawsql-ts

Version:

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

931 lines 181 kB
import { PartitionByClause, OrderByClause, OrderByItem, SelectClause, SelectItem, Distinct, DistinctOn, SortDirection, NullsSortDirection, TableSource, SourceExpression, FromClause, JoinClause, JoinOnClause, JoinUsingClause, FunctionSource, SourceAliasExpression, WhereClause, GroupByClause, HavingClause, SubQuerySource, WindowFrameClause, LimitClause, ForClause, OffsetClause, WindowsClause as WindowClause, CommonTable, WithClause, FetchClause, FetchExpression, InsertClause, OnConflictClause, UpdateClause, DeleteClause, UsingClause, SetClause, ReturningClause, SetClauseItem } from "../models/Clause"; import { HintClause } from "../models/HintClause"; import { BinarySelectQuery, SimpleSelectQuery, ValuesQuery } from "../models/SelectQuery"; import { SqlPrintToken, SqlPrintTokenType, SqlPrintTokenContainerType } from "../models/SqlPrintToken"; import { SelectQueryWithClauseHelper } from "../utils/SelectQueryWithClauseHelper"; import { ValueList, ColumnReference, FunctionCall, UnaryExpression, BinaryExpression, LiteralValue, ParameterExpression, SwitchCaseArgument, CaseKeyValuePair, RawString, IdentifierString, ParenExpression, CastExpression, CaseExpression, ArrayExpression, ArrayQueryExpression, ArraySliceExpression, ArrayIndexExpression, BetweenExpression, JsonPredicateExpression, StringSpecifierExpression, TypeValue, TupleExpression, WindowFrameExpression, QualifiedName, InlineQuery, WindowFrameSpec, WindowFrameBoundStatic, WindowFrameBoundaryValue } from "../models/ValueComponent"; import { ParameterCollector } from "../transformers/ParameterCollector"; import { IdentifierDecorator } from "./IdentifierDecorator"; import { ParameterDecorator } from "./ParameterDecorator"; import { InsertQuery } from "../models/InsertQuery"; import { UpdateQuery } from "../models/UpdateQuery"; import { DeleteQuery } from "../models/DeleteQuery"; import { CreateTableQuery, TableColumnDefinition, ColumnConstraintDefinition, TableConstraintDefinition, ReferenceDefinition } from "../models/CreateTableQuery"; import { MergeQuery, MergeWhenClause, MergeUpdateAction, MergeDeleteAction, MergeInsertAction, MergeDoNothingAction } from "../models/MergeQuery"; import { DropTableStatement, DropIndexStatement, CreateIndexStatement, CreateSchemaStatement, DropSchemaStatement, CommentOnStatement, IndexColumnDefinition, AlterTableStatement, AlterTableAddConstraint, AlterTableDropConstraint, AlterTableAddColumn, AlterTableDropColumn, AlterTableAlterColumnDefault, DropConstraintStatement, ExplainStatement, AnalyzeStatement, CreateSequenceStatement, AlterSequenceStatement } from "../models/DDLStatements"; export var ParameterStyle; (function (ParameterStyle) { ParameterStyle["Anonymous"] = "anonymous"; ParameterStyle["Indexed"] = "indexed"; ParameterStyle["Named"] = "named"; ParameterStyle["Original"] = "original"; })(ParameterStyle || (ParameterStyle = {})); export const PRESETS = { mysql: { identifierEscape: { start: '`', end: '`' }, parameterSymbol: '?', parameterStyle: ParameterStyle.Anonymous, constraintStyle: 'mysql', }, postgres: { identifierEscape: { start: '"', end: '"' }, parameterSymbol: '$', parameterStyle: ParameterStyle.Indexed, castStyle: 'postgres', constraintStyle: 'postgres', }, postgresWithNamedParams: { identifierEscape: { start: '"', end: '"' }, parameterSymbol: ':', parameterStyle: ParameterStyle.Named, castStyle: 'postgres', constraintStyle: 'postgres', }, sqlserver: { identifierEscape: { start: '[', end: ']' }, parameterSymbol: '@', parameterStyle: ParameterStyle.Named, constraintStyle: 'postgres', }, sqlite: { identifierEscape: { start: '"', end: '"' }, parameterSymbol: ':', parameterStyle: ParameterStyle.Named, constraintStyle: 'postgres', }, oracle: { identifierEscape: { start: '"', end: '"' }, parameterSymbol: ':', parameterStyle: ParameterStyle.Named, constraintStyle: 'postgres', }, clickhouse: { identifierEscape: { start: '`', end: '`' }, parameterSymbol: '?', parameterStyle: ParameterStyle.Anonymous, constraintStyle: 'postgres', }, firebird: { identifierEscape: { start: '"', end: '"' }, parameterSymbol: '?', parameterStyle: ParameterStyle.Anonymous, }, db2: { identifierEscape: { start: '"', end: '"' }, parameterSymbol: '?', parameterStyle: ParameterStyle.Anonymous, }, snowflake: { identifierEscape: { start: '"', end: '"' }, parameterSymbol: '?', parameterStyle: ParameterStyle.Anonymous, }, cloudspanner: { identifierEscape: { start: '`', end: '`' }, parameterSymbol: '@', parameterStyle: ParameterStyle.Named, }, duckdb: { identifierEscape: { start: '"', end: '"' }, parameterSymbol: '?', parameterStyle: ParameterStyle.Anonymous, }, cockroachdb: { identifierEscape: { start: '"', end: '"' }, parameterSymbol: '$', parameterStyle: ParameterStyle.Indexed, castStyle: 'postgres', }, athena: { identifierEscape: { start: '"', end: '"' }, parameterSymbol: '?', parameterStyle: ParameterStyle.Anonymous, }, bigquery: { identifierEscape: { start: '`', end: '`' }, parameterSymbol: '@', parameterStyle: ParameterStyle.Named, }, hive: { identifierEscape: { start: '`', end: '`' }, parameterSymbol: '?', parameterStyle: ParameterStyle.Anonymous, }, mariadb: { identifierEscape: { start: '`', end: '`' }, parameterSymbol: '?', parameterStyle: ParameterStyle.Anonymous, }, redshift: { identifierEscape: { start: '"', end: '"' }, parameterSymbol: '$', parameterStyle: ParameterStyle.Indexed, castStyle: 'postgres', }, flinksql: { identifierEscape: { start: '`', end: '`' }, parameterSymbol: '?', parameterStyle: ParameterStyle.Anonymous, }, mongodb: { identifierEscape: { start: '"', end: '"' }, parameterSymbol: '?', parameterStyle: ParameterStyle.Anonymous, }, }; export class SqlPrintTokenParser { static getSelfHandlingComponentTypes() { if (!this._selfHandlingComponentTypes) { this._selfHandlingComponentTypes = new Set([ SimpleSelectQuery.kind, SelectItem.kind, OrderByItem.kind, CaseKeyValuePair.kind, SwitchCaseArgument.kind, ColumnReference.kind, LiteralValue.kind, ParameterExpression.kind, TableSource.kind, SourceAliasExpression.kind, TypeValue.kind, FunctionCall.kind, IdentifierString.kind, QualifiedName.kind ]); } return this._selfHandlingComponentTypes; } constructor(options) { var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m; this.handlers = new Map(); this.index = 1; this.listContinuationCommentComponents = new WeakSet(); this.joinConditionContexts = []; if (options === null || options === void 0 ? void 0 : options.preset) { const preset = options.preset; options = Object.assign(Object.assign({}, preset), options); } this.parameterDecorator = new ParameterDecorator({ prefix: typeof (options === null || options === void 0 ? void 0 : options.parameterSymbol) === 'string' ? options.parameterSymbol : (_b = (_a = options === null || options === void 0 ? void 0 : options.parameterSymbol) === null || _a === void 0 ? void 0 : _a.start) !== null && _b !== void 0 ? _b : ':', suffix: typeof (options === null || options === void 0 ? void 0 : options.parameterSymbol) === 'object' ? options.parameterSymbol.end : '', style: (_c = options === null || options === void 0 ? void 0 : options.parameterStyle) !== null && _c !== void 0 ? _c : 'named' }); this.identifierDecorator = new IdentifierDecorator({ start: (_e = (_d = options === null || options === void 0 ? void 0 : options.identifierEscape) === null || _d === void 0 ? void 0 : _d.start) !== null && _e !== void 0 ? _e : '"', end: (_g = (_f = options === null || options === void 0 ? void 0 : options.identifierEscape) === null || _f === void 0 ? void 0 : _f.end) !== null && _g !== void 0 ? _g : '"', target: (_h = options === null || options === void 0 ? void 0 : options.identifierEscape) === null || _h === void 0 ? void 0 : _h.target }); this.castStyle = (_j = options === null || options === void 0 ? void 0 : options.castStyle) !== null && _j !== void 0 ? _j : 'standard'; this.constraintStyle = (_k = options === null || options === void 0 ? void 0 : options.constraintStyle) !== null && _k !== void 0 ? _k : 'postgres'; this.sourceAliasStyle = this.normalizeAliasKeywordStyle(options === null || options === void 0 ? void 0 : options.sourceAliasStyle); this.columnAliasStyle = this.normalizeAliasKeywordStyle(options === null || options === void 0 ? void 0 : options.columnAliasStyle); this.orderByDefaultDirectionStyle = (_l = options === null || options === void 0 ? void 0 : options.orderByDefaultDirectionStyle) !== null && _l !== void 0 ? _l : 'omit'; this.normalizeJoinConditionOrder = (_m = options === null || options === void 0 ? void 0 : options.joinConditionOrderByDeclaration) !== null && _m !== void 0 ? _m : false; this.handlers.set(ValueList.kind, (expr) => this.visitValueList(expr)); this.handlers.set(ColumnReference.kind, (expr) => this.visitColumnReference(expr)); this.handlers.set(QualifiedName.kind, (expr) => this.visitQualifiedName(expr)); this.handlers.set(FunctionCall.kind, (expr) => this.visitFunctionCall(expr)); this.handlers.set(UnaryExpression.kind, (expr) => this.visitUnaryExpression(expr)); this.handlers.set(BinaryExpression.kind, (expr) => this.visitBinaryExpression(expr)); this.handlers.set(LiteralValue.kind, (expr) => this.visitLiteralValue(expr)); this.handlers.set(ParameterExpression.kind, (expr) => this.visitParameterExpression(expr)); this.handlers.set(SwitchCaseArgument.kind, (expr) => this.visitSwitchCaseArgument(expr)); this.handlers.set(CaseKeyValuePair.kind, (expr) => this.visitCaseKeyValuePair(expr)); this.handlers.set(RawString.kind, (expr) => this.visitRawString(expr)); this.handlers.set(IdentifierString.kind, (expr) => this.visitIdentifierString(expr)); this.handlers.set(ParenExpression.kind, (expr) => this.visitParenExpression(expr)); this.handlers.set(CastExpression.kind, (expr) => this.visitCastExpression(expr)); this.handlers.set(CaseExpression.kind, (expr) => this.visitCaseExpression(expr)); this.handlers.set(ArrayExpression.kind, (expr) => this.visitArrayExpression(expr)); this.handlers.set(ArrayQueryExpression.kind, (expr) => this.visitArrayQueryExpression(expr)); this.handlers.set(ArraySliceExpression.kind, (expr) => this.visitArraySliceExpression(expr)); this.handlers.set(ArrayIndexExpression.kind, (expr) => this.visitArrayIndexExpression(expr)); this.handlers.set(BetweenExpression.kind, (expr) => this.visitBetweenExpression(expr)); this.handlers.set(JsonPredicateExpression.kind, (expr) => this.visitJsonPredicateExpression(expr)); this.handlers.set(StringSpecifierExpression.kind, (expr) => this.visitStringSpecifierExpression(expr)); this.handlers.set(TypeValue.kind, (expr) => this.visitTypeValue(expr)); this.handlers.set(TupleExpression.kind, (expr) => this.visitTupleExpression(expr)); this.handlers.set(InlineQuery.kind, (expr) => this.visitInlineQuery(expr)); this.handlers.set(WindowFrameExpression.kind, (expr) => this.visitWindowFrameExpression(expr)); this.handlers.set(WindowFrameSpec.kind, (expr) => this.visitWindowFrameSpec(expr)); this.handlers.set(WindowFrameBoundStatic.kind, (expr) => this.visitWindowFrameBoundStatic(expr)); this.handlers.set(WindowFrameBoundaryValue.kind, (expr) => this.visitWindowFrameBoundaryValue(expr)); this.handlers.set(PartitionByClause.kind, (expr) => this.visitPartitionByClause(expr)); this.handlers.set(OrderByClause.kind, (expr) => this.visitOrderByClause(expr)); this.handlers.set(OrderByItem.kind, (expr) => this.visitOrderByItem(expr)); // select this.handlers.set(SelectItem.kind, (expr) => this.visitSelectItem(expr)); this.handlers.set(SelectClause.kind, (expr) => this.visitSelectClause(expr)); this.handlers.set(Distinct.kind, (expr) => this.visitDistinct(expr)); this.handlers.set(DistinctOn.kind, (expr) => this.visitDistinctOn(expr)); this.handlers.set(HintClause.kind, (expr) => this.visitHintClause(expr)); // from this.handlers.set(TableSource.kind, (expr) => this.visitTableSource(expr)); this.handlers.set(FunctionSource.kind, (expr) => this.visitFunctionSource(expr)); this.handlers.set(SourceExpression.kind, (expr) => this.visitSourceExpression(expr)); this.handlers.set(SourceAliasExpression.kind, (expr) => this.visitSourceAliasExpression(expr)); this.handlers.set(FromClause.kind, (expr) => this.visitFromClause(expr)); this.handlers.set(JoinClause.kind, (expr) => this.visitJoinClause(expr)); this.handlers.set(JoinOnClause.kind, (expr) => this.visitJoinOnClause(expr)); this.handlers.set(JoinUsingClause.kind, (expr) => this.visitJoinUsingClause(expr)); // where this.handlers.set(WhereClause.kind, (expr) => this.visitWhereClause(expr)); // group this.handlers.set(GroupByClause.kind, (expr) => this.visitGroupByClause(expr)); this.handlers.set(HavingClause.kind, (expr) => this.visitHavingClause(expr)); this.handlers.set(WindowClause.kind, (expr) => this.visitWindowClause(expr)); this.handlers.set(WindowFrameClause.kind, (expr) => this.visitWindowFrameClause(expr)); this.handlers.set(LimitClause.kind, (expr) => this.visitLimitClause(expr)); this.handlers.set(OffsetClause.kind, (expr) => this.visitOffsetClause(expr)); this.handlers.set(FetchClause.kind, (expr) => this.visitFetchClause(expr)); this.handlers.set(FetchExpression.kind, (expr) => this.visitFetchExpression(expr)); this.handlers.set(ForClause.kind, (expr) => this.visitForClause(expr)); // With this.handlers.set(WithClause.kind, (expr) => this.visitWithClause(expr)); this.handlers.set(CommonTable.kind, (expr) => this.visitCommonTable(expr)); // Query this.handlers.set(SimpleSelectQuery.kind, (expr) => this.visitSimpleQuery(expr)); this.handlers.set(SubQuerySource.kind, (expr) => this.visitSubQuerySource(expr)); this.handlers.set(BinarySelectQuery.kind, (expr) => this.visitBinarySelectQuery(expr)); this.handlers.set(ValuesQuery.kind, (expr) => this.visitValuesQuery(expr)); this.handlers.set(TupleExpression.kind, (expr) => this.visitTupleExpression(expr)); this.handlers.set(InsertQuery.kind, (expr) => this.visitInsertQuery(expr)); this.handlers.set(InsertClause.kind, (expr) => this.visitInsertClause(expr)); this.handlers.set(OnConflictClause.kind, (expr) => this.visitOnConflictClause(expr)); this.handlers.set(UpdateQuery.kind, (expr) => this.visitUpdateQuery(expr)); this.handlers.set(UpdateClause.kind, (expr) => this.visitUpdateClause(expr)); this.handlers.set(DeleteQuery.kind, (expr) => this.visitDeleteQuery(expr)); this.handlers.set(DeleteClause.kind, (expr) => this.visitDeleteClause(expr)); this.handlers.set(UsingClause.kind, (expr) => this.visitUsingClause(expr)); this.handlers.set(SetClause.kind, (expr) => this.visitSetClause(expr)); this.handlers.set(SetClauseItem.kind, (expr) => this.visitSetClauseItem(expr)); this.handlers.set(ReturningClause.kind, (expr) => this.visitReturningClause(expr)); this.handlers.set(CreateTableQuery.kind, (expr) => this.visitCreateTableQuery(expr)); this.handlers.set(TableColumnDefinition.kind, (expr) => this.visitTableColumnDefinition(expr)); this.handlers.set(ColumnConstraintDefinition.kind, (expr) => this.visitColumnConstraintDefinition(expr)); this.handlers.set(TableConstraintDefinition.kind, (expr) => this.visitTableConstraintDefinition(expr)); this.handlers.set(ReferenceDefinition.kind, (expr) => this.visitReferenceDefinition(expr)); this.handlers.set(CreateIndexStatement.kind, (expr) => this.visitCreateIndexStatement(expr)); this.handlers.set(CreateSchemaStatement.kind, (expr) => this.visitCreateSchemaStatement(expr)); this.handlers.set(IndexColumnDefinition.kind, (expr) => this.visitIndexColumnDefinition(expr)); this.handlers.set(CreateSequenceStatement.kind, (expr) => this.visitCreateSequenceStatement(expr)); this.handlers.set(AlterSequenceStatement.kind, (expr) => this.visitAlterSequenceStatement(expr)); this.handlers.set(DropTableStatement.kind, (expr) => this.visitDropTableStatement(expr)); this.handlers.set(DropIndexStatement.kind, (expr) => this.visitDropIndexStatement(expr)); this.handlers.set(DropSchemaStatement.kind, (expr) => this.visitDropSchemaStatement(expr)); this.handlers.set(CommentOnStatement.kind, (expr) => this.visitCommentOnStatement(expr)); this.handlers.set(AlterTableStatement.kind, (expr) => this.visitAlterTableStatement(expr)); this.handlers.set(AlterTableAddConstraint.kind, (expr) => this.visitAlterTableAddConstraint(expr)); this.handlers.set(AlterTableDropConstraint.kind, (expr) => this.visitAlterTableDropConstraint(expr)); this.handlers.set(AlterTableAddColumn.kind, (expr) => this.visitAlterTableAddColumn(expr)); this.handlers.set(AlterTableDropColumn.kind, (expr) => this.visitAlterTableDropColumn(expr)); this.handlers.set(AlterTableAlterColumnDefault.kind, (expr) => this.visitAlterTableAlterColumnDefault(expr)); this.handlers.set(DropConstraintStatement.kind, (expr) => this.visitDropConstraintStatement(expr)); this.handlers.set(ExplainStatement.kind, (expr) => this.visitExplainStatement(expr)); this.handlers.set(AnalyzeStatement.kind, (expr) => this.visitAnalyzeStatement(expr)); this.handlers.set(MergeQuery.kind, (expr) => this.visitMergeQuery(expr)); this.handlers.set(MergeWhenClause.kind, (expr) => this.visitMergeWhenClause(expr)); this.handlers.set(MergeUpdateAction.kind, (expr) => this.visitMergeUpdateAction(expr)); this.handlers.set(MergeDeleteAction.kind, (expr) => this.visitMergeDeleteAction(expr)); this.handlers.set(MergeInsertAction.kind, (expr) => this.visitMergeInsertAction(expr)); this.handlers.set(MergeDoNothingAction.kind, (expr) => this.visitMergeDoNothingAction(expr)); } /** * Pretty-prints a BinarySelectQuery (e.g., UNION, INTERSECT, EXCEPT). * This will recursively print left and right queries, separated by the operator. * @param arg BinarySelectQuery */ visitBinarySelectQuery(arg) { const token = new SqlPrintToken(SqlPrintTokenType.container, ''); // Handle positioned comments for BinarySelectQuery (unified spec) if (arg.positionedComments && arg.positionedComments.length > 0) { this.addPositionedCommentsToToken(token, arg); // Clear positioned comments to prevent duplicate processing arg.positionedComments = null; } else if (arg.headerComments && arg.headerComments.length > 0) { if (this.shouldMergeHeaderComments(arg.headerComments)) { const mergedHeaderComment = this.createHeaderMultiLineCommentBlock(arg.headerComments); token.innerTokens.push(mergedHeaderComment); } else { const headerCommentBlocks = this.createCommentBlocks(arg.headerComments, true); token.innerTokens.push(...headerCommentBlocks); } token.innerTokens.push(SqlPrintTokenParser.SPACE_TOKEN); } token.innerTokens.push(this.visit(arg.left)); token.innerTokens.push(SqlPrintTokenParser.SPACE_TOKEN); token.innerTokens.push(new SqlPrintToken(SqlPrintTokenType.keyword, arg.operator.value, SqlPrintTokenContainerType.BinarySelectQueryOperator)); token.innerTokens.push(SqlPrintTokenParser.SPACE_TOKEN); token.innerTokens.push(this.visit(arg.right)); return token; } visitCreateSchemaStatement(arg) { const keywordParts = ['create', 'schema']; if (arg.ifNotExists) { keywordParts.push('if not exists'); } const token = new SqlPrintToken(SqlPrintTokenType.keyword, keywordParts.join(' '), SqlPrintTokenContainerType.CreateSchemaStatement); token.innerTokens.push(SqlPrintTokenParser.SPACE_TOKEN); token.innerTokens.push(arg.schemaName.accept(this)); // Render AUTHORIZATION clause when the owner is explicitly specified. if (arg.authorization) { token.innerTokens.push(SqlPrintTokenParser.SPACE_TOKEN); token.innerTokens.push(new SqlPrintToken(SqlPrintTokenType.keyword, 'authorization')); token.innerTokens.push(SqlPrintTokenParser.SPACE_TOKEN); token.innerTokens.push(arg.authorization.accept(this)); } return token; } /** * Returns an array of tokens representing a comma followed by a space. * This is a common pattern in SQL pretty-printing. */ static commaSpaceTokens() { return [SqlPrintTokenParser.COMMA_TOKEN, SqlPrintTokenParser.SPACE_TOKEN]; } static argumentCommaSpaceTokens() { return [SqlPrintTokenParser.ARGUMENT_SPLIT_COMMA_TOKEN, SqlPrintTokenParser.SPACE_TOKEN]; } visitQualifiedName(arg) { const token = new SqlPrintToken(SqlPrintTokenType.container, '', SqlPrintTokenContainerType.QualifiedName); const hasOwnComments = this.hasPositionedComments(arg) || this.hasLegacyComments(arg); if (arg.namespaces) { for (let i = 0; i < arg.namespaces.length; i++) { token.innerTokens.push(arg.namespaces[i].accept(this)); token.innerTokens.push(SqlPrintTokenParser.DOT_TOKEN); } } // Handle name and its comments carefully // We need to prevent double processing by temporarily clearing the name's comments, // then process them at the QualifiedName level const originalNameComments = arg.name.positionedComments; const originalNameLegacyComments = arg.name.comments; // Temporarily clear name's comments to prevent double processing arg.name.positionedComments = null; arg.name.comments = null; const nameToken = arg.name.accept(this); token.innerTokens.push(nameToken); // Restore original comments arg.name.positionedComments = originalNameComments; arg.name.comments = originalNameLegacyComments; // Apply the name's comments to the qualified name token if (!hasOwnComments && (this.hasPositionedComments(arg.name) || this.hasLegacyComments(arg.name))) { this.addComponentComments(token, arg.name); } // Also handle any comments directly on the QualifiedName itself this.addComponentComments(token, arg); return token; } visitPartitionByClause(arg) { // Print as: partition by ... const token = new SqlPrintToken(SqlPrintTokenType.keyword, 'partition by', SqlPrintTokenContainerType.PartitionByClause); token.innerTokens.push(SqlPrintTokenParser.SPACE_TOKEN); token.innerTokens.push(this.visit(arg.value)); return token; } visitOrderByClause(arg) { // Print as: order by ... const token = new SqlPrintToken(SqlPrintTokenType.keyword, 'order by', SqlPrintTokenContainerType.OrderByClause); token.innerTokens.push(SqlPrintTokenParser.SPACE_TOKEN); for (let i = 0; i < arg.order.length; i++) { if (i > 0) token.innerTokens.push(...SqlPrintTokenParser.commaSpaceTokens()); const item = arg.order[i]; token.innerTokens.push(this.visit(item)); if (!(item instanceof OrderByItem) && this.orderByDefaultDirectionStyle === 'explicit') { token.innerTokens.push(SqlPrintTokenParser.SPACE_TOKEN); token.innerTokens.push(new SqlPrintToken(SqlPrintTokenType.keyword, 'asc')); } } return token; } /** * Print an OrderByItem (expression [asc|desc] [nulls first|last]) */ visitOrderByItem(arg) { // arg: OrderByItem const token = new SqlPrintToken(SqlPrintTokenType.container, '', SqlPrintTokenContainerType.OrderByItem); token.innerTokens.push(this.visit(arg.value)); if (arg.sortDirection && arg.sortDirection !== SortDirection.Ascending) { token.innerTokens.push(SqlPrintTokenParser.SPACE_TOKEN); token.innerTokens.push(new SqlPrintToken(SqlPrintTokenType.keyword, 'desc')); } else if (this.orderByDefaultDirectionStyle === 'explicit') { token.innerTokens.push(SqlPrintTokenParser.SPACE_TOKEN); token.innerTokens.push(new SqlPrintToken(SqlPrintTokenType.keyword, 'asc')); } if (arg.nullsPosition) { if (arg.nullsPosition === NullsSortDirection.First) { token.innerTokens.push(SqlPrintTokenParser.SPACE_TOKEN); token.innerTokens.push(new SqlPrintToken(SqlPrintTokenType.keyword, 'nulls first')); } else if (arg.nullsPosition === NullsSortDirection.Last) { token.innerTokens.push(SqlPrintTokenParser.SPACE_TOKEN); token.innerTokens.push(new SqlPrintToken(SqlPrintTokenType.keyword, 'nulls last')); } } if (arg.comments && arg.comments.length > 0) { token.innerTokens.push(SqlPrintTokenParser.SPACE_TOKEN); token.innerTokens.push(...this.createInlineCommentSequence(arg.comments)); } return token; } parse(arg) { // reset parameter index before parsing this.index = 1; const token = this.visit(arg); const paramsRaw = ParameterCollector.collect(arg).sort((a, b) => { var _a, _b; return ((_a = a.index) !== null && _a !== void 0 ? _a : 0) - ((_b = b.index) !== null && _b !== void 0 ? _b : 0); }); const style = this.parameterDecorator.style; if (style === ParameterStyle.Named || style === ParameterStyle.Original) { // Named: { name: value, ... } const paramsObj = {}; for (const p of paramsRaw) { const key = p.name.value; if (paramsObj.hasOwnProperty(key)) { if (paramsObj[key] !== p.value) { throw new Error(`Duplicate parameter name '${key}' with different values detected during query composition.`); } // If value is the same, skip (already set) continue; } paramsObj[key] = p.value; } return { token, params: paramsObj }; } else if (style === ParameterStyle.Indexed) { // Indexed: [value1, value2, ...] (sorted by index) const paramsArr = paramsRaw.map(p => p.value); return { token, params: paramsArr }; } else if (style === ParameterStyle.Anonymous) { // Anonymous: [value1, value2, ...] (sorted by index, name is empty) const paramsArr = paramsRaw.map(p => p.value); return { token, params: paramsArr }; } // Fallback (just in case) return { token, params: [] }; } /** * Check if a component handles its own comments */ componentHandlesOwnComments(component) { // First check if component has a handlesOwnComments method if ('handlesOwnComments' in component && typeof component.handlesOwnComments === 'function') { return component.handlesOwnComments(); } return SqlPrintTokenParser.getSelfHandlingComponentTypes().has(component.getKind()); } visit(arg) { const handler = this.handlers.get(arg.getKind()); if (handler) { const token = handler(arg); if (!this.componentHandlesOwnComments(arg)) { this.addComponentComments(token, arg); } return token; } throw new Error(`[SqlPrintTokenParser] No handler for kind: ${arg.getKind().toString()}`); } /** * Check if a component has positioned comments */ hasPositionedComments(component) { var _a, _b; return ((_b = (_a = component.positionedComments) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0) > 0; } /** * Check if a component has legacy comments */ hasLegacyComments(component) { var _a, _b; return ((_b = (_a = component.comments) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0) > 0; } /** * Centralized comment handling - checks positioned comments first, falls back to legacy */ addComponentComments(token, component) { if (this.hasPositionedComments(component)) { this.addPositionedCommentsToToken(token, component); } else if (this.hasLegacyComments(component)) { this.addCommentsToToken(token, component.comments); } } /** * Adds positioned comment tokens to a SqlPrintToken for inline formatting */ addPositionedCommentsToToken(token, component) { if (!this.hasPositionedComments(component)) { return; } // Handle 'before' comments - add inline at the beginning with spaces const beforeComments = component.getPositionedComments('before'); if (beforeComments.length > 0) { const commentBlocks = this.createCommentBlocks(beforeComments); for (let i = commentBlocks.length - 1; i >= 0; i--) { token.innerTokens.unshift(commentBlocks[i]); } } // Handle 'after' comments - add inline after the main content const afterComments = component.getPositionedComments('after'); if (afterComments.length > 0) { const commentBlocks = this.createCommentBlocks(afterComments); // Append after comments with spaces for inline formatting for (const commentBlock of commentBlocks) { token.innerTokens.push(SqlPrintTokenParser.SPACE_TOKEN); token.innerTokens.push(commentBlock); } } // Clear positioned comments to prevent duplicate processing (unified spec) // Only clear for specific component types that are known to have duplication issues const componentsWithDuplicationIssues = [ SqlPrintTokenContainerType.CaseExpression, SqlPrintTokenContainerType.SwitchCaseArgument, SqlPrintTokenContainerType.CaseKeyValuePair, SqlPrintTokenContainerType.SelectClause, // SELECT clauses have manual + automatic processing SqlPrintTokenContainerType.LiteralValue, SqlPrintTokenContainerType.IdentifierString, SqlPrintTokenContainerType.DistinctOn, SqlPrintTokenContainerType.SourceAliasExpression, SqlPrintTokenContainerType.SimpleSelectQuery, SqlPrintTokenContainerType.WhereClause // WHERE clauses also have duplication issues ]; if (token.containerType && componentsWithDuplicationIssues.includes(token.containerType)) { component.positionedComments = null; } } /** * Adds comment tokens to a SqlPrintToken based on the comments array */ addCommentsToToken(token, comments) { if (!(comments === null || comments === void 0 ? void 0 : comments.length)) { return; } const commentBlocks = this.createCommentBlocks(comments); this.insertCommentBlocksWithSpacing(token, commentBlocks); } /** * Creates inline comment sequence for multiple comments without newlines */ createInlineCommentSequence(comments) { const commentTokens = []; for (let i = 0; i < comments.length; i++) { const comment = comments[i]; if (comment.trim()) { // Add comment token directly const commentToken = new SqlPrintToken(SqlPrintTokenType.comment, this.formatComment(comment)); commentTokens.push(commentToken); // Add space between comments (except after last comment) if (i < comments.length - 1) { const spaceToken = new SqlPrintToken(SqlPrintTokenType.space, ' '); commentTokens.push(spaceToken); } } } return commentTokens; } /** * Creates CommentBlock containers for the given comments. * Each CommentBlock contains: Comment -> CommentNewline -> Space. * @param comments Raw comment strings to convert into CommentBlock tokens. * @param isHeaderComment Marks the generated blocks as originating from header comments when true. */ createCommentBlocks(comments, isHeaderComment = false) { // Create individual comment blocks for each comment entry const commentBlocks = []; for (const comment of comments) { // Accept comments that have content after trim OR are separator lines OR are empty (for structure preservation) const trimmed = comment.trim(); const isSeparatorLine = /^[-=_+*#]+$/.test(trimmed); if (trimmed || isSeparatorLine || comment === '') { commentBlocks.push(this.createSingleCommentBlock(comment, isHeaderComment)); } } return commentBlocks; } /** * Determines if a comment should be merged with consecutive comments */ shouldMergeComment(trimmed) { const isSeparatorLine = /^[-=_+*#]+$/.test(trimmed); // Don't merge line comments unless they are separator-only lines if (!isSeparatorLine && trimmed.startsWith('--')) { return false; } // Don't merge if it's already a proper multi-line block comment if (trimmed.startsWith('/*') && trimmed.endsWith('*/')) { const inner = trimmed.slice(2, -2).trim(); if (!inner) { return false; } if (trimmed.includes('\n')) { return false; } } // Merge all other content including separator lines, plain text, and single-line block comments // Separator lines within comment blocks should be merged together return true; } /** * Creates a multi-line block comment structure from consecutive comments * Returns a CommentBlock containing multiple comment lines for proper LinePrinter integration */ /** * Creates a single CommentBlock with the standard structure: * Comment -> CommentNewline -> Space * * This structure supports both formatting modes: * - Multiline mode: Comment + newline (space is filtered as leading space) * - Oneliner mode: Comment + space (commentNewline is skipped) */ createSingleCommentBlock(comment, isHeaderComment = false) { const commentBlock = new SqlPrintToken(SqlPrintTokenType.container, '', SqlPrintTokenContainerType.CommentBlock); if (isHeaderComment) { commentBlock.markAsHeaderComment(); } // Add comment token - preserve original format for line comments const commentToken = new SqlPrintToken(SqlPrintTokenType.comment, this.formatComment(comment)); commentBlock.innerTokens.push(commentToken); // Add conditional newline token for multiline mode const commentNewlineToken = new SqlPrintToken(SqlPrintTokenType.commentNewline, ''); commentBlock.innerTokens.push(commentNewlineToken); // Add space token for oneliner mode spacing const spaceToken = new SqlPrintToken(SqlPrintTokenType.space, ' '); commentBlock.innerTokens.push(spaceToken); return commentBlock; } /** * Formats a comment, preserving line comment format for -- comments * and converting others to block format for safety */ formatComment(comment) { const trimmed = comment.trim(); if (!trimmed) { return '/* */'; } const isSeparatorLine = /^[-=_+*#]+$/.test(trimmed); if (isSeparatorLine) { return this.formatBlockComment(trimmed); } if (trimmed.startsWith('--')) { return this.formatLineComment(trimmed.slice(2)); } if (trimmed.startsWith('/*') && trimmed.endsWith('*/')) { return this.formatBlockComment(trimmed); } return this.formatBlockComment(trimmed); } /** * Inserts comment blocks into a token and handles spacing logic. * Adds separator spaces for clause-level containers and manages duplicate space removal. */ insertCommentBlocksWithSpacing(token, commentBlocks) { // For SelectItem, append comment blocks after ensuring spacing if (token.containerType === SqlPrintTokenContainerType.SelectItem) { if (token.innerTokens.length > 0) { const lastToken = token.innerTokens[token.innerTokens.length - 1]; if (lastToken.type !== SqlPrintTokenType.space) { token.innerTokens.push(SqlPrintTokenParser.SPACE_TOKEN); } } token.innerTokens.push(...commentBlocks); return; } // Special handling for SelectClause to add space between keyword and comment if (token.containerType === SqlPrintTokenContainerType.SelectClause) { // For SelectClause, comments need to be inserted after the keyword with a space separator // Current structure: [keyword text, space, other tokens...] // Desired structure: [keyword text, space, comments, space, other tokens...] token.innerTokens.unshift(SqlPrintTokenParser.SPACE_TOKEN, ...commentBlocks); return; } // Special handling for IdentifierString to add space before comment if (token.containerType === SqlPrintTokenContainerType.IdentifierString) { if (token.innerTokens.length > 0) { const lastToken = token.innerTokens[token.innerTokens.length - 1]; if (lastToken.type !== SqlPrintTokenType.space) { token.innerTokens.push(SqlPrintTokenParser.SPACE_TOKEN); } } token.innerTokens.push(...commentBlocks); return; } token.innerTokens.unshift(...commentBlocks); // Add a separator space after comments only for certain container types // where comments need to be separated from main content const needsSeparatorSpace = this.shouldAddSeparatorSpace(token.containerType); if (needsSeparatorSpace) { const separatorSpace = new SqlPrintToken(SqlPrintTokenType.space, ' '); token.innerTokens.splice(commentBlocks.length, 0, separatorSpace); // Remove the original space token after our separator if it exists // This prevents duplicate spaces when comments are added if (token.innerTokens.length > commentBlocks.length + 1 && token.innerTokens[commentBlocks.length + 1].type === SqlPrintTokenType.space) { token.innerTokens.splice(commentBlocks.length + 1, 1); } } else { // For containers that don't need separator space, still remove duplicate spaces if (token.innerTokens.length > commentBlocks.length && token.innerTokens[commentBlocks.length].type === SqlPrintTokenType.space) { token.innerTokens.splice(commentBlocks.length, 1); } } } /** * Handles positioned comments for ParenExpression with special spacing rules. * ParenExpression comments should be adjacent to parentheses without separator spaces. */ addPositionedCommentsToParenExpression(token, component) { if (!component.positionedComments) { return; } // For ParenExpression: (/* comment */ content /* comment */) // Comments should be placed immediately after opening paren and before closing paren // Handle 'before' comments - place after opening parenthesis without space const beforeComments = component.getPositionedComments('before'); if (beforeComments.length > 0) { const commentBlocks = this.createCommentBlocks(beforeComments); // Insert after opening paren (index 1) without separator space let insertIndex = 1; for (const commentBlock of commentBlocks) { token.innerTokens.splice(insertIndex, 0, commentBlock); insertIndex++; } } // Handle 'after' comments - place before closing parenthesis without space const afterComments = component.getPositionedComments('after'); if (afterComments.length > 0) { const commentBlocks = this.createCommentBlocks(afterComments); const closingIndex = token.innerTokens.length - 1; let insertIndex = closingIndex + 1; for (const commentBlock of commentBlocks) { token.innerTokens.splice(insertIndex, 0, SqlPrintTokenParser.SPACE_TOKEN, commentBlock); insertIndex += 2; } } } /** * Determines whether a separator space should be added after comments for the given container type. * * Clause-level containers (SELECT, FROM, WHERE, etc.) need separator spaces because: * - Comments appear before the main clause content * - A space is needed to separate comment block from SQL tokens * * Item-level containers (SelectItem, etc.) don't need separator spaces because: * - Comments are inline with the item content * - Spacing is handled by existing token structure */ shouldAddSeparatorSpace(containerType) { return this.isClauseLevelContainer(containerType); } /** * Checks if the container type represents a SQL clause (as opposed to an item within a clause). */ isClauseLevelContainer(containerType) { switch (containerType) { case SqlPrintTokenContainerType.SelectClause: case SqlPrintTokenContainerType.FromClause: case SqlPrintTokenContainerType.WhereClause: case SqlPrintTokenContainerType.GroupByClause: case SqlPrintTokenContainerType.HavingClause: case SqlPrintTokenContainerType.OrderByClause: case SqlPrintTokenContainerType.LimitClause: case SqlPrintTokenContainerType.OffsetClause: case SqlPrintTokenContainerType.WithClause: case SqlPrintTokenContainerType.SimpleSelectQuery: return true; default: return false; } } /** * Formats a comment string as a block comment with security sanitization. * Prevents SQL injection by removing dangerous comment sequences. */ formatBlockComment(comment) { const hasDelimiters = comment.startsWith('/*') && comment.endsWith('*/'); const rawContent = hasDelimiters ? comment.slice(2, -2) : comment; const escapedContent = this.escapeCommentDelimiters(rawContent); const normalized = escapedContent.replace(/\r?\n/g, '\n'); const lines = normalized .split('\n') .map(line => line.replace(/\s+/g, ' ').trim()) .filter(line => line.length > 0); if (lines.length === 0) { return '/* */'; } const isSeparatorLine = lines.length === 1 && /^[-=_+*#]+$/.test(lines[0]); if (!hasDelimiters) { // Flatten free-form comments to a single block to avoid leaking multi-line structures. if (isSeparatorLine) { return `/* ${lines[0]} */`; } const flattened = lines.join(' '); return `/* ${flattened} */`; } if (isSeparatorLine || lines.length === 1) { return `/* ${lines[0]} */`; } const body = lines.map(line => ` ${line}`).join('\n'); return `/*\n${body}\n*/`; } shouldMergeHeaderComments(comments) { if (comments.length <= 1) { return false; } return comments.some(comment => { const trimmed = comment.trim(); return /^[-=_+*#]{3,}$/.test(trimmed) || trimmed.startsWith('- ') || trimmed.startsWith('* '); }); } createHeaderMultiLineCommentBlock(headerComments) { const commentBlock = new SqlPrintToken(SqlPrintTokenType.container, '', SqlPrintTokenContainerType.CommentBlock); commentBlock.markAsHeaderComment(); if (headerComments.length === 0) { const commentToken = new SqlPrintToken(SqlPrintTokenType.comment, '/* */'); commentBlock.innerTokens.push(commentToken); } else { const openToken = new SqlPrintToken(SqlPrintTokenType.comment, '/*'); commentBlock.innerTokens.push(openToken); commentBlock.innerTokens.push(new SqlPrintToken(SqlPrintTokenType.commentNewline, '')); for (const line of headerComments) { const sanitized = this.escapeCommentDelimiters(line); const lineToken = new SqlPrintToken(SqlPrintTokenType.comment, ` ${sanitized}`); commentBlock.innerTokens.push(lineToken); commentBlock.innerTokens.push(new SqlPrintToken(SqlPrintTokenType.commentNewline, '')); } const closeToken = new SqlPrintToken(SqlPrintTokenType.comment, '*/'); commentBlock.innerTokens.push(closeToken); } commentBlock.innerTokens.push(new SqlPrintToken(SqlPrintTokenType.commentNewline, '')); commentBlock.innerTokens.push(new SqlPrintToken(SqlPrintTokenType.space, ' ')); return commentBlock; } /** * Formats text as a single-line comment while sanitizing unsafe sequences. */ formatLineComment(content) { // Normalize content to a single line and remove dangerous sequences const sanitized = this.sanitizeLineCommentContent(content); if (!sanitized) { return '--'; } return `-- ${sanitized}`; } /** * Sanitizes content intended for a single-line comment. */ sanitizeLineCommentContent(content) { // Replace comment delimiters to avoid nested comment injection let sanitized = this.escapeCommentDelimiters(content) .replace(/\r?\n/g, ' ') .replace(/\u2028|\u2029/g, ' ') .replace(/\s+/g, ' ') .trim(); if (sanitized.startsWith('--')) { sanitized = sanitized.slice(2).trimStart(); } return sanitized; } escapeCommentDelimiters(content) { return content .replace(/\/\*/g, '\\/\\*') .replace(/\*\//g, '*\\/'); } visitValueList(arg) { const token = new SqlPrintToken(SqlPrintTokenType.container, '', SqlPrintTokenContainerType.ValueList); for (let i = 0; i < arg.values.length; i++) { if (i > 0) { token.innerTokens.push(...SqlPrintTokenParser.argumentCommaSpaceTokens()); } token.innerTokens.push(this.visit(arg.values[i])); } return token; } visitColumnReference(arg) { const token = new SqlPrintToken(SqlPrintTokenType.container, '', SqlPrintTokenContainerType.ColumnReference); const hasOwnComments = this.hasPositionedComments(arg) || this.hasLegacyComments(arg); const originalNamePositionedComments = arg.qualifiedName.name.positionedComments; const originalNameComments = arg.qualifiedName.name.comments; if (hasOwnComments) { arg.qualifiedName.name.positionedComments = null; arg.qualifiedName.name.comments = null; } token.innerTokens.push(arg.qualifiedName.accept(this)); if (hasOwnComments) { arg.qualifiedName.name.positionedComments = originalNamePositionedComments; arg.qualifiedName.name.comments = originalNameComments; } this.addComponentComments(token, arg); return token; } visitFunctionCall(arg) { const token = new SqlPrintToken(SqlPrintTokenType.container, '', SqlPrintTokenContainerType.FunctionCall); token.innerTokens.push(arg.qualifiedName.accept(this)); token.innerTokens.push(SqlPrintTokenParser.PAREN_OPEN_TOKEN); if (arg