Skip to content

Fix LOGICAL_ERROR when loading an existing Compact part under a non-adaptive table policy - #111626

Merged
Avogar merged 3 commits into
ClickHouse:masterfrom
groeneai:groeneai-fix-load-compact-part-non-adaptive
Aug 12, 2026
Merged

Fix LOGICAL_ERROR when loading an existing Compact part under a non-adaptive table policy#111626
Avogar merged 3 commits into
ClickHouse:masterfrom
groeneai:groeneai-fix-load-compact-part-non-adaptive

Conversation

@groeneai

Copy link
Copy Markdown
Collaborator

Changelog category (leave one):

  • Bug Fix (user-visible misbehavior in an official stable release)

Changelog entry (a user-readable short description of the changes that goes into CHANGELOG.md):

Fixed a LOGICAL_ERROR (Cannot create part with type Compact and storage type Full because table does not support polymorphic parts) raised when a MergeTree table loads a pre-existing Compact part while its current settings disable polymorphic parts (non-adaptive granularity). This could happen, for example, when a read-only table shares an on-disk path with another table and reloads its parts via SYSTEM RESTART DISK or on server startup. In debug and sanitizer builds the exception aborts the server.

Description

Found by the Stress test (amd_msan) check on an unrelated PR (STID 3286-0cd6), with the same error recurring across several unrelated PRs and check types (Stress msan/tsan/asan, Integration msan/tsan) over the last two months. No tracking issue existed.

Report: https://s3.amazonaws.com/clickhouse-test-reports/json.html?PR=109360&sha=3791a7836dd7b2f089113ba6666a14b6f9c26492&name_0=PR&name_1=Stress%20test%20%28amd_msan%29

Root cause. MergeTreeData::canUsePolymorphicParts() is equivalent to canUseAdaptiveGranularity(). When a table's current settings make it false (non-adaptive granularity), the part LOAD path rejected an already-existing Compact part in two places, both deriving the part's validity from the table's current create-time policy rather than the part's physical on-disk reality:

  1. MergeTreeDataPartBuilder::build() threw the ... does not support polymorphic parts LOGICAL_ERROR directly.
  2. Even without (1), the IMergeTreeDataPart constructor initializes index_granularity_info from the current policy, building MarkType(adaptive=false, Compact), which throws Non-Wide data part type with non-adaptive granularity.

But a Compact part is always adaptive by invariant (see the MarkType constructor), and an existing on-disk part's type is a fact, not a choice. The current policy only governs the format chosen for NEW parts, which MergeTreeData::choosePartFormat() already enforces (it returns {Wide, Full} when polymorphic parts are disabled).

Fix.

  • MergeTreeIndexGranularityInfo(storage, settings, type): a Compact part type now always yields an adaptive mark type, regardless of the table's current policy. Wide keeps its policy-derived adaptivity (a Wide part may legitimately be non-adaptive). This covers both the load path and mutations that inherit a Compact source part's format.
  • MergeTreeDataPartBuilder::build(): removed the redundant polymorphic-parts guard. New-part format selection is enforced by choosePartFormat; the type reaching build() otherwise belongs to an existing part (read from disk, or inherited by a mutation), where it is a physical fact.

New-part creation behavior is unchanged. The change only makes ClickHouse load pre-existing Compact parts it currently rejects.

