From a296bd6c3cb57a660d1953dcac175726e20a31ce Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Wed, 27 May 2026 13:52:56 +0800 Subject: [PATCH] fix(duckdb): explicitly close connections to release file locks on Windows DuckDB opens database files with exclusive locks on Windows. Previously connections were only released via Drop, which may not release the file handle synchronously, causing "file already in use" errors when reconnecting or testing connections in quick succession. --- crates/dbx-core/src/connection.rs | 4 +++- crates/dbx-core/src/db/duckdb_driver.rs | 24 ++++++++++++++++++++++++ src-tauri/src/commands/connection.rs | 9 ++++++--- 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/crates/dbx-core/src/connection.rs b/crates/dbx-core/src/connection.rs index 8d9533e34a..8635a897b3 100644 --- a/crates/dbx-core/src/connection.rs +++ b/crates/dbx-core/src/connection.rs @@ -529,7 +529,9 @@ impl AppState { .cloned() .collect(); for key in keys_to_remove { - conns.remove(&key); + if let Some(PoolKind::DuckDb(con)) = conns.remove(&key) { + crate::db::duckdb_driver::close_connection(con); + } } } diff --git a/crates/dbx-core/src/db/duckdb_driver.rs b/crates/dbx-core/src/db/duckdb_driver.rs index cf3b6238c6..5d2bfeb8d0 100644 --- a/crates/dbx-core/src/db/duckdb_driver.rs +++ b/crates/dbx-core/src/db/duckdb_driver.rs @@ -54,6 +54,30 @@ pub fn is_memory_database_path(path: &str) -> bool { path.trim().eq_ignore_ascii_case(":memory:") } +/// Closes a DuckDB connection, releasing the file lock. +/// +/// Unlike relying on Drop, this calls `duckdb_disconnect` synchronously +/// so the file handle is released before this function returns. +/// On Windows this prevents "file already in use" errors when reconnecting. +pub fn close_connection(con: Arc>) { + match Arc::try_unwrap(con) { + Ok(mutex) => { + match mutex.into_inner() { + Ok(conn) => { + let _ = conn.close(); + } + Err(poisoned) => { + let _ = poisoned.into_inner().close(); + } + } + } + Err(_) => { + // Arc still referenced elsewhere (e.g. running queries); + // the last holder will drop and close the connection. + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src-tauri/src/commands/connection.rs b/src-tauri/src/commands/connection.rs index 905411922a..7b916a5528 100644 --- a/src-tauri/src/commands/connection.rs +++ b/src-tauri/src/commands/connection.rs @@ -269,8 +269,9 @@ pub async fn test_connection(state: State<'_, Arc>, config: Connection if state.duckdb_existing_pool_is_usable_for_config(&config).await? { Ok("Connection successful".to_string()) } else { - db::duckdb_driver::connect_path(&expand_tilde(&config.host)) - .map(|_| "Connection successful".to_string()) + let con = db::duckdb_driver::connect_path(&expand_tilde(&config.host))?; + dbx_core::db::duckdb_driver::close_connection(con); + Ok("Connection successful".to_string()) } } DatabaseType::MongoDb => { @@ -474,7 +475,9 @@ pub async fn disconnect_db(state: State<'_, Arc>, connection_id: Strin PoolKind::Postgres(p) => p.close(), PoolKind::Sqlite(_) => {} PoolKind::Redis(_) => {} - PoolKind::DuckDb(_) => {} + PoolKind::DuckDb(con) => { + dbx_core::db::duckdb_driver::close_connection(con); + } PoolKind::MongoDb(_) => {} PoolKind::ClickHouse(_) => {} PoolKind::SqlServer(_) => {}