sluice

The binlog keeps your SQL comments — and our TRUNCATE parser didn't know

A leading -- comment on a TRUNCATE made our CDC reader miss the statement entirely. The source emptied; the target kept every row, forever, with no error and no lag.

Landed in sluice · 2026-06-13 · MySQL & Vitess

Observed — MySQL binlog CDC. Internally Bug 140 (fixed in PR #208). Postgres is immune (see below).

What happened #

A CDC stream from MySQL silently diverged: the source ran a TRUNCATE, the source table emptied, and the target kept every one of its rows — indefinitely, with no error and no replication lag to hint at the gap. The stream looked perfectly healthy. It just never applied the truncate.

Why (the mechanism) #

MySQL's binlog QUERY_EVENT preserves a statement's leading comment verbatim — it strips only the trailing delimiter. Our CDC reader recognized a truncate by checking whether the event body starts with TRUNCATE. So a statement written as:

-- clear staging
TRUNCATE TABLE t;

arrived in the binlog as -- clear staging TRUNCATE TABLE t, failed the “starts with TRUNCATE” test, and fell through to generic DDL handling — which quietly did nothing for this statement. The truncate was never applied to the target.

This is not a synthetic-harness artifact. Hand-written migrations and ORM/APM query tags (/* trace=... */, -- deploy 2026-...) prepend comments to statements routinely, and MySQL dutifully records them in the binlog. Any consumer that pattern-matches SQL out of a binlog by prefix will trip on them.

The repro #

Run a commented TRUNCATE against a MySQL source under CDC and watch the target keep its rows:

-- on the source, under an active CDC stream:
INSERT INTO t VALUES (1), (2), (3);
-- leading comment: preserved verbatim in the binlog QUERY_EVENT
-- clear staging
TRUNCATE TABLE t;

-- source: 0 rows.  target (before the fix): still 3 rows, no error, no lag.

It was found by a randomized convergence fuzzer whose 5th generated transaction happened to be a commented TRUNCATE — a shape no hand-written test corpus in the project had ever produced.

What sluice does about it #

The reader now strips leading comment prefixes (both -- line comments and /* ... */ block comments) before pattern-matching the statement, so a commented TRUNCATE is recognized and applied like any other. Postgres was never affected: pgoutput emits a typed TruncateMessage with the relation OIDs, so there is no string to parse and no comment to trip over — the immunity is a direct consequence of a typed replication protocol versus a re-parsed SQL one.

The transferable lesson #

Two lessons, both cheap to internalize. First: anything that pattern-matches SQL text out of a binlog must normalize comments (and whitespace) before matching — the binlog is not the clean statement you typed, it's the statement plus whatever the client prepended. Second: randomized differential convergence testing finds the shapes your hand-written corpus never will. A fuzzer that runs the same random workload through two implementations and diffs the targets surfaces exactly this class of “nobody thought to write that test” bug.

Primary sources #