Added regression test 04371_load_compact_part_non_adaptive_reader.sh (a non-adaptive reader shares a plain_rewritable path with an adaptive writer and loads the writer's Compact part via SYSTEM RESTART DISK). It aborts the server without the fix and passes with it.

…daptive policy

canUsePolymorphicParts() is equivalent to canUseAdaptiveGranularity(). When a table's
current settings make it false (non-adaptive granularity), the part LOAD path rejected an
already-existing Compact part in two places, both deriving the part's validity from the
table's current create-time policy rather than the part's physical on-disk reality:

  1. MergeTreeDataPartBuilder::build() threw "... does not support polymorphic parts".
  2. Even without (1), the IMergeTreeDataPart constructor initializes index_granularity_info
     from the current policy, building MarkType(adaptive=false, Compact), which throws
     "Non-Wide data part type with non-adaptive granularity".

But a Compact part is always adaptive by invariant (see the MarkType constructor), and an
existing on-disk part's type is a fact, not a choice. The current policy only governs the
format chosen for NEW parts, which MergeTreeData::choosePartFormat() already enforces
(it returns {Wide, Full} when polymorphic parts are disabled). In debug and sanitizer
builds the exception aborts the server.

Reproduced by, for example, a read-only table that shares an on-disk path with another
table and reloads its parts via SYSTEM RESTART DISK or on server startup. Found by the
Stress test (amd_msan) check, recurring across several unrelated PRs and check types.

Fix:
  - MergeTreeIndexGranularityInfo(storage, settings, type): a Compact part type now always
    yields an adaptive mark type, regardless of the table's current policy; Wide keeps its
    policy-derived adaptivity. This covers the load path and mutations that inherit a
    Compact source part's format.
  - MergeTreeDataPartBuilder::build(): removed the redundant polymorphic-parts guard.
    New-part format selection is enforced by choosePartFormat; the type reaching build()
    otherwise belongs to an existing part where it is a physical fact.

New-part creation behavior is unchanged. Added regression test
04371_load_compact_part_non_adaptive_reader.sh.

Since a corrupt part that looks Compact is now caught while its columns are read (and
detached as broken) instead of aborting at construction, the debug/sanitizer skip in the
existing test_merge_tree_load_parts_filesystem_error is no longer needed and was removed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@groeneai

Copy link
Copy Markdown
Collaborator Author
Internal second-model review — adjudication log (click to expand)

Pre-publication review by an independent model (engine: codex; 2 findings across 1 full pass + 1 delta re-review; bounded).

# Sev Finding Verdict Evidence / action
1 💡 Comments/test still say non-adaptive tables can't load Compact parts; the debug/sanitizer skip in test_merge_tree_load_parts_filesystem_error is obsolete AGREE — fixed Verified locally: corrupt "Compact-looking" part is now detached as broken (not a build-time abort), so the skip is unneeded and the test runs on all builds. Removed the skip + updated its rationale; clarified the MergeTreeDataPartCompact.h comment (new Compact parts aren't created for non-adaptive tables, but existing ones can be loaded).
2 ⚠️ A row-changing mutation of a Compact part under index_granularity_bytes = 0 could produce one-row granules DISAGREE Scenario is unreachable and this PR does not make it reachable: a fresh igb=0 table only creates Wide parts (choosePartFormat); ATTACH PARTITION of a Compact part is rejected ("inconsistent granularity with table"); the only loader of a Compact part under a non-adaptive policy is a shared plain_rewritable path whose holder must be readonly=true (mutations rejected with TABLE_IS_PERMANENTLY_READ_ONLY); a corruption-induced Compact-looking part is detached as broken, never mutated. Also, createMergeTreeIndexGranularity returns an empty adaptive object for Compact parts, so the cited 0 / size_of_row path is not taken. No change made (avoiding a speculative defensive edit).

Severity: ❌ blocker / ⚠️ major / 💡 nit. DISAGREE verdicts carry recorded evidence and are terminal per finding.

Session id: cron:clickhouse-author-slot-1:20260723-131900

@groeneai

