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; + `); + }); + }); });