Fix index granularity collapsing to 1 when mutating an adaptive part on a table with index_granularity_bytes = 0 - #111694
Conversation
…on a table with index_granularity_bytes = 0 An adaptive part (a Wide part with .mrk2 marks, or a Compact part) can be placed on a table whose index_granularity_bytes = 0 (non-adaptive) via RESTORE ... AS ... with allow_different_table_def = 1. A row-changing mutation (ALTER DELETE, TTL, or any mutation that rewrites all columns) then rewrites the part, inheriting the source part's adaptive index_granularity_info. The writer therefore runs with can_use_adaptive = true while the table's index_granularity_bytes = 0. In computeIndexGranularity the adaptive branch then fell through to index_granularity_bytes / size_of_row = 0 / N = 0, clamped to 1, producing one mark per row (thousands-fold mark and primary-index bloat). Data stayed correct, so correct-result tests could not catch it. Fix: in the adaptive branch, when index_granularity_bytes = 0 (byte-based sizing disabled) use the fixed row granularity instead of dividing by a zero byte budget. This enforces the invariant the function already documented. The normal adaptive path (index_granularity_bytes > 0) and the non-adaptive path are unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pre-PR validation gate (click to expand)
Session id: cron:clickhouse-author-slot-6:20260723-202100 |
Internal second-model review — adjudication log (click to expand)Pre-publication review by an independent model (engine: codex; 5 findings across 1 full pass + 1 fix round + 1 delta recheck; all AGREE and fixed before publishing).
Severity: ❌ blocker / Session id: cron:clickhouse-author-slot-6:20260723-202100 |
|
cc @alesapin @CurtizJ - could you review this? A row-changing mutation of an adaptive part (Wide .mrk2 or Compact) that sits on an |
|
Workflow [PR], commit [72a5c13] Summary: ❌
AI ReviewSummaryThis PR fixes Final Verdict✅ No blockers or majors found. LLVM Coverage Report
Changed lines: Changed C/C++ lines covered: 6/6 (100.00%) · Uncovered code |
LLVM Coverage Report
Changed lines: Changed C/C++ lines covered: 6/6 (100.00%) · Uncovered code |
CI finish ledger — ac3f0e5CI fully finished (Config + Finish Workflow SUCCESS, 2h30m buffer). Both Fast tests passed, so no PR-caused failure. Every failed check has an owner; only
Session id: cron:our-pr-ci-monitor:20260724-063000 |
|
Fixing PR for the I also need to correct the reason I gave in that ledger row. I wrote "measurement timing-sensitive", which is wrong -- the failure is deterministic, not timing-dependent. All 5 occurrences of this signature over the last 45 days carry a nonzero randomized #111502 pins |
The test only exercised the Wide route. The Compact route reaches the same granularity computation and was raised twice by an automated reviewer on the sibling PR ClickHouse#111626, which is now merged and no longer aborts the restore of a Compact part onto a table with a non-adaptive granularity policy, so the route became testable. Measured on this branch, both routes reproduce the defect without the fix and are correct with it: after RESTORE and a row-changing mutation of a 10000-row part, marks go 10000 -> 3 (Wide) and 10000 -> 2 (Compact). Also drop the test's references to C++ internals and to the fix itself, and one source comment line that narrated the defect rather than stating the invariant.
|
Pushed a master merge plus a second test arm. What changed and why:
Internal second-model review (Gate B, cold re-review): 0 findingsGate B re-reviewed the full scope cold against the frozen PR body: 0 findings (engine codex, $10.50; PR total $49.58 across 2 Gate A and 4 Gate B rounds). My own independent cold review raised 2 nits, both DISAGREE with evidence, 0 blockers, 0 majors:
Validation on this head: both directions on one base with two build IDs (identity asserted against each running server); the test file reddens on the pre-fix binary with both reference lines moving independently, so the new arm is not riding on the existing one; 50/50 randomized runs pass; the four non-defective configurations are unchanged. Verified during review that the line this diff replaces is from an earlier commit of mine, not a maintainer's, and that the change map is exactly three files with no deletions against the merged base. |
Build profile diff (arm_release)Comparing ✅ No significant changes. Binary sizes
Only the stripped binary is compared: the official master build keeps debug symbols while PR builds strip them, so the other binaries differ by construction. Compile time of recompiled translation units7 translation units recompiled, 6 s compile time in total, 7 of them have a recent master baseline. |
CI finish ledger - 72a5c13Every failure below has an owner: a fixing PR (mine or external), or a full-effort fix task
Session id: cron:our-pr-ci-monitor:20260822-023000 |
Related: #111626
Changelog category (leave one):
Changelog entry (a user-readable short description of the changes that goes into CHANGELOG.md):
Fixes index granularity collapsing to one mark per row (large mark and primary-index bloat) when a row-changing mutation rewrites an adaptive data part that lives on a table with
index_granularity_bytes = 0.Description
A part written with byte-based granularity can land on a table where that sizing is disabled (
index_granularity_bytes = 0), for example viaRESTORE ... AS ... SETTINGS allow_different_table_def = 1. A row-changing mutation (ALTER ... DELETE, TTL, any full-column rewrite) then rewrites it and inherits the source's granularity info, so the writer sizes granules by bytes while the byte budget is zero.computeIndexGranularitycomputed0 / size_of_row = 0, which the floor below turned into 1: one mark per row, a thousands-fold mark and primary-index bloat. Data stayed correct, so correct-result tests did not catch it.Fix: when
index_granularity_bytes = 0, use the fixed row granularity instead of dividing a zero byte budget. Theindex_granularity_bytes > 0and non-adaptive paths are untouched, verified byte-for-byte against a pre-fix build across small and large byte budgets, both part types, andindex_granularity = 1.The guard sits on the single function every part writer reaches, so it covers every route into that state, not only the reproducer's: also a restored
Compactpart, unblocked by #111626, and a replicated merge, which honours the assigning replica's part format.The test covers both part types, and both arms fail against a pre-fix build: for the 10000-row part below, marks go from 10000 to 3 (
Wide) and 10000 to 2 (Compact).Workflow [PR]
Sync PR [sync-upstream/pr/111694]