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
// 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.
///
/// Pass an empty string to get the default catalog. This function can only
/// be called from within an active transaction (e.g. during a registered
/// function callback).
///
/// # 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.
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)
};
// SAFETY: c_str was allocated by `DuckDB` and must be freed.
if !c_str.is_null() {
unsafe {
libduckdb_sys::duckdb_free(c_str.cast::<core::ffi::c_void>());
}
}
// SAFETY: val must be destroyed.
let mut val_mut = val;
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 = "bundled-test"))]
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) };
}
}