Skip to main content

better_duck_core/raw/
statement.rs

1use std::{ffi::CString, mem, ptr};
2
3use crate::ffi::{
4    duckdb_clear_bindings, duckdb_destroy_prepare, duckdb_execute_prepared, duckdb_nparams,
5    duckdb_prepare, duckdb_result, DuckDBSuccess,
6};
7
8use crate::{
9    error::{Error, Result},
10    ffi,
11    ffi::duckdb_prepared_statement,
12    helpers::duck_result::{result_from_duckdb_prepare, result_from_duckdb_result},
13    raw::{connection::RawConnection, result::DuckResult},
14    types::appendable::AppendAble,
15};
16
17/// A prepared DuckDB statement that can be executed one or more times.
18///
19/// After calling [`execute`](Statement::execute), the statement can be reused
20/// by calling [`clear_bindings`](Statement::clear_bindings) and re-binding parameters.
21pub struct Statement<'a> {
22    /// Reference to the underlying DuckDB connection.
23    con: &'a RawConnection,
24    /// Pointer to the prepared DuckDB statement (FFI resource).
25    stmt: duckdb_prepared_statement,
26    /// 1-based index of the next parameter to bind (incremented by each `bind` call).
27    bind_idx: u64,
28}
29
30impl Statement<'_> {
31    /// Prepares a new `Statement` from an SQL string.
32    ///
33    /// # Errors
34    ///
35    /// Returns an error if the SQL cannot be compiled into a prepared statement.
36    pub(super) fn new<'a, 'b: 'a>(
37        con: &'b RawConnection,
38        sql: &str,
39    ) -> Result<Statement<'a>> {
40        let mut stmt: duckdb_prepared_statement = ptr::null_mut();
41        let c_str = std::ffi::CString::new(sql)?;
42        // SAFETY: `con.con` is a valid open `duckdb_connection`; `c_str` is a valid
43        // null-terminated C string. `stmt` is a valid output pointer.
44        let resp = unsafe { duckdb_prepare(con.con, c_str.as_ptr(), &mut stmt) };
45        result_from_duckdb_prepare(resp, stmt)?;
46        Ok(Statement { con, stmt, bind_idx: 0 })
47    }
48
49    /// Returns a reference to the raw prepared-statement pointer.
50    #[allow(unused)]
51    #[inline]
52    fn raw(&self) -> &duckdb_prepared_statement {
53        &self.stmt
54    }
55
56    /// Returns a reference to the underlying raw connection.
57    #[allow(unused)]
58    #[inline]
59    fn connection(&self) -> &RawConnection {
60        self.con
61    }
62}
63
64// Exposed API
65impl Statement<'_> {
66    /// Binds a value to the next positional parameter (1-based).
67    ///
68    /// The first call binds parameter 1, the second call parameter 2, and so on.
69    /// Call [`clear_bindings`](Statement::clear_bindings) to reset the counter.
70    ///
71    /// # Errors
72    ///
73    /// Returns an error if the underlying DuckDB bind call fails.
74    #[must_use = "bind result should be checked"]
75    #[allow(unused)]
76    #[inline]
77    pub fn bind<T: AppendAble>(
78        &mut self,
79        binder: &mut T,
80    ) -> Result<()> {
81        self.bind_idx += 1;
82        // Pass the 1-based index directly to stmt_append.
83        self.bind_at(binder, self.bind_idx)
84    }
85
86    /// Binds a value to the parameter at the given 1-based index.
87    ///
88    /// # Arguments
89    ///
90    /// * `binder` - The value to bind, must implement [`AppendAble`].
91    /// * `idx` - The 1-based parameter index.
92    ///
93    /// # Errors
94    ///
95    /// Returns an error if the underlying DuckDB bind call fails.
96    #[allow(unused)]
97    #[inline]
98    pub fn bind_at<T: AppendAble>(
99        &self,
100        binder: &mut T,
101        idx: u64,
102    ) -> Result<()> {
103        binder.stmt_append(idx, self.stmt)
104    }
105
106    /// Executes the prepared statement and returns the result.
107    ///
108    /// The statement can be re-executed after calling [`clear_bindings`](Statement::clear_bindings)
109    /// and re-binding parameters.
110    ///
111    /// # Errors
112    ///
113    /// Returns an error if execution fails.
114    #[must_use = "execute returns the query result; dropping it without reading discards rows"]
115    #[allow(unused)]
116    pub fn execute(&mut self) -> Result<DuckResult> {
117        // SAFETY: `mem::zeroed::<duckdb_result>()` produces an all-zeros value, which is
118        // the correct initial state for a `duckdb_result` output parameter. `duckdb_result`
119        // is a small `Copy` struct with no self-referential fields, so it needs no stable
120        // heap address — DuckResult::new takes it by value.
121        let mut out = unsafe { mem::zeroed::<duckdb_result>() };
122        // SAFETY: `self.stmt` is a valid prepared statement. `&mut out` provides a pointer
123        // to the stack-local zeroed `duckdb_result`. Ownership transfers to `DuckResult::new`,
124        // whose `Drop` calls `duckdb_destroy_result` once.
125        let resp = unsafe { duckdb_execute_prepared(self.stmt, &mut out as *mut duckdb_result) };
126        result_from_duckdb_result(resp, &mut out as *mut duckdb_result)?;
127        Ok(DuckResult::new(out))
128    }
129
130    /// Returns the number of parameters in the prepared statement.
131    #[allow(unused)]
132    #[inline]
133    pub fn bind_parameter_count(&self) -> usize {
134        // SAFETY: `self.stmt` is a valid prepared statement.
135        unsafe { duckdb_nparams(self.stmt) as usize }
136    }
137
138    /// Clears all parameter bindings and resets the bind index to zero.
139    ///
140    /// After calling this method, subsequent [`bind`](Statement::bind) calls start
141    /// from parameter 1 again.
142    ///
143    /// # Errors
144    ///
145    /// Returns an error if the DuckDB clear-bindings call fails.
146    #[must_use = "clear_bindings result should be checked"]
147    #[allow(unused)]
148    #[inline]
149    pub fn clear_bindings(&mut self) -> Result<()> {
150        // SAFETY: `self.stmt` is a valid prepared statement.
151        let res = unsafe { duckdb_clear_bindings(self.stmt) };
152        if res != DuckDBSuccess {
153            Err(Error::DuckDBFailure(
154                crate::ffi::Error::new(crate::ffi::DuckDBError),
155                Some("Failed to clear bindings".to_owned()),
156            ))
157        } else {
158            self.bind_idx = 0;
159            Ok(())
160        }
161    }
162
163    /// Returns `true` if the prepared statement pointer is null (not initialized).
164    #[allow(unused)]
165    #[inline]
166    pub fn is_null(&self) -> bool {
167        self.stmt.is_null()
168    }
169}
170
171/// Destroys the prepared statement when the `Statement` is dropped.
172impl Drop for Statement<'_> {
173    fn drop(&mut self) {
174        // SAFETY: `self.stmt` is a valid duckdb_prepared_statement (or null).
175        // `duckdb_destroy_prepare` is idempotent and handles the non-null check itself,
176        // but we guard here as a belt-and-suspenders measure.
177        unsafe {
178            if !self.stmt.is_null() {
179                duckdb_destroy_prepare(&mut self.stmt);
180            }
181        }
182    }
183}
184
185/// A prepared statement that can be reset and re-executed with different bindings.
186///
187/// Unlike `Statement`, `CachedStatement` is not tied to a connection's lifetime.
188/// It remains valid as long as the underlying database is open.
189///
190/// This type is used by Diesel statement cache
191/// (`StatementCache<DuckDb, CachedStatement>`).
192pub struct CachedStatement {
193    /// SQL source retained for statement-cache key comparisons.
194    ///
195    /// Not read within `better-duck-core` itself; consumed by
196    /// `StatementCache` implementation in `better-duck-diesel`.
197    #[allow(dead_code)]
198    pub(crate) sql: Box<str>,
199    /// Raw prepared-statement handle.
200    stmt: ffi::duckdb_prepared_statement,
201}
202
203impl CachedStatement {
204    /// Prepares `sql` against the given connection.
205    ///
206    /// # Errors
207    ///
208    /// Returns [`Error::DuckDBFailure`] if DuckDB cannot parse or plan the query,
209    /// or [`Error::NulError`] if `sql` contains an interior nul byte.
210    pub fn prepare(
211        conn: &RawConnection,
212        sql: impl AsRef<str>,
213    ) -> Result<Self> {
214        let sql_str = sql.as_ref();
215        let mut stmt: ffi::duckdb_prepared_statement = ptr::null_mut();
216        let c_str = CString::new(sql_str)?;
217        // SAFETY: `conn.con` is a valid open duckdb_connection. `c_str` is a
218        // valid null-terminated CString that outlives this call. `&mut stmt` is a
219        // valid output pointer. `duckdb_prepare` does not retain either pointer.
220        let r = unsafe { ffi::duckdb_prepare(conn.con, c_str.as_ptr(), &mut stmt) };
221        result_from_duckdb_prepare(r, stmt)?;
222        Ok(CachedStatement { sql: sql_str.into(), stmt })
223    }
224
225    /// Resets all parameter bindings so the statement can be re-executed.
226    ///
227    /// # Errors
228    ///
229    /// Returns [`Error::DuckDBFailure`] if the DuckDB clear-bindings call fails.
230    pub fn reset_bindings(&mut self) -> Result<()> {
231        // SAFETY: `self.stmt` is a valid prepared statement — the Drop impl enforces this.
232        let r = unsafe { ffi::duckdb_clear_bindings(self.stmt) };
233        if r == ffi::DuckDBSuccess {
234            Ok(())
235        } else {
236            Err(Error::DuckDBFailure(ffi::Error::new(r), None))
237        }
238    }
239
240    /// Binds `value` at the given **1-based** parameter index.
241    ///
242    /// # Errors
243    ///
244    /// Returns an error if the underlying DuckDB bind call fails or `idx` is out of range.
245    pub fn bind<T: AppendAble + ?Sized>(
246        &mut self,
247        idx: u64,
248        value: &mut T,
249    ) -> Result<()> {
250        value.stmt_append(idx, self.stmt)
251    }
252
253    /// Executes the prepared statement and returns the result.
254    ///
255    /// Works for all statement types:
256    /// - **SELECT** — iterate rows via the [`Iterator`] impl on [`DuckResult`].
257    /// - **INSERT / UPDATE / DELETE** — check [`DuckResult::changes()`] for affected rows.
258    /// - **DDL** (`CREATE TABLE` etc.) — `.changes()` returns `0`, no rows to iterate.
259    /// - **INSERT … RETURNING** — iterate rows and/or call `.changes()`.
260    ///
261    /// # Errors
262    ///
263    /// Returns [`Error::DuckDBFailure`] if execution fails.
264    #[must_use = "the DuckResult carries both affected-row count (.changes()) and row iterator — consume it"]
265    pub fn execute(&mut self) -> Result<DuckResult> {
266        // SAFETY: `mem::zeroed::<ffi::duckdb_result>()` is the correct initialization for a
267        // DuckDB result output parameter. `duckdb_result` is a small `Copy` struct with no
268        // self-referential fields, so it needs no stable heap address — DuckResult::new
269        // takes it by value.
270        let mut out = unsafe { mem::zeroed::<ffi::duckdb_result>() };
271        // SAFETY: `self.stmt` is a valid prepared statement. `&mut out` provides a raw
272        // pointer to the stack-local zeroed duckdb_result output buffer. Ownership
273        // transfers to DuckResult::new; its Drop calls duckdb_destroy_result exactly once.
274        let r =
275            unsafe { ffi::duckdb_execute_prepared(self.stmt, &mut out as *mut ffi::duckdb_result) };
276        result_from_duckdb_result(r, &mut out as *mut ffi::duckdb_result)?;
277        Ok(DuckResult::new(out))
278    }
279}
280
281impl Drop for CachedStatement {
282    fn drop(&mut self) {
283        if !self.stmt.is_null() {
284            // SAFETY: `self.stmt` is a valid prepared statement not yet destroyed.
285            // The null guard ensures this path runs at most once.
286            unsafe { ffi::duckdb_destroy_prepare(&mut self.stmt) };
287        }
288    }
289}
290
291// SAFETY: A prepared statement is tied to its database, not to a thread.
292// The database handle is reference-counted via Arc<RawDatabase>, making it
293// safe to move a CachedStatement to another thread.
294unsafe impl Send for CachedStatement {}
295
296#[cfg(test)]
297mod tests {
298    use super::*;
299    use crate::config::Config;
300    use crate::helpers::path::path_to_cstring;
301    use crate::raw::connection::RawConnection;
302    use crate::types::appendable::AppendAble;
303
304    struct DummyAppendAble;
305    impl AppendAble for DummyAppendAble {
306        fn stmt_append(
307            &mut self,
308            _idx: u64,
309            _stmt: duckdb_prepared_statement,
310        ) -> Result<()> {
311            Ok(())
312        }
313        fn appender_append(
314            &mut self,
315            _appender: crate::ffi::duckdb_appender,
316        ) -> Result<()> {
317            Ok(())
318        }
319    }
320
321    fn get_test_connection() -> RawConnection {
322        let c_path = path_to_cstring(":memory:".as_ref()).unwrap();
323        let config = Config::default().with("duckdb_api", "rust").unwrap();
324        RawConnection::open_with_flags(&c_path, config).unwrap()
325    }
326
327    #[test]
328    fn test_new() {
329        let con = get_test_connection();
330        let sql = "SELECT 1";
331        let stmt = Statement::new(&con, sql);
332        assert!(stmt.is_ok());
333    }
334
335    #[test]
336    fn test_bind_and_bind_at() {
337        let con = get_test_connection();
338        let sql = "SELECT ?";
339        let mut stmt = Statement::new(&con, sql).unwrap();
340        let mut dummy = DummyAppendAble;
341        assert!(stmt.bind(&mut dummy).is_ok());
342        assert!(stmt.bind_at(&mut dummy, 1).is_ok());
343    }
344
345    #[test]
346    fn test_raw_and_connection() {
347        let con = get_test_connection();
348        let sql = "SELECT 1";
349        let stmt = Statement::new(&con, sql).unwrap();
350        let _raw = stmt.raw();
351        let _con = stmt.connection();
352    }
353
354    #[test]
355    fn test_execute() {
356        let con = get_test_connection();
357        let sql = "SELECT 1";
358        let mut stmt = Statement::new(&con, sql).unwrap();
359        let result = stmt.execute();
360        assert!(result.is_ok());
361    }
362
363    #[test]
364    fn test_execute_can_be_called_multiple_times() {
365        let con = get_test_connection();
366        let sql = "SELECT 1";
367        let mut stmt = Statement::new(&con, sql).unwrap();
368        assert!(stmt.execute().is_ok());
369        assert!(stmt.execute().is_ok());
370    }
371
372    #[test]
373    fn test_clear_bindings_resets_idx() {
374        let con = get_test_connection();
375        let sql = "SELECT $1";
376        let mut stmt = Statement::new(&con, sql).unwrap();
377        let mut dummy = DummyAppendAble;
378        stmt.bind(&mut dummy).unwrap();
379        assert_eq!(stmt.bind_idx, 1);
380        stmt.clear_bindings().unwrap();
381        assert_eq!(stmt.bind_idx, 0);
382    }
383}