diff --git a/src/formatter/ExpressionFormatter.ts b/src/formatter/ExpressionFormatter.ts index 2c1fdc634c..10181ac233 100644 --- a/src/formatter/ExpressionFormatter.ts +++ b/src/formatter/ExpressionFormatter.ts @@ -204,7 +204,38 @@ export default class ExpressionFormatter { this.formatNode(node.property); } + /** + * Whether the parenthesis or bracket directly continues the previous expression, + * as in `quantile(0.9)(x)`, `splitByChar(',', s)[1]` or `` `arr`[1] ``. + * It is then not separated from the previous expression by a space. + */ + private continuesPreviousNode(node: ParenthesisNode): boolean { + const prev = this.nodes[this.index - 1]; + // Only for a node of this list, not for the parenthesis that belongs to a function call + if (!prev || this.nodes[this.index] !== node) { + return false; + } + switch (node.openParen) { + case '(': + return prev.type === NodeType.function_call; + case '[': + return ( + (prev.type === NodeType.identifier && prev.quoted) || + prev.type === NodeType.function_call || + prev.type === NodeType.property_access || + prev.type === NodeType.array_subscript || + prev.type === NodeType.parenthesis + ); + default: + return false; + } + } + private formatParenthesis(node: ParenthesisNode) { + if (this.continuesPreviousNode(node)) { + this.layout.add(WS.NO_SPACE); + } + const inlineLayout = this.formatInlineExpression(node.children, node.openParen); if (inlineLayout) { diff --git a/test/bigquery.test.ts b/test/bigquery.test.ts index d92e089b67..24d221a28f 100644 --- a/test/bigquery.test.ts +++ b/test/bigquery.test.ts @@ -50,6 +50,17 @@ describe('BigQueryFormatter', () => { supportsStrings(format, ['""-bs', "''-bs", "R''", 'R""', "B''", 'B""']); supportsIdentifiers(format, ['``']); supportsArrayLiterals(format, { withArrayPrefix: true, withoutArrayPrefix: true }); + + it('formats a subscript after a function call or quoted identifier without a space', () => { + const result = format("SELECT SPLIT(s, ',')[OFFSET(1)], `col`[0] FROM t;"); + expect(result).toBe(dedent` + SELECT + SPLIT(s, ',')[OFFSET(1)], + \`col\`[0] + FROM + t; + `); + }); supportsBetween(format); supportsJoin(format, { without: ['NATURAL'] }); supportsSetOperations(format, [ diff --git a/test/clickhouse.test.ts b/test/clickhouse.test.ts index 225b0480f0..afad768bd3 100644 --- a/test/clickhouse.test.ts +++ b/test/clickhouse.test.ts @@ -167,6 +167,47 @@ describe('ClickhouseFormatter', () => { supportsWindow(format); supportsLimiting(format, { limit: true, offset: false }); supportsArrayAndMapAccessors(format); + + describe('Chained calls and subscripts', () => { + it('formats the arguments of a parametric aggregate function without a space', () => { + const result = format(`SELECT quantile(0.9)(x), quantiles(0.5, 0.9)(x) FROM t;`); + expect(result).toBe(dedent` + SELECT + quantile(0.9)(x), + quantiles(0.5, 0.9)(x) + FROM + t; + `); + }); + + it('formats a subscript after a function call, parenthesis, quoted identifier or subscript without a space', () => { + const result = format( + "SELECT splitByChar(',', s)[1], (arr)[1], `signals`[1], t.`signals`[2], c[1][2] FROM t;" + ); + expect(result).toBe(dedent` + SELECT + splitByChar(',', s)[1], + (arr)[1], + \`signals\`[1], + t.\`signals\`[2], + c[1][2] + FROM + t; + `); + }); + + it('keeps the space before an array literal that follows a keyword', () => { + const result = format(`SELECT * FROM t WHERE x IN [1, 2];`); + expect(result).toBe(dedent` + SELECT + * + FROM + t + WHERE + x IN [1, 2]; + `); + }); + }); supportsDataTypeCase(format); it('supports map literals', () => { diff --git a/test/postgresql.test.ts b/test/postgresql.test.ts index e8e99adfea..582657c505 100644 --- a/test/postgresql.test.ts +++ b/test/postgresql.test.ts @@ -31,6 +31,18 @@ describe('PostgreSqlFormatter', () => { supportsDropTable(format, { ifExists: true }); supportsConstraints(format, ['NO ACTION', 'RESTRICT', 'CASCADE', 'SET NULL', 'SET DEFAULT']); supportsArrayLiterals(format, { withArrayPrefix: true }); + + it('formats a subscript after a quoted identifier, function call or parenthesis without a space', () => { + const result = format(`SELECT "col"[1], string_to_array(s, ',')[1], (ARRAY[1, 2])[1] FROM t;`); + expect(result).toBe(dedent` + SELECT + "col"[1], + string_to_array(s, ',')[1], + (ARRAY[1, 2])[1] + FROM + t; + `); + }); supportsUpdate(format, { whereCurrentOf: true }); supportsTruncateTable(format, { withoutTable: true }); supportsStrings(format, ["''-qq", "U&''", "X''", "B''", "E''", '$$']);