Skip to main content

better_duck_core/asynchronous/
connection.rs

1use std::{path::Path, sync::Arc};
2
3use parking_lot::Mutex;
4
5use crate::{
6    connection::Connection,
7    error::{Error, Result},
8    raw::{
9        connection::{ConnectionInner, QueryControl},
10        statement::CachedStatement,
11    },
12    result_set::ResultSet,
13    types::{appendable::AppendAble, value::DuckValue},
14    Appender,
15};
16
17/// An async facade over a [`Connection`].
18///
19/// Every method dispatches to `tokio::task::spawn_blocking`, so the connection
20/// never blocks the async executor. The handle is cheap to clone; clones share
21/// one connection, serialized by an internal mutex.
22///
23/// # Cancellation
24///
25/// Query control ([`query_control`](AsyncConnection::query_control),
26/// [`interrupt`](AsyncConnection::interrupt)) goes through a separate
27/// [`QueryControl`] source that does **not** take the connection mutex, so it
28/// works *while* a native query holds that mutex on a blocking thread.
29///
30/// The query-executing futures are cancel-on-drop: dropping one requests
31/// interruption of the native query via that control. Note the honest limitation
32/// — `tokio::task::spawn_blocking` tasks cannot be aborted, so dropping the
33/// future does not instantly stop native work; it signals DuckDB to interrupt
34/// and lets the blocking task wind down. The connection becomes usable again once
35/// that task releases the mutex.
36///
37/// # Panics
38///
39/// These methods panic if called outside a Tokio runtime context, matching
40/// `tokio::task::spawn_blocking`.
41#[derive(Clone)]
42pub struct AsyncConnection {
43    inner: Arc<Mutex<Connection>>,
44    /// A mutex-free handle to the same underlying connection, used only to mint
45    /// [`QueryControl`]s for interrupt/progress. Points at the *same*
46    /// `ConnectionInner` as `inner`'s `Connection`, so it observes the generation
47    /// that query execution advances.
48    control_src: Arc<ConnectionInner>,
49}
50
51impl AsyncConnection {
52    /// Wraps an existing [`Connection`] for async use.
53    pub fn new(conn: Connection) -> AsyncConnection {
54        let control_src = Arc::clone(conn.inner());
55        AsyncConnection { inner: Arc::new(Mutex::new(conn)), control_src }
56    }
57
58    /// Recovers the inner [`Connection`] if this is the last handle.
59    ///
60    /// Returns `self` unchanged (as `Err`) if other clones of this handle exist.
61    pub fn try_into_inner(self) -> std::result::Result<Connection, AsyncConnection> {
62        let control_src = Arc::clone(&self.control_src);
63        Arc::try_unwrap(self.inner)
64            .map(Mutex::into_inner)
65            .map_err(|inner| AsyncConnection { inner, control_src })
66    }
67
68    /// Returns a [`QueryControl`] for the query currently running (or next to run)
69    /// on this connection.
70    ///
71    /// Does not take the connection mutex, so it can be called — and used to
72    /// interrupt — while a query holds that mutex on a blocking thread.
73    #[must_use]
74    pub fn query_control(&self) -> QueryControl {
75        QueryControl::from_inner(Arc::clone(&self.control_src))
76    }
77
78    /// Requests interruption of the query currently running on this connection.
79    ///
80    /// Returns `true` if an interrupt was signalled to DuckDB. Non-blocking and
81    /// safe to call from any thread while a query runs.
82    pub fn interrupt(&self) -> bool {
83        self.query_control().interrupt()
84    }
85
86    /// Opens a connection to a DuckDB database at the given file path.
87    ///
88    /// # Errors
89    ///
90    /// Returns an error if the database cannot be opened.
91    pub async fn open<P>(path: P) -> Result<AsyncConnection>
92    where
93        P: AsRef<Path> + Send + 'static,
94    {
95        let conn = tokio::task::spawn_blocking(move || Connection::open(path))
96            .await
97            .map_err(|e| Error::BackgroundTaskFailed(e.to_string()))??;
98        Ok(AsyncConnection::new(conn))
99    }
100
101    /// Opens an in-memory DuckDB connection.
102    ///
103    /// # Errors
104    ///
105    /// Returns an error if the connection cannot be established.
106    pub async fn open_in_memory() -> Result<AsyncConnection> {
107        let conn = tokio::task::spawn_blocking(Connection::open_in_memory)
108            .await
109            .map_err(|e| Error::BackgroundTaskFailed(e.to_string()))??;
110        Ok(AsyncConnection::new(conn))
111    }
112
113    /// Runs an arbitrary closure against the connection on a blocking thread.
114    ///
115    /// This is the primitive every other method on this type is built from. Use
116    /// it directly for transactions and anything the typed helpers don't cover.
117    ///
118    /// # Errors
119    ///
120    /// Returns an error if the closure returns one, or if the background task
121    /// panics or is cancelled.
122    pub async fn with_connection<F, T>(
123        &self,
124        f: F,
125    ) -> Result<T>
126    where
127        F: FnOnce(&mut Connection) -> Result<T> + Send + 'static,
128        T: Send + 'static,
129    {
130        let inner = Arc::clone(&self.inner);
131        let handle = tokio::task::spawn_blocking(move || {
132            let mut guard = inner.lock();
133            f(&mut guard)
134        });
135
136        // Interrupt the native query if this future is dropped before the blocking
137        // task finishes. `spawn_blocking` tasks cannot be aborted, so this does not
138        // stop the task instantly; it signals DuckDB to interrupt, letting the task
139        // wind down and release the connection. The guard is disarmed on normal
140        // completion so a finished query is never spuriously interrupted.
141        let mut interrupt_on_drop = InterruptOnDrop::new(self.query_control());
142        let result = handle.await.map_err(|e| Error::BackgroundTaskFailed(e.to_string()));
143        interrupt_on_drop.disarm();
144        result?
145    }
146
147    /// Executes one or more SQL statements separated by semicolons.
148    ///
149    /// # Errors
150    ///
151    /// Returns an error if any statement fails to execute.
152    pub async fn execute_batch<S>(
153        &self,
154        sql: S,
155    ) -> Result<()>
156    where
157        S: Into<String> + Send,
158    {
159        let sql = sql.into();
160        self.with_connection(move |conn| conn.execute_batch(&sql)).await
161    }
162
163    /// Prepares and executes a SQL statement, materializing the result.
164    ///
165    /// # Errors
166    ///
167    /// Returns an error if DuckDB cannot prepare or execute the statement.
168    pub async fn execute<S>(
169        &self,
170        sql: S,
171    ) -> Result<ResultSet>
172    where
173        S: Into<String> + Send,
174    {
175        let sql = sql.into();
176        self.with_connection(move |conn| conn.execute(&sql)?.materialize()).await
177    }
178
179    /// Prepares and executes a parameterized SQL statement, materializing the result.
180    ///
181    /// # Errors
182    ///
183    /// Returns an error if preparation, binding, or execution fails.
184    pub async fn execute_with<S>(
185        &self,
186        sql: S,
187        binds: Vec<DuckValue>,
188    ) -> Result<ResultSet>
189    where
190        S: Into<String> + Send,
191    {
192        let sql = sql.into();
193        self.with_connection(move |conn| {
194            let mut owned = binds;
195            let mut refs: Vec<&mut dyn AppendAble> =
196                owned.iter_mut().map(|v| v as &mut dyn AppendAble).collect();
197            conn.execute_with(&sql, &mut refs)?.materialize()
198        })
199        .await
200    }
201
202    /// Prepares and executes `sql` **incrementally**, stepping the query one
203    /// DuckDB task per `spawn_blocking` dispatch and yielding to the async runtime
204    /// between tasks.
205    ///
206    /// This is the async form of [`CachedStatement::pending`]: rather than run the
207    /// whole query inside one blocking call, each `execute_task` runs on its own
208    /// blocking dispatch, so a long query neither monopolises a blocking thread nor
209    /// blocks cancellation. Dropping the returned future stops stepping and, like
210    /// every query path here, requests interruption of the in-flight task via the
211    /// mutex-free [`QueryControl`].
212    ///
213    /// # Errors
214    ///
215    /// Returns an error if preparation or any execution task fails.
216    pub async fn execute_pending<S>(
217        &self,
218        sql: S,
219    ) -> Result<ResultSet>
220    where
221        S: Into<String> + Send,
222    {
223        use crate::raw::pending::{OwnedPending, PendingState};
224
225        let sql = sql.into();
226
227        // Prepare the statement and create the owned pending on one dispatch.
228        let mut pending: OwnedPending = self
229            .with_connection(move |conn| CachedStatement::prepare(conn.db(), &sql)?.into_pending())
230            .await?;
231
232        // Step one task per dispatch, yielding between tasks. `OwnedPending` is
233        // `Send` and `'static`, so it moves in and out of each blocking task; the
234        // connection mutex is held only for the duration of a single `execute_task`.
235        loop {
236            // Move the pending into the blocking task, step once, move it back out
237            // alongside the resulting state.
238            let (next, returned) = self
239                .dispatch(move || {
240                    let state = pending.execute_task();
241                    (state, pending)
242                })
243                .await?;
244            pending = returned;
245
246            match next {
247                PendingState::Ready => break,
248                PendingState::NotReady => {
249                    // This thread has more of the query to run; yield so the runtime
250                    // can poll other tasks / observe a drop, then step again.
251                    tokio::task::yield_now().await;
252                },
253                PendingState::NoTasksAvailable => {
254                    // No task for this thread — the pipeline is owned by background
255                    // workers, or the query is trivial and only finalises in `execute`.
256                    // Stepping would never itself report `Ready` in that case, so stop
257                    // and let `execute` below drive it to completion; cancellation is
258                    // still honoured by the interrupt-on-drop guard around that dispatch.
259                    break;
260                },
261                PendingState::Error => {
262                    // Surface DuckDB's error; `execute` below would also, but this
263                    // avoids a redundant dispatch.
264                    let msg = pending.error();
265                    return Err(Error::Engine(crate::error::EngineError::unavailable(Some(
266                        msg.unwrap_or_else(|| "pending execution failed".to_owned()),
267                    ))));
268                },
269                PendingState::Unknown(_) => {
270                    // Treat an unrecognised state as "keep stepping" but yield first.
271                    tokio::task::yield_now().await;
272                },
273            }
274        }
275
276        // Materialise the final result on a last dispatch, then own it off-thread.
277        self.dispatch(move || pending.execute()?.materialize()).await?
278    }
279
280    /// Runs `f` on a blocking thread with cancel-on-drop interruption, but without
281    /// taking the connection mutex — `f` owns whatever handles it needs. Used by
282    /// [`execute_pending`](AsyncConnection::execute_pending) to move an
283    /// `OwnedPending` in and out of each dispatch.
284    async fn dispatch<F, T>(
285        &self,
286        f: F,
287    ) -> Result<T>
288    where
289        F: FnOnce() -> T + Send + 'static,
290        T: Send + 'static,
291    {
292        let handle = tokio::task::spawn_blocking(f);
293        let mut interrupt_on_drop = InterruptOnDrop::new(self.query_control());
294        let out = handle.await.map_err(|e| Error::BackgroundTaskFailed(e.to_string()));
295        interrupt_on_drop.disarm();
296        out
297    }
298
299    /// Runs a closure against a bulk-insert [`Appender`] for `table`/`schema` on a
300    /// blocking thread.
301    ///
302    /// The appender never leaves the closure — it holds raw FFI pointers, so it
303    /// cannot be exposed as a standalone async handle.
304    ///
305    /// # Errors
306    ///
307    /// Returns an error if the table does not exist, the appender cannot be
308    /// created, or the closure returns an error.
309    pub async fn with_appender<F, T>(
310        &self,
311        table: impl Into<String> + Send,
312        schema: impl Into<String> + Send,
313        f: F,
314    ) -> Result<T>
315    where
316        F: FnOnce(&mut Appender) -> Result<T> + Send + 'static,
317        T: Send + 'static,
318    {
319        let table = table.into();
320        let schema = schema.into();
321        self.with_connection(move |conn| {
322            let mut appender = conn.appender(&table, &schema)?;
323            f(&mut appender)
324        })
325        .await
326    }
327
328    /// Reads the progress of the query currently running on this connection.
329    ///
330    /// Returns `None` if no query is running. Non-blocking — reads through the
331    /// mutex-free control source.
332    #[must_use]
333    pub fn progress(&self) -> Option<crate::raw::connection::QueryProgress> {
334        self.query_control().progress()
335    }
336}
337
338/// Requests interruption of the running query when dropped, unless disarmed.
339///
340/// Used to make the async query futures cancel-on-drop: if the caller drops the
341/// future (or it is cancelled) before the blocking task completes, the guard
342/// signals DuckDB to interrupt the native query. On normal completion the guard
343/// is [`disarm`](InterruptOnDrop::disarm)ed so no interrupt is sent.
344///
345/// Generation scoping in [`QueryControl`] makes a late interrupt safe: if the
346/// query already finished, the control is stale and `interrupt` is a no-op, so a
347/// subsequent query on the same connection is never hit.
348struct InterruptOnDrop {
349    control: QueryControl,
350    armed: bool,
351}
352
353impl InterruptOnDrop {
354    fn new(control: QueryControl) -> InterruptOnDrop {
355        InterruptOnDrop { control, armed: true }
356    }
357
358    fn disarm(&mut self) {
359        self.armed = false;
360    }
361}
362
363impl Drop for InterruptOnDrop {
364    fn drop(&mut self) {
365        if self.armed {
366            self.control.interrupt();
367        }
368    }
369}
370
371#[cfg(test)]
372mod tests {
373    use super::*;
374
375    fn assert_send<T: Send>(_: T) {}
376    fn assert_send_sync<T: Send + Sync>() {}
377    fn assert_clone<T: Clone>() {}
378
379    #[test]
380    fn async_connection_is_send_sync_clone() {
381        assert_send_sync::<AsyncConnection>();
382        assert_clone::<AsyncConnection>();
383    }
384
385    #[tokio::test]
386    async fn execute_future_is_send() {
387        let conn = AsyncConnection::open_in_memory().await.unwrap();
388        assert_send(conn.execute("SELECT 1"));
389    }
390
391    #[tokio::test]
392    async fn execute_batch_then_execute() {
393        let conn = AsyncConnection::open_in_memory().await.unwrap();
394        conn.execute_batch("CREATE TABLE t (id INTEGER)").await.unwrap();
395        conn.execute_batch("INSERT INTO t VALUES (1)").await.unwrap();
396        let result = conn.execute("SELECT id FROM t").await.unwrap();
397        assert_eq!(result.len(), 1);
398    }
399
400    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
401    async fn execute_pending_steps_a_query_to_completion() {
402        let conn = AsyncConnection::open_in_memory().await.unwrap();
403        conn.execute_batch("CREATE TABLE t (id INTEGER)").await.unwrap();
404        conn.execute_batch("INSERT INTO t VALUES (1), (2), (3)").await.unwrap();
405
406        // Incremental stepping yields the same result as one-shot execute().
407        let set = conn.execute_pending("SELECT count(*) AS n FROM t").await.unwrap();
408        match set.rows()[0].get("n").unwrap() {
409            DuckValue::BigInt(n) => assert_eq!(*n, 3),
410            other => panic!("expected BigInt(3), got {other:?}"),
411        }
412    }
413
414    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
415    async fn execute_pending_reports_a_prepare_error() {
416        let conn = AsyncConnection::open_in_memory().await.unwrap();
417        // A reference to a missing table fails while preparing the statement (the
418        // catalog lookup happens at prepare, before pending), so the pending path
419        // must surface that error — a `DuckDBFailure` from the prepare adapter —
420        // rather than hang. (Engine-typed errors come from the *execution* path;
421        // prepare failures keep the DuckDBFailure shape.)
422        let err = conn.execute_pending("SELECT * FROM no_such_table").await.unwrap_err();
423        assert!(
424            matches!(err, Error::DuckDBFailure(..) | Error::Engine(_)),
425            "expected a prepare/engine error, got {err:?}"
426        );
427    }
428
429    #[tokio::test]
430    async fn execute_with_owned_binds() {
431        let conn = AsyncConnection::open_in_memory().await.unwrap();
432        conn.execute_batch("CREATE TABLE t (id INTEGER)").await.unwrap();
433        conn.execute_batch("INSERT INTO t VALUES (1), (2), (3)").await.unwrap();
434        let result = conn
435            .execute_with("SELECT id FROM t WHERE id = $1", vec![DuckValue::Int(2)])
436            .await
437            .unwrap();
438        assert_eq!(result.len(), 1);
439    }
440
441    #[tokio::test]
442    async fn error_variant_survives_boundary() {
443        let conn = AsyncConnection::open_in_memory().await.unwrap();
444        let err = conn.execute_batch("NOT VALID SQL").await.unwrap_err();
445        // A malformed statement is a typed engine error (parser/syntax) that must
446        // survive being moved out of the blocking task and across the await point.
447        assert!(matches!(err, Error::Engine(_)), "unexpected error variant: {err:?}");
448    }
449
450    #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
451    async fn concurrent_queries_on_cloned_handles() {
452        let conn = AsyncConnection::open_in_memory().await.unwrap();
453        conn.execute_batch("CREATE TABLE t (id INTEGER)").await.unwrap();
454        let mut handles = Vec::new();
455        for i in 0..8 {
456            let c = conn.clone();
457            handles.push(tokio::spawn(async move {
458                c.execute_batch(format!("INSERT INTO t VALUES ({i})")).await.unwrap();
459            }));
460        }
461        for h in handles {
462            h.await.unwrap();
463        }
464        let result = conn.execute("SELECT count(*) AS c FROM t").await.unwrap();
465        match result.rows()[0].get("c").unwrap() {
466            DuckValue::BigInt(n) => assert_eq!(*n, 8),
467            other => panic!("expected BigInt, got {other:?}"),
468        }
469    }
470
471    #[tokio::test]
472    async fn with_appender_bulk_insert() {
473        let conn = AsyncConnection::open_in_memory().await.unwrap();
474        conn.execute_batch("CREATE TABLE t (id INTEGER)").await.unwrap();
475        conn.with_appender("t", "main", |appender| {
476            for i in 0..100i32 {
477                appender.append(&mut DuckValue::Int(i))?;
478            }
479            Ok(())
480        })
481        .await
482        .unwrap();
483        let result = conn.execute("SELECT count(*) AS c FROM t").await.unwrap();
484        match result.rows()[0].get("c").unwrap() {
485            DuckValue::BigInt(n) => assert_eq!(*n, 100),
486            other => panic!("expected BigInt, got {other:?}"),
487        }
488    }
489
490    #[tokio::test]
491    async fn interrupt_is_a_noop_when_idle_and_control_is_send_sync() {
492        fn assert_send_sync<T: Send + Sync>() {}
493        assert_send_sync::<QueryControl>();
494
495        let conn = AsyncConnection::open_in_memory().await.unwrap();
496        // A fresh control targets the *next* query to run, so it is active and its
497        // interrupt signals DuckDB (which harmlessly no-ops with no query running).
498        // After a query completes, that control goes stale.
499        let control = conn.query_control();
500        assert!(control.is_active(), "a fresh control targets the next query");
501        conn.execute_batch("CREATE TABLE t (id INTEGER)").await.unwrap();
502        assert!(!control.is_active(), "control is stale once a query has run");
503        assert!(!control.interrupt(), "stale interrupt is a no-op");
504    }
505
506    /// The core guarantee: control (interrupt/progress) never waits on the
507    /// mutex a running native query holds. A query runs on one task while the main
508    /// task calls `interrupt()`/`progress()` — those must return *promptly*, not
509    /// block until the query releases the connection.
510    #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
511    async fn control_does_not_block_on_the_running_query() {
512        use std::time::Duration;
513
514        let conn = AsyncConnection::open_in_memory().await.unwrap();
515        conn.execute_batch("CREATE TABLE t (id INTEGER)").await.unwrap();
516
517        // Run a query that takes a little while on a background task holding the
518        // connection mutex.
519        let runner = conn.clone();
520        let query = tokio::spawn(async move {
521            runner
522                .execute_batch(
523                    "CREATE TABLE big AS \
524                     SELECT t1.range AS a FROM range(200000) t1, range(200) t2",
525                )
526                .await
527        });
528
529        // Give the query time to acquire the mutex and start.
530        tokio::time::sleep(Duration::from_millis(20)).await;
531
532        // Control calls must return promptly even though the mutex is held. If they
533        // took the mutex, this would block until the query finished.
534        let control_calls = tokio::time::timeout(Duration::from_secs(5), async {
535            let _ = conn.interrupt();
536            let _ = conn.progress();
537        });
538        control_calls.await.expect("interrupt/progress must not block on the running query");
539
540        // Let the query task finish (interrupted or completed) so the connection is
541        // released; the test's point is already proven above.
542        let _ = query.await.unwrap();
543    }
544
545    /// Dropping a query future does not wedge the connection: a subsequent query on
546    /// the same `AsyncConnection` still succeeds. The dropped future requests
547    /// interruption; whether DuckDB stops the query early or it completes on its
548    /// own, the mutex is released and the connection recovers.
549    #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
550    async fn dropped_query_future_leaves_connection_usable() {
551        use std::time::Duration;
552
553        let conn = AsyncConnection::open_in_memory().await.unwrap();
554
555        // A quick query. We drop its future under a 1ms timeout: the future may or
556        // may not have finished, but either way the blocking task completes on its
557        // own and releases the connection. This exercises the drop path (which arms
558        // interrupt-on-drop) without depending on interrupt latency.
559        let work = conn.execute_batch("CREATE TABLE small AS SELECT range AS a FROM range(1000)");
560        let _ = tokio::time::timeout(Duration::from_millis(1), work).await;
561
562        // The connection recovers within a generous bound.
563        let followup =
564            tokio::time::timeout(Duration::from_secs(30), conn.execute("SELECT 42 AS v"));
565        let result = followup.await.expect("connection must recover within 30s").unwrap();
566        match result.rows()[0].get("v").unwrap() {
567            DuckValue::Int(n) => assert_eq!(*n, 42),
568            other => panic!("expected Int(42), got {other:?}"),
569        }
570    }
571
572    /// A control minted before a query can observe its progress while it runs, and
573    /// a stale control from a finished query reports nothing.
574    #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
575    async fn stale_control_does_not_observe_a_later_query() {
576        let conn = AsyncConnection::open_in_memory().await.unwrap();
577        conn.execute_batch("CREATE TABLE t (id INTEGER)").await.unwrap();
578
579        // Mint a control, run a query to completion so the control goes stale.
580        let stale = conn.query_control();
581        conn.execute("INSERT INTO t VALUES (1)").await.unwrap();
582
583        assert!(!stale.is_active(), "control is stale after its query completed");
584        assert!(!stale.interrupt(), "stale interrupt is a no-op");
585        assert!(stale.progress().is_none(), "stale control observes no later query");
586
587        // The connection still works normally.
588        let result = conn.execute("SELECT count(*) AS c FROM t").await.unwrap();
589        match result.rows()[0].get("c").unwrap() {
590            DuckValue::BigInt(n) => assert_eq!(*n, 1),
591            other => panic!("expected BigInt(1), got {other:?}"),
592        }
593    }
594
595    #[tokio::test]
596    async fn with_connection_transaction_rollback() {
597        let conn = AsyncConnection::open_in_memory().await.unwrap();
598        conn.execute_batch("CREATE TABLE t (id INTEGER)").await.unwrap();
599        conn.with_connection(|c| {
600            c.execute_batch("BEGIN")?;
601            c.execute_batch("INSERT INTO t VALUES (1)")?;
602            c.execute_batch("ROLLBACK")?;
603            Ok(())
604        })
605        .await
606        .unwrap();
607        let result = conn.execute("SELECT count(*) AS c FROM t").await.unwrap();
608        match result.rows()[0].get("c").unwrap() {
609            DuckValue::BigInt(n) => assert_eq!(*n, 0),
610            other => panic!("expected BigInt, got {other:?}"),
611        }
612    }
613}