Copy link
Copy Markdown
Collaborator Author
Pre-PR validation gate (click to expand)
# Question Answer
a Deterministic repro? Yes. Two MergeTree tables share one plain_rewritable path; writer (adaptive) creates a Compact part; reader (index_granularity_bytes=0, non-adaptive) shares the path; SYSTEM RESTART DISK <reader-disk> reloads and loads the Compact part, aborting the server at MergeTreeDataPartBuilder::build(). Codified as 04371_load_compact_part_non_adaptive_reader.sh. Exact CI stack (STID 3286-0cd6) reproduced.
b Root cause explained? Yes. canUsePolymorphicParts() == canUseAdaptiveGranularity(). When it is false, the LOAD path rejects an existing Compact part in two spots deriving validity from the current create-time policy: (1) the build() guard throws directly; (2) the IMergeTreeDataPart ctor builds MarkType(adaptive=false, Compact) which throws. But a Compact part is always adaptive by invariant, and an existing part's type is a physical fact; new-part format is enforced separately by choosePartFormat.
c Fix matches root cause? Yes. (A) MergeTreeIndexGranularityInfo(storage,settings,type) forces adaptive=true for a Compact type (invariant), covering load + mutation-inherit. (B) removed the redundant create-time guard in build(). Directly targets the mechanism in (b).
d Test intent preserved / new tests added? Yes. New regression test 04371_.... No existing test weakened; choosePartFormat create-time enforcement untouched (sanity: small insert -> Compact; min_bytes_for_wide_part=0 -> Wide; igb=0 -> Wide). The existing test_merge_tree_load_parts_filesystem_error still detaches the corrupt part as broken (re-verified); its obsolete debug/sanitizer skip was removed so it now covers this fix on all builds.
e Both directions demonstrated? Yes. Baseline f2c8136d: test aborts the server (Received signal Aborted (6) at build():66). Fixed 1cbae537: passes (Compact / 1 Hello), server alive; 50/50 stable.
f Fix is general across code paths? Yes. Change A fixes the single point where mark-type adaptivity is decided, covering ALL load sites (13 withPartFormatFromDisk callers: load/clone/move/fetch/attach) AND mutation-inherit paths; Change B removes the guard for all callers. Not a symptom guard - it fixes the invariant at its origin.
g Fix generalizes across inputs? Yes. Keyed on part TYPE (the only dimension the guard/MarkType cares about): Compact (incl. cmrk4 with_substreams) -> adaptive by invariant; Wide keeps policy-derived adaptivity (a Wide part may legitimately be non-adaptive - unchanged). Verified Compact loads, non-adaptive table still creates Wide + queryable + survives DETACH/ATTACH.
h Backward compatible? Yes. Strictly more permissive: loads pre-existing Compact parts master currently rejects. No setting default / on-disk / wire / metadata format change; no new validation on existing data; no SettingsChangesHistory.cpp entry needed. New-part creation unchanged.
i Invariants and contracts preserved? Yes. The MarkType invariant "non-adaptive => Wide only" is upheld: Change A only ever sets adaptive=true for Compact, never adaptive=false for a non-Wide type. initializeIndexGranularityInfo still refines granularity from the on-disk mark type, so the member-init value is a consistent starting point. build()'s other assertions are untouched.

Session id: cron:clickhouse-author-slot-1:20260723-131900

@groeneai

Copy link
Copy Markdown
Collaborator Author

cc @hanfei1991 @CheSema — could you review this? It fixes a LOGICAL_ERROR (server abort in debug/sanitizer builds) when a MergeTree table loads an already-existing Compact part while its current settings disable polymorphic parts (non-adaptive granularity). The load/construction path derived the part's validity from the table's current create-time policy instead of the part's on-disk reality; a Compact part is always adaptive by invariant, so it now loads regardless, while new-part format selection stays enforced by choosePartFormat.

@PedroTadim PedroTadim added the can be tested Allows running workflows for external contributors label Jul 23, 2026
@clickhouse-gh

clickhouse-gh Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Workflow [PR], commit [77456c9]

Summary:

job_name test_name status info comment
Stateless tests (amd_tsan, parallel) FAIL
00084_external_aggregation FAIL cidb IGNORED
Stateless tests (amd_msan, WasmEdge, parallel, 2/2) FAIL
03101_analyzer_identifiers_2 FAIL cidb IGNORED

AI Review

Summary

This PR fixes the LOGICAL_ERROR raised when a readonly non-adaptive MergeTree table reloads an on-disk Compact part, and the readonly SYSTEM RESTART DISK / refresh_parts_interval path itself looks covered. I found one remaining regression: the same relaxation now lets RESTORE attach Compact parts into a writable index_granularity_bytes = 0 table, and follow-up full-part mutations rebuild them with one-row granules.

Findings

⚠️ Majors

  • [src/Storages/MergeTree/MergeTreeDataPartBuilder.cpp:61] Removing the polymorphic-parts guard also affects RESTORE ... SETTINGS allow_different_table_def = 1: loadPartRestoredFromBackup builds the backup part from disk, and StorageMergeTree::attachRestoredParts adds it without the granularity compatibility check used by partition attach/move. That makes it possible for a writable non-adaptive table to hold active Compact parts. Later DELETE / REWRITE_PARTS mutations preserve that Compact format but recompute adaptive granularity from the target table settings, where index_granularity_bytes = 0 collapses every mark to one row in computeIndexGranularity, producing pathological mutated parts.
