diff --git a/package.json b/package.json index bf1d2f7dea..65673220bf 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,8 @@ }, "dependencies": { "argparse": "^2.0.1", - "nearley": "^2.20.1" + "nearley": "^2.20.1", + "picocolors": "^1.1.1" }, "devDependencies": { "@babel/cli": "^7.10.4", diff --git a/src/FormatOptions.ts b/src/FormatOptions.ts index 4af41184b0..2814094eb2 100644 --- a/src/FormatOptions.ts +++ b/src/FormatOptions.ts @@ -11,6 +11,17 @@ export type FunctionCase = KeywordCase; export type LogicalOperatorNewline = 'before' | 'after'; +export type ColorKeys = + | 'keyword' + | 'operator' + | 'comment' + | 'string' + | 'number' + | 'function' + | 'parenthesis' + | 'identifier' + | 'dataType'; + export interface FormatOptions { tabWidth: number; useTabs: boolean; @@ -26,4 +37,8 @@ export interface FormatOptions { newlineBeforeSemicolon: boolean; params?: ParamItems | string[]; paramTypes?: ParamTypes; + compactParenthesis: boolean; + maxLengthInParenthesis?: number; + colors: boolean; + colorsMap: Record string>; } diff --git a/src/formatter/BlockLayout.ts b/src/formatter/BlockLayout.ts new file mode 100644 index 0000000000..542503d203 --- /dev/null +++ b/src/formatter/BlockLayout.ts @@ -0,0 +1,68 @@ +import { stripColors } from '../utils.js'; +import Indentation from './Indentation.js'; +import Layout, { WS } from './Layout.js'; + +export default class BlockLayout extends Layout { + private line = 0; + private length = 0; + + // internal buffer for service tokens + private buffer: (WS | string)[] = []; + + constructor( + indentation: Indentation, + private expressionWidth: number, + private colorized: boolean + ) { + super(indentation); + } + + public add(...items: (WS | string)[]) { + for (const item of items) { + // We add a comma as a service token so that the transfer of a comma to + // a new line is not a matter of chance, and it always + // remains on the same line as the element before it. + if (typeof item !== 'string' || item === ',') { + this.buffer.push(item); + continue; + } + + const forceInsert = this.length === 0; + const insertCurrLineBuff = this.buffer.reduce( + (ret, service_item) => this.addLengthCurrentLine(service_item) && ret, + true + ); + const insertCurrLine = this.addLengthCurrentLine(item) && insertCurrLineBuff; + + if (insertCurrLine || forceInsert) { + super.add(...this.buffer, item); + } else { + super.add(...this.buffer, WS.NEWLINE, WS.INDENT, item); + } + + this.buffer = []; + + if (!insertCurrLine) { + this.line++; + this.length = 0; + } + } + } + + private addLengthCurrentLine(item: WS | string) { + item = this.colorized && typeof item === 'string' ? stripColors(item) : item; + + if (typeof item === 'string') { + this.length += item.length; + return this.length <= this.expressionWidth; + } + + if (item === WS.NO_NEWLINE || item === WS.NO_SPACE) { + this.length--; + return true; + } + + this.length++; + return this.length <= this.expressionWidth; + } +} diff --git a/src/formatter/ExpressionFormatter.ts b/src/formatter/ExpressionFormatter.ts index a306e22fd1..1fcf625093 100644 --- a/src/formatter/ExpressionFormatter.ts +++ b/src/formatter/ExpressionFormatter.ts @@ -1,5 +1,7 @@ -import { FormatOptions } from '../FormatOptions.js'; +import * as regex from '../lexer/regexFactory.js'; +import { ColorKeys, FormatOptions } from '../FormatOptions.js'; import { equalizeWhitespace, isMultiline, last } from '../utils.js'; +import { limitNodesByType, limitWithComment } from './LimitNodes.js'; import Params from './Params.js'; import { isTabularStyle } from './config.js'; @@ -36,6 +38,7 @@ import { import Layout, { WS } from './Layout.js'; import toTabularFormat, { isTabularToken } from './tabularStyle.js'; import InlineLayout, { InlineLayoutError } from './InlineLayout.js'; +import BlockLayout from './BlockLayout.js'; interface ExpressionFormatterParams { cfg: FormatOptions; @@ -156,7 +159,7 @@ export default class ExpressionFormatter { private formatParameterizedDataType(node: ParameterizedDataTypeNode) { this.withComments(node.dataType, () => { - this.layout.add(this.showDataType(node.dataType)); + this.layout.add(this.colorize('dataType', this.showDataType(node.dataType))); }); this.formatNode(node.parenthesis); } @@ -166,13 +169,13 @@ export default class ExpressionFormatter { switch (node.array.type) { case NodeType.data_type: - formattedArray = this.showDataType(node.array); + formattedArray = this.colorize('dataType', this.showDataType(node.array)); break; case NodeType.keyword: - formattedArray = this.showKw(node.array); + formattedArray = this.colorize('keyword', this.showKw(node.array)); break; default: - formattedArray = this.showIdentifier(node.array); + formattedArray = this.colorize('identifier', this.showIdentifier(node.array)); break; } @@ -190,27 +193,66 @@ export default class ExpressionFormatter { } private formatParenthesis(node: ParenthesisNode) { - const inlineLayout = this.formatInlineExpression(node.children); + const maxLength = this.cfg.maxLengthInParenthesis; + const limit = limitNodesByType( + node.children, + NodeType.parameter, + limitWithComment, + 0, + maxLength + ); + + this.params.addPositionalParameterIndex(limit.skippedFront); + + const openParen = this.colorize('parenthesis', node.openParen); + const closeParen = this.colorize('parenthesis', node.closeParen); + + if (this.cfg.compactParenthesis) { + const singleIndent = this.layout.indentation.getSingleIndent(); + // Because the bracket is on the same line, + // we have to align the text one character less than necessary. + // I don't know how else to achieve the same effect with API library. + const partIndent = singleIndent.length > 1 ? singleIndent.slice(0, -1) : singleIndent; - if (inlineLayout) { - this.layout.add(node.openParen); - this.layout.add(...inlineLayout.getLayoutItems()); - this.layout.add(WS.NO_SPACE, node.closeParen, WS.SPACE); + this.layout.add(openParen, partIndent); + this.layout.indentation.increaseBlockLevel(); + + const elements = this.formatBlockExpression(limit.nodes).getLayoutItems(); + const hasNewline = elements.some(item => item === WS.NEWLINE); + this.layout.add(...elements); + + this.layout.indentation.decreaseBlockLevel(); + + if (hasNewline) { + this.layout.add(WS.NEWLINE, WS.INDENT); + } + + this.layout.add(closeParen, WS.SPACE); } else { - this.layout.add(node.openParen, WS.NEWLINE); + const inlineLayout = this.formatInlineExpression(limit.nodes); - if (isTabularStyle(this.cfg)) { - this.layout.add(WS.INDENT); - this.layout = this.formatSubExpression(node.children); + if (inlineLayout) { + this.layout.add(openParen); + this.layout.add(...inlineLayout.getLayoutItems()); + this.layout.add(WS.NO_SPACE, closeParen, WS.SPACE); } else { - this.layout.indentation.increaseBlockLevel(); - this.layout.add(WS.INDENT); - this.layout = this.formatSubExpression(node.children); - this.layout.indentation.decreaseBlockLevel(); - } + this.layout.add(openParen, WS.NEWLINE); - this.layout.add(WS.NEWLINE, WS.INDENT, node.closeParen, WS.SPACE); + if (isTabularStyle(this.cfg)) { + this.layout.add(WS.INDENT); + this.layout = this.formatSubExpression(limit.nodes); + } else { + this.layout.indentation.increaseBlockLevel(); + this.layout.add(WS.INDENT); + this.layout = this.formatSubExpression(limit.nodes); + this.layout.indentation.decreaseBlockLevel(); + } + + this.layout.add(WS.NEWLINE, WS.INDENT, closeParen, WS.SPACE); + } } + + this.params.addPositionalParameterIndex(limit.skippedBack); } private formatBetweenPredicate(node: BetweenPredicateNode) { @@ -318,15 +360,20 @@ export default class ExpressionFormatter { } private formatLiteral(node: LiteralNode) { - this.layout.add(node.text, WS.SPACE); + const isString = /^("|')/.test(node.text) && 'string'; + this.layout.add(this.colorize(isString || 'number', node.text), WS.SPACE); } private formatIdentifier(node: IdentifierNode) { - this.layout.add(this.showIdentifier(node), WS.SPACE); + this.layout.add(this.colorize('identifier', this.showIdentifier(node)), WS.SPACE); } private formatParameter(node: ParameterNode) { - this.layout.add(this.params.get(node), WS.SPACE); + const param = this.params.get(node); + const isString = /^("|')/.test(param) && 'string'; + const isNumber = regex.number(true).test(param) && 'number'; + + this.layout.add(this.colorize(isString || isNumber || undefined, param), WS.SPACE); } private formatOperator({ text }: OperatorNode) { @@ -367,24 +414,26 @@ export default class ExpressionFormatter { } private formatLineComment(node: LineCommentNode) { + const comment = this.colorize('comment', node.text); + if (isMultiline(node.precedingWhitespace || '')) { - this.layout.add(WS.NEWLINE, WS.INDENT, node.text, WS.MANDATORY_NEWLINE, WS.INDENT); + this.layout.add(WS.NEWLINE, WS.INDENT, comment, WS.MANDATORY_NEWLINE, WS.INDENT); } else if (this.layout.getLayoutItems().length > 0) { - this.layout.add(WS.NO_NEWLINE, WS.SPACE, node.text, WS.MANDATORY_NEWLINE, WS.INDENT); + this.layout.add(WS.NO_NEWLINE, WS.SPACE, comment, WS.MANDATORY_NEWLINE, WS.INDENT); } else { // comment is the first item in code - no need to add preceding spaces - this.layout.add(node.text, WS.MANDATORY_NEWLINE, WS.INDENT); + this.layout.add(comment, WS.MANDATORY_NEWLINE, WS.INDENT); } } private formatBlockComment(node: BlockCommentNode | DisableCommentNode) { if (node.type === NodeType.block_comment && this.isMultilineBlockComment(node)) { this.splitBlockComment(node.text).forEach(line => { - this.layout.add(WS.NEWLINE, WS.INDENT, line); + this.layout.add(WS.NEWLINE, WS.INDENT, this.colorize('comment', line)); }); this.layout.add(WS.NEWLINE, WS.INDENT); } else { - this.layout.add(node.text, WS.SPACE); + this.layout.add(this.colorize('comment', node.text), WS.SPACE); } } @@ -462,7 +511,7 @@ export default class ExpressionFormatter { cfg: this.cfg, dialectCfg: this.dialectCfg, params: this.params, - layout: new InlineLayout(this.cfg.expressionWidth), + layout: new InlineLayout(this.cfg.expressionWidth, this.cfg.colors), inline: true, }).format(nodes); } catch (e) { @@ -480,6 +529,24 @@ export default class ExpressionFormatter { } } + private formatBlockExpression(nodes: AstNode[]): Layout { + return new ExpressionFormatter({ + cfg: this.cfg, + dialectCfg: this.dialectCfg, + params: this.params, + layout: new BlockLayout(this.layout.indentation, this.cfg.expressionWidth, this.cfg.colors), + inline: true, + }).format(nodes); + } + + private colorize(colorKey: ColorKeys | undefined, text: string): string { + if (!this.cfg.colors || !colorKey) { + return text; + } + + return this.cfg.colorsMap[colorKey](text); + } + private formatKeywordNode(node: KeywordNode): void { switch (node.tokenType) { case TokenType.RESERVED_JOIN: @@ -524,14 +591,17 @@ export default class ExpressionFormatter { } private formatDataType(node: DataTypeNode) { - this.layout.add(this.showDataType(node), WS.SPACE); + this.layout.add(this.colorize('dataType', this.showDataType(node)), WS.SPACE); } private showKw(node: KeywordNode): string { if (isTabularToken(node.tokenType)) { - return toTabularFormat(this.showNonTabularKw(node), this.cfg.indentStyle); + return this.colorize( + 'keyword', + toTabularFormat(this.showNonTabularKw(node), this.cfg.indentStyle) + ); } else { - return this.showNonTabularKw(node); + return this.colorize('keyword', this.showNonTabularKw(node)); } } @@ -549,9 +619,12 @@ export default class ExpressionFormatter { private showFunctionKw(node: KeywordNode): string { if (isTabularToken(node.tokenType)) { - return toTabularFormat(this.showNonTabularFunctionKw(node), this.cfg.indentStyle); + return this.colorize( + 'function', + toTabularFormat(this.showNonTabularFunctionKw(node), this.cfg.indentStyle) + ); } else { - return this.showNonTabularFunctionKw(node); + return this.colorize('function', this.showNonTabularFunctionKw(node)); } } diff --git a/src/formatter/InlineLayout.ts b/src/formatter/InlineLayout.ts index e4fc2ab092..c94e1484c2 100644 --- a/src/formatter/InlineLayout.ts +++ b/src/formatter/InlineLayout.ts @@ -1,4 +1,5 @@ // eslint-disable-next-line max-classes-per-file +import { stripColors } from '../utils.js'; import Indentation from './Indentation.js'; import Layout, { WS } from './Layout.js'; @@ -16,7 +17,7 @@ export default class InlineLayout extends Layout { // but only when there actually is a space to remove. private trailingSpace = false; - constructor(private expressionWidth: number) { + constructor(private expressionWidth: number, private colorized: boolean) { super(new Indentation('')); // no indentation in inline layout } @@ -30,6 +31,8 @@ export default class InlineLayout extends Layout { } private addToLength(item: WS | string) { + item = this.colorized && typeof item === 'string' ? stripColors(item) : item; + if (typeof item === 'string') { this.length += item.length; this.trailingSpace = false; diff --git a/src/formatter/LimitNodes.ts b/src/formatter/LimitNodes.ts new file mode 100644 index 0000000000..3688700e3d --- /dev/null +++ b/src/formatter/LimitNodes.ts @@ -0,0 +1,83 @@ +import { AstNode, NodeType } from '../parser/ast.js'; + +export type LimitCb = (result: LimitResult, skipped: number, front: boolean) => void; +export interface LimitResult { + skippedFront: number; + skippedBack: number; + nodes: AstNode[]; +} + +export function limitWithComment(result: LimitResult, skipped: number, front: boolean) { + if (!skipped) { + return; + } + + result.nodes.push({ + type: NodeType.block_comment, + text: `/* ${front ? `${skipped} more items ...` : `... ${skipped} more items`} */`, + precedingWhitespace: '', + }); +} + +export function limitNodesByType( + nodes: AstNode[], + type: NodeType, + cbIfSkipped: LimitCb, + start: number, + end?: number +) { + const buffer: AstNode[] = []; + const result: LimitResult = { + skippedFront: 0, + skippedBack: 0, + nodes: [], + }; + let hitCurrType = 0; + // We want to start taking a piece + let grub = false; + + if (start === 0 && (end === undefined || end >= nodes.length)) { + return { + skippedFront: 0, + skippedBack: 0, + nodes, + }; + } + + for (let i = start; i < nodes.length; i++) { + const node = nodes[i]; + + if (node.type !== type) { + if (grub) { + buffer.push(node); + } + + continue; + } + + if (hitCurrType < start) { + hitCurrType++; + continue; + } else { + if (!grub) { + result.skippedFront = hitCurrType; + cbIfSkipped(result, hitCurrType, true); + } + + grub = true; + } + + if (end !== undefined && hitCurrType >= end) { + const sumToEnd = nodes.slice(i).reduce((acc, n) => acc + (n.type === type ? 1 : 0), 0); + result.skippedBack = sumToEnd; + cbIfSkipped(result, sumToEnd, false); + break; + } + + hitCurrType++; + result.nodes.push(...buffer, node); + buffer.length = 0; + } + + return result; +} diff --git a/src/formatter/Params.ts b/src/formatter/Params.ts index cae1aa95d9..e71eef6771 100644 --- a/src/formatter/Params.ts +++ b/src/formatter/Params.ts @@ -39,4 +39,8 @@ export default class Params { public setPositionalParameterIndex(i: number) { this.index = i; } + + public addPositionalParameterIndex(i: number) { + this.index += i; + } } diff --git a/src/lexer/Tokenizer.ts b/src/lexer/Tokenizer.ts index ba761de0ae..6184e3654e 100644 --- a/src/lexer/Tokenizer.ts +++ b/src/lexer/Tokenizer.ts @@ -50,9 +50,7 @@ export default class Tokenizer { }, { type: TokenType.NUMBER, - regex: cfg.underscoresInNumbers - ? /(?:0x[0-9a-fA-F_]+|0b[01_]+|(?:-\s*)?(?:[0-9_]*\.[0-9_]+|[0-9_]+(?:\.[0-9_]*)?)(?:[eE][-+]?[0-9_]+(?:\.[0-9_]+)?)?)(?![\w\p{Alphabetic}])/uy - : /(?:0x[0-9a-fA-F]+|0b[01]+|(?:-\s*)?(?:[0-9]*\.[0-9]+|[0-9]+(?:\.[0-9]*)?)(?:[eE][-+]?[0-9]+(?:\.[0-9]+)?)?)(?![\w\p{Alphabetic}])/uy, + regex: regex.number(cfg.underscoresInNumbers), }, // RESERVED_KEYWORD_PHRASE and RESERVED_DATA_TYPE_PHRASE is matched before all other keyword tokens // to e.g. prioritize matching "TIMESTAMP WITH TIME ZONE" phrase over "WITH" clause. diff --git a/src/lexer/regexFactory.ts b/src/lexer/regexFactory.ts index a466e0ce57..ded8b580d3 100644 --- a/src/lexer/regexFactory.ts +++ b/src/lexer/regexFactory.ts @@ -150,6 +150,11 @@ export const stringPattern = (quoteTypes: QuoteType[]): string => export const string = (quoteTypes: QuoteType[]): RegExp => patternToRegex(stringPattern(quoteTypes)); +export const number = (underscoresInNumbers?: boolean): RegExp => + underscoresInNumbers + ? /(?:0x[0-9a-fA-F_]+|0b[01_]+|(?:-\s*)?(?:[0-9_]*\.[0-9_]+|[0-9_]+(?:\.[0-9_]*)?)(?:[eE][-+]?[0-9_]+(?:\.[0-9_]+)?)?)(?![\w\p{Alphabetic}])/uy + : /(?:0x[0-9a-fA-F]+|0b[01]+|(?:-\s*)?(?:[0-9]*\.[0-9]+|[0-9]+(?:\.[0-9]*)?)(?:[eE][-+]?[0-9]+(?:\.[0-9]+)?)?)(?![\w\p{Alphabetic}])/uy; + /** * Builds a RegExp for valid identifiers in a SQL dialect */ @@ -168,14 +173,14 @@ export const identifierPattern = ({ // Unicode letters, diacritical marks and underscore const letter = '\\p{Alphabetic}\\p{Mark}_'; // Numbers 0..9, plus various unicode numbers - const number = '\\p{Decimal_Number}'; + const dNumber = '\\p{Decimal_Number}'; const firstChars = escapeRegExp(first ?? ''); const restChars = escapeRegExp(rest ?? ''); const pattern = allowFirstCharNumber - ? `[${letter}${number}${firstChars}][${letter}${number}${restChars}]*` - : `[${letter}${firstChars}][${letter}${number}${restChars}]*`; + ? `[${letter}${dNumber}${firstChars}][${letter}${dNumber}${restChars}]*` + : `[${letter}${firstChars}][${letter}${dNumber}${restChars}]*`; return dashes ? withDashes(pattern) : pattern; }; diff --git a/src/sqlFormatter.ts b/src/sqlFormatter.ts index d65159b6f6..c874b6d8f2 100644 --- a/src/sqlFormatter.ts +++ b/src/sqlFormatter.ts @@ -1,3 +1,4 @@ +import pc from 'picocolors'; import * as allDialects from './allDialects.js'; import { FormatOptions } from './FormatOptions.js'; @@ -53,6 +54,19 @@ const defaultOptions: FormatOptions = { linesBetweenQueries: 1, denseOperators: false, newlineBeforeSemicolon: false, + compactParenthesis: false, + colors: false, + colorsMap: { + keyword: pc.cyan, + operator: pc.reset, + comment: pc.gray, + string: pc.magenta, + number: pc.green, + function: pc.blue, + parenthesis: pc.reset, + identifier: pc.yellow, + dataType: pc.reset, + }, }; /** diff --git a/src/utils.ts b/src/utils.ts index 0c65188780..9244587a2b 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -18,6 +18,10 @@ export const equalizeWhitespace = (s: string) => s.replace(/\s+/gu, ' '); // True when string contains multiple lines export const isMultiline = (text: string): boolean => /\n/.test(text); +export const stripColors = (s: string) => + // eslint-disable-next-line no-control-regex + s.replace(/(?:\u001b\[\d\dm)+(.+)(?:\u001b\[\d\dm)+/gu, '$1'); + // Given a type and a field name, returns a type where this field is optional // // For example, these two type definitions are equivalent: diff --git a/static/index.html b/static/index.html index 6a6fc6f3ef..0123d18cf9 100644 --- a/static/index.html +++ b/static/index.html @@ -155,6 +155,25 @@

Options

+
+ + +
+
+ + +
+
+ + +
diff --git a/static/index.js b/static/index.js index f07fc5dde6..9203d61c97 100644 --- a/static/index.js +++ b/static/index.js @@ -16,6 +16,9 @@ const attachFormat = () => { const lineBetweenQueries = document.getElementById('lineBetweenQueries'); const denseOperators = document.getElementById('denseOperators'); const newlineBeforeSemicolon = document.getElementById('newlineBeforeSemicolon'); + const maxLengthInParenthesis = document.getElementById('maxLengthInParenthesis'); + const useCompactParenthesis = document.getElementById('useCompactParenthesis'); + const useColors = document.getElementById('useColors'); function showOutput(text) { output.value = text; @@ -46,6 +49,9 @@ const attachFormat = () => { lineBetweenQueries: lineBetweenQueries.value, denseOperators: denseOperators.checked, newlineBeforeSemicolon: newlineBeforeSemicolon.checked, + maxLengthInParenthesis: maxLengthInParenthesis.value, + compactParenthesis: useCompactParenthesis.checked, + colors: useColors.checked, }; showOutput(sqlFormatter.format(input.value, config)); } catch (e) { diff --git a/test/options/colors.ts b/test/options/colors.ts new file mode 100644 index 0000000000..e9486414bb --- /dev/null +++ b/test/options/colors.ts @@ -0,0 +1,84 @@ +import dedent from 'dedent-js'; +import pc from 'picocolors'; + +import { FormatFn } from '../../src/sqlFormatter.js'; + +export default function supportsColors(format: FormatFn) { + const testColors = { + keyword: pc.bgBlue, + operator: pc.bgCyan, + comment: pc.bgGreen, + parenthesis: pc.bgMagenta, + identifier: pc.bgRed, + function: pc.bgYellow, + string: pc.blue, + number: pc.cyan, + dataType: pc.green, + }; + + it('check color for correctness', () => { + const result = format( + `SELECT + /* + * This is a block comment + */ + *, test as col, + MiN(price) AS min_price, Cast(item_code AS INT), + FROM + -- This is another comment + MyTable -- One final comment + WHERE 1 = "321" AND 1 = '321' or ? = ?; + CREATE TABLE users ( user_id iNt PRIMARY KEY, total_earnings Decimal(5, 2) NOT NULL ); + `, + { + colors: true, + colorsMap: testColors, + params: ['1', '"b"'], + } + ); + expect(result).toBe(dedent` + \x1B[44mSELECT\x1B[49m + \x1B[42m/*\x1B[49m + \x1B[42m * This is a block comment\x1B[49m + \x1B[42m */\x1B[49m + *, + \x1B[41mtest\x1B[49m \x1B[44mas\x1B[49m \x1B[41mcol\x1B[49m, + \x1B[43mMiN\x1B[49m\x1B[45m(\x1B[49m\x1B[41mprice\x1B[49m\x1B[45m)\x1B[49m \x1B[44mAS\x1B[49m \x1B[41mmin_price\x1B[49m, + \x1B[43mCast\x1B[49m\x1B[45m(\x1B[49m\x1B[41mitem_code\x1B[49m \x1B[44mAS\x1B[49m \x1B[32mINT\x1B[39m\x1B[45m)\x1B[49m, + \x1B[44mFROM\x1B[49m + \x1B[42m-- This is another comment\x1B[49m + \x1B[41mMyTable\x1B[49m \x1B[42m-- One final comment\x1B[49m + \x1B[44mWHERE\x1B[49m + \x1B[36m1\x1B[39m = \x1B[41m"321"\x1B[49m + \x1B[44mAND\x1B[49m \x1B[36m1\x1B[39m = \x1B[34m'321'\x1B[39m + \x1B[44mor\x1B[49m \x1B[36m1\x1B[39m = \x1B[34m"b"\x1B[39m; + + \x1B[44mCREATE TABLE\x1B[49m \x1B[41musers\x1B[49m \x1B[45m(\x1B[49m + \x1B[41muser_id\x1B[49m \x1B[32miNt\x1B[39m \x1B[44mPRIMARY\x1B[49m \x1B[44mKEY\x1B[49m, + \x1B[41mtotal_earnings\x1B[49m \x1B[32mDecimal\x1B[39m\x1B[45m(\x1B[49m\x1B[36m5\x1B[39m, \x1B[36m2\x1B[39m\x1B[45m)\x1B[49m \x1B[44mNOT\x1B[49m \x1B[44mNULL\x1B[49m + \x1B[45m)\x1B[49m; + `); + }); + + it('color for long inline', () => { + const result = format( + dedent` + INSERT INTO user VALUES + (${'?'.repeat(12).split('').join(',')}), + (${'?'.repeat(12).split('').join(',')}) + `, + { + colors: true, + colorsMap: testColors, + params: Array.from({ length: 24 }, (_, i) => i.toString()), + } + ); + expect(result).toBe(dedent` + \x1B[44mINSERT INTO\x1B[49m + \x1B[41muser\x1B[49m + \x1B[44mVALUES\x1B[49m + \x1B[45m(\x1B[49m\x1B[36m0\x1B[39m, \x1B[36m1\x1B[39m, \x1B[36m2\x1B[39m, \x1B[36m3\x1B[39m, \x1B[36m4\x1B[39m, \x1B[36m5\x1B[39m, \x1B[36m6\x1B[39m, \x1B[36m7\x1B[39m, \x1B[36m8\x1B[39m, \x1B[36m9\x1B[39m, \x1B[36m10\x1B[39m, \x1B[36m11\x1B[39m\x1B[45m)\x1B[49m, + \x1B[45m(\x1B[49m\x1B[36m12\x1B[39m, \x1B[36m13\x1B[39m, \x1B[36m14\x1B[39m, \x1B[36m15\x1B[39m, \x1B[36m16\x1B[39m, \x1B[36m17\x1B[39m, \x1B[36m18\x1B[39m, \x1B[36m19\x1B[39m, \x1B[36m20\x1B[39m, \x1B[36m21\x1B[39m, \x1B[36m22\x1B[39m, \x1B[36m23\x1B[39m\x1B[45m)\x1B[49m + `); + }); +} diff --git a/test/options/compactParenthesis.ts b/test/options/compactParenthesis.ts new file mode 100644 index 0000000000..05dff5264a --- /dev/null +++ b/test/options/compactParenthesis.ts @@ -0,0 +1,23 @@ +import dedent from 'dedent-js'; + +import { FormatFn } from '../../src/sqlFormatter.js'; + +export default function supportsCompactParenthesis(format: FormatFn) { + it('check option compactParenthesis works as expected', () => { + const result = format(`INSERT INTO user VALUES (${'?'.repeat(100).split('').join(',')})`, { + compactParenthesis: true, + }); + expect(result).toBe(dedent` + INSERT INTO + user + VALUES + ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, + ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, + ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, + ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, + ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, + ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? + ) + `); + }); +} diff --git a/test/options/maxOperatorArgsLength.ts b/test/options/maxOperatorArgsLength.ts new file mode 100644 index 0000000000..86ef2bbf50 --- /dev/null +++ b/test/options/maxOperatorArgsLength.ts @@ -0,0 +1,57 @@ +import dedent from 'dedent-js'; + +import { FormatFn } from '../../src/sqlFormatter.js'; + +export default function supportsMaxOperatorArgsLength(format: FormatFn) { + it('set max operator args limit to 30', () => { + const result = format(`INSERT INTO user VALUES (${'?'.repeat(100).split('').join(',')})`, { + maxLengthInParenthesis: 30, + }); + expect(result).toBe(dedent` + INSERT INTO + user + VALUES + ( + ${'?'.repeat(30).split('').join(',\n ')} /* ... 70 more items */ + ) + `); + }); + + it('max operator args limit and compactParenthesis', () => { + const result = format(`INSERT INTO user VALUES (${'?'.repeat(100).split('').join(',')})`, { + compactParenthesis: true, + maxLengthInParenthesis: 30, + }); + expect(result).toBe(dedent` + INSERT INTO + user + VALUES + ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, + ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? + /* ... 70 more items */ + ) + `); + }); + + it('check correctness with params and limit', () => { + const result = format( + dedent` + INSERT INTO user VALUES + (${'?'.repeat(10).split('').join(',')}), + (${'?'.repeat(10).split('').join(',')}) + `, + { + compactParenthesis: true, + maxLengthInParenthesis: 5, + params: Array.from({ length: 20 }, (_, i) => i.toString()), + } + ); + expect(result).toBe(dedent` + INSERT INTO + user + VALUES + ( 0, 1, 2, 3, 4 /* ... 5 more items */), + ( 10, 11, 12, 13, 14 /* ... 5 more items */) + `); + }); +} diff --git a/test/sqlite.test.ts b/test/sqlite.test.ts index 36057ab351..bfba8b8bd1 100644 --- a/test/sqlite.test.ts +++ b/test/sqlite.test.ts @@ -25,6 +25,9 @@ import supportsOnConflict from './features/onConflict.js'; import supportsDataTypeCase from './options/dataTypeCase.js'; import supportsNumbers from './features/numbers.js'; import supportsReturning from './features/returning.js'; +import supportsCompactParenthesis from './options/compactParenthesis.js'; +import supportsMaxOperatorArgsLength from './options/maxOperatorArgsLength.js'; +import supportsColors from './options/colors.js'; describe('SqliteFormatter', () => { const language = 'sqlite'; @@ -59,6 +62,9 @@ describe('SqliteFormatter', () => { supportsWindow(format); supportsLimiting(format, { limit: true, offset: true }); supportsDataTypeCase(format); + supportsCompactParenthesis(format); + supportsMaxOperatorArgsLength(format); + supportsColors(format); it('supports REPLACE INTO syntax', () => { expect(format(`REPLACE INTO tbl VALUES (1,'Leopard'),(2,'Dog');`)).toBe(dedent` diff --git a/yarn.lock b/yarn.lock index 84c3890142..2e56d2b18e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5477,6 +5477,11 @@ picocolors@^1.0.0, picocolors@^1.1.0: resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz" integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== +picocolors@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" + integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== + picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3, picomatch@^2.3.1: version "2.3.1" resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz"