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}