Final Verdict

Changes requested: the readonly shared-disk load fix is reasonable, but the same relaxation must not make RESTORE accept Compact parts into writable non-adaptive tables unless the mutation path is hardened too.

@clickhouse-gh clickhouse-gh Bot added the pr-bugfix Pull request with bugfix, not backported by default label Jul 23, 2026
…r check

The new regression test 04371 configures the reader table with
index_granularity_bytes = 0 (so canUsePolymorphicParts() is false and the
Compact-part load path is exercised) but left min_bytes_for_wide_part at its
default 10485760. On a non-adaptive table a nonzero wide-part threshold makes
MergeTreeData log a "settings will be ignored" <Warning> at CREATE. The Fast
test job runs .sh tests with --send_logs_level=warning, so that warning reached
the client stderr and tripped the empty-stderr check (Reason: having stderror),
even though the query results were correct and the fix under test worked.

Set min_bytes_for_wide_part = min_rows_for_wide_part = 0 on the reader table.
These thresholds are meaningless on a non-adaptive table (that is exactly what
the warning says), and zeroing them keeps canUsePolymorphicParts() false via
index_granularity_bytes = 0, so the load-path scenario the PR fixes is still
reproduced. No source change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@groeneai

Copy link
Copy Markdown
Collaborator Author

CI finish ledger — 416ca73

CI fully finished (Config + Finish Workflow SUCCESS, >5h buffer). The earlier PR-caused Fast test failure on our new test 04371_load_compact_part_non_adaptive_reader (the benign non-adaptive-granularity <Warning> tripping the stderr matcher) is fixed on this head (04371 passes; Fast test green). Every remaining failed check has an owner; only CH Inc sync is exempt.

