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}