#120643·ClickHouse

Refused DROP/TRUNCATE DATABASE cancels merges forever on surviving tables

Author: clickgapaiCreated Sep 17, 2026Updated Sep 17, 2026
Labelscomp-ddl

Describe what's wrong

When DROP DATABASE db or TRUNCATE DATABASE db is refused part-way (size guard, dependency check, KILL QUERY), every table left behind stays attached, readable and writable but can never merge again. OPTIMIZE TABLE ... FINAL throws Code: 236 ... Cancelled merging parts. (ABORTED) and background merges never run, so parts accumulate until Too many parts starts rejecting INSERTs. Only a server restart or DETACH/ATTACH clears it.

  • Root cause: The irreversible preparation is done for ALL tables up-front (line 611) while every condition that can refuse the operation is evaluated per-table AFTER it (line 362 / line 683). One refused table therefore permanently disables merges and moves on every table in the database that survives. This PR fixes exactly this shape for TRUNCATE [ALL] TABLES FROM db by routing it away from the preparation path, but leaves DROP DATABASE and bare TRUNCATE DATABASE on it -- and TRUNCATE DATABASE is documented to keep the database alive.
Analysis details (evidence, affected locations, impact)

Why we believe this is a bug: executeToDatabaseImpl ([src/Interpreters/InterpreterDropQuery.cpp:476](https://github.com/ClickHouse/ClickHouse/blob/2d19d1b6e073/src/Interpreters/InterpreterDropQuery.cpp#L476)) -> prepare_tables(tables_to_prepare) at line 611 -> my_table_ptr->flushAndPrepareForShutdown() at line 581 -> StorageMergeTree::flushAndPrepareForShutdown -> merger_mutator.merges_blocker.cancelForever() ([src/Storages/StorageMergeTree.cpp:294](https://github.com/ClickHouse/ClickHouse/blob/2d19d1b6e073/src/Storages/StorageMergeTree.cpp#L294)) plus background_operations_assignee.finish() (line 297). Only afterwards does the per-table loop at 681-696 run, where checkTableCanBeDropped (line 362) or throwIfKilled (line 683) can abort it.

Affected locations:

Impact: A DROP DATABASE stopped by the default 50 GB max_table_size_to_drop guard -- the exact case the guard exists for -- silently bricks merges on every remaining table in that database. Same for KILL QUERY on a slow DROP DATABASE, and for TRUNCATE DATABASE, which is documented to keep the database. Symptom is immediate for OPTIMIZE ... FINAL (ABORTED) and progressive for writes (parts grow without bound until Too many parts). Regression window: #98161, merged 2026-02-27, i.e. 26.3 and later.

Does it reproduce on most recent release?

Yes — confirmed on current master (commit 2d19d1b6e073).

How to reproduce

▶ Run on ClickHouse Fiddle

Reproducer
sql
CREATE DATABASE IF NOT EXISTS fiddle_db;

-- Test: a TRUNCATE/DROP DATABASE refused by max_table_size_to_drop must leave the surviving tables mergeable.

DROP DATABASE IF EXISTS fiddle_db_1;
CREATE DATABASE fiddle_db_1;

CREATE TABLE fiddle_db_1.t (id UInt64) ENGINE = MergeTree ORDER BY id;
INSERT INTO fiddle_db_1.t VALUES (100);
TRUNCATE DATABASE fiddle_db_1 SETTINGS max_table_size_to_drop = 1; -- { serverError TABLE_SIZE_EXCEEDS_MAX_DROP_SIZE_LIMIT }
INSERT INTO fiddle_db_1.t VALUES (7);
INSERT INTO fiddle_db_1.t VALUES (16);
OPTIMIZE TABLE fiddle_db_1.t FINAL;
SELECT count(), sum(id) FROM fiddle_db_1.t;
SELECT count() FROM system.parts WHERE database = 'fiddle_db_1' AND table = 't' AND active;

DROP TABLE fiddle_db_1.t;
CREATE TABLE fiddle_db_1.t (id UInt64) ENGINE = MergeTree ORDER BY id;
INSERT INTO fiddle_db_1.t VALUES (100);
DROP DATABASE fiddle_db_1 SETTINGS max_table_size_to_drop = 1; -- { serverError TABLE_SIZE_EXCEEDS_MAX_DROP_SIZE_LIMIT }
INSERT INTO fiddle_db_1.t VALUES (7);
INSERT INTO fiddle_db_1.t VALUES (16);
OPTIMIZE TABLE fiddle_db_1.t FINAL;
SELECT count(), sum(id) FROM fiddle_db_1.t;
SELECT count() FROM system.parts WHERE database = 'fiddle_db_1' AND table = 't' AND active;

DROP TABLE fiddle_db_1.t;
DROP DATABASE fiddle_db_1;

Expected behavior

Expected output of the reproducer above:

3	123
1
3	123
1

Error message and/or stacktrace

Actual output of the reproducer above on master (2d19d1b6e073):

(identical on both runs)
Suggested fix

Move the refusal checks ahead of the irreversible preparation: run checkTableCanBeDropped / checkTableCanBeRemovedOrRenamed and the kill check for the whole tables_to_drop list before calling prepare_tables at line 611. Alternatively make StorageMergeTree::flushAndPrepareForShutdown reversible (a scoped ActionLock plus re-start() of the assignees) so an aborted database operation can restore the storages it prepared.

Additional context

Same pattern as #118663 (found by: fix_path; fix #119916 (open) touches src/Interpreters/InterpreterDropQuery.cpp).

Open risks:

  • DETACH DATABASE also runs through line 611; there the storages are destroyed on success, but a refused detach (assertCanBeDetached) would leave the same limbo -- not measured here.
  • The surviving tables also lost cleanup_thread (StorageMergeTree.cpp:301), so outdated parts are never cleaned up either; I only measured the merge symptom.

Found during automated review of PR #119425; whether that PR introduced it could not be established, so nobody is tagged. Severity P1 · Finding h_pr119425_001