quack_rs/config.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//! RAII wrapper for `DuckDB` database configuration.
7//!
8//! [`DbConfig`] wraps `duckdb_config` and provides a builder-style API for
9//! setting configuration options before opening a `DuckDB` database.
10//!
11//! Extension authors typically receive an already-opened connection and do not
12//! need to open databases themselves. `DbConfig` is useful when an extension
13//! needs to open a **secondary** `DuckDB` database from within its callbacks —
14//! for example, a virtual table that reads from another `.duckdb` file.
15//!
16//! # Example
17//!
18//! ```rust,no_run
19//! use quack_rs::config::DbConfig;
20//!
21//! // fn open_secondary() -> Result<(), quack_rs::error::ExtensionError> {
22//! // let config = DbConfig::new()?
23//! // .set("access_mode", "READ_ONLY")?
24//! // .set("threads", "4")?;
25//! // // Pass config.as_raw() to duckdb_open_ext(...)
26//! // Ok(())
27//! // }
28//! ```
29//!
30//! # Available configuration flags
31//!
32//! Use [`DbConfig::flag_count`] and [`DbConfig::get_flag`] to enumerate all
33//! supported option names and their descriptions at runtime.
34
35use std::ffi::{CStr, CString};
36
37use libduckdb_sys::{
38 duckdb_config, duckdb_config_count, duckdb_create_config, duckdb_destroy_config,
39 duckdb_get_config_flag, duckdb_set_config, DuckDBSuccess,
40};
41
42use crate::error::ExtensionError;
43
44/// RAII wrapper for a `duckdb_config` handle.
45///
46/// Configuration options are set via [`set`][DbConfig::set] and consumed by
47/// passing [`as_raw`][DbConfig::as_raw] to `duckdb_open_ext`.
48/// The handle is destroyed automatically when [`DbConfig`] is dropped.
49#[must_use]
50pub struct DbConfig {
51 config: duckdb_config,
52}
53
54impl DbConfig {
55 /// Creates a new, empty `DuckDB` configuration object.
56 ///
57 /// # Errors
58 ///
59 /// Returns `ExtensionError` if `DuckDB` fails to allocate the config
60 /// (which is extremely rare in practice).
61 pub fn new() -> Result<Self, ExtensionError> {
62 let mut config: duckdb_config = std::ptr::null_mut();
63 // SAFETY: out_config is a valid pointer to a null duckdb_config.
64 let state = unsafe { duckdb_create_config(&raw mut config) };
65 if state == DuckDBSuccess {
66 Ok(Self { config })
67 } else {
68 Err(ExtensionError::new("duckdb_create_config failed"))
69 }
70 }
71
72 /// Sets a single configuration option.
73 ///
74 /// Common options include `"access_mode"` (`"READ_ONLY"` / `"READ_WRITE"`),
75 /// `"threads"` (number of CPU threads), and `"memory_limit"` (e.g. `"1GB"`).
76 /// Use [`DbConfig::flag_count`] / [`DbConfig::get_flag`] to enumerate all
77 /// available options at runtime.
78 ///
79 /// # Errors
80 ///
81 /// Returns `ExtensionError` if the option name or value is not recognised
82 /// by `DuckDB`.
83 ///
84 /// # Errors
85 ///
86 /// Returns `ExtensionError` if `name` or `value` contain interior null bytes,
87 /// or if `DuckDB` does not recognise the option.
88 pub fn set(self, name: &str, value: &str) -> Result<Self, ExtensionError> {
89 let c_name = CString::new(name).map_err(|_| {
90 ExtensionError::new(format!("config name '{name}' contains a null byte"))
91 })?;
92 let c_value = CString::new(value).map_err(|_| {
93 ExtensionError::new(format!("config value '{value}' contains a null byte"))
94 })?;
95 // SAFETY: self.config is a valid handle; c_name and c_value are NUL-terminated.
96 let state = unsafe { duckdb_set_config(self.config, c_name.as_ptr(), c_value.as_ptr()) };
97 if state == DuckDBSuccess {
98 Ok(self)
99 } else {
100 Err(ExtensionError::new(format!(
101 "duckdb_set_config failed for option '{name}' = '{value}'"
102 )))
103 }
104 }
105
106 /// Returns the total number of available configuration flags.
107 ///
108 /// Use this together with [`get_flag`][DbConfig::get_flag] to enumerate all
109 /// configuration options that `DuckDB` accepts.
110 #[must_use]
111 pub fn flag_count() -> usize {
112 // SAFETY: pure read of a DuckDB global table; no state required.
113 unsafe { duckdb_config_count() }
114 }
115
116 /// Returns the name and description for the configuration flag at `index`.
117 ///
118 /// `index` must be less than [`flag_count()`][DbConfig::flag_count].
119 ///
120 /// # Errors
121 ///
122 /// Returns `ExtensionError` if `index` is out of range or `DuckDB` fails
123 /// to retrieve the flag information.
124 pub fn get_flag(index: usize) -> Result<(String, String), ExtensionError> {
125 let mut name_ptr: *const std::os::raw::c_char = std::ptr::null();
126 let mut desc_ptr: *const std::os::raw::c_char = std::ptr::null();
127
128 // SAFETY: out-pointers are valid stack locations; DuckDB sets them to
129 // pointers into its own static tables (no allocation, no free needed).
130 let state = unsafe { duckdb_get_config_flag(index, &raw mut name_ptr, &raw mut desc_ptr) };
131
132 if state != DuckDBSuccess {
133 return Err(ExtensionError::new(format!(
134 "duckdb_get_config_flag({index}) failed"
135 )));
136 }
137
138 // SAFETY: DuckDB sets these pointers to valid NUL-terminated strings when
139 // the call succeeds.
140 let name = unsafe { CStr::from_ptr(name_ptr) }
141 .to_string_lossy()
142 .into_owned();
143 let desc = unsafe { CStr::from_ptr(desc_ptr) }
144 .to_string_lossy()
145 .into_owned();
146
147 Ok((name, desc))
148 }
149
150 /// Returns the underlying `duckdb_config` handle.
151 ///
152 /// Pass this to `duckdb_open_ext` to open a database with these settings.
153 ///
154 /// The handle remains owned by `DbConfig`; do **not** call
155 /// `duckdb_destroy_config` on the returned value.
156 #[must_use]
157 #[inline]
158 pub const fn as_raw(&self) -> duckdb_config {
159 self.config
160 }
161}
162
163impl Drop for DbConfig {
164 fn drop(&mut self) {
165 if !self.config.is_null() {
166 // SAFETY: self.config is a valid handle allocated by duckdb_create_config.
167 unsafe {
168 duckdb_destroy_config(&raw mut self.config);
169 }
170 }
171 }
172}
173
174// Note: DbConfig calls real DuckDB C API functions (duckdb_create_config,
175// duckdb_set_config, duckdb_config_count, etc.) which are only available when
176// the loadable-extension dispatch table has been populated — i.e. inside a
177// loaded extension or after `InMemoryDb::open()`. Direct unit tests are not
178// possible without that initialization (Pitfall P9). Verify DbConfig via E2E
179// SQLLogicTests in your extension's test suite.