Skip to content

fix(bigquery): do not merge the rest of the query when angle brackets never balance - #975

Open
Jeremy-xuan wants to merge 1 commit into
sql-formatter-org:masterfrom
Jeremy-xuan:fix/parameterized-type-runaway
Open

Jeremy-xuan wants to merge 1 commit into
sql-formatter-org:masterfrom
Jeremy-xuan:fix/parameterized-type-runaway

Conversation

@Jeremy-xuan

Copy link
Copy Markdown

fix(bigquery): do not merge the rest of the query when angle brackets never balance

What's wrong

For a nested parameterized type that has a comment between ARRAY/STRUCT and <,
BigQuery formatting silently destroyed the rest of the query:

SELECT ARRAY /* c */ <ARRAY<INT64>>[1] FROM t;
SELECT
  ARRAY /* c */ < ARRAY<INT64>>[1]FROMt ;

FROM t; became a single identifier FROMt ;. A trailing alias goes the same way:

SELECT ARRAY /* c */ <ARRAY<INT64>>[1] AS x;   --  ... [1]ASx ;

No error is raised: valid SQL goes in, broken SQL comes out. Line comments do the
same (ARRAY -- c\n<ARRAY<INT64>>[1]).

Root cause

findClosingAngleBracketIndex() counts angle-bracket depth and, if the brackets
never balance, returns the last token index:

  // bigquery.formatter.ts
  for (let i = startIndex; i < tokens.length; i++) {
    ...
    } else if (token.text === '>>') {
      level -= 2;
    }
    if (level === 0) {
      return i;
    }
  }
  return tokens.length - 1;      // <- here

The caller then does tokens.slice(i, endIndex + 1) and .join(''), so "not found"
means "merge everything up to the end of the query into one identifier", and
i = endIndex skips the remaining tokens.

Two details decide whether it triggers:

  • The comment is what makes the outer ARRAY fail the adjacency test
    (tokens[i + 1]?.text === '<'), so only the inner ARRAY is combined — on
    its own, and starting at a depth that goes wrong.
  • The depth counter starts at 0 and increments before testing, so level === 0
    can never fire at the starting <. ">>" then subtracts 2; when level is
    exactly 1 it lands on −1, skipping 0 forever, and the loop falls through to
    the tokens.length - 1 fallback. This is why the nested depth has to be even
    (depth 2 and 4 corrupt, depth 1 and 3 do not) and why writing > > as two
    tokens does not trigger it.

The fix

Return −1 for "not found" and let the caller leave the tokens alone:

      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;
      }

The type is then formatted as-is. ARRAY /* c */ < ARRAY < INT64 >> [1] is not
pretty, but it is correct and the query survives — the failure becomes cosmetic
instead of destructive.

Tests

Four tests in test/bigquery.test.ts cover a nested type with a block comment, with
a line comment, with a trailing alias, and a nested type without comments (to show
that path is untouched). Three of them fail on master.

pnpm test   →  Test Suites: 27 passed, 27 total
               Tests:       5850 passed, 1 skipped, 5851 total
pnpm run lint / pretty:check / ts:check / build   →  all clean

Regression check

Comparing master against the fix over 822 inputs (800 SQL literals harvested from
the test suite plus 22 targeted nested-type cases), with "the formatter must not
create or destroy words" (word-multiset preservation against the input) as the
criterion: 725 byte-identical, 9 improved, 0 regressions. Types without comments
are byte-identical, and only BigQuery has this slice-and-join path — the other 13
dialects tested behave the same before and after.

Not addressed here

… 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 */ <ARRAY<INT64>>[1] FROM t;

produced a single identifier "ARRAY<INT64>>[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.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant