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