Skip to main content

better_duck_core/
connection.rs

1use std::path::Path;
2
3use crate::{
4    config::Config,
5    database::Database,
6    error::Result,
7    helpers::path::path_to_cstring,
8    raw::{appender::Appender, connection::RawConnection, result::DuckResult},
9    types::appendable::AppendAble,
10};
11
12/// A high-level DuckDB connection.
13///
14/// `Connection` wraps a `RawConnection` and exposes a safe, ergonomic API for
15/// opening databases, executing SQL, and creating appenders.
16///
17/// # Example
18///
19/// ```rust,no_run
20/// use better_duck_core::connection::Connection;
21///
22/// let mut conn = Connection::open_in_memory().expect("open in-memory db");
23/// conn.execute_batch("CREATE TABLE t (id INTEGER)").expect("create table");
24/// conn.execute_batch("INSERT INTO t VALUES (1)").expect("insert");
25/// ```
26pub struct Connection(RawConnection);
27
28impl Connection {
29    /// Wraps an existing [`RawConnection`], for use by [`Database::connect`].
30    pub(crate) fn from_raw(raw: RawConnection) -> Connection {
31        Connection(raw)
32    }
33}
34
35// File-db implementation
36impl Connection {
37    /// Opens a connection to a DuckDB database at the given file path.
38    ///
39    /// # Errors
40    ///
41    /// Returns an error if the database cannot be opened or the path contains a nul byte.
42    #[must_use = "connection should be used or explicitly dropped"]
43    #[inline]
44    #[allow(unused)]
45    pub fn open<P: AsRef<Path>>(path: P) -> Result<Connection> {
46        Self::open_with_flags(path, Config::default())
47    }
48
49    /// Opens a connection to a DuckDB database at the given path with additional config.
50    ///
51    /// # Errors
52    ///
53    /// Returns an error if the database cannot be opened or the path contains a nul byte.
54    #[must_use = "connection should be used or explicitly dropped"]
55    #[inline]
56    #[allow(unused)]
57    pub fn open_with_flags<P: AsRef<Path>>(
58        path: P,
59        config: Config,
60    ) -> Result<Connection> {
61        let c_path = path_to_cstring(path.as_ref())?;
62        let config = config.with("duckdb_api", "rust")?;
63        RawConnection::open_with_flags(&c_path, config).map(Connection)
64    }
65}
66
67// In-memory implementation
68impl Connection {
69    /// Opens an in-memory DuckDB connection.
70    ///
71    /// # Errors
72    ///
73    /// Returns an error if the connection cannot be established.
74    #[must_use = "connection should be used or explicitly dropped"]
75    #[inline]
76    #[allow(unused)]
77    pub fn open_in_memory() -> Result<Connection> {
78        Self::open_in_memory_with_flags(Config::default())
79    }
80
81    /// Opens an in-memory DuckDB connection with additional config.
82    ///
83    /// # Errors
84    ///
85    /// Returns an error if the connection cannot be established.
86    #[must_use = "connection should be used or explicitly dropped"]
87    #[inline]
88    #[allow(unused)]
89    pub fn open_in_memory_with_flags(config: Config) -> Result<Connection> {
90        Self::open_with_flags(":memory:", config)
91    }
92}
93
94impl Connection {
95    /// Executes one or more SQL statements separated by semicolons.
96    ///
97    /// The result of each statement is discarded. Use this for DDL
98    /// (`CREATE TABLE`, `DROP TABLE`) and simple DML (`INSERT`, `UPDATE`, `DELETE`).
99    ///
100    /// # Errors
101    ///
102    /// Returns an error if any statement fails to execute.
103    ///
104    /// # Example
105    ///
106    /// ```rust
107    /// # use better_duck_core::connection::Connection;
108    /// # fn main() -> better_duck_core::error::Result<()> {
109    /// let mut conn = Connection::open_in_memory()?;
110    /// conn.execute_batch("CREATE TABLE t (id INTEGER)")?;
111    /// conn.execute_batch("INSERT INTO t VALUES (1)")?;
112    /// # Ok(())
113    /// # }
114    /// ```
115    #[must_use = "execute_batch result should be checked"]
116    #[allow(unused)]
117    pub fn execute_batch(
118        &mut self,
119        sql: impl AsRef<str>,
120    ) -> Result<()> {
121        self.0.query(sql).map(|_| ())
122    }
123
124    /// Prepares and executes a SQL statement, returning the result.
125    ///
126    /// Works for all statement types:
127    /// - **SELECT** — iterate rows via the [`Iterator`] impl on
128    ///   [`DuckResult`].
129    /// - **INSERT / UPDATE / DELETE** — check [`DuckResult::changes()`] for affected rows.
130    /// - **DDL** (`CREATE TABLE`, `DROP TABLE`, etc.) — `.changes()` returns `0`, no rows.
131    /// - **INSERT … RETURNING** — both iterate rows and check `.changes()`.
132    ///
133    /// For parameterized statements use [`execute_with`](Connection::execute_with).
134    ///
135    /// # Errors
136    ///
137    /// Returns an error if DuckDB cannot prepare or execute the statement.
138    ///
139    /// # Examples
140    ///
141    /// ```rust
142    /// # use better_duck_core::connection::Connection;
143    /// # fn main() -> better_duck_core::error::Result<()> {
144    /// let mut conn = Connection::open_in_memory()?;
145    /// conn.execute_batch("CREATE TABLE t (id INTEGER)")?;
146    /// let n = conn.execute("INSERT INTO t VALUES (1)")?.changes();
147    /// assert_eq!(n, 1);
148    /// # Ok(())
149    /// # }
150    /// ```
151    #[must_use = "the DuckResult carries both affected-row count (.changes()) and a row iterator — consume it"]
152    pub fn execute(
153        &mut self,
154        sql: impl AsRef<str>,
155    ) -> Result<DuckResult> {
156        self.0.execute(sql, &mut [])
157    }
158
159    /// Prepares and executes a parameterized SQL statement, returning the result.
160    ///
161    /// # Errors
162    ///
163    /// Returns an error if preparation, binding, or execution fails.
164    #[must_use = "the DuckResult carries both affected-row count (.changes()) and a row iterator — consume it"]
165    pub fn execute_with(
166        &mut self,
167        sql: impl AsRef<str>,
168        binds: &mut [&mut dyn AppendAble],
169    ) -> Result<DuckResult> {
170        self.0.execute(sql, binds)
171    }
172
173    /// Creates an appender for bulk-inserting rows into the given table and schema.
174    ///
175    /// # Errors
176    ///
177    /// Returns an error if the table does not exist or the appender cannot be created.
178    #[must_use = "appender should be used to insert rows"]
179    #[allow(unused)]
180    pub fn appender(
181        &mut self,
182        table: &str,
183        schema: &str,
184    ) -> Result<Appender> {
185        self.0.appender(table, schema)
186    }
187}
188
189impl Connection {
190    /// Closes the connection explicitly.
191    ///
192    /// The connection is also closed automatically on drop.
193    ///
194    /// # Errors
195    ///
196    /// Always returns `Ok(())`.
197    #[must_use = "close result should be checked"]
198    #[inline]
199    #[allow(unused)]
200    pub fn close(&mut self) -> Result<()> {
201        self.0.close()
202    }
203
204    /// Returns `true` if the connection is open.
205    #[inline]
206    #[allow(unused)]
207    pub fn is_open(&self) -> bool {
208        !self.0.con.is_null()
209    }
210
211    /// Returns a reference to the underlying `RawConnection`.
212    ///
213    /// This provides access to low-level operations such as `prepare`.
214    #[inline]
215    #[allow(unused)]
216    #[allow(private_interfaces)]
217    pub fn db(&self) -> &RawConnection {
218        &self.0
219    }
220
221    /// Opens a second, independent connection to the same database as this one.
222    ///
223    /// Cheap: one `duckdb_connect` call, no file I/O. See [`Database::connect`] for
224    /// details on what "the same database" means for `:memory:` connections.
225    ///
226    /// # Errors
227    ///
228    /// Returns an error if the connection cannot be established.
229    #[inline]
230    pub fn try_clone(&self) -> Result<Connection> {
231        self.0.try_clone().map(Connection)
232    }
233
234    /// Returns a shareable handle to the database backing this connection.
235    ///
236    /// Use [`Database::connect`] to open further connections to the same database —
237    /// including, for `:memory:` databases, connections that observe the same data.
238    #[inline]
239    pub fn database(&self) -> Database {
240        Database::from_raw(std::sync::Arc::clone(&self.0.db))
241    }
242
243    /// Returns the raw `duckdb_connection` handle for internal FFI use (e.g. the
244    /// `udf` module's function registration, which needs the handle directly).
245    #[cfg(feature = "udf")]
246    #[inline]
247    pub(crate) fn raw_con(&self) -> crate::ffi::duckdb_connection {
248        self.0.con
249    }
250}
251
252// SAFETY: DuckDB connections are safe to move between threads (they do not hold
253// thread-local state). Each `Connection` owns its `RawConnection` exclusively.
254unsafe impl Send for Connection {}
255
256#[cfg(test)]
257mod connection_tests {
258    use super::*;
259    use crate::config::Config;
260
261    #[test]
262    fn test_open_in_memory() {
263        let mut conn = Connection::open_in_memory().unwrap();
264        assert!(conn.is_open());
265        conn.close().unwrap();
266        assert!(!conn.is_open());
267    }
268
269    #[test]
270    fn test_open_with_flags() {
271        let config = Config::default().with("duckdb_api", "rust").unwrap();
272        let mut conn = Connection::open_with_flags(":memory:", config).unwrap();
273        assert!(conn.is_open());
274        conn.close().unwrap();
275        assert!(!conn.is_open());
276    }
277
278    #[test]
279    fn test_batch_execution() {
280        let mut conn = Connection::open_in_memory().unwrap();
281        let exec = conn.execute_batch("CREATE TABLE test (id INTEGER, name TEXT)");
282        assert!(exec.is_ok(), "{}", exec.unwrap_err());
283        let exec = conn.execute_batch("INSERT INTO test VALUES (1, 'example')");
284        assert!(exec.is_ok(), "{}", exec.unwrap_err());
285        conn.close().unwrap();
286    }
287}