Skip to main content

better_duck_core/
connection.rs

1use std::ffi::{CStr, CString};
2use std::os::raw::c_void;
3use std::path::Path;
4
5use crate::{
6    config::Config,
7    database::Database,
8    error::{Error, Result},
9    ffi,
10    helpers::path::path_to_cstring,
11    raw::{
12        appender::Appender, client_context::ClientContext, connection::RawConnection,
13        profiling::ProfilingNode, result::DuckResult, table_description::TableDescription,
14    },
15    types::appendable::AppendAble,
16};
17
18/// A high-level DuckDB connection.
19///
20/// `Connection` wraps a `RawConnection` and exposes a safe, ergonomic API for
21/// opening databases, executing SQL, and creating appenders.
22///
23/// # Example
24///
25/// ```rust,no_run
26/// use better_duck_core::connection::Connection;
27///
28/// let mut conn = Connection::open_in_memory().expect("open in-memory db");
29/// conn.execute_batch("CREATE TABLE t (id INTEGER)").expect("create table");
30/// conn.execute_batch("INSERT INTO t VALUES (1)").expect("insert");
31/// ```
32pub struct Connection(RawConnection);
33
34impl Connection {
35    /// Wraps an existing [`RawConnection`], for use by [`Database::connect`].
36    pub(crate) fn from_raw(raw: RawConnection) -> Connection {
37        Connection(raw)
38    }
39}
40
41// File-db implementation
42impl Connection {
43    /// Opens a connection to a DuckDB database at the given file path.
44    ///
45    /// # Errors
46    ///
47    /// Returns an error if the database cannot be opened or the path contains a nul byte.
48    #[must_use = "connection should be used or explicitly dropped"]
49    #[inline]
50    #[allow(unused)]
51    pub fn open<P: AsRef<Path>>(path: P) -> Result<Connection> {
52        Self::open_with_flags(path, Config::default())
53    }
54
55    /// Opens a connection to a DuckDB database at the given path with additional config.
56    ///
57    /// # Errors
58    ///
59    /// Returns an error if the database cannot be opened or the path contains a nul byte.
60    #[must_use = "connection should be used or explicitly dropped"]
61    #[inline]
62    #[allow(unused)]
63    pub fn open_with_flags<P: AsRef<Path>>(
64        path: P,
65        config: Config,
66    ) -> Result<Connection> {
67        let c_path = path_to_cstring(path.as_ref())?;
68        let config = config.with("duckdb_api", "rust")?;
69        RawConnection::open_with_flags(&c_path, config).map(Connection)
70    }
71}
72
73// In-memory implementation
74impl Connection {
75    /// Opens an in-memory DuckDB connection.
76    ///
77    /// # Errors
78    ///
79    /// Returns an error if the connection cannot be established.
80    #[must_use = "connection should be used or explicitly dropped"]
81    #[inline]
82    #[allow(unused)]
83    pub fn open_in_memory() -> Result<Connection> {
84        Self::open_in_memory_with_flags(Config::default())
85    }
86
87    /// Opens an in-memory DuckDB connection with additional config.
88    ///
89    /// # Errors
90    ///
91    /// Returns an error if the connection cannot be established.
92    #[must_use = "connection should be used or explicitly dropped"]
93    #[inline]
94    #[allow(unused)]
95    pub fn open_in_memory_with_flags(config: Config) -> Result<Connection> {
96        Self::open_with_flags(":memory:", config)
97    }
98}
99
100impl Connection {
101    /// Executes one or more SQL statements separated by semicolons.
102    ///
103    /// The result of each statement is discarded. Use this for DDL
104    /// (`CREATE TABLE`, `DROP TABLE`) and simple DML (`INSERT`, `UPDATE`, `DELETE`).
105    ///
106    /// # Errors
107    ///
108    /// Returns an error if any statement fails to execute.
109    ///
110    /// # Example
111    ///
112    /// ```rust
113    /// # use better_duck_core::connection::Connection;
114    /// # fn main() -> better_duck_core::error::Result<()> {
115    /// let mut conn = Connection::open_in_memory()?;
116    /// conn.execute_batch("CREATE TABLE t (id INTEGER)")?;
117    /// conn.execute_batch("INSERT INTO t VALUES (1)")?;
118    /// # Ok(())
119    /// # }
120    /// ```
121    #[must_use = "execute_batch result should be checked"]
122    #[allow(unused)]
123    pub fn execute_batch(
124        &mut self,
125        sql: impl AsRef<str>,
126    ) -> Result<()> {
127        self.0.query(sql).map(|_| ())
128    }
129
130    /// Parses a (possibly multi-statement) SQL string into a batch of individually
131    /// preparable statements.
132    ///
133    /// DuckDB performs the parsing — there is no Rust-side statement splitting, so
134    /// semicolons inside string literals, comments, or dollar-quoted bodies do not
135    /// cause a false split. Each statement in the returned
136    /// [`ExtractedStatements`](crate::raw::extracted::ExtractedStatements) batch is
137    /// prepared on demand via
138    /// [`prepare`](crate::raw::extracted::ExtractedStatements::prepare).
139    ///
140    /// # Errors
141    ///
142    /// Returns an error if `sql` contains an interior nul byte, or if DuckDB cannot
143    /// parse the batch.
144    ///
145    /// # Examples
146    ///
147    /// ```rust
148    /// # use better_duck_core::connection::Connection;
149    /// # fn main() -> better_duck_core::error::Result<()> {
150    /// let conn = Connection::open_in_memory()?;
151    /// let batch = conn.extract_statements("SELECT 1 AS a; SELECT 2 AS b")?;
152    /// assert_eq!(batch.len(), 2);
153    /// let mut second = batch.prepare(1)?;
154    /// let mut rows = second.execute()?;
155    /// assert!(rows.next().is_some());
156    /// # Ok(())
157    /// # }
158    /// ```
159    #[must_use = "extract_statements returns a batch; prepare its statements to run them"]
160    pub fn extract_statements(
161        &self,
162        sql: impl AsRef<str>,
163    ) -> Result<crate::raw::extracted::ExtractedStatements> {
164        self.0.extract_statements(sql)
165    }
166
167    /// Prepares and executes a SQL statement, returning the result.
168    ///
169    /// Works for all statement types:
170    /// - **SELECT** — iterate rows via the [`Iterator`] impl on
171    ///   [`DuckResult`].
172    /// - **INSERT / UPDATE / DELETE** — check [`DuckResult::changes()`] for affected rows.
173    /// - **DDL** (`CREATE TABLE`, `DROP TABLE`, etc.) — `.changes()` returns `0`, no rows.
174    /// - **INSERT … RETURNING** — both iterate rows and check `.changes()`.
175    ///
176    /// For parameterized statements use [`execute_with`](Connection::execute_with).
177    ///
178    /// # Errors
179    ///
180    /// Returns an error if DuckDB cannot prepare or execute the statement.
181    ///
182    /// # Examples
183    ///
184    /// ```rust
185    /// # use better_duck_core::connection::Connection;
186    /// # fn main() -> better_duck_core::error::Result<()> {
187    /// let mut conn = Connection::open_in_memory()?;
188    /// conn.execute_batch("CREATE TABLE t (id INTEGER)")?;
189    /// let n = conn.execute("INSERT INTO t VALUES (1)")?.changes();
190    /// assert_eq!(n, 1);
191    /// # Ok(())
192    /// # }
193    /// ```
194    #[must_use = "the DuckResult carries both affected-row count (.changes()) and a row iterator — consume it"]
195    pub fn execute(
196        &mut self,
197        sql: impl AsRef<str>,
198    ) -> Result<DuckResult> {
199        self.0.execute(sql, &mut [])
200    }
201
202    /// Prepares `sql`, binds each value in `values` as consecutive positional
203    /// parameters (`$1`, `$2`, …), and executes the statement once.
204    ///
205    /// All values share a single type `T`, so this suits a parameterized DML
206    /// statement filled from a homogeneous iterator. For heterogeneous binds use
207    /// [`execute_with`](Connection::execute_with).
208    ///
209    /// # Errors
210    ///
211    /// Returns an error if preparation, binding, or execution fails, or if the
212    /// statement reports that no rows changed.
213    ///
214    /// # Examples
215    ///
216    /// ```rust
217    /// # use better_duck_core::connection::Connection;
218    /// # fn main() -> better_duck_core::error::Result<()> {
219    /// let mut conn = Connection::open_in_memory()?;
220    /// conn.execute("CREATE TABLE t (a INTEGER, b INTEGER)")?;
221    /// conn.insert::<i32, _>("INSERT INTO t VALUES ($1, $2)", [10, 20])?;
222    /// # Ok(())
223    /// # }
224    /// ```
225    #[must_use = "insert result should be checked"]
226    pub fn insert<T: AppendAble, I>(
227        &mut self,
228        sql: &str,
229        values: I,
230    ) -> Result<()>
231    where
232        I: IntoIterator<Item = T>,
233    {
234        self.0.insert(sql, values)
235    }
236
237    /// Prepares and executes a parameterized SQL statement, returning the result.
238    ///
239    /// # Errors
240    ///
241    /// Returns an error if preparation, binding, or execution fails.
242    #[must_use = "the DuckResult carries both affected-row count (.changes()) and a row iterator — consume it"]
243    pub fn execute_with(
244        &mut self,
245        sql: impl AsRef<str>,
246        binds: &mut [&mut dyn AppendAble],
247    ) -> Result<DuckResult> {
248        self.0.execute(sql, binds)
249    }
250
251    /// Returns the table names `query` reads from, as determined by DuckDB's own
252    /// parser — no custom SQL parsing. Handles quoted/qualified identifiers, CTEs,
253    /// joins, and subqueries.
254    ///
255    /// With `qualified = true` each name is fully qualified (`catalog.schema.table`);
256    /// with `false` only the bare (unescaped) table name is returned. The order and
257    /// de-duplication follow DuckDB. A query that reads no tables yields an empty
258    /// vector.
259    ///
260    /// # Errors
261    ///
262    /// Returns an error if `query` contains an interior NUL. It also returns an error
263    /// if DuckDB reports a parse failure by returning a null value.
264    ///
265    /// # Panics / aborts
266    ///
267    /// `query` **must be syntactically valid SQL.** On a syntax error the underlying
268    /// `duckdb_get_table_names` throws a C++ `ParserException` that unwinds across the
269    /// FFI boundary; Rust cannot catch a foreign exception, so the **process aborts**
270    /// rather than returning an error. This is an upstream DuckDB C-API defect (the
271    /// same one that affects statement extraction), not something this wrapper can
272    /// intercept. Validate untrusted SQL elsewhere before calling this.
273    pub fn table_names(
274        &self,
275        query: impl AsRef<str>,
276        qualified: bool,
277    ) -> Result<Vec<String>> {
278        let c_query = CString::new(query.as_ref())?;
279        // SAFETY: `self.0.handle()` is a valid open connection; `c_query` is a valid
280        // null-terminated string that outlives the call and is not retained. On a
281        // parse failure DuckDB returns a null value.
282        let mut value =
283            unsafe { ffi::duckdb_get_table_names(self.0.handle(), c_query.as_ptr(), qualified) };
284        if value.is_null() {
285            return Err(Error::ConversionError(
286                crate::error::DuckDBConversionError::ConversionError(
287                    "could not determine table names (query failed to parse)".to_owned(),
288                ),
289            ));
290        }
291        // SAFETY: `value` is a valid VARCHAR[] duckdb_value returned above.
292        let names = unsafe { varchar_list_to_vec(value) };
293        // SAFETY: `value` was returned by `duckdb_get_table_names`; destroy exactly once.
294        unsafe { ffi::duckdb_destroy_value(&mut value) };
295        Ok(names)
296    }
297
298    /// Describes `schema.table` (default catalog), giving indexed access to column
299    /// names and `DEFAULT` flags via [`TableDescription`].
300    ///
301    /// The API has no column-count accessor; obtain the number of columns from a
302    /// trusted bounds source (an appender's column count, or a `SELECT … LIMIT 0`
303    /// schema) and read `0..count`.
304    ///
305    /// # Errors
306    ///
307    /// Returns an error if the names contain an interior NUL, or if DuckDB cannot
308    /// describe the table (the error carries DuckDB's catalog-aware message).
309    pub fn table_description(
310        &self,
311        schema: &str,
312        table: &str,
313    ) -> Result<TableDescription> {
314        TableDescription::create(self.0.handle(), schema, table)
315    }
316
317    /// Like [`table_description`](Connection::table_description) but with an explicit
318    /// catalog (`None` uses DuckDB's default).
319    ///
320    /// # Errors
321    ///
322    /// As [`table_description`](Connection::table_description), plus an interior NUL
323    /// in `catalog`.
324    pub fn table_description_ext(
325        &self,
326        catalog: Option<&str>,
327        schema: &str,
328        table: &str,
329    ) -> Result<TableDescription> {
330        TableDescription::create_ext(self.0.handle(), catalog, schema, table)
331    }
332
333    /// Registers `ty` as a custom (aliased) logical type in this connection's
334    /// catalog, so its alias can be used as a type name in SQL. Give `ty` a name
335    /// first with [`LogicalType::set_alias`](crate::types::LogicalType::set_alias).
336    ///
337    /// # Errors
338    ///
339    /// Returns an error if `ty` has no alias, or DuckDB rejects the registration
340    /// (e.g. a name conflict).
341    pub fn register_logical_type(
342        &self,
343        ty: &crate::types::LogicalType,
344    ) -> Result<()> {
345        if ty.alias().is_none() {
346            return Err(Error::ConversionError(
347                crate::error::DuckDBConversionError::ConversionError(
348                    "a logical type must have an alias before it can be registered".to_owned(),
349                ),
350            ));
351        }
352        // SAFETY: `self.0.handle()` is a valid open connection; `ty.as_raw()` is a valid
353        // logical type owned by `ty` for the call (DuckDB copies it). The `info` arg is
354        // optional and DuckDB accepts a null handle (no extra create-type metadata).
355        let state = unsafe {
356            ffi::duckdb_register_logical_type(self.0.handle(), ty.as_raw(), std::ptr::null_mut())
357        };
358        crate::helpers::duck_result::check_state(state)
359    }
360
361    /// Whether the current query on this connection has finished executing.
362    ///
363    /// Meaningful while driving execution manually with the external task scheduler
364    /// ([`TaskState`](crate::raw::task_state::TaskState)): a background query is done
365    /// once this returns `true`. (`duckdb_execution_is_finished`.)
366    #[must_use]
367    pub fn execution_is_finished(&self) -> bool {
368        // SAFETY: `self.0.handle()` is a valid open connection.
369        unsafe { ffi::duckdb_execution_is_finished(self.0.handle()) }
370    }
371
372    /// Materialises the connection's query-profiling tree into an owned
373    /// [`ProfilingNode`], or `None` if profiling is disabled or no query has run.
374    ///
375    /// The tree is copied out eagerly, so it stays valid after later queries and
376    /// after the connection is dropped. Enable profiling with
377    /// `PRAGMA enable_profiling = 'no_output'` (and optionally `PRAGMA profiling_mode`).
378    #[must_use]
379    pub fn profiling_info(&self) -> Option<ProfilingNode> {
380        // SAFETY: `self.0.handle()` is a valid open connection.
381        unsafe { crate::raw::profiling::profiling_info(self.0.handle()) }
382    }
383
384    /// Fetches a single metric from the root profiling node, or `None` if profiling
385    /// is disabled or no query has run.
386    ///
387    /// # Panics / aborts
388    ///
389    /// `key` **must be a metric that exists** (e.g. one returned by
390    /// [`profiling_info`](Connection::profiling_info)'s
391    /// [`metrics`](ProfilingNode::metrics)). Despite its C-API docs, DuckDB's
392    /// `duckdb_profiling_info_get_value` throws a C++ exception for an unknown key
393    /// that unwinds across the FFI boundary and **aborts the process** — an upstream
394    /// defect this wrapper cannot intercept. For a safe all-metrics snapshot use
395    /// [`profiling_info`](Connection::profiling_info) instead.
396    #[must_use]
397    pub fn profiling_metric(
398        &self,
399        key: &str,
400    ) -> Option<String> {
401        // SAFETY: `self.0.handle()` is a valid open connection.
402        unsafe { crate::raw::profiling::profiling_metric(self.0.handle(), key) }
403    }
404
405    /// Returns this connection's [`ClientContext`], exposing its stable connection
406    /// id. The returned context borrows `self`, so it cannot outlive the connection.
407    ///
408    /// Returns `None` if DuckDB does not provide a context for the connection.
409    #[must_use]
410    pub fn client_context(&self) -> Option<ClientContext<'_>> {
411        let mut ctx: crate::ffi::duckdb_client_context = std::ptr::null_mut();
412        // SAFETY: `self.0.handle()` is a valid open connection; `ctx` is a valid
413        // out-pointer DuckDB writes an owned client-context handle into.
414        unsafe { crate::ffi::duckdb_connection_get_client_context(self.0.handle(), &mut ctx) };
415        // SAFETY: `ctx` is either null or an owned client-context handle whose owner
416        // (this connection) outlives the returned borrow.
417        unsafe { ClientContext::from_raw(ctx) }
418    }
419
420    /// Creates an appender for bulk-inserting rows into the given table and schema.
421    ///
422    /// # Errors
423    ///
424    /// Returns an error if the table does not exist or the appender cannot be created.
425    #[must_use = "appender should be used to insert rows"]
426    #[allow(unused)]
427    pub fn appender(
428        &mut self,
429        table: &str,
430        schema: &str,
431    ) -> Result<Appender> {
432        self.0.appender(table, schema)
433    }
434
435    /// Creates an appender for `[catalog.]schema.table` in a specific attached
436    /// catalog (`None` uses the default catalog).
437    ///
438    /// # Errors
439    ///
440    /// Returns an error if a name contains an interior NUL, or the appender cannot be
441    /// created (e.g. the table/catalog does not exist).
442    #[must_use = "appender should be used to insert rows"]
443    pub fn appender_ext(
444        &mut self,
445        catalog: Option<&str>,
446        schema: &str,
447        table: &str,
448    ) -> Result<Appender> {
449        self.0.appender_ext(catalog, schema, table)
450    }
451
452    /// Creates a query appender whose appended rows feed `query` (INSERT/UPDATE/
453    /// DELETE/MERGE), referring to the appended data by `table_name` (default
454    /// `"appended_data"`). `types` are the appended columns' types; `column_names`
455    /// optionally names them.
456    ///
457    /// # Errors
458    ///
459    /// Returns an error on an interior NUL in any string, or if DuckDB rejects the
460    /// query or column set.
461    #[must_use = "appender should be used to insert rows"]
462    pub fn appender_query(
463        &mut self,
464        query: &str,
465        types: &[crate::types::LogicalType],
466        table_name: Option<&str>,
467        column_names: Option<&[&str]>,
468    ) -> Result<Appender> {
469        self.0.appender_query(query, types, table_name, column_names)
470    }
471}
472
473impl Connection {
474    /// Closes the connection explicitly.
475    ///
476    /// This consumes the connection so it cannot be used after closing. The
477    /// connection is also closed automatically on drop.
478    ///
479    /// # Errors
480    ///
481    /// Returns an error if appenders created from this connection are still alive.
482    /// The connection stays open in that case and closes once the last of them is
483    /// dropped.
484    #[must_use = "close result should be checked"]
485    #[inline]
486    pub fn close(self) -> Result<()> {
487        self.0.close()
488    }
489
490    /// Returns `true` if the connection is open.
491    ///
492    /// Always `true`: [`close`](Connection::close) consumes the connection, so a
493    /// `Connection` value can only ever refer to an open connection. Retained so
494    /// existing callers keep compiling.
495    #[inline]
496    #[allow(unused)]
497    pub fn is_open(&self) -> bool {
498        true
499    }
500
501    /// Returns a reference to the underlying `RawConnection`.
502    ///
503    /// This provides access to low-level operations such as `prepare`.
504    #[inline]
505    #[allow(unused)]
506    #[allow(private_interfaces)]
507    pub fn db(&self) -> &RawConnection {
508        &self.0
509    }
510
511    /// Opens a second, independent connection to the same database as this one.
512    ///
513    /// Cheap: one `duckdb_connect` call, no file I/O. See [`Database::connect`] for
514    /// details on what "the same database" means for `:memory:` connections.
515    ///
516    /// # Errors
517    ///
518    /// Returns an error if the connection cannot be established.
519    #[inline]
520    pub fn try_clone(&self) -> Result<Connection> {
521        self.0.try_clone().map(Connection)
522    }
523
524    /// Returns a [`QueryControl`](crate::QueryControl) for interrupting or observing the
525    /// query running on this connection from another thread.
526    ///
527    /// Mint the control *before* starting the query (typically on another thread),
528    /// then call [`QueryControl::interrupt`](crate::QueryControl::interrupt) or
529    /// [`QueryControl::progress`](crate::QueryControl::progress) while it
530    /// runs. The control is generation-scoped: once the query finishes, it can no
531    /// longer affect a later query on the same connection.
532    #[inline]
533    #[must_use]
534    pub fn query_control(&self) -> crate::raw::connection::QueryControl {
535        self.0.query_control()
536    }
537
538    /// Returns the shared connection owner, for building a mutex-free
539    /// [`QueryControl`] source.
540    ///
541    /// Only the async layer needs this, so it is gated on the `async` feature to
542    /// avoid a dead-code warning in the default build.
543    #[cfg(feature = "async")]
544    #[inline]
545    pub(crate) fn inner(&self) -> &std::sync::Arc<crate::raw::connection::ConnectionInner> {
546        self.0.inner()
547    }
548
549    /// Returns a shareable handle to the database backing this connection.
550    ///
551    /// Use [`Database::connect`] to open further connections to the same database —
552    /// including, for `:memory:` databases, connections that observe the same data.
553    #[inline]
554    pub fn database(&self) -> Database {
555        Database::from_raw(std::sync::Arc::clone(self.0.database()))
556    }
557
558    /// Returns the raw `duckdb_connection` handle for internal FFI use (e.g. the
559    /// `udf` module's function registration, which needs the handle directly).
560    #[cfg(feature = "udf")]
561    #[inline]
562    pub(crate) fn raw_con(&self) -> crate::ffi::duckdb_connection {
563        self.0.handle()
564    }
565}
566
567/// Copies a `LIST(VARCHAR)` `duckdb_value` (e.g. the result of
568/// `duckdb_get_table_names`) into an owned `Vec<String>`.
569///
570/// # Safety
571///
572/// `value` must be a valid `duckdb_value` of type `VARCHAR[]`. The value itself is
573/// only read (the caller still owns and must destroy it); each child value and each
574/// `duckdb_get_varchar` string allocated here is freed before returning.
575unsafe fn varchar_list_to_vec(value: ffi::duckdb_value) -> Vec<String> {
576    // SAFETY: `value` is a valid LIST value per the contract.
577    let n = unsafe { ffi::duckdb_get_list_size(value) };
578    let mut out = Vec::with_capacity(n as usize);
579    for i in 0..n {
580        // SAFETY: `i` is within [0, n); `duckdb_get_list_child` returns a newly
581        // allocated child `duckdb_value` that we destroy below.
582        let mut child = unsafe { ffi::duckdb_get_list_child(value, i) };
583        // SAFETY: `child` is a valid VARCHAR value; `duckdb_get_varchar` returns a
584        // heap `char*` (or null) that must be freed with `duckdb_free`.
585        let c = unsafe { ffi::duckdb_get_varchar(child) };
586        if !c.is_null() {
587            // SAFETY: `c` is a valid, non-null, null-terminated C string.
588            out.push(unsafe { CStr::from_ptr(c) }.to_string_lossy().into_owned());
589            // SAFETY: `c` was allocated by DuckDB and ownership transferred to us.
590            unsafe { ffi::duckdb_free(c as *mut c_void) };
591        }
592        // SAFETY: `child` was allocated by `duckdb_get_list_child`; destroy exactly once.
593        unsafe { ffi::duckdb_destroy_value(&mut child) };
594    }
595    out
596}
597
598// SAFETY: DuckDB connections are safe to move between threads (they do not hold
599// thread-local state). Each `Connection` owns its `RawConnection` exclusively.
600unsafe impl Send for Connection {}
601
602#[cfg(test)]
603mod connection_tests {
604    use super::*;
605    use crate::config::Config;
606
607    #[test]
608    fn test_open_in_memory() {
609        let conn = Connection::open_in_memory().unwrap();
610        assert!(conn.is_open());
611        conn.close().unwrap();
612    }
613
614    #[test]
615    fn register_logical_type_makes_an_alias_usable_as_a_sql_type() {
616        let mut conn = Connection::open_in_memory().unwrap();
617        // An aliased INTEGER type registers and can then be used as a SQL type name.
618        let mut ty = crate::types::LogicalType::of::<i32>().unwrap();
619        ty.set_alias("my_int").unwrap();
620        conn.register_logical_type(&ty).unwrap();
621        // The registered alias is now a usable column/cast type.
622        conn.execute_batch("CREATE TABLE t (v my_int)").unwrap();
623        conn.execute_batch("INSERT INTO t VALUES (7)").unwrap();
624        let mut rows = conn.execute("SELECT v FROM t").unwrap();
625        assert_eq!(
626            rows.next().unwrap().unwrap().get("v"),
627            Some(&crate::types::value::DuckValue::Int(7))
628        );
629    }
630
631    #[test]
632    fn register_logical_type_requires_an_alias() {
633        let conn = Connection::open_in_memory().unwrap();
634        let ty = crate::types::LogicalType::of::<i32>().unwrap();
635        // No alias set → rejected before touching DuckDB.
636        assert!(conn.register_logical_type(&ty).is_err());
637    }
638
639    #[test]
640    fn table_names_simple_join_and_no_tables() {
641        let conn = Connection::open_in_memory().unwrap();
642        // Simple single-table read.
643        assert_eq!(conn.table_names("SELECT * FROM foo", false).unwrap(), vec!["foo".to_owned()]);
644        // Join across two tables (order/dedup follow DuckDB — compare as a set).
645        let mut joined = conn.table_names("SELECT * FROM a JOIN b ON a.id = b.id", false).unwrap();
646        joined.sort();
647        assert_eq!(joined, vec!["a".to_owned(), "b".to_owned()]);
648        // A query that reads no tables.
649        assert!(conn.table_names("SELECT 1", false).unwrap().is_empty());
650    }
651
652    #[test]
653    fn table_names_quoted_cte_and_subquery() {
654        let conn = Connection::open_in_memory().unwrap();
655        // Quoted identifier with a space is returned unescaped.
656        assert_eq!(
657            conn.table_names("SELECT * FROM \"My Table\"", false).unwrap(),
658            vec!["My Table".to_owned()]
659        );
660        // A CTE name is not a real table; only the underlying base table is reported.
661        let cte =
662            conn.table_names("WITH c AS (SELECT * FROM base) SELECT * FROM c", false).unwrap();
663        assert_eq!(cte, vec!["base".to_owned()]);
664        // Subquery: the inner table is reported.
665        assert_eq!(
666            conn.table_names("SELECT * FROM (SELECT * FROM inner_t) x", false).unwrap(),
667            vec!["inner_t".to_owned()]
668        );
669    }
670
671    #[test]
672    fn table_names_qualified_flag_is_threaded_through() {
673        let mut conn = Connection::open_in_memory().unwrap();
674        conn.execute_batch("CREATE TABLE t (id INTEGER)").unwrap();
675        // Both flag values resolve the referenced table; DuckDB decides how much
676        // qualification to add (a bare reference may stay bare), so we assert the
677        // table is present rather than a specific catalog.schema.table shape.
678        let qualified = conn.table_names("SELECT * FROM t", true).unwrap();
679        assert_eq!(qualified.len(), 1);
680        assert!(qualified[0].ends_with("t"), "expected ...t, got {:?}", qualified[0]);
681        // Unqualified form is the bare name.
682        assert_eq!(conn.table_names("SELECT * FROM t", false).unwrap(), vec!["t".to_owned()]);
683    }
684
685    #[test]
686    fn table_names_rejects_interior_nul_before_ffi() {
687        // An interior NUL is caught while building the CString, before the FFI call,
688        // so it never reaches DuckDB's parser.
689        let conn = Connection::open_in_memory().unwrap();
690        assert!(
691            matches!(conn.table_names("SELECT * FROM t\0x", false), Err(Error::NulError(_))),
692            "interior NUL must be rejected"
693        );
694    }
695
696    // NOTE: malformed SQL (e.g. "SELECT FROM WHERE") is intentionally NOT exercised
697    // here. `duckdb_get_table_names` parses the query internally and, on a syntax
698    // error, throws a C++ ParserException that unwinds across the FFI boundary; Rust
699    // cannot catch a foreign exception, so the process aborts
700    // ("fatal runtime error: Rust cannot catch foreign exceptions"). This is the same
701    // upstream DuckDB C-API defect documented for extract_statements.
702    // `table_names` therefore documents that the caller must pass
703    // syntactically valid SQL; only the interior-NUL guard (which runs before the FFI
704    // call) is testable as a rejection.
705
706    #[test]
707    fn test_open_with_flags() {
708        let config = Config::default().with("duckdb_api", "rust").unwrap();
709        let conn = Connection::open_with_flags(":memory:", config).unwrap();
710        assert!(conn.is_open());
711        conn.close().unwrap();
712    }
713
714    #[test]
715    fn test_batch_execution() {
716        let mut conn = Connection::open_in_memory().unwrap();
717        let exec = conn.execute_batch("CREATE TABLE test (id INTEGER, name TEXT)");
718        assert!(exec.is_ok(), "{}", exec.unwrap_err());
719        let exec = conn.execute_batch("INSERT INTO test VALUES (1, 'example')");
720        assert!(exec.is_ok(), "{}", exec.unwrap_err());
721        conn.close().unwrap();
722    }
723
724    #[test]
725    fn close_consumes_the_connection() {
726        let conn = Connection::open_in_memory().unwrap();
727        conn.close().unwrap();
728    }
729
730    #[test]
731    fn execute_with_binds_values_and_reports_changes() {
732        let mut conn = Connection::open_in_memory().unwrap();
733        conn.execute_batch("CREATE TABLE test (id INTEGER, name VARCHAR)").unwrap();
734        let mut id = 7_i32;
735        let mut name = String::from("bound");
736        let mut result = conn
737            .execute_with("INSERT INTO test VALUES ($1, $2)", &mut [&mut id, &mut name])
738            .unwrap();
739        assert_eq!(result.changes(), 1);
740
741        let mut rows = conn.execute("SELECT id, name FROM test").unwrap();
742        let row = rows.next().unwrap().unwrap();
743        assert_eq!(row.get("id").unwrap(), &crate::types::value::DuckValue::Int(7));
744        assert_eq!(
745            row.get("name").unwrap(),
746            &crate::types::value::DuckValue::Text("bound".to_owned())
747        );
748    }
749
750    #[test]
751    fn nul_sql_and_missing_appender_table_return_errors() {
752        let mut conn = Connection::open_in_memory().unwrap();
753        assert!(matches!(
754            conn.execute_batch("SELECT 1;\0SELECT 2"),
755            Err(crate::error::Error::NulError(_))
756        ));
757        assert!(matches!(conn.execute("SELECT '\0'"), Err(crate::error::Error::NulError(_))));
758        assert!(conn.appender("missing_table", "main").is_err());
759    }
760}