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
100impl std::fmt::Debug for Database {
101    fn fmt(
102        &self,
103        f: &mut std::fmt::Formatter<'_>,
104    ) -> std::fmt::Result {
105        f.debug_struct("Database").finish_non_exhaustive()
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112
113    #[test]
114    fn database_connect_shares_in_memory_state() {
115        let db = Database::open_in_memory().unwrap();
116        let mut a = db.connect().unwrap();
117        let mut b = db.connect().unwrap();
118        a.execute_batch("CREATE TABLE t (id INTEGER)").unwrap();
119        b.execute_batch("INSERT INTO t VALUES (1)").unwrap();
120        let mut result = a.execute("SELECT count(*) AS c FROM t").unwrap();
121        let row = result.next().unwrap().unwrap();
122        match row.get("c").unwrap() {
123            crate::types::value::DuckValue::BigInt(n) => assert_eq!(*n, 1),
124            other => panic!("expected BigInt, got {other:?}"),
125        }
126    }
127
128    #[test]
129    fn separate_open_in_memory_are_independent() {
130        let mut a = Connection::open_in_memory().unwrap();
131        let mut b = Connection::open_in_memory().unwrap();
132        a.execute_batch("CREATE TABLE t (id INTEGER)").unwrap();
133        // `b` never had `t` created, so this must fail — proving isolation.
134        assert!(b.execute_batch("INSERT INTO t VALUES (1)").is_err());
135    }
136
137    #[test]
138    fn database_outlives_connection() {
139        let db = Database::open_in_memory().unwrap();
140        let mut conn = db.connect().unwrap();
141        drop(db);
142        // The Arc<RawDatabase> is kept alive by `conn`'s RawConnection.
143        conn.execute_batch("CREATE TABLE t (id INTEGER)").unwrap();
144    }
145
146    #[test]
147    fn connection_try_clone_shares_state() {
148        let mut a = Connection::open_in_memory().unwrap();
149        a.execute_batch("CREATE TABLE t (id INTEGER)").unwrap();
150        let mut b = a.try_clone().unwrap();
151        b.execute_batch("INSERT INTO t VALUES (1)").unwrap();
152        let mut result = a.execute("SELECT count(*) AS c FROM t").unwrap();
153        let row = result.next().unwrap().unwrap();
154        match row.get("c").unwrap() {
155            crate::types::value::DuckValue::BigInt(n) => assert_eq!(*n, 1),
156            other => panic!("expected BigInt, got {other:?}"),
157        }
158    }
159
160    #[test]
161    fn connection_database_round_trip() {
162        let conn = Connection::open_in_memory().unwrap();
163        let db = conn.database();
164        let mut other = db.connect().unwrap();
165        other.execute_batch("CREATE TABLE t (id INTEGER)").unwrap();
166    }
167
168    #[test]
169    fn database_is_send_and_sync() {
170        fn assert_send_sync<T: Send + Sync>() {}
171        assert_send_sync::<Database>();
172    }
173}