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};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConfigOptionScope {
Local,
Session,
Global,
}
impl ConfigOptionScope {
#[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,
}
}
}
#[must_use]
pub struct ConfigOptionBuilder {
name: CString,
description: Option<CString>,
option_type: Option<TypeId>,
default_value: Option<CString>,
scope: ConfigOptionScope,
}
impl ConfigOptionBuilder {
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,
})
}
#[must_use]
pub fn name(&self) -> &str {
self.name.to_str().unwrap_or("")
}
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)
}
pub const fn option_type(mut self, type_id: TypeId) -> Self {
self.option_type = Some(type_id);
self
}
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)
}
pub const fn scope(mut self, scope: ConfigOptionScope) -> Self {
self.scope = scope;
self
}
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);
let option: duckdb_config_option = unsafe { duckdb_create_config_option() };
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 {
unsafe {
duckdb_config_option_set_description(option, desc.as_ptr());
}
}
if let Some(ref val) = self.default_value {
let dv = unsafe { duckdb_create_varchar(val.as_ptr()) };
unsafe {
duckdb_config_option_set_default_value(option, dv);
}
let mut dv_mut = dv;
unsafe {
duckdb_destroy_value(&raw mut dv_mut);
}
}
let result = unsafe { duckdb_register_config_option(con, option) };
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);
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() {
let builder = ConfigOptionBuilder::try_new("opt").unwrap();
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() {
let _builder = ConfigOptionBuilder::try_new("my_ext_threshold")
.unwrap()
.description("Maximum threshold")
.unwrap()
.option_type(TypeId::BigInt)
.default_value("100")
.unwrap()
.scope(ConfigOptionScope::Global);
}
}