Skip to main content

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.