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}