use std::path::Path;
use redb::{ReadOnlyDatabase, ReadableDatabase, ReadableTable, ReadableTableMetadata, TableError};
use trusty_common::memory_core::store::kg_store::{
ACTIVE_SUBJECT_COUNTS, DELETED_VECTORS, DRAWERS, ROOMS, VECTORS,
};
const KG_FILE: &str = "kg.redb";
const VECTOR_FILE: &str = "index.usearch.redb";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) struct PalaceDiskStats {
pub drawer_count: usize,
pub vector_count: usize,
pub kg_triple_count: usize,
pub room_count: usize,
}
pub(super) fn read(data_dir: &Path) -> Result<PalaceDiskStats, String> {
let kg = open_read_only(&data_dir.join(KG_FILE))?;
let rtx = kg
.begin_read()
.map_err(|e| format!("begin read on {KG_FILE}: {e}"))?;
let drawer_count = table_len(&rtx, DRAWERS, "drawers")?;
let room_count = room_count(&rtx)?;
let kg_triple_count = active_triple_count(&rtx)?;
drop(rtx);
drop(kg);
let vector_path = data_dir.join(VECTOR_FILE);
let vector_count = if vector_path.exists() {
let vectors = open_read_only(&vector_path)?;
let rtx = vectors
.begin_read()
.map_err(|e| format!("begin read on {VECTOR_FILE}: {e}"))?;
let live = table_len(&rtx, VECTORS, "vectors")?;
let dead = table_len(&rtx, DELETED_VECTORS, "deleted_vectors")?;
live.saturating_sub(dead)
} else {
0
};
Ok(PalaceDiskStats {
drawer_count,
vector_count,
kg_triple_count,
room_count,
})
}
fn open_read_only(path: &Path) -> Result<ReadOnlyDatabase, String> {
ReadOnlyDatabase::open(path).map_err(|e| {
format!(
"{} is not readable: {e}",
path.file_name().unwrap_or(path.as_os_str()).display()
)
})
}
fn table_len<K, V>(
rtx: &redb::ReadTransaction,
table: redb::TableDefinition<'_, K, V>,
name: &str,
) -> Result<usize, String>
where
K: redb::Key + 'static,
V: redb::Value + 'static,
{
match rtx.open_table(table) {
Ok(t) => t
.len()
.map(|n| n as usize)
.map_err(|e| format!("read {name} length: {e}")),
Err(TableError::TableDoesNotExist(_)) => Ok(0),
Err(e) => Err(format!("open {name} table: {e}")),
}
}
fn room_count(rtx: &redb::ReadTransaction) -> Result<usize, String> {
let total = table_len(rtx, ROOMS, "rooms")?;
let marker = match rtx.open_table(ROOMS) {
Ok(t) => t
.get(uuid::Uuid::nil().as_bytes().as_slice())
.map_err(|e| format!("read rooms schema marker: {e}"))?
.is_some(),
Err(TableError::TableDoesNotExist(_)) => false,
Err(e) => return Err(format!("open rooms table: {e}")),
};
Ok(total.saturating_sub(usize::from(marker)))
}
fn active_triple_count(rtx: &redb::ReadTransaction) -> Result<usize, String> {
let counts = match rtx.open_table(ACTIVE_SUBJECT_COUNTS) {
Ok(t) => t,
Err(TableError::TableDoesNotExist(_)) => return Ok(0),
Err(e) => return Err(format!("open active_subject_counts table: {e}")),
};
let mut total: u64 = 0;
for entry in counts
.iter()
.map_err(|e| format!("iter active_subject_counts: {e}"))?
{
let (_, v) = entry.map_err(|e| format!("read active_subject_counts row: {e}"))?;
let raw = v.value();
let mut buf = [0u8; 8];
let take = raw.len().min(8);
buf[..take].copy_from_slice(&raw[..take]);
total = total.saturating_add(u64::from_le_bytes(buf));
}
Ok(total as usize)
}
#[cfg(test)]
mod tests {
use super::*;
use trusty_common::memory_core::{Palace, PalaceId, PalaceRegistry};
fn seed_palace(root: &Path, name: &str, drawers: usize) {
let registry = PalaceRegistry::with_max_open(4);
let palace = Palace {
id: PalaceId::new(name),
name: name.to_string(),
description: None,
created_at: chrono::Utc::now(),
data_dir: root.join(name),
};
let handle = registry
.create_palace(root, palace)
.unwrap_or_else(|e| panic!("create_palace({name}): {e:#}"));
for i in 0..drawers {
let drawer = trusty_common::memory_core::Drawer::new(
uuid::Uuid::new_v4(),
format!("seeded drawer {i}"),
);
handle
.kg
.store()
.upsert_drawer(&drawer)
.unwrap_or_else(|e| panic!("upsert_drawer({i}): {e:#}"));
}
drop(handle);
registry.remove(&PalaceId::new(name));
drop(registry);
}
#[test]
fn disk_stats_counts_a_palace_that_was_never_opened() {
let tmp = tempfile::tempdir().expect("tempdir");
seed_palace(tmp.path(), "cold", 3);
let stats = read(&tmp.path().join("cold")).expect("a closed palace must be readable");
assert_eq!(
stats.drawer_count, 3,
"the drawer count must come off disk, not from a cache miss"
);
assert_eq!(stats.vector_count, 0, "nothing was embedded");
}
#[test]
fn disk_stats_refuses_a_palace_held_open_for_writing() {
let tmp = tempfile::tempdir().expect("tempdir");
let registry = PalaceRegistry::with_max_open(4);
let palace = Palace {
id: PalaceId::new("hot"),
name: "hot".to_string(),
description: None,
created_at: chrono::Utc::now(),
data_dir: tmp.path().join("hot"),
};
let _handle = registry
.create_palace(tmp.path(), palace)
.expect("create_palace");
let err = read(&tmp.path().join("hot"))
.expect_err("a writer-held palace must not be read from disk");
assert!(
err.contains("kg.redb"),
"the error must name the file it could not read: {err}"
);
}
#[test]
fn disk_stats_reports_a_missing_palace_directory() {
let tmp = tempfile::tempdir().expect("tempdir");
let err = read(&tmp.path().join("absent")).expect_err("no redb file to read");
assert!(
err.contains("kg.redb"),
"the error must name the file it looked for: {err}"
);
}
}