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    /// Pass an empty string to get the default catalog. This function can only
78    /// be called from within an active transaction (e.g. during a registered
79    /// function callback).
80    ///
81    /// # Safety
82    ///
83    /// Must be called from within an active transaction context.
84    pub unsafe fn catalog(&self, name: &CStr) -> Option<Catalog> {
85        // SAFETY: self.ctx is valid, caller ensures active transaction.
86        let catalog = unsafe { duckdb_client_context_get_catalog(self.ctx, name.as_ptr()) };
87        if catalog.is_null() {
88            None
89        } else {
90            // SAFETY: catalog is non-null and valid.
91            Some(unsafe { Catalog::from_raw(catalog) })
92        }
93    }
94
95    /// Retrieves a configuration option value by name.
96    ///
97    /// Returns the value as a string, or `None` if the option does not exist.
98    pub fn config_option(&self, name: &CStr) -> Option<String> {
99        let mut scope: duckdb_config_option_scope = 0;
100        // SAFETY: self.ctx is valid.
101        let val: duckdb_value = unsafe {
102            duckdb_client_context_get_config_option(self.ctx, name.as_ptr(), &raw mut scope)
103        };
104        if val.is_null() {
105            return None;
106        }
107        // SAFETY: val is a valid duckdb_value.
108        let c_str = unsafe { duckdb_get_varchar(val) };
109        let result = if c_str.is_null() {
110            None
111        } else {
112            // SAFETY: c_str is a valid null-terminated string.
113            unsafe { CStr::from_ptr(c_str) }
114                .to_str()
115                .ok()
116                .map(String::from)
117        };
118        // SAFETY: c_str was allocated by `DuckDB` and must be freed.
119        if !c_str.is_null() {
120            unsafe {
121                libduckdb_sys::duckdb_free(c_str.cast::<core::ffi::c_void>());
122            }
123        }
124        // SAFETY: val must be destroyed.
125        let mut val_mut = val;
126        unsafe {
127            duckdb_destroy_value(&raw mut val_mut);
128        }
129        result
130    }
131
132    /// Returns the connection ID associated with this client context.
133    #[must_use]
134    pub fn connection_id(&self) -> u64 {
135        // SAFETY: self.ctx is valid.
136        unsafe { duckdb_client_context_get_connection_id(self.ctx) }
137    }
138}
139
140impl Drop for ClientContext {
141    fn drop(&mut self) {
142        // SAFETY: self.ctx was obtained from a valid `DuckDB` API call.
143        unsafe {
144            duckdb_destroy_client_context(&raw mut self.ctx);
145        }
146    }
147}
148
149#[cfg(all(test, feature = "bundled-test"))]
150mod tests {
151    use super::*;
152
153    /// Opens a raw `duckdb_connection` for testing.
154    fn open_raw_connection() -> (libduckdb_sys::duckdb_database, duckdb_connection) {
155        // Ensure dispatch table is populated.
156        let _db = crate::testing::InMemoryDb::open().unwrap();
157
158        let mut db: libduckdb_sys::duckdb_database = core::ptr::null_mut();
159        let mut con: duckdb_connection = core::ptr::null_mut();
160
161        // SAFETY: dispatch table is initialized, nullptr opens in-memory.
162        unsafe {
163            let rc = libduckdb_sys::duckdb_open(core::ptr::null(), &raw mut db);
164            assert_eq!(rc, libduckdb_sys::DuckDBSuccess, "duckdb_open failed");
165            let rc = libduckdb_sys::duckdb_connect(db, &raw mut con);
166            assert_eq!(rc, libduckdb_sys::DuckDBSuccess, "duckdb_connect failed");
167        }
168        (db, con)
169    }
170
171    /// Closes a raw connection and database.
172    unsafe fn close_raw_connection(
173        mut con: duckdb_connection,
174        mut db: libduckdb_sys::duckdb_database,
175    ) {
176        unsafe {
177            libduckdb_sys::duckdb_disconnect(&raw mut con);
178            libduckdb_sys::duckdb_close(&raw mut db);
179        }
180    }
181
182    #[test]
183    fn from_connection_succeeds() {
184        let (db, con) = open_raw_connection();
185
186        // SAFETY: con is a valid open connection.
187        let ctx = unsafe { ClientContext::from_connection(con) };
188        assert!(
189            ctx.is_ok(),
190            "from_connection should succeed: {:?}",
191            ctx.err()
192        );
193
194        drop(ctx.unwrap());
195        // SAFETY: valid handles.
196        unsafe { close_raw_connection(con, db) };
197    }
198
199    #[test]
200    fn connection_id_returns_nonzero() {
201        let (db, con) = open_raw_connection();
202
203        // SAFETY: con is a valid open connection.
204        let ctx = unsafe { ClientContext::from_connection(con) }.unwrap();
205        // Connection IDs are assigned sequentially starting from a positive value.
206        // We just verify the call doesn't crash and returns something.
207        let _id = ctx.connection_id();
208
209        drop(ctx);
210        // SAFETY: valid handles.
211        unsafe { close_raw_connection(con, db) };
212    }
213
214    #[test]
215    fn config_option_returns_some_for_known_setting() {
216        let (db, con) = open_raw_connection();
217
218        // SAFETY: con is a valid open connection.
219        let ctx = unsafe { ClientContext::from_connection(con) }.unwrap();
220
221        // "threads" is a well-known DuckDB config option.
222        let threads = ctx.config_option(c"threads");
223        assert!(threads.is_some(), "'threads' config option should exist");
224        // The value should be a parseable positive integer.
225        let val: usize = threads.unwrap().parse().expect("threads should be numeric");
226        assert!(val > 0, "threads should be > 0");
227
228        drop(ctx);
229        // SAFETY: valid handles.
230        unsafe { close_raw_connection(con, db) };
231    }
232
233    #[test]
234    fn catalog_returns_some_for_default() {
235        let (db, con) = open_raw_connection();
236
237        // Start a transaction so we have an active transaction context.
238        // SAFETY: con is valid.
239        unsafe {
240            let sql = c"BEGIN TRANSACTION";
241            libduckdb_sys::duckdb_query(con, sql.as_ptr(), core::ptr::null_mut());
242        }
243
244        // SAFETY: con is a valid open connection.
245        let ctx = unsafe { ClientContext::from_connection(con) }.unwrap();
246
247        // Empty name = default catalog. Must be called within a transaction.
248        // SAFETY: within an active transaction.
249        let catalog = unsafe { ctx.catalog(c"") };
250        // Note: catalog lookup may or may not succeed depending on DuckDB version
251        // internals. We just verify the call doesn't crash.
252        drop(catalog);
253
254        drop(ctx);
255        // Rollback the transaction.
256        // SAFETY: con is valid.
257        unsafe {
258            let sql = c"ROLLBACK";
259            libduckdb_sys::duckdb_query(con, sql.as_ptr(), core::ptr::null_mut());
260        }
261        // SAFETY: valid handles.
262        unsafe { close_raw_connection(con, db) };
263    }
264}