Skip to main content

better_duck_core/
config.rs

1use crate::{
2    error::{Error, Result},
3    ffi,
4};
5use std::{
6    default::Default,
7    ffi::{CStr, CString},
8    os::raw::c_char,
9    ptr,
10};
11
12use strum::{Display, EnumString};
13
14/// duckdb access mode, default is Automatic
15#[derive(Debug, Eq, PartialEq, EnumString, Display)]
16pub enum AccessMode {
17    /// Access mode of the database AUTOMATIC
18    #[strum(to_string = "AUTOMATIC")]
19    Automatic,
20    /// Access mode of the database READ_ONLY
21    #[strum(to_string = "READ_ONLY")]
22    ReadOnly,
23    /// Access mode of the database READ_WRITE
24    #[strum(to_string = "READ_WRITE")]
25    ReadWrite,
26}
27
28/// duckdb default order, default is Asc
29#[derive(Debug, Eq, PartialEq, EnumString, Display)]
30pub enum DefaultOrder {
31    /// The order type, ASC
32    #[strum(to_string = "ASC")]
33    Asc,
34    /// The order type, DESC
35    #[strum(to_string = "DESC")]
36    Desc,
37}
38
39/// duckdb default null order, default is nulls first
40#[derive(Debug, Eq, PartialEq, EnumString, Display)]
41pub enum DefaultNullOrder {
42    /// Null ordering, NullsFirst
43    #[strum(to_string = "NULLS_FIRST")]
44    NullsFirst,
45    /// Null ordering, NullsLast
46    #[strum(to_string = "NULLS_LAST")]
47    NullsLast,
48}
49
50/// A DuckDB configuration flag's human-readable name and description, as reported
51/// by [`Config::flag`] / [`Config::flags`].
52#[derive(Debug, Clone, PartialEq, Eq)]
53pub struct ConfigFlag {
54    /// The flag's name (e.g. `"access_mode"`), usable as a key for [`Config::with`].
55    pub name: String,
56    /// A human-readable description of what the flag controls.
57    pub description: String,
58}
59
60/// The version string of the linked DuckDB library (e.g. `"v1.5.5"`).
61///
62/// Wraps `duckdb_library_version`, whose result is a static string owned by DuckDB
63/// (never freed); the bytes are copied into an owned `String`.
64#[must_use]
65pub fn library_version() -> String {
66    // SAFETY: `duckdb_library_version` returns a static, null-terminated string that
67    // must NOT be freed; we only read and copy it.
68    let ptr = unsafe { ffi::duckdb_library_version() };
69    if ptr.is_null() {
70        return String::new();
71    }
72    // SAFETY: `ptr` is a valid, non-null, null-terminated static C string.
73    unsafe { CStr::from_ptr(ptr) }.to_string_lossy().into_owned()
74}
75
76/// duckdb configuration
77/// Refer to <https://github.com/duckdb/duckdb/blob/master/src/main/config.cpp>
78#[derive(Default)]
79pub struct Config {
80    config: Option<ffi::duckdb_config>,
81}
82
83impl Config {
84    pub(crate) fn duckdb_config(&self) -> ffi::duckdb_config {
85        self.config.unwrap_or(std::ptr::null_mut() as ffi::duckdb_config)
86    }
87
88    /// The number of configuration flags DuckDB recognises.
89    ///
90    /// Flags are addressable by index in `0..flag_count()` via [`Config::flag`].
91    #[must_use]
92    pub fn flag_count() -> usize {
93        // SAFETY: `duckdb_config_count` takes no arguments and only reads a static table.
94        unsafe { ffi::duckdb_config_count() }
95    }
96
97    /// The name and description of the configuration flag at `index`, or `None` if
98    /// `index >= flag_count()`.
99    ///
100    /// Wraps `duckdb_get_config_flag`; the returned name/description are static
101    /// strings owned by DuckDB (never freed) and are copied into the [`ConfigFlag`].
102    #[must_use]
103    pub fn flag(index: usize) -> Option<ConfigFlag> {
104        let mut name: *const c_char = ptr::null();
105        let mut description: *const c_char = ptr::null();
106        // SAFETY: `name`/`description` are valid out-pointers. On success DuckDB writes
107        // pointers to static strings (which must NOT be freed); an out-of-range index
108        // returns `DuckDBError` and leaves them untouched.
109        let state = unsafe { ffi::duckdb_get_config_flag(index, &mut name, &mut description) };
110        if state != ffi::DuckDBSuccess || name.is_null() || description.is_null() {
111            return None;
112        }
113        // SAFETY: `name` is a valid, non-null, null-terminated static C string owned
114        // by DuckDB; we copy it out and never free it.
115        let name = unsafe { CStr::from_ptr(name) }.to_string_lossy().into_owned();
116        // SAFETY: `description` is likewise a valid static C string owned by DuckDB.
117        let description = unsafe { CStr::from_ptr(description) }.to_string_lossy().into_owned();
118        Some(ConfigFlag { name, description })
119    }
120
121    /// Iterates every configuration flag DuckDB recognises, in index order.
122    pub fn flags() -> impl Iterator<Item = ConfigFlag> {
123        (0..Self::flag_count()).filter_map(Self::flag)
124    }
125
126    /// enable autoload extensions
127    #[allow(unused)]
128    pub fn enable_autoload_extension(
129        mut self,
130        enabled: bool,
131    ) -> Result<Config> {
132        self.set("autoinstall_known_extensions", &(enabled as i32).to_string())?;
133        self.set("autoload_known_extensions", &(enabled as i32).to_string())?;
134        Ok(self)
135    }
136
137    /// Access mode of the database (`AUTOMATIC`, `READ_ONLY`, or `READ_WRITE`)
138    #[allow(unused)]
139    pub fn access_mode(
140        mut self,
141        mode: AccessMode,
142    ) -> Result<Config> {
143        self.set("access_mode", &mode.to_string())?;
144        Ok(self)
145    }
146
147    /// Metadata from DuckDB callers
148    #[allow(unused)]
149    pub fn custom_user_agent(
150        mut self,
151        custom_user_agent: &str,
152    ) -> Result<Config> {
153        self.set("custom_user_agent", custom_user_agent)?;
154        Ok(self)
155    }
156
157    /// The order type used when none is specified (`ASC` or `DESC`)
158    #[allow(unused)]
159    pub fn default_order(
160        mut self,
161        order: DefaultOrder,
162    ) -> Result<Config> {
163        self.set("default_order", &order.to_string())?;
164        Ok(self)
165    }
166
167    /// Null ordering used when none is specified (`NULLS_FIRST` or `NULLS_LAST`)
168    #[allow(unused)]
169    pub fn default_null_order(
170        mut self,
171        null_order: DefaultNullOrder,
172    ) -> Result<Config> {
173        self.set("default_null_order", &null_order.to_string())?;
174        Ok(self)
175    }
176
177    /// Allow the database to access external state (through e.g. COPY TO/FROM, CSV readers, pandas replacement scans, etc)
178    #[allow(unused)]
179    pub fn enable_external_access(
180        mut self,
181        enabled: bool,
182    ) -> Result<Config> {
183        self.set("enable_external_access", &enabled.to_string())?;
184        Ok(self)
185    }
186
187    /// Whether or not object cache is used to cache e.g. Parquet metadata
188    #[allow(unused)]
189    pub fn enable_object_cache(
190        mut self,
191        enabled: bool,
192    ) -> Result<Config> {
193        self.set("enable_object_cache", &enabled.to_string())?;
194        Ok(self)
195    }
196
197    /// Allow to load third-party duckdb extensions.
198    #[allow(unused)]
199    pub fn allow_unsigned_extensions(mut self) -> Result<Config> {
200        self.set("allow_unsigned_extensions", "true")?;
201        Ok(self)
202    }
203
204    /// The maximum memory of the system (e.g. 1GB)
205    #[allow(unused)]
206    pub fn max_memory(
207        mut self,
208        memory: &str,
209    ) -> Result<Config> {
210        self.set("max_memory", memory)?;
211        Ok(self)
212    }
213
214    /// The number of total threads used by the system
215    #[allow(unused)]
216    pub fn threads(
217        mut self,
218        thread_num: i64,
219    ) -> Result<Config> {
220        self.set("threads", &thread_num.to_string())?;
221        Ok(self)
222    }
223
224    /// Add any setting to the config. DuckDB will return an error if the setting is unknown or
225    /// otherwise invalid.
226    pub fn with(
227        mut self,
228        key: impl AsRef<str>,
229        value: impl AsRef<str>,
230    ) -> Result<Config> {
231        self.set(key.as_ref(), value.as_ref())?;
232        Ok(self)
233    }
234
235    fn set(
236        &mut self,
237        key: &str,
238        value: &str,
239    ) -> Result<()> {
240        if self.config.is_none() {
241            let mut config: ffi::duckdb_config = ptr::null_mut();
242            // SAFETY: `config` is a valid output pointer; `duckdb_create_config` initializes it.
243            let state = unsafe { ffi::duckdb_create_config(&mut config) };
244            if state != ffi::DuckDBSuccess {
245                return Err(Error::DuckDBFailure(
246                    ffi::Error::new(state),
247                    Some("failed to create duckdb_config".to_owned()),
248                ));
249            }
250            self.config = Some(config);
251        }
252
253        let c_key = CString::new(key)?;
254        let c_value = CString::new(value)?;
255        // SAFETY: `self.config` is Some — either it was just initialized above or it was
256        // already Some. `c_key` and `c_value` are valid null-terminated C strings that
257        // outlive this call. `duckdb_set_config` does not retain the string pointers.
258        let state = unsafe {
259            ffi::duckdb_set_config(
260                self.config.expect("config always initialized before this point"),
261                c_key.as_ptr() as *const c_char,
262                c_value.as_ptr() as *const c_char,
263            )
264        };
265        if state != ffi::DuckDBSuccess {
266            return Err(Error::DuckDBFailure(
267                ffi::Error::new(state),
268                Some(format!("set {key}:{value} error")),
269            ));
270        }
271        Ok(())
272    }
273}
274
275// SAFETY: `duckdb_config` is only mutated through `&mut self` methods (`set`), so no
276// two threads can touch it concurrently even after a move. DuckDB does not associate
277// the config handle with the thread that created it before it is consumed by
278// `duckdb_open_ext`/`duckdb_connect`.
279unsafe impl Send for Config {}
280
281impl Drop for Config {
282    fn drop(&mut self) {
283        // SAFETY: `cfg` is a valid duckdb_config created in `set` and not yet destroyed.
284        // `take()` sets `self.config` to None, so this runs at most once even if
285        // `Drop` is called multiple times (which Rust prevents, but belt-and-suspenders).
286        if let Some(mut cfg) = self.config.take() {
287            // SAFETY: `cfg` is a valid duckdb_config created in `set` and not yet
288            // destroyed. `take()` ensures this block runs at most once.
289            unsafe { ffi::duckdb_destroy_config(&mut cfg) };
290        }
291    }
292}
293
294#[cfg(test)]
295mod tests {
296    use super::*;
297
298    // Dummy error module for testing if not present
299    #[allow(dead_code)]
300    mod error {
301        use std::fmt;
302
303        #[derive(Debug)]
304        pub enum Error {
305            DuckDBFailure(super::ffi::Error, Option<String>),
306        }
307        pub type Result<T> = std::result::Result<T, Error>;
308        impl fmt::Display for Error {
309            fn fmt(
310                &self,
311                f: &mut fmt::Formatter<'_>,
312            ) -> fmt::Result {
313                write!(f, "{:?}", self)
314            }
315        }
316        impl std::error::Error for Error {}
317    }
318
319    #[test]
320    fn test_access_mode_enum() {
321        assert_eq!(AccessMode::Automatic.to_string(), "AUTOMATIC");
322        assert_eq!(AccessMode::ReadOnly.to_string(), "READ_ONLY");
323        assert_eq!(AccessMode::ReadWrite.to_string(), "READ_WRITE");
324        assert_eq!("AUTOMATIC".parse::<AccessMode>().unwrap(), AccessMode::Automatic);
325        assert_eq!("READ_ONLY".parse::<AccessMode>().unwrap(), AccessMode::ReadOnly);
326        assert_eq!("READ_WRITE".parse::<AccessMode>().unwrap(), AccessMode::ReadWrite);
327    }
328
329    #[test]
330    fn test_default_order_enum() {
331        assert_eq!(DefaultOrder::Asc.to_string(), "ASC");
332        assert_eq!(DefaultOrder::Desc.to_string(), "DESC");
333        assert_eq!("ASC".parse::<DefaultOrder>().unwrap(), DefaultOrder::Asc);
334        assert_eq!("DESC".parse::<DefaultOrder>().unwrap(), DefaultOrder::Desc);
335    }
336
337    #[test]
338    fn test_default_null_order_enum() {
339        assert_eq!(DefaultNullOrder::NullsFirst.to_string(), "NULLS_FIRST");
340        assert_eq!(DefaultNullOrder::NullsLast.to_string(), "NULLS_LAST");
341        assert_eq!(
342            "NULLS_FIRST".parse::<DefaultNullOrder>().unwrap(),
343            DefaultNullOrder::NullsFirst
344        );
345        assert_eq!("NULLS_LAST".parse::<DefaultNullOrder>().unwrap(), DefaultNullOrder::NullsLast);
346    }
347
348    #[test]
349    fn test_enable_autoload_extension() {
350        let config = Config::default().enable_autoload_extension(true);
351        assert!(config.is_ok());
352        let config = Config::default().enable_autoload_extension(false);
353        assert!(config.is_ok());
354    }
355
356    #[test]
357    fn test_access_mode_method() {
358        let config = Config::default().access_mode(AccessMode::ReadOnly);
359        assert!(config.is_ok());
360    }
361
362    #[test]
363    fn test_custom_user_agent() {
364        let config = Config::default().custom_user_agent("my-agent/1.0");
365        assert!(config.is_ok());
366    }
367
368    #[test]
369    fn test_default_order_method() {
370        let config = Config::default().default_order(DefaultOrder::Desc);
371        assert!(config.is_ok());
372    }
373
374    #[test]
375    fn test_default_null_order_method() {
376        let config = Config::default().default_null_order(DefaultNullOrder::NullsLast);
377        assert!(config.is_ok());
378    }
379
380    #[test]
381    fn test_enable_external_access() {
382        let config = Config::default().enable_external_access(true);
383        assert!(config.is_ok());
384    }
385
386    #[test]
387    fn test_enable_object_cache() {
388        let config = Config::default().enable_object_cache(true);
389        assert!(config.is_ok());
390    }
391
392    #[test]
393    fn test_allow_unsigned_extensions() {
394        let config = Config::default().allow_unsigned_extensions();
395        assert!(config.is_ok());
396    }
397
398    #[test]
399    fn test_max_memory() {
400        let config = Config::default().max_memory("512MB");
401        assert!(config.is_ok());
402    }
403
404    #[test]
405    fn test_threads() {
406        let config = Config::default().threads(8);
407        assert!(config.is_ok());
408    }
409
410    #[test]
411    fn test_with() {
412        let config = Config::default().with("some_key", "some_value");
413        assert!(config.is_ok());
414    }
415
416    #[test]
417    fn test_set_multiple_options() {
418        let config = Config::default()
419            .enable_autoload_extension(true)
420            .and_then(|c| c.access_mode(AccessMode::ReadWrite))
421            .and_then(|c| c.max_memory("1GB"))
422            .and_then(|c| c.threads(4));
423        assert!(config.is_ok());
424    }
425
426    #[test]
427    fn test_config_drop() {
428        // Just ensure drop does not panic
429        let config = Config::default().enable_autoload_extension(true).unwrap();
430        drop(config);
431    }
432
433    #[test]
434    fn test_rejects_interior_nul_in_key_or_value() {
435        let key_error = Config::default().with("bad\0key", "value").err().unwrap();
436        assert!(matches!(key_error, Error::NulError(_)));
437
438        let value_error = Config::default().with("threads", "1\0extra").err().unwrap();
439        assert!(matches!(value_error, Error::NulError(_)));
440    }
441
442    #[test]
443    fn config_is_send() {
444        fn assert_send<T: Send>() {}
445        assert_send::<Config>();
446    }
447
448    #[test]
449    fn library_version_is_reported() {
450        let version = super::library_version();
451        assert!(!version.is_empty(), "library version must not be empty");
452        // DuckDB reports versions like "v1.5.5"; at minimum it should contain a digit.
453        assert!(version.chars().any(|c| c.is_ascii_digit()), "version {version:?} has no digit");
454    }
455
456    #[test]
457    fn config_flags_are_discoverable() {
458        let count = Config::flag_count();
459        assert!(count > 0, "DuckDB must expose at least one config flag");
460
461        // Index 0 is in range; one past the end is not.
462        assert!(Config::flag(0).is_some());
463        assert!(Config::flag(count).is_none(), "index == count must be out of range");
464
465        // Iterating yields exactly `count` flags, each with a non-empty name.
466        let flags: Vec<_> = Config::flags().collect();
467        assert_eq!(flags.len(), count);
468        assert!(flags.iter().all(|f| !f.name.is_empty()));
469
470        // A stable, well-known flag is present and usable as a `with` key.
471        let threads = flags.iter().find(|f| f.name == "threads").expect("`threads` flag missing");
472        assert!(!threads.description.is_empty());
473        assert!(Config::default().with(&threads.name, "2").is_ok());
474    }
475}