Check / test Reason Owner / fixing PR
Fast test / 04371_load_compact_part_non_adaptive_reader PR-caused (benign non-adaptive-granularity Warning tripping the stderr matcher) PR-caused -> fixed in this PR @ 416ca73
Integration tests (amd_tsan, 3/6) / test_replicated_database::test_replicated_table_structure_alter flaky (chronic trunk flake, 670 PRs / 142 master in 30d; issue #110036; unrelated to this MergeTree granularity PR) a fix task is being worked (issue #110036 investigation; fixing-PR link to follow here)
Upgrade check (amd_release) / Changed + New settings not reflected in settings changes history flaky/infra (165 PRs / 0 master in 7d; upgrade-check settings-history baseline) #104860 (merged) - join_runtime_filter_min_probe_rows SettingsChangesHistory entry
Upgrade check (amd_release) / Error message in clickhouse-server.log infra - DiskLocalCheckThread clickhouse_disk_checker teardown-race (0 master) #110773 (ours, open)
Sync - CH Inc sync (private, not actionable)

Session id: cron:our-pr-ci-monitor:20260724-063000

The regression test only drove the part-load path through SYSTEM RESTART DISK.
The same LOGICAL_ERROR is reachable from the background refresh task, and that
form is what CI actually hits: in the Stress reds the abort comes from a
BackgroundSchedulePool thread via

  MergeTreeDataPartBuilder::build (MergeTreeDataPartBuilder.cpp:79)
    <- MergeTreeData::loadDataPart (MergeTreeData.cpp:2487)
    <- MergeTreeData::loadDataPartWithRetries (MergeTreeData.cpp:2594)
    <- MergeTreeData::refreshDataPartsOnce (MergeTreeData.cpp:3191)
    <- MergeTreeData::refreshDataParts (MergeTreeData.cpp:3112)

rather than from a client query, so it aborts the server instead of failing a
statement. Add a second reader on the same plain_rewritable path that carries
refresh_parts_interval and no explicit restart. It is created before the insert
so the part arrives after the initial load and the refresh task is what loads
it. The reader disk stays readonly, both because refreshDataPartsOnce requires
it and because the task is only started when all disks are readonly.

The new reader keeps min_bytes_for_wide_part = 0 and min_rows_for_wide_part = 0
next to index_granularity_bytes = 0 for the same reason as the existing one: on
a non-adaptive table a nonzero threshold logs a "settings will be ignored"
warning at CREATE, which trips the Fast test empty-stderr check.

Verified both directions on a debug build. Against current master, the refresh
shape alone aborts the server with the signature above; with the fix the part
loads and the server stays up. 50 consecutive runs across 5 parallel copies pass.
Comment on lines +61 to +63
/// No polymorphic-parts policy check here: it governs only the format of NEW parts and is
/// enforced by MergeTreeData::choosePartFormat. The type reaching build() otherwise belongs to
/// an existing part (loaded from disk, or inherited by a mutation), where it is a physical fact.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assumption is too broad for the restore path. loadPartRestoredFromBackup also comes through withPartFormatFromDisk(), and StorageMergeTree::attachRestoredParts never runs canReplacePartition(), so RESTORE ... SETTINGS allow_different_table_def = 1 can now attach a backup Compact part into a writable index_granularity_bytes = 0 table.

Once that happens, follow-up DELETE / REWRITE_PARTS mutations keep part->getFormat() (StorageMergeTree.cpp:1972) but recompute granularity from the target table settings (MutateTask.cpp:2345-2350, MergeTreeDataPartWriterOnDisk.cpp:106-112). In computeIndexGranularity(), index_granularity_bytes = 0 on the adaptive path normalizes every mark to 1 row (MergeTreeIndexGranularity.cpp:142-157), so the restored part mutates into a pathological one-row-granule Compact part.

So this change does not only relax the readonly refresh/restart scenario from the PR description; it also opens a writable state that regresses post-restore mutations badly. We need to keep rejecting this combination on restore/attach (or harden the mutation path) before removing the build-time policy check here.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The mechanism is right and I reproduced it, but the conclusion that this change opens the regression is not: the collapse pre-exists this PR, and the fix already exists at the funnel you name in #111694.

I measured four things on a debug build, server buildId() matched against the ELF on every run.

1. Your scenario on this PR's head (Build ID 1cbae537) behaves exactly as you describe: RESTORE ... allow_different_table_def = 1 lands a Compact part on the writable index_granularity_bytes = 0 table, then ALTER TABLE ... DELETE rewrites it to 9999 rows with 10000 marks (data correct, 9999 / 49995000).

2. The same scenario on unmodified master (snapshot whose MergeTreeDataPartBuilder.cpp, MergeTreeIndexGranularityInfo.cpp and MergeTreeIndexGranularity.cpp are md5-identical to origin/master) does not reach the mutation at all. It aborts inside the restore with the signature this PR fixes:

Logical error: 'Cannot create part with type Compact and storage type Full because table does not support polymorphic parts'
MergeTreeDataPartBuilder.cpp:79   build()
MergeTreeData.cpp:8358            loadPartRestoredFromBackup(...)
MergeTreeData.cpp:8331            restorePartFromBackup(...)
MergeTreeData.cpp:8267            restorePartsFromBackup(...)

In a release build that exception is caught by loadPartRestoredFromBackup's own catch (...) and routed to mark_broken, so the restored part is detached as broken-from-backup and the data is silently dropped. That is the current behaviour of the state you want to keep rejecting.

3. The collapse is pre-existing and is not caused by the Compact route. On that same unmodified master snapshot I ran the identical restore with a Wide adaptive part (min_bytes_for_wide_part = 0, .mrk2 marks): RESTORED, then the mutation gives 9999 rows with 10000 marks, no abort, no <Fatal>. So an adaptive part already reaches a writable index_granularity_bytes = 0 table through RESTORE today and already mutates into one-row granules. canReplacePartition never runs on the restore path on master either. My change adds one more part type to a funnel that is already wrong; it does not create the defect, and the guard I remove is not what was protecting it.

4. #111694's guard alone covers both routes, which is why no extra restore/attach check is needed. I applied only its src/ hunk on top of this head and rebuilt (Build ID moved 1cbae537 to c08a5c57):

restored part without the guard with #111694's guard
Compact (your scenario) 9999 rows / 10000 marks 9999 rows / 2 marks
Wide (pre-existing on master) 9999 rows / 10000 marks 9999 rows / 3 marks

Reverting the hunk and rebuilding restores 10000 marks, so the guard is causal. It suffices for every entry point because MergeTreeDataPartWriterCompact.cpp:266 and MergeTreeDataPartWriterWide.cpp:339 both go through MergeTreeDataPartWriterOnDisk::computeIndexGranularity at :106 into DB::computeIndexGranularity, where that guard sits; the only other callers of it are unit tests. However the part arrived, whether by load, restore, attach, mutation inherit or a replicated merge taking the format from the log entry, the granularity is computed at that one place.

So I am keeping this PR to the load-path logical error and leaving the granularity computation to #111694, which is deliberately based on master because the defect is reachable there without this PR. I have noted the Compact-via-RESTORE route on that PR so its test matrix covers it alongside the Wide one it currently uses.

@clickhouse-gh

clickhouse-gh Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

LLVM Coverage Report

Metric Baseline Current Δ
Lines 86.20% 86.20% +0.00%
Functions 92.00% 92.00% +0.00%
Branches 78.40% 78.40% +0.00%

Changed lines: Changed C/C++ lines covered: 10/10 (100.00%) · Uncovered code

Full report · Diff report

@groeneai

Copy link
Copy Markdown
Collaborator Author

CI finish ledger - 77456c9

Every failure below has an owner: a fixing PR (mine or external), or a full-effort fix task
whose fixing-PR link will be posted here when it opens. Only CH Inc sync is exempt.

Check / test Reason Owner / fixing PR
Stateless tests (amd_tsan, parallel) / 00084_external_aggregation flaky, query-level Code 241 at the test's own max_memory_usage (would use 314.10 MiB ... maximum: 286.10 MiB, no current RSS token, so not host OOM); runner rerun 2/2 passed #112054 (external, open): raises the limit 300000000 -> 600000000 so the spilling peak keeps headroom
Stateless tests (amd_msan, WasmEdge, parallel, 2/2) / 03101_analyzer_identifiers_2 flaky session-current-database loss: Code 60 UNKNOWN_TABLE ... Unknown table expression identifier 'test1' ... Maybe you meant test_jatsykh8.test? on SELECT t.a FROM (SELECT a FROM test1) AS t; the hint naming the run's own randomized database proves current_database was not the test database at that query, since the hint generator skips the current one. Runner rerun 5/5 passed a fix task is moved to pending (investigating at full effort, fixing-PR link to follow here)
Sync - CH Inc sync (private, not actionable)

Both failures are unrelated to this diff, which only touches part-loading granularity handling and its stateless test. Fast test, Style check and all five Bugfix validation jobs are green on this head.

Session id: cron:our-pr-ci-monitor:20260728-033000

@Avogar Avogar self-assigned this Jul 29, 2026
@groeneai

Copy link
Copy Markdown
Collaborator Author

Fixing PR for 03101_analyzer_identifiers_2: #111990 (external, open).

The ledger line above attributed this to a loss of current_database, reasoning that the hint
names the run's own randomized database and that the hint generator skips the current one. That
reasoning is wrong on both halves, so I am correcting it here.

IdentifierResolver::tryGetTableNameHint starts the search from context->getCurrentDatabase()
(src/Analyzer/Resolve/IdentifierResolver.cpp:404-405), and TableNameHints::getHintForTable
returns a (database, table) pair for a match found in that very database
(src/Interpreters/DatabaseCatalog.cpp:2376); only when nothing in it is visible does it fall
through to the cross-database search that skips it. So a hint naming the test's own database is
what a correct current_database produces, and it says nothing about the current database being
lost. On an unmodified master build the same query yields Maybe you meant mydb.test? with the
current database set to mydb throughout.

The actual mechanism is session loss, not database loss. test1 is a CREATE TEMPORARY TABLE, and
temporary tables live in the server-side session (Context::external_tables_mapping, registered
through getSessionContext()->addExternalTable, src/Interpreters/InterpreterCreateQuery.cpp:2094).
The client re-establishes its connection when a ping does not come back in time, which starts a new
session; the handshake re-sends only the default database (src/Client/Connection.cpp:466), so the
temporary table is gone while currentDatabase() still reads correctly. That is why the hint is
ordinary and why the failing query is the first unqualified reference to test1 after the three
serverError statements, each of which reconnects on the expected error
(src/Client/ClientBase.cpp:3455-3465). The same run also explains the variant with no hint at all
(PR #110610): the hint appears only when a similarly named table happens to exist.

#111990 removes the ping before every query, so a slow server no longer costs the client its
session. Its description names this test and this mechanism, and it ships
04655_client_reconnect_after_expected_error.sh for the expected-error reconnect path. The sibling
half of the family, the tests that combine USE {CLICKHOUSE_DATABASE:Identifier} with unqualified
names, was fixed separately by #111982 (merged 2026-07-26); that fix is client-side and cannot cover
this test, which contains no USE and no query parameter.

Nothing here is caused by this pull request.

@Avogar
Avogar added this pull request to the merge queue Aug 12, 2026
Merged via the queue into ClickHouse:master with commit 4b34059 Aug 12, 2026
175 of 178 checks passed
@robot-ch-test-poll4 robot-ch-test-poll4 added the pr-synced-to-cloud The PR is synced to the cloud repo label Aug 12, 2026
groeneai added a commit to groeneai/ClickHouse that referenced this pull request Aug 21, 2026
groeneai added a commit to groeneai/ClickHouse that referenced this pull request Aug 21, 2026
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.
@groeneai

Copy link
Copy Markdown
Collaborator Author

This fix is missing on the active release branches, and 26.7 aborts on it in CI. Requesting the v26.7-must-backport label here so the robot opens the backport.

What happens: Upgrade check boots the previous release first and runs that release's own test suite against it (upgrade_runner.sh:148 points --queries at the --depth=1 26.7 checkout). On a recent run the 26.7 binary aborted in the pre-upgrade phase, before the new binary ever started:

  • clickhouse-server.err.log:227791 - (version 26.7.5.10 (official build), git hash: c7d5ecce0dd). Banner census over the whole file: 5184 lines from 26.7.5.10, 12 from 26.8.1.1.
  • clickhouse-server.upgrade.log (the new-binary phase) greps 0 for polymorphic in 54817 lines.
  • The failing query in the fatal is byte-identical to 26.7:tests/queries/0_stateless/03640_skip_indexes_with_or.sql, which sets index_granularity_bytes = 0, so canUseAdaptiveGranularity() is false. The stress profile sets ignore_drop_queries_probability = 1. (it is in the fatal's own Changed settings: line), so InterpreterDropQuery.cpp:220-227 silently ignores the test's DROP for a table that stores data on disk. The table survives, and the next server start reloads its Compact part into the guard removed here.

abort_on_logical_error.yaml is linked for non-fast-test runs, which is why a release build aborts rather than raising a catchable exception. On a real 26.7 server this is a LOGICAL_ERROR, not a crash, so the practical cost is CI noise on unrelated pull requests rather than a user-visible failure.

The port is clean and complete. git merge-tree --write-tree --merge-base=0083c8c9dda784c^ origin/26.7 0083c8c9dda784c returns rc=0, and in the resulting tree both load-bearing hunks are present: the guard removal in MergeTreeDataPartBuilder.cpp and the || type_ != MergeTreeDataPartType::Wide change in MergeTreeIndexGranularityInfo.cpp. The second one matters on its own - without it the port still throws "Non-Wide data part type with non-adaptive granularity". 04371_load_compact_part_non_adaptive_reader.sh comes along, and no setting is touched, so there is no SettingsChangesHistory entry to lose.

Asking only for 26.7, which is where the abort was observed. select_backport_branches expands v26.7-must-backport to exactly ['26.7']. 26.6, 26.5 and 26.3 carry the same guard and also port cleanly if you want them; 25.8 conflicts in MergeTreeIndexGranularityInfo.cpp and would need a hand-adapted port, so pr-must-backport is not the right label here.

@groeneai

Copy link
Copy Markdown
Collaborator Author

Hi @alexey-milovidov, gentle ping on the request above: could you apply v26.7-must-backport here? The guard removed in this PR is still present on 26.7 and absent on master, and on 26.7 it aborts Upgrade check in the pre-upgrade phase. The port is clean (git merge-tree rc=0, both load-bearing hunks present) and select_backport_branches expands the label to exactly ['26.7'].

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

can be tested Allows running workflows for external contributors pr-bugfix Pull request with bugfix, not backported by default pr-synced-to-cloud The PR is synced to the cloud repo

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants