Build1 publisher3 min readPublished
sqlfluff's whitespace fix turns two minus signs into a comment, breaking a test fixture
A round-trip parse check over 2,249 of sqlfluff's own test fixtures returned 39 suspicious outputs. Two survived triage as real bugs, and one of them fires on default config against ordinary MySQL DDL.
The Engineer · Build desk

What happened
- The check ran sqlfluff at commit a54076a over 2,249 of its own test fixtures across 28 dialects, parsing each fixture, fixing it, then parsing the output again, in roughly an hour single-process.
- The raw run flagged 39 outputs, eight that parsed before the fix and not after plus 31 that did not converge on a second pass, and 37 of the 39 turned out not to be bugs.
- The fixture SELECT 1 * - - 5 AS a, 99 AS b FROM t came back as SELECT 99 AS b, 1 * --5 AS a FROM t, with the fused minus signs opening a line comment over the remainder of the line.
- RF06's quote stripping turned CREATE USER 'jeffrey'@'localhost' into CREATE USER jeffrey@localhost, output that sqlfluff's own parser rejects.
- The two findings that survived triage went upstream as sqlfluff issues 8415 and 8462.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- exposure A CI job running --fix over MySQL or MariaDB DDL can rewrite account specifications into SQL the linter cannot read back, and nobody had to enable anything beyond the shipped rule set to get there.
- constraint A fixer needs more than a green round-trip as its only gate: the output that quietly changed meaning still parsed cleanly on the way back in.
- decision sqlfluff's maintainers now pick between one check where fixes are applied and per-rule patches, including for a rule whose entire job is indentation.
- cost Triage is where the reporter's effort goes: filing the run raw would have put 31 expected non-convergences in front of a maintainer, and by the author's account one soft claim discredits the hard ones beside it.
sqlfluff removes whitespace it considers redundant [11]. Sometimes the characters on either side of that whitespace are tokens that fuse into a different token when they touch [11]. In the fixture `SELECT 1 * - - 5 AS a, 99 AS b FROM t`, the two minus signs closed up into `--`, which starts a line comment in SQL, and the fixer also reordered the select targets so the damaged column landed at the end and took the rest of the line with it [12].
Two more fixtures show the general shape. SQLite fuses `4 | ~ ~ ~4` into `4 | ~~~4`, a different token with no comment involved [13]. Oracle fuses the keywords `MULTISET EXCEPT` into `MULTISETEXCEPT`, and the rule responsible there is LT02, an indentation rule [14].
That last one is the argument for guarding at the token layer instead of patching rule by rule. "A rule that only ever adjusts indentation should not have to know what a token is," the author of the dev.to writeup wrote, and "the check belongs where fixes are applied" [15]. Five of the eight unparsable outputs pointed at one rule, and were filed as a single issue rather than five [9][16].
The second bug needs no unusual SQL. RF06 removes unnecessary quotes from identifiers, and in MySQL and MariaDB `'jeffrey'@'localhost'` is one account specification whose quotes are part of it [17]. `CREATE USER 'jeffrey'@'localhost'` comes back as `CREATE USER jeffrey@localhost`, which sqlfluff itself cannot parse [18]. Default config, no `.sqlfluff` file needed, and the same rewrite hits `GRANT ... TO`, `DROP USER` and `DEFINER =` clauses [19].
Corruption in this run means the fixture parsed before the fix and not after; unstable means fixing twice does not converge [3]. All 31 unstable results were expected, because sqlfluff's CLI loops the fixer until the output stabilises, so one unconverged pass is the tool working [5]. That is 79 percent of the flagged outputs [25]. Nine of the author's first ten findings on an earlier run were the same noise [6]. The author's point was that unstable means unresolved, not a confirmed bug [7]. One corruption candidate failed the other way: `ALTER USER 'jeffrey'@'localhost'` does not parse after the fix, and does not parse before it either, so it is a parser gap [8].
Eight unparsable outputs from 2,249 fixtures is 0.36 percent [26], and the corpus is nobody's production SQL: the fixtures were written by maintainers to sit on the edges of the grammar [29]. For the fusion bug to reach your repository you need adjacent tokens that lex differently when joined, like `1 * - - 5` [11][12]. For RF06 you need a MySQL grant script [17][19]. The whole run took roughly an hour single-process across 28 dialects, about 1.6 seconds per fixture [2][27]. The author's framing of the risk is that `--fix` writes across a whole repository, usually in CI, usually without anyone reading the diff [23], and that fixtures get used to test the parser while nobody points them all at the fixer at once [24].
The invariant is one line: if a fixture parses cleanly, its fix output must parse cleanly too [1]. It catches only the outputs that fail to parse. `1 * --5` parses perfectly and simply means something else, so the author added a second check for output that parses but has lost meaning [21]. The writeup's account of that second check breaks off before any results are reported [22].
What to watch
- Whether sqlfluff maintainers take the token-layer guard proposed in #8415 or patch the whitespace and indentation rules one at a time.
- Whether RF06 gains a MySQL and MariaDB account-specification exception under #8462, or the quote-stripping default changes instead.
- Results from the second check, for fix output that parses but has changed meaning, which the writeup has not yet published.