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
JavaScript
"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