use std::sync::RwLock;
use rudb_common::{Error, Memory, Result, Value, human};
use rudb_parse::ast::Scope;
use crate::config::{Config, parse_size};
#[derive(Debug)]
pub(crate) struct Settings {
defaults: Config,
current: RwLock<Config>,
disabled: RwLock<String>,
}
impl Settings {
pub(crate) const NAMES: [&'static str; 3] = ["disabled_optimizers", "memory_limit", "threads"];
pub(crate) fn new(config: Config) -> Self {
Self {
defaults: config,
current: RwLock::new(config),
disabled: RwLock::new(String::new()),
}
}
pub(crate) fn disabled_optimizers(&self) -> String {
self.disabled.read().unwrap_or_else(|held| held.into_inner()).clone()
}
pub(crate) fn config(&self) -> Config {
*self.current.read().unwrap_or_else(|held| held.into_inner())
}
pub(crate) fn defaults(&self) -> Config {
self.defaults
}
pub(crate) fn apply(
&self,
memory: &Memory,
name: &str,
scope: Scope,
value: Option<&Value>,
) -> Result<()> {
let word = if value.is_some() { "SET" } else { "RESET" };
let verb = if value.is_some() { "set" } else { "reset" };
match scope {
Scope::Local => {
return Err(Error::not_implemented(format!("{word} LOCAL is not implemented.")));
}
Scope::Session => {
return Err(Error::catalog(format!("option \"{name}\" cannot be {verb} locally")));
}
Scope::Global | Scope::Unwritten => {}
}
if !Self::NAMES.contains(&name) {
let known: Vec<String> =
Self::NAMES.iter().map(|known| format!("\"{known}\"")).collect();
return Err(Error::catalog(format!(
"unrecognized configuration parameter \"{name}\"\n\nDid you mean: {}",
known.join(", ")
)));
}
match name {
"disabled_optimizers" => {
let text = match value {
None => String::new(),
Some(value) => text_of(value),
};
rudb_opt::pass::Context::without(&text)?;
*self.disabled.write().unwrap_or_else(|held| held.into_inner()) = text;
}
"memory_limit" => {
let limit = match value {
None => self.defaults.memory_limit(),
Some(value) => bytes_of(&text_of(value))?,
};
let config = self.config();
self.replace(match limit {
Some(bytes) => config.with_memory_limit(bytes),
None => config.with_no_memory_limit(),
});
memory.set_limit(limit);
}
"threads" => {
let threads = match value {
None => self.defaults.threads(),
Some(value) => threads_of(value)?,
};
self.replace(self.config().with_threads(threads)?);
}
_ => unreachable!("the name was one of NAMES a moment ago"),
}
Ok(())
}
pub(crate) fn value(&self, name: &str) -> Result<String> {
let config = self.config();
match name {
"disabled_optimizers" => Ok(self.disabled_optimizers()),
"memory_limit" => Ok(config.memory_limit().map_or("unlimited".to_string(), human)),
"threads" => Ok(config.threads().to_string()),
_ => {
let known: Vec<String> =
Self::NAMES.iter().map(|known| format!("\"{known}\"")).collect();
Err(Error::catalog(format!(
"unrecognized configuration parameter \"{name}\"\n\nDid you mean: {}",
known.join(", ")
)))
}
}
}
fn replace(&self, config: Config) {
*self.current.write().unwrap_or_else(|held| held.into_inner()) = config;
}
}
fn text_of(value: &Value) -> String {
match value {
Value::Varchar(text) => text.clone(),
Value::Null => String::new(),
other => other.to_string(),
}
}
fn threads_of(value: &Value) -> Result<usize> {
let count = match value {
Value::TinyInt(count) => i128::from(*count),
Value::SmallInt(count) => i128::from(*count),
Value::Integer(count) => i128::from(*count),
Value::BigInt(count) => i128::from(*count),
Value::HugeInt(count) => *count,
Value::Varchar(text) => text.trim().parse::<i128>().map_err(|_| {
Error::invalid_input(format!(
"Failed to cast value: Could not convert string '{text}' to INT64"
))
})?,
other => {
return Err(Error::invalid_input(format!(
"Failed to cast value: Could not convert {} to INT64",
other.logical_type()
)));
}
};
usize::try_from(count)
.ok()
.filter(|count| *count >= 1)
.ok_or_else(|| Error::syntax("Must have at least 1 thread!"))
}
fn bytes_of(text: &str) -> Result<Option<u64>> {
let trimmed = text.trim();
if trimmed == "-1" {
return Ok(None);
}
if !trimmed.contains(|character: char| character.is_ascii_alphabetic()) {
return Err(Error::parser(
"Unknown unit for memory: '' (expected: KB, MB, GB, TB for 1000^i units or KiB, MiB, GiB, TiB for 1024^i units)",
));
}
parse_size(trimmed).map(Some)
}
#[cfg(test)]
mod tests {
use super::{Settings, bytes_of};
use crate::config::Config;
use rudb_common::{Memory, Value};
use rudb_parse::ast::Scope;
fn settings() -> (Settings, Memory) {
(Settings::new(Config::new().with_no_memory_limit()), Memory::unlimited())
}
#[test]
fn a_size_is_the_number_times_the_unit_and_the_unit_is_required() {
assert_eq!(bytes_of("1GB").expect("a size"), Some(1_000_000_000));
assert_eq!(bytes_of("1GiB").expect("a size"), Some(1 << 30));
assert_eq!(bytes_of("1.5gb").expect("a lower case size"), Some(1_500_000_000));
assert_eq!(bytes_of("100 MB").expect("a spaced size"), Some(100_000_000));
assert_eq!(bytes_of("-1").expect("the way to say no limit"), None);
let error = bytes_of("0").expect_err("a number with no unit");
assert!(error.message().starts_with("Unknown unit for memory: ''"), "{}", error.message());
let error = bytes_of("abc").expect_err("not a number at all");
assert_eq!(error.message(), "Memory must have a number (e.g. 1GB)");
}
#[test]
fn setting_the_memory_limit_moves_the_budget_every_query_is_held_to() {
let (settings, memory) = settings();
assert_eq!(memory.limit(), None);
let value = Value::Varchar("1GiB".into());
settings.apply(&memory, "memory_limit", Scope::Unwritten, Some(&value)).expect("a size");
assert_eq!(memory.limit(), Some(1 << 30));
assert_eq!(settings.value("memory_limit").expect("a setting"), "1.0 GiB");
settings.apply(&memory, "memory_limit", Scope::Unwritten, None).expect("a reset");
assert_eq!(memory.limit(), None, "reset goes back to what the database was opened with");
}
#[test]
fn a_pass_that_is_not_a_pass_is_refused_by_the_statement_that_named_it() {
let (settings, memory) = settings();
let value = Value::Varchar("bogus".into());
let error = settings
.apply(&memory, "disabled_optimizers", Scope::Unwritten, Some(&value))
.expect_err("not a pass");
assert_eq!(error.code().duckdb_name(), "Parser Error");
assert_eq!(settings.disabled_optimizers(), "", "a refused set changed nothing");
}
#[test]
fn a_name_that_is_not_a_setting_says_so_with_the_names_there_are() {
let (settings, memory) = settings();
let error =
settings.apply(&memory, "bogus", Scope::Unwritten, None).expect_err("not a setting");
assert_eq!(error.code().duckdb_name(), "Catalog Error");
assert!(
error.message().starts_with("unrecognized configuration parameter \"bogus\""),
"{}",
error.message()
);
assert!(error.message().contains("\"memory_limit\""), "{}", error.message());
}
#[test]
fn the_two_scopes_this_database_does_not_have_are_two_different_sentences() {
let (settings, memory) = settings();
let value = Value::BigInt(2);
let error = settings
.apply(&memory, "threads", Scope::Local, Some(&value))
.expect_err("no local scope");
assert_eq!(error.message(), "SET LOCAL is not implemented.");
let error = settings
.apply(&memory, "threads", Scope::Session, Some(&value))
.expect_err("no session copy");
assert_eq!(error.message(), "option \"threads\" cannot be set locally");
let error =
settings.apply(&memory, "threads", Scope::Local, None).expect_err("no local scope");
assert_eq!(error.message(), "RESET LOCAL is not implemented.");
let error =
settings.apply(&memory, "threads", Scope::Session, None).expect_err("no session copy");
assert_eq!(error.message(), "option \"threads\" cannot be reset locally");
}
#[test]
fn a_thread_count_is_a_whole_number_of_at_least_one() {
let (settings, memory) = settings();
settings
.apply(&memory, "threads", Scope::Global, Some(&Value::BigInt(4)))
.expect("four threads");
assert_eq!(settings.value("threads").expect("a setting"), "4");
let error = settings
.apply(&memory, "threads", Scope::Global, Some(&Value::BigInt(0)))
.expect_err("no threads at all");
assert_eq!(error.message(), "Must have at least 1 thread!");
let text = Value::Varchar("abc".into());
let error = settings
.apply(&memory, "threads", Scope::Global, Some(&text))
.expect_err("not a number");
assert_eq!(
error.message(),
"Failed to cast value: Could not convert string 'abc' to INT64"
);
}
}