UNPKG

rawsql-ts

Version:

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

944 lines 72.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.SqlPrinter = void 0; const SqlPrintToken_1 = require("../models/SqlPrintToken"); const LinePrinter_1 = require("./LinePrinter"); const FormatOptionResolver_1 = require("./FormatOptionResolver"); const OnelineFormattingHelper_1 = require("./OnelineFormattingHelper"); const CREATE_TABLE_SINGLE_PAREN_KEYWORDS = new Set(['unique', 'check', 'key', 'index']); const CREATE_TABLE_MULTI_PAREN_KEYWORDS = new Set(['primary key', 'foreign key', 'unique key']); const CREATE_TABLE_PAREN_KEYWORDS_WITH_IDENTIFIER = new Set(['references']); /** * SqlPrinter formats a SqlPrintToken tree into a SQL string with flexible style options. * * This class provides various formatting options including: * - Indentation control (character and size) * - Line break styles for commas and AND operators * - Keyword case transformation * - Comment handling * - WITH clause formatting styles * * @example * const printer = new SqlPrinter({ * indentChar: ' ', * indentSize: 1, * keywordCase: 'upper', * commaBreak: 'after', * withClauseStyle: 'cte-oneline' * }); * const formatted = printer.print(sqlToken); */ class SqlPrinter { /** * @param options Optional style settings for pretty printing */ constructor(options) { var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u; /** Track whether we are currently inside a WITH clause for full-oneline formatting */ this.insideWithClause = false; /** Tracks nesting depth while formatting MERGE WHEN predicate segments */ this.mergeWhenPredicateDepth = 0; /** Pending line comment that needs a forced newline before next token */ this.pendingLineCommentBreak = null; /** Accumulates lines when reconstructing multi-line block comments inside CommentBlocks */ this.smartCommentBlockBuilder = null; // Resolve logical options to their control character representations before applying defaults. const resolvedIndentChar = (0, FormatOptionResolver_1.resolveIndentCharOption)(options === null || options === void 0 ? void 0 : options.indentChar); const resolvedNewline = (0, FormatOptionResolver_1.resolveNewlineOption)(options === null || options === void 0 ? void 0 : options.newline); this.indentChar = resolvedIndentChar !== null && resolvedIndentChar !== void 0 ? resolvedIndentChar : ''; this.indentSize = (_a = options === null || options === void 0 ? void 0 : options.indentSize) !== null && _a !== void 0 ? _a : 0; // The default newline character is set to a blank space (' ') to enable one-liner formatting. // This is intentional and differs from the LinePrinter default of '\r\n'. this.newline = resolvedNewline !== null && resolvedNewline !== void 0 ? resolvedNewline : ' '; this.commaBreak = (_b = options === null || options === void 0 ? void 0 : options.commaBreak) !== null && _b !== void 0 ? _b : 'none'; this.cteCommaBreak = (_c = options === null || options === void 0 ? void 0 : options.cteCommaBreak) !== null && _c !== void 0 ? _c : this.commaBreak; this.valuesCommaBreak = (_d = options === null || options === void 0 ? void 0 : options.valuesCommaBreak) !== null && _d !== void 0 ? _d : this.commaBreak; this.andBreak = (_e = options === null || options === void 0 ? void 0 : options.andBreak) !== null && _e !== void 0 ? _e : 'none'; this.orBreak = (_f = options === null || options === void 0 ? void 0 : options.orBreak) !== null && _f !== void 0 ? _f : 'none'; this.keywordCase = (_g = options === null || options === void 0 ? void 0 : options.keywordCase) !== null && _g !== void 0 ? _g : 'none'; this.commentExportMode = this.resolveCommentExportMode(options === null || options === void 0 ? void 0 : options.exportComment); this.withClauseStyle = (_h = options === null || options === void 0 ? void 0 : options.withClauseStyle) !== null && _h !== void 0 ? _h : 'standard'; this.commentStyle = (_j = options === null || options === void 0 ? void 0 : options.commentStyle) !== null && _j !== void 0 ? _j : 'block'; this.parenthesesOneLine = (_k = options === null || options === void 0 ? void 0 : options.parenthesesOneLine) !== null && _k !== void 0 ? _k : false; this.betweenOneLine = (_l = options === null || options === void 0 ? void 0 : options.betweenOneLine) !== null && _l !== void 0 ? _l : false; this.valuesOneLine = (_m = options === null || options === void 0 ? void 0 : options.valuesOneLine) !== null && _m !== void 0 ? _m : false; this.joinOneLine = (_o = options === null || options === void 0 ? void 0 : options.joinOneLine) !== null && _o !== void 0 ? _o : false; this.caseOneLine = (_p = options === null || options === void 0 ? void 0 : options.caseOneLine) !== null && _p !== void 0 ? _p : false; this.subqueryOneLine = (_q = options === null || options === void 0 ? void 0 : options.subqueryOneLine) !== null && _q !== void 0 ? _q : false; this.indentNestedParentheses = (_r = options === null || options === void 0 ? void 0 : options.indentNestedParentheses) !== null && _r !== void 0 ? _r : false; this.insertColumnsOneLine = (_s = options === null || options === void 0 ? void 0 : options.insertColumnsOneLine) !== null && _s !== void 0 ? _s : false; this.whenOneLine = (_t = options === null || options === void 0 ? void 0 : options.whenOneLine) !== null && _t !== void 0 ? _t : false; const onelineOptions = { parenthesesOneLine: this.parenthesesOneLine, betweenOneLine: this.betweenOneLine, valuesOneLine: this.valuesOneLine, joinOneLine: this.joinOneLine, caseOneLine: this.caseOneLine, subqueryOneLine: this.subqueryOneLine, insertColumnsOneLine: this.insertColumnsOneLine, withClauseStyle: this.withClauseStyle, }; this.onelineHelper = new OnelineFormattingHelper_1.OnelineFormattingHelper(onelineOptions); this.linePrinter = new LinePrinter_1.LinePrinter(this.indentChar, this.indentSize, this.newline, this.commaBreak); // Initialize this.indentIncrementContainers = new Set((_u = options === null || options === void 0 ? void 0 : options.indentIncrementContainerTypes) !== null && _u !== void 0 ? _u : [ SqlPrintToken_1.SqlPrintTokenContainerType.SelectClause, SqlPrintToken_1.SqlPrintTokenContainerType.FromClause, SqlPrintToken_1.SqlPrintTokenContainerType.WhereClause, SqlPrintToken_1.SqlPrintTokenContainerType.GroupByClause, SqlPrintToken_1.SqlPrintTokenContainerType.HavingClause, SqlPrintToken_1.SqlPrintTokenContainerType.WindowFrameExpression, SqlPrintToken_1.SqlPrintTokenContainerType.PartitionByClause, SqlPrintToken_1.SqlPrintTokenContainerType.OrderByClause, SqlPrintToken_1.SqlPrintTokenContainerType.WindowClause, SqlPrintToken_1.SqlPrintTokenContainerType.LimitClause, SqlPrintToken_1.SqlPrintTokenContainerType.OffsetClause, SqlPrintToken_1.SqlPrintTokenContainerType.SubQuerySource, SqlPrintToken_1.SqlPrintTokenContainerType.BinarySelectQueryOperator, SqlPrintToken_1.SqlPrintTokenContainerType.Values, SqlPrintToken_1.SqlPrintTokenContainerType.WithClause, SqlPrintToken_1.SqlPrintTokenContainerType.SwitchCaseArgument, SqlPrintToken_1.SqlPrintTokenContainerType.CaseKeyValuePair, SqlPrintToken_1.SqlPrintTokenContainerType.CaseThenValue, SqlPrintToken_1.SqlPrintTokenContainerType.ElseClause, SqlPrintToken_1.SqlPrintTokenContainerType.CaseElseValue, SqlPrintToken_1.SqlPrintTokenContainerType.SimpleSelectQuery, SqlPrintToken_1.SqlPrintTokenContainerType.CreateTableDefinition, SqlPrintToken_1.SqlPrintTokenContainerType.AlterTableStatement, SqlPrintToken_1.SqlPrintTokenContainerType.IndexColumnList, SqlPrintToken_1.SqlPrintTokenContainerType.SetClause // Note: CommentBlock is intentionally excluded from indentIncrementContainers // because it serves as a grouping mechanism without affecting indentation. // CaseExpression, SwitchCaseArgument, CaseKeyValuePair, and ElseClause // are not included by default to maintain backward compatibility with tests. // SqlPrintTokenContainerType.CommonTable is also excluded by default. ]); } /** * Converts a SqlPrintToken tree to a formatted SQL string. * @param token The root SqlPrintToken to format * @param level Initial indentation level (default: 0) * @returns Formatted SQL string * @example * const printer = new SqlPrinter({ indentChar: ' ', keywordCase: 'upper' }); * const formatted = printer.print(sqlToken); */ print(token, level = 0) { // initialize this.linePrinter = new LinePrinter_1.LinePrinter(this.indentChar, this.indentSize, this.newline, this.commaBreak); this.insideWithClause = false; // Reset WITH clause context this.pendingLineCommentBreak = null; this.smartCommentBlockBuilder = null; if (this.linePrinter.lines.length > 0 && level !== this.linePrinter.lines[0].level) { this.linePrinter.lines[0].level = level; } this.appendToken(token, level, undefined, 0, false, undefined, false); return this.linePrinter.print(); } // Resolve legacy boolean values into explicit comment export modes. resolveCommentExportMode(option) { if (option === undefined) { return 'none'; } if (option === true) { return 'full'; } if (option === false) { return 'none'; } return option; } // Determine whether the current mode allows emitting inline comments. rendersInlineComments() { return this.commentExportMode === 'full'; } // Decide if a comment block or token should be rendered given its context. shouldRenderComment(token, context) { if (context === null || context === void 0 ? void 0 : context.forceRender) { return this.commentExportMode !== 'none'; } switch (this.commentExportMode) { case 'full': return true; case 'none': return false; case 'header-only': return token.isHeaderComment === true; case 'top-header-only': return token.isHeaderComment === true && Boolean(context === null || context === void 0 ? void 0 : context.isTopLevelContainer); default: return false; } } appendToken(token, level, parentContainerType, caseContextDepth = 0, indentParentActive = false, commentContext, previousSiblingWasOpenParen = false) { // Track WITH clause context for full-oneline formatting const wasInsideWithClause = this.insideWithClause; if (token.containerType === SqlPrintToken_1.SqlPrintTokenContainerType.WithClause && this.withClauseStyle === 'full-oneline') { this.insideWithClause = true; } if (this.shouldSkipToken(token)) { return; } const containerIsTopLevel = parentContainerType === undefined; let leadingCommentCount = 0; // Collect leading comment blocks with context so we can respect the export mode. const leadingCommentContexts = []; if (token.innerTokens && token.innerTokens.length > 0) { while (leadingCommentCount < token.innerTokens.length) { const leadingCandidate = token.innerTokens[leadingCommentCount]; if (leadingCandidate.containerType !== SqlPrintToken_1.SqlPrintTokenContainerType.CommentBlock) { break; } const context = { position: 'leading', isTopLevelContainer: containerIsTopLevel, }; const shouldRender = this.shouldRenderComment(leadingCandidate, context); leadingCommentContexts.push({ token: leadingCandidate, context, shouldRender }); leadingCommentCount++; } } const hasRenderableLeadingComment = leadingCommentContexts.some(item => item.shouldRender); const leadingCommentIndentLevel = hasRenderableLeadingComment ? this.getLeadingCommentIndentLevel(parentContainerType, level) : null; if (hasRenderableLeadingComment && !this.isOnelineMode() && this.shouldAddNewlineBeforeLeadingComments(parentContainerType)) { const currentLine = this.linePrinter.getCurrentLine(); if (currentLine.text.trim().length > 0) { // Align the newline before leading comments with the intended comment indentation. this.linePrinter.appendNewline(leadingCommentIndentLevel !== null && leadingCommentIndentLevel !== void 0 ? leadingCommentIndentLevel : level); } } for (const leading of leadingCommentContexts) { if (!leading.shouldRender) { continue; } // Keep leading comment processing aligned with its computed indentation level. this.appendToken(leading.token, leadingCommentIndentLevel !== null && leadingCommentIndentLevel !== void 0 ? leadingCommentIndentLevel : level, token.containerType, caseContextDepth, indentParentActive, leading.context, false); } if (this.smartCommentBlockBuilder && token.containerType !== SqlPrintToken_1.SqlPrintTokenContainerType.CommentBlock && token.type !== SqlPrintToken_1.SqlPrintTokenType.commentNewline) { this.flushSmartCommentBlockBuilder(); } if (this.pendingLineCommentBreak !== null) { if (!this.isOnelineMode()) { this.linePrinter.appendNewline(this.pendingLineCommentBreak); } const shouldSkipToken = token.type === SqlPrintToken_1.SqlPrintTokenType.commentNewline; this.pendingLineCommentBreak = null; if (shouldSkipToken) { return; } } // Fallback context applies when the caller did not provide comment metadata. const effectiveCommentContext = commentContext !== null && commentContext !== void 0 ? commentContext : { position: 'inline', isTopLevelContainer: containerIsTopLevel, }; if (token.containerType === SqlPrintToken_1.SqlPrintTokenContainerType.CommentBlock) { if (!this.shouldRenderComment(token, effectiveCommentContext)) { return; } const commentLevel = this.getCommentBaseIndentLevel(level, parentContainerType); this.handleCommentBlockContainer(token, commentLevel, effectiveCommentContext); return; } const current = this.linePrinter.getCurrentLine(); const isCaseContext = this.isCaseContext(token.containerType); const nextCaseContextDepth = isCaseContext ? caseContextDepth + 1 : caseContextDepth; const shouldIndentNested = this.shouldIndentNestedParentheses(token, previousSiblingWasOpenParen); // Handle different token types if (token.type === SqlPrintToken_1.SqlPrintTokenType.keyword) { this.handleKeywordToken(token, level, parentContainerType, caseContextDepth); } else if (token.type === SqlPrintToken_1.SqlPrintTokenType.comma) { this.handleCommaToken(token, level, parentContainerType); } else if (token.type === SqlPrintToken_1.SqlPrintTokenType.parenthesis) { this.handleParenthesisToken(token, level, indentParentActive, parentContainerType); } else if (token.type === SqlPrintToken_1.SqlPrintTokenType.operator && token.text.toLowerCase() === 'and') { this.handleAndOperatorToken(token, level, parentContainerType, caseContextDepth); } else if (token.type === SqlPrintToken_1.SqlPrintTokenType.operator && token.text.toLowerCase() === 'or') { this.handleOrOperatorToken(token, level, parentContainerType, caseContextDepth); } else if (token.containerType === SqlPrintToken_1.SqlPrintTokenContainerType.JoinClause) { this.handleJoinClauseToken(token, level); } else if (token.type === SqlPrintToken_1.SqlPrintTokenType.comment) { if (this.shouldRenderComment(token, effectiveCommentContext)) { const commentLevel = this.getCommentBaseIndentLevel(level, parentContainerType); this.printCommentToken(token.text, commentLevel, parentContainerType); } } else if (token.type === SqlPrintToken_1.SqlPrintTokenType.space) { this.handleSpaceToken(token, parentContainerType); } else if (token.type === SqlPrintToken_1.SqlPrintTokenType.commentNewline) { if (this.whenOneLine && parentContainerType === SqlPrintToken_1.SqlPrintTokenContainerType.MergeWhenClause) { return; } const commentLevel = this.getCommentBaseIndentLevel(level, parentContainerType); this.handleCommentNewlineToken(token, commentLevel); } else if (token.containerType === SqlPrintToken_1.SqlPrintTokenContainerType.CommonTable && this.withClauseStyle === 'cte-oneline') { this.handleCteOnelineToken(token, level); return; // Return early to avoid processing innerTokens } else if (this.shouldFormatContainerAsOneline(token, shouldIndentNested)) { this.handleOnelineToken(token, level); return; // Return early to avoid processing innerTokens } else if (!this.tryAppendInsertClauseTokenText(token.text, parentContainerType)) { this.linePrinter.appendText(token.text); } // append keyword tokens(not indented) if (token.keywordTokens && token.keywordTokens.length > 0) { for (let i = 0; i < token.keywordTokens.length; i++) { const keywordToken = token.keywordTokens[i]; this.appendToken(keywordToken, level, token.containerType, nextCaseContextDepth, indentParentActive, undefined, false); } } let innerLevel = level; let increasedIndent = false; const shouldIncreaseIndent = this.indentIncrementContainers.has(token.containerType) || shouldIndentNested; const delayIndentNewline = shouldIndentNested && token.containerType === SqlPrintToken_1.SqlPrintTokenContainerType.ParenExpression; const isAlterTableStatement = token.containerType === SqlPrintToken_1.SqlPrintTokenContainerType.AlterTableStatement; let deferAlterTableIndent = false; const alignExplainChild = this.shouldAlignExplainStatementChild(parentContainerType, token.containerType); if (alignExplainChild) { // Keep EXPLAIN target statements flush left so they render like standalone statements. if (!this.isOnelineMode() && current.text !== '') { this.linePrinter.appendNewline(level); } innerLevel = level; increasedIndent = false; } else if (!this.isOnelineMode() && shouldIncreaseIndent) { if (this.insideWithClause && this.withClauseStyle === 'full-oneline') { // Keep everything on one line for full-oneline WITH clauses. } else if (delayIndentNewline) { innerLevel = level + 1; increasedIndent = true; } else if (current.text !== '') { if (isAlterTableStatement) { // Delay the first line break so ALTER TABLE keeps the table name on the opening line. innerLevel = level + 1; increasedIndent = true; deferAlterTableIndent = true; } else { let targetIndentLevel = level + 1; if (token.containerType === SqlPrintToken_1.SqlPrintTokenContainerType.SetClause && parentContainerType === SqlPrintToken_1.SqlPrintTokenContainerType.MergeUpdateAction) { targetIndentLevel = level + 2; } if (this.shouldAlignCreateTableSelect(token.containerType, parentContainerType)) { innerLevel = level; increasedIndent = false; this.linePrinter.appendNewline(level); } else { innerLevel = targetIndentLevel; increasedIndent = true; this.linePrinter.appendNewline(innerLevel); } } } else if (token.containerType === SqlPrintToken_1.SqlPrintTokenContainerType.SetClause) { innerLevel = parentContainerType === SqlPrintToken_1.SqlPrintTokenContainerType.MergeUpdateAction ? level + 2 : level + 1; increasedIndent = true; current.level = innerLevel; } } const isMergeWhenClause = this.whenOneLine && token.containerType === SqlPrintToken_1.SqlPrintTokenContainerType.MergeWhenClause; let mergePredicateActive = isMergeWhenClause; let alterTableTableRendered = false; let alterTableIndentInserted = false; for (let i = leadingCommentCount; i < token.innerTokens.length; i++) { const child = token.innerTokens[i]; const nextChild = token.innerTokens[i + 1]; const previousEntry = this.findPreviousSignificantToken(token.innerTokens, i); const previousChild = previousEntry === null || previousEntry === void 0 ? void 0 : previousEntry.token; const priorEntry = previousEntry ? this.findPreviousSignificantToken(token.innerTokens, previousEntry.index) : undefined; const priorChild = priorEntry === null || priorEntry === void 0 ? void 0 : priorEntry.token; const childIsAction = this.isMergeActionContainer(child); const nextIsAction = this.isMergeActionContainer(nextChild); const inMergePredicate = mergePredicateActive && !childIsAction; if (isAlterTableStatement) { if (child.containerType === SqlPrintToken_1.SqlPrintTokenContainerType.QualifiedName) { // Track when the table name has been printed so we can defer indentation until after it. alterTableTableRendered = true; } else if (deferAlterTableIndent && alterTableTableRendered && !alterTableIndentInserted) { if (!this.isOnelineMode()) { this.linePrinter.appendNewline(innerLevel); } alterTableIndentInserted = true; deferAlterTableIndent = false; if (!this.isOnelineMode() && child.type === SqlPrintToken_1.SqlPrintTokenType.space) { // Drop the space token because we already emitted a newline. continue; } } } if (child.type === SqlPrintToken_1.SqlPrintTokenType.space) { if (this.shouldConvertSpaceToClauseBreak(token.containerType, nextChild)) { if (!this.isOnelineMode()) { // Use a dedicated indent resolver so clause breaks can shift indentation for nested blocks. const clauseBreakIndent = this.getClauseBreakIndentLevel(token.containerType, innerLevel); this.linePrinter.appendNewline(clauseBreakIndent); } if (isMergeWhenClause && nextIsAction) { mergePredicateActive = false; } continue; } this.handleSpaceToken(child, token.containerType, nextChild, previousChild, priorChild); continue; } const previousChildWasOpenParen = (previousChild === null || previousChild === void 0 ? void 0 : previousChild.type) === SqlPrintToken_1.SqlPrintTokenType.parenthesis && previousChild.text.trim() === '('; const childIndentParentActive = token.containerType === SqlPrintToken_1.SqlPrintTokenContainerType.ParenExpression ? shouldIndentNested : indentParentActive; if (inMergePredicate) { this.mergeWhenPredicateDepth++; } const childCommentContext = child.containerType === SqlPrintToken_1.SqlPrintTokenContainerType.CommentBlock ? { position: 'inline', isTopLevelContainer: containerIsTopLevel } : undefined; this.appendToken(child, innerLevel, token.containerType, nextCaseContextDepth, childIndentParentActive, childCommentContext, previousChildWasOpenParen); if (inMergePredicate) { this.mergeWhenPredicateDepth--; } if (childIsAction && isMergeWhenClause) { mergePredicateActive = false; } } if (this.smartCommentBlockBuilder && this.smartCommentBlockBuilder.mode === 'line') { this.flushSmartCommentBlockBuilder(); } // Exit WITH clause context when we finish processing WithClause container if (token.containerType === SqlPrintToken_1.SqlPrintTokenContainerType.WithClause && this.withClauseStyle === 'full-oneline') { this.insideWithClause = false; // Add newline after WITH clause to separate it from main SELECT this.linePrinter.appendNewline(level); return; // Return early to avoid additional newline below } // indent level down if (increasedIndent && shouldIncreaseIndent && !(this.insideWithClause && this.withClauseStyle === 'full-oneline') && !delayIndentNewline) { this.linePrinter.appendNewline(level); } } shouldAlignExplainStatementChild(parentType, childType) { if (parentType !== SqlPrintToken_1.SqlPrintTokenContainerType.ExplainStatement) { return false; } switch (childType) { case SqlPrintToken_1.SqlPrintTokenContainerType.SimpleSelectQuery: case SqlPrintToken_1.SqlPrintTokenContainerType.InsertQuery: case SqlPrintToken_1.SqlPrintTokenContainerType.UpdateQuery: case SqlPrintToken_1.SqlPrintTokenContainerType.DeleteQuery: case SqlPrintToken_1.SqlPrintTokenContainerType.MergeQuery: return true; default: return false; } } isCaseContext(containerType) { switch (containerType) { case SqlPrintToken_1.SqlPrintTokenContainerType.CaseExpression: case SqlPrintToken_1.SqlPrintTokenContainerType.CaseKeyValuePair: case SqlPrintToken_1.SqlPrintTokenContainerType.CaseThenValue: case SqlPrintToken_1.SqlPrintTokenContainerType.CaseElseValue: case SqlPrintToken_1.SqlPrintTokenContainerType.SwitchCaseArgument: return true; default: return false; } } /** * Determines if a token should be skipped during printing. * Tokens are skipped if they have no content and no inner tokens, * except for special token types that have semantic meaning despite empty text. */ shouldSkipToken(token) { // Special tokens with semantic meaning should never be skipped if (token.type === SqlPrintToken_1.SqlPrintTokenType.commentNewline) { return false; } // Skip tokens that have no content and no children return (!token.innerTokens || token.innerTokens.length === 0) && token.text === ''; } applyKeywordCase(text) { if (this.keywordCase === 'upper') { return text.toUpperCase(); } else if (this.keywordCase === 'lower') { return text.toLowerCase(); } return text; } handleKeywordToken(token, level, parentContainerType, caseContextDepth = 0) { const lower = token.text.toLowerCase(); if (lower === 'and' && (this.andBreak !== 'none' || (this.whenOneLine && parentContainerType === SqlPrintToken_1.SqlPrintTokenContainerType.MergeWhenClause))) { this.handleAndOperatorToken(token, level, parentContainerType, caseContextDepth); return; } else if (lower === 'or' && (this.orBreak !== 'none' || (this.whenOneLine && parentContainerType === SqlPrintToken_1.SqlPrintTokenContainerType.MergeWhenClause))) { this.handleOrOperatorToken(token, level, parentContainerType, caseContextDepth); return; } const text = this.applyKeywordCase(token.text); if (caseContextDepth > 0) { this.linePrinter.appendText(text); return; } this.ensureSpaceBeforeKeyword(); this.linePrinter.appendText(text); } ensureSpaceBeforeKeyword() { const currentLine = this.linePrinter.getCurrentLine(); if (currentLine.text === '') { return; } const lastChar = currentLine.text[currentLine.text.length - 1]; if (lastChar === '(') { return; } this.ensureTrailingSpace(); } ensureTrailingSpace() { const currentLine = this.linePrinter.getCurrentLine(); if (currentLine.text === '') { return; } if (!currentLine.text.endsWith(' ')) { currentLine.text += ' '; } currentLine.text = currentLine.text.replace(/\s+$/, ' '); } /** * Normalizes INSERT column list token text when one-line formatting is active. */ tryAppendInsertClauseTokenText(text, parentContainerType) { const currentLineText = this.linePrinter.getCurrentLine().text; const result = this.onelineHelper.formatInsertClauseToken(text, parentContainerType, currentLineText, () => this.ensureTrailingSpace()); if (!result.handled) { return false; } if (result.text) { this.linePrinter.appendText(result.text); } return true; } handleCommaToken(token, level, parentContainerType) { const text = token.text; const isWithinWithClause = parentContainerType === SqlPrintToken_1.SqlPrintTokenContainerType.WithClause; let effectiveCommaBreak = this.onelineHelper.resolveCommaBreak(parentContainerType, this.commaBreak, this.cteCommaBreak, this.valuesCommaBreak); if (parentContainerType === SqlPrintToken_1.SqlPrintTokenContainerType.SetClause) { effectiveCommaBreak = 'before'; } // Skip comma newlines when inside WITH clause with full-oneline style if (this.insideWithClause && this.withClauseStyle === 'full-oneline') { this.linePrinter.appendText(text); } // Special handling for commas in WithClause when withClauseStyle is 'cte-oneline' else if (this.withClauseStyle === 'cte-oneline' && isWithinWithClause) { this.linePrinter.appendText(text); this.linePrinter.appendNewline(level); } else if (effectiveCommaBreak === 'before') { const previousCommaBreak = this.linePrinter.commaBreak; if (previousCommaBreak !== 'before') { this.linePrinter.commaBreak = 'before'; } if (!(this.insideWithClause && this.withClauseStyle === 'full-oneline')) { this.linePrinter.appendNewline(level); if (this.newline === ' ') { // Remove the spacer introduced by space newlines so commas attach directly to the preceding token. this.linePrinter.trimTrailingWhitespaceFromPreviousLine(); } if (parentContainerType === SqlPrintToken_1.SqlPrintTokenContainerType.InsertClause) { // Align comma-prefixed column entries under the INSERT column indentation. this.linePrinter.getCurrentLine().level = level + 1; } } this.linePrinter.appendText(text); if (previousCommaBreak !== 'before') { this.linePrinter.commaBreak = previousCommaBreak; } } else if (effectiveCommaBreak === 'after') { const previousCommaBreak = this.linePrinter.commaBreak; if (previousCommaBreak !== 'after') { this.linePrinter.commaBreak = 'after'; } if (!(this.insideWithClause && this.withClauseStyle === 'full-oneline')) { this.linePrinter.appendNewline(level); } this.linePrinter.appendText(text); if (!(this.insideWithClause && this.withClauseStyle === 'full-oneline')) { this.linePrinter.appendNewline(level); } if (previousCommaBreak !== 'after') { this.linePrinter.commaBreak = previousCommaBreak; } } else if (effectiveCommaBreak === 'none') { const previousCommaBreak = this.linePrinter.commaBreak; if (previousCommaBreak !== 'none') { this.linePrinter.commaBreak = 'none'; } this.linePrinter.appendText(text); if (this.onelineHelper.isInsertClauseOneline(parentContainerType)) { this.ensureTrailingSpace(); } if (previousCommaBreak !== 'none') { this.linePrinter.commaBreak = previousCommaBreak; } } else { this.linePrinter.appendText(text); } } handleAndOperatorToken(token, level, parentContainerType, caseContextDepth = 0) { const text = this.applyKeywordCase(token.text); if (caseContextDepth > 0) { this.linePrinter.appendText(text); return; } if (this.whenOneLine && (parentContainerType === SqlPrintToken_1.SqlPrintTokenContainerType.MergeWhenClause || this.mergeWhenPredicateDepth > 0)) { this.linePrinter.appendText(text); return; } if (this.andBreak === 'before') { // Skip newline when inside WITH clause with full-oneline style if (!(this.insideWithClause && this.withClauseStyle === 'full-oneline')) { this.linePrinter.appendNewline(level); } this.linePrinter.appendText(text); } else if (this.andBreak === 'after') { this.linePrinter.appendText(text); // Skip newline when inside WITH clause with full-oneline style if (!(this.insideWithClause && this.withClauseStyle === 'full-oneline')) { this.linePrinter.appendNewline(level); } } else { this.linePrinter.appendText(text); } } handleParenthesisToken(token, level, indentParentActive, parentContainerType) { if (token.text === '(') { this.linePrinter.appendText(token.text); if ((parentContainerType === SqlPrintToken_1.SqlPrintTokenContainerType.InsertClause || parentContainerType === SqlPrintToken_1.SqlPrintTokenContainerType.MergeInsertAction) && this.insertColumnsOneLine) { return; } if (!this.isOnelineMode()) { if (this.shouldBreakAfterOpeningParen(parentContainerType)) { this.linePrinter.appendNewline(level + 1); } else if (indentParentActive && !(this.insideWithClause && this.withClauseStyle === 'full-oneline')) { this.linePrinter.appendNewline(level); } } return; } if (token.text === ')' && !this.isOnelineMode()) { if (this.shouldBreakBeforeClosingParen(parentContainerType)) { this.linePrinter.appendNewline(Math.max(level, 0)); this.linePrinter.appendText(token.text); return; } if (indentParentActive && !(this.insideWithClause && this.withClauseStyle === 'full-oneline')) { const closingLevel = Math.max(level - 1, 0); this.linePrinter.appendNewline(closingLevel); } } this.linePrinter.appendText(token.text); } handleOrOperatorToken(token, level, parentContainerType, caseContextDepth = 0) { const text = this.applyKeywordCase(token.text); // Leave OR untouched inside CASE branches to preserve inline evaluation order. if (caseContextDepth > 0) { this.linePrinter.appendText(text); return; } if (this.whenOneLine && (parentContainerType === SqlPrintToken_1.SqlPrintTokenContainerType.MergeWhenClause || this.mergeWhenPredicateDepth > 0)) { this.linePrinter.appendText(text); return; } if (this.orBreak === 'before') { // Insert a newline before OR unless WITH full-oneline mode suppresses breaks. if (!(this.insideWithClause && this.withClauseStyle === 'full-oneline')) { this.linePrinter.appendNewline(level); } this.linePrinter.appendText(text); } else if (this.orBreak === 'after') { this.linePrinter.appendText(text); // Break after OR when multi-line formatting is active. if (!(this.insideWithClause && this.withClauseStyle === 'full-oneline')) { this.linePrinter.appendNewline(level); } } else { this.linePrinter.appendText(text); } } /** * Decide whether a parentheses group should increase indentation when inside nested structures. * We only expand groups that contain further parentheses so simple comparisons stay compact. */ shouldIndentNestedParentheses(token, previousSiblingWasOpenParen = false) { if (!this.indentNestedParentheses) { return false; } if (token.containerType !== SqlPrintToken_1.SqlPrintTokenContainerType.ParenExpression) { return false; } // Look for nested parentheses containers. If present, indent to highlight grouping. return previousSiblingWasOpenParen || token.innerTokens.some((child) => this.containsParenExpression(child)); } /** * Recursively inspect descendants to find additional parentheses groups. * Helps detect complex boolean groups like ((A) OR (B) OR (C)). */ containsParenExpression(token) { if (token.containerType === SqlPrintToken_1.SqlPrintTokenContainerType.ParenExpression) { return true; } for (const child of token.innerTokens) { if (this.containsParenExpression(child)) { return true; } } return false; } handleJoinClauseToken(token, level) { const text = this.applyKeywordCase(token.text); // before join clause, add newline when multiline formatting is allowed if (this.onelineHelper.shouldInsertJoinNewline(this.insideWithClause)) { this.linePrinter.appendNewline(level); } this.linePrinter.appendText(text); } /** * Decides whether the current container should collapse into a single line. */ shouldFormatContainerAsOneline(token, shouldIndentNested) { return this.onelineHelper.shouldFormatContainer(token, shouldIndentNested); } /** * Detects an INSERT column list that must stay on a single line. */ isInsertClauseOneline(parentContainerType) { return this.onelineHelper.isInsertClauseOneline(parentContainerType); } /** * Handles space tokens with context-aware filtering. * Skips spaces in CommentBlocks when in specific CTE modes to prevent duplication. */ handleSpaceToken(token, parentContainerType, nextToken, previousToken, priorToken) { if (this.smartCommentBlockBuilder && this.smartCommentBlockBuilder.mode === 'line') { this.flushSmartCommentBlockBuilder(); } const currentLineText = this.linePrinter.getCurrentLine().text; if (this.onelineHelper.shouldSkipInsertClauseSpace(parentContainerType, nextToken, currentLineText)) { return; } if (this.onelineHelper.shouldSkipCommentBlockSpace(parentContainerType, this.insideWithClause)) { const currentLine = this.linePrinter.getCurrentLine(); if (currentLine.text !== '' && !currentLine.text.endsWith(' ')) { this.linePrinter.appendText(' '); } return; } // Skip redundant spaces before structural parentheses in CREATE TABLE DDL. if (this.shouldSkipSpaceBeforeParenthesis(parentContainerType, nextToken, previousToken, priorToken)) { return; } this.linePrinter.appendText(token.text); } findPreviousSignificantToken(tokens, index) { for (let i = index - 1; i >= 0; i--) { const candidate = tokens[i]; if (candidate.type === SqlPrintToken_1.SqlPrintTokenType.space || candidate.type === SqlPrintToken_1.SqlPrintTokenType.commentNewline) { continue; } if (candidate.type === SqlPrintToken_1.SqlPrintTokenType.comment && !this.rendersInlineComments()) { continue; } return { token: candidate, index: i }; } return undefined; } shouldSkipSpaceBeforeParenthesis(parentContainerType, nextToken, previousToken, priorToken) { if (!nextToken || nextToken.type !== SqlPrintToken_1.SqlPrintTokenType.parenthesis || nextToken.text !== '(') { return false; } if (!parentContainerType || !this.isCreateTableSpacingContext(parentContainerType)) { return false; } if (!previousToken) { return false; } if (this.isCreateTableNameToken(previousToken, parentContainerType)) { return true; } if (this.isCreateTableConstraintKeyword(previousToken, parentContainerType)) { return true; } if (priorToken && this.isCreateTableConstraintKeyword(priorToken, parentContainerType)) { if (this.isIdentifierAttachedToConstraint(previousToken, priorToken, parentContainerType)) { return true; } } return false; } shouldAlignCreateTableSelect(containerType, parentContainerType) { return containerType === SqlPrintToken_1.SqlPrintTokenContainerType.SimpleSelectQuery && parentContainerType === SqlPrintToken_1.SqlPrintTokenContainerType.CreateTableQuery; } isCreateTableSpacingContext(parentContainerType) { switch (parentContainerType) { case SqlPrintToken_1.SqlPrintTokenContainerType.CreateTableQuery: case SqlPrintToken_1.SqlPrintTokenContainerType.CreateTableDefinition: case SqlPrintToken_1.SqlPrintTokenContainerType.TableConstraintDefinition: case SqlPrintToken_1.SqlPrintTokenContainerType.ColumnConstraintDefinition: case SqlPrintToken_1.SqlPrintTokenContainerType.ReferenceDefinition: return true; default: return false; } } isCreateTableNameToken(previousToken, parentContainerType) { if (parentContainerType !== SqlPrintToken_1.SqlPrintTokenContainerType.CreateTableQuery) { return false; } return previousToken.containerType === SqlPrintToken_1.SqlPrintTokenContainerType.QualifiedName; } isCreateTableConstraintKeyword(token, parentContainerType) { if (token.type !== SqlPrintToken_1.SqlPrintTokenType.keyword) { return false; } const text = token.text.toLowerCase(); if (parentContainerType === SqlPrintToken_1.SqlPrintTokenContainerType.ReferenceDefinition) { return CREATE_TABLE_PAREN_KEYWORDS_WITH_IDENTIFIER.has(text); } if (CREATE_TABLE_SINGLE_PAREN_KEYWORDS.has(text)) { return true; } if (CREATE_TABLE_MULTI_PAREN_KEYWORDS.has(text)) { return true; } return false; } isIdentifierAttachedToConstraint(token, keywordToken, parentContainerType) { if (!token) { return false; } if (parentContainerType === SqlPrintToken_1.SqlPrintTokenContainerType.ReferenceDefinition) { return token.containerType === SqlPrintToken_1.SqlPrintTokenContainerType.QualifiedName && CREATE_TABLE_PAREN_KEYWORDS_WITH_IDENTIFIER.has(keywordToken.text.toLowerCase()); } if (parentContainerType === SqlPrintToken_1.SqlPrintTokenContainerType.TableConstraintDefinition || parentContainerType === SqlPrintToken_1.SqlPrintTokenContainerType.ColumnConstraintDefinition) { const normalized = keywordToken.text.toLowerCase(); if (CREATE_TABLE_SINGLE_PAREN_KEYWORDS.has(normalized) || CREATE_TABLE_MULTI_PAREN_KEYWORDS.has(normalized)) { return token.containerType === SqlPrintToken_1.SqlPrintTokenContainerType.IdentifierString; } } return false; } printCommentToken(text, level, parentContainerType) { const trimmed = text.trim(); if (!trimmed) { return; } if (this.commentStyle === 'smart' && parentContainerType === SqlPrintToken_1.SqlPrintTokenContainerType.CommentBlock) { if (this.handleSmartCommentBlockToken(text, trimmed, level)) { return; } } if (this.commentStyle === 'smart') { const normalized = this.normalizeCommentForSmart(trimmed); if (normalized.lines.length > 1 || normalized.forceBlock) { const blockText = this.buildBlockComment(normalized.lines, level); this.linePrinter.appendText(blockText); } else { const content = normalized.lines[0]; const lineText = content ? `-- ${content}` : '--'; if (parentContainerType === SqlPrintToken_1.SqlPrintTokenContainerType.CommentBlock) { this.linePrinter.appendText(lineText); this.pendingLineCommentBreak = this.resolveCommentIndentLevel(level, parentContainerType); } else { this.linePrinter.appendText(lineText); const effectiveLevel = this.resolveCommentIndentLevel(level, parentContainerType); this.linePrinter.appendNewline(effectiveLevel); } } } else { if (trimmed.startsWith('/*') && trimmed.endsWith('*/')) { if (/\r?\n/.test(trimmed)) { // Keep multi-line block comments intact by normalizing line endings once. const newlineReplacement = this.isOnelineMode() ? ' ' : (typeof this.newline === 'string' ? this.newline : '\n'); const normalized = trimmed.replace(/\r?\n/g, newlineReplacement); this.linePrinter.appendText(normalized); } else { this.linePrinter.appendText(trimmed); } } else { this.linePrinter.appendText(trimmed); } if (trimmed.startsWith('--')) { if (parentContainerType === SqlPrintToken_1.SqlPrintTokenContainerType.CommentBlock) { this.pendingLineCommentBreak = this.resolveCommentIndentLevel(level, parentContainerType); } else { const effectiveLevel = this.resolveCommentIndentLevel(level, parentContainerType); this.linePrinter.appendNewline(effectiveLevel); } } } } handleSmartCommentBlockToken(raw, trimmed, level) { if (!this.smartCommentBlockBuilder) { if (trimmed === '/*') { // Begin assembling a multi-line block comment that is emitted as split tokens this.smartCommentBlockBuilder = { lines: [], level, mode: 'block' }; return true; } const lineContent = this.extractLineCommentContent(trimmed); if (lineContent !== null) { this.smartCommentBlockBuilder = { lines: [lineContent], level, mode: 'line', }; return true; } return false; } if (this.smartCommentBlockBuilder.mode === 'block') { if (trimmed === '*/') { const { lines, level: blockLevel } = this.smartCommentBlockBuilder; const blockText = this.buildBlockComment(lines, blockLevel); this.linePrinter.appendText(blockText); this.pendingLineCommentBreak = blockLevel; th