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}