use nodedb_types::DatabaseId;
use crate::data::executor::checkpoint_encoding::{dec_component, enc_component};
use crate::types::TenantId;
pub(super) const SPARSE_VECTOR_CKPT_MANIFEST: &str = "MANIFEST";
pub(crate) fn sparse_vector_ckpt_dir(
data_dir: &std::path::Path,
core_id: usize,
) -> std::path::PathBuf {
data_dir
.join("sparse-vector-ckpt")
.join(format!("core-{core_id}"))
}
pub(crate) fn sparse_vector_ckpt_gen_dir(
ckpt_dir: &std::path::Path,
generation: u64,
) -> std::path::PathBuf {
ckpt_dir.join(format!("gen-{generation}"))
}
pub(super) fn sparse_vector_checkpoint_stem(db: u64, tid: u64, coll: &str, field: &str) -> String {
format!(
"{}_{}_{}_{}",
db,
tid,
enc_component(coll),
enc_component(field)
)
}
pub(crate) fn sparse_vector_checkpoint_prefix(db: u64, tid: u64, coll: &str) -> String {
format!("{}_{}_{}_", db, tid, enc_component(coll))
}
pub(super) fn parse_sparse_vector_key(
stem: &str,
) -> Option<(DatabaseId, TenantId, String, String)> {
let parts: Vec<&str> = stem.split('_').collect();
if parts.len() != 4 {
return None;
}
let db: u64 = parts[0].parse().ok()?;
let tid: u64 = parts[1].parse().ok()?;
let coll = dec_component(parts[2]);
let field = dec_component(parts[3]);
Some((DatabaseId::new(db), TenantId::new(tid), coll, field))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn per_core_dirs_are_distinct() {
let base = std::path::Path::new("/data");
let d0 = sparse_vector_ckpt_dir(base, 0);
let d1 = sparse_vector_ckpt_dir(base, 1);
assert_ne!(d0, d1);
assert!(d0.to_str().expect("utf8 path").contains("core-0"));
assert!(d1.to_str().expect("utf8 path").contains("core-1"));
}
#[test]
fn generation_dirs_are_distinct() {
let base = std::path::Path::new("/data/sparse-vector-ckpt/core-0");
assert_ne!(
sparse_vector_ckpt_gen_dir(base, 0),
sparse_vector_ckpt_gen_dir(base, 1)
);
}
#[test]
fn stem_roundtrips_through_parse() {
let stem = sparse_vector_checkpoint_stem(7, 42, "my_docs", "title_field");
let parsed = parse_sparse_vector_key(&stem).expect("stem must parse");
assert_eq!(parsed.0, DatabaseId::new(7));
assert_eq!(parsed.1, TenantId::new(42));
assert_eq!(parsed.2, "my_docs");
assert_eq!(parsed.3, "title_field");
}
#[test]
fn prefix_matches_stem_for_same_collection() {
let stem = sparse_vector_checkpoint_stem(3, 9, "d_b", "f");
let prefix = sparse_vector_checkpoint_prefix(3, 9, "d_b");
assert!(
stem.starts_with(&prefix),
"stem {stem} must start with prefix {prefix}"
);
}
#[test]
fn prefix_does_not_match_a_longer_collection_name() {
let other = sparse_vector_checkpoint_stem(3, 9, "docs_archive", "f");
let prefix = sparse_vector_checkpoint_prefix(3, 9, "docs");
assert!(
!other.starts_with(&prefix),
"prefix {prefix} must not swallow {other}"
);
}
#[test]
fn non_numeric_stem_is_none() {
assert!(parse_sparse_vector_key("a_b_c_d").is_none());
}
#[test]
fn wrong_part_count_is_none() {
assert!(parse_sparse_vector_key("1_2_3").is_none());
assert!(parse_sparse_vector_key("1_2_3_4_5").is_none());
}
}