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