Skip to content

ClickHouse 24.8 → 26.3 LTS (#376) - #435

Merged
hshimizu merged 2 commits into
mainfrom
issue-376-clickhouse-26-3
Aug 11, 2026
Merged

ClickHouse 24.8 → 26.3 LTS (#376)#435
hshimizu merged 2 commits into
mainfrom
issue-376-clickhouse-26-3

Conversation

@hshimizu

Copy link
Copy Markdown
Contributor

Closes #376.

ClickHouse 24.8 → 26.3 LTS (26.3.17.110). 65 pins, 7 exact-build pins, one stray 24.3. One commit — provably unsplittable, since use_query_condition_cache=0, which the gate harness needs, is Code: 115 on 24.8.

26.3 only; 24.8 support is dropped. The pre-existing minimum-version guard moves with it: --mode init now refuses anything below 26.3, and 24.8 is the too-old fixture we prove we reject. No compatibility shims, no dual-version paths, nothing to migrate — nothing has ever been deployed.

The defect the upgrade exposed

26.3 turns async_insert on by default, and it is 1.76× slower for us (5 interleaved reps, no distribution overlap). Pinning it on one insert path left the other inheriting it — in production that path is migration bookkeeping's write-then-read-back. logs_tail_live failed 3 of 6 runs against a stock 26.3 server and passed 6 of 6 with it off. Now pinned at every client construction — seven, derived rather than listed — with system.query_log showing all 2,709 inserts carrying '0' and 10/10 clean runs.

Query plans that moved — judged, never regenerated

Three of 79 gated shapes, all moved-correct, zero regressions, nothing halted:

  • explain_indexes::stage3_not_contains… — 26.x adds <Combined skip indexes> for the AND/OR mix a negated line filter renders; net granules 12/12 on both servers.
  • query_log_gates::body_search_skip_index_prunes_most_granules — the marks ratio genuinely fails (13/14), because use_skip_indexes_on_data_read is default-on from 26.1 and moves skip filtering from mark selection to data read. The same query reads the same 8,192 rows for 11% fewer bytes. Replaced by a same-server control identity.
  • live_metrics_cache — 26.3 reserves ~1.17 MiB per query before reading, breaking the test's own first half; fixture constant re-derived.

The regex dialect table did not move. 26 constructs, identical verdicts on both servers.

What the review changed

The first stage-3 replacement was tautological — it derived its expectation from the same catalog the planner reads, so removing a skip index from the DDL made the gate pass. A second helper had the identical defect. Both replaced by committed expectations; the break that used to pass now fails 5/5.

A version string still leaked: ClickHouse server errors were rendered with to_string() on all three read surfaces. Fixed at the choke point — on the error type's own Display — so a fourth surface cannot reintroduce it, demonstrated by the reviewer writing one.

Two limits, stated rather than filed

The pruning floor is 4× against a measured 7× baseline. Total loss of pruning is caught; an index degrading to roughly half its power is not. Tightening it without measuring variance would buy a flaky gate, so it is recorded as a bound.

A parser defect was present in the revision that passed code review round 2 — capturing Condition: made the block reader consume the next block's title, silently dropping every second index. The old two-row fixture hid it; the 100k corpus exposed it immediately. Fixed and pinned by a hermetic test. Worth recording that a PASS was given over a gate reading half its input.

Not run, reported as not run

The three cluster-leg steps need fixed per-node IPs and cannot run under rootless podman. Census: 56 steps, 53 run, 53 pass, 3 CI-only. live_tls passes against a 26.3 TLS container.

#412 is not closed by this — a claim that it was is withdrawn. The final-chunk path is sound on 26.3; the non-final-chunk path is unverified and looks reachable. Seven stale sites corrected.

After merge

The shared development instance pulsus-dev-ch is still 24.8 and is swapped as a separate announced step — deliberately not part of this change, since other work depends on it continuously.

24.8 support is dropped: one supported version, no version-aware code, no
expectation forks. The version pins are a sweep; the judgement is
concentrated in three places, and each verdict below is from a run on this
branch rather than from the plan.

**The floor.** `check_version` refuses < 26.3; `SchemaError::UnsupportedVersion`
carries the real reason (an HTTP-200 mid-stream exception is untaggable on
24.8, so it cannot be told from result text — #412). `docs/configuration.md`
and `docs/schemas.md` say the same thing the binary does. CI's below-minimum
negative leg now starts 24.8, the version we just dropped, and greps for
`26.3`; run verbatim against a real 24.8.14.39 it exits 1 with a message
naming 26.3.

**The EXPLAIN judgement.** One of 79 gated shapes moved: `!=` renders a
negated conjunction, and 26.x reports an extra `<Combined skip indexes>`
pseudo-block for that AND/OR mix. Judged moved-correct — net granule
selection is 12/12 on a 100k corpus on both servers, i.e. a negated line
filter prunes nothing on either.

The stage-3 expectation is **committed, not derived**. A first attempt read
the key list and index names out of `system.tables.sorting_key` and
`system.data_skipping_indices`; code review caught that the planner reads the
same catalog, so removing an index from the DDL moved both sides and the gate
passed on a table that had lost one. It is now a literal prefix plus a SET of
`Name:` + `Description:` + `Condition:` triples.

Bloom-filter indexes emit no `Condition:` at all (measured across five index
types; `minmax` and `set` do), so for stage 3's two the only thing separating
a working filter from a dead one is whether it rules granules out — invisible
on the old two-row fixture. The line-filter shapes now seed a 100k-row corpus
and each declares how much it must prune against a same-server
`use_skip_indexes = 0` control. Four DDL/fixture breaks prove the gate:
dropping `idx_body_ngrams` fails 5/5 shapes, changing an index's type behind
its name fails, reordering the sorting key fails on the prefix, and leaving
every index declared and correct while making it rule out nothing fails 3/3
shapes that are supposed to prune.

Order is not asserted, and that part of the change stands: on 24.8 the `Skip`
block order follows the DDL, on 26.3 the planner chooses it, and it is
fixture-dependent there — a 50k fixture flips it where a 100k one does not,
with identical net granules throughout.

**The `SelectedMarks` gate FAILED, it did not drift.** 13/14 against a
`<= 0.5` bound, because `use_skip_indexes_on_data_read` is default-on from
26.1 and moves skip filtering from mark selection into the data read. The
read did not get worse — same `read_rows` (8_192) for 11% fewer bytes. The
ratio is replaced by a same-server `read_bytes`/`read_rows` control identity
measured in the run (12.20x / 6.60x against pre-committed floors of 4x/3x),
and `use_query_condition_cache = 0` is pinned in the harness and read back
out of `system.query_log` so deleting the pin reddens.

**`async_insert`.** ClickHouse flipped it to `1` at 26.2. Measured under the
plan's pre-committed rule — five `pulsus-write` live suites, warmup
discarded, 5 interleaved reps between two otherwise identical servers — the
new default is **1.76x slower** with no overlap between the distributions,
because our writer already batches. Pinned to `0` on every path that issues
an INSERT: `insert_block`, `execute` (migration bookkeeping is
read-your-write), and the four `xtask` benchmark clients, which talk to the
server directly and would otherwise measure a configuration we do not ship.
`logs_tail_live` failed 3 of 6 runs against a stock 26.3 server before the
pin reached `execute()`; every insert in a fresh run now carries it.
`use_skip_indexes_on_data_read` is adopted as-is: zero byte/row difference
across 5 interleaved reps.

**The version-leak checks.** Three hermetic sites asserted the literal
`"version 24.8"`, which on 26.3 appears nowhere — they would have passed
while testing nothing. `MUST_NOT_LEAK`'s entry is deleted; the hermetic sites
now derive the forbidden string from their own fixture or match a version
SHAPE, and the live site reads `SELECT version()` off the connected server.
Code review then found a third path between those two halves — logs,
Prometheus and traces all render `ReadError::Clickhouse(ChError::Server)`
with `e.to_string()`, tail included — so the redaction now sits on
`ChError::Server`'s `Display`, the one place a server message becomes a
rendered string, with a test on each of the three surfaces.

**#331's rot check fired**, exactly as it was written to. All 28 flag-head
entries keep their subjects and change direction: 0 on 24.8.14.39, 1 on
26.3.17.110, with all 34 premises and 4 never-match probes unmoved. The
workaround is retained — retiring it is #331's call.

**#412 is NOT closed, and nothing in the tree says otherwise any more.**
26.3 tags the final chunk, but `extract_exception` runs per chunk and a
non-final chunk ending `))\n` still reaches the forgeable search. Five sites
said it closes with #376 and are corrected, plus two more that scoped the
forgery to 24.8 when it is not version-scoped. Found by sweeping the whole
tree on the SUBJECT (`git grep -n '#412' -- .`, 35 lines in 11 files, each
read) rather than on the claim's wording under a path filter, which is what
let the root `Cargo.toml` hide from three earlier sweeps.

**The regex dialect was re-measured, not re-labelled**: 26 constructs crossed
against both servers, every verdict identical including `\C`.

New: `pulsus-clickhouse --test injected_settings` proves every setting this
workspace injects exists on the connected server and is not obsolete, with a
CI step and a hermetic twin that fails when the checked-in list drifts from
the source.

Provenance is untouched: `docs/decisions/`, `docs/benchmarks/m1-*`, the
differential ledger, `log_label_bounds/*.txt` and `docs/benchmarks/data/*.json`
keep their versions, and the archived 24.8 error-body captures keep their
bytes with a note saying what they record.

Full evidence, one row per gated shape:
docs/benchmarks/clickhouse-26.3-plan-deltas.md
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Move to the latest ClickHouse LTS (24.8 → 26.3)

1 participant