Skip to main content

qualia_client_core/wellfair/
graph_store.rs

1//! Materialized graph quins — append-only projection of checkpointed WAL commits.
2
3use std::fs::{self, OpenOptions};
4use std::io::{Read, Seek, SeekFrom, Write};
5use std::path::{Path, PathBuf};
6
7use qualia_core_db::NQuin;
8
9pub const GRAPH_QUINS_FILE: &str = "wellfair/graph/quins.bin";
10pub const MAX_LIST: usize = 512;
11
12pub struct GraphStore {
13    path: PathBuf,
14    quin_count: usize,
15}
16
17impl GraphStore {
18    pub fn open(storage_root: impl AsRef<Path>) -> std::io::Result<Self> {
19        let path = storage_root.as_ref().join(GRAPH_QUINS_FILE);
20        if let Some(parent) = path.parent() {
21            fs::create_dir_all(parent)?;
22        }
23        if !path.exists() {
24            OpenOptions::new().create(true).write(true).open(&path)?;
25        }
26        let quin_count = count_quins(&path)?;
27        Ok(Self { path, quin_count })
28    }
29
30    pub fn count(&self) -> usize {
31        self.quin_count
32    }
33
34    pub fn append_quins(&mut self, quins: &[NQuin]) -> std::io::Result<()> {
35        if quins.is_empty() {
36            return Ok(());
37        }
38        let mut file = OpenOptions::new().append(true).open(&self.path)?;
39        for quin in quins {
40            let bytes = quin_as_bytes(quin);
41            file.write_all(bytes)?;
42        }
43        file.sync_all()?;
44        self.quin_count += quins.len();
45        Ok(())
46    }
47
48    pub fn list_recent(&self, limit: usize) -> std::io::Result<Vec<NQuin>> {
49        let mut file = fs::File::open(&self.path)?;
50        let len = file.metadata()?.len();
51        let quin_size = std::mem::size_of::<NQuin>() as u64;
52        if len < quin_size {
53            return Ok(Vec::new());
54        }
55        let total = (len / quin_size) as usize;
56        let keep = limit.min(MAX_LIST).min(total);
57        let start = len - (keep as u64 * quin_size);
58        file.seek(SeekFrom::Start(start))?;
59        let mut buffer = vec![0u8; keep * std::mem::size_of::<NQuin>()];
60        file.read_exact(&mut buffer)?;
61        Ok(decode_quins(&buffer))
62    }
63}
64
65fn count_quins(path: &Path) -> std::io::Result<usize> {
66    let len = fs::metadata(path)?.len();
67    Ok((len as usize) / std::mem::size_of::<NQuin>())
68}
69
70fn quin_as_bytes(quin: &NQuin) -> &[u8] {
71    unsafe {
72        std::slice::from_raw_parts(
73            (quin as *const NQuin) as *const u8,
74            std::mem::size_of::<NQuin>(),
75        )
76    }
77}
78
79fn decode_quins(buffer: &[u8]) -> Vec<NQuin> {
80    let quin_size = std::mem::size_of::<NQuin>();
81    buffer
82        .chunks_exact(quin_size)
83        .map(|chunk| unsafe { std::ptr::read_unaligned(chunk.as_ptr() as *const NQuin) })
84        .collect()
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90
91    #[test]
92    fn graph_store_round_trip() {
93        let dir = tempfile::tempdir().unwrap();
94        let mut store = GraphStore::open(dir.path()).unwrap();
95        let q = NQuin {
96            subject: 1,
97            predicate: 2,
98            object: 3,
99            context: 4,
100            metadata: 5,
101            parity: 6,
102        };
103        store.append_quins(&[q]).unwrap();
104        assert_eq!(store.count(), 1);
105        let listed = store.list_recent(10).unwrap();
106        assert_eq!(listed.len(), 1);
107        assert_eq!(listed[0].subject, 1);
108    }
109}