Refused DROP/TRUNCATE DATABASE cancels merges forever on surviving tables
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 dbby routing it away from the preparation path, but leavesDROP DATABASEand bareTRUNCATE DATABASEon it -- andTRUNCATE DATABASEis documented to keep the database alive.
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:
src/Interpreters/InterpreterDropQuery.cpp:579—prepare_tablesenqueuesflushAndPrepareForShutdownfor every table before any refusal can happensrc/Interpreters/InterpreterDropQuery.cpp:611—prepare_tables(tables_to_prepare);-- unconditional for DROP / DETACH / bare TRUNCATE DATABASEsrc/Interpreters/InterpreterDropQuery.cpp:362—table->checkTableCanBeDropped(context_);-- the size guard, reached only after preparationsrc/Interpreters/InterpreterDropQuery.cpp:683—query_status->throwIfKilled();-- the KILL QUERY trigger, also after preparationsrc/Interpreters/InterpreterDropQuery.cpp:692— existing comment acknowledges the limbo state but predates #98161, when the call was still a no-op for MergeTreesrc/Storages/StorageMergeTree.cpp:294—merges_blocker.cancelForever()-- irreversible for a storage that stays alive
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
ReproducerCREATE 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
1Error message and/or stacktrace
Actual output of the reproducer above on master (2d19d1b6e073):
(identical on both runs)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.
Same pattern as #118663 (found by: fix_path; fix #119916 (open) touches src/Interpreters/InterpreterDropQuery.cpp).
Open risks:
DETACH DATABASEalso 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
Source: ClickHouse/ClickHouse