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}