better_duck_core/
database.rs1use 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#[derive(Clone)]
36pub struct Database {
37 inner: Arc<RawDatabase>,
38}
39
40impl Database {
41 pub(crate) fn from_raw(inner: Arc<RawDatabase>) -> Database {
43 Database { inner }
44 }
45
46 pub fn open<P: AsRef<Path>>(path: P) -> Result<Database> {
52 Self::open_with_flags(path, Config::default())
53 }
54
55 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 pub fn open_in_memory() -> Result<Database> {
75 Self::open_in_memory_with_flags(Config::default())
76 }
77
78 pub fn open_in_memory_with_flags(config: Config) -> Result<Database> {
84 Self::open_with_flags(":memory:", config)
85 }
86
87 pub fn connect(&self) -> Result<Connection> {
96 RawConnection::new(Arc::clone(&self.inner)).map(Connection::from_raw)
97 }
98
99 #[cfg(feature = "udf")]
104 pub(crate) fn raw_db(&self) -> crate::ffi::duckdb_database {
105 self.inner.0
106 }
107
108 pub(crate) fn handle(&self) -> crate::ffi::duckdb_database {
111 self.inner.0
112 }
113
114 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 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 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}