gioser

tawasuyu/brahman

Rust 582TOML 222Markdown 73JSON 198635 Sutichaykuna

⎇ 7831c0c827134e4753a9631765ab76a3fb3b63ba crates/modules/nouser/core/src/db.rs Rust · 313 siq'ikuna · 9.3 KB Ñawpa willay Pi imata llamkarqan ch'uya

feat(nouser): persistencia sled write-through del MonadDb 7831c0c82 · Sergio ·

1//! DB de Mónadas y archivos. Backend dual:
2//!
3//! - **Memoria** (default, cache): `BTreeMap<Id, T>` para reads O(log n).
4//! - **Persistencia** (opcional): sled-backed write-through. Si se abre
5//!   con `MonadDb::open(path)`, cada `insert_*` escribe a sled además
6//!   de la cache. Reads siempre vienen de la cache.
7//!
8//! Wire format: JSON via serde_json. Los manifestos son chicos y
9//! ocasionalmente inspeccionables a mano (`sled-cli`); JSON gana sobre
10//! postcard en debuggability.
11
12use std::collections::BTreeMap;
13use std::path::Path;
14
15use nouser_card::{FileEntry, FileId, MonadId, MonadManifest};
16use thiserror::Error;
17
18#[derive(Debug, Error)]
19pub enum MonadDbError {
20    #[error("sled: {0}")]
21    Sled(#[from] sled::Error),
22    #[error("JSON: {0}")]
23    Json(#[from] serde_json::Error),
24    #[error("ULID inválido en clave: {0}")]
25    BadKey(String),
26}
27
28const TREE_FILES: &str = "files";
29const TREE_MONADS: &str = "monads";
30
31/// Store de Mónadas + archivos. Cache en memoria + persistencia
32/// opcional sled.
33pub struct MonadDb {
34    files: BTreeMap<FileId, FileEntry>,
35    monads: BTreeMap<MonadId, MonadManifest>,
36    persistence: Option<sled::Db>,
37}
38
39impl Default for MonadDb {
40    fn default() -> Self {
41        Self::new()
42    }
43}
44
45impl MonadDb {
46    /// Store en memoria pura (sin persistencia). El estado se pierde al salir.
47    pub fn new() -> Self {
48        Self {
49            files: BTreeMap::new(),
50            monads: BTreeMap::new(),
51            persistence: None,
52        }
53    }
54
55    /// Abre (o crea) un store sled-backed en `path`. Carga el contenido
56    /// existente a la cache antes de devolver.
57    pub fn open(path: impl AsRef<Path>) -> Result<Self, MonadDbError> {
58        let db = sled::open(path)?;
59        let mut files = BTreeMap::new();
60        let mut monads = BTreeMap::new();
61
62        let files_tree = db.open_tree(TREE_FILES)?;
63        for kv in files_tree.iter() {
64            let (k, v) = kv?;
65            let id = decode_key(&k)?;
66            let entry: FileEntry = serde_json::from_slice(&v)?;
67            files.insert(id, entry);
68        }
69        let monads_tree = db.open_tree(TREE_MONADS)?;
70        for kv in monads_tree.iter() {
71            let (k, v) = kv?;
72            let id = decode_key(&k)?;
73            let monad: MonadManifest = serde_json::from_slice(&v)?;
74            monads.insert(id, monad);
75        }
76
77        Ok(Self {
78            files,
79            monads,
80            persistence: Some(db),
81        })
82    }
83
84    /// `true` si tiene backend persistente.
85    pub fn is_persistent(&self) -> bool {
86        self.persistence.is_some()
87    }
88
89    // ---- Files ----
90
91    pub fn insert_file(&mut self, file: FileEntry) -> Option<FileEntry> {
92        if let Some(db) = &self.persistence {
93            // Write-through: si falla el persist, lo logeamos pero la
94            // memoria queda actualizada. Filosofía: cache nunca miente
95            // sobre el último estado conocido en este proceso.
96            if let Err(e) = persist_file(db, &file) {
97                eprintln!("[MonadDb] persist file falló: {e}");
98            }
99        }
100        self.files.insert(file.id, file)
101    }
102
103    pub fn ingest_files(&mut self, files: Vec<FileEntry>) {
104        for f in files {
105            self.insert_file(f);
106        }
107    }
108
109    pub fn file(&self, id: FileId) -> Option<&FileEntry> {
110        self.files.get(&id)
111    }
112
113    pub fn files(&self) -> impl Iterator<Item = &FileEntry> + '_ {
114        self.files.values()
115    }
116
117    pub fn file_count(&self) -> usize {
118        self.files.len()
119    }
120
121    // ---- Monads ----
122
123    pub fn insert_monad(&mut self, monad: MonadManifest) -> Option<MonadManifest> {
124        if let Some(db) = &self.persistence {
125            if let Err(e) = persist_monad(db, &monad) {
126                eprintln!("[MonadDb] persist monad falló: {e}");
127            }
128        }
129        self.monads.insert(monad.id, monad)
130    }
131
132    pub fn replace_monads(&mut self, monads: Vec<MonadManifest>) {
133        // Si hay persistencia, limpiar tree antes de insertar.
134        if let Some(db) = &self.persistence {
135            if let Ok(tree) = db.open_tree(TREE_MONADS) {
136                let _ = tree.clear();
137            }
138        }
139        self.monads.clear();
140        for m in monads {
141            self.insert_monad(m);
142        }
143    }
144
145    pub fn monad(&self, id: MonadId) -> Option<&MonadManifest> {
146        self.monads.get(&id)
147    }
148
149    pub fn monads(&self) -> impl Iterator<Item = &MonadManifest> + '_ {
150        self.monads.values()
151    }
152
153    pub fn monad_count(&self) -> usize {
154        self.monads.len()
155    }
156
157    /// Resuelve los archivos miembros de una Mónada como referencias.
158    /// Skipea silenciosamente IDs que ya no estén en la tabla `files`.
159    pub fn resolve_members(&self, monad_id: MonadId) -> Vec<&FileEntry> {
160        match self.monads.get(&monad_id) {
161            Some(m) => m.members.iter().filter_map(|id| self.files.get(id)).collect(),
162            None => Vec::new(),
163        }
164    }
165}
166
167fn persist_file(db: &sled::Db, f: &FileEntry) -> Result<(), MonadDbError> {
168    let tree = db.open_tree(TREE_FILES)?;
169    let key = f.id.to_string();
170    let val = serde_json::to_vec(f)?;
171    tree.insert(key.as_bytes(), val)?;
172    Ok(())
173}
174
175fn persist_monad(db: &sled::Db, m: &MonadManifest) -> Result<(), MonadDbError> {
176    let tree = db.open_tree(TREE_MONADS)?;
177    let key = m.id.to_string();
178    let val = serde_json::to_vec(m)?;
179    tree.insert(key.as_bytes(), val)?;
180    Ok(())
181}
182
183fn decode_key(k: &[u8]) -> Result<ulid::Ulid, MonadDbError> {
184    let s = std::str::from_utf8(k).map_err(|_| MonadDbError::BadKey(format!("{:?}", k)))?;
185    ulid::Ulid::from_string(s).map_err(|_| MonadDbError::BadKey(s.to_string()))
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191    use nouser_card::Lens;
192    use ulid::Ulid;
193
194    fn mk_file(path: &str) -> FileEntry {
195        FileEntry {
196            id: FileId::from(Ulid::new()),
197            path: std::path::PathBuf::from(path),
198            content_hash: None,
199            size: 100,
200            mtime_ms: 0,
201            extension: Some("rs".into()),
202        }
203    }
204
205    #[test]
206    fn ingest_and_lookup() {
207        let mut db = MonadDb::new();
208        let f1 = mk_file("/a/x.rs");
209        let f2 = mk_file("/a/y.rs");
210        let id1 = f1.id;
211        db.ingest_files(vec![f1, f2]);
212        assert_eq!(db.file_count(), 2);
213        assert!(db.file(id1).is_some());
214        assert!(!db.is_persistent());
215    }
216
217    #[test]
218    fn resolve_members_filters_missing() {
219        let mut db = MonadDb::new();
220        let f1 = mk_file("/x/a.rs");
221        let id1 = f1.id;
222        db.insert_file(f1);
223
224        let mut m = MonadManifest::new("test");
225        m.members.insert(id1);
226        m.members.insert(FileId::from(Ulid::new())); // miembro fantasma
227        m.dominant_lens = Lens::Code;
228        m.touch();
229
230        let mid = m.id;
231        db.insert_monad(m);
232
233        let resolved = db.resolve_members(mid);
234        assert_eq!(resolved.len(), 1);
235        assert_eq!(resolved[0].id, id1);
236    }
237
238    #[test]
239    fn replace_monads_clears_old() {
240        let mut db = MonadDb::new();
241        let mut m1 = MonadManifest::new("a");
242        m1.members.insert(FileId::from(Ulid::new()));
243        m1.touch();
244        db.insert_monad(m1);
245        assert_eq!(db.monad_count(), 1);
246
247        let mut m2 = MonadManifest::new("b");
248        m2.members.insert(FileId::from(Ulid::new()));
249        m2.touch();
250        db.replace_monads(vec![m2]);
251        assert_eq!(db.monad_count(), 1);
252        assert!(db.monads().next().unwrap().label == "b");
253    }
254
255    #[test]
256    fn persistence_roundtrip() {
257        let tmp = tempfile::tempdir().unwrap();
258        let dbpath = tmp.path().join("monads.sled");
259
260        // Escribimos algunos datos
261        {
262            let mut db = MonadDb::open(&dbpath).expect("open");
263            assert!(db.is_persistent());
264            let f = mk_file("/persist/a.rs");
265            let fid = f.id;
266            db.insert_file(f);
267
268            let mut m = MonadManifest::new("persist-test");
269            m.members.insert(fid);
270            m.dominant_lens = Lens::Code;
271            m.touch();
272            db.insert_monad(m);
273        }
274
275        // Reabrimos y verificamos que están
276        let db = MonadDb::open(&dbpath).expect("reopen");
277        assert_eq!(db.file_count(), 1);
278        assert_eq!(db.monad_count(), 1);
279        let m = db.monads().next().unwrap();
280        assert_eq!(m.label, "persist-test");
281        assert_eq!(m.cardinality, 1);
282    }
283
284    #[test]
285    fn replace_monads_purges_persistent_tree() {
286        let tmp = tempfile::tempdir().unwrap();
287        let dbpath = tmp.path().join("replace.sled");
288
289        {
290            let mut db = MonadDb::open(&dbpath).unwrap();
291            let mut m1 = MonadManifest::new("old");
292            m1.members.insert(FileId::from(Ulid::new()));
293            m1.touch();
294            db.insert_monad(m1);
295        }
296
297        // Reabrir, replace, verificar
298        {
299            let mut db = MonadDb::open(&dbpath).unwrap();
300            assert_eq!(db.monad_count(), 1);
301            let mut m2 = MonadManifest::new("new");
302            m2.members.insert(FileId::from(Ulid::new()));
303            m2.touch();
304            db.replace_monads(vec![m2]);
305            assert_eq!(db.monad_count(), 1);
306        }
307
308        // Tercera apertura: sólo "new" sobrevive
309        let db = MonadDb::open(&dbpath).unwrap();
310        assert_eq!(db.monad_count(), 1);
311        assert_eq!(db.monads().next().unwrap().label, "new");
312    }
313}