UNPKG

rawsql-ts

Version:

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

333 lines 15.7 kB
import { SqlPrintTokenType, SqlPrintTokenContainerType } from "../models/SqlPrintToken"; import { LinePrinter } from "./LinePrinter"; /** * 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); */ export class SqlPrinter { /** * @param options Optional style settings for pretty printing */ constructor(options) { var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k; /** Track whether we are currently inside a WITH clause for full-oneline formatting */ this.insideWithClause = false; this.indentChar = (_a = options === null || options === void 0 ? void 0 : options.indentChar) !== null && _a !== void 0 ? _a : ''; this.indentSize = (_b = options === null || options === void 0 ? void 0 : options.indentSize) !== null && _b !== void 0 ? _b : 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 = (_c = options === null || options === void 0 ? void 0 : options.newline) !== null && _c !== void 0 ? _c : ' '; this.commaBreak = (_d = options === null || options === void 0 ? void 0 : options.commaBreak) !== null && _d !== void 0 ? _d : 'none'; this.andBreak = (_e = options === null || options === void 0 ? void 0 : options.andBreak) !== null && _e !== void 0 ? _e : 'none'; this.keywordCase = (_f = options === null || options === void 0 ? void 0 : options.keywordCase) !== null && _f !== void 0 ? _f : 'none'; this.exportComment = (_g = options === null || options === void 0 ? void 0 : options.exportComment) !== null && _g !== void 0 ? _g : false; this.strictCommentPlacement = (_h = options === null || options === void 0 ? void 0 : options.strictCommentPlacement) !== null && _h !== void 0 ? _h : false; this.withClauseStyle = (_j = options === null || options === void 0 ? void 0 : options.withClauseStyle) !== null && _j !== void 0 ? _j : 'standard'; this.linePrinter = new LinePrinter(this.indentChar, this.indentSize, this.newline); // Initialize this.indentIncrementContainers = new Set((_k = options === null || options === void 0 ? void 0 : options.indentIncrementContainerTypes) !== null && _k !== void 0 ? _k : [ SqlPrintTokenContainerType.SelectClause, SqlPrintTokenContainerType.FromClause, SqlPrintTokenContainerType.WhereClause, SqlPrintTokenContainerType.GroupByClause, SqlPrintTokenContainerType.HavingClause, SqlPrintTokenContainerType.WindowFrameExpression, SqlPrintTokenContainerType.PartitionByClause, SqlPrintTokenContainerType.OrderByClause, SqlPrintTokenContainerType.WindowClause, SqlPrintTokenContainerType.LimitClause, SqlPrintTokenContainerType.OffsetClause, SqlPrintTokenContainerType.SubQuerySource, SqlPrintTokenContainerType.BinarySelectQueryOperator, SqlPrintTokenContainerType.Values, SqlPrintTokenContainerType.WithClause, SqlPrintTokenContainerType.SwitchCaseArgument, SqlPrintTokenContainerType.CaseKeyValuePair, SqlPrintTokenContainerType.CaseThenValue, SqlPrintTokenContainerType.ElseClause, SqlPrintTokenContainerType.CaseElseValue, SqlPrintTokenContainerType.SimpleSelectQuery // 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(this.indentChar, this.indentSize, this.newline); this.insideWithClause = false; // Reset WITH clause context if (this.linePrinter.lines.length > 0 && level !== this.linePrinter.lines[0].level) { this.linePrinter.lines[0].level = level; } this.appendToken(token, level, undefined); return this.linePrinter.print(); } appendToken(token, level, parentContainerType) { // Track WITH clause context for full-oneline formatting const wasInsideWithClause = this.insideWithClause; if (token.containerType === SqlPrintTokenContainerType.WithClause && this.withClauseStyle === 'full-oneline') { this.insideWithClause = true; } if (this.shouldSkipToken(token)) { return; } const current = this.linePrinter.getCurrentLine(); // Handle different token types if (token.type === SqlPrintTokenType.keyword) { this.handleKeywordToken(token, level); } else if (token.type === SqlPrintTokenType.comma) { this.handleCommaToken(token, level, parentContainerType); } else if (token.type === SqlPrintTokenType.operator && token.text.toLowerCase() === 'and') { this.handleAndOperatorToken(token, level); } else if (token.containerType === "JoinClause") { this.handleJoinClauseToken(token, level); } else if (token.type === SqlPrintTokenType.comment) { // Handle comments as regular tokens - let the standard processing handle everything if (this.exportComment) { this.linePrinter.appendText(token.text); } } else if (token.type === SqlPrintTokenType.space) { this.handleSpaceToken(token, parentContainerType); } else if (token.type === SqlPrintTokenType.commentNewline) { this.handleCommentNewlineToken(token, level); } else if (token.containerType === SqlPrintTokenContainerType.CommonTable && this.withClauseStyle === 'cte-oneline') { this.handleCteOnelineToken(token, level); return; // Return early to avoid processing innerTokens } else { 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); } } let innerLevel = level; // indent level up if (!this.isOnelineMode() && current.text !== '' && this.indentIncrementContainers.has(token.containerType)) { // Skip newline for any container when inside WITH clause with full-oneline style if (!(this.insideWithClause && this.withClauseStyle === 'full-oneline')) { innerLevel++; this.linePrinter.appendNewline(innerLevel); } } for (let i = 0; i < token.innerTokens.length; i++) { const child = token.innerTokens[i]; this.appendToken(child, innerLevel, token.containerType); } // Exit WITH clause context when we finish processing WithClause container if (token.containerType === 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 (innerLevel !== level) { // Skip newline for any container when inside WITH clause with full-oneline style if (!(this.insideWithClause && this.withClauseStyle === 'full-oneline')) { this.linePrinter.appendNewline(level); } } } /** * 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 === 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) { const text = this.applyKeywordCase(token.text); this.linePrinter.appendText(text); } handleCommaToken(token, level, parentContainerType) { const text = token.text; // 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' && parentContainerType === SqlPrintTokenContainerType.WithClause) { this.linePrinter.appendText(text); this.linePrinter.appendNewline(level); } else if (this.commaBreak === '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.commaBreak === '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); } } handleAndOperatorToken(token, level) { const text = this.applyKeywordCase(token.text); 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); } } handleJoinClauseToken(token, level) { const text = this.applyKeywordCase(token.text); // before join clause, add newline (skip when inside WITH clause with full-oneline style) if (!(this.insideWithClause && this.withClauseStyle === 'full-oneline')) { this.linePrinter.appendNewline(level); } this.linePrinter.appendText(text); } /** * Handles space tokens with context-aware filtering. * Skips spaces in CommentBlocks when in specific CTE modes to prevent duplication. */ handleSpaceToken(token, parentContainerType) { if (this.shouldSkipCommentBlockSpace(parentContainerType)) { return; } this.linePrinter.appendText(token.text); } /** * Determines whether to skip space tokens in CommentBlocks. * Prevents duplicate spacing in CTE full-oneline mode only. */ shouldSkipCommentBlockSpace(parentContainerType) { return parentContainerType === SqlPrintTokenContainerType.CommentBlock && this.insideWithClause && this.withClauseStyle === 'full-oneline'; } /** * Handles commentNewline tokens with conditional newline behavior. * In multiline mode (newline !== ' '), adds a newline after comments. * In oneliner mode (newline === ' '), does nothing to keep comments on same line. * Skips newlines in CTE modes (full-oneline, cte-oneline) to maintain one-line format. */ handleCommentNewlineToken(token, level) { if (this.shouldSkipCommentNewline()) { return; } if (!this.isOnelineMode()) { this.linePrinter.appendNewline(level); } } /** * Determines whether to skip commentNewline tokens. * Skips in CTE modes to maintain one-line formatting. */ shouldSkipCommentNewline() { return (this.insideWithClause && this.withClauseStyle === 'full-oneline') || this.withClauseStyle === 'cte-oneline'; } /** * Determines if the printer is in oneliner mode. * Oneliner mode uses single spaces instead of actual newlines. */ isOnelineMode() { return this.newline === ' '; } /** * Handles CTE tokens with one-liner formatting. * Creates a nested SqlPrinter instance for proper CTE oneline formatting. */ handleCteOnelineToken(token, level) { const onelinePrinter = this.createCteOnelinePrinter(); const onelineResult = onelinePrinter.print(token, level); const cleanedResult = this.cleanDuplicateSpaces(onelineResult); this.linePrinter.appendText(cleanedResult); } /** * Creates a SqlPrinter instance configured for CTE oneline formatting. */ createCteOnelinePrinter() { return new SqlPrinter({ indentChar: '', indentSize: 0, newline: ' ', commaBreak: this.commaBreak, andBreak: this.andBreak, keywordCase: this.keywordCase, exportComment: this.exportComment, strictCommentPlacement: this.strictCommentPlacement, withClauseStyle: 'standard', // Prevent recursive processing }); } /** * Removes duplicate consecutive spaces while preserving single spaces. * Simple and safe space normalization for CTE oneline mode. */ cleanDuplicateSpaces(text) { return text.replace(/\s{2,}/g, ' '); } } //# sourceMappingURL=SqlPrinter.js.map