From 4c9d4bfc66ef08db7673f7daaf7446ffeae8a881 Mon Sep 17 00:00:00 2001 From: Yarchik Date: Tue, 4 Aug 2026 01:46:28 +0100 Subject: [PATCH 1/6] fix: keep a space between an operator and a following sign with denseOperators A binary operator immediately followed by a unary + or - was glued to it with denseOperators, so 'SELECT 5 % -2' became '5%-2'. PostgreSQL lexes a run of operator characters greedily, so an operator containing one of ~!@#%^&|`? keeps a trailing sign: '%' and '-' merge into a single '%-' operator (which does not exist), '@>' and '-' into '@>-', and the jsonb '?' and '-' into '?-'. The query then errors or changes meaning. Generalize the existing '--' line-comment guard to also keep a space in these cases. --- src/formatter/Layout.ts | 25 +++++++++++++++++++++---- test/postgresql.test.ts | 19 +++++++++++++++++++ 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/src/formatter/Layout.ts b/src/formatter/Layout.ts index 39fd4071b7..067224edf2 100644 --- a/src/formatter/Layout.ts +++ b/src/formatter/Layout.ts @@ -57,10 +57,12 @@ export default class Layout { this.items.push(WS.SINGLE_INDENT); break; default: - // Don't glue a layout item starting with "-" directly onto one ending with - // "-": that forms "--", which re-parses as a line comment and - // swallows the rest of the line (e.g. densing "a - -b" into "a--b"). - if (item.startsWith('-') && this.lastItemEndsWith('-')) { + // Don't glue an item starting with "-"/"+" onto a preceding operator when + // the two would re-lex as one token: "-" onto "-" forms "--" (a line + // comment that swallows the rest of the line), and a sign onto an operator + // containing ~!@#%^&|`? forms a merged operator like "%-" or "@>-" that parses + // differently (e.g. densing "5 % -2" into "5%-2"). + if (this.wouldMergeIntoOperator(item)) { this.items.push(WS.SPACE); } this.items.push(item); @@ -73,6 +75,21 @@ export default class Layout { return typeof lastItem === 'string' && lastItem.endsWith(suffix); } + private wouldMergeIntoOperator(item: string): boolean { + if (!item.startsWith('-') && !item.startsWith('+')) { + return false; + } + const lastItem = last(this.items); + if (typeof lastItem !== 'string') { + return false; + } + const run = /[-+*/<>=~!@#%^&|`?]+$/u.exec(lastItem)?.[0]; + if (!run) { + return false; + } + return (item.startsWith('-') && run.endsWith('-')) || /[~!@#%^&|`?]/u.test(run); + } + private trimHorizontalWhitespace() { while (isHorizontalWhitespace(last(this.items))) { this.items.pop(); diff --git a/test/postgresql.test.ts b/test/postgresql.test.ts index e8e99adfea..5fc010b292 100644 --- a/test/postgresql.test.ts +++ b/test/postgresql.test.ts @@ -249,6 +249,25 @@ describe('PostgreSqlFormatter', () => { `); }); + it('keeps a space between an operator and a following sign with denseOperators', () => { + expect(format('SELECT 5 % -2, 2 ^ -2, 8 # -1', { denseOperators: true })).toBe(dedent` + SELECT + 5% -2, + 2^ -2, + 8# -1 + `); + expect(format(`SELECT '[1,2]'::jsonb @> -1`, { denseOperators: true })).toBe(dedent` + SELECT + '[1,2]'::jsonb@> -1 + `); + expect(format(`SELECT data ? -1 FROM t`, { denseOperators: true })).toBe(dedent` + SELECT + data? -1 + FROM + t + `); + }); + // Issue #813 it('supports OR REPLACE in CREATE FUNCTION', () => { expect(format(`CREATE OR REPLACE FUNCTION foo ();`)).toBe(dedent` From 80ab884dcaffa9565ef903dd782b83bd197929a7 Mon Sep 17 00:00:00 2001 From: Yarchik Date: Fri, 7 Aug 2026 11:25:12 +0100 Subject: [PATCH 2/6] fix: scope the operator-sign spacing to dialects that combine operators The guard that keeps a space between an operator and a following +/- sign only prevents a real bug where the target dialect lexes a run of operator characters as a single operator, so 5 % -2 densed to 5%-2 re-parses as the operator %-. That is PostgreSQL and Redshift; MySQL, standard SQL and the rest have fixed operator sets and re-parse 5%-2 as 5 % -2, so the extra space is not needed. Gate the operator-run branch behind a new operatorsCombine dialect option (true for postgresql/redshift). The -- line-comment guard is unchanged and stays universal, so a - -b keeps its space in every dialect. --- src/dialect.ts | 1 + src/formatter/ExpressionFormatter.ts | 5 +++++ src/formatter/Formatter.ts | 5 ++++- src/formatter/Layout.ts | 16 ++++++++++------ src/languages/postgresql/postgresql.formatter.ts | 1 + src/languages/redshift/redshift.formatter.ts | 1 + test/mysql.test.ts | 8 ++++++++ 7 files changed, 30 insertions(+), 7 deletions(-) diff --git a/src/dialect.ts b/src/dialect.ts index 5a4b40ec77..0c04d7598b 100644 --- a/src/dialect.ts +++ b/src/dialect.ts @@ -47,4 +47,5 @@ const processDialectFormatOptions = ({ (options.tabularOnelineClauses ?? options.onelineClauses).map(name => [name, true]) ), identifierDashes: Boolean(tokenizerOptions.identChars?.dashes), + operatorsCombine: Boolean(options.operatorsCombine), }); diff --git a/src/formatter/ExpressionFormatter.ts b/src/formatter/ExpressionFormatter.ts index 2c1fdc634c..4c0ee2fa3e 100644 --- a/src/formatter/ExpressionFormatter.ts +++ b/src/formatter/ExpressionFormatter.ts @@ -54,6 +54,9 @@ export interface DialectFormatOptions { onelineClauses: string[]; // List of clauses that should be formatted on a single line in tabular style tabularOnelineClauses?: string[]; + // True in dialects that lex a run of operator characters as a single operator + // (PostgreSQL, Redshift), where two operators densed together re-parse as one. + operatorsCombine?: boolean; } // Contains the same data as DialectFormatOptions, @@ -66,6 +69,8 @@ export interface ProcessedDialectFormatOptions { // In such dialects the "-" operator must keep its surrounding spaces, // otherwise "a - b" densed to "a-b" would re-parse as a single identifier. identifierDashes: boolean; + // See DialectFormatOptions.operatorsCombine. + operatorsCombine: boolean; } /** Formats a generic SQL expression */ diff --git a/src/formatter/Formatter.ts b/src/formatter/Formatter.ts index 8f10f87791..48a723458e 100644 --- a/src/formatter/Formatter.ts +++ b/src/formatter/Formatter.ts @@ -48,7 +48,10 @@ export default class Formatter { cfg: this.cfg, dialectCfg: this.dialect.formatOptions, params: this.params, - layout: new Layout(new Indentation(indentString(this.cfg))), + layout: new Layout( + new Indentation(indentString(this.cfg)), + this.dialect.formatOptions.operatorsCombine + ), }).format(statement.children); if (!statement.hasSemicolon) { diff --git a/src/formatter/Layout.ts b/src/formatter/Layout.ts index 067224edf2..55bb27e044 100644 --- a/src/formatter/Layout.ts +++ b/src/formatter/Layout.ts @@ -25,7 +25,7 @@ export type LayoutItem = WS.SPACE | WS.SINGLE_INDENT | WS.NEWLINE | WS.MANDATORY export default class Layout { private items: LayoutItem[] = []; - constructor(public indentation: Indentation) {} + constructor(public indentation: Indentation, private operatorsCombine = false) {} /** * Appends token strings and whitespace modifications to SQL string. @@ -58,10 +58,11 @@ export default class Layout { break; default: // Don't glue an item starting with "-"/"+" onto a preceding operator when - // the two would re-lex as one token: "-" onto "-" forms "--" (a line - // comment that swallows the rest of the line), and a sign onto an operator - // containing ~!@#%^&|`? forms a merged operator like "%-" or "@>-" that parses - // differently (e.g. densing "5 % -2" into "5%-2"). + // the two would re-lex as one token. "-" onto "-" forms "--" (a line + // comment that swallows the rest of the line) in every dialect. In dialects + // that lex a run of operator characters as a single operator (PostgreSQL, + // Redshift), a sign onto an operator containing ~!@#%^&|`? also merges + // (e.g. densing "5 % -2" into "5%-2", which re-parses as the operator "%-"). if (this.wouldMergeIntoOperator(item)) { this.items.push(WS.SPACE); } @@ -87,7 +88,10 @@ export default class Layout { if (!run) { return false; } - return (item.startsWith('-') && run.endsWith('-')) || /[~!@#%^&|`?]/u.test(run); + if (item.startsWith('-') && run.endsWith('-')) { + return true; + } + return this.operatorsCombine && /[~!@#%^&|`?]/u.test(run); } private trimHorizontalWhitespace() { diff --git a/src/languages/postgresql/postgresql.formatter.ts b/src/languages/postgresql/postgresql.formatter.ts index d3d8a51fd2..8ffe13a473 100644 --- a/src/languages/postgresql/postgresql.formatter.ts +++ b/src/languages/postgresql/postgresql.formatter.ts @@ -408,5 +408,6 @@ export const postgresql: DialectOptions = { alwaysDenseOperators: ['::', ':'], onelineClauses: [...standardOnelineClauses, ...tabularOnelineClauses], tabularOnelineClauses, + operatorsCombine: true, }, }; diff --git a/src/languages/redshift/redshift.formatter.ts b/src/languages/redshift/redshift.formatter.ts index ef4a8e2f9b..619a507cd6 100644 --- a/src/languages/redshift/redshift.formatter.ts +++ b/src/languages/redshift/redshift.formatter.ts @@ -182,5 +182,6 @@ export const redshift: DialectOptions = { alwaysDenseOperators: ['::'], onelineClauses: [...standardOnelineClauses, ...tabularOnelineClauses], tabularOnelineClauses, + operatorsCombine: true, }, }; diff --git a/test/mysql.test.ts b/test/mysql.test.ts index e6bcd37af9..c626de924e 100644 --- a/test/mysql.test.ts +++ b/test/mysql.test.ts @@ -114,4 +114,12 @@ describe('MySqlFormatter', () => { DROP DEFAULT; `); }); + + it('does not space a sign after an operator in dense mode', () => { + expect(format('SELECT 5 % -2, 5 & -2', { denseOperators: true })).toBe(dedent` + SELECT + 5%-2, + 5&-2 + `); + }); }); From c09a142ad77e7d3984f1232626ab7b1fa13d7352 Mon Sep 17 00:00:00 2001 From: Yarchik Date: Mon, 7 Sep 2026 14:55:25 +0100 Subject: [PATCH 3/6] refactor: rename the merge guard and cover every combining operator character Renames wouldMergeIntoOperator to isItemSafeToAppend with the boolean inverted, moves the explanation onto the method as a doc comment, and names the matched run precedingOperatorChars. Drops lastItemEndsWith, which the previous commit left unused. Extends the PostgreSQL test to every operator character that can swallow a following sign instead of a sample of them. Co-Authored-By: Claude Opus 5 (1M context) --- src/formatter/Layout.ts | 43 +++++++++++++++++++++-------------------- test/postgresql.test.ts | 14 ++++++++++++++ 2 files changed, 36 insertions(+), 21 deletions(-) diff --git a/src/formatter/Layout.ts b/src/formatter/Layout.ts index 55bb27e044..8682fa54b6 100644 --- a/src/formatter/Layout.ts +++ b/src/formatter/Layout.ts @@ -57,13 +57,7 @@ export default class Layout { this.items.push(WS.SINGLE_INDENT); break; default: - // Don't glue an item starting with "-"/"+" onto a preceding operator when - // the two would re-lex as one token. "-" onto "-" forms "--" (a line - // comment that swallows the rest of the line) in every dialect. In dialects - // that lex a run of operator characters as a single operator (PostgreSQL, - // Redshift), a sign onto an operator containing ~!@#%^&|`? also merges - // (e.g. densing "5 % -2" into "5%-2", which re-parses as the operator "%-"). - if (this.wouldMergeIntoOperator(item)) { + if (!this.isItemSafeToAppend(item)) { this.items.push(WS.SPACE); } this.items.push(item); @@ -71,27 +65,34 @@ export default class Layout { } } - private lastItemEndsWith(suffix: string): boolean { - const lastItem = last(this.items); - return typeof lastItem === 'string' && lastItem.endsWith(suffix); - } - - private wouldMergeIntoOperator(item: string): boolean { + /** + * Whether `item` can be written directly after the preceding item without the + * two re-lexing as a single token. + * + * Only an item starting with "-" or "+" is at risk, and only when the preceding + * item ends in operator characters. "-" after a trailing "-" forms "--", a line + * comment that swallows the rest of the line, in every dialect. In a dialect that + * lexes a run of operator characters as one operator, a sign after an operator + * containing any of ~!@#%^&|`? merges too: "5 % -2" written densely as "5%-2" + * re-parses as the operator "%-". + */ + private isItemSafeToAppend(item: string): boolean { if (!item.startsWith('-') && !item.startsWith('+')) { - return false; + return true; } const lastItem = last(this.items); if (typeof lastItem !== 'string') { - return false; - } - const run = /[-+*/<>=~!@#%^&|`?]+$/u.exec(lastItem)?.[0]; - if (!run) { - return false; + return true; } - if (item.startsWith('-') && run.endsWith('-')) { + // The operator characters the new item would be written against. + const precedingOperatorChars = /[-+*/<>=~!@#%^&|`?]+$/u.exec(lastItem)?.[0]; + if (!precedingOperatorChars) { return true; } - return this.operatorsCombine && /[~!@#%^&|`?]/u.test(run); + if (item.startsWith('-') && precedingOperatorChars.endsWith('-')) { + return false; + } + return !(this.operatorsCombine && /[~!@#%^&|`?]/u.test(precedingOperatorChars)); } private trimHorizontalWhitespace() { diff --git a/test/postgresql.test.ts b/test/postgresql.test.ts index 5fc010b292..b23648f1a3 100644 --- a/test/postgresql.test.ts +++ b/test/postgresql.test.ts @@ -249,6 +249,20 @@ describe('PostgreSqlFormatter', () => { `); }); + // Every character that lets PostgreSQL lex a run as a single operator can swallow a + // following sign, so each one is checked rather than a sample. The tenth such + // character, a backtick, is legal in CREATE OPERATOR but the lexer never yields it + // as an operator, so it is not reachable from here. + it.each(['~', '!~', '@>', '#', '%', '^', '&', '|', '?'])( + 'keeps a space between the operator %s and a following sign with denseOperators', + operator => { + expect(format(`SELECT a ${operator} -1`, { denseOperators: true })).toBe(dedent` + SELECT + a${operator} -1 + `); + } + ); + it('keeps a space between an operator and a following sign with denseOperators', () => { expect(format('SELECT 5 % -2, 2 ^ -2, 8 # -1', { denseOperators: true })).toBe(dedent` SELECT From 210dea24ef1c436d37e4e04d565785f9527d75aa Mon Sep 17 00:00:00 2001 From: Yarchik Date: Thu, 17 Sep 2026 12:48:54 +0100 Subject: [PATCH 4/6] refactor: split the two hazards in the append guard Each hazard is now its own condition, independent of the one before it, and the explanation lives with the regex it describes rather than above the method. Layout no longer defaults operatorsCombine to false, so InlineLayout -- the other Layout construction site -- has to state which dialect it is formatting. Co-Authored-By: Claude Opus 5 (1M context) --- src/formatter/ExpressionFormatter.ts | 6 ++-- src/formatter/InlineLayout.ts | 4 +-- src/formatter/Layout.ts | 42 ++++++++++++++-------------- test/unit/Layout.test.ts | 2 +- 4 files changed, 27 insertions(+), 27 deletions(-) diff --git a/src/formatter/ExpressionFormatter.ts b/src/formatter/ExpressionFormatter.ts index 4c0ee2fa3e..1502468d91 100644 --- a/src/formatter/ExpressionFormatter.ts +++ b/src/formatter/ExpressionFormatter.ts @@ -54,8 +54,8 @@ export interface DialectFormatOptions { onelineClauses: string[]; // List of clauses that should be formatted on a single line in tabular style tabularOnelineClauses?: string[]; - // True in dialects that lex a run of operator characters as a single operator - // (PostgreSQL, Redshift), where two operators densed together re-parse as one. + // True in dialects that lex a run of operator characters as a single operator, + // where an operator and a following sign densed together re-parse as one operator. operatorsCombine?: boolean; } @@ -514,7 +514,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.dialectCfg.operatorsCombine), inline: true, enclosingParenthesis, }).format(nodes); diff --git a/src/formatter/InlineLayout.ts b/src/formatter/InlineLayout.ts index e4fc2ab092..acb5a40ab4 100644 --- a/src/formatter/InlineLayout.ts +++ b/src/formatter/InlineLayout.ts @@ -16,8 +16,8 @@ export default class InlineLayout extends Layout { // but only when there actually is a space to remove. private trailingSpace = false; - constructor(private expressionWidth: number) { - super(new Indentation('')); // no indentation in inline layout + constructor(private expressionWidth: number, operatorsCombine: boolean) { + super(new Indentation(''), operatorsCombine); // no indentation in inline layout } public add(...items: (WS | string)[]) { diff --git a/src/formatter/Layout.ts b/src/formatter/Layout.ts index 8682fa54b6..cf2c2216b8 100644 --- a/src/formatter/Layout.ts +++ b/src/formatter/Layout.ts @@ -2,6 +2,15 @@ import { last } from '../utils.js'; import Indentation from './Indentation.js'; +const STARTS_WITH_SIGN = /^[-+]/u; + +// An operator that would take a following sign into its own name. +// PostgreSQL lexes a run of operator characters as a single operator, and such a name +// may only end in "+" or "-" when it also contains one of ~!@#%^&|`? -- so "@-" is an +// operator name, while "*-" is not. +// https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-OPERATORS +const ENDS_WITH_SIGN_ABSORBING_OPERATOR = /[~!@#%^&|`?][-+*/<>=~!@#%^&|`?]*$/u; + /** Whitespace modifiers to be used with add() method */ export enum WS { SPACE, // Adds single space @@ -25,7 +34,7 @@ export type LayoutItem = WS.SPACE | WS.SINGLE_INDENT | WS.NEWLINE | WS.MANDATORY export default class Layout { private items: LayoutItem[] = []; - constructor(public indentation: Indentation, private operatorsCombine = false) {} + constructor(public indentation: Indentation, private operatorsCombine: boolean) {} /** * Appends token strings and whitespace modifications to SQL string. @@ -65,34 +74,25 @@ export default class Layout { } } - /** - * Whether `item` can be written directly after the preceding item without the - * two re-lexing as a single token. - * - * Only an item starting with "-" or "+" is at risk, and only when the preceding - * item ends in operator characters. "-" after a trailing "-" forms "--", a line - * comment that swallows the rest of the line, in every dialect. In a dialect that - * lexes a run of operator characters as one operator, a sign after an operator - * containing any of ~!@#%^&|`? merges too: "5 % -2" written densely as "5%-2" - * re-parses as the operator "%-". - */ + /** Whether `item` can be written right after the preceding item without the two re-lexing as one. */ private isItemSafeToAppend(item: string): boolean { - if (!item.startsWith('-') && !item.startsWith('+')) { - return true; - } const lastItem = last(this.items); if (typeof lastItem !== 'string') { return true; } - // The operator characters the new item would be written against. - const precedingOperatorChars = /[-+*/<>=~!@#%^&|`?]+$/u.exec(lastItem)?.[0]; - if (!precedingOperatorChars) { - return true; + // "a - -b" densed to "a--b" would re-parse as a line comment. + if (lastItem.endsWith('-') && item.startsWith('-')) { + return false; } - if (item.startsWith('-') && precedingOperatorChars.endsWith('-')) { + // "5 % -2" densed to "5%-2" would re-parse as the single operator "%-". + if ( + this.operatorsCombine && + ENDS_WITH_SIGN_ABSORBING_OPERATOR.test(lastItem) && + STARTS_WITH_SIGN.test(item) + ) { return false; } - return !(this.operatorsCombine && /[~!@#%^&|`?]/u.test(precedingOperatorChars)); + return true; } private trimHorizontalWhitespace() { diff --git a/test/unit/Layout.test.ts b/test/unit/Layout.test.ts index 1b8c00a9d4..e54277252e 100644 --- a/test/unit/Layout.test.ts +++ b/test/unit/Layout.test.ts @@ -8,7 +8,7 @@ describe('Layout', () => { indentation.increaseTopLevel(); indentation.increaseTopLevel(); - const layout = new Layout(indentation); + const layout = new Layout(indentation, false); layout.add(...items); return layout.toString(); } From 16a3ab7d462bf43db3d1d6d474ab04c39543fd97 Mon Sep 17 00:00:00 2001 From: Yarchik Date: Thu, 17 Sep 2026 12:48:54 +0100 Subject: [PATCH 5/6] fix: limit the operator-sign spacing to PostgreSQL Redshift has the same operator-name rule, but a narrower set of operators; it gets its own pull request. Co-Authored-By: Claude Opus 5 (1M context) --- src/languages/redshift/redshift.formatter.ts | 1 - test/redshift.test.ts | 3 +++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/languages/redshift/redshift.formatter.ts b/src/languages/redshift/redshift.formatter.ts index 619a507cd6..ef4a8e2f9b 100644 --- a/src/languages/redshift/redshift.formatter.ts +++ b/src/languages/redshift/redshift.formatter.ts @@ -182,6 +182,5 @@ export const redshift: DialectOptions = { alwaysDenseOperators: ['::'], onelineClauses: [...standardOnelineClauses, ...tabularOnelineClauses], tabularOnelineClauses, - operatorsCombine: true, }, }; diff --git a/test/redshift.test.ts b/test/redshift.test.ts index dc890be75d..bbc6921513 100644 --- a/test/redshift.test.ts +++ b/test/redshift.test.ts @@ -52,6 +52,9 @@ describe('RedshiftFormatter', () => { [...standardOperators, '^', '%', '@', '|/', '||/', '&', '|', '~', '<<', '>>', '||'], { any: true, + // Redshift inherits the operator syntax of PostgreSQL, + // but the formatter doesn't take that into account yet. + operatorsAbsorbSign: true, } ); supportsJoin(format); From 84ab9aa82a2f38eeea0753d29773a382a54d3cac Mon Sep 17 00:00:00 2001 From: Yarchik Date: Thu, 17 Sep 2026 12:48:54 +0100 Subject: [PATCH 6/6] test: cover the operator-sign spacing in every dialect Every dialect that doesn't combine operators now asserts that a sign denses onto each operator it declares, instead of a single MySQL example. For PostgreSQL both classes of operator are covered with both signs, which also removes the duplicated cases. Co-Authored-By: Claude Opus 5 (1M context) --- test/features/operators.ts | 14 +++++++++++ test/mysql.test.ts | 8 ------- test/postgresql.test.ts | 49 +++++++++++++++----------------------- 3 files changed, 33 insertions(+), 38 deletions(-) diff --git a/test/features/operators.ts b/test/features/operators.ts index ad1770a93f..b72f44a762 100644 --- a/test/features/operators.ts +++ b/test/features/operators.ts @@ -7,6 +7,10 @@ export const standardOperators = ['+', '-', '*', '/', '>', '<', '=', '<>', '<=', type OperatorsConfig = { logicalOperators?: string[]; any?: boolean; + // Set in dialects where an operator can take a following sign into its own name, + // so that "1 % -2" must not be densed to "1%-2". + // Such dialects test the spacing of each of their operators separately. + operatorsAbsorbSign?: boolean; }; export default function supportsOperators( @@ -44,6 +48,16 @@ export default function supportsOperators( }); } + if (!cfg.operatorsAbsorbSign) { + operators + .filter(op => !op.endsWith('-')) + .forEach(op => { + it(`denses a sign after ${op} operator in dense mode`, () => { + expect(format(`foo ${op} -2`, { denseOperators: true })).toBe(`foo${op}-2`); + }); + }); + } + (cfg.logicalOperators || ['AND', 'OR']).forEach(op => { it(`supports ${op} operator`, () => { const result = format(`SELECT true ${op} false AS foo;`); diff --git a/test/mysql.test.ts b/test/mysql.test.ts index c626de924e..e6bcd37af9 100644 --- a/test/mysql.test.ts +++ b/test/mysql.test.ts @@ -114,12 +114,4 @@ describe('MySqlFormatter', () => { DROP DEFAULT; `); }); - - it('does not space a sign after an operator in dense mode', () => { - expect(format('SELECT 5 % -2, 5 & -2', { denseOperators: true })).toBe(dedent` - SELECT - 5%-2, - 5&-2 - `); - }); }); diff --git a/test/postgresql.test.ts b/test/postgresql.test.ts index b23648f1a3..3d2316a6e1 100644 --- a/test/postgresql.test.ts +++ b/test/postgresql.test.ts @@ -139,7 +139,7 @@ describe('PostgreSqlFormatter', () => { '&&&', '|=|', ], - { any: true } + { any: true, operatorsAbsorbSign: true } ); supportsIsDistinctFrom(format); supportsJoin(format); @@ -249,38 +249,27 @@ describe('PostgreSqlFormatter', () => { `); }); - // Every character that lets PostgreSQL lex a run as a single operator can swallow a - // following sign, so each one is checked rather than a sample. The tenth such - // character, a backtick, is legal in CREATE OPERATOR but the lexer never yields it - // as an operator, so it is not reachable from here. - it.each(['~', '!~', '@>', '#', '%', '^', '&', '|', '?'])( - 'keeps a space between the operator %s and a following sign with denseOperators', - operator => { - expect(format(`SELECT a ${operator} -1`, { denseOperators: true })).toBe(dedent` - SELECT - a${operator} -1 - `); + // An operator containing one of the ten characters ~!@#%^&|`? may end in "-" or "+", + // so densing a sign onto it would extend the operator name instead. Nine of the ten are + // covered here, together with the operators that end in "-" themselves. The tenth, the + // backtick, is never yielded as an operator by the lexer and can't be reached from here. + it.each(['~', '!~', '@', '@>', '#', '#>>', '%', '^', '&', '|', '||', '?', '@-@', '?-', '-|-'])( + 'keeps a space after %s operator and before a sign in dense mode', + op => { + expect(format(`foo ${op} -1`, { denseOperators: true })).toBe(`foo${op} -1`); + expect(format(`foo ${op} +1`, { denseOperators: true })).toBe(`foo${op} +1`); } ); - it('keeps a space between an operator and a following sign with denseOperators', () => { - expect(format('SELECT 5 % -2, 2 ^ -2, 8 # -1', { denseOperators: true })).toBe(dedent` - SELECT - 5% -2, - 2^ -2, - 8# -1 - `); - expect(format(`SELECT '[1,2]'::jsonb @> -1`, { denseOperators: true })).toBe(dedent` - SELECT - '[1,2]'::jsonb@> -1 - `); - expect(format(`SELECT data ? -1 FROM t`, { denseOperators: true })).toBe(dedent` - SELECT - data? -1 - FROM - t - `); - }); + // The remaining operators are built only from -+*/<>= and can't take a sign into their + // name, so a sign denses onto them just like in any other dialect. + it.each(['*', '/', '<', '>=', '<<', '->', '->>', '<>', '<->'])( + 'denses a sign after %s operator in dense mode', + op => { + expect(format(`foo ${op} -1`, { denseOperators: true })).toBe(`foo${op}-1`); + expect(format(`foo ${op} +1`, { denseOperators: true })).toBe(`foo${op}+1`); + } + ); // Issue #813 it('supports OR REPLACE in CREATE FUNCTION', () => {