use std::collections::BTreeSet;
use nodedb_types::Namespace;
use crate::error::LiteError;
use crate::storage::engine::{StorageEngine, WriteOp};
use super::key;
const RESERVED_FIRST_SEGMENTS: &[&str] = &[
"kv",
"kv_sorted",
"meta",
"crdt",
"collection",
"graph",
"fts",
"catalog",
"manifest",
"strict_schema",
"columnar_schema",
"document_bitemporal",
"graph_bitemporal",
"vr",
"v",
];
fn is_legacy_entry(key: &[u8], value: &[u8], prefix: &str) -> bool {
let Ok(key) = std::str::from_utf8(key) else {
return false;
};
let Some(rest) = key.strip_prefix(prefix) else {
return false;
};
let Some((path, _value)) = rest.split_once(':') else {
return false;
};
if path.is_empty() {
return false;
}
match zerompk::from_msgpack::<Vec<String>>(value) {
Ok(ids) => !ids.is_empty() && ids.iter().all(|id| !id.is_empty()),
Err(_) => false,
}
}
pub(crate) async fn clear_legacy_entries<S: StorageEngine>(
storage: &S,
collections: &BTreeSet<String>,
) -> Result<u64, LiteError> {
let marker = key::legacy_cleared_key();
if storage.get(Namespace::Meta, &marker).await?.is_some() {
return Ok(0);
}
let mut ops = Vec::new();
for collection in collections {
let mut prefixes = vec![format!("kv:{collection}:")];
if !RESERVED_FIRST_SEGMENTS.contains(&collection.as_str()) {
prefixes.push(format!("{collection}:"));
}
for prefix in prefixes {
for (k, v) in storage
.scan_prefix(Namespace::Meta, prefix.as_bytes())
.await?
{
if is_legacy_entry(&k, &v, &prefix) {
ops.push(WriteOp::Delete {
ns: Namespace::Meta,
key: k,
});
}
}
}
}
let removed = ops.len() as u64;
ops.push(WriteOp::Put {
ns: Namespace::Meta,
key: marker,
value: Vec::new(),
});
storage.batch_write(&ops).await?;
Ok(removed)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::storage::pagedb_storage::PagedbStorageMem;
fn ids(list: &[&str]) -> Vec<u8> {
let owned: Vec<String> = list.iter().map(|s| (*s).to_string()).collect();
zerompk::to_msgpack_vec(&owned).expect("encode")
}
#[tokio::test]
async fn only_provable_old_entries_are_removed_once() {
let storage = PagedbStorageMem::open_in_memory().await.expect("storage");
let put = |k: &'static str, v: Vec<u8>| {
let storage = &storage;
async move {
storage
.put(Namespace::Meta, k.as_bytes(), &v)
.await
.expect("put")
}
};
put("users:email:a@x", ids(&["u1", "u2"])).await;
put("users:$.email:b:c", ids(&["u3"])).await;
put("users:email", ids(&["u1"])).await;
put("users:note:x", b"{\"a\":1}".to_vec()).await;
put("ghost:email:a@x", ids(&["g1"])).await;
put("kv:ghost:email:a@x", ids(&["k1"])).await;
put("kv:users:email:a@x", ids(&["k1"])).await;
let known: BTreeSet<String> = ["users", "kv"].iter().map(|s| (*s).to_string()).collect();
assert_eq!(
clear_legacy_entries(&storage, &known).await.expect("clear"),
3
);
let get = |k: &'static str| {
let storage = &storage;
async move {
storage
.get(Namespace::Meta, k.as_bytes())
.await
.expect("get")
}
};
assert!(get("users:email:a@x").await.is_none());
assert!(get("users:$.email:b:c").await.is_none());
assert!(get("kv:users:email:a@x").await.is_none());
for kept in [
"users:email",
"users:note:x",
"ghost:email:a@x",
"kv:ghost:email:a@x",
] {
assert!(get(kept).await.is_some(), "{kept} is kept");
}
put("users:email:late", ids(&["u9"])).await;
assert_eq!(
clear_legacy_entries(&storage, &known).await.expect("clear"),
0
);
assert!(get("users:email:late").await.is_some());
}
}