use nodedb_types::Namespace;
use crate::error::LiteError;
use crate::storage::engine::StorageEngine;
use std::collections::HashMap;
use super::super::key::{
coll_prefix, doc_prefix, latest_version_key, parse_sys_from, versioned_doc_key,
};
use super::super::value::{DecodedVersion, VersionTag, decode_value};
pub async fn versioned_get_current<S: StorageEngine>(
storage: &S,
collection: &str,
doc_id: &str,
) -> Result<Option<DecodedVersion>, LiteError> {
let pointer_key = latest_version_key(collection, doc_id);
let Some(pointer_bytes) = storage.get(Namespace::LatestVersion, &pointer_key).await? else {
return Ok(None);
};
let sys_from_str =
std::str::from_utf8(&pointer_bytes).map_err(|_| LiteError::Serialization {
detail: "LatestVersion pointer is not valid UTF-8".into(),
})?;
let sys_from_ms: i64 = sys_from_str
.trim()
.parse()
.map_err(|_| LiteError::Serialization {
detail: format!("LatestVersion pointer is not a valid i64 decimal: {sys_from_str:?}"),
})?;
let history_key = versioned_doc_key(collection, doc_id, sys_from_ms)?;
let Some(history_bytes) = storage
.get(Namespace::DocumentHistory, &history_key)
.await?
else {
return Err(LiteError::Serialization {
detail: format!(
"LatestVersion pointer for {collection}/{doc_id} points to \
system_from_ms={sys_from_ms} but no DocumentHistory row exists"
),
});
};
let decoded = decode_value(&history_bytes)?;
if decoded.is_live() {
Ok(Some(decoded))
} else {
Ok(None)
}
}
pub async fn versioned_get_as_of<S: StorageEngine>(
storage: &S,
collection: &str,
doc_id: &str,
system_as_of_ms: i64,
valid_time_ms: Option<i64>,
) -> Result<Option<DecodedVersion>, LiteError> {
let prefix = doc_prefix(collection, doc_id);
let entries = storage
.scan_prefix(Namespace::DocumentHistory, &prefix)
.await?;
for (_key, value) in entries.iter().rev() {
let decoded = decode_value(value)?;
let sys_from = parse_sys_from(_key).ok_or_else(|| LiteError::Serialization {
detail: "document history key missing NUL separator".into(),
})?;
if sys_from > system_as_of_ms {
continue;
}
if decoded.tag != VersionTag::Live {
return Ok(None);
}
if let Some(vt) = valid_time_ms
&& (vt < decoded.valid_from_ms || vt >= decoded.valid_until_ms)
{
return Ok(None);
}
return Ok(Some(decoded));
}
Ok(None)
}
pub async fn scan_live_documents<S: StorageEngine>(
storage: &S,
collection: &str,
) -> Result<Vec<(String, Vec<u8>)>, LiteError> {
let prefix = coll_prefix(collection);
let entries = storage
.scan_prefix(Namespace::DocumentHistory, &prefix)
.await?;
let mut latest: HashMap<String, (VersionTag, Vec<u8>)> = HashMap::new();
for (key, value) in &entries {
let after_prefix = match key.get(prefix.len()..) {
Some(s) => s,
None => continue,
};
let nul = match after_prefix.iter().position(|&b| b == 0) {
Some(p) => p,
None => continue,
};
let doc_id = match std::str::from_utf8(&after_prefix[..nul]) {
Ok(s) => s.to_owned(),
Err(_) => continue,
};
let decoded = match decode_value(value) {
Ok(d) => d,
Err(_) => continue,
};
latest.insert(doc_id, (decoded.tag, decoded.body));
}
Ok(latest
.into_iter()
.filter(|(_, (tag, _))| *tag == VersionTag::Live)
.map(|(id, (_, body))| (id, body))
.collect())
}
#[cfg(test)]
mod tests {
use crate::storage::engine::WriteOp;
use crate::storage::pagedb_storage::PagedbStorageMem;
use super::super::super::key::{format_sys_from, latest_version_key, versioned_doc_key};
use super::super::super::value::encode_value;
use super::super::write::{versioned_put, versioned_tombstone};
use super::*;
async fn mem_storage() -> PagedbStorageMem {
PagedbStorageMem::open_in_memory()
.await
.expect("open in-memory storage")
}
#[tokio::test]
async fn latest_version_insert_pointer_present() {
let s = mem_storage().await;
versioned_put(&s, "c", "d1", b"hello", 100, None, None)
.await
.unwrap();
let ptr_key = latest_version_key("c", "d1");
let ptr = s
.get(Namespace::LatestVersion, &ptr_key)
.await
.unwrap()
.expect("LatestVersion pointer must exist after insert");
assert_eq!(ptr, format_sys_from(100).into_bytes());
let v = versioned_get_current(&s, "c", "d1").await.unwrap().unwrap();
assert_eq!(v.body, b"hello");
assert!(v.is_live());
}
#[tokio::test]
async fn latest_version_update_pointer_tracks_new() {
let s = mem_storage().await;
versioned_put(&s, "c", "d1", b"v1", 100, None, None)
.await
.unwrap();
versioned_put(&s, "c", "d1", b"v2", 200, None, None)
.await
.unwrap();
let ptr_key = latest_version_key("c", "d1");
let ptr = s
.get(Namespace::LatestVersion, &ptr_key)
.await
.unwrap()
.expect("pointer must exist");
assert_eq!(ptr, format_sys_from(200).into_bytes());
let v = versioned_get_current(&s, "c", "d1").await.unwrap().unwrap();
assert_eq!(v.body, b"v2");
let v1 = versioned_get_as_of(&s, "c", "d1", 150, None)
.await
.unwrap()
.expect("v1 visible at t=150");
assert_eq!(v1.body, b"v1");
}
#[tokio::test]
async fn latest_version_tombstone_removes_pointer() {
let s = mem_storage().await;
versioned_put(&s, "c", "d1", b"hello", 100, None, None)
.await
.unwrap();
versioned_tombstone(&s, "c", "d1", 200, None).await.unwrap();
let ptr_key = latest_version_key("c", "d1");
let ptr = s.get(Namespace::LatestVersion, &ptr_key).await.unwrap();
assert!(ptr.is_none(), "pointer must be deleted after tombstone");
assert!(
versioned_get_current(&s, "c", "d1")
.await
.unwrap()
.is_none()
);
}
#[tokio::test]
async fn latest_version_multi_update_then_tombstone() {
let s = mem_storage().await;
versioned_put(&s, "c", "d1", b"v1", 100, None, None)
.await
.unwrap();
versioned_put(&s, "c", "d1", b"v2", 200, None, None)
.await
.unwrap();
versioned_put(&s, "c", "d1", b"v3", 300, None, None)
.await
.unwrap();
versioned_tombstone(&s, "c", "d1", 400, None).await.unwrap();
assert!(
versioned_get_current(&s, "c", "d1")
.await
.unwrap()
.is_none()
);
for (t, body) in [(150, b"v1"), (250, b"v2"), (350, b"v3")] {
let v = versioned_get_as_of(&s, "c", "d1", t, None)
.await
.unwrap()
.unwrap_or_else(|| panic!("version at t={t} must be present"));
assert_eq!(v.body.as_slice(), body as &[u8]);
}
}
#[tokio::test]
async fn put_get_current_roundtrip() {
let s = mem_storage().await;
versioned_put(&s, "c", "d1", b"hello", 100, None, None)
.await
.unwrap();
let v = versioned_get_current(&s, "c", "d1").await.unwrap().unwrap();
assert_eq!(v.body, b"hello");
assert!(v.is_live());
}
#[tokio::test]
async fn tombstone_hides_live() {
let s = mem_storage().await;
versioned_put(&s, "c", "d1", b"hello", 100, None, None)
.await
.unwrap();
versioned_tombstone(&s, "c", "d1", 200, None).await.unwrap();
assert!(
versioned_get_current(&s, "c", "d1")
.await
.unwrap()
.is_none()
);
}
#[tokio::test]
async fn scan_live_documents_skips_tombstoned() {
let s = mem_storage().await;
versioned_put(&s, "c", "alive", b"body", 100, None, None)
.await
.unwrap();
versioned_put(&s, "c", "dead", b"body", 100, None, None)
.await
.unwrap();
versioned_tombstone(&s, "c", "dead", 200, None)
.await
.unwrap();
let mut docs = scan_live_documents(&s, "c").await.unwrap();
docs.sort();
assert_eq!(docs, vec![("alive".to_owned(), b"body".to_vec())]);
}
#[tokio::test]
async fn get_current_reads_pointerless_row_after_manual_pointer() {
let s = mem_storage().await;
let history_key = versioned_doc_key("c", "d1", 100).unwrap();
let history_value = encode_value(VersionTag::Live, 100, i64::MAX, b"body");
let ptr_key = latest_version_key("c", "d1");
s.batch_write(&[
WriteOp::Put {
ns: Namespace::DocumentHistory,
key: history_key,
value: history_value,
},
WriteOp::Put {
ns: Namespace::LatestVersion,
key: ptr_key,
value: format_sys_from(100).into_bytes(),
},
])
.await
.unwrap();
let v = versioned_get_current(&s, "c", "d1").await.unwrap().unwrap();
assert_eq!(v.body, b"body");
}
}