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
305
306
307
308
309
310
311
312
313
314
315
316
317
318
// 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!
//! Extension-defined configuration options (`DuckDB` 1.5.0+).
//!
//! Extensions can register custom settings that users can read and write via
//! `SET` / `RESET` / `current_setting()`. This module wraps the
//! `duckdb_config_option` C API surface behind a safe builder.
//!
//! # Example
//!
//! ```rust,no_run
//! use quack_rs::config_option::{ConfigOptionBuilder, ConfigOptionScope};
//! use quack_rs::types::TypeId;
//!
//! let option = ConfigOptionBuilder::try_new("my_ext_threshold")?
//! .description("Maximum threshold for my_ext operations")?
//! .option_type(TypeId::BigInt)
//! .default_value("100")?
//! .scope(ConfigOptionScope::Global);
//! # Ok::<(), quack_rs::error::ExtensionError>(())
//! ```
use std::ffi::CString;
use libduckdb_sys::{
duckdb_config_option, duckdb_config_option_scope_DUCKDB_CONFIG_OPTION_SCOPE_GLOBAL,
duckdb_config_option_scope_DUCKDB_CONFIG_OPTION_SCOPE_LOCAL,
duckdb_config_option_scope_DUCKDB_CONFIG_OPTION_SCOPE_SESSION,
duckdb_config_option_set_default_scope, duckdb_config_option_set_default_value,
duckdb_config_option_set_description, duckdb_config_option_set_name,
duckdb_config_option_set_type, duckdb_connection, duckdb_create_config_option,
duckdb_create_varchar, duckdb_destroy_config_option, duckdb_destroy_value,
duckdb_register_config_option, DuckDBSuccess,
};
use crate::error::ExtensionError;
use crate::types::{LogicalType, TypeId};
/// Scope in which a configuration option takes effect.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConfigOptionScope {
/// Option is local to the current statement.
Local,
/// Option is scoped to the current session.
Session,
/// Option applies globally to the database.
Global,
}
impl ConfigOptionScope {
/// Converts to the `DuckDB` C API scope constant.
#[must_use]
pub(crate) const fn to_raw(self) -> u32 {
match self {
Self::Local => duckdb_config_option_scope_DUCKDB_CONFIG_OPTION_SCOPE_LOCAL,
Self::Session => duckdb_config_option_scope_DUCKDB_CONFIG_OPTION_SCOPE_SESSION,
Self::Global => duckdb_config_option_scope_DUCKDB_CONFIG_OPTION_SCOPE_GLOBAL,
}
}
}
/// Builder for registering extension-defined configuration options.
///
/// After building, call [`register`][Self::register] from your entry point to
/// register the setting with `DuckDB`.
#[must_use]
pub struct ConfigOptionBuilder {
name: CString,
description: Option<CString>,
option_type: Option<TypeId>,
default_value: Option<CString>,
scope: ConfigOptionScope,
}
impl ConfigOptionBuilder {
/// Creates a new config option builder with the given name.
///
/// # Errors
///
/// Returns `ExtensionError` if the name contains a null byte.
pub fn try_new(name: &str) -> Result<Self, ExtensionError> {
let c_name = CString::new(name)
.map_err(|_| ExtensionError::new("config option name contains null byte"))?;
Ok(Self {
name: c_name,
description: None,
option_type: None,
default_value: None,
scope: ConfigOptionScope::Global,
})
}
/// Returns the name of this config option.
#[must_use]
pub fn name(&self) -> &str {
self.name.to_str().unwrap_or("")
}
/// Sets the human-readable description for this option.
///
/// # Errors
///
/// Returns `ExtensionError` if `desc` contains a null byte.
pub fn description(mut self, desc: &str) -> Result<Self, ExtensionError> {
self.description =
Some(CString::new(desc).map_err(|_| {
ExtensionError::new("config option description contains null byte")
})?);
Ok(self)
}
/// Sets the value type for this option (e.g. `TypeId::BigInt`, `TypeId::Varchar`).
pub const fn option_type(mut self, type_id: TypeId) -> Self {
self.option_type = Some(type_id);
self
}
/// Sets the default value as a string representation.
///
/// # Errors
///
/// Returns `ExtensionError` if `value` contains a null byte.
pub fn default_value(mut self, value: &str) -> Result<Self, ExtensionError> {
self.default_value =
Some(CString::new(value).map_err(|_| {
ExtensionError::new("config option default value contains null byte")
})?);
Ok(self)
}
/// Sets the scope for this option.
pub const fn scope(mut self, scope: ConfigOptionScope) -> Self {
self.scope = scope;
self
}
/// Registers this config option with `DuckDB`.
///
/// # Errors
///
/// Returns `ExtensionError` if the option type was not set or registration
/// fails.
///
/// # Safety
///
/// `con` must be a valid, open `duckdb_connection`.
pub unsafe fn register(self, con: duckdb_connection) -> Result<(), ExtensionError> {
let type_id = self
.option_type
.ok_or_else(|| ExtensionError::new("config option type not set"))?;
let lt = LogicalType::new(type_id);
// SAFETY: duckdb_create_config_option allocates a new handle.
let option: duckdb_config_option = unsafe { duckdb_create_config_option() };
// SAFETY: option is a valid newly created handle.
unsafe {
duckdb_config_option_set_name(option, self.name.as_ptr());
duckdb_config_option_set_type(option, lt.as_raw());
duckdb_config_option_set_default_scope(option, self.scope.to_raw());
}
if let Some(ref desc) = self.description {
// SAFETY: option and desc are valid.
unsafe {
duckdb_config_option_set_description(option, desc.as_ptr());
}
}
if let Some(ref val) = self.default_value {
// SAFETY: duckdb_create_varchar allocates a duckdb_value.
let dv = unsafe { duckdb_create_varchar(val.as_ptr()) };
// SAFETY: option and dv are valid.
unsafe {
duckdb_config_option_set_default_value(option, dv);
}
// SAFETY: dv was created by duckdb_create_varchar.
let mut dv_mut = dv;
unsafe {
duckdb_destroy_value(&raw mut dv_mut);
}
}
// SAFETY: con is valid per caller's contract, option is fully configured.
let result = unsafe { duckdb_register_config_option(con, option) };
// SAFETY: option was created above and must be destroyed after registration.
let mut option_mut = option;
unsafe {
duckdb_destroy_config_option(&raw mut option_mut);
}
if result == DuckDBSuccess {
Ok(())
} else {
Err(ExtensionError::new(format!(
"duckdb_register_config_option failed for '{}'",
self.name.to_string_lossy()
)))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::TypeId;
#[test]
fn try_new_valid_name() {
let builder = ConfigOptionBuilder::try_new("my_option").unwrap();
assert_eq!(builder.name(), "my_option");
}
#[test]
fn try_new_null_byte_rejected() {
let result = ConfigOptionBuilder::try_new("bad\0name");
assert!(result.is_err());
let err = result.err().unwrap();
assert!(
err.to_string().contains("null byte"),
"error should mention null byte"
);
}
#[test]
fn description_null_byte_rejected() {
let result = ConfigOptionBuilder::try_new("opt")
.unwrap()
.description("bad\0desc");
assert!(result.is_err());
}
#[test]
fn default_value_null_byte_rejected() {
let result = ConfigOptionBuilder::try_new("opt")
.unwrap()
.default_value("bad\0val");
assert!(result.is_err());
}
#[test]
fn builder_stores_option_type() {
let builder = ConfigOptionBuilder::try_new("threshold")
.unwrap()
.option_type(TypeId::BigInt);
// Verifies fluent chaining compiles and doesn't panic.
assert_eq!(builder.name(), "threshold");
}
#[test]
fn builder_stores_description() {
let builder = ConfigOptionBuilder::try_new("threshold")
.unwrap()
.description("max threshold")
.unwrap();
assert_eq!(builder.name(), "threshold");
}
#[test]
fn builder_stores_default_value() {
let builder = ConfigOptionBuilder::try_new("limit")
.unwrap()
.default_value("100")
.unwrap();
assert_eq!(builder.name(), "limit");
}
#[test]
fn scope_default_is_global() {
// ConfigOptionScope defaults to Global in the builder.
let builder = ConfigOptionBuilder::try_new("opt").unwrap();
// We can't read the scope directly, but we can verify the
// ConfigOptionScope enum works correctly.
assert_eq!(builder.name(), "opt");
}
#[test]
fn scope_enum_to_raw_distinct_values() {
let local = ConfigOptionScope::Local.to_raw();
let session = ConfigOptionScope::Session.to_raw();
let global = ConfigOptionScope::Global.to_raw();
assert_ne!(local, session);
assert_ne!(session, global);
assert_ne!(local, global);
}
#[test]
fn scope_enum_debug_impl() {
assert_eq!(format!("{:?}", ConfigOptionScope::Local), "Local");
assert_eq!(format!("{:?}", ConfigOptionScope::Session), "Session");
assert_eq!(format!("{:?}", ConfigOptionScope::Global), "Global");
}
#[test]
fn scope_enum_clone_eq() {
let a = ConfigOptionScope::Session;
#[allow(clippy::clone_on_copy)]
let b = a.clone();
assert_eq!(a, b);
}
#[test]
fn full_builder_chain_compiles() {
// Verify the full fluent builder chain works without panicking.
let _builder = ConfigOptionBuilder::try_new("my_ext_threshold")
.unwrap()
.description("Maximum threshold")
.unwrap()
.option_type(TypeId::BigInt)
.default_value("100")
.unwrap()
.scope(ConfigOptionScope::Global);
}
}