fix(drift): compare SQL expressions in ClickHouse's canonical form#201
fix(drift): compare SQL expressions in ClickHouse's canonical form#201alvarogar4 wants to merge 2 commits into
Conversation
ClickHouse reformats expressions when it stores them — spacing argument separators and operators, adding precedence parens, rewriting `INTERVAL 5 YEAR` to `toIntervalYear(5)`. So a skip index, PARTITION BY, ORDER BY, or TTL written `cityHash64(a,b)` is stored `cityHash64(a, b)` and chkit drift reported permanent drift no migration could fix (#195). No local string normalizer can reproduce that rewriting. drift now normalizes expressions through ClickHouse's own formatter. The drift command collects every fragment across the compared tables, formats them in one batched `formatQuerySingleLineOrNull` round-trip (which yields NULL per unparseable fragment instead of failing the batch), and injects a map-backed canonicalizer into compareTableShape. Each field falls back to the existing string normalization when the fragment isn't in the map — an older server, a parse failure, or offline — so behavior is never worse than before. compareTableShape stays pure and synchronous; the ClickHouse call lives in the async payload builder. Adds @chkit/clickhouse `canonicalizeSqlFragments`. Closes #195 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Biy2vG7iixRiBsBBsFg5Nu
Review follow-up. canonicalizeSqlFragments inlined every fragment across all compared tables into one query. On a large schema the string-literal array could exceed ClickHouse's default max_query_size (256 KiB), throw, and — since buildSqlCanonicalizer swallows the error and falls back — silently revert the whole drift run to string comparison, no-opping the feature exactly where it matters. Fragments are now split into batches bounded well under the limit, each run as its own query and merged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Biy2vG7iixRiBsBBsFg5Nu
2a04fa0 to
016a776
Compare
|
Ran a high-effort adversarial review on this PR. It surfaced one real robustness gap, now fixed in the latest commit: Batching (fixed). The review's other two flags were verified and refuted: the collector/comparer are in exact lockstep today (no silent no-op), and the swallow-and-fall-back is intended, documented graceful degradation. One low-severity efficiency note (fragments normalized twice per run) I left as-is deliberately — it keeps The sibling PRs got the same scrutiny: #200 was fuzzed old-vs-new (~1M inputs/function) and found behavior-preserving with no defects; #199 is a small denylist with a regression + mutation test. |
Summary
Fixes #195.
chkit driftreported permanent, unfixable drift whenever a schema expression was spelled differently from how ClickHouse stores it. ClickHouse runs a full expression printer on the way in — it spaces argument separators and operators, adds precedence parentheses (n*2+1→(n * 2) + 1), and rewritesINTERVAL 5 YEAR→toIntervalYear(5). So a skip index /PARTITION BY/ORDER BY/TTLwrittencityHash64(a,b)is storedcityHash64(a, b), andchkit checkfailed in CI on a difference no migration could resolve.No local string normalizer can reproduce ClickHouse's precedence/INTERVAL rewriting, so only ClickHouse can produce the canonical form.
driftnow normalizes expressions through the server's own formatter.How it works
TTL, key/partition clause elements, and SELECT-projection queries — at the exact granularity they're compared.formatQuerySingleLineOrNull, which returnsNULLfor a fragment it can't parse instead of failing the whole batch.SqlCanonicalizeris injected intocompareTableShape. Each field falls back to the existing string normalization when a fragment isn't in the map — an older server without the function, a parse failure, or offline — so behavior is never worse than today.compareTableShapestays pure and synchronous; the ClickHouse call lives in the async payload builder. New@chkit/clickhouseexport:canonicalizeSqlFragments.Test plan
sipHash64vscityHash64) still drifts;collectTableSqlFragmentsgathers the right fragments at the right granularitycanonicalizeSqlFragmentsunit tests (mock executor): SELECT-prefix stripping, no-wrap query mode,NULLomission, quote/backslash escaping, empty-input short-circuitcityHash64(a,b)and TTLINTERVAL 30 DAY— both stored reformatted by ClickHouse — now reportsDrifted: no(wasindex_mismatch+ttl_mismatch)Scope
Covers the fragment types the issue named (skip indexes,
TTL,PARTITION BY) plus key clauses and SELECT-projection queries. The canonical snapshot form is left untouched — this is a drift-comparison change only, so no existing snapshots churn on upgrade. Index-only projection expressions keep their existing normalizer (already comma-spaced; operator expressions there are rare).🤖 Generated with Claude Code
https://claude.ai/code/session_01Biy2vG7iixRiBsBBsFg5Nu