fix: a nullable UNIQUE key stalls the entire replication stream - #1396
Conversation
A MySQL table with a UNIQUE key but no PRIMARY KEY takes the UNIQUE key as its ClickHouse sorting key (#1394). MySQL permits any number of NULLs in a UNIQUE index, so that key may name nullable columns -- unlike a PRIMARY KEY, whose columns MySQL forces to NOT NULL. ClickHouse rejects a nullable sorting key with Code: 44 ILLEGAL_COLUMN unless allow_nullable_key is enabled. #1394 emitted that setting only for the keyless all-columns fallback (isKeylessTable), not for the UNIQUE-key path, so such a table produced: CREATE TABLE ... ORDER BY (a) <- no SETTINGS allow_nullable_key=1 and the CREATE TABLE failed. Because DDL is retried indefinitely, the failure did not merely skip that table -- it stalled the WHOLE replication stream. Measured on the published 2.10.0 image (1304-7da7cc526d2f6f396f3ea7bbd9d018e52a5f16dc-lt), MySQL 8.0.36 ROW/FULL/GTID -> ClickHouse 24.8.14.10547: table mysql ch note n_uk 5 1 nullable UNIQUE, no PK -- CREATE failed u_notnull 5 0 created before the stall, never populated u_nullable 5 - table never created u_composite 3 - table never created u_pk_control 3 - table never created <- unrelated, PK table 30 retries later the stream had made no further progress. Three of those tables have no nullable key at all; they were lost purely because they were behind the failing DDL in the stream. The fix keys the setting on whether the sorting key was DERIVED by the parser (UNIQUE-key path or all-columns fallback) rather than on isKeylessTable alone. A PRIMARY KEY sorting key still does not emit it -- MySQL guarantees those columns are NOT NULL, and emitting it unconditionally would silently permit nullable keys ClickHouse is right to reject. After the fix, same scenarios on a fresh stack: table mysql ch sorting_key result f_uk 4 4 a PASS (nullable UNIQUE, no PK) f_uk_notnull 5 5 a PASS f_uk_comp 3 3 a, b PASS (composite, nullable) f_pk 2 2 a PASS (no allow_nullable_key) f_keyless 2 2 a, b PASS f_alembic 1 1 version_num PASS f_behind 2 2 id PASS (stream not stalled) Code: 44 errors: 0 Content verified too, not just counts: f_uk survives an UPDATE and a DELETE as 1:a,2:bX,3:c,4:d, matching MySQL exactly. Why the existing tests missed it: every UNIQUE-key case in CreateTableUniqueKeySortKeyTest declares its column NOT NULL, and the DDL is only rejected once a nullable column reaches the key. Three tests are added -- nullable single-column UNIQUE, partially-nullable composite UNIQUE, and a guard that the PRIMARY KEY path does NOT emit the setting. The first two fail on the unmodified parent (8 run / 2 failures, precisely the new nullable cases) and pass with the fix. Test: CreateTableUniqueKeySortKeyTest 8 run / 0 failures, CreateTableNoKeySortKeyTest 7 run / 0 failures. MySqlDDLParserListenerImplTest#testAutoCreateTable fails identically on the unmodified parent commit 33ff707 -- pre-existing, unrelated to this change.
SummaryA MySQL table with a UNIQUE key but no PRIMARY KEY whose key column is nullable produces a Root cause#1394 added A PRIMARY KEY never needs the setting (MySQL forces those columns NOT NULL), so the fix keys on whether the sorting key was derived by the parser rather than on Measured — beforeMySQL 8.0.36 ROW/FULL/GTID -> ClickHouse 24.8.14.10547: 30 retries later the stream had made no further progress. The last three tables have no nullable key at all — they were lost purely because they sat behind the failing DDL. Measured — afterContent verified, not just counts: Why existing tests missed itEvery UNIQUE-key case in Failing-first proven: on the unmodified parent Test results
Rest of the image validated cleanEverything else in the published image passed end-to-end: BIT b8/b16/b24/b64 exact vs MySQL |
Addresses the judge-panel blocker on the previous commit. That commit fixed the Code: 44 stall by emitting allow_nullable_key for a nullable UNIQUE-key sorting key -- which made the CREATE TABLE succeed, but traded a loud stall for silent data loss. MySQL does not treat NULLs as equal for uniqueness, so a nullable UNIQUE index permits any number of rows whose key is NULL. It is not a row identity. ClickHouse DOES compare NULLs as equal in a sorting key, so adopting such a key makes ReplacingMergeTree collapse those distinct source rows into one. Measured on MySQL 8.0.36 ROW/FULL/GTID -> ClickHouse 24.8.14.10547 with the previous commit's jar, UNIQUE KEY(a) over a nullable `a`: table mysql ch contents z_nulluk 4 2 ch has keyed,third -- 'first','second' LOST z_nulluk_comp 3 2 partially-nullable composite, same loss So a UNIQUE key is now adopted as the sorting key ONLY when every one of its columns is NOT NULL. Otherwise the table has no usable declared identity and falls through to the existing all-columns fallback, which reproduces MySQL's own semantics for such a table: rows are distinguished by value. A warning names the table and the reason. The nullability of each column is already known while parsing the column definitions, so it is recorded there (notNullColumnNames, cleared per CREATE TABLE so a reused listener cannot leak state) and the UNIQUE key's columns are checked against it. Column-level (`uk INT UNIQUE`) and table-level (`UNIQUE KEY (a,b)`) declarations both route through the same column parse, so both are covered; index prefix lengths (`a(10)`) are stripped since they narrow the index, not the column. The allow_nullable_key gate is now keyed on nullableSortingKey rather than "was the key derived": only the all-columns fallback can name nullable columns. A PRIMARY KEY is NOT NULL by MySQL's rule and a UNIQUE key is now adopted only when NOT NULL, so neither needs the setting -- and emitting it where unnecessary would silently permit nullable keys ClickHouse is right to reject. This is what testNotNullCompositeUniqueKeyRemainsSortKey caught. End-to-end on a fresh stack with the fixed jar: table mysql ch sorting_key result q_nulluk 3 3 a, v PASS (nullable UNIQUE) q_nulluk_comp 3 3 a, b, v PASS (partially nullable) q_nnuk 4 4 a PASS (NOT NULL UNIQUE kept) q_nnuk_comp 3 3 a, b PASS q_pk 2 2 a PASS q_keyless 2 2 a, v PASS q_alembic 1 1 version_num PASS q_behind 2 2 id PASS (stream not stalled) Code: 44 errors: 0 Content verified, not just counts: q_nulluk holds first,keyed,second_upd on both sides after an UPDATE and a DELETE that each targeted a NULL-keyed row. The three NULL-keyed rows are preserved and individually addressable, which is the property the previous commit lost. Tests: CreateTableUniqueKeySortKeyTest 9 run / 0 failures, CreateTableNoKeySortKeyTest 7 run / 0 failures. testNullableUniqueKeyFallsBackToAllColumns and testPartiallyNullableCompositeUniqueKeyFallsBackToAllColumns cover the collapse; testNotNullCompositeUniqueKeyRemainsSortKey guards the safe case from over-correction.
Correction — second commit added (
|
Uh oh!
There was an error while loading. Please reload this page.