Skip to main content

better_duck_core/raw/
pending.rs

1//! Incremental ("pending") execution of a prepared statement.
2//!
3//! `duckdb_pending_prepared` turns a prepared statement into a
4//! [`PendingResult`], which runs the query one task at a time
5//! ([`execute_task`](PendingResult::execute_task)) so control can return to the
6//! caller between tasks — e.g. to poll for cancellation via
7//! [`QueryControl`](crate::raw::connection::QueryControl). When the state reports
8//! ready, [`execute`](PendingResult::execute) materialises the final
9//! [`DuckResult`], transferring result ownership exactly once.
10
11use std::ffi::CStr;
12use std::marker::PhantomData;
13use std::mem;
14
15use crate::{
16    error::{EngineError, Error, Result},
17    ffi::{
18        duckdb_destroy_pending, duckdb_execute_pending, duckdb_pending_error,
19        duckdb_pending_execute_check_state, duckdb_pending_execute_task,
20        duckdb_pending_execution_is_finished, duckdb_pending_prepared, duckdb_pending_result,
21        duckdb_pending_state, duckdb_result, DuckDBSuccess,
22    },
23    raw::{result::DuckResult, statement::CachedStatement},
24};
25
26/// The state of a pending execution, as reported by DuckDB.
27///
28/// Mirrors `duckdb_pending_state`. `#[non_exhaustive]` and carries
29/// [`Unknown`](PendingState::Unknown) so a value a future DuckDB adds is
30/// preserved rather than lost.
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
32#[non_exhaustive]
33pub enum PendingState {
34    /// The result is ready; call [`execute`](PendingResult::execute).
35    Ready,
36    /// More tasks remain; call [`execute_task`](PendingResult::execute_task) again.
37    NotReady,
38    /// Execution failed; [`error`](PendingResult::error) has the message.
39    Error,
40    /// No tasks are currently available (e.g. waiting on another thread).
41    NoTasksAvailable,
42    /// A state this build does not recognise; the raw value is preserved.
43    Unknown(duckdb_pending_state),
44}
45
46impl PendingState {
47    fn from_raw(raw: duckdb_pending_state) -> PendingState {
48        use crate::ffi as f;
49        match raw {
50            f::duckdb_pending_state_DUCKDB_PENDING_RESULT_READY => PendingState::Ready,
51            f::duckdb_pending_state_DUCKDB_PENDING_RESULT_NOT_READY => PendingState::NotReady,
52            f::duckdb_pending_state_DUCKDB_PENDING_ERROR => PendingState::Error,
53            f::duckdb_pending_state_DUCKDB_PENDING_NO_TASKS_AVAILABLE => {
54                PendingState::NoTasksAvailable
55            },
56            other => PendingState::Unknown(other),
57        }
58    }
59
60    /// Returns the raw `duckdb_pending_state` for this state.
61    fn to_raw(self) -> duckdb_pending_state {
62        use crate::ffi as f;
63        match self {
64            PendingState::Ready => f::duckdb_pending_state_DUCKDB_PENDING_RESULT_READY,
65            PendingState::NotReady => f::duckdb_pending_state_DUCKDB_PENDING_RESULT_NOT_READY,
66            PendingState::Error => f::duckdb_pending_state_DUCKDB_PENDING_ERROR,
67            PendingState::NoTasksAvailable => {
68                f::duckdb_pending_state_DUCKDB_PENDING_NO_TASKS_AVAILABLE
69            },
70            PendingState::Unknown(raw) => raw,
71        }
72    }
73
74    /// Returns `true` if this state means execution has finished (result ready).
75    ///
76    /// Uses DuckDB's own `duckdb_pending_execution_is_finished` rather than
77    /// comparing to `Ready` directly, so the definition stays in sync with DuckDB.
78    #[must_use]
79    pub fn is_finished(self) -> bool {
80        // SAFETY: `duckdb_pending_execution_is_finished` is a pure classifier over
81        // the state value; no handle is involved.
82        unsafe { duckdb_pending_execution_is_finished(self.to_raw()) }
83    }
84}
85
86/// A pending (incrementally-executed) query, borrowing the statement it runs.
87///
88/// Owns the `duckdb_pending_result` handle and destroys it in [`Drop`]. Borrows
89/// the [`CachedStatement`] for `'a` so the statement (and, through it, the
90/// connection) cannot be destroyed while the pending execution references it.
91pub struct PendingResult<'a> {
92    pending: duckdb_pending_result,
93    _stmt: PhantomData<&'a CachedStatement>,
94}
95
96impl<'a> PendingResult<'a> {
97    /// Creates a pending execution from a prepared statement.
98    ///
99    /// Bind parameters on `stmt` *before* calling this.
100    ///
101    /// # Errors
102    ///
103    /// Returns [`Error::Engine`] with DuckDB's message if the pending result
104    /// cannot be created. The pending handle is destroyed regardless of outcome
105    /// (in `Drop` on success, or here on failure), as DuckDB requires.
106    pub(crate) fn new(stmt: &'a CachedStatement) -> Result<PendingResult<'a>> {
107        let mut pending: duckdb_pending_result = std::ptr::null_mut();
108        // SAFETY: `stmt.handle()` is a valid prepared statement kept alive by the
109        // `'a` borrow; `&mut pending` is a valid output pointer. DuckDB requires the
110        // pending result to be destroyed regardless of the return code.
111        let rc = unsafe { duckdb_pending_prepared(stmt.handle(), &mut pending) };
112        if rc != DuckDBSuccess {
113            // SAFETY: `pending` is the (possibly-error) handle DuckDB produced; its
114            // error string is borrowed until we destroy it, so copy first.
115            let message = unsafe { pending_error_message(pending) };
116            // SAFETY: `pending` is destroyed exactly once here on the failure path.
117            unsafe { duckdb_destroy_pending(&mut pending) };
118            return Err(Error::Engine(EngineError::unavailable(Some(
119                message.unwrap_or_else(|| "failed to create pending result".to_owned()),
120            ))));
121        }
122        Ok(PendingResult { pending, _stmt: PhantomData })
123    }
124
125    /// Executes a single task, returning the resulting [`PendingState`].
126    ///
127    /// Call repeatedly while the state is [`NotReady`](PendingState::NotReady) or
128    /// [`NoTasksAvailable`](PendingState::NoTasksAvailable); stop on
129    /// [`Ready`](PendingState::Ready) (then call [`execute`](PendingResult::execute))
130    /// or [`Error`](PendingState::Error).
131    #[must_use = "the returned state says whether to keep stepping, finish, or stop on error"]
132    pub fn execute_task(&mut self) -> PendingState {
133        // SAFETY: `self.pending` is a valid, live pending result.
134        PendingState::from_raw(unsafe { duckdb_pending_execute_task(self.pending) })
135    }
136
137    /// Returns the current [`PendingState`] without executing a task.
138    #[must_use]
139    pub fn check_state(&mut self) -> PendingState {
140        // SAFETY: `self.pending` is a valid, live pending result.
141        PendingState::from_raw(unsafe { duckdb_pending_execute_check_state(self.pending) })
142    }
143
144    /// Returns DuckDB's current error message for this pending result, if any.
145    #[must_use]
146    pub fn error(&self) -> Option<String> {
147        // SAFETY: `self.pending` is a valid, live pending result; the error string
148        // is borrowed (freed on destroy), so it is copied out here.
149        unsafe { pending_error_message(self.pending) }
150    }
151
152    /// Runs the pending execution to completion and returns the materialised
153    /// [`DuckResult`], consuming `self` so the result is transferred exactly once.
154    ///
155    /// This drives any remaining tasks internally; call it directly for the simple
156    /// case, or step with [`execute_task`](PendingResult::execute_task) first when
157    /// you need to interleave cancellation checks.
158    ///
159    /// # Errors
160    ///
161    /// Returns [`Error::Engine`] carrying DuckDB's classification/message if
162    /// execution fails.
163    #[must_use = "the DuckResult owns the query output; consume it"]
164    pub fn execute(mut self) -> Result<DuckResult> {
165        // SAFETY: `self.pending` is valid; `finish_pending` runs `duckdb_execute_pending`,
166        // destroys the pending handle exactly once, and nulls it so `Drop` is a no-op.
167        unsafe { finish_pending(&mut self.pending) }
168    }
169}
170
171/// Runs `duckdb_execute_pending`, destroys the pending handle exactly once
172/// (nulling `*pending` so a later `Drop` is a no-op), and materialises the result.
173///
174/// Shared by [`PendingResult::execute`] and [`OwnedPending::execute`].
175///
176/// # Safety
177///
178/// `*pending` must be a valid, non-null `duckdb_pending_result` not yet destroyed.
179unsafe fn finish_pending(pending: &mut duckdb_pending_result) -> Result<DuckResult> {
180    // SAFETY: a zeroed `duckdb_result` is the correct initial output state.
181    let mut out = unsafe { mem::zeroed::<duckdb_result>() };
182    // SAFETY: `*pending` is valid; `&mut out` is a valid output pointer.
183    // `duckdb_execute_pending` writes the (possibly-error) result into `out`;
184    // ownership of `out` transfers to `DuckResult`/the error adapter, which destroys
185    // it exactly once.
186    let rc = unsafe { duckdb_execute_pending(*pending, &mut out as *mut duckdb_result) };
187    // SAFETY: `*pending` is valid; destroyed exactly once and nulled here.
188    unsafe { duckdb_destroy_pending(pending) };
189    crate::helpers::duck_result::result_from_duckdb_result(rc, &mut out as *mut duckdb_result)?;
190    Ok(DuckResult::new(out))
191}
192
193/// An owned pending execution: like [`PendingResult`] but it *owns* its
194/// [`CachedStatement`] instead of borrowing one, so it carries no lifetime and is
195/// `'static`.
196///
197/// This is what lets the async adapter step a query one task per
198/// `spawn_blocking` dispatch — the whole `OwnedPending` moves in and out of each
199/// blocking task, which a borrow-based `PendingResult` could not do.
200pub struct OwnedPending {
201    /// Destroyed first (declared before `_stmt`): the pending references the
202    /// statement, so it must be torn down before the statement's handle.
203    pending: duckdb_pending_result,
204    /// Owned statement, kept alive (with its connection) for the pending's whole
205    /// life. Never read directly — the pending uses the handle DuckDB copied at
206    /// creation — but its `Drop` must run *after* the pending's, hence the field
207    /// order above.
208    _stmt: CachedStatement,
209}
210
211// SAFETY: `OwnedPending` owns its pending handle and `CachedStatement` outright
212// and exposes no interior mutability. DuckDB permits moving a pending result to a
213// different thread as long as it is not used from two at once; `&mut self` on
214// every stepping method and the lack of `Sync` guarantee no concurrent use. This
215// mirrors `CachedStatement`'s own `Send` (which this value also contains).
216unsafe impl Send for OwnedPending {}
217
218impl OwnedPending {
219    /// Creates an owned pending execution from an owned prepared statement.
220    ///
221    /// Bind parameters on `stmt` before calling this.
222    ///
223    /// # Errors
224    ///
225    /// Returns [`Error::Engine`] if DuckDB cannot create the pending result.
226    pub(crate) fn new(stmt: CachedStatement) -> Result<OwnedPending> {
227        let mut pending: duckdb_pending_result = std::ptr::null_mut();
228        // SAFETY: `stmt.handle()` is a valid prepared statement owned by `stmt`, which
229        // this value retains; `&mut pending` is a valid output pointer. DuckDB requires
230        // the pending result to be destroyed regardless of the return code.
231        let rc = unsafe { duckdb_pending_prepared(stmt.handle(), &mut pending) };
232        if rc != DuckDBSuccess {
233            // SAFETY: `pending` is the (possibly-error) handle; error string borrowed
234            // until destroy, so copy first, then destroy exactly once.
235            let message = unsafe { pending_error_message(pending) };
236            // SAFETY: destroyed exactly once here on the failure path.
237            unsafe { duckdb_destroy_pending(&mut pending) };
238            return Err(Error::Engine(EngineError::unavailable(Some(
239                message.unwrap_or_else(|| "failed to create pending result".to_owned()),
240            ))));
241        }
242        Ok(OwnedPending { pending, _stmt: stmt })
243    }
244
245    /// Executes a single task, returning the resulting [`PendingState`].
246    #[must_use = "the returned state says whether to keep stepping, finish, or stop on error"]
247    pub fn execute_task(&mut self) -> PendingState {
248        // SAFETY: `self.pending` is a valid, live pending result.
249        PendingState::from_raw(unsafe { duckdb_pending_execute_task(self.pending) })
250    }
251
252    /// Returns DuckDB's current error message, if any.
253    #[must_use]
254    pub fn error(&self) -> Option<String> {
255        // SAFETY: `self.pending` is a valid, live pending result.
256        unsafe { pending_error_message(self.pending) }
257    }
258
259    /// Runs to completion and returns the materialised [`DuckResult`], consuming
260    /// `self` so the result is transferred exactly once.
261    ///
262    /// # Errors
263    ///
264    /// Returns [`Error::Engine`] carrying DuckDB's classification/message on failure.
265    #[must_use = "the DuckResult owns the query output; consume it"]
266    pub fn execute(mut self) -> Result<DuckResult> {
267        // SAFETY: `self.pending` is valid; `finish_pending` executes, destroys the
268        // handle once, and nulls it so `Drop` is a no-op. `self._stmt` drops afterwards.
269        unsafe { finish_pending(&mut self.pending) }
270    }
271}
272
273impl Drop for OwnedPending {
274    fn drop(&mut self) {
275        if self.pending.is_null() {
276            return;
277        }
278        // SAFETY: `self.pending` is a valid, non-null handle owned exclusively by this
279        // value (null-guarded above); destroyed exactly once, before `stmt` drops.
280        unsafe { duckdb_destroy_pending(&mut self.pending) };
281    }
282}
283
284/// Copies a pending result's borrowed error string into an owned `String`.
285///
286/// # Safety
287///
288/// `pending` must be a valid, non-null `duckdb_pending_result`.
289unsafe fn pending_error_message(pending: duckdb_pending_result) -> Option<String> {
290    if pending.is_null() {
291        return None;
292    }
293    // SAFETY: `pending` is valid per the contract; `duckdb_pending_error` returns a
294    // borrowed C string (or null) owned by the pending result — copied, not freed.
295    let raw = unsafe { duckdb_pending_error(pending) };
296    if raw.is_null() {
297        return None;
298    }
299    // SAFETY: `raw` is a non-null, null-terminated C string borrowed from the
300    // pending result; we copy it into an owned String without freeing it.
301    Some(unsafe { CStr::from_ptr(raw) }.to_string_lossy().into_owned())
302}
303
304impl Drop for PendingResult<'_> {
305    fn drop(&mut self) {
306        if self.pending.is_null() {
307            return;
308        }
309        // SAFETY: `self.pending` is a valid, non-null handle owned exclusively by
310        // this value (null-guarded above); destroyed exactly once.
311        unsafe { duckdb_destroy_pending(&mut self.pending) };
312    }
313}
314
315#[cfg(test)]
316mod tests {
317    use super::*;
318    use crate::{
319        config::Config, helpers::path::path_to_cstring, raw::connection::RawConnection,
320        types::value::DuckValue,
321    };
322
323    fn conn() -> RawConnection {
324        let path = path_to_cstring(":memory:".as_ref()).unwrap();
325        let config = Config::default().with("duckdb_api", "rust").unwrap();
326        RawConnection::open_with_flags(&path, config).unwrap()
327    }
328
329    #[test]
330    fn step_to_ready_then_execute_yields_the_result() {
331        let con = conn();
332        let stmt = CachedStatement::prepare(&con, "SELECT 7 AS v").unwrap();
333        let mut pending = PendingResult::new(&stmt).unwrap();
334
335        // Step tasks best-effort: stepping must never surface an error, and we stop
336        // as soon as a step reports finished. Reaching a finished state purely by
337        // stepping is NOT guaranteed on every build — a trivial query can keep
338        // reporting `NoTasksAvailable` (its pipeline is owned by background workers, or
339        // it only finalises in `execute`) — so this loop is a bounded best-effort, not
340        // a hard requirement.
341        for _ in 0..1_000 {
342            let state = pending.execute_task();
343            assert_ne!(state, PendingState::Error, "unexpected error: {:?}", pending.error());
344            if state.is_finished() {
345                break;
346            }
347        }
348
349        // `execute` drives any remaining tasks to completion and transfers the result
350        // exactly once, regardless of how far stepping got.
351        let mut result = pending.execute().unwrap();
352        let row = result.next().unwrap().unwrap();
353        assert_eq!(row.get("v"), Some(&DuckValue::Int(7)));
354    }
355
356    #[test]
357    fn execute_without_manual_stepping_drives_to_completion() {
358        let con = conn();
359        let stmt = CachedStatement::prepare(&con, "SELECT 'hi' AS s").unwrap();
360        let pending = PendingResult::new(&stmt).unwrap();
361        // execute() drives remaining tasks internally.
362        let mut result = pending.execute().unwrap();
363        let row = result.next().unwrap().unwrap();
364        assert_eq!(row.get("s"), Some(&DuckValue::Text("hi".into())));
365    }
366
367    #[test]
368    fn is_finished_matches_duckdb_classifier() {
369        // `Ready` is finished; `NotReady` is not. (The exact classification of
370        // `Error`/`NoTasksAvailable` is DuckDB's own via
371        // `duckdb_pending_execution_is_finished`, so we don't pin it here.)
372        assert!(PendingState::Ready.is_finished());
373        assert!(!PendingState::NotReady.is_finished());
374    }
375
376    #[test]
377    fn unknown_state_round_trips() {
378        assert_eq!(PendingState::from_raw(999), PendingState::Unknown(999));
379        assert_eq!(PendingState::Unknown(999).to_raw(), 999);
380    }
381
382    // NOTE: a pending-execution *runtime error* test is intentionally omitted.
383    // `execute()` routes failures through the same `result_from_duckdb_result`
384    // adapter as `Statement::execute`/`RawConnection::query`, whose typed-error
385    // path is covered in `helpers/duck_result.rs` and the connection/statement
386    // suites. Constructing a query that reliably errors *during pending stepping*
387    // (not folded at prepare, not aborting across FFI like the extract path) is
388    // brittle and build-dependent, so the error path is covered at the shared
389    // adapter instead of here.
390}