Skip to main content

better_duck_core/
database.rs

1use std::{path::Path, sync::Arc};
2
3use crate::{
4    config::Config,
5    connection::Connection,
6    error::Result,
7    helpers::path::path_to_cstring,
8    raw::connection::{RawConnection, RawDatabase},
9};
10
11/// A shared handle to an open DuckDB database.
12///
13/// Cloning a `Database` is cheap (an `Arc` bump) and does not touch DuckDB. Use
14/// [`connect`](Database::connect) to spawn independent [`Connection`]s that share
15/// this database — including, for `:memory:` databases, connections that observe
16/// the same data.
17///
18/// This is different from calling [`Connection::open_in_memory`] multiple times,
19/// which gives each connection its own independent in-memory database:
20///
21/// ```rust
22/// use better_duck_core::{connection::Connection, database::Database};
23///
24/// // Shared: both connections see the same in-memory data.
25/// let db = Database::open_in_memory().expect("open database");
26/// let mut a = db.connect().expect("connect");
27/// let mut b = db.connect().expect("connect");
28/// a.execute_batch("CREATE TABLE t (id INTEGER)").expect("create table");
29/// b.execute_batch("INSERT INTO t VALUES (1)").expect("insert");
30///
31/// // Independent: each has its own in-memory database.
32/// let mut x = Connection::open_in_memory().expect("open");
33/// let mut y = Connection::open_in_memory().expect("open");
34/// ```
35#[derive(Clone)]
36pub struct Database {
37    inner: Arc<RawDatabase>,
38}
39
40impl Database {
41    /// Wraps an existing `Arc<RawDatabase>`, for use by [`Connection::database`].
42    pub(crate) fn from_raw(inner: Arc<RawDatabase>) -> Database {
43        Database { inner }
44    }
45
46    /// Opens a database at the given file path.
47    ///
48    /// # Errors
49    ///
50    /// Returns an error if the database cannot be opened or the path contains a nul byte.
51    pub fn open<P: AsRef<Path>>(path: P) -> Result<Database> {
52        Self::open_with_flags(path, Config::default())
53    }
54
55    /// Opens a database at the given file 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    pub fn open_with_flags<P: AsRef<Path>>(
61        path: P,
62        config: Config,
63    ) -> Result<Database> {
64        let c_path = path_to_cstring(path.as_ref())?;
65        let config = config.with("duckdb_api", "rust")?;
66        RawDatabase::open_with_flags(&c_path, config).map(|db| Database { inner: Arc::new(db) })
67    }
68
69    /// Opens an in-memory database.
70    ///
71    /// # Errors
72    ///
73    /// Returns an error if the database cannot be opened.
74    pub fn open_in_memory() -> Result<Database> {
75        Self::open_in_memory_with_flags(Config::default())
76    }
77
78    /// Opens an in-memory database with additional config.
79    ///
80    /// # Errors
81    ///
82    /// Returns an error if the database cannot be opened.
83    pub fn open_in_memory_with_flags(config: Config) -> Result<Database> {
84        Self::open_with_flags(":memory:", config)
85    }
86
87    /// Opens a new connection to this database.
88    ///
89    /// This is a single `duckdb_connect` call — no file I/O. All connections opened
90    /// from this `Database` (or its clones) share the same underlying data.
91    ///
92    /// # Errors
93    ///
94    /// Returns an error if the connection cannot be established.
95    pub fn connect(&self) -> Result<Connection> {
96        RawConnection::new(Arc::clone(&self.inner)).map(Connection::from_raw)
97    }
98
99    /// Returns the raw `duckdb_database` handle for internal FFI use (e.g. the
100    /// `udf` module's replacement-scan registration, which needs the handle
101    /// directly — DuckDB scopes replacement scans per-database, not
102    /// per-connection).
103    #[cfg(feature = "udf")]
104    pub(crate) fn raw_db(&self) -> crate::ffi::duckdb_database {
105        self.inner.0
106    }
107
108    /// The raw `duckdb_database` handle, for internal FFI use by the external task
109    /// scheduler ([`crate::raw::task_state`]).
110    pub(crate) fn handle(&self) -> crate::ffi::duckdb_database {
111        self.inner.0
112    }
113
114    /// A cloned `Arc` to the underlying database, so a resource (e.g. a task state)
115    /// can keep the database alive for its own lifetime.
116    pub(crate) fn arc(&self) -> Arc<RawDatabase> {
117        Arc::clone(&self.inner)
118    }
119}
120
121impl std::fmt::Debug for Database {
122    fn fmt(
123        &self,
124        f: &mut std::fmt::Formatter<'_>,
125    ) -> std::fmt::Result {
126        f.debug_struct("Database").finish_non_exhaustive()
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133
134    #[test]
135    fn database_connect_shares_in_memory_state() {
136        let db = Database::open_in_memory().unwrap();
137        let mut a = db.connect().unwrap();
138        let mut b = db.connect().unwrap();
139        a.execute_batch("CREATE TABLE t (id INTEGER)").unwrap();
140        b.execute_batch("INSERT INTO t VALUES (1)").unwrap();
141        let mut result = a.execute("SELECT count(*) AS c FROM t").unwrap();
142        let row = result.next().unwrap().unwrap();
143        match row.get("c").unwrap() {
144            crate::types::value::DuckValue::BigInt(n) => assert_eq!(*n, 1),
145            other => panic!("expected BigInt, got {other:?}"),
146        }
147    }
148
149    #[test]
150    fn separate_open_in_memory_are_independent() {
151        let mut a = Connection::open_in_memory().unwrap();
152        let mut b = Connection::open_in_memory().unwrap();
153        a.execute_batch("CREATE TABLE t (id INTEGER)").unwrap();
154        // `b` never had `t` created, so this must fail — proving isolation.
155        assert!(b.execute_batch("INSERT INTO t VALUES (1)").is_err());
156    }
157
158    #[test]
159    fn database_outlives_connection() {
160        let db = Database::open_in_memory().unwrap();
161        let mut conn = db.connect().unwrap();
162        drop(db);
163        // The Arc<RawDatabase> is kept alive by `conn`'s RawConnection.
164        conn.execute_batch("CREATE TABLE t (id INTEGER)").unwrap();
165    }
166
167    #[test]
168    fn connection_try_clone_shares_state() {
169        let mut a = Connection::open_in_memory().unwrap();
170        a.execute_batch("CREATE TABLE t (id INTEGER)").unwrap();
171        let mut b = a.try_clone().unwrap();
172        b.execute_batch("INSERT INTO t VALUES (1)").unwrap();
173        let mut result = a.execute("SELECT count(*) AS c FROM t").unwrap();
174        let row = result.next().unwrap().unwrap();
175        match row.get("c").unwrap() {
176            crate::types::value::DuckValue::BigInt(n) => assert_eq!(*n, 1),
177            other => panic!("expected BigInt, got {other:?}"),
178        }
179    }
180
181    #[test]
182    fn connection_database_round_trip() {
183        let conn = Connection::open_in_memory().unwrap();
184        let db = conn.database();
185        let mut other = db.connect().unwrap();
186        other.execute_batch("CREATE TABLE t (id INTEGER)").unwrap();
187    }
188
189    #[test]
190    fn database_is_send_and_sync() {
191        fn assert_send_sync<T: Send + Sync>() {}
192        assert_send_sync::<Database>();
193    }
194
195    #[test]
196    fn file_database_persists_after_all_handles_are_dropped() {
197        let directory = tempfile::tempdir().unwrap();
198        let path = directory.path().join("persistent.duckdb");
199
200        {
201            let database = Database::open(&path).unwrap();
202            let mut connection = database.connect().unwrap();
203            connection
204                .execute_batch(
205                    "CREATE TABLE persisted (value INTEGER); INSERT INTO persisted VALUES (42)",
206                )
207                .unwrap();
208        }
209
210        let database = Database::open(&path).unwrap();
211        let mut connection = database.connect().unwrap();
212        let mut rows = connection.execute("SELECT value FROM persisted").unwrap();
213        let row = rows.next().unwrap().unwrap();
214        assert_eq!(row.get("value").unwrap(), &crate::types::value::DuckValue::Int(42));
215    }
216
217    #[test]
218    fn rejects_a_path_with_an_embedded_nul() {
219        let error = Database::open(std::path::Path::new("bad\0path.duckdb")).unwrap_err();
220        assert!(matches!(error, crate::error::Error::NulError(_)));
221    }
222
223    #[test]
224    fn config_is_applied_when_opening_database() {
225        let config = Config::default().threads(1).unwrap();
226        let database = Database::open_in_memory_with_flags(config).unwrap();
227        let mut connection = database.connect().unwrap();
228        let mut rows = connection.execute("SELECT current_setting('threads') AS threads").unwrap();
229        let row = rows.next().unwrap().unwrap();
230        assert_eq!(row.get("threads").unwrap(), &crate::types::value::DuckValue::BigInt(1));
231    }
232}