Skip to main content

better_duck_core/raw/
instance_cache.rs

1//! RAII wrapper for `duckdb_instance_cache` — opt-in cache-backed database opening.
2//!
3//! An instance cache lets several `:memory:`-or-file opens of the *same* path share
4//! one underlying database instance, instead of each open creating an independent
5//! one. This is strictly opt-in: it never changes [`Database::open`]'s default
6//! semantics. [`InstanceCache`] owns the cache handle (destroyed once on drop);
7//! databases obtained from it are ordinary [`Database`]s that close independently.
8// FFI pointer args are used safely inside `unsafe` blocks.
9#![allow(clippy::not_unsafe_ptr_arg_deref)]
10
11use std::ffi::{c_void, CStr};
12use std::path::Path;
13use std::ptr;
14use std::sync::Arc;
15
16use crate::{
17    config::Config,
18    database::Database,
19    error::{Error, Result},
20    ffi::{
21        duckdb_create_instance_cache, duckdb_database, duckdb_destroy_instance_cache, duckdb_free,
22        duckdb_get_or_create_from_cache, duckdb_instance_cache, DuckDBSuccess,
23    },
24    helpers::path::path_to_cstring,
25    raw::connection::RawDatabase,
26};
27
28/// An owned `duckdb_instance_cache` (destroyed once on drop).
29///
30/// Deliberately `!Send + !Sync`: DuckDB gives no concurrency guarantee for a shared
31/// instance cache, so a handle must stay on the thread that created it.
32pub struct InstanceCache {
33    ptr: duckdb_instance_cache,
34    _not_thread_safe: std::marker::PhantomData<*const ()>,
35}
36
37impl InstanceCache {
38    /// Creates a new, empty instance cache.
39    #[must_use]
40    pub fn new() -> InstanceCache {
41        // SAFETY: always valid to call; returns a freshly allocated cache handle.
42        let ptr = unsafe { duckdb_create_instance_cache() };
43        InstanceCache { ptr, _not_thread_safe: std::marker::PhantomData }
44    }
45
46    /// Opens the database at `path` through this cache: if a database for `path`
47    /// already exists in the cache it is returned (sharing the same instance),
48    /// otherwise a new one is created and cached. `:memory:` and an empty path both
49    /// mean an in-memory database.
50    ///
51    /// The returned [`Database`] closes independently; the cache keeps its own
52    /// reference until dropped.
53    ///
54    /// # Errors
55    ///
56    /// Returns an error if the path contains an interior NUL or DuckDB fails to open
57    /// the database (its error message is surfaced).
58    pub fn get_or_create<P: AsRef<Path>>(
59        &self,
60        path: P,
61        config: Config,
62    ) -> Result<Database> {
63        let c_path = path_to_cstring(path.as_ref())?;
64        let config = config.with("duckdb_api", "rust")?;
65        let mut db: duckdb_database = ptr::null_mut();
66        let mut c_err: *mut std::os::raw::c_char = ptr::null_mut();
67        // SAFETY: `self.ptr` is a valid cache; `c_path` is a valid NUL-terminated
68        // string; `db`/`c_err` are valid out-pointers. On error DuckDB allocates
69        // `c_err`, which we free with `duckdb_free`.
70        let state = unsafe {
71            duckdb_get_or_create_from_cache(
72                self.ptr,
73                c_path.as_ptr(),
74                &mut db,
75                config.duckdb_config(),
76                &mut c_err,
77            )
78        };
79        if state != DuckDBSuccess {
80            let msg = if c_err.is_null() {
81                None
82            } else {
83                // SAFETY: `c_err` is a valid, non-null, NUL-terminated C string DuckDB
84                // allocated on failure.
85                let m = unsafe { CStr::from_ptr(c_err) }.to_string_lossy().into_owned();
86                // SAFETY: `c_err` was allocated by DuckDB; free it once.
87                unsafe { duckdb_free(c_err as *mut c_void) };
88                Some(m)
89            };
90            return Err(Error::DuckDBFailure(crate::ffi::Error::new(state), msg));
91        }
92        // SAFETY: on success `db` is a valid open database handle we now own.
93        let raw = unsafe { RawDatabase::new(db) }?;
94        Ok(Database::from_raw(Arc::new(raw)))
95    }
96}
97
98impl Default for InstanceCache {
99    fn default() -> Self {
100        Self::new()
101    }
102}
103
104impl Drop for InstanceCache {
105    fn drop(&mut self) {
106        if !self.ptr.is_null() {
107            // SAFETY: `self.ptr` is a valid, non-null cache created by
108            // `duckdb_create_instance_cache` and not yet destroyed; destroyed once.
109            unsafe { duckdb_destroy_instance_cache(&mut self.ptr) };
110        }
111    }
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117
118    #[test]
119    fn cache_shares_one_in_memory_instance_across_opens() {
120        let cache = InstanceCache::new();
121        // Two opens of the same named in-memory path share one instance.
122        let a = cache.get_or_create(":memory:cachetest", Config::default()).unwrap();
123        let b = cache.get_or_create(":memory:cachetest", Config::default()).unwrap();
124        let mut ca = a.connect().unwrap();
125        ca.execute_batch("CREATE TABLE t (v INTEGER)").unwrap();
126        ca.execute_batch("INSERT INTO t VALUES (7)").unwrap();
127        // `b` observes `a`'s data because they share the cached instance.
128        let mut cb = b.connect().unwrap();
129        let mut rows = cb.execute("SELECT v FROM t").unwrap();
130        assert_eq!(
131            rows.next().unwrap().unwrap().get("v"),
132            Some(&crate::types::value::DuckValue::Int(7))
133        );
134    }
135
136    #[test]
137    fn distinct_named_paths_are_independent() {
138        let cache = InstanceCache::new();
139        let a = cache.get_or_create(":memory:one", Config::default()).unwrap();
140        let b = cache.get_or_create(":memory:two", Config::default()).unwrap();
141        let mut ca = a.connect().unwrap();
142        ca.execute_batch("CREATE TABLE t (v INTEGER)").unwrap();
143        // A different cached path does not see `a`'s table.
144        let mut cb = b.connect().unwrap();
145        assert!(cb.execute("SELECT v FROM t").is_err());
146    }
147}