Skip to main content

better_duck_core/raw/
statement.rs

1use std::{
2    ffi::{CStr, CString},
3    mem, ptr,
4    sync::Arc,
5};
6
7use crate::ffi::{
8    duckdb_bind_parameter_index, duckdb_clear_bindings, duckdb_destroy_prepare,
9    duckdb_execute_prepared, duckdb_free, duckdb_nparams, duckdb_param_logical_type,
10    duckdb_param_type, duckdb_parameter_name, duckdb_prepare,
11    duckdb_prepared_statement_column_count, duckdb_prepared_statement_column_logical_type,
12    duckdb_prepared_statement_column_name, duckdb_prepared_statement_column_type,
13    duckdb_prepared_statement_type, duckdb_result, duckdb_statement_type, duckdb_type,
14    DuckDBSuccess,
15};
16
17use crate::{
18    error::{Error, Result},
19    ffi,
20    ffi::duckdb_prepared_statement,
21    helpers::duck_result::{result_from_duckdb_prepare, result_from_duckdb_result},
22    raw::{
23        connection::{ConnectionInner, RawConnection},
24        result::DuckResult,
25    },
26    types::{appendable::AppendAble, LogicalType, TypeInfo},
27};
28
29/// The kind of SQL statement a prepared statement holds.
30///
31/// Mirrors DuckDB's `duckdb_statement_type`. `#[non_exhaustive]` because DuckDB
32/// adds statement kinds across releases; an id this build does not recognise is
33/// preserved verbatim in [`StatementType::Unknown`] rather than lost.
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
35#[non_exhaustive]
36pub enum StatementType {
37    /// `DUCKDB_STATEMENT_TYPE_INVALID`
38    Invalid,
39    /// `SELECT`
40    Select,
41    /// `INSERT`
42    Insert,
43    /// `UPDATE`
44    Update,
45    /// `EXPLAIN`
46    Explain,
47    /// `DELETE`
48    Delete,
49    /// `PREPARE`
50    Prepare,
51    /// `CREATE`
52    Create,
53    /// `EXECUTE`
54    Execute,
55    /// `ALTER`
56    Alter,
57    /// `TRANSACTION`
58    Transaction,
59    /// `COPY`
60    Copy,
61    /// `ANALYZE`
62    Analyze,
63    /// `SET VARIABLE`
64    VariableSet,
65    /// `CREATE FUNCTION`
66    CreateFunc,
67    /// `DROP`
68    Drop,
69    /// `EXPORT`
70    Export,
71    /// `PRAGMA`
72    Pragma,
73    /// `VACUUM`
74    Vacuum,
75    /// `CALL`
76    Call,
77    /// `SET`
78    Set,
79    /// `LOAD`
80    Load,
81    /// `RELATION`
82    Relation,
83    /// `EXTENSION`
84    Extension,
85    /// `LOGICAL_PLAN`
86    LogicalPlan,
87    /// `ATTACH`
88    Attach,
89    /// `DETACH`
90    Detach,
91    /// `MULTI`
92    Multi,
93    /// A statement kind this build does not recognise; the raw id is preserved.
94    Unknown(duckdb_statement_type),
95}
96
97impl StatementType {
98    /// Classifies a raw `duckdb_statement_type`, preserving unrecognised values.
99    #[must_use]
100    pub fn from_raw(raw: duckdb_statement_type) -> StatementType {
101        use crate::ffi as f;
102        match raw {
103            f::duckdb_statement_type_DUCKDB_STATEMENT_TYPE_INVALID => StatementType::Invalid,
104            f::duckdb_statement_type_DUCKDB_STATEMENT_TYPE_SELECT => StatementType::Select,
105            f::duckdb_statement_type_DUCKDB_STATEMENT_TYPE_INSERT => StatementType::Insert,
106            f::duckdb_statement_type_DUCKDB_STATEMENT_TYPE_UPDATE => StatementType::Update,
107            f::duckdb_statement_type_DUCKDB_STATEMENT_TYPE_EXPLAIN => StatementType::Explain,
108            f::duckdb_statement_type_DUCKDB_STATEMENT_TYPE_DELETE => StatementType::Delete,
109            f::duckdb_statement_type_DUCKDB_STATEMENT_TYPE_PREPARE => StatementType::Prepare,
110            f::duckdb_statement_type_DUCKDB_STATEMENT_TYPE_CREATE => StatementType::Create,
111            f::duckdb_statement_type_DUCKDB_STATEMENT_TYPE_EXECUTE => StatementType::Execute,
112            f::duckdb_statement_type_DUCKDB_STATEMENT_TYPE_ALTER => StatementType::Alter,
113            f::duckdb_statement_type_DUCKDB_STATEMENT_TYPE_TRANSACTION => {
114                StatementType::Transaction
115            },
116            f::duckdb_statement_type_DUCKDB_STATEMENT_TYPE_COPY => StatementType::Copy,
117            f::duckdb_statement_type_DUCKDB_STATEMENT_TYPE_ANALYZE => StatementType::Analyze,
118            f::duckdb_statement_type_DUCKDB_STATEMENT_TYPE_VARIABLE_SET => {
119                StatementType::VariableSet
120            },
121            f::duckdb_statement_type_DUCKDB_STATEMENT_TYPE_CREATE_FUNC => StatementType::CreateFunc,
122            f::duckdb_statement_type_DUCKDB_STATEMENT_TYPE_DROP => StatementType::Drop,
123            f::duckdb_statement_type_DUCKDB_STATEMENT_TYPE_EXPORT => StatementType::Export,
124            f::duckdb_statement_type_DUCKDB_STATEMENT_TYPE_PRAGMA => StatementType::Pragma,
125            f::duckdb_statement_type_DUCKDB_STATEMENT_TYPE_VACUUM => StatementType::Vacuum,
126            f::duckdb_statement_type_DUCKDB_STATEMENT_TYPE_CALL => StatementType::Call,
127            f::duckdb_statement_type_DUCKDB_STATEMENT_TYPE_SET => StatementType::Set,
128            f::duckdb_statement_type_DUCKDB_STATEMENT_TYPE_LOAD => StatementType::Load,
129            f::duckdb_statement_type_DUCKDB_STATEMENT_TYPE_RELATION => StatementType::Relation,
130            f::duckdb_statement_type_DUCKDB_STATEMENT_TYPE_EXTENSION => StatementType::Extension,
131            f::duckdb_statement_type_DUCKDB_STATEMENT_TYPE_LOGICAL_PLAN => {
132                StatementType::LogicalPlan
133            },
134            f::duckdb_statement_type_DUCKDB_STATEMENT_TYPE_ATTACH => StatementType::Attach,
135            f::duckdb_statement_type_DUCKDB_STATEMENT_TYPE_DETACH => StatementType::Detach,
136            f::duckdb_statement_type_DUCKDB_STATEMENT_TYPE_MULTI => StatementType::Multi,
137            other => StatementType::Unknown(other),
138        }
139    }
140}
141
142/// Prepared-statement metadata shared by [`Statement`] and [`CachedStatement`].
143///
144/// These operate on a raw `duckdb_prepared_statement`; both wrappers delegate to
145/// them so parameter/column introspection lives in one place.
146///
147/// # Safety
148///
149/// Every function requires `stmt` to be a valid, live `duckdb_prepared_statement`.
150mod meta {
151    use super::*;
152
153    /// The statement kind (`duckdb_prepared_statement_type`).
154    pub(super) fn statement_type(stmt: duckdb_prepared_statement) -> StatementType {
155        // SAFETY: `stmt` is a valid prepared statement.
156        StatementType::from_raw(unsafe { duckdb_prepared_statement_type(stmt) })
157    }
158
159    /// Number of parameters (`duckdb_nparams`).
160    pub(super) fn param_count(stmt: duckdb_prepared_statement) -> u64 {
161        // SAFETY: `stmt` is valid.
162        unsafe { duckdb_nparams(stmt) }
163    }
164
165    /// The name of the parameter at the given 1-based index, if any.
166    ///
167    /// Returns `None` for an out-of-range index (DuckDB returns null) or a
168    /// positional (anonymous) parameter.
169    pub(super) fn parameter_name(
170        stmt: duckdb_prepared_statement,
171        idx: u64,
172    ) -> Option<String> {
173        // SAFETY: `stmt` is valid; `duckdb_parameter_name` returns an owned `char*`
174        // (free with `duckdb_free`) or null, which `owned_c_string` copies + frees.
175        unsafe { owned_c_string(duckdb_parameter_name(stmt, idx)) }
176    }
177
178    /// The coarse `duckdb_type` of the parameter at the 1-based index.
179    pub(super) fn param_type(
180        stmt: duckdb_prepared_statement,
181        idx: u64,
182    ) -> duckdb_type {
183        // SAFETY: `stmt` is valid.
184        unsafe { duckdb_param_type(stmt, idx) }
185    }
186
187    /// The lossless [`TypeInfo`] of the parameter at the 1-based index.
188    pub(super) fn param_type_info(
189        stmt: duckdb_prepared_statement,
190        idx: u64,
191    ) -> Option<TypeInfo> {
192        // SAFETY: `stmt` is valid; `duckdb_param_logical_type` returns an owned handle
193        // (or null) wrapped into RAII, described, then destroyed on drop.
194        LogicalType::from_raw(unsafe { duckdb_param_logical_type(stmt, idx) })
195            .ok()
196            .map(|lt| lt.describe())
197    }
198
199    /// Resolves a named parameter to its 1-based index
200    /// (`duckdb_bind_parameter_index`).
201    ///
202    /// # Errors
203    ///
204    /// [`Error::NulError`] if `name` has an interior nul; [`Error::InvalidParameterName`]
205    /// if DuckDB does not know the name.
206    pub(super) fn parameter_index(
207        stmt: duckdb_prepared_statement,
208        name: &str,
209    ) -> Result<u64> {
210        let c_name = CString::new(name)?;
211        let mut idx: crate::ffi::idx_t = 0;
212        // SAFETY: `stmt` is valid; `c_name` is a valid null-terminated string that
213        // outlives the call; `&mut idx` is a valid output pointer.
214        let rc = unsafe { duckdb_bind_parameter_index(stmt, &mut idx, c_name.as_ptr()) };
215        if rc == DuckDBSuccess {
216            Ok(idx)
217        } else {
218            Err(Error::InvalidParameterName(name.to_owned()))
219        }
220    }
221
222    /// Number of result columns (`duckdb_prepared_statement_column_count`).
223    pub(super) fn column_count(stmt: duckdb_prepared_statement) -> u64 {
224        // SAFETY: `stmt` is valid.
225        unsafe { duckdb_prepared_statement_column_count(stmt) }
226    }
227
228    /// The name of the result column at `idx`, if in range.
229    pub(super) fn column_name(
230        stmt: duckdb_prepared_statement,
231        idx: u64,
232    ) -> Option<String> {
233        // SAFETY: `stmt` is valid; owned `char*` (or null) copied + freed.
234        unsafe { owned_c_string(duckdb_prepared_statement_column_name(stmt, idx)) }
235    }
236
237    /// The coarse `duckdb_type` of the result column at `idx`.
238    pub(super) fn column_type(
239        stmt: duckdb_prepared_statement,
240        idx: u64,
241    ) -> duckdb_type {
242        // SAFETY: `stmt` is valid.
243        unsafe { duckdb_prepared_statement_column_type(stmt, idx) }
244    }
245
246    /// The lossless [`TypeInfo`] of the result column at `idx`.
247    pub(super) fn column_type_info(
248        stmt: duckdb_prepared_statement,
249        idx: u64,
250    ) -> Option<TypeInfo> {
251        // SAFETY: `stmt` is valid; owned handle (or null) wrapped, described, dropped.
252        LogicalType::from_raw(unsafe { duckdb_prepared_statement_column_logical_type(stmt, idx) })
253            .ok()
254            .map(|lt| lt.describe())
255    }
256
257    /// Copies a DuckDB-owned `char*` into an owned `String`, freeing it with
258    /// `duckdb_free`. Returns `None` for null.
259    ///
260    /// # Safety
261    ///
262    /// `ptr` must be null or a `char*` DuckDB allocated for the caller to free.
263    unsafe fn owned_c_string(ptr: *const std::os::raw::c_char) -> Option<String> {
264        if ptr.is_null() {
265            return None;
266        }
267        // SAFETY: non-null, valid null-terminated C string per the contract.
268        let owned = unsafe { CStr::from_ptr(ptr) }.to_string_lossy().into_owned();
269        // SAFETY: DuckDB allocated `ptr`; ownership transferred to us to free.
270        unsafe { duckdb_free(ptr as *mut std::os::raw::c_void) };
271        Some(owned)
272    }
273}
274
275/// A prepared DuckDB statement that can be executed one or more times.
276///
277/// After calling [`execute`](Statement::execute), the statement can be reused
278/// by calling [`clear_bindings`](Statement::clear_bindings) and re-binding parameters.
279pub struct Statement<'a> {
280    /// Reference to the underlying DuckDB connection.
281    con: &'a RawConnection,
282    /// Pointer to the prepared DuckDB statement (FFI resource).
283    stmt: duckdb_prepared_statement,
284    /// 1-based index of the next parameter to bind (incremented by each `bind` call).
285    bind_idx: u64,
286}
287
288impl Statement<'_> {
289    /// Prepares a new `Statement` from an SQL string.
290    ///
291    /// # Errors
292    ///
293    /// Returns an error if the SQL cannot be compiled into a prepared statement.
294    pub(super) fn new<'a, 'b: 'a>(
295        con: &'b RawConnection,
296        sql: &str,
297    ) -> Result<Statement<'a>> {
298        let mut stmt: duckdb_prepared_statement = ptr::null_mut();
299        let c_str = std::ffi::CString::new(sql)?;
300        // SAFETY: `con` is a live `RawConnection`, so its handle is a valid open
301        // `duckdb_connection`; `c_str` is a valid null-terminated C string. `stmt` is a
302        // valid output pointer. The `'a` borrow keeps the connection alive for the
303        // statement's whole lifetime.
304        let resp = unsafe { duckdb_prepare(con.handle(), c_str.as_ptr(), &mut stmt) };
305        result_from_duckdb_prepare(resp, stmt)?;
306        Ok(Statement { con, stmt, bind_idx: 0 })
307    }
308
309    /// Returns a reference to the raw prepared-statement pointer.
310    #[allow(unused)]
311    #[inline]
312    fn raw(&self) -> &duckdb_prepared_statement {
313        &self.stmt
314    }
315
316    /// Returns a reference to the underlying raw connection.
317    #[allow(unused)]
318    #[inline]
319    fn connection(&self) -> &RawConnection {
320        self.con
321    }
322}
323
324// Exposed API
325impl Statement<'_> {
326    /// Binds a value to the next positional parameter (1-based).
327    ///
328    /// The first call binds parameter 1, the second call parameter 2, and so on.
329    /// Call [`clear_bindings`](Statement::clear_bindings) to reset the counter.
330    ///
331    /// # Errors
332    ///
333    /// Returns an error if the underlying DuckDB bind call fails.
334    #[must_use = "bind result should be checked"]
335    #[allow(unused)]
336    #[inline]
337    pub fn bind<T: AppendAble>(
338        &mut self,
339        binder: &mut T,
340    ) -> Result<()> {
341        self.bind_idx += 1;
342        // Pass the 1-based index directly to stmt_append.
343        self.bind_at(binder, self.bind_idx)
344    }
345
346    /// Binds a value to the parameter at the given 1-based index.
347    ///
348    /// # Arguments
349    ///
350    /// * `binder` - The value to bind, must implement [`AppendAble`].
351    /// * `idx` - The 1-based parameter index.
352    ///
353    /// # Errors
354    ///
355    /// Returns an error if the underlying DuckDB bind call fails.
356    #[allow(unused)]
357    #[inline]
358    pub fn bind_at<T: AppendAble>(
359        &self,
360        binder: &mut T,
361        idx: u64,
362    ) -> Result<()> {
363        binder.stmt_append(idx, self.stmt)
364    }
365
366    /// Executes the prepared statement and returns the result.
367    ///
368    /// The statement can be re-executed after calling [`clear_bindings`](Statement::clear_bindings)
369    /// and re-binding parameters.
370    ///
371    /// # Errors
372    ///
373    /// Returns an error if execution fails.
374    #[must_use = "execute returns the query result; dropping it without reading discards rows"]
375    #[allow(unused)]
376    pub fn execute(&mut self) -> Result<DuckResult> {
377        // SAFETY: `mem::zeroed::<duckdb_result>()` produces an all-zeros value, which is
378        // the correct initial state for a `duckdb_result` output parameter. `duckdb_result`
379        // is a small `Copy` struct with no self-referential fields, so it needs no stable
380        // heap address — DuckResult::new takes it by value.
381        let mut out = unsafe { mem::zeroed::<duckdb_result>() };
382        // SAFETY: `self.stmt` is a valid prepared statement. `&mut out` provides a pointer
383        // to the stack-local zeroed `duckdb_result`. Ownership transfers to `DuckResult::new`,
384        // whose `Drop` calls `duckdb_destroy_result` once.
385        let resp = unsafe { duckdb_execute_prepared(self.stmt, &mut out as *mut duckdb_result) };
386        // The query is finished: advance the generation so a `QueryControl` minted
387        // for it can no longer interrupt a later query.
388        self.con.inner().advance_query();
389        result_from_duckdb_result(resp, &mut out as *mut duckdb_result)?;
390        Ok(DuckResult::new(out))
391    }
392
393    /// Returns the number of parameters in the prepared statement.
394    #[allow(unused)]
395    #[inline]
396    pub fn bind_parameter_count(&self) -> usize {
397        // SAFETY: `self.stmt` is a valid prepared statement.
398        unsafe { duckdb_nparams(self.stmt) as usize }
399    }
400
401    /// Clears all parameter bindings and resets the bind index to zero.
402    ///
403    /// After calling this method, subsequent [`bind`](Statement::bind) calls start
404    /// from parameter 1 again.
405    ///
406    /// # Errors
407    ///
408    /// Returns an error if the DuckDB clear-bindings call fails.
409    #[must_use = "clear_bindings result should be checked"]
410    #[allow(unused)]
411    #[inline]
412    pub fn clear_bindings(&mut self) -> Result<()> {
413        // SAFETY: `self.stmt` is a valid prepared statement.
414        let res = unsafe { duckdb_clear_bindings(self.stmt) };
415        if res != DuckDBSuccess {
416            Err(Error::DuckDBFailure(
417                crate::ffi::Error::new(crate::ffi::DuckDBError),
418                Some("Failed to clear bindings".to_owned()),
419            ))
420        } else {
421            self.bind_idx = 0;
422            Ok(())
423        }
424    }
425
426    /// Returns `true` if the prepared statement pointer is null (not initialized).
427    #[allow(unused)]
428    #[inline]
429    pub fn is_null(&self) -> bool {
430        self.stmt.is_null()
431    }
432}
433
434/// Destroys the prepared statement when the `Statement` is dropped.
435impl Drop for Statement<'_> {
436    fn drop(&mut self) {
437        // SAFETY: `self.stmt` is a valid duckdb_prepared_statement (or null).
438        // `duckdb_destroy_prepare` is idempotent and handles the non-null check itself,
439        // but we guard here as a belt-and-suspenders measure.
440        unsafe {
441            if !self.stmt.is_null() {
442                duckdb_destroy_prepare(&mut self.stmt);
443            }
444        }
445    }
446}
447
448/// A prepared statement that can be reset and re-executed with different bindings.
449///
450/// The statement is prepared on — and therefore belongs to — the connection
451/// passed to [`prepare`](CachedStatement::prepare). DuckDB scopes transactions
452/// to a connection, so the statement must never be prepared on a different one:
453/// doing so would execute outside any `BEGIN`/`ROLLBACK` the caller has open.
454///
455/// The statement retains that connection, so it cannot outlive it. Unlike a
456/// `Statement`, it carries no Rust lifetime, which lets it live in a
457/// `StatementCache` alongside the connection it was prepared on.
458///
459/// This type is used by Diesel statement cache
460/// (`StatementCache<DuckDb, CachedStatement>`).
461pub struct CachedStatement {
462    /// Keeps the *connection* this statement was prepared on open for at least as
463    /// long as the statement.
464    ///
465    /// This is deliberately the connection rather than the database. Retaining only
466    /// `Arc<RawDatabase>` would keep the database open while allowing the connection
467    /// to be disconnected first, leaving `duckdb_destroy_prepare` to run against a
468    /// dead connection. Retaining a *cloned* connection would be equally wrong in the
469    /// other direction: that opens a separate DuckDB connection, which would silently
470    /// run cached statements outside the caller's transaction.
471    _connection: Arc<ConnectionInner>,
472    /// The SQL text this statement was prepared from, retained alongside the handle.
473    ///
474    /// This is `pub(crate)`, so it is not visible outside `better-duck-core`. The
475    /// Diesel `StatementCache` keys its entries by its own query fragment and passes
476    /// the SQL *into* [`prepare`](CachedStatement::prepare) rather than reading it
477    /// back from here. Within the crate it is currently only inspected by tests,
478    /// hence `#[allow(dead_code)]` for non-test builds.
479    #[allow(dead_code)]
480    pub(crate) sql: Box<str>,
481    /// Raw prepared-statement handle.
482    stmt: ffi::duckdb_prepared_statement,
483}
484
485impl CachedStatement {
486    /// Prepares `sql` against the given connection.
487    ///
488    /// The statement is bound to `conn` and shares its transaction state.
489    ///
490    /// # Errors
491    ///
492    /// Returns [`Error::DuckDBFailure`] if DuckDB cannot parse or plan the query,
493    /// or [`Error::NulError`] if `sql` contains an interior nul byte.
494    pub fn prepare(
495        conn: &RawConnection,
496        sql: impl AsRef<str>,
497    ) -> Result<Self> {
498        let sql_str = sql.as_ref();
499        let mut stmt: ffi::duckdb_prepared_statement = ptr::null_mut();
500        let c_str = CString::new(sql_str)?;
501        // SAFETY: `conn`'s handle is a valid open duckdb_connection owned by the
502        // caller. `c_str` is a valid null-terminated CString that outlives this
503        // call, and `&mut stmt` is a valid output pointer. Preparing on the
504        // caller's own connection keeps the statement inside that connection's
505        // transaction scope.
506        let r = unsafe { ffi::duckdb_prepare(conn.handle(), c_str.as_ptr(), &mut stmt) };
507        result_from_duckdb_prepare(r, stmt)?;
508        Ok(CachedStatement::from_prepared(Arc::clone(conn.inner()), stmt, sql_str.into()))
509    }
510
511    /// Wraps an already-prepared handle into a `CachedStatement`.
512    ///
513    /// Shared by [`prepare`](CachedStatement::prepare) and the extracted-statement
514    /// path ([`ExtractedStatements::prepare`](crate::raw::extracted::ExtractedStatements::prepare)),
515    /// so both produce the identical wrapper — same connection-retention and
516    /// destruction invariants.
517    pub(crate) fn from_prepared(
518        connection: Arc<ConnectionInner>,
519        stmt: duckdb_prepared_statement,
520        sql: Box<str>,
521    ) -> CachedStatement {
522        CachedStatement { _connection: connection, sql, stmt }
523    }
524
525    /// Returns the raw prepared-statement handle, for crate-internal FFI (e.g. the
526    /// pending-execution wrapper). The handle stays owned by this `CachedStatement`.
527    #[inline]
528    pub(crate) fn handle(&self) -> duckdb_prepared_statement {
529        self.stmt
530    }
531
532    /// Begins incremental ("pending") execution of this statement.
533    ///
534    /// Bind parameters first. The returned
535    /// [`PendingResult`](crate::raw::pending::PendingResult) borrows `self`, runs
536    /// the query one task at a time, and materialises the final result on
537    /// `execute()`.
538    ///
539    /// # Errors
540    ///
541    /// Returns an error if DuckDB cannot create the pending result.
542    pub fn pending(&self) -> Result<crate::raw::pending::PendingResult<'_>> {
543        crate::raw::pending::PendingResult::new(self)
544    }
545
546    /// Consumes this statement into an owned, `'static` pending execution.
547    ///
548    /// Unlike [`pending`](CachedStatement::pending), the returned
549    /// [`OwnedPending`](crate::raw::pending::OwnedPending) *owns* the statement, so
550    /// it can be stepped across `spawn_blocking` dispatches by the async adapter.
551    ///
552    /// # Errors
553    ///
554    /// Returns an error if DuckDB cannot create the pending result.
555    #[allow(dead_code)]
556    pub fn into_pending(self) -> Result<crate::raw::pending::OwnedPending> {
557        crate::raw::pending::OwnedPending::new(self)
558    }
559
560    /// Resets all parameter bindings so the statement can be re-executed.
561    ///
562    /// # Errors
563    ///
564    /// Returns [`Error::DuckDBFailure`] if the DuckDB clear-bindings call fails.
565    pub fn reset_bindings(&mut self) -> Result<()> {
566        // SAFETY: `self.stmt` is a valid prepared statement — the Drop impl enforces this.
567        let r = unsafe { ffi::duckdb_clear_bindings(self.stmt) };
568        if r == ffi::DuckDBSuccess {
569            Ok(())
570        } else {
571            Err(Error::DuckDBFailure(ffi::Error::new(r), None))
572        }
573    }
574
575    /// Binds `value` at the given **1-based** parameter index.
576    ///
577    /// # Errors
578    ///
579    /// Returns an error if the underlying DuckDB bind call fails or `idx` is out of range.
580    pub fn bind<T: AppendAble + ?Sized>(
581        &mut self,
582        idx: u64,
583        value: &mut T,
584    ) -> Result<()> {
585        value.stmt_append(idx, self.stmt)
586    }
587
588    /// Binds `value` to the parameter identified by `name` (e.g. `$id` → `"id"`).
589    ///
590    /// Resolves the name to its 1-based index via `duckdb_bind_parameter_index`,
591    /// then binds there.
592    ///
593    /// # Errors
594    ///
595    /// [`Error::InvalidParameterName`] if the statement has no such parameter,
596    /// [`Error::NulError`] if `name` contains an interior nul, or a bind failure.
597    pub fn bind_named<T: AppendAble + ?Sized>(
598        &mut self,
599        name: &str,
600        value: &mut T,
601    ) -> Result<()> {
602        let idx = meta::parameter_index(self.stmt, name)?;
603        value.stmt_append(idx, self.stmt)
604    }
605
606    /// Returns the kind of SQL statement this prepared statement holds.
607    #[must_use]
608    pub fn statement_type(&self) -> StatementType {
609        meta::statement_type(self.stmt)
610    }
611
612    /// Number of parameters in the statement.
613    #[must_use]
614    pub fn parameter_count(&self) -> u64 {
615        meta::param_count(self.stmt)
616    }
617
618    /// The name of the parameter at the 1-based `index`, or `None` for a
619    /// positional parameter or an out-of-range index.
620    #[must_use]
621    pub fn parameter_name(
622        &self,
623        index: u64,
624    ) -> Option<String> {
625        meta::parameter_name(self.stmt, index)
626    }
627
628    /// The coarse `duckdb_type` of the parameter at the 1-based `index`.
629    #[must_use]
630    pub fn parameter_type(
631        &self,
632        index: u64,
633    ) -> duckdb_type {
634        meta::param_type(self.stmt, index)
635    }
636
637    /// The lossless [`TypeInfo`] of the parameter at the 1-based `index`.
638    #[must_use]
639    pub fn parameter_logical_type(
640        &self,
641        index: u64,
642    ) -> Option<TypeInfo> {
643        meta::param_type_info(self.stmt, index)
644    }
645
646    /// Resolves a named parameter to its 1-based index.
647    ///
648    /// # Errors
649    ///
650    /// [`Error::InvalidParameterName`] if unknown, [`Error::NulError`] on interior nul.
651    pub fn parameter_index(
652        &self,
653        name: &str,
654    ) -> Result<u64> {
655        meta::parameter_index(self.stmt, name)
656    }
657
658    /// Number of result columns the statement will produce.
659    #[must_use]
660    pub fn column_count(&self) -> u64 {
661        meta::column_count(self.stmt)
662    }
663
664    /// The name of the result column at `index`, if in range.
665    #[must_use]
666    pub fn column_name(
667        &self,
668        index: u64,
669    ) -> Option<String> {
670        meta::column_name(self.stmt, index)
671    }
672
673    /// The coarse `duckdb_type` of the result column at `index`.
674    #[must_use]
675    pub fn column_type(
676        &self,
677        index: u64,
678    ) -> duckdb_type {
679        meta::column_type(self.stmt, index)
680    }
681
682    /// The lossless [`TypeInfo`] of the result column at `index`.
683    #[must_use]
684    pub fn column_logical_type(
685        &self,
686        index: u64,
687    ) -> Option<TypeInfo> {
688        meta::column_type_info(self.stmt, index)
689    }
690
691    /// Executes the prepared statement and returns the result.
692    ///
693    /// Works for all statement types:
694    /// - **SELECT** — iterate rows via the [`Iterator`] impl on [`DuckResult`].
695    /// - **INSERT / UPDATE / DELETE** — check [`DuckResult::changes()`] for affected rows.
696    /// - **DDL** (`CREATE TABLE` etc.) — `.changes()` returns `0`, no rows to iterate.
697    /// - **INSERT … RETURNING** — iterate rows and/or call `.changes()`.
698    ///
699    /// # Errors
700    ///
701    /// Returns [`Error::DuckDBFailure`] if execution fails.
702    #[must_use = "the DuckResult carries both affected-row count (.changes()) and row iterator — consume it"]
703    pub fn execute(&mut self) -> Result<DuckResult> {
704        // SAFETY: `mem::zeroed::<ffi::duckdb_result>()` is the correct initialization for a
705        // DuckDB result output parameter. `duckdb_result` is a small `Copy` struct with no
706        // self-referential fields, so it needs no stable heap address — DuckResult::new
707        // takes it by value.
708        let mut out = unsafe { mem::zeroed::<ffi::duckdb_result>() };
709        // SAFETY: `self.stmt` is a valid prepared statement. `&mut out` provides a raw
710        // pointer to the stack-local zeroed duckdb_result output buffer. Ownership
711        // transfers to DuckResult::new; its Drop calls duckdb_destroy_result exactly once.
712        let r =
713            unsafe { ffi::duckdb_execute_prepared(self.stmt, &mut out as *mut ffi::duckdb_result) };
714        // The query is finished: advance the generation so a `QueryControl` minted
715        // for it can no longer interrupt a later query.
716        self._connection.advance_query();
717        result_from_duckdb_result(r, &mut out as *mut ffi::duckdb_result)?;
718        Ok(DuckResult::new(out))
719    }
720}
721
722impl Drop for CachedStatement {
723    fn drop(&mut self) {
724        if !self.stmt.is_null() {
725            // SAFETY: `self.stmt` is a valid prepared statement not yet destroyed.
726            // The null guard ensures this path runs at most once.
727            unsafe { ffi::duckdb_destroy_prepare(&mut self.stmt) };
728        }
729    }
730}
731
732// SAFETY: the only non-`Send` field is the raw `stmt` pointer; `Arc<ConnectionInner>`
733// is already `Send` because `ConnectionInner` is `Send + Sync`. DuckDB permits a
734// prepared statement to be used from a different thread than the one that prepared it,
735// provided it is not used concurrently — `CachedStatement` takes `&mut self` for every
736// operation that touches the handle and is deliberately not `Sync`, so no two threads
737// can drive it at once. Retaining the connection means moving the statement to another
738// thread also keeps its connection alive, so the handle cannot dangle.
739unsafe impl Send for CachedStatement {}
740
741#[cfg(test)]
742mod tests {
743    use super::*;
744    use crate::config::Config;
745    use crate::ffi::DUCKDB_TYPE_DUCKDB_TYPE_INTEGER;
746    use crate::helpers::path::path_to_cstring;
747    use crate::raw::connection::RawConnection;
748    use crate::types::{appendable::AppendAble, value::DuckValue};
749
750    struct CheckedI32(i32);
751    struct DummyAppendAble;
752
753    impl AppendAble for DummyAppendAble {
754        fn stmt_append(
755            &mut self,
756            _idx: u64,
757            _stmt: duckdb_prepared_statement,
758        ) -> Result<()> {
759            Ok(())
760        }
761
762        fn appender_append(
763            &mut self,
764            _appender: ffi::duckdb_appender,
765        ) -> Result<()> {
766            unreachable!("DummyAppendAble is only used for statement binding")
767        }
768    }
769
770    impl AppendAble for CheckedI32 {
771        fn stmt_append(
772            &mut self,
773            idx: u64,
774            stmt: duckdb_prepared_statement,
775        ) -> Result<()> {
776            // SAFETY: `stmt` comes from a live Statement and the scalar value is copied.
777            let state = unsafe { ffi::duckdb_bind_int32(stmt, idx, self.0) };
778            if state == DuckDBSuccess {
779                Ok(())
780            } else {
781                Err(Error::DuckDBFailure(ffi::Error::new(state), None))
782            }
783        }
784
785        fn appender_append(
786            &mut self,
787            _appender: crate::ffi::duckdb_appender,
788        ) -> Result<()> {
789            unreachable!("CheckedI32 is only used for statement binding")
790        }
791    }
792
793    fn get_test_connection() -> RawConnection {
794        let c_path = path_to_cstring(":memory:".as_ref()).unwrap();
795        let config = Config::default().with("duckdb_api", "rust").unwrap();
796        RawConnection::open_with_flags(&c_path, config).unwrap()
797    }
798
799    fn assert_single_value(
800        mut result: DuckResult,
801        column: &str,
802        expected: DuckValue,
803    ) {
804        let row = result.next().expect("expected one row").unwrap();
805        assert_eq!(row.get(column), Some(&expected));
806        assert!(result.next().is_none());
807    }
808
809    #[test]
810    fn test_new() {
811        let con = get_test_connection();
812        let sql = "SELECT 1";
813        let stmt = Statement::new(&con, sql);
814        assert!(stmt.is_ok());
815    }
816
817    #[test]
818    fn test_prepare_rejects_invalid_sql_and_interior_nul() {
819        let con = get_test_connection();
820
821        assert!(matches!(Statement::new(&con, "SELEC 1"), Err(Error::DuckDBFailure(..))));
822        assert!(matches!(Statement::new(&con, "SELECT \0 1"), Err(Error::NulError(_))));
823        assert!(matches!(CachedStatement::prepare(&con, "SELEC 1"), Err(Error::DuckDBFailure(..))));
824        assert!(matches!(CachedStatement::prepare(&con, "SELECT \0 1"), Err(Error::NulError(_))));
825    }
826
827    #[test]
828    fn test_statement_binds_real_values_and_reports_parameter_errors() {
829        let con = get_test_connection();
830        let mut stmt = Statement::new(&con, "SELECT $1::INTEGER + $2::INTEGER AS total").unwrap();
831        assert_eq!(stmt.bind_parameter_count(), 2);
832
833        let mut first = CheckedI32(19);
834        let mut second = CheckedI32(23);
835        stmt.bind(&mut first).unwrap();
836        stmt.bind(&mut second).unwrap();
837        assert_single_value(stmt.execute().unwrap(), "total", DuckValue::Int(42));
838
839        let mut out_of_range = CheckedI32(99);
840        assert!(matches!(stmt.bind_at(&mut out_of_range, 3), Err(Error::DuckDBFailure(..))));
841        assert_single_value(stmt.execute().unwrap(), "total", DuckValue::Int(42));
842    }
843
844    #[test]
845    fn test_statement_clear_bindings_resets_and_reuses() {
846        let con = get_test_connection();
847        let mut stmt = Statement::new(&con, "SELECT $1::INTEGER AS value").unwrap();
848        let mut first = CheckedI32(7);
849        stmt.bind(&mut first).unwrap();
850        assert_single_value(stmt.execute().unwrap(), "value", DuckValue::Int(7));
851
852        stmt.clear_bindings().unwrap();
853        assert_eq!(stmt.bind_idx, 0);
854        let mut second = CheckedI32(11);
855        stmt.bind(&mut second).unwrap();
856        assert_single_value(stmt.execute().unwrap(), "value", DuckValue::Int(11));
857    }
858
859    #[test]
860    fn test_cached_statement_retains_sql_and_resets_for_reuse() {
861        let con = get_test_connection();
862        let sql = "SELECT $1::INTEGER AS value";
863        let mut stmt = CachedStatement::prepare(&con, sql).unwrap();
864        assert_eq!(stmt.sql.as_ref(), sql);
865
866        let mut first = CheckedI32(100);
867        stmt.bind(1, &mut first).unwrap();
868        assert_single_value(stmt.execute().unwrap(), "value", DuckValue::Int(100));
869
870        stmt.reset_bindings().unwrap();
871        let mut second = CheckedI32(200);
872        stmt.bind(1, &mut second).unwrap();
873        assert_single_value(stmt.execute().unwrap(), "value", DuckValue::Int(200));
874    }
875
876    #[test]
877    fn borrowing_pending_execution_runs_the_statement() {
878        // `pending()` borrows the statement (unlike `into_pending`, which consumes
879        // it) and drives the query to completion via `execute()`.
880        let con = get_test_connection();
881        let stmt = CachedStatement::prepare(&con, "SELECT 1 AS v").unwrap();
882        let result = stmt.pending().unwrap().execute().unwrap();
883        assert_single_value(result, "v", DuckValue::Int(1));
884    }
885
886    #[test]
887    fn test_cached_statement_recovers_after_invalid_bind_position() {
888        let con = get_test_connection();
889        let mut stmt = CachedStatement::prepare(&con, "SELECT $1::INTEGER AS value").unwrap();
890        let mut invalid = CheckedI32(1);
891        assert!(matches!(stmt.bind(0, &mut invalid), Err(Error::DuckDBFailure(..))));
892
893        stmt.reset_bindings().unwrap();
894        let mut valid = CheckedI32(55);
895        stmt.bind(1, &mut valid).unwrap();
896        assert_single_value(stmt.execute().unwrap(), "value", DuckValue::Int(55));
897    }
898
899    #[test]
900    fn test_raw_and_connection() {
901        let con = get_test_connection();
902        let sql = "SELECT 1";
903        let stmt = Statement::new(&con, sql).unwrap();
904        let _raw = stmt.raw();
905        let _con = stmt.connection();
906    }
907
908    #[test]
909    fn test_execute() {
910        let con = get_test_connection();
911        let sql = "SELECT 1";
912        let mut stmt = Statement::new(&con, sql).unwrap();
913        let result = stmt.execute();
914        assert!(result.is_ok());
915    }
916
917    #[test]
918    fn test_execute_can_be_called_multiple_times() {
919        let con = get_test_connection();
920        let sql = "SELECT 1";
921        let mut stmt = Statement::new(&con, sql).unwrap();
922        assert!(stmt.execute().is_ok());
923        assert!(stmt.execute().is_ok());
924    }
925
926    #[test]
927    fn test_clear_bindings_resets_idx() {
928        let con = get_test_connection();
929        let sql = "SELECT $1";
930        let mut stmt = Statement::new(&con, sql).unwrap();
931        let mut dummy = DummyAppendAble;
932        stmt.bind(&mut dummy).unwrap();
933        assert_eq!(stmt.bind_idx, 1);
934        stmt.clear_bindings().unwrap();
935        assert_eq!(stmt.bind_idx, 0);
936    }
937
938    #[test]
939    fn statement_type_is_classified() {
940        let mut con = get_test_connection();
941        let select = CachedStatement::prepare(&con, "SELECT 1").unwrap();
942        assert_eq!(select.statement_type(), StatementType::Select);
943        drop(select);
944
945        con.query("CREATE TABLE t (id INTEGER)").unwrap();
946        let insert = CachedStatement::prepare(&con, "INSERT INTO t VALUES (1)").unwrap();
947        assert_eq!(insert.statement_type(), StatementType::Insert);
948    }
949
950    #[test]
951    fn prepared_parameter_metadata_names_types_and_index() {
952        let con = get_test_connection();
953        // Two named parameters. (DuckDB rejects mixing named `$x` and positional
954        // `?` in one statement, so both are named here.)
955        let stmt = CachedStatement::prepare(&con, "SELECT $id::INTEGER AS a, $label::VARCHAR AS b")
956            .unwrap();
957        assert_eq!(stmt.parameter_count(), 2);
958        // Each named parameter reports its name; index round-trips.
959        assert_eq!(stmt.parameter_name(1).as_deref(), Some("id"));
960        assert_eq!(stmt.parameter_name(2).as_deref(), Some("label"));
961        assert_eq!(stmt.parameter_index("id").unwrap(), 1);
962        assert_eq!(stmt.parameter_index("label").unwrap(), 2);
963        assert_eq!(stmt.parameter_type(1), DUCKDB_TYPE_DUCKDB_TYPE_INTEGER);
964        assert_eq!(
965            stmt.parameter_logical_type(1),
966            Some(TypeInfo::Scalar(DUCKDB_TYPE_DUCKDB_TYPE_INTEGER))
967        );
968
969        // Missing name and out-of-range index are precise errors / None.
970        assert!(
971            matches!(stmt.parameter_index("nope"), Err(Error::InvalidParameterName(n)) if n == "nope")
972        );
973        assert!(matches!(stmt.parameter_index("a\0b"), Err(Error::NulError(_))));
974        assert_eq!(stmt.parameter_name(999), None);
975    }
976
977    #[test]
978    fn prepared_output_column_schema() {
979        let con = get_test_connection();
980        let stmt = CachedStatement::prepare(
981            &con,
982            "SELECT 1::INTEGER AS id, CAST(1.5 AS DECIMAL(8,2)) AS amount",
983        )
984        .unwrap();
985        assert_eq!(stmt.column_count(), 2);
986        assert_eq!(stmt.column_name(0).as_deref(), Some("id"));
987        assert_eq!(stmt.column_name(1).as_deref(), Some("amount"));
988        assert_eq!(stmt.column_type(0), DUCKDB_TYPE_DUCKDB_TYPE_INTEGER);
989        // Lossless output type keeps the declared DECIMAL precision.
990        assert_eq!(stmt.column_logical_type(1), Some(TypeInfo::Decimal { width: 8, scale: 2 }));
991        assert_eq!(stmt.column_name(999), None);
992    }
993
994    #[test]
995    fn bind_named_binds_by_parameter_name() {
996        let con = get_test_connection();
997        let mut stmt = CachedStatement::prepare(&con, "SELECT $value::INTEGER AS value").unwrap();
998        let mut v = CheckedI32(77);
999        stmt.bind_named("value", &mut v).unwrap();
1000        assert_single_value(stmt.execute().unwrap(), "value", DuckValue::Int(77));
1001
1002        // An unknown name is a precise error, and the statement stays usable.
1003        let mut other = CheckedI32(1);
1004        assert!(matches!(
1005            stmt.bind_named("missing", &mut other),
1006            Err(Error::InvalidParameterName(n)) if n == "missing"
1007        ));
1008    }
1009}