pub(crate) mod dynamic;
use std::collections::HashMap;
use std::fmt;
use std::path::PathBuf;
use std::str::FromStr;
use std::sync::LazyLock;
use std::time::Duration;
use crate::iam::file::extract_allowed_paths;
use crate::str::ParseBytes;
pub const SERVER_NAME: &str = "SurrealDB";
pub const ID_CHARS: [char; 36] = [
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i',
'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
];
pub const PROTECTED_PARAM_NAMES: &[&str] = &["access", "auth", "token", "session"];
pub const NOTIFICATIONS_CHANNEL_SIZE: usize = 15_000;
#[derive(Clone, Debug)]
pub struct ConfigMap {
values: HashMap<String, String>,
}
impl Default for ConfigMap {
fn default() -> Self {
Self::empty()
}
}
impl ConfigMap {
pub fn empty() -> Self {
ConfigMap {
values: HashMap::new(),
}
}
pub fn with_key_value<K, V>(mut self, key: K, value: V) -> Self
where
String: From<K>,
String: From<V>,
{
self.values.insert(key.into(), value.into());
self
}
pub fn from_env() -> Self {
Self::from_env_prefix("SURREAL_")
}
pub fn from_env_prefix(prefix: &str) -> Self {
let mut values = HashMap::new();
for (k, v) in std::env::vars() {
let Some(x) = k.strip_prefix(prefix) else {
continue;
};
let key_name = x.to_lowercase();
values.insert(key_name, v);
}
ConfigMap {
values,
}
}
pub fn map_keys<F: FnMut(String) -> String>(self, mut f: F) -> Self {
Self {
values: self.values.into_iter().map(|(k, v)| (f(k), v)).collect(),
}
}
pub fn from_config_string(s: &str) -> Self {
let values = s
.split('&')
.filter_map(|x| {
let (k, v) = x.split_once('=')?;
Some((k.to_lowercase(), v.to_string()))
})
.collect();
ConfigMap {
values,
}
}
pub fn join(mut self, other: ConfigMap) -> Self {
for (k, v) in other.values {
self.values.insert(k, v);
}
self
}
pub fn load<C: Config>(&self) -> C {
let mut def = C::default();
def.parse(self);
def
}
pub fn parse_key<S: FromStr>(&self, key: &str, value: &mut S) -> &Self {
self.parse_key_with(key, value, |x| S::from_str(x).ok())
}
pub fn parse_key_option<S: FromStr>(&self, key: &str, value: &mut Option<S>) -> &Self {
self.parse_key_with(key, value, |x| S::from_str(x).ok().map(Some))
}
pub fn parse_key_bool(&self, key: &str, value: &mut bool) -> &Self {
self.parse_key_with(key, value, |x| {
if x.eq_ignore_ascii_case("true") || x == "1" {
Some(true)
} else if x.eq_ignore_ascii_case("false") || x == "0" {
Some(false)
} else {
None
}
})
}
pub fn parse_key_with<R, F: FnOnce(&str) -> Option<R>>(
&self,
key: &str,
value: &mut R,
f: F,
) -> &Self {
let Some(v) = self.values.get(key) else {
return self;
};
let Some(v) = f(v) else {
warn!("Could not parse configuration value for key `{}`", key.to_uppercase());
return self;
};
*value = v;
self
}
pub fn has_key(&self, key: &str) -> bool {
self.values.contains_key(key)
}
}
pub trait Config: Default {
fn parse(&mut self, map: &ConfigMap);
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum LiveQueryEngine {
#[default]
Inline,
Router,
}
impl fmt::Display for LiveQueryEngine {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Inline => f.write_str("inline"),
Self::Router => f.write_str("router"),
}
}
}
impl FromStr for LiveQueryEngine {
type Err = String;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"inline" => Ok(Self::Inline),
"router" => Ok(Self::Router),
v => Err(format!("Invalid live query engine: '{v}'. Expected 'inline' or 'router'")),
}
}
}
#[derive(Debug)]
pub struct CommonConfig {
pub memory_threshold: usize,
pub max_concurrent_tasks: usize,
pub max_computation_depth: u32,
pub max_object_parsing_depth: u32,
pub max_query_parsing_depth: u32,
pub max_expression_parsing_depth: u32,
pub idiom_recursion_limit: u32,
pub regex_size_limit: usize,
pub regex_cache_size: usize,
pub transaction_cache_size: usize,
pub datastore_cache_size: usize,
pub export_batch_size: u32,
pub fts_doc_ids_batch_size: u32,
pub operator_buffer_size: usize,
pub scan_batch_size: usize,
pub max_order_limit_priority_queue_size: u32,
pub topk_threshold_pushdown_enabled: bool,
pub gql_max_join_build_rows: usize,
pub gql_max_path_rows: usize,
pub gql_max_output_rows: usize,
pub scripting_max_stack_size: usize,
pub scripting_max_memory_limit: usize,
pub scripting_max_time_limit: Duration,
pub max_http_redirects: usize,
pub max_http_idle_connections_per_host: usize,
pub max_http_idle_connections: usize,
pub http_idle_timeout_secs: u64,
pub http_connect_timeout_secs: u64,
pub insecure_forward_access_errors: bool,
pub external_sorting_buffer_limit: usize,
pub generation_allocation_limit: usize,
pub string_similarity_limit: usize,
pub file_allowlist: Vec<PathBuf>,
pub global_bucket: Option<String>,
pub global_bucket_enforced: bool,
pub surrealdb_user_agent: String,
pub hnsw_cache_size: u64,
pub diskann_cache_size: u64,
pub surrealism_cache_size: usize,
pub surrealism_max_memory: Option<usize>,
pub surrealism_max_execution_time: Option<u64>,
pub surrealism_max_kv_entries: Option<usize>,
pub surrealism_max_kv_value_bytes: Option<usize>,
pub surrealism_max_fs_bytes: u64,
pub surrealism_max_pool_size: usize,
pub surrealism_log_level: String,
pub live_query_engine: LiveQueryEngine,
pub live_query_retention: Duration,
}
impl Default for CommonConfig {
fn default() -> Self {
Self {
memory_threshold: 0,
#[cfg(not(target_family = "wasm"))]
max_concurrent_tasks: 64,
#[cfg(target_family = "wasm")]
max_concurrent_tasks: 1,
max_computation_depth: 120,
max_object_parsing_depth: 100,
max_query_parsing_depth: 20,
max_expression_parsing_depth: 128,
idiom_recursion_limit: 256,
regex_size_limit: 10 * 1024 * 1024,
regex_cache_size: 1_000,
transaction_cache_size: 512,
datastore_cache_size: 1_000,
export_batch_size: 1000,
fts_doc_ids_batch_size: 1000,
operator_buffer_size: 2,
scan_batch_size: crate::exec::operators::scan::common::DEFAULT_SCAN_BATCH_SIZE,
max_order_limit_priority_queue_size: 1000,
topk_threshold_pushdown_enabled: true,
gql_max_join_build_rows: 1_000_000,
gql_max_path_rows: 1_000_000,
gql_max_output_rows: 1_000_000,
scripting_max_stack_size: 256 * 1024,
scripting_max_memory_limit: 2 << 20,
scripting_max_time_limit: Duration::from_secs(5),
max_http_redirects: 10,
max_http_idle_connections_per_host: 128,
max_http_idle_connections: 1000,
http_idle_timeout_secs: 90,
http_connect_timeout_secs: 30,
insecure_forward_access_errors: false,
external_sorting_buffer_limit: 50_000,
generation_allocation_limit: 2 << 20,
string_similarity_limit: 16384,
file_allowlist: Vec::new(),
global_bucket: None,
global_bucket_enforced: false,
surrealdb_user_agent: "SurrealDB".to_string(),
hnsw_cache_size: 256 * 1024 * 1024,
diskann_cache_size: 256 * 1024 * 1024,
surrealism_cache_size: 100,
surrealism_max_memory: None,
surrealism_max_execution_time: None,
surrealism_max_kv_entries: None,
surrealism_max_kv_value_bytes: None,
surrealism_max_fs_bytes: 100 * 1024 * 1024,
surrealism_max_pool_size: 8,
surrealism_log_level: "debug".to_string(),
live_query_engine: LiveQueryEngine::Inline,
live_query_retention: Duration::from_secs(3600),
}
}
}
impl Config for CommonConfig {
fn parse(&mut self, map: &ConfigMap) {
map.parse_key_with("memory_threshold", &mut self.memory_threshold, parse_memory_threshold)
.parse_key("max_concurrent_tasks", &mut self.max_concurrent_tasks)
.parse_key("max_computation_depth", &mut self.max_computation_depth)
.parse_key("max_object_parsing_depth", &mut self.max_object_parsing_depth)
.parse_key("max_query_parsing_depth", &mut self.max_query_parsing_depth)
.parse_key("max_expression_parsing_depth", &mut self.max_expression_parsing_depth)
.parse_key("regex_size_limit", &mut self.regex_size_limit)
.parse_key("regex_cache_size", &mut self.regex_cache_size)
.parse_key("transaction_cache_size", &mut self.transaction_cache_size)
.parse_key("datastore_cache_size", &mut self.datastore_cache_size)
.parse_key("surrealism_cache_size", &mut self.surrealism_cache_size)
.parse_key("export_batch_size", &mut self.export_batch_size)
.parse_key("fts_doc_ids_batch_size", &mut self.fts_doc_ids_batch_size)
.parse_key("operator_buffer_size", &mut self.operator_buffer_size)
.parse_key("scan_batch_size", &mut self.scan_batch_size)
.parse_key(
"max_order_limit_priority_queue_size",
&mut self.max_order_limit_priority_queue_size,
)
.parse_key("topk_threshold_pushdown_enabled", &mut self.topk_threshold_pushdown_enabled)
.parse_key("gql_max_join_build_rows", &mut self.gql_max_join_build_rows)
.parse_key("gql_max_path_rows", &mut self.gql_max_path_rows)
.parse_key("gql_max_output_rows", &mut self.gql_max_output_rows)
.parse_key("scripting_max_stack_size", &mut self.scripting_max_stack_size)
.parse_key("scripting_max_memory_limit", &mut self.scripting_max_memory_limit)
.parse_key_with("scripting_max_time_limit", &mut self.scripting_max_time_limit, |x| {
x.parse().map(Duration::from_millis).ok()
})
.parse_key("max_http_redirects", &mut self.max_http_redirects)
.parse_key(
"max_http_idle_connections_per_host",
&mut self.max_http_idle_connections_per_host,
)
.parse_key("max_http_idle_connections", &mut self.max_http_idle_connections)
.parse_key("http_idle_timeout_secs", &mut self.http_idle_timeout_secs)
.parse_key("http_connect_timeout_secs", &mut self.http_connect_timeout_secs)
.parse_key("insecure_forward_access_errors", &mut self.insecure_forward_access_errors)
.parse_key("external_sorting_buffer_limit", &mut self.external_sorting_buffer_limit)
.parse_key_with(
"generation_allocation_limit",
&mut self.generation_allocation_limit,
|x| x.parse::<usize>().ok().map(|x| 2 << x.min(28)),
)
.parse_key("string_similarity_limit", &mut self.string_similarity_limit)
.parse_key("hnsw_cache_size", &mut self.hnsw_cache_size)
.parse_key("diskann_cache_size", &mut self.diskann_cache_size)
.parse_key_with("file_allowlist", &mut self.file_allowlist, |x| {
Some(extract_allowed_paths(x, true, "file"))
})
.parse_key("surrealdb_user_agent", &mut self.surrealdb_user_agent)
.parse_key_option("surrealism_max_memory", &mut self.surrealism_max_memory)
.parse_key_option(
"surrealism_max_execution_time",
&mut self.surrealism_max_execution_time,
)
.parse_key_option("surrealism_max_kv_entries", &mut self.surrealism_max_kv_entries)
.parse_key_option(
"surrealism_max_kv_value_bytes",
&mut self.surrealism_max_kv_value_bytes,
)
.parse_key("surrealism_max_fs_bytes", &mut self.surrealism_max_fs_bytes)
.parse_key_with("surrealism_log_level", &mut self.surrealism_log_level, |s| {
Some(s.to_string())
})
.parse_key("live_query_engine", &mut self.live_query_engine)
.parse_key_with("live_query_retention", &mut self.live_query_retention, |x| {
crate::kvs::config::parse_duration(x).ok()
});
}
}
pub static MEMORY_THRESHOLD: LazyLock<usize> = LazyLock::new(|| {
std::env::var("SURREAL_MEMORY_THRESHOLD")
.ok()
.and_then(|x| parse_memory_threshold(&x))
.unwrap_or(0)
});
fn parse_memory_threshold(value: &str) -> Option<usize> {
value.parse_bytes::<usize>().ok().map(|x| match x {
0 => 0,
x => x.max(1024 * 1024),
})
}
pub static HNSW_BUILD_SEED: LazyLock<Option<u64>> = LazyLock::new(|| {
std::env::var("SURREAL_HNSW_BUILD_SEED").ok().and_then(|s| s.parse::<u64>().ok())
});
pub static RAND_SEED: LazyLock<Option<u64>> =
LazyLock::new(|| match std::env::var("SURREAL_RAND_SEED") {
Ok(v) => match v.parse::<u64>() {
Ok(seed) => Some(seed),
Err(_) => {
warn!("Ignoring invalid SURREAL_RAND_SEED value `{v}`; expected a u64");
None
}
},
Err(_) => None,
});
pub static DISKANN_FILTER_PREFETCH_MIN_CHUNK: LazyLock<usize> = LazyLock::new(|| {
std::env::var("SURREAL_DISKANN_FILTER_PREFETCH_MIN_CHUNK")
.ok()
.and_then(|s| s.parse::<usize>().ok())
.filter(|n| *n > 0)
.unwrap_or(64)
});
pub static DISKANN_FILTER_PREFETCH_MAX_CHUNK: LazyLock<usize> = LazyLock::new(|| {
std::env::var("SURREAL_DISKANN_FILTER_PREFETCH_MAX_CHUNK")
.ok()
.and_then(|s| s.parse::<usize>().ok())
.filter(|n| *n > 0)
.unwrap_or(4096)
});
pub static GENERATION_ALLOCATION_LIMIT: LazyLock<usize> = LazyLock::new(|| {
let n = std::env::var("SURREAL_GENERATION_ALLOCATION_LIMIT")
.map(|s| s.parse::<u32>().unwrap_or(20))
.unwrap_or(20);
2usize.pow(n.min(28))
});
pub static STRING_SIMILARITY_LIMIT: LazyLock<usize> =
lazy_env_parse!("SURREAL_STRING_SIMILARITY_LIMIT", usize, 16384);
pub static REGEX_SIZE_LIMIT: LazyLock<usize> =
lazy_env_parse!("SURREAL_REGEX_SIZE_LIMIT", usize, 10 * 1024 * 1024);
pub static REGEX_CACHE_SIZE: LazyLock<usize> =
lazy_env_parse!("SURREAL_REGEX_CACHE_SIZE", usize, 1_000);
pub static SURREALISM_MAX_POOL_SIZE: LazyLock<usize> =
lazy_env_parse!("SURREAL_SURREALISM_MAX_POOL_SIZE", usize, 8);
#[cfg(any(feature = "kv-mem", feature = "kv-rocksdb", feature = "kv-surrealkv"))]
#[cfg(not(target_family = "wasm"))]
pub static KVS_THREADPOOL_SIZE: LazyLock<usize> = LazyLock::new(|| {
let default = || {
let cores = num_cpus::get();
if cores >= 16 {
cores
} else {
16
}
};
const MINIMUM_OVERRIDE: usize = 4;
match std::env::var("SURREAL_KVS_THREADPOOL_SIZE") {
Err(_) => default(),
Ok(s) if s.is_empty() => default(),
Ok(s) => match s.parse::<usize>() {
Ok(n) if n >= MINIMUM_OVERRIDE => n,
Ok(n) => {
tracing::warn!(
target: "surrealdb::kvs::threadpool",
"SURREAL_KVS_THREADPOOL_SIZE={n} is below the minimum of {MINIMUM_OVERRIDE}; using default",
);
default()
}
Err(_) => {
tracing::warn!(
target: "surrealdb::kvs::threadpool",
"SURREAL_KVS_THREADPOOL_SIZE={s:?} is not a valid integer; using default",
);
default()
}
},
}
});
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn topk_threshold_pushdown_kill_switch_parses() {
let mut config = CommonConfig::default();
assert!(config.topk_threshold_pushdown_enabled, "feature defaults on");
let map = ConfigMap::empty().with_key_value("topk_threshold_pushdown_enabled", "false");
config.parse(&map);
assert!(!config.topk_threshold_pushdown_enabled, "config map disables the feature");
}
#[test]
fn gql_match_limits_parse_from_config_map() {
let mut config = CommonConfig::default();
assert_eq!(config.gql_max_join_build_rows, 1_000_000);
assert_eq!(config.gql_max_path_rows, 1_000_000);
assert_eq!(config.gql_max_output_rows, 1_000_000);
let map = ConfigMap::empty()
.with_key_value("gql_max_join_build_rows", "5")
.with_key_value("gql_max_path_rows", "7")
.with_key_value("gql_max_output_rows", "9");
config.parse(&map);
assert_eq!(config.gql_max_join_build_rows, 5);
assert_eq!(config.gql_max_path_rows, 7);
assert_eq!(config.gql_max_output_rows, 9);
}
#[test]
fn memory_threshold_configmap_parses_byte_suffixes() {
let mut config = CommonConfig::default();
assert_eq!(config.memory_threshold, 0, "default is no threshold");
let map = ConfigMap::empty().with_key_value("memory_threshold", "1792mb");
config.parse(&map);
assert_eq!(config.memory_threshold, 1792 * 1024 * 1024);
let map = ConfigMap::empty().with_key_value("memory_threshold", "1g");
config.parse(&map);
assert_eq!(config.memory_threshold, 1024 * 1024 * 1024);
let map = ConfigMap::empty().with_key_value("memory_threshold", "1879048192");
config.parse(&map);
assert_eq!(config.memory_threshold, 1792 * 1024 * 1024);
let map = ConfigMap::empty().with_key_value("memory_threshold", "10");
config.parse(&map);
assert_eq!(config.memory_threshold, 1024 * 1024);
config.memory_threshold = 0;
let map = ConfigMap::empty().with_key_value("memory_threshold", "garbage");
config.parse(&map);
assert_eq!(config.memory_threshold, 0, "unparseable value must not change the field");
}
#[test]
fn memory_threshold_parses_byte_suffixes() {
assert_eq!(parse_memory_threshold("1792mb"), Some(1792 * 1024 * 1024));
assert_eq!(parse_memory_threshold("1g"), Some(1024 * 1024 * 1024));
assert_eq!(parse_memory_threshold("1879048192"), Some(1792 * 1024 * 1024));
assert_eq!(parse_memory_threshold("0"), Some(0));
assert_eq!(parse_memory_threshold("10"), Some(1024 * 1024));
assert_eq!(parse_memory_threshold("garbage"), None);
}
}