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);