Skip to main content

quack_rs/
client_context.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//! Client context access (`DuckDB` 1.5.0+).
7//!
8//! The client context provides access to the connection's catalog, configuration
9//! options, file system, and connection ID from within registered function
10//! callbacks (scalar, table, aggregate, etc.).
11//!
12//! # Obtaining a `ClientContext`
13//!
14//! Use [`ClientContext::from_connection`] from within an extension entry point,
15//! or obtain one from a callback via the `duckdb_*_get_client_context` family
16//! of C API functions.
17
18use std::ffi::CStr;
19
20use libduckdb_sys::{
21    duckdb_client_context, duckdb_client_context_get_catalog,
22    duckdb_client_context_get_config_option, duckdb_client_context_get_connection_id,
23    duckdb_config_option_scope, duckdb_connection, duckdb_connection_get_client_context,
24    duckdb_destroy_client_context, duckdb_destroy_value, duckdb_get_varchar, duckdb_value,
25};
26
27use crate::catalog::Catalog;
28use crate::error::ExtensionError;
29
30/// RAII wrapper for a `duckdb_client_context`.
31///
32/// Provides access to the connection's catalog, configuration, and file system.
33/// Automatically destroyed when dropped.
34pub struct ClientContext {
35    ctx: duckdb_client_context,
36}
37
38impl ClientContext {
39    /// Obtain a client context from a `duckdb_connection`.
40    ///
41    /// # Errors
42    ///
43    /// Returns `ExtensionError` if the context cannot be obtained.
44    ///
45    /// # Safety
46    ///
47    /// `con` must be a valid, open `duckdb_connection`.
48    pub unsafe fn from_connection(con: duckdb_connection) -> Result<Self, ExtensionError> {
49        let mut ctx: duckdb_client_context = core::ptr::null_mut();
50        // SAFETY: con is valid per caller's contract.
51        unsafe { duckdb_connection_get_client_context(con, &raw mut ctx) };
52        if ctx.is_null() {
53            return Err(ExtensionError::new(
54                "failed to obtain client context from connection",
55            ));
56        }
57        Ok(Self { ctx })
58    }
59
60    /// Wrap a raw `duckdb_client_context` handle.
61    ///
62    /// # Safety
63    ///
64    /// `ctx` must be a valid, non-null `duckdb_client_context`.
65    pub const unsafe fn from_raw(ctx: duckdb_client_context) -> Self {
66        Self { ctx }
67    }
68
69    /// Returns the raw handle.
70    #[must_use]
71    pub const fn as_raw(&self) -> duckdb_client_context {
72        self.ctx
73    }
74
75    /// Retrieves a database catalog by name.
76    ///
77    /// Returns `None` when there is no such catalog — and in two cases that are
78    /// easy to mistake for one:
79    ///
80    /// - **`name` is empty.** `duckdb_client_context_get_catalog` rejects that
81    ///   outright (`strlen(name) == 0` is an explicit early return); it is not
82    ///   a way to ask for "the default". The catalog of an in-memory database
83    ///   is named `memory`; a file database's is the file's stem.
84    /// - **No transaction is active.** `DuckDB` checks
85    ///   `transaction.HasActiveTransaction()` and returns null otherwise, so
86    ///   this works inside a function callback but not on an idle
87    ///   auto-commit connection.
88    ///
89    /// # Safety
90    ///
91    /// Must be called from within an active transaction context.
92    pub unsafe fn catalog(&self, name: &CStr) -> Option<Catalog> {
93        // SAFETY: self.ctx is valid, caller ensures active transaction.
94        let catalog = unsafe { duckdb_client_context_get_catalog(self.ctx, name.as_ptr()) };
95        if catalog.is_null() {
96            None
97        } else {
98            // SAFETY: catalog is non-null and valid.
99            Some(unsafe { Catalog::from_raw(catalog) })
100        }
101    }
102
103    /// Retrieves a configuration option value by name.
104    ///
105    /// Returns the value as a string, or `None` if the option does not exist.
106    ///
107    /// # Do not use this to probe for a setting that may not exist
108    ///
109    /// `DuckDB` 1.5.5's `duckdb_client_context_get_config_option` reads the
110    /// lookup's scope before checking the lookup succeeded:
111    ///
112    /// ```text
113    /// // src/main/capi/config_options-c.cpp
114    /// switch (ctx.TryGetCurrentSetting(option_name, result).GetScope()) {
115    ///
116    /// // src/include/duckdb/main/setting_info.hpp
117    /// SettingScope GetScope() {
118    ///     D_ASSERT(scope != SettingScope::INVALID);
119    /// ```
120    ///
121    /// A missing setting yields `SettingScope::INVALID`, so `GetScope()` trips
122    /// that assertion. In a release `DuckDB` — what users run — `D_ASSERT`
123    /// compiles out, the function's own `default:` branch handles `INVALID`,
124    /// and this returns `None` as documented. Against a `DuckDB` built **with
125    /// debug assertions**, the process aborts. Verified against 1.5.5.
126    ///
127    /// So this is safe for a setting you registered or know exists, and unsafe
128    /// as an existence check. To ask whether a setting exists, use SQL, which
129    /// has no such path:
130    ///
131    /// ```sql
132    /// SELECT count(*) FROM duckdb_settings() WHERE name = 'my_setting';
133    /// ```
134    pub fn config_option(&self, name: &CStr) -> Option<String> {
135        let mut scope: duckdb_config_option_scope = 0;
136        // SAFETY: self.ctx is valid.
137        let val: duckdb_value = unsafe {
138            duckdb_client_context_get_config_option(self.ctx, name.as_ptr(), &raw mut scope)
139        };
140        if val.is_null() {
141            return None;
142        }
143        // SAFETY: val is a valid duckdb_value.
144        let c_str = unsafe { duckdb_get_varchar(val) };
145        let result = if c_str.is_null() {
146            None
147        } else {
148            // SAFETY: c_str is a valid null-terminated string.
149            unsafe { CStr::from_ptr(c_str) }
150                .to_str()
151                .ok()
152                .map(String::from)
153        };
154        if !c_str.is_null() {
155            // SAFETY: `duckdb_get_varchar` returns a `char *` DuckDB allocated
156            // (`duckdb_malloc` + `memcpy`), so this owns it and must free it.
157            unsafe {
158                libduckdb_sys::duckdb_free(c_str.cast::<core::ffi::c_void>());
159            }
160        }
161        let mut val_mut = val;
162        // SAFETY: `val` came from `duckdb_client_context_get_config_option`,
163        // which returns an owned `duckdb_value`; it is not used afterwards.
164        unsafe {
165            duckdb_destroy_value(&raw mut val_mut);
166        }
167        result
168    }
169
170    /// Returns the connection ID associated with this client context.
171    #[must_use]
172    pub fn connection_id(&self) -> u64 {
173        // SAFETY: self.ctx is valid.
174        unsafe { duckdb_client_context_get_connection_id(self.ctx) }
175    }
176}
177
178impl Drop for ClientContext {
179    fn drop(&mut self) {
180        // SAFETY: self.ctx was obtained from a valid `DuckDB` API call.
181        unsafe {
182            duckdb_destroy_client_context(&raw mut self.ctx);
183        }
184    }
185}
186
187#[cfg(all(test, feature = "_duckdb-testing"))]
188mod tests {
189    use super::*;
190
191    /// Opens a raw `duckdb_connection` for testing.
192    fn open_raw_connection() -> (libduckdb_sys::duckdb_database, duckdb_connection) {
193        // Ensure dispatch table is populated.
194        let _db = crate::testing::InMemoryDb::open().unwrap();
195
196        let mut db: libduckdb_sys::duckdb_database = core::ptr::null_mut();
197        let mut con: duckdb_connection = core::ptr::null_mut();
198
199        // SAFETY: dispatch table is initialized, nullptr opens in-memory.
200        unsafe {
201            let rc = libduckdb_sys::duckdb_open(core::ptr::null(), &raw mut db);
202            assert_eq!(rc, libduckdb_sys::DuckDBSuccess, "duckdb_open failed");
203            let rc = libduckdb_sys::duckdb_connect(db, &raw mut con);
204            assert_eq!(rc, libduckdb_sys::DuckDBSuccess, "duckdb_connect failed");
205        }
206        (db, con)
207    }
208
209    /// Closes a raw connection and database.
210    unsafe fn close_raw_connection(
211        mut con: duckdb_connection,
212        mut db: libduckdb_sys::duckdb_database,
213    ) {
214        unsafe {
215            libduckdb_sys::duckdb_disconnect(&raw mut con);
216            libduckdb_sys::duckdb_close(&raw mut db);
217        }
218    }
219
220    #[test]
221    fn from_connection_succeeds() {
222        let (db, con) = open_raw_connection();
223
224        // SAFETY: con is a valid open connection.
225        let ctx = unsafe { ClientContext::from_connection(con) };
226        assert!(
227            ctx.is_ok(),
228            "from_connection should succeed: {:?}",
229            ctx.err()
230        );
231
232        drop(ctx.unwrap());
233        // SAFETY: valid handles.
234        unsafe { close_raw_connection(con, db) };
235    }
236
237    #[test]
238    fn connection_id_returns_nonzero() {
239        let (db, con) = open_raw_connection();
240
241        // SAFETY: con is a valid open connection.
242        let ctx = unsafe { ClientContext::from_connection(con) }.unwrap();
243        // Connection IDs are assigned sequentially starting from a positive value.
244        // We just verify the call doesn't crash and returns something.
245        let _id = ctx.connection_id();
246
247        drop(ctx);
248        // SAFETY: valid handles.
249        unsafe { close_raw_connection(con, db) };
250    }
251
252    #[test]
253    fn config_option_returns_some_for_known_setting() {
254        let (db, con) = open_raw_connection();
255
256        // SAFETY: con is a valid open connection.
257        let ctx = unsafe { ClientContext::from_connection(con) }.unwrap();
258
259        // "threads" is a well-known DuckDB config option.
260        let threads = ctx.config_option(c"threads");
261        assert!(threads.is_some(), "'threads' config option should exist");
262        // The value should be a parseable positive integer.
263        let val: usize = threads.unwrap().parse().expect("threads should be numeric");
264        assert!(val > 0, "threads should be > 0");
265
266        drop(ctx);
267        // SAFETY: valid handles.
268        unsafe { close_raw_connection(con, db) };
269    }
270
271    #[test]
272    fn catalog_returns_some_for_default() {
273        let (db, con) = open_raw_connection();
274
275        // Start a transaction so we have an active transaction context.
276        // SAFETY: con is valid.
277        unsafe {
278            let sql = c"BEGIN TRANSACTION";
279            libduckdb_sys::duckdb_query(con, sql.as_ptr(), core::ptr::null_mut());
280        }
281
282        // SAFETY: con is a valid open connection.
283        let ctx = unsafe { ClientContext::from_connection(con) }.unwrap();
284
285        // Empty name = default catalog. Must be called within a transaction.
286        // SAFETY: within an active transaction.
287        let catalog = unsafe { ctx.catalog(c"") };
288        // Note: catalog lookup may or may not succeed depending on DuckDB version
289        // internals. We just verify the call doesn't crash.
290        drop(catalog);
291
292        drop(ctx);
293        // Rollback the transaction.
294        // SAFETY: con is valid.
295        unsafe {
296            let sql = c"ROLLBACK";
297            libduckdb_sys::duckdb_query(con, sql.as_ptr(), core::ptr::null_mut());
298        }
299        // SAFETY: valid handles.
300        unsafe { close_raw_connection(con, db) };
301    }
302}
303
304crate::debug_repr::impl_handle_debug!(ClientContext.ctx);