Skip to main content

quack_rs/
instance_cache.rs

1// SPDX-License-Identifier: MIT
2// Copyright 2026 Tom F. <https://github.com/tomtom215/>
3// My way of giving something small back to the open source community
4// and encouraging more Rust development!
5
6//! Database instance cache (`DuckDB` 1.5.0+).
7//!
8//! An [`InstanceCache`] lets multiple connections share a single underlying
9//! `DuckDB` instance for a given database path. Opening the same path twice
10//! through the cache returns handles backed by the *same* instance, which avoids
11//! the "database is already open in another process/instance" conflict and saves
12//! the cost of re-initialising the database.
13//!
14//! This is primarily useful for extensions or host integrations that open
15//! secondary databases on behalf of a query.
16//!
17//! # Example
18//!
19//! ```rust,no_run
20//! use quack_rs::instance_cache::InstanceCache;
21//!
22//! # fn demo() -> Result<(), quack_rs::error::ExtensionError> {
23//! let cache = InstanceCache::new();
24//! // Returns a duckdb_database the caller owns and must close with duckdb_close.
25//! let db = cache.get_or_create(c"my.db", None)?;
26//! # let _ = db;
27//! # Ok(())
28//! # }
29//! ```
30
31use std::ffi::CStr;
32use std::os::raw::c_char;
33
34use libduckdb_sys::{
35    duckdb_config, duckdb_create_instance_cache, duckdb_database, duckdb_destroy_instance_cache,
36    duckdb_free, duckdb_get_or_create_from_cache, duckdb_instance_cache, DuckDBSuccess,
37};
38
39use crate::config::DbConfig;
40use crate::error::ExtensionError;
41
42/// RAII wrapper for a `duckdb_instance_cache`.
43///
44/// Automatically destroyed when dropped. Databases obtained from the cache
45/// remain valid until they are individually closed and the cache is dropped.
46pub struct InstanceCache {
47    cache: duckdb_instance_cache,
48}
49
50impl InstanceCache {
51    /// Creates a new, empty instance cache.
52    #[must_use]
53    pub fn new() -> Self {
54        // SAFETY: duckdb_create_instance_cache allocates an owned handle.
55        let cache = unsafe { duckdb_create_instance_cache() };
56        Self { cache }
57    }
58
59    /// Opens `path` through the cache, creating the instance if it does not yet
60    /// exist or returning a handle to the cached one if it does.
61    ///
62    /// Pass `config` to control how a freshly-created instance is configured; it
63    /// is ignored when an instance already exists for `path`.
64    ///
65    /// The returned `duckdb_database` is owned by the caller and **must** be
66    /// closed with `duckdb_close` when no longer needed.
67    ///
68    /// # Errors
69    ///
70    /// Returns an [`ExtensionError`] carrying `DuckDB`'s message if the instance
71    /// cannot be opened or created.
72    pub fn get_or_create(
73        &self,
74        path: &CStr,
75        config: Option<&DbConfig>,
76    ) -> Result<duckdb_database, ExtensionError> {
77        let mut out_db: duckdb_database = std::ptr::null_mut();
78        let mut out_err: *mut c_char = std::ptr::null_mut();
79        let cfg: duckdb_config = config.map_or(std::ptr::null_mut(), DbConfig::as_raw);
80        // SAFETY: self.cache and path are valid; out_db and out_err are valid
81        // out-pointers; cfg is either null or a valid duckdb_config.
82        let state = unsafe {
83            duckdb_get_or_create_from_cache(
84                self.cache,
85                path.as_ptr(),
86                &raw mut out_db,
87                cfg,
88                &raw mut out_err,
89            )
90        };
91        if state == DuckDBSuccess && !out_db.is_null() {
92            return Ok(out_db);
93        }
94        let message = if out_err.is_null() {
95            "failed to open database from instance cache".to_owned()
96        } else {
97            // SAFETY: out_err is a valid null-terminated string allocated by DuckDB.
98            let msg = unsafe { CStr::from_ptr(out_err) }
99                .to_str()
100                .unwrap_or("failed to open database from instance cache")
101                .to_owned();
102            // SAFETY: out_err was allocated by DuckDB and must be freed.
103            unsafe { duckdb_free(out_err.cast()) };
104            msg
105        };
106        Err(ExtensionError::new(message))
107    }
108
109    /// Returns the raw handle.
110    #[inline]
111    #[must_use]
112    pub const fn as_raw(&self) -> duckdb_instance_cache {
113        self.cache
114    }
115}
116
117impl Default for InstanceCache {
118    fn default() -> Self {
119        Self::new()
120    }
121}
122
123impl Drop for InstanceCache {
124    fn drop(&mut self) {
125        if !self.cache.is_null() {
126            // SAFETY: self.cache is a valid handle that we own.
127            unsafe { duckdb_destroy_instance_cache(&raw mut self.cache) };
128        }
129    }
130}
131
132#[cfg(all(test, feature = "bundled-test"))]
133mod tests {
134    use super::*;
135
136    #[test]
137    fn open_in_memory_via_cache() {
138        // Ensure the dispatch table is populated.
139        let _db = crate::testing::InMemoryDb::open().unwrap();
140
141        let cache = InstanceCache::new();
142        // Empty path opens an in-memory database.
143        let result = cache.get_or_create(c"", None);
144        assert!(result.is_ok(), "get_or_create failed: {:?}", result.err());
145        let mut db = result.unwrap();
146        // SAFETY: db is a valid duckdb_database returned from the cache.
147        unsafe { libduckdb_sys::duckdb_close(&raw mut db) };
148    }
149}