use crate::graph::storage::mode::StorageMode;
pub(crate) const DEFER_ENV_VAR: &str = "KGLITE_DEFER_INDEX_REBUILD";
pub(crate) const MAX_LOAD_ENV_VAR: &str = "KGLITE_MAX_LOAD_MB";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LoadOptions {
pub storage: Option<StorageMode>,
pub defer_index_rebuild: bool,
pub max_load_bytes: Option<u64>,
}
impl LoadOptions {
pub fn new() -> Self {
LoadOptions {
storage: None,
defer_index_rebuild: defer_index_rebuild_default(),
max_load_bytes: max_load_bytes_default(),
}
}
pub fn with_storage(mut self, mode: StorageMode) -> Self {
self.storage = Some(mode);
self
}
pub fn with_defer_index_rebuild(mut self, defer: bool) -> Self {
self.defer_index_rebuild = defer;
self
}
pub fn with_max_load_bytes(mut self, max_bytes: Option<u64>) -> Self {
self.max_load_bytes = max_bytes;
self
}
}
impl Default for LoadOptions {
fn default() -> Self {
Self::new()
}
}
fn defer_index_rebuild_default() -> bool {
match std::env::var(DEFER_ENV_VAR) {
Ok(raw) => parse_flag(&raw),
Err(_) => false,
}
}
fn max_load_bytes_default() -> Option<u64> {
let raw = std::env::var(MAX_LOAD_ENV_VAR).ok()?;
parse_max_load_mb(&raw)
}
fn parse_max_load_mb(raw: &str) -> Option<u64> {
let trimmed = raw.trim();
if trimmed.is_empty() {
return None;
}
match trimmed.parse::<u64>() {
Ok(mb) => Some(mb.saturating_mul(1024 * 1024)),
Err(_) => {
eprintln!(
"kglite: ignoring {MAX_LOAD_ENV_VAR}={trimmed:?} — expected a whole number of \
megabytes; loading with NO memory ceiling"
);
None
}
}
}
fn parse_flag(raw: &str) -> bool {
match raw.trim().to_ascii_lowercase().as_str() {
"" => false,
"1" | "true" | "yes" | "on" => true,
"0" | "false" | "no" | "off" => false,
other => {
eprintln!(
"kglite: ignoring {DEFER_ENV_VAR}={other:?} — expected one of \
1/true/yes/on or 0/false/no/off; loading with indexes built"
);
false
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn builders_compose_and_default_matches_new() {
assert_eq!(LoadOptions::default(), LoadOptions::new());
let options = LoadOptions::new()
.with_storage(StorageMode::Mapped)
.with_defer_index_rebuild(true)
.with_max_load_bytes(Some(64 * 1024 * 1024));
assert_eq!(options.storage, Some(StorageMode::Mapped));
assert!(options.defer_index_rebuild);
assert_eq!(options.max_load_bytes, Some(64 * 1024 * 1024));
assert_eq!(
options.clone().with_max_load_bytes(None).max_load_bytes,
None
);
assert!(!options.with_defer_index_rebuild(false).defer_index_rebuild);
}
#[test]
fn unparseable_flag_is_loud_and_falls_back_to_eager() {
assert!(parse_flag("on"));
assert!(parse_flag(" TRUE "));
assert!(!parse_flag("off"));
assert!(!parse_flag(""));
assert!(!parse_flag("maybe"));
}
#[test]
fn max_load_mb_parses_megabytes_and_is_loud_when_it_cannot() {
assert_eq!(parse_max_load_mb("512"), Some(512 * 1024 * 1024));
assert_eq!(parse_max_load_mb(" 512 "), Some(512 * 1024 * 1024));
assert_eq!(parse_max_load_mb("0"), Some(0));
assert_eq!(parse_max_load_mb(""), None);
for bad in ["1024MB", "1O24", "1.5", "-1", "lots"] {
assert_eq!(parse_max_load_mb(bad), None, "{bad} parsed as a ceiling");
}
assert_eq!(parse_max_load_mb(&u64::MAX.to_string()), Some(u64::MAX));
}
}