From 4f0b9a34581d03781b177b2a9500a6f0a6580f00 Mon Sep 17 00:00:00 2001 From: Jeremy-xuan <2938717844@qq.com> Date: Sat, 19 Sep 2026 16:43:15 +0800 Subject: [PATCH] fix(bigquery): do not merge the rest of the query when angle brackets never balance findClosingAngleBracketIndex() walked the token stream counting angle bracket depth and returned the last index when the brackets never balanced. The caller then merged everything from the type up to that index into a single IDENTIFIER. For a nested type this could swallow the remainder of the query: SELECT ARRAY /* c */ >[1] FROM t; produced a single identifier "ARRAY>[1]FROMt ;", turning valid SQL into silently broken SQL. A comment between ARRAY/STRUCT and "<" is what makes the outer type fail to combine, so the inner type is combined on its own; the remaining closing depth is then even and ">>" drives the depth counter from 1 to -1, skipping 0, so the loop never finds a closing bracket. Returning -1 for "not found" and leaving the tokens untouched fixes it. The tokens are now formatted as-is, which is not pretty but is correct, and can no longer destroy the query. Types without comments are unaffected. --- src/languages/bigquery/bigquery.formatter.ts | 12 ++++- test/bigquery.test.ts | 56 ++++++++++++++++++++ 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/src/languages/bigquery/bigquery.formatter.ts b/src/languages/bigquery/bigquery.formatter.ts index 398951015a..6d70484cf6 100644 --- a/src/languages/bigquery/bigquery.formatter.ts +++ b/src/languages/bigquery/bigquery.formatter.ts @@ -225,6 +225,13 @@ function combineParameterizedTypes(tokens: Token[]) { if ((isToken.ARRAY(token) || isToken.STRUCT(token)) && tokens[i + 1]?.text === '<') { const endIndex = findClosingAngleBracketIndex(tokens, i + 1); + if (endIndex === -1) { + // Unbalanced angle brackets. There is no safe place to end the type, + // so leave the tokens as they are instead of swallowing the rest of + // the query into a single identifier. + processed.push(token); + continue; + } const typeDefTokens = tokens.slice(i, endIndex + 1); processed.push({ type: TokenType.IDENTIFIER, @@ -265,5 +272,8 @@ function findClosingAngleBracketIndex(tokens: Token[], startIndex: number): numb return i; } } - return tokens.length - 1; + // Brackets never balanced. Reporting "not found" lets the caller leave the + // tokens untouched; returning the last index used to make it merge the whole + // remaining token stream into one identifier. + return -1; } diff --git a/test/bigquery.test.ts b/test/bigquery.test.ts index d92e089b67..fa28b3fb4c 100644 --- a/test/bigquery.test.ts +++ b/test/bigquery.test.ts @@ -636,4 +636,60 @@ describe('BigQueryFormatter', () => { expect(format(input, { linesBetweenQueries: 0 })).toBe(input); }); }); + describe('BigQuery parameterized types with comments', () => { + // A comment between ARRAY/STRUCT and "<" makes the outer type fail to + // combine, and the inner type is then combined on its own. When the angle + // brackets of that inner type never balance, the search for the closing + // bracket used to fall back to the last token, merging the whole rest of + // the query into a single identifier: the query after the type was + // silently destroyed. These tests lock down that the tokens are left + // alone instead. + const nestedTypeWithBlockComment = 'SELECT ARRAY /* c */ >[1] FROM t;'; + const nestedTypeWithLineComment = 'SELECT ARRAY -- c\n>[1] FROM t;'; + + it('keeps the rest of the query after a nested type with a block comment', () => { + const result = format(nestedTypeWithBlockComment); + expect(result).toBe(dedent` + SELECT + ARRAY /* c */ < ARRAY < INT64 >> [1] + FROM + t; + `); + // The regression: FROM and t used to be merged into one identifier. + expect(result).toContain('FROM'); + expect(result).not.toMatch(/FROMt/); + }); + + it('keeps the rest of the query after a nested type with a line comment', () => { + const result = format(nestedTypeWithLineComment); + expect(result).toBe(dedent` + SELECT + ARRAY -- c + < ARRAY < INT64 >> [1] + FROM + t; + `); + expect(result).not.toMatch(/FROMt/); + }); + + it('keeps a trailing alias after a nested type with a comment', () => { + const result = format('SELECT STRUCT /* c */ >(1) AS x FROM t;'); + expect(result).toBe(dedent` + SELECT + STRUCT /* c */ < a ARRAY < INT64 >> (1) AS x + FROM + t; + `); + expect(result).not.toMatch(/ASx/); + }); + + it('keeps formatting nested types without comments unchanged', () => { + expect(format('SELECT ARRAY>[1] FROM t;')).toBe(dedent` + SELECT + ARRAY>[1] + FROM + t; + `); + }); + }); });