ClickHouse 24.8 → 26.3 LTS (#376) - #435
Merged
Merged
Conversation
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
This was referenced Aug 12, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #376.
ClickHouse 24.8 → 26.3 LTS (
26.3.17.110). 65 pins, 7 exact-build pins, one stray24.3. One commit — provably unsplittable, sinceuse_query_condition_cache=0, which the gate harness needs, isCode: 115on 24.8.26.3 only; 24.8 support is dropped. The pre-existing minimum-version guard moves with it:
--mode initnow 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_inserton 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_livefailed 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 — withsystem.query_logshowing 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), becauseuse_skip_indexes_on_data_readis 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 ownDisplay— 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_tlspasses 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-chis still 24.8 and is swapped as a separate announced step — deliberately not part of this change, since other work depends on it continuously.