You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
ClickHouse, however, reformats expressions when it stores them — most visibly, it prints one space after every argument separator. So a schema written as cityHash64(a,b) is stored by ClickHouse as cityHash64(a, b), and because both the canonical form and the drift fingerprint run through normalizeSQLFragment, the two never compare equal.
The result is drift that reports forever and that no migration can fix — chkit generate sees no diff, because it canonicalizes both of its own sides identically.
Reproduction
CREATETABLEdefault.t (a String, b String, INDEX skip_idx cityHash64(a,b) TYPE minmax GRANULARITY 1)
ENGINE = MergeTree ORDER BY a;
ClickHouse echoes it back reformatted (verified on 26.3.9.1):
INDEX skip_idx cityHash64(a, b) TYPE minmax GRANULARITY 1
compareTableShape against the live shape returns reasonCodes: ['index_mismatch'], indexDiffs: ['i'].
Impact
Drift never reads clean for any skip index whose expression calls a 2+ argument function without a space after the comma (cityHash64(user_id,ts), concat(a,b), substring(s,1,3)).
chkit check fails in CI on a difference that does not exist, and chkit generate produces no migration that would resolve it. The only workaround is to hand-edit the schema to match ClickHouse's formatting.
The same fragment normalizer backs ttl and partitionBy, so those are exposed to the same class of mismatch.
Investigation Notes
packages/core/src/sql-normalizer.ts — normalizeSQLFragment collapses whitespace and nothing else.
Both sides apply the same lossy normalizer, so the author's spelling survives on the expected side while ClickHouse's spelling arrives on the actual side.
Index-only projections were given a faithful normalizer in PR #193 (normalizeProjectionIndex in packages/core/src/projection.ts) that peels redundant parens and spaces argument separators, validated form-by-form against a live instance. That fix was deliberately scoped to projections: changing normalizeSQLFragment globally would alter the canonical form of every existing SQL fragment and churn users' snapshots on upgrade, which needs its own change and a migration story.
Expected Behavior
A schema fragment and the live fragment ClickHouse stores for it compare equal whenever they describe the same expression, regardless of the author's spacing.
Proposed Fix
Two options, in increasing order of ambition:
Extend normalizeSQLFragment to match ClickHouse's printer for the mechanical parts (space after argument separators, at minimum), reusing the quote-aware scanning already in spaceAfterCommas (packages/core/src/projection.ts). Note the blast radius: this changes the canonical form of indexes, TTL, and partitionBy, so existing snapshots would need a rewrite or a compatibility pass to avoid spurious migrations on upgrade.
Compare fragments semantically rather than by string, e.g. by asking ClickHouse to normalize both sides, or by parsing to an AST. Robust but a much larger change.
Whichever is chosen, normalizeProjectionIndex should collapse into the shared helper so projections and skip indexes cannot diverge.
Verification
Unit: compareTableShape with expected cityHash64(a,b) and actual cityHash64(a, b) returns null.
Bug Description
normalizeSQLFragmentonly collapses whitespace runs:ClickHouse, however, reformats expressions when it stores them — most visibly, it prints one space after every argument separator. So a schema written as
cityHash64(a,b)is stored by ClickHouse ascityHash64(a, b), and because both the canonical form and the drift fingerprint run throughnormalizeSQLFragment, the two never compare equal.The result is drift that reports forever and that no migration can fix —
chkit generatesees no diff, because it canonicalizes both of its own sides identically.Reproduction
ClickHouse echoes it back reformatted (verified on 26.3.9.1):
With the schema declaring the un-spaced spelling:
compareTableShapeagainst the live shape returnsreasonCodes: ['index_mismatch'], indexDiffs: ['i'].Impact
cityHash64(user_id,ts),concat(a,b),substring(s,1,3)).chkit checkfails in CI on a difference that does not exist, andchkit generateproduces no migration that would resolve it. The only workaround is to hand-edit the schema to match ClickHouse's formatting.ttlandpartitionBy, so those are exposed to the same class of mismatch.Investigation Notes
packages/core/src/sql-normalizer.ts—normalizeSQLFragmentcollapses whitespace and nothing else.packages/core/src/canonical.ts:40—expression: normalizeSQLFragment(index.expression).packages/cli/src/commands/drift/compare.ts:223—expr=${normalizeSQLFragment(index.expression)}.Both sides apply the same lossy normalizer, so the author's spelling survives on the expected side while ClickHouse's spelling arrives on the actual side.
Index-only projections were given a faithful normalizer in PR #193 (
normalizeProjectionIndexinpackages/core/src/projection.ts) that peels redundant parens and spaces argument separators, validated form-by-form against a live instance. That fix was deliberately scoped to projections: changingnormalizeSQLFragmentglobally would alter the canonical form of every existing SQL fragment and churn users' snapshots on upgrade, which needs its own change and a migration story.Expected Behavior
A schema fragment and the live fragment ClickHouse stores for it compare equal whenever they describe the same expression, regardless of the author's spacing.
Proposed Fix
Two options, in increasing order of ambition:
normalizeSQLFragmentto match ClickHouse's printer for the mechanical parts (space after argument separators, at minimum), reusing the quote-aware scanning already inspaceAfterCommas(packages/core/src/projection.ts). Note the blast radius: this changes the canonical form of indexes, TTL, andpartitionBy, so existing snapshots would need a rewrite or a compatibility pass to avoid spurious migrations on upgrade.Whichever is chosen,
normalizeProjectionIndexshould collapse into the shared helper so projections and skip indexes cannot diverge.Verification
compareTableShapewith expectedcityHash64(a,b)and actualcityHash64(a, b)returnsnull.generatere-emit a drop + rebuild).Found while verifying #183 / PR #193.