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#[derive(Debug, Eq, PartialEq, EnumString, Display)]
16pub enum AccessMode {
17 #[strum(to_string = "AUTOMATIC")]
19 Automatic,
20 #[strum(to_string = "READ_ONLY")]
22 ReadOnly,
23 #[strum(to_string = "READ_WRITE")]
25 ReadWrite,
26}
27
28#[derive(Debug, Eq, PartialEq, EnumString, Display)]
30pub enum DefaultOrder {
31 #[strum(to_string = "ASC")]
33 Asc,
34 #[strum(to_string = "DESC")]
36 Desc,
37}
38
39#[derive(Debug, Eq, PartialEq, EnumString, Display)]
41pub enum DefaultNullOrder {
42 #[strum(to_string = "NULLS_FIRST")]
44 NullsFirst,
45 #[strum(to_string = "NULLS_LAST")]
47 NullsLast,
48}
49
50#[derive(Debug, Clone, PartialEq, Eq)]
53pub struct ConfigFlag {
54 pub name: String,
56 pub description: String,
58}
59
60#[must_use]
65pub fn library_version() -> String {
66 let ptr = unsafe { ffi::duckdb_library_version() };
69 if ptr.is_null() {
70 return String::new();
71 }
72 unsafe { CStr::from_ptr(ptr) }.to_string_lossy().into_owned()
74}
75
76#[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 #[must_use]
92 pub fn flag_count() -> usize {
93 unsafe { ffi::duckdb_config_count() }
95 }
96
97 #[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 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 let name = unsafe { CStr::from_ptr(name) }.to_string_lossy().into_owned();
116 let description = unsafe { CStr::from_ptr(description) }.to_string_lossy().into_owned();
118 Some(ConfigFlag { name, description })
119 }
120
121 pub fn flags() -> impl Iterator<Item = ConfigFlag> {
123 (0..Self::flag_count()).filter_map(Self::flag)
124 }
125
126 #[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 #[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 #[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 #[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 #[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(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 #[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(unused)]
199 pub fn allow_unsigned_extensions(mut self) -> Result<Config> {
200 self.set("allow_unsigned_extensions", "true")?;
201 Ok(self)
202 }
203
204 #[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 #[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 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 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 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
275unsafe impl Send for Config {}
280
281impl Drop for Config {
282 fn drop(&mut self) {
283 if let Some(mut cfg) = self.config.take() {
287 unsafe { ffi::duckdb_destroy_config(&mut cfg) };
290 }
291 }
292}
293
294#[cfg(test)]
295mod tests {
296 use super::*;
297
298 #[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 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 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 assert!(Config::flag(0).is_some());
463 assert!(Config::flag(count).is_none(), "index == count must be out of range");
464
465 let flags: Vec<_> = Config::flags().collect();
467 assert_eq!(flags.len(), count);
468 assert!(flags.iter().all(|f| !f.name.is_empty()));
469
470 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}