Skip to main content

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}