qualia_client_core/wellfair/
graph_store.rs1use 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}