better_duck_core/raw/connection.rs
1use std::{
2 ffi::{c_void, CStr, CString},
3 mem,
4 os::raw::c_char,
5 ptr, str,
6 sync::{
7 atomic::{AtomicU64, Ordering},
8 Arc,
9 },
10};
11
12use crate::{
13 config::Config,
14 error::{Error, Result},
15 ffi::{
16 duckdb_close, duckdb_connect, duckdb_connection, duckdb_database, duckdb_disconnect,
17 duckdb_free, duckdb_interrupt, duckdb_open_ext, duckdb_query, duckdb_query_progress,
18 duckdb_result, DuckDBError, DuckDBSuccess, Error as FFIError,
19 },
20 helpers::duck_result::result_from_duckdb_result,
21 raw::{
22 appender::Appender,
23 result::DuckResult,
24 statement::{CachedStatement, Statement},
25 },
26 types::appendable::AppendAble,
27};
28
29/// `RawDatabase` is a low-level wrapper around a DuckDB database handle.
30///
31/// This struct provides direct access to the underlying DuckDB database pointer.
32/// It is intended for advanced use cases where you need to manage the database handle manually.
33///
34/// **Thread Safety:**
35/// `RawDatabase` itself is **not** thread-safe. If you need to share it between threads or
36/// multiple connections, wrap it in a thread-safe container such as [`Arc`](std::sync::Arc).
37///
38/// # Example
39///
40/// ```rust,ignore
41/// use std::sync::Arc;
42/// use better_duck_core::raw::RawDatabase; // raw is crate-private
43/// use better_duck_core::ffi;
44///
45/// let mut db: ffi::duckdb_database = std::ptr::null_mut();
46/// let path = std::ffi::CString::new(":memory:").unwrap();
47/// let r = unsafe { ffi::duckdb_open(path.as_ptr(), &mut db) };
48/// assert_eq!(r, ffi::DuckDBSuccess);
49/// let raw_db = unsafe { RawDatabase::new(db).unwrap() };
50/// let shared = Arc::new(raw_db);
51/// ```
52pub struct RawDatabase(pub(crate) duckdb_database);
53impl RawDatabase {
54 /// Creates a new [`RawDatabase`] from an existing raw database handle.
55 ///
56 /// # Safety
57 ///
58 /// `db` must be a valid, open `duckdb_database` obtained from a successful call to
59 /// `duckdb_open` or `duckdb_open_ext`. Passing a null or invalid pointer is
60 /// undefined behavior.
61 ///
62 /// # Errors
63 ///
64 /// Returns an error if `db` is null.
65 #[inline]
66 pub unsafe fn new(db: duckdb_database) -> Result<RawDatabase> {
67 if db.is_null() {
68 return Err(Error::DuckDBFailure(
69 FFIError::new(DuckDBError),
70 Some("database is null".to_owned()),
71 ));
72 }
73 Ok(RawDatabase(db))
74 }
75
76 /// Opens a database at the given path with the specified config.
77 ///
78 /// Pass a path of `":memory:"` for an in-memory database.
79 ///
80 /// # Errors
81 ///
82 /// Returns an error if the database cannot be opened.
83 pub(crate) fn open_with_flags(
84 c_path: &CStr,
85 config: Config,
86 ) -> Result<RawDatabase> {
87 // SAFETY: `c_path` is a valid null-terminated C string. `db` and `c_err` are valid
88 // output pointers. On error we free `c_err` via `duckdb_free`.
89 unsafe {
90 let mut db: duckdb_database = ptr::null_mut();
91 let mut c_err = std::ptr::null_mut();
92 let r = duckdb_open_ext(c_path.as_ptr(), &mut db, config.duckdb_config(), &mut c_err);
93 if r != DuckDBSuccess {
94 let msg = Some(CStr::from_ptr(c_err).to_string_lossy().to_string());
95 duckdb_free(c_err as *mut c_void);
96 return Err(Error::DuckDBFailure(FFIError::new(r), msg));
97 }
98 RawDatabase::new(db)
99 }
100 }
101}
102// SAFETY: The DuckDB database handle is internally reference-counted and thread-safe.
103// Multiple connections (each on its own thread) may share the same database handle.
104unsafe impl Send for RawDatabase {}
105// SAFETY: Read-only access to the database handle (`db.0`) does not mutate DuckDB state.
106// All mutation goes through `duckdb_connect`/`duckdb_close` which are themselves thread-safe.
107unsafe impl Sync for RawDatabase {}
108
109impl Drop for RawDatabase {
110 #[inline]
111 fn drop(&mut self) {
112 // SAFETY: `self.0` is a valid duckdb_database (or null). `duckdb_close` accepts
113 // null and is idempotent. After this call the handle is invalidated.
114 unsafe {
115 if !self.0.is_null() {
116 duckdb_close(&mut self.0);
117 }
118 }
119 }
120}
121
122/// Sole owner of one open DuckDB connection handle.
123///
124/// Every resource that DuckDB scopes to a connection — prepared statements,
125/// appenders, and later pending/control handles — retains an
126/// [`Arc<ConnectionInner>`]. That makes "a child cannot outlive its connection"
127/// a type invariant instead of a convention the caller has to remember: the
128/// handle is disconnected in [`Drop`], which cannot run while any child still
129/// holds a reference.
130///
131/// Sharing the *connection* is deliberate, and different from sharing the
132/// database. Opening a second connection to the same database would put the
133/// child in a different transaction scope, so cached statements and appenders
134/// would silently run outside the caller's `BEGIN`/`ROLLBACK`.
135pub(crate) struct ConnectionInner {
136 /// Owned connection handle. Disconnected exactly once, in `Drop`.
137 con: duckdb_connection,
138 /// Keeps the database open for at least as long as this connection.
139 db: Arc<RawDatabase>,
140 /// Monotonic counter identifying the *current* query on this connection.
141 ///
142 /// Advanced once each query completes (see [`ConnectionInner::advance_query`]).
143 /// A [`QueryControl`] captures the generation live when it is minted; its
144 /// [`interrupt`](QueryControl::interrupt) is a no-op once the generation has
145 /// moved on, so a delayed cancellation can never interrupt a *later* query.
146 generation: AtomicU64,
147}
148
149impl ConnectionInner {
150 /// Opens a connection against `db`.
151 ///
152 /// # Errors
153 ///
154 /// Returns [`Error::DuckDBFailure`] if DuckDB cannot establish the connection.
155 fn connect(db: Arc<RawDatabase>) -> Result<Arc<ConnectionInner>> {
156 let mut con: duckdb_connection = ptr::null_mut();
157 // SAFETY: `db.0` is a valid open duckdb_database kept alive by the `Arc`;
158 // `con` is a valid output pointer.
159 let r = unsafe { duckdb_connect(db.0, &mut con) };
160 if r != DuckDBSuccess {
161 // SAFETY: `con` may be partially initialized on failure; `duckdb_disconnect`
162 // handles null/invalid handles gracefully and nulls the pointer.
163 unsafe { duckdb_disconnect(&mut con) };
164 return Err(Error::DuckDBFailure(FFIError::new(r), Some("connect error".to_owned())));
165 }
166 Ok(Arc::new(ConnectionInner { con, db, generation: AtomicU64::new(0) }))
167 }
168
169 /// Returns the raw connection handle.
170 ///
171 /// Crate-internal: callers must not retain the handle beyond the borrow, and
172 /// must not use it concurrently from another thread (see the `Sync` note below).
173 #[inline]
174 pub(crate) fn handle(&self) -> duckdb_connection {
175 self.con
176 }
177
178 /// Reads the current query generation.
179 #[inline]
180 pub(crate) fn generation(&self) -> u64 {
181 self.generation.load(Ordering::Acquire)
182 }
183
184 /// Advances the query generation, invalidating any outstanding
185 /// [`QueryControl`] minted for the query that just finished.
186 ///
187 /// Called at the end of every query/statement execution.
188 #[inline]
189 pub(crate) fn advance_query(&self) {
190 self.generation.fetch_add(1, Ordering::AcqRel);
191 }
192
193 /// Returns the shared database this connection belongs to.
194 #[inline]
195 pub(crate) fn database(&self) -> &Arc<RawDatabase> {
196 &self.db
197 }
198}
199
200impl Drop for ConnectionInner {
201 #[inline]
202 fn drop(&mut self) {
203 // SAFETY: `self.con` is the valid handle produced by `connect` and has not been
204 // disconnected before: `ConnectionInner` is only reachable behind an `Arc`, so
205 // this runs exactly once, when the last child reference is released.
206 // `duckdb_disconnect` returns void and tolerates null, so this cannot fail or panic.
207 unsafe { duckdb_disconnect(&mut self.con) };
208 }
209}
210
211// SAFETY: a `duckdb_connection` holds no thread-local state, so DuckDB permits using
212// it from a thread other than the one that created it. `ConnectionInner` owns its
213// handle outright and exposes no interior mutability, so transferring it (always
214// behind an `Arc`) to another thread cannot invalidate the handle.
215unsafe impl Send for ConnectionInner {}
216
217// SAFETY: DuckDB does *not* allow two overlapping calls on one connection, so `Sync`
218// is justified only because a shared `&ConnectionInner` cannot produce such a call.
219// `con` is private and its sole accessor is the crate-internal `handle()`; every type
220// that can reach it (`RawConnection`, `CachedStatement`, `Appender`) is `Send` but not
221// `Sync`, and each takes `&mut self` for the operations that drive the connection.
222// So `&ConnectionInner` can be observed from several threads while the connection
223// itself stays exclusively owned by whichever handle is using it.
224unsafe impl Sync for ConnectionInner {}
225
226/// A snapshot of a running query's progress.
227///
228/// Values mirror DuckDB's `duckdb_query_progress_type`. `percentage` is `-1.0`
229/// when DuckDB cannot estimate progress (e.g. progress reporting disabled, or no
230/// query running).
231#[derive(Debug, Clone, Copy, PartialEq)]
232pub struct QueryProgress {
233 /// Completion in `0.0..=100.0`, or `-1.0` when unknown.
234 pub percentage: f64,
235 /// Rows processed so far.
236 pub rows_processed: u64,
237 /// Total rows the query expects to process.
238 pub total_rows_to_process: u64,
239}
240
241/// A narrow, thread-safe handle for interrupting or observing one query.
242///
243/// Minted by [`Connection::query_control`](crate::connection::Connection::query_control).
244/// Unlike the connection itself,
245/// `QueryControl` is `Clone + Send + Sync`: DuckDB explicitly permits
246/// `duckdb_interrupt` and `duckdb_query_progress` to be called from a *different*
247/// thread than the one running the query — that is their entire purpose. It
248/// exposes **only** those two read/signal operations, never anything that would
249/// execute SQL, so it cannot create a second concurrent user of the connection.
250///
251/// # Generation scoping
252///
253/// The control captures the connection's query generation when minted.
254/// [`interrupt`](QueryControl::interrupt) only signals DuckDB while that
255/// generation is still current; once the query completes (advancing the
256/// generation) it becomes a no-op. This makes a delayed or racing cancellation
257/// unable to interrupt a subsequent, unrelated query on the same connection.
258#[derive(Clone)]
259pub struct QueryControl {
260 inner: Arc<ConnectionInner>,
261 generation: u64,
262}
263
264impl QueryControl {
265 /// Mints a control from the shared connection owner, capturing the current
266 /// generation.
267 ///
268 /// Crate-internal: lets the async layer obtain a control from its retained
269 /// `Arc<ConnectionInner>` *without* locking the mutex that guards the
270 /// `Connection` — which is the whole point, since that mutex is held by the
271 /// running native query the control needs to interrupt.
272 ///
273 /// Gated on `async`: only the async layer mints a control this way, so under
274 /// the default build it would otherwise be dead code.
275 #[cfg(feature = "async")]
276 #[inline]
277 pub(crate) fn from_inner(inner: Arc<ConnectionInner>) -> QueryControl {
278 let generation = inner.generation();
279 QueryControl { inner, generation }
280 }
281
282 /// Requests interruption of the query this control was minted for.
283 ///
284 /// Idempotent, and a no-op once that query has finished (the generation has
285 /// advanced). Returns `true` if the interrupt was actually signalled to
286 /// DuckDB, `false` if it was skipped as stale.
287 pub fn interrupt(&self) -> bool {
288 if self.inner.generation() != self.generation {
289 return false;
290 }
291 // SAFETY: `self.inner` keeps the connection alive, so the handle is valid.
292 // DuckDB documents `duckdb_interrupt` as safe to call from another thread
293 // while a query runs on the connection; it only sets an interrupt flag.
294 unsafe { duckdb_interrupt(self.inner.handle()) };
295 true
296 }
297
298 /// Reads the progress of the query currently running on the connection.
299 ///
300 /// Returns `None` once the query this control was minted for has finished
301 /// (the generation has advanced), so a stale control cannot report a later
302 /// query's progress as its own.
303 pub fn progress(&self) -> Option<QueryProgress> {
304 if self.inner.generation() != self.generation {
305 return None;
306 }
307 // SAFETY: `self.inner` keeps the connection alive. DuckDB documents
308 // `duckdb_query_progress` as safe to call while a query runs; it copies a
309 // small POD struct out.
310 let raw = unsafe { duckdb_query_progress(self.inner.handle()) };
311 Some(QueryProgress {
312 percentage: raw.percentage,
313 rows_processed: raw.rows_processed,
314 total_rows_to_process: raw.total_rows_to_process,
315 })
316 }
317
318 /// Returns `true` if the query this control was minted for is still current.
319 #[must_use]
320 pub fn is_active(&self) -> bool {
321 self.inner.generation() == self.generation
322 }
323}
324
325/// A low-level connection to a DuckDB database.
326///
327/// `RawConnection` is a handle to a shared [`ConnectionInner`]. It provides methods to
328/// execute SQL commands and to create connection-scoped children.
329///
330/// # Thread Safety
331///
332/// A `RawConnection` may be moved between threads but not used from two at once.
333/// For a genuinely independent connection to the same database, use
334/// [`try_clone`](RawConnection::try_clone), which performs a fresh `duckdb_connect`
335/// and therefore gets its own transaction scope.
336///
337/// # Resource Management
338///
339/// The connection is disconnected once this handle and every child it produced
340/// (prepared statements, appenders) have been dropped. The database stays open
341/// until the last connection to it is released. [`close`](RawConnection::close)
342/// consumes the handle and reports whether children were still outstanding.
343///
344/// # Example
345///
346/// ```rust,ignore
347/// use better_duck_core::raw::RawConnection; // raw is crate-private
348/// use std::ffi::CString;
349/// let path = CString::new(":memory:").unwrap();
350/// let mut conn = RawConnection::open_with_flags(&path, Default::default()).unwrap();
351/// let _ = conn.query("CREATE TABLE test (id INTEGER, name TEXT)").unwrap();
352/// let mut conn2 = conn.try_clone().unwrap();
353/// ```
354pub struct RawConnection {
355 /// Shared owner of the connection handle.
356 inner: Arc<ConnectionInner>,
357}
358
359impl RawConnection {
360 /// Returns the underlying raw DuckDB connection handle.
361 #[inline]
362 pub(crate) fn handle(&self) -> duckdb_connection {
363 self.inner.handle()
364 }
365
366 /// Returns the shared connection owner, for children that must outlive this handle.
367 #[inline]
368 pub(crate) fn inner(&self) -> &Arc<ConnectionInner> {
369 &self.inner
370 }
371
372 /// Returns the shared database backing this connection.
373 #[inline]
374 pub(crate) fn database(&self) -> &Arc<RawDatabase> {
375 self.inner.database()
376 }
377
378 /// Creates a new `RawConnection` from an existing [`RawDatabase`].
379 ///
380 /// # Errors
381 ///
382 /// Returns an error if the connection cannot be established.
383 #[inline]
384 pub(crate) fn new(db: Arc<RawDatabase>) -> Result<RawConnection> {
385 ConnectionInner::connect(db).map(|inner| RawConnection { inner })
386 }
387
388 /// Opens a new connection to the database at the given path with the specified config.
389 ///
390 /// Pass a path of `":memory:"` for an in-memory database.
391 ///
392 /// # Errors
393 ///
394 /// Returns an error if the database cannot be opened or the connection cannot be
395 /// established.
396 pub fn open_with_flags(
397 c_path: &CStr,
398 config: Config,
399 ) -> Result<RawConnection> {
400 RawConnection::new(Arc::new(RawDatabase::open_with_flags(c_path, config)?))
401 }
402
403 /// Closes the connection, releasing the underlying DuckDB handle.
404 ///
405 /// Consuming the handle makes double-close unrepresentable. The connection is
406 /// disconnected as soon as the last child (prepared statement, appender) is also
407 /// released, so dropping a `RawConnection` is equally safe — `close` exists to
408 /// *report* outstanding children rather than to silence them.
409 ///
410 /// # Errors
411 ///
412 /// Returns [`Error::DuckDBFailure`] if prepared statements or appenders created
413 /// from this connection are still alive. The connection stays open in that case,
414 /// and is disconnected when the last of them is dropped.
415 pub fn close(self) -> Result<()> {
416 match Arc::try_unwrap(self.inner) {
417 // Dropping the sole owner disconnects here.
418 Ok(inner) => {
419 drop(inner);
420 Ok(())
421 },
422 Err(shared) => {
423 let outstanding = Arc::strong_count(&shared).saturating_sub(1);
424 Err(Error::DuckDBFailure(
425 FFIError::new(DuckDBError),
426 Some(format!(
427 "cannot close connection: {outstanding} prepared statement(s) or \
428 appender(s) still borrow it"
429 )),
430 ))
431 },
432 }
433 }
434
435 /// Opens a second, independent connection to the same database.
436 ///
437 /// This performs a fresh `duckdb_connect`, so the new connection has its own
438 /// transaction scope. To share *this* connection's transaction with a child
439 /// resource, pass [`inner`](RawConnection::inner) instead of cloning.
440 ///
441 /// # Errors
442 ///
443 /// Returns `Error::DuckDBFailure` if the connection cannot be established.
444 pub fn try_clone(&self) -> Result<Self> {
445 RawConnection::new(Arc::clone(self.database()))
446 }
447
448 /// Executes a SQL statement and returns the result.
449 ///
450 /// Use this for DDL (`CREATE TABLE`, `DROP`, etc.) and DML (`INSERT`, `UPDATE`,
451 /// `DELETE`). For reading data, use [`prepare`](RawConnection::prepare) and
452 /// [`Statement::execute`](crate::raw::statement::Statement::execute).
453 ///
454 /// # Errors
455 ///
456 /// Returns an error if the SQL cannot be executed or if `sql` contains a nul byte.
457 #[must_use = "query returns a DuckResult; discard explicitly with `let _ = ...` if not needed"]
458 pub fn query(
459 &mut self,
460 sql: impl AsRef<str>,
461 ) -> Result<DuckResult> {
462 let c_str = CString::new(sql.as_ref())?;
463 // SAFETY: `mem::zeroed::<duckdb_result>()` produces an all-zeros value, which is
464 // the correct initial state for a `duckdb_result` output parameter. `duckdb_result`
465 // is a small `Copy` struct (a handful of counters/pointers) with no self-referential
466 // fields, so it needs no stable heap address — DuckResult::new takes it by value.
467 let mut out = unsafe { mem::zeroed::<duckdb_result>() };
468 // SAFETY: `self.con` is a valid open duckdb_connection established in
469 // `open_with_flags` and not yet disconnected. `c_str` is a valid null-terminated
470 // CString that outlives this call. `&mut out` provides a pointer to the stack-local
471 // zeroed `duckdb_result`. Ownership transfers to `DuckResult::new`, whose `Drop`
472 // calls `duckdb_destroy_result` exactly once.
473 let r = unsafe {
474 duckdb_query(
475 self.handle(),
476 c_str.as_ptr() as *const c_char,
477 &mut out as *mut duckdb_result,
478 )
479 };
480 // The query is finished (whether it succeeded or failed): advance the
481 // generation so any `QueryControl` minted for it can no longer interrupt.
482 self.inner.advance_query();
483 result_from_duckdb_result(r, &mut out as *mut duckdb_result)?;
484 Ok(DuckResult::new(out))
485 }
486
487 /// Returns a [`QueryControl`] for the query currently running (or about to run)
488 /// on this connection.
489 ///
490 /// The control can interrupt that query or read its progress from another
491 /// thread. It captures the connection's current query generation, so once the
492 /// query completes the control's [`interrupt`](QueryControl::interrupt) becomes
493 /// a no-op — a delayed cancellation cannot affect a *later* query.
494 #[must_use]
495 pub fn query_control(&self) -> QueryControl {
496 QueryControl { inner: Arc::clone(&self.inner), generation: self.inner.generation() }
497 }
498
499 /// Prepares a SQL statement for execution.
500 ///
501 /// The returned [`Statement`] can be executed one or more times, optionally with
502 /// different bound parameters.
503 ///
504 /// # Errors
505 ///
506 /// Returns an error if the SQL cannot be compiled into a prepared statement or if
507 /// `sql` contains a nul byte.
508 #[must_use = "prepare returns a Statement; call execute() to run it"]
509 #[allow(unused)]
510 pub fn prepare(
511 &self,
512 sql: impl AsRef<str>,
513 ) -> Result<Statement<'_>> {
514 Statement::new(self, sql.as_ref())
515 }
516
517 /// Parses a (possibly multi-statement) SQL string into an
518 /// [`ExtractedStatements`](crate::raw::extracted::ExtractedStatements) batch,
519 /// each statement preparable on demand.
520 ///
521 /// DuckDB does the parsing — there is no Rust-side statement splitting.
522 ///
523 /// # Errors
524 ///
525 /// Returns an error if `sql` contains a nul byte or cannot be parsed.
526 #[must_use = "extract_statements returns a batch; prepare its statements to run them"]
527 pub fn extract_statements(
528 &self,
529 sql: impl AsRef<str>,
530 ) -> Result<crate::raw::extracted::ExtractedStatements> {
531 crate::raw::extracted::ExtractedStatements::extract(self, sql.as_ref())
532 }
533
534 /// Creates a new appender for the specified table and schema.
535 ///
536 /// The appender retains *this* connection, so appended rows participate in any
537 /// transaction open on it.
538 ///
539 /// # Errors
540 ///
541 /// Returns an error if the table does not exist or the appender cannot be created.
542 #[must_use = "appender returns an Appender that must be used to insert rows"]
543 pub fn appender(
544 &mut self,
545 table: &str,
546 schema: &str,
547 ) -> Result<Appender> {
548 Appender::new(Arc::clone(self.inner()), table, schema)
549 }
550
551 /// Creates an appender for `[catalog.]schema.table`, addressing a specific
552 /// attached catalog (`None` uses the default catalog).
553 ///
554 /// # Errors
555 ///
556 /// Returns an error if a name contains an interior NUL, or the appender cannot be
557 /// created (e.g. the table/catalog does not exist).
558 #[must_use = "appender returns an Appender that must be used to insert rows"]
559 pub fn appender_ext(
560 &mut self,
561 catalog: Option<&str>,
562 schema: &str,
563 table: &str,
564 ) -> Result<Appender> {
565 Appender::new_ext(Arc::clone(self.inner()), catalog, schema, table)
566 }
567
568 /// Creates a query appender: appended rows feed `query` (INSERT/UPDATE/DELETE/
569 /// MERGE), which refers to the appended data by `table_name` (default
570 /// `"appended_data"`). `types` are the appended columns' types; `column_names`
571 /// optionally names them.
572 ///
573 /// # Errors
574 ///
575 /// Returns an error on an interior NUL in any string, or if DuckDB rejects the
576 /// query or column set.
577 #[must_use = "appender returns an Appender that must be used to insert rows"]
578 pub fn appender_query(
579 &mut self,
580 query: &str,
581 types: &[crate::types::LogicalType],
582 table_name: Option<&str>,
583 column_names: Option<&[&str]>,
584 ) -> Result<Appender> {
585 Appender::new_query(Arc::clone(self.inner()), query, types, table_name, column_names)
586 }
587
588 /// Executes a parameterized INSERT statement for each value in `values`.
589 ///
590 /// # Errors
591 ///
592 /// Returns an error if the statement fails to execute or if no rows were inserted.
593 #[must_use = "insert result should be checked"]
594 pub fn insert<T: AppendAble, I>(
595 &mut self,
596 sql: &str,
597 values: I,
598 ) -> Result<()>
599 where
600 I: IntoIterator<Item = T>,
601 {
602 let mut stmt = Statement::new(self, sql)?;
603 for mut each in values {
604 stmt.bind(&mut each)?;
605 }
606 let mut res = stmt.execute()?;
607 if res.changes() > 0 {
608 Ok(())
609 } else {
610 Err(Error::DuckDBFailure(
611 FFIError::new(DuckDBError),
612 Some("Failed to insert values".to_owned()),
613 ))
614 }
615 }
616
617 /// Prepares `sql`, binds `binds` in order, and executes the statement.
618 ///
619 /// Works for all statement types. Use [`DuckResult::changes()`] for affected rows
620 /// (DML), or iterate the result for SELECT / RETURNING queries.
621 /// Pass `&mut []` for not parameterized queries.
622 ///
623 /// # Errors
624 ///
625 /// Returns [`Error::DuckDBFailure`] if preparation, binding, or execution fails.
626 #[must_use = "the DuckResult carries both affected-row count (.changes()) and row iterator"]
627 pub fn execute(
628 &mut self,
629 sql: impl AsRef<str>,
630 binds: &mut [&mut dyn AppendAble],
631 ) -> Result<DuckResult> {
632 let mut stmt = CachedStatement::prepare(self, sql)?;
633 for (i, bind) in binds.iter_mut().enumerate() {
634 stmt.bind((i + 1) as u64, *bind)?;
635 }
636 stmt.execute()
637 }
638}
639
640// `RawConnection` deliberately does not implement `Clone`. Cloning used to mean
641// "open a *separate* DuckDB connection", which silently moved the clone into a
642// different transaction scope, and it panicked when `duckdb_connect` failed. Use
643// `try_clone` for an independent connection, or `inner()` to share this one.
644//
645// No `Drop` impl is needed: `ConnectionInner` owns the handle and disconnects when
646// the last reference — this handle or any child — is released. That destructor
647// returns void and therefore cannot panic.
648
649#[cfg(test)]
650mod tests {
651 use super::*;
652
653 #[test]
654 fn test_raw_connection_open() {
655 let path = CString::new(":memory:").unwrap();
656 let config = Config::default();
657 let conn = RawConnection::open_with_flags(&path, config);
658 assert!(conn.is_ok());
659 }
660
661 #[test]
662 fn test_raw_connection_execute() {
663 let path = CString::new(":memory:").unwrap();
664 let config = Config::default();
665 let mut conn = RawConnection::open_with_flags(&path, config).unwrap();
666 let result = conn.query("CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)");
667 assert!(result.is_ok(), "{}", result.err().unwrap());
668 }
669
670 #[test]
671 fn test_raw_connection_prepare() {
672 let path = CString::new(":memory:").unwrap();
673 let config = Config::default();
674 let mut conn = RawConnection::open_with_flags(&path, config).unwrap();
675
676 let result = conn.query("CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)");
677 assert!(result.is_ok(), "{}", result.err().unwrap());
678
679 let stmt = conn.prepare("SELECT * FROM test");
680 assert!(stmt.is_ok(), "{}", stmt.err().unwrap());
681 }
682
683 #[test]
684 fn test_raw_connection_appender() {
685 let path = CString::new(":memory:").unwrap();
686 let config = Config::default();
687 let mut conn = RawConnection::open_with_flags(&path, config).unwrap();
688
689 let result = conn.query("CREATE TABLE test_table (id INTEGER PRIMARY KEY, name TEXT)");
690 assert!(result.is_ok(), "{}", result.err().unwrap());
691
692 let appender = conn.appender("test_table", "main");
693 assert!(appender.is_ok(), "{}", appender.err().unwrap());
694 }
695
696 #[test]
697 fn raw_database_rejects_null_handle() {
698 // SAFETY: null is explicitly supported as an error case and is never dereferenced.
699 let error = unsafe { RawDatabase::new(ptr::null_mut()) }.err().unwrap();
700 assert!(matches!(
701 error,
702 Error::DuckDBFailure(_, Some(message)) if message == "database is null"
703 ));
704 }
705
706 #[test]
707 fn raw_connection_rejects_nul_and_invalid_sql() {
708 let path = CString::new(":memory:").unwrap();
709 let mut conn = RawConnection::open_with_flags(&path, Config::default()).unwrap();
710 assert!(matches!(conn.query("SELECT\0 1"), Err(Error::NulError(_))));
711 assert!(conn.query("SELECT * FROM missing_table").is_err());
712 assert!(conn.prepare("SELECT FROM").is_err());
713 assert!(conn.appender("missing_table", "main").is_err());
714 }
715
716 /// `close` consumes the handle, so a second call cannot be written at all —
717 /// double-close is a compile error rather than a runtime guard.
718 #[test]
719 fn close_consumes_the_connection() {
720 let path = CString::new(":memory:").unwrap();
721 let conn = RawConnection::open_with_flags(&path, Config::default()).unwrap();
722 conn.close().unwrap();
723 }
724
725 #[test]
726 fn close_reports_outstanding_children() {
727 let path = CString::new(":memory:").unwrap();
728 let mut conn = RawConnection::open_with_flags(&path, Config::default()).unwrap();
729 conn.query("CREATE TABLE held (id INTEGER)").unwrap();
730 let appender = conn.appender("held", "main").unwrap();
731
732 let error = conn.close().unwrap_err();
733 assert!(
734 matches!(error, Error::DuckDBFailure(_, Some(ref m)) if m.contains("still borrow it")),
735 "unexpected error: {error:?}"
736 );
737
738 // The connection stayed open; dropping the last child disconnects it.
739 drop(appender);
740 }
741
742 /// Regression: a `CachedStatement` retains its *connection*, so the connection
743 /// handle cannot be disconnected while the statement is still alive. Retaining
744 /// only the database would leave `duckdb_destroy_prepare` running against a
745 /// disconnected connection.
746 #[test]
747 fn cached_statement_keeps_its_connection_alive() {
748 use crate::raw::statement::CachedStatement;
749
750 let path = CString::new(":memory:").unwrap();
751 let mut conn = RawConnection::open_with_flags(&path, Config::default()).unwrap();
752 conn.query("CREATE TABLE kept (id INTEGER); INSERT INTO kept VALUES (5)").unwrap();
753
754 let mut stmt = CachedStatement::prepare(&conn, "SELECT id FROM kept").unwrap();
755 drop(conn);
756
757 let mut rows = stmt.execute().unwrap();
758 let row = rows.next().unwrap().unwrap();
759 assert_eq!(row.get("id").unwrap(), &crate::types::value::DuckValue::Int(5));
760 }
761
762 /// A statement may be moved to another thread; it carries its connection with it.
763 #[test]
764 fn cached_statement_moves_across_threads() {
765 use crate::raw::statement::CachedStatement;
766
767 let path = CString::new(":memory:").unwrap();
768 let mut conn = RawConnection::open_with_flags(&path, Config::default()).unwrap();
769 conn.query("CREATE TABLE moved (id INTEGER); INSERT INTO moved VALUES (9)").unwrap();
770 let mut stmt = CachedStatement::prepare(&conn, "SELECT id FROM moved").unwrap();
771 drop(conn);
772
773 let value = std::thread::spawn(move || {
774 let mut rows = stmt.execute().unwrap();
775 let row = rows.next().unwrap().unwrap();
776 row.get("id").unwrap().clone()
777 })
778 .join()
779 .unwrap();
780
781 assert_eq!(value, crate::types::value::DuckValue::Int(9));
782 }
783
784 /// Regression: the appender runs on the caller's own connection, so its rows
785 /// join the caller's transaction and roll back with it. Previously the appender
786 /// was created on a *separate* cloned connection and the rows survived.
787 #[test]
788 fn appender_rows_join_the_callers_transaction() {
789 let path = CString::new(":memory:").unwrap();
790 let mut conn = RawConnection::open_with_flags(&path, Config::default()).unwrap();
791 conn.query("CREATE TABLE txn_rows (id INTEGER)").unwrap();
792
793 conn.query("BEGIN TRANSACTION").unwrap();
794 {
795 let mut appender = conn.appender("txn_rows", "main").unwrap();
796 appender.append(&mut 42_i32).unwrap();
797 appender.save().unwrap();
798 }
799 conn.query("ROLLBACK").unwrap();
800
801 let mut rows = conn.query("SELECT count(*) AS n FROM txn_rows").unwrap();
802 let row = rows.next().unwrap().unwrap();
803 assert_eq!(
804 row.get("n").unwrap(),
805 &crate::types::value::DuckValue::BigInt(0),
806 "appended rows must roll back with the caller's transaction"
807 );
808 }
809
810 #[test]
811 fn cloned_raw_connection_shares_database() {
812 let path = CString::new(":memory:").unwrap();
813 let mut first = RawConnection::open_with_flags(&path, Config::default()).unwrap();
814 first.query("CREATE TABLE shared (value INTEGER); INSERT INTO shared VALUES (3)").unwrap();
815 let mut second = first.try_clone().unwrap();
816 let mut rows = second.query("SELECT value FROM shared").unwrap();
817 let row = rows.next().unwrap().unwrap();
818 assert_eq!(row.get("value").unwrap(), &crate::types::value::DuckValue::Int(3));
819 }
820
821 #[test]
822 fn insert_reports_when_no_rows_change() {
823 let path = CString::new(":memory:").unwrap();
824 let mut conn = RawConnection::open_with_flags(&path, Config::default()).unwrap();
825 let error = conn.insert::<i32, _>("SELECT $1", std::iter::once(1)).unwrap_err();
826 assert!(matches!(
827 error,
828 Error::DuckDBFailure(_, Some(message)) if message == "Failed to insert values"
829 ));
830 }
831
832 #[test]
833 fn query_control_goes_stale_after_the_query_completes() {
834 let path = CString::new(":memory:").unwrap();
835 let mut conn = RawConnection::open_with_flags(&path, Config::default()).unwrap();
836
837 let control = conn.query_control();
838 assert!(control.is_active(), "freshly minted control is active");
839
840 // Running a query advances the generation, retiring the control.
841 conn.query("SELECT 1").unwrap();
842 assert!(!control.is_active(), "control is stale once its query finished");
843
844 // A stale control neither signals DuckDB nor reports progress.
845 assert!(!control.interrupt(), "stale interrupt is a no-op");
846 assert!(control.progress().is_none(), "stale control reports no progress");
847 }
848
849 #[test]
850 fn interrupt_is_idempotent_and_generation_scoped() {
851 let path = CString::new(":memory:").unwrap();
852 let mut conn = RawConnection::open_with_flags(&path, Config::default()).unwrap();
853
854 // A control for the current (not-yet-run) query can be signalled repeatedly.
855 let control = conn.query_control();
856 assert!(control.interrupt(), "first interrupt signals");
857 assert!(control.interrupt(), "interrupt is idempotent while active");
858
859 // The interrupt flag applies to the connection; a benign query still runs.
860 // (DuckDB clears the flag when the interrupted query is processed.)
861 let _ = conn.query("SELECT 1");
862
863 // A control minted for an earlier generation cannot interrupt a later query.
864 let stale = conn.query_control();
865 conn.query("SELECT 2").unwrap();
866 conn.query("SELECT 3").unwrap();
867 assert!(!stale.interrupt(), "control cannot reach across generations");
868 }
869
870 #[test]
871 fn query_control_is_send_and_sync() {
872 fn assert_send_sync<T: Send + Sync>() {}
873 assert_send_sync::<QueryControl>();
874 }
875
876 /// A `QueryControl` interrupts a genuinely long-running query from another
877 /// thread, and the interrupted query returns an error rather than completing.
878 #[test]
879 fn interrupt_stops_a_long_running_query() {
880 use std::sync::mpsc;
881 use std::thread;
882
883 let path = CString::new(":memory:").unwrap();
884 let mut conn = RawConnection::open_with_flags(&path, Config::default()).unwrap();
885 let control = conn.query_control();
886
887 let (tx, rx) = mpsc::channel();
888 // Interrupt shortly after the query starts, from another thread.
889 let interrupter = thread::spawn(move || {
890 // Wait until the main thread signals the query is about to run.
891 rx.recv().unwrap();
892 thread::sleep(std::time::Duration::from_millis(50));
893 control.interrupt();
894 });
895
896 tx.send(()).unwrap();
897 // A large cross join runs long enough to be interrupted mid-flight.
898 let result = conn.query(
899 "SELECT count(*) FROM range(1000000) t1, range(1000000) t2 WHERE t1.range = t2.range",
900 );
901 interrupter.join().unwrap();
902
903 // Either DuckDB reported the interrupt as an error, or (rarely) the query
904 // finished first. If it errored, it must be a *typed engine* error — the
905 // interrupt path goes through `duckdb_result_error_type`, not a bare
906 // `DuckDBFailure`. DuckDB labels an interrupted query `INVALID` (not
907 // `INTERRUPT`) on this build, so we assert the typed shape, not the exact
908 // kind, which is DuckDB's to decide.
909 if let Err(err) = result {
910 assert!(
911 matches!(&err, Error::Engine(_)),
912 "an interrupted query must surface a typed engine error, got {err:?}"
913 );
914 }
915 }
916}