From 670a265c8e108995ce8cefef2036e9d384abb8c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Rafael?= Date: Tue, 10 Mar 2026 10:12:48 +0000 Subject: [PATCH] Fix sqlite delete hang by deferring rowid deletes --- src/storage/sqlite_delete.cpp | 27 +++++++++----- .../sql/storage/attach_delete_hang_repro.test | 36 +++++++++++++++++++ 2 files changed, 55 insertions(+), 8 deletions(-) create mode 100644 test/sql/storage/attach_delete_hang_repro.test diff --git a/src/storage/sqlite_delete.cpp b/src/storage/sqlite_delete.cpp index 8b28385..2409c46 100644 --- a/src/storage/sqlite_delete.cpp +++ b/src/storage/sqlite_delete.cpp @@ -6,6 +6,7 @@ #include "storage/sqlite_transaction.hpp" #include "sqlite_db.hpp" #include "sqlite_stmt.hpp" +#include namespace duckdb { @@ -25,7 +26,10 @@ class SQLiteDeleteGlobalState : public GlobalSinkState { SQLiteTableEntry &table; SQLiteStatement statement; + std::mutex rowid_lock; + vector rowids; idx_t delete_count; + bool delete_done = false; }; string GetDeleteSQL(const string &table_name) { @@ -38,9 +42,7 @@ string GetDeleteSQL(const string &table_name) { unique_ptr SQLiteDelete::GetGlobalSinkState(ClientContext &context) const { auto &sqlite_table = table.Cast(); - auto &transaction = SQLiteTransaction::Get(context, sqlite_table.catalog); auto result = make_uniq(sqlite_table); - result->statement = transaction.GetDB().Prepare(GetDeleteSQL(sqlite_table.name)); return std::move(result); } @@ -53,12 +55,8 @@ SinkResultType SQLiteDelete::Sink(ExecutionContext &context, DataChunk &chunk, O chunk.Flatten(); auto &row_identifiers = chunk.data[row_id_index]; auto row_data = FlatVector::GetData(row_identifiers); - for (idx_t i = 0; i < chunk.size(); i++) { - gstate.statement.Bind(0, row_data[i]); - gstate.statement.Step(); - gstate.statement.Reset(); - } - gstate.delete_count += chunk.size(); + std::lock_guard lock(gstate.rowid_lock); + gstate.rowids.insert(gstate.rowids.end(), row_data, row_data + chunk.size()); return SinkResultType::NEED_MORE_INPUT; } @@ -67,6 +65,19 @@ SinkResultType SQLiteDelete::Sink(ExecutionContext &context, DataChunk &chunk, O //===--------------------------------------------------------------------===// SourceResultType SQLiteDelete::GetDataInternal(ExecutionContext &context, DataChunk &chunk, OperatorSourceInput &input) const { auto &insert_gstate = sink_state->Cast(); + if (!insert_gstate.delete_done) { + if (!insert_gstate.statement.IsOpen()) { + auto &transaction = SQLiteTransaction::Get(context.client, insert_gstate.table.catalog); + insert_gstate.statement = transaction.GetDB().Prepare(GetDeleteSQL(insert_gstate.table.name)); + } + for (auto row_id : insert_gstate.rowids) { + insert_gstate.statement.Bind(0, row_id); + insert_gstate.statement.Step(); + insert_gstate.statement.Reset(); + } + insert_gstate.delete_count = insert_gstate.rowids.size(); + insert_gstate.delete_done = true; + } chunk.SetCardinality(1); chunk.SetValue(0, 0, Value::BIGINT(insert_gstate.delete_count)); diff --git a/test/sql/storage/attach_delete_hang_repro.test b/test/sql/storage/attach_delete_hang_repro.test new file mode 100644 index 0000000..f54f095 --- /dev/null +++ b/test/sql/storage/attach_delete_hang_repro.test @@ -0,0 +1,36 @@ +# name: test/sql/storage/attach_delete_hang_repro.test +# description: reproduce large full-table delete behavior on attached SQLite table +# group: [sqlite_storage] + +require sqlite_scanner + +statement ok +SET threads=8 + +statement ok +ATTACH '__TEST_DIR__/attach_delete_hang_repro.db' AS s (TYPE SQLITE) + +statement ok +CREATE TABLE s.t(i BIGINT, p VARCHAR, b BOOLEAN, ts TIMESTAMPTZ); + +query I +INSERT INTO s.t +SELECT i, 'f/' || i::VARCHAR || '.parquet', TRUE, now() +FROM range(50000) t(i); +---- +50000 + +query I +SELECT COUNT(*) FROM s.t; +---- +50000 + +query I +DELETE FROM s.t; +---- +50000 + +query I +SELECT COUNT(*) FROM s.t; +---- +0