1use std::collections::{HashMap, HashSet, VecDeque};
13use std::time::{SystemTime, UNIX_EPOCH};
14
15use dejadb_core::error::{Hash, DejaDbError, Result};
16use dejadb_core::format::deserialize::{deserialize_blob, DeserializedGrain};
17use dejadb_core::format::serialize::serialize_grain;
18use dejadb_core::types::Grain;
19use turso::{Builder, Connection, Value};
20use zeroize::Zeroize;
21
22pub const OP_ADD: i64 = 1;
24pub const OP_SUPERSEDE: i64 = 2;
25pub const OP_FORGET: i64 = 3; #[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum Axis {
30 World,
32 Knowledge,
34}
35
36#[derive(Debug, Clone)]
38pub struct OpRecord {
39 pub op_seq: i64,
40 pub hlc: i64,
41 pub op: i64,
42 pub hash: Hash,
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub enum Direction {
49 Out,
50 In,
51 Both,
52}
53
54#[derive(Debug, Clone)]
56pub struct BundleStats {
57 pub ops: usize,
58 pub bytes: u64,
59 pub last_op_seq: i64,
60}
61
62#[derive(Debug, Clone, Default)]
64pub struct ImportStats {
65 pub applied: usize,
66 pub skipped: usize,
67}
68
69pub trait EmbedBackend: Send + Sync {
74 fn dim(&self) -> usize;
75 fn embed(&self, text: &str) -> Result<Vec<f32>>;
76 fn model(&self) -> &str {
80 "unspecified"
81 }
82}
83
84pub struct CommandEmbed {
91 argv: Vec<String>,
92 dim: usize,
93 model: String,
94}
95
96impl CommandEmbed {
97 pub fn new(cmd: &str, model: Option<&str>) -> Result<Self> {
101 let argv: Vec<String> = cmd.split_whitespace().map(str::to_string).collect();
102 if argv.is_empty() {
103 return Err(DejaDbError::Validation("embed command is empty".into()));
104 }
105 let mut ce = CommandEmbed {
106 argv,
107 dim: 0,
108 model: model.unwrap_or("command").to_string(),
109 };
110 let probe = ce.run("dimension probe")?;
111 if probe.is_empty() {
112 return Err(DejaDbError::Validation(
113 "embed command returned an empty vector".into(),
114 ));
115 }
116 ce.dim = probe.len();
117 Ok(ce)
118 }
119
120 fn run(&self, text: &str) -> Result<Vec<f32>> {
121 use std::io::Write;
122 use std::process::{Command, Stdio};
123 let cmd_err = |e: std::io::Error| {
124 DejaDbError::Storage(format!("embed command '{}': {e}", self.argv[0]))
125 };
126 let mut child = Command::new(&self.argv[0])
127 .args(&self.argv[1..])
128 .stdin(Stdio::piped())
129 .stdout(Stdio::piped())
130 .stderr(Stdio::inherit())
131 .spawn()
132 .map_err(cmd_err)?;
133 {
134 let mut stdin = child.stdin.take().expect("stdin piped");
135 stdin.write_all(text.as_bytes()).map_err(cmd_err)?;
136 }
138 let out = child.wait_with_output().map_err(cmd_err)?;
139 if !out.status.success() {
140 return Err(DejaDbError::Storage(format!(
141 "embed command '{}' exited with {}",
142 self.argv[0], out.status
143 )));
144 }
145 serde_json::from_slice::<Vec<f32>>(&out.stdout).map_err(|e| {
146 DejaDbError::Validation(format!(
147 "embed command output must be a JSON array of numbers: {e}"
148 ))
149 })
150 }
151}
152
153impl EmbedBackend for CommandEmbed {
154 fn dim(&self) -> usize {
155 self.dim
156 }
157 fn embed(&self, text: &str) -> Result<Vec<f32>> {
158 let v = self.run(text)?;
159 if v.len() != self.dim {
160 return Err(DejaDbError::Validation(format!(
161 "embed command returned {} dims, expected {}",
162 v.len(),
163 self.dim
164 )));
165 }
166 Ok(v)
167 }
168 fn model(&self) -> &str {
169 &self.model
170 }
171}
172
173pub trait RerankBackend: Send + Sync {
181 fn rerank(&self, query: &str, docs: &[&str]) -> Result<Vec<f32>>;
186 fn model(&self) -> &str {
188 "unspecified"
189 }
190}
191
192pub trait QueryExpander: Send + Sync {
200 fn expand(&self, query: &str) -> Vec<String>;
203}
204
205pub struct EnglishExpander {
209 max_variants: usize,
211}
212
213impl Default for EnglishExpander {
214 fn default() -> Self {
215 Self { max_variants: 4 }
216 }
217}
218
219impl EnglishExpander {
220 pub fn new(max_variants: usize) -> Self {
221 Self { max_variants: max_variants.clamp(1, 16) }
222 }
223
224 fn synonyms(token: &str) -> &'static [&'static str] {
226 match token {
228 "cell" | "cellphone" => &["mobile", "phone"],
229 "mobile" => &["cell", "phone"],
230 "phone" => &["cell", "mobile", "telephone"],
231 "email" | "e-mail" => &["mail"],
232 "buy" | "bought" | "purchased" => &["purchase"],
233 "purchase" => &["buy"],
234 "car" | "automobile" => &["vehicle"],
235 "vehicle" => &["car"],
236 "doctor" | "physician" => &["doctor", "physician"],
237 "kid" | "kids" | "child" => &["child", "children"],
238 "spouse" => &["wife", "husband", "partner"],
239 "job" => &["work", "employer"],
240 "home" | "house" => &["residence", "address"],
241 "birthday" => &["birthdate", "born"],
242 "big" => &["large"],
243 "small" => &["little"],
244 _ => &[],
245 }
246 }
247
248 fn stem(token: &str) -> Option<String> {
251 let lower = token;
252 for suf in ["ing", "ed", "es", "s"] {
253 if lower.len() > suf.len() + 2 && lower.ends_with(suf) {
254 let stem = &lower[..lower.len() - suf.len()];
255 if stem.len() >= 3 {
256 return Some(stem.to_string());
257 }
258 }
259 }
260 None
261 }
262}
263
264impl QueryExpander for EnglishExpander {
265 fn expand(&self, query: &str) -> Vec<String> {
266 let tokens: Vec<String> = query
267 .split_whitespace()
268 .map(|t| t.trim_matches(|c: char| !c.is_alphanumeric()).to_lowercase())
269 .filter(|t| !t.is_empty())
270 .collect();
271 if tokens.is_empty() {
272 return Vec::new();
273 }
274 let mut variants: Vec<String> = Vec::new();
275 let mut seen: HashSet<String> = HashSet::new();
276 let original = tokens.join(" ");
277 seen.insert(original.clone());
278
279 for (i, tok) in tokens.iter().enumerate() {
281 for syn in Self::synonyms(tok) {
282 let mut v = tokens.clone();
283 v[i] = (*syn).to_string();
284 let s = v.join(" ");
285 if seen.insert(s.clone()) {
286 variants.push(s);
287 if variants.len() >= self.max_variants {
288 return variants;
289 }
290 }
291 }
292 }
293
294 let stemmed: Vec<String> = tokens
296 .iter()
297 .map(|t| Self::stem(t).unwrap_or_else(|| t.clone()))
298 .collect();
299 let s = stemmed.join(" ");
300 if seen.insert(s.clone()) {
301 variants.push(s);
302 }
303
304 variants.truncate(self.max_variants);
305 variants
306 }
307}
308
309#[derive(Debug, Clone, Copy, Default)]
314pub struct RecallTuning {
315 pub query_expansion: bool,
318 pub rerank: bool,
321 pub diversity_lambda: Option<f32>,
325}
326
327#[derive(Debug, Clone)]
329pub struct FactDraft {
330 pub subject: String,
331 pub relation: String,
332 pub object: String,
333 pub confidence: f64,
334}
335
336#[derive(Debug, Clone)]
338pub struct RememberResult {
339 pub observation: Hash,
340 pub facts: Vec<Hash>,
341}
342
343#[derive(Debug, Clone)]
345pub struct VerifyReport {
346 pub integrity: String,
347 pub fts_notes: Vec<String>,
350 pub grains: usize,
351 pub hash_mismatches: usize,
352 pub undecodable: usize,
353}
354
355#[derive(Debug, Clone)]
357pub struct StoreStats {
358 pub grains: usize,
359 pub current: usize,
360 pub triples: usize,
361 pub terms: usize,
362 pub ops: usize,
363 pub events_indexed: usize,
364}
365
366#[derive(Debug, Clone)]
368pub struct HistoryEntry {
369 pub hash: Hash,
370 pub object: String,
371 pub created_at: i64,
372 pub confidence: f64,
373 pub superseded_by: Option<Hash>,
374}
375
376#[derive(Debug, Clone)]
382pub struct ForkGroup {
383 pub namespace: String,
384 pub subject: String,
385 pub relation: String,
386 pub heads: Vec<Hash>,
387}
388
389const BUNDLE_MAGIC: &[u8; 4] = b"MGB1";
390
391pub const RRF_K0: f64 = 60.0;
394
395const REFINE_POOL: usize = 64;
400
401pub struct DejaDbOptions {
403 pub entity_relations: HashSet<String>,
406 pub index_text: bool,
410 pub encryption_key: Option<[u8; 32]>,
418}
419
420impl Default for DejaDbOptions {
421 fn default() -> Self {
422 let ents = [
423 "mg:delegates_to",
424 "mg:owned_by",
425 "mg:assigned_to",
426 "mg:depends_on",
427 "mg:handed_off_to",
428 "mg:capable_of",
429 "delegates_to",
430 "reports_to",
431 "part_of",
432 "knows",
433 ];
434 DejaDbOptions {
435 entity_relations: ents.iter().map(|s| s.to_string()).collect(),
436 index_text: true,
437 encryption_key: None,
438 }
439 }
440}
441
442const SCHEMA: &[&str] = &[
443 "CREATE TABLE IF NOT EXISTS meta(k TEXT PRIMARY KEY, v TEXT)",
444 "CREATE TABLE IF NOT EXISTS terms(id INTEGER PRIMARY KEY, term TEXT UNIQUE)",
445 "CREATE TABLE IF NOT EXISTS grains(
446 seq INTEGER PRIMARY KEY,
447 hash BLOB,
448 ns INTEGER, gtype INTEGER, created_at INTEGER,
449 s INTEGER, p INTEGER, o INTEGER,
450 vf INTEGER, vt INTEGER,
451 svf INTEGER, svt INTEGER,
452 superseded_by BLOB, supersedes BLOB,
453 text TEXT,
454 blob BLOB NOT NULL)",
455 "CREATE UNIQUE INDEX IF NOT EXISTS idx_grains_hash ON grains(hash)",
456 "CREATE TABLE IF NOT EXISTS embeddings(seq INTEGER PRIMARY KEY, vec BLOB)",
457 "CREATE TABLE IF NOT EXISTS triples(ns INTEGER, s INTEGER, p INTEGER, o INTEGER, seq INTEGER, cur INTEGER)",
458 "CREATE INDEX IF NOT EXISTS idx_spo ON triples(ns,s,p,o,seq)",
459 "CREATE INDEX IF NOT EXISTS idx_pos ON triples(ns,p,o,s,seq)",
460 "CREATE INDEX IF NOT EXISTS idx_triples_seq ON triples(seq)",
461 "CREATE TABLE IF NOT EXISTS osp(ns INTEGER, o INTEGER, s INTEGER, p INTEGER, seq INTEGER, cur INTEGER)",
462 "CREATE INDEX IF NOT EXISTS idx_osp ON osp(ns,o,s)",
463 "CREATE INDEX IF NOT EXISTS idx_osp_seq ON osp(seq)",
464 "CREATE TABLE IF NOT EXISTS entity_latest(ns INTEGER, s INTEGER, p INTEGER, o INTEGER, seq INTEGER, hash BLOB, PRIMARY KEY(ns,s,p))",
465 "CREATE TABLE IF NOT EXISTS heads(ns INTEGER, s INTEGER, p INTEGER, seq INTEGER, hash BLOB, created_at INTEGER, PRIMARY KEY(ns,s,p,seq))",
466 "CREATE TABLE IF NOT EXISTS oplog(op_seq INTEGER PRIMARY KEY, hlc INTEGER, op INTEGER, hash BLOB)",
467 "CREATE TABLE IF NOT EXISTS thread_idx(ns INTEGER, session INTEGER, seq INTEGER)",
468 "CREATE INDEX IF NOT EXISTS idx_thread ON thread_idx(ns, session, seq)",
469];
470
471fn pi(x: i64) -> Value {
472 Value::Integer(x)
473}
474fn pb(b: Vec<u8>) -> Value {
475 Value::Blob(b)
476}
477fn pt(s: &str) -> Value {
478 Value::Text(s.to_string())
479}
480fn opt_i(v: Option<i64>) -> Value {
481 match v {
482 Some(x) => Value::Integer(x),
483 None => Value::Null,
484 }
485}
486
487fn hex32(k: &[u8; 32]) -> String {
489 use std::fmt::Write;
490 let mut s = String::with_capacity(64);
491 for b in k {
492 let _ = write!(s, "{b:02x}");
493 }
494 s
495}
496
497const KDF_M_COST: u32 = 19_456; const KDF_T_COST: u32 = 2; const KDF_P_COST: u32 = 1; const KDF_SALT_LEN: usize = 16;
504
505fn kdf_err<E: std::fmt::Display>(e: E) -> DejaDbError {
506 DejaDbError::CryptoError(e.to_string())
507}
508
509fn load_or_create_kdf_sidecar(sidecar: &str) -> Result<([u8; KDF_SALT_LEN], u32, u32, u32)> {
513 match std::fs::read_to_string(sidecar) {
514 Ok(text) => parse_kdf_sidecar(&text, sidecar),
515 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
516 let mut salt = [0u8; KDF_SALT_LEN];
517 getrandom::getrandom(&mut salt).map_err(kdf_err)?;
518 let line = format!(
519 "v1 argon2id {} {KDF_M_COST} {KDF_T_COST} {KDF_P_COST}\n",
520 hex::encode(salt)
521 );
522 match std::fs::OpenOptions::new().write(true).create_new(true).open(sidecar) {
525 Ok(mut f) => {
526 use std::io::Write;
527 f.write_all(line.as_bytes()).map_err(kdf_err)?;
528 Ok((salt, KDF_M_COST, KDF_T_COST, KDF_P_COST))
529 }
530 Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {
531 let text = std::fs::read_to_string(sidecar).map_err(kdf_err)?;
532 parse_kdf_sidecar(&text, sidecar)
533 }
534 Err(e) => Err(kdf_err(e)),
535 }
536 }
537 Err(e) => Err(kdf_err(e)),
538 }
539}
540
541fn parse_kdf_sidecar(text: &str, sidecar: &str) -> Result<([u8; KDF_SALT_LEN], u32, u32, u32)> {
543 let toks: Vec<&str> = text.split_whitespace().collect();
544 if toks.len() != 6 || toks[0] != "v1" || toks[1] != "argon2id" {
545 return Err(kdf_err(format!("malformed KDF sidecar: {sidecar}")));
546 }
547 let salt_bytes = hex::decode(toks[2]).map_err(kdf_err)?;
548 if salt_bytes.len() != KDF_SALT_LEN {
549 return Err(kdf_err("KDF salt has wrong length"));
550 }
551 let mut salt = [0u8; KDF_SALT_LEN];
552 salt.copy_from_slice(&salt_bytes);
553 let m = toks[3].parse::<u32>().map_err(kdf_err)?;
554 let t = toks[4].parse::<u32>().map_err(kdf_err)?;
555 let p = toks[5].parse::<u32>().map_err(kdf_err)?;
556 if !(8..=1_048_576).contains(&m) || !(1..=16).contains(&t) || !(1..=16).contains(&p) {
559 return Err(kdf_err("KDF parameters out of range"));
560 }
561 Ok((salt, m, t, p))
562}
563
564impl DejaDB {
565 pub fn derive_key_for(path: &str, passphrase: &str) -> Result<zeroize::Zeroizing<[u8; 32]>> {
573 if passphrase.trim().is_empty() {
574 return Err(kdf_err("passphrase must not be empty or whitespace-only"));
575 }
576 let sidecar = format!("{path}.kdf");
577 let (salt, m, t, p) = load_or_create_kdf_sidecar(&sidecar)?;
578 let params = argon2::Params::new(m, t, p, Some(32)).map_err(kdf_err)?;
579 let argon = argon2::Argon2::new(argon2::Algorithm::Argon2id, argon2::Version::V0x13, params);
580 let mut key = zeroize::Zeroizing::new([0u8; 32]);
581 argon
582 .hash_password_into(passphrase.as_bytes(), &salt, &mut key[..])
583 .map_err(kdf_err)?;
584 Ok(key)
585 }
586
587 pub fn open_with_passphrase(path: &str, passphrase: &str) -> Result<Self> {
591 let key = Self::derive_key_for(path, passphrase)?;
592 Self::open_with(
593 path,
594 DejaDbOptions { encryption_key: Some(*key), ..DejaDbOptions::default() },
595 )
596 }
597}
598
599
600fn vec_to_json(v: &[f32]) -> String {
601 let mut s = String::with_capacity(v.len() * 8);
602 s.push('[');
603 for (i, x) in v.iter().enumerate() {
604 if i > 0 { s.push(','); }
605 s.push_str(&format!("{x}"));
606 }
607 s.push(']');
608 s
609}
610
611fn now_ms() -> i64 {
612 SystemTime::now()
613 .duration_since(UNIX_EPOCH)
614 .map(|d| d.as_millis() as i64)
615 .unwrap_or(0)
616}
617
618fn db_err<E: std::fmt::Display>(e: E) -> DejaDbError {
619 DejaDbError::Storage(e.to_string())
620}
621
622fn v_i64(v: &Value) -> Option<i64> {
623 match v {
624 Value::Integer(i) => Some(*i),
625 _ => None,
626 }
627}
628
629fn v_blob(v: &Value) -> Option<Vec<u8>> {
630 match v {
631 Value::Blob(b) => Some(b.clone()),
632 _ => None,
633 }
634}
635
636fn v_f64(v: &Value) -> Option<f64> {
637 match v {
638 Value::Real(r) => Some(*r),
639 Value::Integer(i) => Some(*i as f64),
640 _ => None,
641 }
642}
643
644fn seq_csv(seqs: &[i64]) -> String {
647 let mut s = String::with_capacity(seqs.len() * 6);
648 for (i, x) in seqs.iter().enumerate() {
649 if i > 0 {
650 s.push(',');
651 }
652 s.push_str(&x.to_string());
653 }
654 s
655}
656
657struct GrainPrep {
659 blob: Vec<u8>,
660 hash: Hash,
661 ns_id: i64,
662 s: Option<i64>,
663 p: Option<i64>,
664 o: Option<i64>,
665 osp: bool,
666 session: Option<i64>,
667 vf: Option<i64>,
668 vt: Option<i64>,
669 created: i64,
670 gtype: i64,
671 text: Option<String>,
672 embedding: Option<Vec<f32>>,
673}
674
675struct GrainView {
677 ns: String,
678 subject: Option<String>,
679 relation: Option<String>,
680 object: Option<String>,
681 session: Option<String>,
682 vf: Option<i64>,
683 vt: Option<i64>,
684 created_at: i64,
685 gtype: u8,
686}
687
688fn extract_view(view: &DeserializedGrain) -> GrainView {
689 GrainView {
690 ns: view.get_str("namespace").unwrap_or("shared").to_string(),
691 subject: view.get_str("subject").map(str::to_string),
692 relation: view.get_str("relation").map(str::to_string),
693 object: view.get_str("object").map(str::to_string),
694 session: view.get_str("session_id").map(str::to_string),
695 vf: view.get_i64("valid_from"),
696 vt: view.get_i64("valid_to"),
697 created_at: view.get_i64("created_at").unwrap_or_else(now_ms),
698 gtype: view.grain_type as u8,
699 }
700}
701
702fn projected_text(view: &DeserializedGrain) -> Option<String> {
709 if let Some(et) = view.get_str("embedding_text") {
710 if !et.trim().is_empty() {
711 return Some(et.to_string());
712 }
713 }
714 let mut parts: Vec<String> = Vec::new();
715 if let (Some(s), Some(r), Some(o)) = (
716 view.get_str("subject"),
717 view.get_str("relation"),
718 view.get_str("object"),
719 ) {
720 parts.push(format!("{s} {r} {o}"));
721 }
722 if let Some(c) = view.get_str("content") {
723 parts.push(c.to_string());
724 }
725 if parts.is_empty() {
726 None
727 } else {
728 Some(parts.join(" "))
729 }
730}
731
732pub struct DejaDB {
734 rt: tokio::runtime::Runtime,
735 _db: turso::Database,
736 conn: Connection,
737 dict: HashMap<String, i64>,
738 next_term: i64,
739 next_seq: i64,
740 next_op: i64,
741 hlc_last: i64,
742 entity_rels: HashSet<String>,
743 index_text: bool,
744 embedder: Option<Box<dyn EmbedBackend>>,
745 reranker: Option<Box<dyn RerankBackend>>,
747 expander: Option<Box<dyn QueryExpander>>,
750 meta_embed: Option<(String, usize)>,
754 warnings: Vec<String>,
757 blob_dir: std::path::PathBuf,
758 st_probe_sp: Option<turso::Statement>,
760 st_probe_s: Option<turso::Statement>,
761 st_fetch_seq: Option<turso::Statement>,
762 st_latest: Option<turso::Statement>,
763}
764
765async fn ensure_stmt<'a>(
766 slot: &'a mut Option<turso::Statement>,
767 conn: &Connection,
768 sql: &str,
769) -> Result<&'a mut turso::Statement> {
770 if slot.is_none() {
771 *slot = Some(conn.prepare(sql).await.map_err(db_err)?);
772 }
773 Ok(slot.as_mut().unwrap())
774}
775
776impl DejaDB {
777 pub fn open(path: &str) -> Result<Self> {
782 Self::open_internal(path, None)
783 }
784
785 pub fn open_with(path: &str, opts: DejaDbOptions) -> Result<Self> {
789 Self::open_internal(path, Some(opts))
790 }
791
792 pub fn open_encrypted(path: &str, key: [u8; 32]) -> Result<Self> {
798 Self::open_with(
799 path,
800 DejaDbOptions { encryption_key: Some(key), ..DejaDbOptions::default() },
801 )
802 }
803
804 fn open_internal(path: &str, explicit: Option<DejaDbOptions>) -> Result<Self> {
805 let rt = tokio::runtime::Builder::new_current_thread()
806 .enable_all()
807 .build()
808 .map_err(db_err)?;
809 let enc_key = explicit
813 .as_ref()
814 .and_then(|o| o.encryption_key)
815 .map(zeroize::Zeroizing::new);
816 let (db, conn) = rt.block_on(async {
817 let mut b = Builder::new_local(path).experimental_index_method(true);
818 if let Some(k) = &enc_key {
819 let hexkey = zeroize::Zeroizing::new(hex32(k));
827 b = b.experimental_encryption(true).with_encryption(turso::EncryptionOpts {
828 cipher: "aes256gcm".to_string(),
829 hexkey: (*hexkey).clone(),
830 });
831 }
832 let db = b.build().await.map_err(db_err)?;
833 let conn = db.connect().map_err(db_err)?;
834 for sql in SCHEMA {
835 conn.execute(sql, ()).await.map_err(db_err)?;
836 }
837 Ok::<_, DejaDbError>((db, conn))
838 })?;
839
840 let meta: HashMap<String, String> = rt.block_on(async {
842 let mut m = HashMap::new();
843 let mut rows = conn.query("SELECT k, v FROM meta", ()).await.map_err(db_err)?;
844 while let Some(row) = rows.next().await.map_err(db_err)? {
845 if let (Value::Text(k), Value::Text(v)) = (
846 row.get_value(0).map_err(db_err)?,
847 row.get_value(1).map_err(db_err)?,
848 ) {
849 m.insert(k, v);
850 }
851 }
852 Ok::<_, DejaDbError>(m)
853 })?;
854 let declared_text = meta.get("text_index").map(|v| v == "1");
855 let declared_rels: Option<HashSet<String>> = meta
856 .get("entity_relations")
857 .and_then(|v| serde_json::from_str::<Vec<String>>(v).ok())
858 .map(|v| v.into_iter().collect());
859 let meta_embed = match (
860 meta.get("embedding_model"),
861 meta.get("embedding_dim").and_then(|d| d.parse::<usize>().ok()),
862 ) {
863 (Some(m), Some(d)) => Some((m.clone(), d)),
864 _ => None,
865 };
866
867 let mut warnings: Vec<String> = Vec::new();
868 if enc_key.is_some() {
869 warnings.push(
870 "encryption-at-rest ON (AES-256-GCM): the memory database is encrypted; the \
871 .blobs CAS sidecar is NOT yet encrypted — keep sensitive media out of this file \
872 (or avoid put_blob) until blob encryption lands"
873 .into(),
874 );
875 }
876 let mut opts = match explicit {
877 Some(o) => {
878 if let Some(d) = declared_text {
879 if d != o.index_text {
880 warnings.push(format!(
881 "file declared text_index={}; explicit open changed it to {} (re-stamped) — \
882 grains written under the old setting keep their old indexing",
883 if d { "on" } else { "off" },
884 if o.index_text { "on" } else { "off" },
885 ));
886 }
887 }
888 if let Some(ref d) = declared_rels {
889 if *d != o.entity_relations {
890 warnings.push(
891 "file-declared entity_relations differ from explicit options (re-stamped) — \
892 OSP rows indexed under the old set are unchanged"
893 .into(),
894 );
895 }
896 }
897 o
898 }
899 None => DejaDbOptions {
900 index_text: declared_text.unwrap_or(true),
901 entity_relations: declared_rels
902 .unwrap_or_else(|| DejaDbOptions::default().entity_relations),
903 encryption_key: None,
904 },
905 };
906 opts.encryption_key.zeroize();
910
911 rt.block_on(async {
913 conn.execute(
914 "INSERT OR REPLACE INTO meta(k, v) VALUES ('text_index', ?1)",
915 (pt(if opts.index_text { "1" } else { "0" }),),
916 )
917 .await
918 .map_err(db_err)?;
919 let mut rels: Vec<&String> = opts.entity_relations.iter().collect();
920 rels.sort();
921 let rels = serde_json::to_string(&rels).unwrap_or_else(|_| "[]".into());
922 conn.execute(
923 "INSERT OR REPLACE INTO meta(k, v) VALUES ('entity_relations', ?1)",
924 (pt(&rels),),
925 )
926 .await
927 .map_err(db_err)?;
928 if opts.index_text {
929 conn.execute("CREATE INDEX IF NOT EXISTS idx_fts ON grains USING fts (text)", ())
934 .await
935 .map_err(db_err)?;
936 }
937 Ok::<_, DejaDbError>(())
938 })?;
939
940 let (dict, next_term, next_seq, next_op, hlc_last) = rt.block_on(async {
942 let mut dict = HashMap::new();
943 let mut next_term = 1i64;
944 {
945 let mut rows = conn.query("SELECT id, term FROM terms", ()).await.map_err(db_err)?;
946 while let Some(row) = rows.next().await.map_err(db_err)? {
947 let id = v_i64(&row.get_value(0).map_err(db_err)?).unwrap_or(0);
948 if let Value::Text(t) = row.get_value(1).map_err(db_err)? {
949 dict.insert(t, id);
950 }
951 next_term = next_term.max(id + 1);
952 }
953 }
954 let one = |sql: &'static str| {
955 let conn = conn.clone();
956 async move {
957 let mut rows = conn.query(sql, ()).await.map_err(db_err)?;
958 let v = match rows.next().await.map_err(db_err)? {
959 Some(row) => v_i64(&row.get_value(0).map_err(db_err)?).unwrap_or(0),
960 None => 0,
961 };
962 Ok::<i64, DejaDbError>(v)
963 }
964 };
965 let next_seq = one("SELECT COALESCE(MAX(seq),0) FROM grains").await? + 1;
966 let next_op = one("SELECT COALESCE(MAX(op_seq),0) FROM oplog").await? + 1;
967 let hlc_last = one("SELECT COALESCE(MAX(hlc),0) FROM oplog").await?;
968 Ok::<_, DejaDbError>((dict, next_term, next_seq, next_op, hlc_last))
969 })?;
970
971 let blob_dir = std::path::PathBuf::from(format!("{}.blobs", path));
972 std::fs::create_dir_all(&blob_dir).map_err(db_err)?;
973
974 Ok(DejaDB {
975 rt,
976 _db: db,
977 conn,
978 dict,
979 next_term,
980 next_seq,
981 next_op,
982 hlc_last,
983 entity_rels: opts.entity_relations,
984 index_text: opts.index_text,
985 embedder: None,
986 reranker: None,
987 expander: None,
988 meta_embed,
989 warnings,
990 blob_dir,
991 st_probe_sp: None,
992 st_probe_s: None,
993 st_fetch_seq: None,
994 st_latest: None,
995 })
996 }
997
998 pub fn set_embedder(&mut self, e: Box<dyn EmbedBackend>) {
1006 let (model, dim) = (e.model().to_string(), e.dim());
1007 match &self.meta_embed {
1008 Some((m, d)) => {
1009 if *d != dim {
1010 self.warnings.push(format!(
1011 "embedding mismatch: file vectors are {m}@{d}, injected backend is \
1012 {model}@{dim} — vector recall may be degraded"
1013 ));
1014 } else if *m != model && m != "unspecified" && model != "unspecified" {
1015 self.warnings.push(format!(
1016 "embedding model differs: file declares {m}, injected {model} (same dim {dim})"
1017 ));
1018 }
1019 }
1020 None => {
1021 let conn = &self.conn;
1022 let ok = self.rt.block_on(async {
1023 conn.execute(
1024 "INSERT OR REPLACE INTO meta(k, v) VALUES ('embedding_model', ?1)",
1025 (pt(&model),),
1026 )
1027 .await
1028 .map_err(db_err)?;
1029 conn.execute(
1030 "INSERT OR REPLACE INTO meta(k, v) VALUES ('embedding_dim', ?1)",
1031 (pt(&dim.to_string()),),
1032 )
1033 .await
1034 .map_err(db_err)?;
1035 Ok::<_, DejaDbError>(())
1036 });
1037 if ok.is_ok() {
1038 self.meta_embed = Some((model, dim));
1039 }
1040 }
1041 }
1042 self.embedder = Some(e);
1043 }
1044
1045 pub fn set_reranker(&mut self, r: Box<dyn RerankBackend>) {
1049 self.reranker = Some(r);
1050 }
1051
1052 pub fn has_reranker(&self) -> bool {
1054 self.reranker.is_some()
1055 }
1056
1057 pub fn set_query_expander(&mut self, e: Box<dyn QueryExpander>) {
1061 self.expander = Some(e);
1062 }
1063
1064 pub fn index_text_enabled(&self) -> bool {
1067 self.index_text
1068 }
1069
1070 pub fn defer_text_index(&mut self) -> Result<bool> {
1083 if !self.index_text {
1084 return Ok(false);
1085 }
1086 let conn = &self.conn;
1087 self.rt.block_on(async {
1088 match conn.execute("DROP INDEX idx_fts", ()).await {
1089 Ok(_) => Ok(true),
1090 Err(e) => {
1091 let s = e.to_string().to_ascii_lowercase();
1093 if s.contains("no such index") {
1094 Ok(false)
1095 } else {
1096 Err(db_err(e))
1097 }
1098 }
1099 }
1100 })
1101 }
1102
1103 pub fn rebuild_text_index(&mut self) -> Result<usize> {
1115 if !self.index_text {
1116 return Err(DejaDbError::Validation(
1117 "text indexing is off for this file — reopen with --index-text true \
1118 (open_with index_text) before rebuilding the FTS index"
1119 .to_string(),
1120 ));
1121 }
1122 let conn = &self.conn;
1126 let rows: Vec<(i64, Vec<u8>)> = self.rt.block_on(async {
1127 let mut out = Vec::new();
1128 let mut rows = conn
1129 .query("SELECT seq, blob FROM grains WHERE text IS NULL", ())
1130 .await
1131 .map_err(db_err)?;
1132 while let Some(row) = rows.next().await.map_err(db_err)? {
1133 let seq = v_i64(&row.get_value(0).map_err(db_err)?).unwrap_or(0);
1134 if let Value::Blob(b) = row.get_value(1).map_err(db_err)? {
1135 out.push((seq, b));
1136 }
1137 }
1138 Ok::<_, DejaDbError>(out)
1139 })?;
1140 let mut updates: Vec<(i64, String)> = Vec::new();
1141 for (seq, blob) in &rows {
1142 let view = deserialize_blob(blob)?;
1143 if let Some(t) = projected_text(&view) {
1144 updates.push((*seq, t));
1145 }
1146 }
1147 let backfilled = updates.len();
1148 self.rt.block_on(async {
1149 conn.execute("BEGIN", ()).await.map_err(db_err)?;
1150 let r = async {
1151 for (seq, t) in &updates {
1152 conn.execute(
1153 "UPDATE grains SET text = ?1 WHERE seq = ?2",
1154 (pt(t), pi(*seq)),
1155 )
1156 .await
1157 .map_err(db_err)?;
1158 }
1159 Ok::<_, DejaDbError>(())
1160 }
1161 .await;
1162 match r {
1163 Ok(()) => conn.execute("COMMIT", ()).await.map_err(db_err).map(|_| ()),
1164 Err(e) => {
1165 let _ = conn.execute("ROLLBACK", ()).await;
1166 Err(e)
1167 }
1168 }
1169 })?;
1170 self.rt.block_on(async {
1172 conn.execute(
1173 "CREATE INDEX IF NOT EXISTS idx_fts ON grains USING fts (text)",
1174 (),
1175 )
1176 .await
1177 .map_err(db_err)
1178 })?;
1179 Ok(backfilled)
1180 }
1181
1182 pub fn embedder_dim(&self) -> Option<usize> {
1185 self.embedder.as_ref().map(|e| e.dim())
1186 }
1187
1188 pub fn declared_embedding(&self) -> Option<(&str, usize)> {
1191 self.meta_embed.as_ref().map(|(m, d)| (m.as_str(), *d))
1192 }
1193
1194 pub fn open_warnings(&self) -> &[String] {
1197 &self.warnings
1198 }
1199
1200 fn next_hlc(&mut self) -> i64 {
1201 let wall = now_ms() << 16;
1202 self.hlc_last = if wall > self.hlc_last { wall } else { self.hlc_last + 1 };
1203 self.hlc_last
1204 }
1205
1206 fn term_id(&mut self, term: &str) -> Result<i64> {
1208 if let Some(id) = self.dict.get(term) {
1209 return Ok(*id);
1210 }
1211 let id = self.next_term;
1212 self.next_term += 1;
1213 let conn = &self.conn;
1214 self.rt.block_on(async {
1215 conn.execute("INSERT INTO terms(id, term) VALUES (?1, ?2)", (pi(id), pt(term)))
1216 .await
1217 .map_err(db_err)
1218 })?;
1219 self.dict.insert(term.to_string(), id);
1220 Ok(id)
1221 }
1222
1223 fn term_lookup(&self, term: &str) -> Option<i64> {
1224 self.dict.get(term).copied()
1225 }
1226
1227 fn term_str(&self, id: i64) -> Option<String> {
1228 self.dict
1229 .iter()
1230 .find(|(_, v)| **v == id)
1231 .map(|(k, _)| k.clone())
1232 }
1233
1234 pub fn add<G: Grain + 'static>(&mut self, grain: &G) -> Result<Hash> {
1238 self.add_batch_inner(std::slice::from_ref(&(grain as &dyn AddableDyn)))
1239 .map(|mut v| v.remove(0))
1240 }
1241
1242 pub fn add_batch(&mut self, grains: &[&dyn AddableDyn]) -> Result<Vec<Hash>> {
1244 self.add_batch_inner(grains)
1245 }
1246
1247 pub fn add_if_novel<G: Grain + 'static>(&mut self, grain: &G) -> Result<(Hash, bool)> {
1260 self.add_dyn_if_novel(grain as &dyn AddableDyn)
1261 }
1262
1263 fn add_dyn_if_novel(&mut self, grain: &dyn AddableDyn) -> Result<(Hash, bool)> {
1264 let (blob, _hash) = grain.serialize_dyn()?;
1265 let gv = extract_view(&deserialize_blob(&blob)?);
1266 if let (Some(sj), Some(rl), Some(ob)) =
1267 (gv.subject.as_deref(), gv.relation.as_deref(), gv.object.as_deref())
1268 {
1269 if let (Some(ns_id), Some(s_id), Some(p_id), Some(o_id)) = (
1272 self.term_lookup(&gv.ns),
1273 self.term_lookup(sj),
1274 self.term_lookup(rl),
1275 self.term_lookup(ob),
1276 ) {
1277 if let Some(existing) = self.head_hash_for_object(ns_id, s_id, p_id, o_id)? {
1278 return Ok((existing, false));
1279 }
1280 }
1281 }
1282 let h = self
1283 .add_batch_inner(std::slice::from_ref(&grain))?
1284 .remove(0);
1285 Ok((h, true))
1286 }
1287
1288 fn head_hash_for_object(&mut self, ns: i64, s: i64, p: i64, o: i64) -> Result<Option<Hash>> {
1291 let conn = &self.conn;
1292 let bytes = self.rt.block_on(async {
1293 let mut rows = conn
1294 .query(
1295 "SELECT hash FROM entity_latest WHERE ns=?1 AND s=?2 AND p=?3 AND o=?4",
1296 (pi(ns), pi(s), pi(p), pi(o)),
1297 )
1298 .await
1299 .map_err(db_err)?;
1300 Ok::<_, DejaDbError>(match rows.next().await.map_err(db_err)? {
1301 Some(row) => v_blob(&row.get_value(0).map_err(db_err)?),
1302 None => None,
1303 })
1304 })?;
1305 match bytes {
1306 Some(b) => Ok(Some(Hash::try_from_bytes(&b)?)),
1307 None => Ok(None),
1308 }
1309 }
1310
1311 fn prep_from_blob(&mut self, blob: Vec<u8>, hash: Hash) -> Result<GrainPrep> {
1313 let view = deserialize_blob(&blob)?;
1314 let gv = extract_view(&view);
1315 let ns_id = self.term_id(&gv.ns)?;
1316 let (mut s, mut p, mut o, mut osp) = (None, None, None, false);
1317 if let (Some(sj), Some(rl), Some(ob)) = (&gv.subject, &gv.relation, &gv.object) {
1318 s = Some(self.term_id(sj)?);
1319 p = Some(self.term_id(rl)?);
1320 o = Some(self.term_id(ob)?);
1321 osp = self.entity_rels.contains(rl.as_str());
1322 }
1323 let session = match &gv.session {
1324 Some(x) => Some(self.term_id(x)?),
1325 None => None,
1326 };
1327 let projected = projected_text(&view);
1328 let text = if self.index_text { projected.clone() } else { None };
1329 let embed_text = projected;
1330 let embedding = match (&self.embedder, &embed_text) {
1331 (Some(e), Some(t)) => Some(e.embed(t)?),
1332 _ => None,
1333 };
1334 Ok(GrainPrep {
1335 blob,
1336 hash,
1337 ns_id,
1338 s,
1339 p,
1340 o,
1341 osp,
1342 session,
1343 vf: gv.vf,
1344 vt: gv.vt,
1345 created: gv.created_at,
1346 gtype: gv.gtype as i64,
1347 text,
1348 embedding,
1349 })
1350 }
1351
1352 fn add_batch_inner(&mut self, grains: &[&dyn AddableDyn]) -> Result<Vec<Hash>> {
1353 let mut preps = Vec::with_capacity(grains.len());
1355 for g in grains {
1356 let (blob, hash) = g.serialize_dyn()?;
1357 preps.push(self.prep_from_blob(blob, hash)?);
1358 }
1359
1360 let first_seq = self.next_seq;
1361 self.next_seq += preps.len() as i64;
1362 let first_op = self.next_op;
1363 self.next_op += preps.len() as i64;
1364 let hlc0 = self.next_hlc();
1365 self.hlc_last = hlc0 + preps.len() as i64 - 1;
1366
1367 let conn = &self.conn;
1368 let hashes: Vec<Hash> = preps.iter().map(|p| p.hash).collect();
1369 self.rt.block_on(async {
1370 conn.execute("BEGIN", ()).await.map_err(db_err)?;
1371 let r = async {
1372 let mut st_g = conn
1373 .prepare(
1374 "INSERT INTO grains(seq,hash,ns,gtype,created_at,s,p,o,vf,vt,svf,svt,superseded_by,supersedes,text,blob)
1375 VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,NULL,NULL,NULL,?12,?13)",
1376 )
1377 .await
1378 .map_err(db_err)?;
1379 let mut st_t = conn
1380 .prepare("INSERT INTO triples(ns,s,p,o,seq,cur) VALUES (?1,?2,?3,?4,?5,1)")
1381 .await
1382 .map_err(db_err)?;
1383 let mut st_o = conn
1384 .prepare("INSERT INTO osp(ns,o,s,p,seq,cur) VALUES (?1,?2,?3,?4,?5,1)")
1385 .await
1386 .map_err(db_err)?;
1387 let mut st_e = conn
1388 .prepare("INSERT OR REPLACE INTO entity_latest(ns,s,p,o,seq,hash) VALUES (?1,?2,?3,?4,?5,?6)")
1389 .await
1390 .map_err(db_err)?;
1391 let mut st_l = conn
1392 .prepare("INSERT INTO oplog(op_seq,hlc,op,hash) VALUES (?1,?2,?3,?4)")
1393 .await
1394 .map_err(db_err)?;
1395 let mut st_th = conn
1396 .prepare("INSERT INTO thread_idx(ns,session,seq) VALUES (?1,?2,?3)")
1397 .await
1398 .map_err(db_err)?;
1399 for (i, pr) in preps.iter().enumerate() {
1400 let seq = first_seq + i as i64;
1401 st_g.execute((
1402 pi(seq),
1403 pb(pr.hash.as_bytes().to_vec()),
1404 pi(pr.ns_id),
1405 pi(pr.gtype),
1406 pi(pr.created),
1407 opt_i(pr.s),
1408 opt_i(pr.p),
1409 opt_i(pr.o),
1410 opt_i(pr.vf),
1411 opt_i(pr.vt),
1412 pi(pr.created),
1413 match &pr.text { Some(t) => pt(t), None => Value::Null },
1414 pb(pr.blob.clone()),
1415 ))
1416 .await
1417 .map_err(db_err)?;
1418 if let (Some(s), Some(p), Some(o)) = (pr.s, pr.p, pr.o) {
1419 st_t.execute((pi(pr.ns_id), pi(s), pi(p), pi(o), pi(seq)))
1420 .await
1421 .map_err(db_err)?;
1422 if pr.osp {
1423 st_o.execute((pi(pr.ns_id), pi(o), pi(s), pi(p), pi(seq)))
1424 .await
1425 .map_err(db_err)?;
1426 }
1427 st_e.execute((
1428 pi(pr.ns_id),
1429 pi(s),
1430 pi(p),
1431 pi(o),
1432 pi(seq),
1433 pb(pr.hash.as_bytes().to_vec()),
1434 ))
1435 .await
1436 .map_err(db_err)?;
1437 conn.execute(
1438 "DELETE FROM heads WHERE ns=?1 AND s=?2 AND p=?3",
1439 (pi(pr.ns_id), pi(s), pi(p)),
1440 )
1441 .await
1442 .map_err(db_err)?;
1443 conn.execute(
1444 "INSERT INTO heads(ns,s,p,seq,hash,created_at) VALUES (?1,?2,?3,?4,?5,?6)",
1445 (pi(pr.ns_id), pi(s), pi(p), pi(seq), pb(pr.hash.as_bytes().to_vec()), pi(pr.created)),
1446 )
1447 .await
1448 .map_err(db_err)?;
1449 }
1450 if let Some(sess) = pr.session {
1451 st_th
1452 .execute((pi(pr.ns_id), pi(sess), pi(seq)))
1453 .await
1454 .map_err(db_err)?;
1455 }
1456 if let Some(ref emb) = pr.embedding {
1457 conn.execute(
1458 "INSERT INTO embeddings(seq, vec) VALUES (?1, vector32(?2))",
1459 (pi(seq), pt(&vec_to_json(emb))),
1460 )
1461 .await
1462 .map_err(db_err)?;
1463 }
1464 st_l.execute((
1465 pi(first_op + i as i64),
1466 pi(hlc0 + i as i64),
1467 pi(OP_ADD),
1468 pb(pr.hash.as_bytes().to_vec()),
1469 ))
1470 .await
1471 .map_err(db_err)?;
1472 }
1473 Ok::<(), DejaDbError>(())
1474 }
1475 .await;
1476 match r {
1477 Ok(()) => conn.execute("COMMIT", ()).await.map_err(db_err).map(|_| ()),
1478 Err(e) => {
1479 let _ = conn.execute("ROLLBACK", ()).await;
1480 Err(e)
1481 }
1482 }
1483 })?;
1484 Ok(hashes)
1485 }
1486
1487 pub fn grains_derived_from(&mut self, parent: &Hash) -> Result<Vec<DeserializedGrain>> {
1497 let parent_hex = parent.to_hex();
1498 let conn = &self.conn;
1499 let blobs = self.rt.block_on(async {
1500 let mut rows = conn
1501 .query("SELECT blob FROM grains ORDER BY seq DESC", ())
1502 .await
1503 .map_err(db_err)?;
1504 let mut out = Vec::new();
1505 while let Some(row) = rows.next().await.map_err(db_err)? {
1506 if let Some(b) = v_blob(&row.get_value(0).map_err(db_err)?) {
1507 out.push(b);
1508 }
1509 }
1510 Ok::<_, DejaDbError>(out)
1511 })?;
1512 let mut result = Vec::new();
1513 for b in &blobs {
1514 let g = deserialize_blob(b)?;
1515 if g.get_str("derived_from") == Some(parent_hex.as_str()) {
1516 result.push(g);
1517 }
1518 }
1519 Ok(result)
1520 }
1521
1522 pub fn recent(
1527 &mut self,
1528 ns: &str,
1529 gtype: Option<dejadb_core::types::GrainType>,
1530 limit: usize,
1531 ) -> Result<Vec<DeserializedGrain>> {
1532 let ns_id = match self.term_lookup(ns) {
1533 Some(x) => x,
1534 None => return Ok(Vec::new()),
1535 };
1536 let gt_ord = gtype.map(|g| g as u8 as i64);
1539 let conn = &self.conn;
1540 let blobs = self.rt.block_on(async {
1541 let mut out = Vec::new();
1542 let mut rows = match gt_ord {
1543 Some(gt) => conn
1544 .query(
1545 "SELECT blob FROM grains WHERE ns=?1 AND gtype=?2 ORDER BY seq DESC LIMIT ?3",
1546 (pi(ns_id), pi(gt), pi(limit as i64)),
1547 )
1548 .await
1549 .map_err(db_err)?,
1550 None => conn
1551 .query(
1552 "SELECT blob FROM grains WHERE ns=?1 ORDER BY seq DESC LIMIT ?2",
1553 (pi(ns_id), pi(limit as i64)),
1554 )
1555 .await
1556 .map_err(db_err)?,
1557 };
1558 while let Some(row) = rows.next().await.map_err(db_err)? {
1559 if let Some(b) = v_blob(&row.get_value(0).map_err(db_err)?) {
1560 out.push(b);
1561 }
1562 }
1563 Ok::<_, DejaDbError>(out)
1564 })?;
1565 blobs.iter().map(|b| deserialize_blob(b)).collect()
1566 }
1567
1568 pub fn get(&mut self, hash: &Hash) -> Result<DeserializedGrain> {
1570 let conn = &self.conn;
1571 let blob = self.rt.block_on(async {
1572 let mut rows = conn
1573 .query(
1574 "SELECT blob FROM grains WHERE hash = ?1",
1575 (pb(hash.as_bytes().to_vec()),),
1576 )
1577 .await
1578 .map_err(db_err)?;
1579 match rows.next().await.map_err(db_err)? {
1580 Some(row) => v_blob(&row.get_value(0).map_err(db_err)?)
1581 .ok_or_else(|| DejaDbError::Storage("blob column not a blob".into())),
1582 None => Err(DejaDbError::NotFound(*hash)),
1583 }
1584 })?;
1585 deserialize_blob(&blob)
1586 }
1587
1588 pub fn recall(
1591 &mut self,
1592 ns: &str,
1593 subject: &str,
1594 relation: Option<&str>,
1595 k: usize,
1596 ) -> Result<Vec<DeserializedGrain>> {
1597 let (ns_id, s_id) = match (self.term_lookup(ns), self.term_lookup(subject)) {
1598 (Some(a), Some(b)) => (a, b),
1599 _ => return Ok(Vec::new()),
1600 };
1601 let p_id = match relation {
1602 Some(r) => match self.term_lookup(r) {
1603 Some(x) => Some(x),
1604 None => return Ok(Vec::new()),
1605 },
1606 None => None,
1607 };
1608 let conn = &self.conn;
1609 let rt = &self.rt;
1610 let slot_sp = &mut self.st_probe_sp;
1611 let slot_s = &mut self.st_probe_s;
1612 let slot_f = &mut self.st_fetch_seq;
1613 let blobs = rt.block_on(async {
1614 let mut out = Vec::new();
1615 let mut seqs: Vec<i64> = Vec::new();
1616 match p_id {
1617 Some(p) => {
1618 let st = ensure_stmt(
1619 slot_sp,
1620 conn,
1621 "SELECT seq FROM triples WHERE ns=?1 AND s=?2 AND p=?3 AND cur=1 ORDER BY seq DESC LIMIT ?4",
1622 )
1623 .await?;
1624 let mut rows = st
1625 .query((pi(ns_id), pi(s_id), pi(p), pi(k as i64)))
1626 .await
1627 .map_err(db_err)?;
1628 while let Some(row) = rows.next().await.map_err(db_err)? {
1629 if let Some(x) = v_i64(&row.get_value(0).map_err(db_err)?) {
1630 seqs.push(x);
1631 }
1632 }
1633 }
1634 None => {
1635 let st = ensure_stmt(
1636 slot_s,
1637 conn,
1638 "SELECT seq FROM triples WHERE ns=?1 AND s=?2 AND cur=1 ORDER BY seq DESC LIMIT ?3",
1639 )
1640 .await?;
1641 let mut rows = st
1642 .query((pi(ns_id), pi(s_id), pi(k as i64)))
1643 .await
1644 .map_err(db_err)?;
1645 while let Some(row) = rows.next().await.map_err(db_err)? {
1646 if let Some(x) = v_i64(&row.get_value(0).map_err(db_err)?) {
1647 seqs.push(x);
1648 }
1649 }
1650 }
1651 }
1652 let st_f = ensure_stmt(slot_f, conn, "SELECT blob FROM grains WHERE seq = ?1").await?;
1653 for seq in seqs {
1654 let mut rows = st_f.query((pi(seq),)).await.map_err(db_err)?;
1655 if let Some(row) = rows.next().await.map_err(db_err)? {
1656 if let Some(b) = v_blob(&row.get_value(0).map_err(db_err)?) {
1657 out.push(b);
1658 }
1659 }
1660 }
1661 Ok::<_, DejaDbError>(out)
1662 })?;
1663 blobs.iter().map(|b| deserialize_blob(b)).collect()
1664 }
1665
1666 pub fn latest(&mut self, ns: &str, subject: &str, relation: &str) -> Result<Option<DeserializedGrain>> {
1668 let (ns_id, s_id, p_id) = match (
1669 self.term_lookup(ns),
1670 self.term_lookup(subject),
1671 self.term_lookup(relation),
1672 ) {
1673 (Some(a), Some(b), Some(c)) => (a, b, c),
1674 _ => return Ok(None),
1675 };
1676 let conn = &self.conn;
1677 let rt = &self.rt;
1678 let slot = &mut self.st_latest;
1679 let hash = rt.block_on(async {
1680 let st = ensure_stmt(
1681 slot,
1682 conn,
1683 "SELECT hash FROM entity_latest WHERE ns=?1 AND s=?2 AND p=?3",
1684 )
1685 .await?;
1686 let mut rows = st
1687 .query((pi(ns_id), pi(s_id), pi(p_id)))
1688 .await
1689 .map_err(db_err)?;
1690 Ok::<_, DejaDbError>(match rows.next().await.map_err(db_err)? {
1691 Some(row) => v_blob(&row.get_value(0).map_err(db_err)?),
1692 None => None,
1693 })
1694 })?;
1695 match hash {
1696 Some(h) => {
1697 let h = Hash::try_from_bytes(&h)?;
1698 Ok(Some(self.get(&h)?))
1699 }
1700 None => Ok(None),
1701 }
1702 }
1703
1704 pub fn thread_tail(&mut self, ns: &str, session: &str, n: usize) -> Result<Vec<DeserializedGrain>> {
1706 let (ns_id, sess_id) = match (self.term_lookup(ns), self.term_lookup(session)) {
1707 (Some(a), Some(b)) => (a, b),
1708 _ => return Ok(Vec::new()),
1709 };
1710 let conn = &self.conn;
1711 let blobs = self.rt.block_on(async {
1712 let mut seqs = Vec::new();
1713 {
1714 let mut rows = conn
1715 .query(
1716 "SELECT seq FROM thread_idx WHERE ns=?1 AND session=?2 ORDER BY seq DESC LIMIT ?3",
1717 (pi(ns_id), pi(sess_id), pi(n as i64)),
1718 )
1719 .await
1720 .map_err(db_err)?;
1721 while let Some(row) = rows.next().await.map_err(db_err)? {
1722 if let Some(x) = v_i64(&row.get_value(0).map_err(db_err)?) {
1723 seqs.push(x);
1724 }
1725 }
1726 }
1727 let mut out = Vec::new();
1728 for seq in seqs.into_iter().rev() {
1729 let mut rows = conn
1730 .query("SELECT blob FROM grains WHERE seq = ?1", (pi(seq),))
1731 .await
1732 .map_err(db_err)?;
1733 if let Some(row) = rows.next().await.map_err(db_err)? {
1734 if let Some(b) = v_blob(&row.get_value(0).map_err(db_err)?) {
1735 out.push(b);
1736 }
1737 }
1738 }
1739 Ok::<_, DejaDbError>(out)
1740 })?;
1741 blobs.iter().map(|b| deserialize_blob(b)).collect()
1742 }
1743
1744 pub fn supersede<G: Grain + 'static>(&mut self, old: &Hash, new_grain: &mut G) -> Result<Hash> {
1750 let conn = &self.conn;
1752 let old_row = self.rt.block_on(async {
1753 let mut rows = conn
1754 .query(
1755 "SELECT seq, ns, s, p, svt FROM grains WHERE hash = ?1",
1756 (pb(old.as_bytes().to_vec()),),
1757 )
1758 .await
1759 .map_err(db_err)?;
1760 match rows.next().await.map_err(db_err)? {
1761 Some(row) => {
1762 let seq = v_i64(&row.get_value(0).map_err(db_err)?).unwrap_or(0);
1763 let ns = v_i64(&row.get_value(1).map_err(db_err)?);
1764 let s = v_i64(&row.get_value(2).map_err(db_err)?);
1765 let p = v_i64(&row.get_value(3).map_err(db_err)?);
1766 let svt = v_i64(&row.get_value(4).map_err(db_err)?);
1767 Ok::<_, DejaDbError>(Some((seq, ns, s, p, svt)))
1768 }
1769 None => Ok(None),
1770 }
1771 })?;
1772 let (old_seq, _ns, old_s, old_p, old_svt) = match old_row {
1773 Some(x) => x,
1774 None => return Err(DejaDbError::NotFound(*old)),
1775 };
1776 if old_svt.is_some() {
1777 return Err(DejaDbError::SupersessionConflict(*old));
1778 }
1779
1780 new_grain.common_mut().derived_from = Some(old.to_hex());
1781 let new_hash = self.add(new_grain)?;
1782 let now = now_ms();
1783
1784 let op_seq = self.next_op;
1785 self.next_op += 1;
1786 let hlc = self.next_hlc();
1787 let conn = &self.conn;
1788 self.rt.block_on(async {
1789 conn.execute("BEGIN", ()).await.map_err(db_err)?;
1790 let r = async {
1791 conn.execute(
1792 "UPDATE grains SET superseded_by=?1, svt=?2 WHERE seq=?3",
1793 (pb(new_hash.as_bytes().to_vec()), pi(now), pi(old_seq)),
1794 )
1795 .await
1796 .map_err(db_err)?;
1797 conn.execute(
1798 "UPDATE grains SET supersedes=?1 WHERE hash=?2",
1799 (pb(old.as_bytes().to_vec()), pb(new_hash.as_bytes().to_vec())),
1800 )
1801 .await
1802 .map_err(db_err)?;
1803 conn.execute("UPDATE triples SET cur=0 WHERE seq=?1", (pi(old_seq),))
1804 .await
1805 .map_err(db_err)?;
1806 conn.execute("UPDATE osp SET cur=0 WHERE seq=?1", (pi(old_seq),))
1807 .await
1808 .map_err(db_err)?;
1809 conn.execute(
1810 "INSERT INTO oplog(op_seq,hlc,op,hash) VALUES (?1,?2,?3,?4)",
1811 (pi(op_seq), pi(hlc), pi(OP_SUPERSEDE), pb(new_hash.as_bytes().to_vec())),
1812 )
1813 .await
1814 .map_err(db_err)?;
1815 Ok::<(), DejaDbError>(())
1816 }
1817 .await;
1818 match r {
1819 Ok(()) => conn.execute("COMMIT", ()).await.map_err(db_err).map(|_| ()),
1820 Err(e) => {
1821 let _ = conn.execute("ROLLBACK", ()).await;
1822 Err(e)
1823 }
1824 }
1825 })?;
1826 let _ = (old_s, old_p);
1827 Ok(new_hash)
1828 }
1829
1830 pub fn forget(&mut self, hash: &Hash) -> Result<()> {
1833 let conn = &self.conn;
1834 let row = self.rt.block_on(async {
1835 let mut rows = conn
1836 .query(
1837 "SELECT seq, ns, s, p FROM grains WHERE hash = ?1",
1838 (pb(hash.as_bytes().to_vec()),),
1839 )
1840 .await
1841 .map_err(db_err)?;
1842 match rows.next().await.map_err(db_err)? {
1843 Some(row) => Ok::<_, DejaDbError>(Some((
1844 v_i64(&row.get_value(0).map_err(db_err)?).unwrap_or(0),
1845 v_i64(&row.get_value(1).map_err(db_err)?),
1846 v_i64(&row.get_value(2).map_err(db_err)?),
1847 v_i64(&row.get_value(3).map_err(db_err)?),
1848 ))),
1849 None => Ok(None),
1850 }
1851 })?;
1852 let (seq, ns, s, p) = match row {
1853 Some(x) => x,
1854 None => return Err(DejaDbError::NotFound(*hash)),
1855 };
1856 let op_seq = self.next_op;
1857 self.next_op += 1;
1858 let hlc = self.next_hlc();
1859 let conn = &self.conn;
1860 self.rt.block_on(async {
1861 conn.execute("BEGIN", ()).await.map_err(db_err)?;
1862 let r = async {
1863 conn.execute("DELETE FROM triples WHERE seq=?1", (pi(seq),))
1864 .await
1865 .map_err(db_err)?;
1866 conn.execute("DELETE FROM osp WHERE seq=?1", (pi(seq),))
1867 .await
1868 .map_err(db_err)?;
1869 conn.execute("DELETE FROM embeddings WHERE seq=?1", (pi(seq),))
1870 .await
1871 .map_err(db_err)?;
1872 conn.execute("DELETE FROM thread_idx WHERE seq=?1", (pi(seq),))
1873 .await
1874 .map_err(db_err)?;
1875 conn.execute("DELETE FROM grains WHERE seq=?1", (pi(seq),))
1876 .await
1877 .map_err(db_err)?;
1878 if let (Some(ns), Some(s), Some(p)) = (ns, s, p) {
1880 conn.execute(
1881 "DELETE FROM entity_latest WHERE ns=?1 AND s=?2 AND p=?3 AND seq=?4",
1882 (pi(ns), pi(s), pi(p), pi(seq)),
1883 )
1884 .await
1885 .map_err(db_err)?;
1886 let mut rows = conn
1887 .query(
1888 "SELECT t.o, t.seq, g.hash FROM triples t JOIN grains g ON g.seq=t.seq
1889 WHERE t.ns=?1 AND t.s=?2 AND t.p=?3 AND t.cur=1 ORDER BY t.seq DESC LIMIT 1",
1890 (pi(ns), pi(s), pi(p)),
1891 )
1892 .await
1893 .map_err(db_err)?;
1894 if let Some(row) = rows.next().await.map_err(db_err)? {
1895 let o = v_i64(&row.get_value(0).map_err(db_err)?).unwrap_or(0);
1896 let sq = v_i64(&row.get_value(1).map_err(db_err)?).unwrap_or(0);
1897 let h = v_blob(&row.get_value(2).map_err(db_err)?).unwrap_or_default();
1898 conn.execute(
1899 "INSERT OR REPLACE INTO entity_latest(ns,s,p,o,seq,hash) VALUES (?1,?2,?3,?4,?5,?6)",
1900 (pi(ns), pi(s), pi(p), pi(o), pi(sq), pb(h)),
1901 )
1902 .await
1903 .map_err(db_err)?;
1904 }
1905 }
1906 conn.execute(
1907 "INSERT INTO oplog(op_seq,hlc,op,hash) VALUES (?1,?2,?3,?4)",
1908 (pi(op_seq), pi(hlc), pi(OP_FORGET), pb(hash.as_bytes().to_vec())),
1909 )
1910 .await
1911 .map_err(db_err)?;
1912 Ok::<(), DejaDbError>(())
1913 }
1914 .await;
1915 match r {
1916 Ok(()) => conn.execute("COMMIT", ()).await.map_err(db_err).map(|_| ()),
1917 Err(e) => {
1918 let _ = conn.execute("ROLLBACK", ()).await;
1919 Err(e)
1920 }
1921 }
1922 })
1923 }
1924
1925 pub fn related(
1932 &mut self,
1933 ns: &str,
1934 start: &str,
1935 relations: &[&str],
1936 dir: Direction,
1937 depth: usize,
1938 cap: usize,
1939 ) -> Result<Vec<String>> {
1940 let ns_id = match self.term_lookup(ns) {
1941 Some(x) => x,
1942 None => return Ok(Vec::new()),
1943 };
1944 let start_id = match self.term_lookup(start) {
1945 Some(x) => x,
1946 None => return Ok(Vec::new()),
1947 };
1948 let rel_ids: Vec<i64> = relations.iter().filter_map(|r| self.term_lookup(r)).collect();
1949 if rel_ids.is_empty() {
1950 return Ok(Vec::new());
1951 }
1952 let depth = depth.min(4);
1953 let cap = cap.min(512);
1954 let conn = &self.conn;
1955 let reached = self.rt.block_on(async {
1956 let mut seen: HashSet<i64> = HashSet::new();
1957 seen.insert(start_id);
1958 let mut order: Vec<i64> = Vec::new();
1959 let mut frontier = vec![start_id];
1960 for _ in 0..depth {
1961 let mut next = Vec::new();
1962 for node in &frontier {
1963 for p in &rel_ids {
1964 if matches!(dir, Direction::Out | Direction::Both) {
1965 let mut rows = conn
1966 .query(
1967 "SELECT o FROM triples WHERE ns=?1 AND s=?2 AND p=?3 AND cur=1 LIMIT 64",
1968 (pi(ns_id), pi(*node), pi(*p)),
1969 )
1970 .await
1971 .map_err(db_err)?;
1972 while let Some(row) = rows.next().await.map_err(db_err)? {
1973 if let Some(o) = v_i64(&row.get_value(0).map_err(db_err)?) {
1974 if seen.insert(o) {
1975 order.push(o);
1976 next.push(o);
1977 if order.len() >= cap {
1978 return Ok::<_, DejaDbError>(order);
1979 }
1980 }
1981 }
1982 }
1983 }
1984 if matches!(dir, Direction::In | Direction::Both) {
1985 let mut rows = conn
1986 .query(
1987 "SELECT s FROM osp WHERE ns=?1 AND o=?2 AND p=?3 AND cur=1 LIMIT 64",
1988 (pi(ns_id), pi(*node), pi(*p)),
1989 )
1990 .await
1991 .map_err(db_err)?;
1992 while let Some(row) = rows.next().await.map_err(db_err)? {
1993 if let Some(s) = v_i64(&row.get_value(0).map_err(db_err)?) {
1994 if seen.insert(s) {
1995 order.push(s);
1996 next.push(s);
1997 if order.len() >= cap {
1998 return Ok::<_, DejaDbError>(order);
1999 }
2000 }
2001 }
2002 }
2003 }
2004 }
2005 }
2006 if next.is_empty() {
2007 break;
2008 }
2009 frontier = next;
2010 }
2011 Ok(order)
2012 })?;
2013 Ok(reached.into_iter().filter_map(|id| self.term_str(id)).collect())
2014 }
2015
2016 pub fn path(
2018 &mut self,
2019 ns: &str,
2020 from: &str,
2021 to: &str,
2022 relations: &[&str],
2023 max_depth: usize,
2024 ) -> Result<Option<Vec<String>>> {
2025 let ns_id = match self.term_lookup(ns) {
2026 Some(x) => x,
2027 None => return Ok(None),
2028 };
2029 let (a, b) = match (self.term_lookup(from), self.term_lookup(to)) {
2030 (Some(a), Some(b)) => (a, b),
2031 _ => return Ok(None),
2032 };
2033 let rel_ids: Vec<i64> = relations.iter().filter_map(|r| self.term_lookup(r)).collect();
2034 if rel_ids.is_empty() {
2035 return Ok(None);
2036 }
2037 let max_depth = max_depth.min(6);
2038 let conn = &self.conn;
2039 let parents = self.rt.block_on(async {
2040 let mut parent: HashMap<i64, i64> = HashMap::new();
2041 let mut q = VecDeque::from([a]);
2042 let mut found = false;
2043 let mut hops = 0usize;
2044 let mut visited: HashSet<i64> = HashSet::from([a]);
2045 'outer: while !q.is_empty() && hops < max_depth {
2046 let level: Vec<i64> = q.drain(..).collect();
2047 for node in level {
2048 for p in &rel_ids {
2049 let mut rows = conn
2050 .query(
2051 "SELECT o FROM triples WHERE ns=?1 AND s=?2 AND p=?3 AND cur=1 LIMIT 64",
2052 (pi(ns_id), pi(node), pi(*p)),
2053 )
2054 .await
2055 .map_err(db_err)?;
2056 while let Some(row) = rows.next().await.map_err(db_err)? {
2057 if let Some(o) = v_i64(&row.get_value(0).map_err(db_err)?) {
2058 if visited.insert(o) {
2059 parent.insert(o, node);
2060 if o == b {
2061 found = true;
2062 break 'outer;
2063 }
2064 q.push_back(o);
2065 if visited.len() > 2048 {
2066 break 'outer;
2067 }
2068 }
2069 }
2070 }
2071 }
2072 }
2073 hops += 1;
2074 }
2075 Ok::<_, DejaDbError>(if found { Some(parent) } else { None })
2076 })?;
2077 Ok(parents.map(|parent| {
2078 let mut chain = vec![b];
2079 let mut cur = b;
2080 while let Some(pr) = parent.get(&cur) {
2081 chain.push(*pr);
2082 cur = *pr;
2083 if cur == a {
2084 break;
2085 }
2086 }
2087 chain.reverse();
2088 chain.into_iter().filter_map(|id| self.term_str(id)).collect()
2089 }))
2090 }
2091
2092 pub fn entity_at(
2094 &mut self,
2095 ns: &str,
2096 subject: &str,
2097 relation: &str,
2098 t: i64,
2099 axis: Axis,
2100 ) -> Result<Option<DeserializedGrain>> {
2101 match axis {
2102 Axis::Knowledge => {
2103 let head = match self.latest(ns, subject, relation)? {
2105 Some(g) => g.hash,
2106 None => return Ok(None),
2107 };
2108 let mut cur = head;
2109 loop {
2110 let conn = &self.conn;
2111 let row = self.rt.block_on(async {
2112 let mut rows = conn
2113 .query(
2114 "SELECT svf, supersedes, blob FROM grains WHERE hash = ?1",
2115 (pb(cur.as_bytes().to_vec()),),
2116 )
2117 .await
2118 .map_err(db_err)?;
2119 match rows.next().await.map_err(db_err)? {
2120 Some(row) => {
2121 let svf = v_i64(&row.get_value(0).map_err(db_err)?);
2122 let sup = v_blob(&row.get_value(1).map_err(db_err)?);
2123 let blob = v_blob(&row.get_value(2).map_err(db_err)?);
2124 Ok::<_, DejaDbError>(Some((svf, sup, blob)))
2125 }
2126 None => Ok(None),
2127 }
2128 })?;
2129 let (svf, sup, blob) = match row {
2130 Some(x) => x,
2131 None => return Ok(None),
2132 };
2133 if svf.unwrap_or(i64::MIN) <= t {
2134 return Ok(Some(deserialize_blob(&blob.unwrap_or_default())?));
2135 }
2136 match sup {
2137 Some(prev) => cur = Hash::try_from_bytes(&prev)?,
2138 None => return Ok(None),
2139 }
2140 }
2141 }
2142 Axis::World => {
2143 let (ns_id, s_id, p_id) = match (
2145 self.term_lookup(ns),
2146 self.term_lookup(subject),
2147 self.term_lookup(relation),
2148 ) {
2149 (Some(a), Some(b), Some(c)) => (a, b, c),
2150 _ => return Ok(None),
2151 };
2152 let conn = &self.conn;
2153 let blob = self.rt.block_on(async {
2154 let mut rows = conn
2155 .query(
2156 "SELECT g.blob FROM triples tr JOIN grains g ON g.seq = tr.seq
2157 WHERE tr.ns=?1 AND tr.s=?2 AND tr.p=?3
2158 AND g.svt IS NULL
2159 AND (g.vf IS NULL OR g.vf <= ?4)
2160 AND (g.vt IS NULL OR g.vt > ?4)
2161 ORDER BY tr.seq DESC LIMIT 1",
2162 (pi(ns_id), pi(s_id), pi(p_id), pi(t)),
2163 )
2164 .await
2165 .map_err(db_err)?;
2166 Ok::<_, DejaDbError>(match rows.next().await.map_err(db_err)? {
2167 Some(row) => v_blob(&row.get_value(0).map_err(db_err)?),
2168 None => None,
2169 })
2170 })?;
2171 match blob {
2172 Some(b) => Ok(Some(deserialize_blob(&b)?)),
2173 None => Ok(None),
2174 }
2175 }
2176 }
2177 }
2178
2179 pub fn has(&mut self, hash: &Hash) -> Result<bool> {
2181 self.has_grain(hash)
2182 }
2183
2184 pub fn search_text(&mut self, ns: &str, query: &str, k: usize) -> Result<Vec<i64>> {
2187 if !self.index_text {
2188 return Ok(Vec::new()); }
2190 let ns_id = match self.term_lookup(ns) {
2191 Some(x) => x,
2192 None => return Ok(Vec::new()),
2193 };
2194 let conn = &self.conn;
2195 self.rt.block_on(async {
2196 let mut out = Vec::new();
2197 let mut rows = conn
2198 .query(
2199 "SELECT seq FROM grains WHERE text MATCH ?1 AND ns = ?2 AND svt IS NULL LIMIT ?3",
2200 (pt(query), pi(ns_id), pi(k as i64)),
2201 )
2202 .await
2203 .map_err(db_err)?;
2204 while let Some(row) = rows.next().await.map_err(db_err)? {
2205 if let Some(s) = v_i64(&row.get_value(0).map_err(db_err)?) {
2206 out.push(s);
2207 }
2208 }
2209 Ok(out)
2210 })
2211 }
2212
2213 pub fn nearest_semantic(
2231 &mut self,
2232 ns: &str,
2233 subject: Option<&str>,
2234 relation: Option<&str>,
2235 text: &str,
2236 k: usize,
2237 ) -> Result<Vec<(Hash, f32)>> {
2238 let Some(embedder) = &self.embedder else {
2239 return Err(DejaDbError::Validation(
2240 "novelty check requires an embedder (e.g. --embed-cmd); none installed".into(),
2241 ));
2242 };
2243 let Some(ns_id) = self.term_lookup(ns) else {
2244 return Ok(Vec::new());
2245 };
2246 let qjson = vec_to_json(&embedder.embed(text)?);
2247 let s_id = match subject {
2250 Some(s) => match self.term_lookup(s) {
2251 Some(x) => Some(x),
2252 None => return Ok(Vec::new()),
2253 },
2254 None => None,
2255 };
2256 let p_id = match relation {
2257 Some(r) => match self.term_lookup(r) {
2258 Some(x) => Some(x),
2259 None => return Ok(Vec::new()),
2260 },
2261 None => None,
2262 };
2263 let conn = &self.conn;
2264 self.rt.block_on(async {
2265 let base = "SELECT g.hash, vector_distance_cos(e.vec, vector32(?2)) AS dist \
2266 FROM embeddings e JOIN grains g ON g.seq = e.seq \
2267 WHERE g.ns = ?1 AND g.svt IS NULL";
2268 let mut rows = match (s_id, p_id) {
2269 (Some(s), Some(p)) => {
2270 conn.query(
2271 &format!("{base} AND g.s = ?3 AND g.p = ?4 ORDER BY dist LIMIT ?5"),
2272 (pi(ns_id), pt(&qjson), pi(s), pi(p), pi(k as i64)),
2273 )
2274 .await
2275 }
2276 (Some(s), None) => {
2277 conn.query(
2278 &format!("{base} AND g.s = ?3 ORDER BY dist LIMIT ?4"),
2279 (pi(ns_id), pt(&qjson), pi(s), pi(k as i64)),
2280 )
2281 .await
2282 }
2283 _ => {
2284 conn.query(
2285 &format!("{base} ORDER BY dist LIMIT ?3"),
2286 (pi(ns_id), pt(&qjson), pi(k as i64)),
2287 )
2288 .await
2289 }
2290 }
2291 .map_err(db_err)?;
2292 let mut out = Vec::new();
2293 while let Some(row) = rows.next().await.map_err(db_err)? {
2294 let h = v_blob(&row.get_value(0).map_err(db_err)?)
2295 .and_then(|b| Hash::try_from_bytes(&b).ok());
2296 let dist = v_f64(&row.get_value(1).map_err(db_err)?).unwrap_or(1.0);
2298 if let Some(h) = h {
2299 out.push((h, (1.0 - dist) as f32));
2300 }
2301 }
2302 Ok(out)
2303 })
2304 }
2305
2306 pub fn search_vector(&mut self, ns: &str, query: &str, k: usize) -> Result<Vec<i64>> {
2307 let (Some(embedder), Some(ns_id)) = (&self.embedder, self.term_lookup(ns)) else {
2308 return Ok(Vec::new());
2309 };
2310 let qv = embedder.embed(query)?;
2311 let qjson = vec_to_json(&qv);
2312 let conn = &self.conn;
2313 self.rt.block_on(async {
2314 let mut out = Vec::new();
2315 let mut rows = conn
2316 .query(
2317 "SELECT e.seq FROM embeddings e JOIN grains g ON g.seq = e.seq
2318 WHERE g.ns = ?1 AND g.svt IS NULL
2319 ORDER BY vector_distance_cos(e.vec, vector32(?2)) LIMIT ?3",
2320 (pi(ns_id), pt(&qjson), pi(k as i64)),
2321 )
2322 .await
2323 .map_err(db_err)?;
2324 while let Some(row) = rows.next().await.map_err(db_err)? {
2325 if let Some(s) = v_i64(&row.get_value(0).map_err(db_err)?) {
2326 out.push(s);
2327 }
2328 }
2329 Ok(out)
2330 })
2331 }
2332
2333 pub fn recall_hybrid(
2339 &mut self,
2340 ns: &str,
2341 subject: Option<&str>,
2342 relation: Option<&str>,
2343 query: Option<&str>,
2344 k: usize,
2345 deadline: Option<std::time::Duration>,
2346 ) -> Result<Vec<DeserializedGrain>> {
2347 self.recall_hybrid_tuned(ns, subject, relation, query, k, deadline, RecallTuning::default())
2348 }
2349
2350 #[allow(clippy::too_many_arguments)] pub fn recall_hybrid_tuned(
2366 &mut self,
2367 ns: &str,
2368 subject: Option<&str>,
2369 relation: Option<&str>,
2370 query: Option<&str>,
2371 k: usize,
2372 deadline: Option<std::time::Duration>,
2373 tuning: RecallTuning,
2374 ) -> Result<Vec<DeserializedGrain>> {
2375 let start = std::time::Instant::now();
2376 let over = |start: &std::time::Instant| match deadline {
2377 Some(d) => start.elapsed() >= d,
2378 None => false,
2379 };
2380
2381 let refine = tuning.rerank || tuning.diversity_lambda.is_some();
2384 let leg_k = if refine {
2385 k.max(REFINE_POOL)
2386 } else {
2387 k.saturating_mul(2)
2388 };
2389
2390 let structural: Vec<i64> = match subject {
2392 Some(s) => self.recall_seqs(ns, s, relation, leg_k)?,
2393 None => Vec::new(),
2394 };
2395 let mut fts_legs: Vec<Vec<i64>> = Vec::new();
2398 if let Some(q) = query {
2399 if !over(&start) {
2400 fts_legs.push(self.search_text(ns, q, leg_k)?);
2401 if tuning.query_expansion && self.index_text {
2402 for variant in self.expand_query(q) {
2403 if over(&start) {
2404 break;
2405 }
2406 let hits = self.search_text(ns, &variant, leg_k)?;
2407 if !hits.is_empty() {
2408 fts_legs.push(hits);
2409 }
2410 }
2411 }
2412 }
2413 }
2414 let vecs: Vec<i64> = match query {
2417 Some(q) if self.embedder.is_some() && !over(&start) => {
2418 self.search_vector(ns, q, leg_k)?
2419 }
2420 _ => Vec::new(),
2421 };
2422 if structural.is_empty() && fts_legs.iter().all(|l| l.is_empty()) && vecs.is_empty() {
2423 return Ok(Vec::new());
2424 }
2425
2426 let mut scores: HashMap<i64, f64> = HashMap::new();
2428 for (rank, seq) in structural.iter().enumerate() {
2429 *scores.entry(*seq).or_insert(0.0) += 1.0 / (RRF_K0 + rank as f64);
2430 }
2431 for leg in &fts_legs {
2432 for (rank, seq) in leg.iter().enumerate() {
2433 *scores.entry(*seq).or_insert(0.0) += 1.0 / (RRF_K0 + rank as f64);
2434 }
2435 }
2436 for (rank, seq) in vecs.iter().enumerate() {
2437 *scores.entry(*seq).or_insert(0.0) += 1.0 / (RRF_K0 + rank as f64);
2438 }
2439 let mut ranked: Vec<(i64, f64)> = scores.into_iter().collect();
2440 ranked.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap().then(b.0.cmp(&a.0)));
2441
2442 let ordered: Vec<i64> = if let Some(q) =
2444 query.filter(|_| tuning.rerank && self.reranker.is_some() && !over(&start))
2445 {
2446 self.rerank_pool(q, &ranked, k)?
2447 } else if let (Some(lambda), Some(q)) = (tuning.diversity_lambda, query) {
2448 if self.embedder.is_some() && !over(&start) {
2449 self.mmr_pool(q, &ranked, lambda, k)?
2450 } else {
2451 ranked.iter().take(k).map(|(s, _)| *s).collect()
2452 }
2453 } else {
2454 ranked.iter().take(k).map(|(s, _)| *s).collect()
2455 };
2456
2457 let mut out = Vec::new();
2458 for seq in ordered {
2459 if over(&start) {
2460 break; }
2462 if let Some(b) = self.blob_by_seq(seq)? {
2463 out.push(deserialize_blob(&b)?);
2464 }
2465 }
2466 Ok(out)
2467 }
2468
2469 fn expand_query(&self, q: &str) -> Vec<String> {
2472 match &self.expander {
2473 Some(e) => e.expand(q),
2474 None => EnglishExpander::default().expand(q),
2475 }
2476 }
2477
2478 fn candidate_text(&mut self, seq: i64) -> Result<String> {
2482 let Some(b) = self.blob_by_seq(seq)? else {
2483 return Ok(String::new());
2484 };
2485 let g = deserialize_blob(&b)?;
2486 Ok(projected_text(&g).unwrap_or_default())
2487 }
2488
2489 fn rerank_pool(&mut self, query: &str, ranked: &[(i64, f64)], k: usize) -> Result<Vec<i64>> {
2494 let pool_n = ranked.len().min(k.max(REFINE_POOL));
2495 let pool: Vec<i64> = ranked.iter().take(pool_n).map(|(s, _)| *s).collect();
2496 if pool.is_empty() {
2497 return Ok(pool);
2498 }
2499 let mut docs: Vec<String> = Vec::with_capacity(pool.len());
2500 for &seq in &pool {
2501 docs.push(self.candidate_text(seq)?);
2502 }
2503 let refs: Vec<&str> = docs.iter().map(|s| s.as_str()).collect();
2504 let reranker = self.reranker.as_ref().expect("caller checked reranker present");
2505 match reranker.rerank(query, &refs) {
2506 Ok(scores) if scores.len() == refs.len() => {
2507 let mut scored: Vec<(i64, f32)> = pool.iter().copied().zip(scores).collect();
2508 scored.sort_by(|a, b| {
2510 b.1.partial_cmp(&a.1)
2511 .unwrap_or(std::cmp::Ordering::Equal)
2512 .then(a.0.cmp(&b.0))
2513 });
2514 Ok(scored.into_iter().take(k).map(|(s, _)| s).collect())
2515 }
2516 _ => Ok(pool.into_iter().take(k).collect()),
2518 }
2519 }
2520
2521 fn mmr_pool(
2527 &mut self,
2528 query: &str,
2529 ranked: &[(i64, f64)],
2530 lambda: f32,
2531 k: usize,
2532 ) -> Result<Vec<i64>> {
2533 let lambda = lambda.clamp(0.0, 1.0);
2534 let pool_n = ranked.len().min(k.max(REFINE_POOL));
2535 let pool: Vec<i64> = ranked.iter().take(pool_n).map(|(s, _)| *s).collect();
2536 if pool.len() < 2 {
2537 return Ok(pool);
2538 }
2539 let qv = match &self.embedder {
2540 Some(e) => e.embed(query)?,
2541 None => return Ok(pool.into_iter().take(k).collect()),
2542 };
2543 let qjson = vec_to_json(&qv);
2544 let rel = self.vec_rel_map(&pool, &qjson)?;
2545 let embedded: Vec<i64> = pool.iter().copied().filter(|s| rel.contains_key(s)).collect();
2547 if embedded.len() < 2 {
2548 return Ok(pool.into_iter().take(k).collect());
2549 }
2550 let sim = self.vec_pairwise_map(&embedded)?;
2551 let sim_of = |a: i64, b: i64| -> f32 {
2552 if a == b {
2553 1.0
2554 } else {
2555 let key = if a < b { (a, b) } else { (b, a) };
2556 *sim.get(&key).unwrap_or(&0.0)
2557 }
2558 };
2559
2560 let target = k.min(embedded.len());
2561 let mut selected: Vec<i64> = Vec::with_capacity(target);
2562 let mut remaining: Vec<i64> = embedded.clone();
2563 while selected.len() < target && !remaining.is_empty() {
2564 let mut best_idx = 0usize;
2565 let mut best_score = f32::MIN;
2566 for (i, &c) in remaining.iter().enumerate() {
2567 let relevance = *rel.get(&c).unwrap_or(&0.0);
2568 let max_sim = selected
2569 .iter()
2570 .map(|&s| sim_of(c, s))
2571 .fold(0.0f32, f32::max);
2572 let mmr = lambda * relevance - (1.0 - lambda) * max_sim;
2573 if mmr > best_score {
2574 best_score = mmr;
2575 best_idx = i;
2576 }
2577 }
2578 selected.push(remaining.remove(best_idx));
2579 }
2580 for s in pool {
2582 if selected.len() >= k {
2583 break;
2584 }
2585 if !selected.contains(&s) {
2586 selected.push(s);
2587 }
2588 }
2589 selected.truncate(k);
2590 Ok(selected)
2591 }
2592
2593 fn vec_rel_map(&mut self, seqs: &[i64], qjson: &str) -> Result<HashMap<i64, f32>> {
2596 if seqs.is_empty() {
2597 return Ok(HashMap::new());
2598 }
2599 let sql = format!(
2600 "SELECT seq, vector_distance_cos(vec, vector32(?1)) FROM embeddings WHERE seq IN ({})",
2601 seq_csv(seqs)
2602 );
2603 let conn = &self.conn;
2604 self.rt.block_on(async {
2605 let mut out = HashMap::new();
2606 let mut rows = conn.query(&sql, (pt(qjson),)).await.map_err(db_err)?;
2607 while let Some(row) = rows.next().await.map_err(db_err)? {
2608 let seq = v_i64(&row.get_value(0).map_err(db_err)?);
2609 let dist = v_f64(&row.get_value(1).map_err(db_err)?);
2610 if let (Some(s), Some(d)) = (seq, dist) {
2611 out.insert(s, 1.0 - d as f32);
2612 }
2613 }
2614 Ok(out)
2615 })
2616 }
2617
2618 fn vec_pairwise_map(&mut self, seqs: &[i64]) -> Result<HashMap<(i64, i64), f32>> {
2621 if seqs.len() < 2 {
2622 return Ok(HashMap::new());
2623 }
2624 let csv = seq_csv(seqs);
2625 let sql = format!(
2626 "SELECT a.seq, b.seq, vector_distance_cos(a.vec, b.vec) \
2627 FROM embeddings a JOIN embeddings b ON a.seq < b.seq \
2628 WHERE a.seq IN ({csv}) AND b.seq IN ({csv})"
2629 );
2630 let conn = &self.conn;
2631 self.rt.block_on(async {
2632 let mut out = HashMap::new();
2633 let mut rows = conn.query(&sql, ()).await.map_err(db_err)?;
2634 while let Some(row) = rows.next().await.map_err(db_err)? {
2635 let a = v_i64(&row.get_value(0).map_err(db_err)?);
2636 let b = v_i64(&row.get_value(1).map_err(db_err)?);
2637 let dist = v_f64(&row.get_value(2).map_err(db_err)?);
2638 if let (Some(a), Some(b), Some(d)) = (a, b, dist) {
2639 out.insert((a, b), 1.0 - d as f32);
2640 }
2641 }
2642 Ok(out)
2643 })
2644 }
2645
2646 fn recall_seqs(&mut self, ns: &str, subject: &str, relation: Option<&str>, k: usize) -> Result<Vec<i64>> {
2647 let (ns_id, s_id) = match (self.term_lookup(ns), self.term_lookup(subject)) {
2648 (Some(a), Some(b)) => (a, b),
2649 _ => return Ok(Vec::new()),
2650 };
2651 let p_id = match relation {
2652 Some(r) => match self.term_lookup(r) {
2653 Some(x) => Some(x),
2654 None => return Ok(Vec::new()),
2655 },
2656 None => None,
2657 };
2658 let conn = &self.conn;
2659 let rt = &self.rt;
2660 let slot_sp = &mut self.st_probe_sp;
2661 let slot_s = &mut self.st_probe_s;
2662 rt.block_on(async {
2663 let mut seqs = Vec::new();
2664 let mut rows = match p_id {
2665 Some(p) => {
2666 let st = ensure_stmt(
2667 slot_sp,
2668 conn,
2669 "SELECT seq FROM triples WHERE ns=?1 AND s=?2 AND p=?3 AND cur=1 ORDER BY seq DESC LIMIT ?4",
2670 )
2671 .await?;
2672 st.query((pi(ns_id), pi(s_id), pi(p), pi(k as i64))).await.map_err(db_err)?
2673 }
2674 None => {
2675 let st = ensure_stmt(
2676 slot_s,
2677 conn,
2678 "SELECT seq FROM triples WHERE ns=?1 AND s=?2 AND cur=1 ORDER BY seq DESC LIMIT ?3",
2679 )
2680 .await?;
2681 st.query((pi(ns_id), pi(s_id), pi(k as i64))).await.map_err(db_err)?
2682 }
2683 };
2684 while let Some(row) = rows.next().await.map_err(db_err)? {
2685 if let Some(x) = v_i64(&row.get_value(0).map_err(db_err)?) {
2686 seqs.push(x);
2687 }
2688 }
2689 Ok(seqs)
2690 })
2691 }
2692
2693 fn blob_by_seq(&mut self, seq: i64) -> Result<Option<Vec<u8>>> {
2694 let conn = &self.conn;
2695 let rt = &self.rt;
2696 let slot = &mut self.st_fetch_seq;
2697 rt.block_on(async {
2698 let st = ensure_stmt(slot, conn, "SELECT blob FROM grains WHERE seq = ?1").await?;
2699 let mut rows = st
2700 .query((pi(seq),))
2701 .await
2702 .map_err(db_err)?;
2703 Ok(match rows.next().await.map_err(db_err)? {
2704 Some(row) => v_blob(&row.get_value(0).map_err(db_err)?),
2705 None => None,
2706 })
2707 })
2708 }
2709
2710 pub fn subjects_with_relation(&mut self, ns: &str, relation: &str) -> Result<Vec<String>> {
2713 let (ns_id, p_id) = match (self.term_lookup(ns), self.term_lookup(relation)) {
2714 (Some(a), Some(b)) => (a, b),
2715 _ => return Ok(Vec::new()),
2716 };
2717 let conn = &self.conn;
2718 let ids = self.rt.block_on(async {
2719 let mut out = Vec::new();
2720 let mut rows = conn
2721 .query(
2722 "SELECT DISTINCT s FROM triples WHERE ns=?1 AND p=?2 AND cur=1",
2723 (pi(ns_id), pi(p_id)),
2724 )
2725 .await
2726 .map_err(db_err)?;
2727 while let Some(row) = rows.next().await.map_err(db_err)? {
2728 if let Some(s) = v_i64(&row.get_value(0).map_err(db_err)?) {
2729 out.push(s);
2730 }
2731 }
2732 Ok::<_, DejaDbError>(out)
2733 })?;
2734 let mut subjects: Vec<String> = ids.into_iter().filter_map(|id| self.term_str(id)).collect();
2735 subjects.sort();
2736 Ok(subjects)
2737 }
2738
2739 #[allow(clippy::type_complexity)] pub fn remember(
2746 &mut self,
2747 ns: &str,
2748 content: &str,
2749 observer: &str,
2750 extractor: Option<&dyn Fn(&str) -> Vec<FactDraft>>,
2751 ) -> Result<RememberResult> {
2752 use dejadb_core::types::Observation;
2753 let mut obs = Observation::new(observer, "llm");
2754 obs.common.namespace = Some(ns.to_string());
2755 obs.common.context = Some(serde_json::json!({ "content": content }));
2756 let observation = self.add(&obs)?;
2757
2758 let mut facts = Vec::new();
2759 if let Some(f) = extractor {
2760 for draft in f(content) {
2761 let mut fact = dejadb_core::types::Fact::new(&draft.subject, &draft.relation, &draft.object);
2762 fact.common.confidence = draft.confidence.clamp(0.0, 1.0);
2763 fact.common.namespace = Some(ns.to_string());
2764 fact.common.derived_from = Some(observation.to_hex());
2765 fact.common.source_type = Some("derived".to_string());
2766 facts.push(self.add(&fact)?);
2767 }
2768 }
2769 Ok(RememberResult { observation, facts })
2770 }
2771
2772 pub fn count(&mut self) -> Result<usize> {
2774 let conn = &self.conn;
2775 self.rt.block_on(async {
2776 let mut rows = conn
2777 .query("SELECT COUNT(*) FROM grains", ())
2778 .await
2779 .map_err(db_err)?;
2780 Ok(match rows.next().await.map_err(db_err)? {
2781 Some(row) => v_i64(&row.get_value(0).map_err(db_err)?).unwrap_or(0) as usize,
2782 None => 0,
2783 })
2784 })
2785 }
2786
2787 pub fn open_forks(&mut self) -> Result<Vec<ForkGroup>> {
2797 let conn = &self.conn;
2798 let groups: Vec<(i64, i64, i64)> = self.rt.block_on(async {
2799 let mut rows = conn
2800 .query(
2801 "SELECT ns, s, p FROM heads GROUP BY ns, s, p HAVING COUNT(*) > 1",
2802 (),
2803 )
2804 .await
2805 .map_err(db_err)?;
2806 let mut out = Vec::new();
2807 while let Some(row) = rows.next().await.map_err(db_err)? {
2808 let ns = v_i64(&row.get_value(0).map_err(db_err)?);
2809 let s = v_i64(&row.get_value(1).map_err(db_err)?);
2810 let p = v_i64(&row.get_value(2).map_err(db_err)?);
2811 if let (Some(ns), Some(s), Some(p)) = (ns, s, p) {
2812 out.push((ns, s, p));
2813 }
2814 }
2815 Ok::<_, DejaDbError>(out)
2816 })?;
2817
2818 let mut forks = Vec::new();
2819 for (ns_id, s_id, p_id) in groups {
2820 let (Some(namespace), Some(subject), Some(relation)) =
2821 (self.term_str(ns_id), self.term_str(s_id), self.term_str(p_id))
2822 else {
2823 continue;
2824 };
2825 let heads = self
2826 .heads(&namespace, &subject, &relation)?
2827 .into_iter()
2828 .map(|(h, _)| h)
2829 .collect();
2830 forks.push(ForkGroup {
2831 namespace,
2832 subject,
2833 relation,
2834 heads,
2835 });
2836 }
2837 Ok(forks)
2838 }
2839
2840 pub fn heads(&mut self, ns: &str, subject: &str, relation: &str) -> Result<Vec<(Hash, i64)>> {
2841 let (Some(ns_id), Some(s_id), Some(p_id)) = (
2842 self.term_lookup(ns),
2843 self.term_lookup(subject),
2844 self.term_lookup(relation),
2845 ) else {
2846 return Ok(Vec::new());
2847 };
2848 let conn = &self.conn;
2849 self.rt.block_on(async {
2850 let mut out = Vec::new();
2851 let mut rows = conn
2852 .query(
2853 "SELECT hash, created_at FROM heads WHERE ns=?1 AND s=?2 AND p=?3
2854 ORDER BY created_at DESC, hash DESC",
2855 (pi(ns_id), pi(s_id), pi(p_id)),
2856 )
2857 .await
2858 .map_err(db_err)?;
2859 while let Some(row) = rows.next().await.map_err(db_err)? {
2860 let h = v_blob(&row.get_value(0).map_err(db_err)?).unwrap_or_default();
2861 let c = v_i64(&row.get_value(1).map_err(db_err)?).unwrap_or(0);
2862 if let Ok(h) = Hash::try_from_bytes(&h) {
2863 out.push((h, c));
2864 }
2865 }
2866 Ok(out)
2867 })
2868 }
2869
2870 pub fn merge_heads<G: Grain + 'static>(
2873 &mut self,
2874 ns: &str,
2875 subject: &str,
2876 relation: &str,
2877 merged: &mut G,
2878 ) -> Result<Hash> {
2879 let tips = self.heads(ns, subject, relation)?;
2880 if tips.len() < 2 {
2881 return Err(DejaDbError::Validation(format!(
2882 "merge_heads needs an open fork; {} head(s) present",
2883 tips.len()
2884 )));
2885 }
2886 merged.common_mut().derived_from = Some(tips[0].0.to_hex());
2890 let parents: Vec<String> = tips.iter().map(|(h, _)| h.to_hex()).collect();
2891 let mut ctx = match merged.common().context.clone() {
2892 Some(serde_json::Value::Object(m)) => m,
2893 _ => serde_json::Map::new(),
2894 };
2895 ctx.insert("merge_parents".into(), serde_json::json!(parents));
2896 merged.common_mut().context = Some(serde_json::Value::Object(ctx));
2897 let merge_hash = self.add(merged)?; let now = now_ms();
2899 let conn = &self.conn;
2900 for (tip, _) in &tips {
2901 let seq = self.rt.block_on(async {
2902 let mut rows = conn
2903 .query("SELECT seq, svt FROM grains WHERE hash=?1", (pb(tip.as_bytes().to_vec()),))
2904 .await
2905 .map_err(db_err)?;
2906 Ok::<_, DejaDbError>(match rows.next().await.map_err(db_err)? {
2907 Some(row) => {
2908 let seq = v_i64(&row.get_value(0).map_err(db_err)?).unwrap_or(0);
2909 let svt = v_i64(&row.get_value(1).map_err(db_err)?);
2910 (svt.is_none()).then_some(seq)
2911 }
2912 None => None,
2913 })
2914 })?;
2915 if let Some(seq) = seq {
2916 self.rt.block_on(async {
2917 conn.execute(
2918 "UPDATE grains SET superseded_by=?1, svt=?2 WHERE seq=?3",
2919 (pb(merge_hash.as_bytes().to_vec()), pi(now), pi(seq)),
2920 )
2921 .await
2922 .map_err(db_err)?;
2923 conn.execute("UPDATE triples SET cur=0 WHERE seq=?1", (pi(seq),))
2924 .await
2925 .map_err(db_err)?;
2926 conn.execute("UPDATE osp SET cur=0 WHERE seq=?1", (pi(seq),))
2927 .await
2928 .map_err(db_err)?;
2929 Ok::<(), DejaDbError>(())
2930 })?;
2931 }
2932 }
2933 Ok(merge_hash)
2934 }
2935
2936 pub fn history(&mut self, ns: &str, subject: &str, relation: &str) -> Result<Vec<HistoryEntry>> {
2939 let head = match self.latest(ns, subject, relation)? {
2940 Some(g) => g.hash,
2941 None => return Ok(Vec::new()),
2942 };
2943 let mut out = Vec::new();
2944 let mut cur = Some(head);
2945 while let Some(h) = cur {
2946 let conn = &self.conn;
2947 let row = self.rt.block_on(async {
2948 let mut rows = conn
2949 .query(
2950 "SELECT blob, superseded_by, supersedes FROM grains WHERE hash = ?1",
2951 (pb(h.as_bytes().to_vec()),),
2952 )
2953 .await
2954 .map_err(db_err)?;
2955 Ok::<_, DejaDbError>(match rows.next().await.map_err(db_err)? {
2956 Some(row) => Some((
2957 v_blob(&row.get_value(0).map_err(db_err)?),
2958 v_blob(&row.get_value(1).map_err(db_err)?),
2959 v_blob(&row.get_value(2).map_err(db_err)?),
2960 )),
2961 None => None,
2962 })
2963 })?;
2964 let (blob, sup_by, supersedes) = match row {
2965 Some(x) => x,
2966 None => break,
2967 };
2968 if let Some(b) = blob {
2969 let g = deserialize_blob(&b)?;
2970 out.push(HistoryEntry {
2971 hash: h,
2972 object: g.get_str("object").unwrap_or_default().to_string(),
2973 created_at: g.get_i64("created_at").unwrap_or(0),
2974 confidence: g.get_f64("confidence").unwrap_or(0.0),
2975 superseded_by: sup_by.and_then(|b| Hash::try_from_bytes(&b).ok()),
2976 });
2977 }
2978 cur = supersedes.and_then(|b| Hash::try_from_bytes(&b).ok());
2979 if out.len() > 512 {
2980 break; }
2982 }
2983 Ok(out)
2984 }
2985
2986 pub fn verify(&mut self) -> Result<VerifyReport> {
2990 let conn = &self.conn;
2991 let (integrity, fts_notes, rows) = self.rt.block_on(async {
2992 let mut real: Vec<String> = Vec::new();
2998 let mut fts_notes: Vec<String> = Vec::new();
2999 {
3000 let mut rows = conn.query("PRAGMA integrity_check", ()).await.map_err(db_err)?;
3001 while let Some(row) = rows.next().await.map_err(db_err)? {
3002 if let Value::Text(s) = row.get_value(0).map_err(db_err)? {
3003 if s == "ok" {
3004 continue;
3005 } else if s.contains("__turso_internal_fts") {
3006 fts_notes.push(s);
3007 } else {
3008 real.push(s);
3009 }
3010 }
3011 }
3012 }
3013 let integ = if real.is_empty() { "ok".to_string() } else { real.join("; ") };
3014 let mut out: Vec<(Vec<u8>, Vec<u8>)> = Vec::new();
3015 let mut rows = conn.query("SELECT hash, blob FROM grains", ()).await.map_err(db_err)?;
3016 while let Some(row) = rows.next().await.map_err(db_err)? {
3017 let h = v_blob(&row.get_value(0).map_err(db_err)?).unwrap_or_default();
3018 let b = v_blob(&row.get_value(1).map_err(db_err)?).unwrap_or_default();
3019 out.push((h, b));
3020 }
3021 Ok::<_, DejaDbError>((integ, fts_notes, out))
3022 })?;
3023 let mut report = VerifyReport {
3024 integrity,
3025 fts_notes,
3026 grains: rows.len(),
3027 hash_mismatches: 0,
3028 undecodable: 0,
3029 };
3030 for (stored, blob) in rows {
3031 match deserialize_blob(&blob) {
3032 Ok(g) => {
3033 if g.hash.as_bytes().as_slice() != stored.as_slice() {
3034 report.hash_mismatches += 1;
3035 }
3036 }
3037 Err(_) => report.undecodable += 1,
3038 }
3039 }
3040 Ok(report)
3041 }
3042
3043 pub fn stats(&mut self) -> Result<StoreStats> {
3045 let conn = &self.conn;
3046 self.rt.block_on(async {
3047 let one = |sql: &'static str| {
3048 let conn = conn.clone();
3049 async move {
3050 let mut rows = conn.query(sql, ()).await.map_err(db_err)?;
3051 Ok::<i64, DejaDbError>(match rows.next().await.map_err(db_err)? {
3052 Some(row) => v_i64(&row.get_value(0).map_err(db_err)?).unwrap_or(0),
3053 None => 0,
3054 })
3055 }
3056 };
3057 Ok(StoreStats {
3058 grains: one("SELECT COUNT(*) FROM grains").await? as usize,
3059 current: one("SELECT COUNT(*) FROM grains WHERE svt IS NULL").await? as usize,
3060 triples: one("SELECT COUNT(*) FROM triples").await? as usize,
3061 terms: one("SELECT COUNT(*) FROM terms").await? as usize,
3062 ops: one("SELECT COUNT(*) FROM oplog").await? as usize,
3063 events_indexed: one("SELECT COUNT(*) FROM thread_idx").await? as usize,
3064 })
3065 })
3066 }
3067
3068 pub fn changes_since(&mut self, after_op_seq: i64, limit: usize) -> Result<Vec<OpRecord>> {
3070 let conn = &self.conn;
3071 self.rt.block_on(async {
3072 let mut out = Vec::new();
3073 let mut rows = conn
3074 .query(
3075 "SELECT op_seq, hlc, op, hash FROM oplog WHERE op_seq > ?1 ORDER BY op_seq LIMIT ?2",
3076 (pi(after_op_seq), pi(limit as i64)),
3077 )
3078 .await
3079 .map_err(db_err)?;
3080 while let Some(row) = rows.next().await.map_err(db_err)? {
3081 out.push(OpRecord {
3082 op_seq: v_i64(&row.get_value(0).map_err(db_err)?).unwrap_or(0),
3083 hlc: v_i64(&row.get_value(1).map_err(db_err)?).unwrap_or(0),
3084 op: v_i64(&row.get_value(2).map_err(db_err)?).unwrap_or(0),
3085 hash: Hash::try_from_bytes(&v_blob(&row.get_value(3).map_err(db_err)?).unwrap_or_default())?,
3086 });
3087 }
3088 Ok(out)
3089 })
3090 }
3091}
3092
3093impl DejaDB {
3094 fn blob_path(&self, hex: &str) -> std::path::PathBuf {
3097 self.blob_dir.join(&hex[..2]).join(&hex[2..])
3098 }
3099
3100 pub fn put_blob(&mut self, bytes: &[u8]) -> Result<String> {
3103 use sha2::{Digest, Sha256};
3104 let hex = hex::encode(Sha256::digest(bytes));
3105 let path = self.blob_path(&hex);
3106 if !path.exists() {
3107 std::fs::create_dir_all(path.parent().unwrap()).map_err(db_err)?;
3108 let tmp = path.with_extension("tmp");
3109 std::fs::write(&tmp, bytes).map_err(db_err)?;
3110 std::fs::rename(&tmp, &path).map_err(db_err)?;
3111 }
3112 Ok(format!("cas://sha256:{hex}"))
3113 }
3114
3115 pub fn get_blob(&mut self, uri: &str) -> Result<Vec<u8>> {
3117 use sha2::{Digest, Sha256};
3118 let hex = uri
3119 .strip_prefix("cas://sha256:")
3120 .ok_or_else(|| DejaDbError::Validation(format!("not a cas uri: {uri}")))?;
3121 let bytes = std::fs::read(self.blob_path(hex))
3122 .map_err(|_| DejaDbError::Storage(format!("blob missing: {uri}")))?;
3123 if hex::encode(Sha256::digest(&bytes)) != hex {
3124 return Err(DejaDbError::Storage(format!("blob corrupt: {uri}")));
3125 }
3126 Ok(bytes)
3127 }
3128
3129 pub fn gc_blobs(&mut self) -> Result<usize> {
3132 let conn = &self.conn;
3134 let blobs: Vec<Vec<u8>> = self.rt.block_on(async {
3135 let mut out = Vec::new();
3136 let mut rows = conn.query("SELECT blob FROM grains", ()).await.map_err(db_err)?;
3137 while let Some(row) = rows.next().await.map_err(db_err)? {
3138 if let Some(b) = v_blob(&row.get_value(0).map_err(db_err)?) {
3139 out.push(b);
3140 }
3141 }
3142 Ok::<_, DejaDbError>(out)
3143 })?;
3144 let mut referenced: HashSet<String> = HashSet::new();
3145 for b in &blobs {
3146 if let Ok(view) = deserialize_blob(b) {
3147 if let Some(refs) = view.fields.get("content_refs").and_then(|v| v.as_array()) {
3148 for r in refs {
3149 let uri = r
3151 .get("uri")
3152 .and_then(|u| u.as_str())
3153 .or_else(|| r.get("u").and_then(|u| u.as_str()));
3154 if let Some(hex) = uri.and_then(|u| u.strip_prefix("cas://sha256:")) {
3155 referenced.insert(hex.to_string());
3156 }
3157 }
3158 }
3159 }
3160 }
3161 let mut removed = 0usize;
3162 if let Ok(shards) = std::fs::read_dir(&self.blob_dir) {
3163 for shard in shards.flatten() {
3164 let prefix = shard.file_name().to_string_lossy().to_string();
3165 if let Ok(files) = std::fs::read_dir(shard.path()) {
3166 for f in files.flatten() {
3167 let rest = f.file_name().to_string_lossy().to_string();
3168 let hex = format!("{prefix}{rest}");
3169 if !referenced.contains(&hex) && std::fs::remove_file(f.path()).is_ok() {
3170 removed += 1;
3171 }
3172 }
3173 }
3174 }
3175 }
3176 Ok(removed)
3177 }
3178
3179 pub fn bundle_since(&mut self, after_op_seq: i64, path: &str) -> Result<BundleStats> {
3186 let ops = self.changes_since(after_op_seq, usize::MAX / 2)?;
3187 let conn = &self.conn;
3188 let rt = &self.rt;
3189 let mut out: Vec<u8> = Vec::with_capacity(64 * 1024);
3190 out.extend_from_slice(BUNDLE_MAGIC);
3191 let mut last = after_op_seq;
3192 for rec in &ops {
3193 let blob: Option<Vec<u8>> = if rec.op == OP_FORGET {
3194 None
3195 } else {
3196 rt.block_on(async {
3197 let mut rows = conn
3198 .query(
3199 "SELECT blob FROM grains WHERE hash = ?1",
3200 (pb(rec.hash.as_bytes().to_vec()),),
3201 )
3202 .await
3203 .map_err(db_err)?;
3204 Ok::<_, DejaDbError>(match rows.next().await.map_err(db_err)? {
3205 Some(row) => v_blob(&row.get_value(0).map_err(db_err)?),
3206 None => None,
3207 })
3208 })?
3209 };
3210 out.push(rec.op as u8);
3211 out.extend_from_slice(&rec.hlc.to_le_bytes());
3212 out.extend_from_slice(rec.hash.as_bytes());
3213 let b = blob.unwrap_or_default();
3214 out.extend_from_slice(&(b.len() as u32).to_le_bytes());
3215 out.extend_from_slice(&b);
3216 last = rec.op_seq;
3217 }
3218 std::fs::write(path, &out).map_err(db_err)?;
3219 Ok(BundleStats {
3220 ops: ops.len(),
3221 bytes: std::fs::metadata(path).map(|m| m.len()).unwrap_or(0),
3222 last_op_seq: last,
3223 })
3224 }
3225
3226 fn blob_by_hash(&mut self, hash: &Hash) -> Result<Option<Vec<u8>>> {
3227 let conn = &self.conn;
3228 self.rt.block_on(async {
3229 let mut rows = conn
3230 .query(
3231 "SELECT blob FROM grains WHERE hash = ?1",
3232 (pb(hash.as_bytes().to_vec()),),
3233 )
3234 .await
3235 .map_err(db_err)?;
3236 Ok::<_, DejaDbError>(match rows.next().await.map_err(db_err)? {
3237 Some(row) => v_blob(&row.get_value(0).map_err(db_err)?),
3238 None => None,
3239 })
3240 })
3241 }
3242
3243 fn has_grain(&mut self, hash: &Hash) -> Result<bool> {
3244 let conn = &self.conn;
3245 self.rt.block_on(async {
3246 let mut rows = conn
3247 .query(
3248 "SELECT 1 FROM grains WHERE hash = ?1",
3249 (pb(hash.as_bytes().to_vec()),),
3250 )
3251 .await
3252 .map_err(db_err)?;
3253 Ok(rows.next().await.map_err(db_err)?.is_some())
3254 })
3255 }
3256
3257 fn insert_blob(&mut self, blob: Vec<u8>, hash: Hash, op: i64, hlc_in: i64) -> Result<()> {
3259 let pr = self.prep_from_blob(blob, hash)?;
3260 let seq = self.next_seq;
3261 self.next_seq += 1;
3262 let op_seq = self.next_op;
3263 self.next_op += 1;
3264 self.hlc_last = self.hlc_last.max(hlc_in);
3265 let conn = &self.conn;
3266 self.rt.block_on(async {
3267 conn.execute("BEGIN", ()).await.map_err(db_err)?;
3268 let r = async {
3269 conn.execute(
3270 "INSERT INTO grains(seq,hash,ns,gtype,created_at,s,p,o,vf,vt,svf,svt,superseded_by,supersedes,text,blob)
3271 VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,NULL,NULL,NULL,?12,?13)",
3272 (
3273 pi(seq),
3274 pb(pr.hash.as_bytes().to_vec()),
3275 pi(pr.ns_id),
3276 pi(pr.gtype),
3277 pi(pr.created),
3278 opt_i(pr.s),
3279 opt_i(pr.p),
3280 opt_i(pr.o),
3281 opt_i(pr.vf),
3282 opt_i(pr.vt),
3283 pi(pr.created),
3284 match &pr.text { Some(t) => pt(t), None => Value::Null },
3285 pb(pr.blob.clone()),
3286 ),
3287 )
3288 .await
3289 .map_err(db_err)?;
3290 if let (Some(s), Some(p), Some(o)) = (pr.s, pr.p, pr.o) {
3291 conn.execute(
3292 "INSERT INTO triples(ns,s,p,o,seq,cur) VALUES (?1,?2,?3,?4,?5,1)",
3293 (pi(pr.ns_id), pi(s), pi(p), pi(o), pi(seq)),
3294 )
3295 .await
3296 .map_err(db_err)?;
3297 if pr.osp {
3298 conn.execute(
3299 "INSERT INTO osp(ns,o,s,p,seq,cur) VALUES (?1,?2,?3,?4,?5,1)",
3300 (pi(pr.ns_id), pi(o), pi(s), pi(p), pi(seq)),
3301 )
3302 .await
3303 .map_err(db_err)?;
3304 }
3305 conn.execute(
3308 "INSERT OR REPLACE INTO heads(ns,s,p,seq,hash,created_at) VALUES (?1,?2,?3,?4,?5,?6)",
3309 (pi(pr.ns_id), pi(s), pi(p), pi(seq), pb(pr.hash.as_bytes().to_vec()), pi(pr.created)),
3310 )
3311 .await
3312 .map_err(db_err)?;
3313 let cur = {
3317 let mut rows = conn
3318 .query(
3319 "SELECT h.created_at, h.hash FROM heads h JOIN entity_latest e
3320 ON e.ns=h.ns AND e.s=h.s AND e.p=h.p AND e.seq=h.seq
3321 WHERE e.ns=?1 AND e.s=?2 AND e.p=?3",
3322 (pi(pr.ns_id), pi(s), pi(p)),
3323 )
3324 .await
3325 .map_err(db_err)?;
3326 match rows.next().await.map_err(db_err)? {
3327 Some(row) => Some((
3328 v_i64(&row.get_value(0).map_err(db_err)?).unwrap_or(0),
3329 v_blob(&row.get_value(1).map_err(db_err)?).unwrap_or_default(),
3330 )),
3331 None => None,
3332 }
3333 };
3334 let wins = match &cur {
3335 Some((c, h)) => (pr.created, pr.hash.as_bytes().as_slice()) > (*c, h.as_slice()),
3336 None => true,
3337 };
3338 if wins {
3339 conn.execute(
3340 "INSERT OR REPLACE INTO entity_latest(ns,s,p,o,seq,hash) VALUES (?1,?2,?3,?4,?5,?6)",
3341 (pi(pr.ns_id), pi(s), pi(p), pi(o), pi(seq), pb(pr.hash.as_bytes().to_vec())),
3342 )
3343 .await
3344 .map_err(db_err)?;
3345 }
3346 }
3347 if let Some(sess) = pr.session {
3348 conn.execute(
3349 "INSERT INTO thread_idx(ns,session,seq) VALUES (?1,?2,?3)",
3350 (pi(pr.ns_id), pi(sess), pi(seq)),
3351 )
3352 .await
3353 .map_err(db_err)?;
3354 }
3355 if let Some(ref emb) = pr.embedding {
3356 conn.execute(
3357 "INSERT INTO embeddings(seq, vec) VALUES (?1, vector32(?2))",
3358 (pi(seq), pt(&vec_to_json(emb))),
3359 )
3360 .await
3361 .map_err(db_err)?;
3362 }
3363 conn.execute(
3364 "INSERT INTO oplog(op_seq,hlc,op,hash) VALUES (?1,?2,?3,?4)",
3365 (pi(op_seq), pi(hlc_in), pi(op), pb(pr.hash.as_bytes().to_vec())),
3366 )
3367 .await
3368 .map_err(db_err)?;
3369 Ok::<(), DejaDbError>(())
3370 }
3371 .await;
3372 match r {
3373 Ok(()) => conn.execute("COMMIT", ()).await.map_err(db_err).map(|_| ()),
3374 Err(e) => {
3375 let _ = conn.execute("ROLLBACK", ()).await;
3376 Err(e)
3377 }
3378 }
3379 })
3380 }
3381
3382 fn apply_supersede_flip(&mut self, old: &Hash, new_hash: &Hash) -> Result<bool> {
3385 let conn = &self.conn;
3386 self.rt.block_on(async {
3387 let old_row = {
3388 let mut rows = conn
3389 .query(
3390 "SELECT seq, svt FROM grains WHERE hash = ?1",
3391 (pb(old.as_bytes().to_vec()),),
3392 )
3393 .await
3394 .map_err(db_err)?;
3395 match rows.next().await.map_err(db_err)? {
3396 Some(row) => Some((
3397 v_i64(&row.get_value(0).map_err(db_err)?).unwrap_or(0),
3398 v_i64(&row.get_value(1).map_err(db_err)?),
3399 )),
3400 None => None,
3401 }
3402 };
3403 let (old_seq, old_svt) = match old_row {
3404 Some(x) => x,
3405 None => return Ok(false), };
3407 if old_svt.is_some() {
3408 let existing = {
3413 let mut rows = conn
3414 .query("SELECT superseded_by FROM grains WHERE seq=?1", (pi(old_seq),))
3415 .await
3416 .map_err(db_err)?;
3417 match rows.next().await.map_err(db_err)? {
3418 Some(row) => v_blob(&row.get_value(0).map_err(db_err)?),
3419 None => None,
3420 }
3421 };
3422 if existing.as_deref() == Some(new_hash.as_bytes().as_slice()) {
3423 return Ok(false); }
3425 let inc = {
3427 let mut rows = conn
3428 .query(
3429 "SELECT seq, ns, s, p, o, created_at FROM grains WHERE hash=?1",
3430 (pb(new_hash.as_bytes().to_vec()),),
3431 )
3432 .await
3433 .map_err(db_err)?;
3434 match rows.next().await.map_err(db_err)? {
3435 Some(row) => Some((
3436 v_i64(&row.get_value(0).map_err(db_err)?).unwrap_or(0),
3437 v_i64(&row.get_value(1).map_err(db_err)?).unwrap_or(0),
3438 v_i64(&row.get_value(2).map_err(db_err)?).unwrap_or(0),
3439 v_i64(&row.get_value(3).map_err(db_err)?).unwrap_or(0),
3440 v_i64(&row.get_value(4).map_err(db_err)?).unwrap_or(0),
3441 v_i64(&row.get_value(5).map_err(db_err)?).unwrap_or(0),
3442 )),
3443 None => None,
3444 }
3445 };
3446 let Some((inc_seq, ns, s, p, o, inc_created)) = inc else { return Ok(false) };
3447 conn.execute(
3448 "INSERT OR REPLACE INTO heads(ns,s,p,seq,hash,created_at) VALUES (?1,?2,?3,?4,?5,?6)",
3449 (pi(ns), pi(s), pi(p), pi(inc_seq), pb(new_hash.as_bytes().to_vec()), pi(inc_created)),
3450 )
3451 .await
3452 .map_err(db_err)?;
3453 let cur = {
3455 let mut rows = conn
3456 .query(
3457 "SELECT h.created_at, h.hash FROM heads h JOIN entity_latest e
3458 ON e.ns=h.ns AND e.s=h.s AND e.p=h.p AND e.seq=h.seq
3459 WHERE e.ns=?1 AND e.s=?2 AND e.p=?3",
3460 (pi(ns), pi(s), pi(p)),
3461 )
3462 .await
3463 .map_err(db_err)?;
3464 match rows.next().await.map_err(db_err)? {
3465 Some(row) => Some((
3466 v_i64(&row.get_value(0).map_err(db_err)?).unwrap_or(0),
3467 v_blob(&row.get_value(1).map_err(db_err)?).unwrap_or_default(),
3468 )),
3469 None => None,
3470 }
3471 };
3472 let incoming_wins = match &cur {
3473 Some((c_created, c_hash)) => {
3474 (inc_created, new_hash.as_bytes().as_slice()) > (*c_created, c_hash.as_slice())
3475 }
3476 None => true,
3477 };
3478 if incoming_wins {
3479 conn.execute(
3480 "INSERT OR REPLACE INTO entity_latest(ns,s,p,o,seq,hash) VALUES (?1,?2,?3,?4,?5,?6)",
3481 (pi(ns), pi(s), pi(p), pi(o), pi(inc_seq), pb(new_hash.as_bytes().to_vec())),
3482 )
3483 .await
3484 .map_err(db_err)?;
3485 }
3486 return Ok(true); }
3488 let now = now_ms();
3489 conn.execute(
3490 "UPDATE grains SET superseded_by=?1, svt=?2 WHERE seq=?3",
3491 (pb(new_hash.as_bytes().to_vec()), pi(now), pi(old_seq)),
3492 )
3493 .await
3494 .map_err(db_err)?;
3495 conn.execute(
3496 "UPDATE grains SET supersedes=?1 WHERE hash=?2",
3497 (pb(old.as_bytes().to_vec()), pb(new_hash.as_bytes().to_vec())),
3498 )
3499 .await
3500 .map_err(db_err)?;
3501 conn.execute("UPDATE triples SET cur=0 WHERE seq=?1", (pi(old_seq),))
3502 .await
3503 .map_err(db_err)?;
3504 conn.execute("UPDATE osp SET cur=0 WHERE seq=?1", (pi(old_seq),))
3505 .await
3506 .map_err(db_err)?;
3507 conn.execute("DELETE FROM heads WHERE seq=?1", (pi(old_seq),))
3508 .await
3509 .map_err(db_err)?;
3510 Ok::<bool, DejaDbError>(true)
3511 })
3512 }
3513
3514 pub fn import_bundle(&mut self, path: &str) -> Result<ImportStats> {
3516 self.import_bundle_until(path, None)
3517 }
3518
3519 pub fn import_bundle_until(&mut self, path: &str, max_hlc: Option<i64>) -> Result<ImportStats> {
3522 let data = std::fs::read(path).map_err(db_err)?;
3523 if data.len() < 4 || &data[..4] != BUNDLE_MAGIC {
3524 return Err(DejaDbError::Format("not a MGB1 bundle".into()));
3525 }
3526 let mut stats = ImportStats::default();
3527 let mut i = 4usize;
3528 while i < data.len() {
3529 if i + 1 + 8 + 32 + 4 > data.len() {
3530 return Err(DejaDbError::Format("truncated bundle record".into()));
3531 }
3532 let op = data[i] as i64;
3533 i += 1;
3534 let hlc = i64::from_le_bytes(data[i..i + 8].try_into().unwrap());
3535 i += 8;
3536 let hash = Hash::try_from_bytes(&data[i..i + 32])?;
3537 i += 32;
3538 let len = u32::from_le_bytes(data[i..i + 4].try_into().unwrap()) as usize;
3539 i += 4;
3540 if i.checked_add(len).is_none_or(|end| end > data.len()) {
3541 return Err(DejaDbError::Format("truncated bundle blob".into()));
3542 }
3543 let blob = data[i..i + len].to_vec();
3544 i += len;
3545
3546 if let Some(t) = max_hlc {
3547 if hlc > t {
3548 stats.skipped += 1;
3549 continue; }
3551 }
3552 match op {
3553 OP_ADD => {
3554 if self.has_grain(&hash)? || blob.is_empty() {
3555 stats.skipped += 1;
3557 continue;
3558 }
3559 self.insert_blob(blob, hash, op, hlc)?;
3560 stats.applied += 1;
3561 }
3562 OP_SUPERSEDE => {
3563 let exists = self.has_grain(&hash)?;
3567 let bytes: Option<Vec<u8>> = if !blob.is_empty() {
3568 Some(blob)
3569 } else if exists {
3570 self.blob_by_hash(&hash)?
3571 } else {
3572 None
3573 };
3574 match bytes {
3575 None => stats.skipped += 1,
3576 Some(bb) => {
3577 let mut changed = false;
3578 if !exists {
3579 self.insert_blob(bb.clone(), hash, op, hlc)?;
3580 changed = true;
3581 }
3582 if let Ok(view) = deserialize_blob(&bb) {
3583 if let Some(df) = view.get_str("derived_from") {
3584 if let Ok(old) = Hash::from_hex(df) {
3585 changed |= self.apply_supersede_flip(&old, &hash)?;
3586 }
3587 }
3588 }
3589 if changed {
3590 stats.applied += 1;
3591 } else {
3592 stats.skipped += 1;
3593 }
3594 }
3595 }
3596 }
3597 OP_FORGET => match self.forget(&hash) {
3598 Ok(()) => stats.applied += 1,
3599 Err(DejaDbError::NotFound(_)) => stats.skipped += 1,
3600 Err(e) => return Err(e),
3601 },
3602 _ => return Err(DejaDbError::Format(format!("unknown bundle op {op}"))),
3603 }
3604 }
3605 Ok(stats)
3606 }
3607}
3608
3609pub mod memory_tool;
3610pub mod migrate;
3611
3612pub trait AddableDyn {
3614 fn serialize_dyn(&self) -> Result<(Vec<u8>, Hash)>;
3615}
3616
3617impl<G: Grain + 'static> AddableDyn for G {
3618 fn serialize_dyn(&self) -> Result<(Vec<u8>, Hash)> {
3619 serialize_grain(self)
3620 }
3621}
3622
3623#[cfg(test)]
3624mod tests {
3625 use super::*;
3632 use tempfile::TempDir;
3633
3634 #[test]
3637 fn hex32_roundtrips_to_64_chars() {
3638 assert_eq!(hex32(&[0u8; 32]), "0".repeat(64));
3639 assert_eq!(hex32(&[0xffu8; 32]), "f".repeat(64));
3640
3641 let mut key = [0u8; 32];
3643 for (i, b) in key.iter_mut().enumerate() {
3644 *b = i as u8;
3645 }
3646 let hexed = hex32(&key);
3647 assert_eq!(hexed.len(), 64);
3648 assert_eq!(&hexed[..2], "00");
3649 assert_eq!(&hexed[62..], "1f"); assert_eq!(hex::decode(&hexed).unwrap(), key.to_vec());
3651 }
3652
3653 #[test]
3654 fn seq_csv_formats_integer_lists() {
3655 assert_eq!(seq_csv(&[]), "");
3656 assert_eq!(seq_csv(&[42]), "42");
3657 assert_eq!(seq_csv(&[1, 2, 3]), "1,2,3");
3658 assert_eq!(seq_csv(&[-1, 0, 7]), "-1,0,7");
3660 }
3661
3662 #[test]
3663 fn vec_to_json_renders_float_arrays() {
3664 assert_eq!(vec_to_json(&[]), "[]");
3665 assert_eq!(vec_to_json(&[1.0]), "[1]");
3666 assert_eq!(vec_to_json(&[1.0, 2.5, -3.25]), "[1,2.5,-3.25]");
3667 }
3668
3669 #[test]
3672 fn value_bridge_roundtrips() {
3673 assert_eq!(v_i64(&pi(42)), Some(42));
3676 assert_eq!(v_i64(&pt("x")), None);
3677 assert_eq!(v_blob(&pb(vec![1, 2, 3])), Some(vec![1, 2, 3]));
3678 assert_eq!(v_blob(&pi(1)), None);
3679 assert_eq!(v_i64(&opt_i(Some(7))), Some(7));
3681 assert!(matches!(opt_i(None), Value::Null));
3682 assert!((v_f64(&Value::Real(1.5)).unwrap() - 1.5).abs() < 1e-12);
3684 assert!((v_f64(&pi(3)).unwrap() - 3.0).abs() < 1e-12);
3685 assert_eq!(v_f64(&pt("x")), None);
3686 }
3687
3688 fn kdf_line(salt: &[u8], m: u32, t: u32, p: u32) -> String {
3691 format!("v1 argon2id {} {m} {t} {p}", hex::encode(salt))
3692 }
3693
3694 #[test]
3695 fn parse_kdf_sidecar_accepts_valid() {
3696 let salt = [7u8; KDF_SALT_LEN];
3697 let text = kdf_line(&salt, KDF_M_COST, KDF_T_COST, KDF_P_COST);
3698 let (got_salt, m, t, p) = parse_kdf_sidecar(&text, "x.kdf").unwrap();
3699 assert_eq!(got_salt, salt);
3700 assert_eq!((m, t, p), (KDF_M_COST, KDF_T_COST, KDF_P_COST));
3701 assert!(parse_kdf_sidecar(&format!(" {text}\n"), "x.kdf").is_ok());
3703 let salt_hex = hex::encode([1u8; KDF_SALT_LEN]);
3705 assert!(parse_kdf_sidecar(&format!("v1 argon2id {salt_hex} 8 1 1"), "x").is_ok());
3706 assert!(parse_kdf_sidecar(&format!("v1 argon2id {salt_hex} 1048576 16 16"), "x").is_ok());
3707 }
3708
3709 #[test]
3710 fn parse_kdf_sidecar_rejects_malformed() {
3711 let salt = hex::encode([1u8; KDF_SALT_LEN]);
3712 assert!(parse_kdf_sidecar("v1 argon2id", "x").is_err());
3714 assert!(parse_kdf_sidecar(&format!("v1 argon2id {salt} 1 2 3 4"), "x").is_err());
3715 assert!(parse_kdf_sidecar(&format!("v2 argon2id {salt} 19456 2 1"), "x").is_err());
3717 assert!(parse_kdf_sidecar(&format!("v1 scrypt {salt} 19456 2 1"), "x").is_err());
3719 assert!(parse_kdf_sidecar("v1 argon2id zzzz 19456 2 1", "x").is_err());
3721 assert!(parse_kdf_sidecar(&format!("v1 argon2id {salt} m 2 1"), "x").is_err());
3722 }
3723
3724 #[test]
3725 fn parse_kdf_sidecar_rejects_wrong_salt_length() {
3726 let short = hex::encode([1u8; 8]);
3729 assert!(parse_kdf_sidecar(&format!("v1 argon2id {short} 19456 2 1"), "x").is_err());
3730 let long = hex::encode([1u8; 32]);
3731 assert!(parse_kdf_sidecar(&format!("v1 argon2id {long} 19456 2 1"), "x").is_err());
3732 }
3733
3734 #[test]
3735 fn parse_kdf_sidecar_rejects_out_of_range_params() {
3736 let salt = hex::encode([1u8; KDF_SALT_LEN]);
3737 assert!(parse_kdf_sidecar(&format!("v1 argon2id {salt} 7 2 1"), "x").is_err());
3739 assert!(parse_kdf_sidecar(&format!("v1 argon2id {salt} 1048577 2 1"), "x").is_err());
3740 assert!(parse_kdf_sidecar(&format!("v1 argon2id {salt} 19456 0 1"), "x").is_err());
3742 assert!(parse_kdf_sidecar(&format!("v1 argon2id {salt} 19456 17 1"), "x").is_err());
3743 assert!(parse_kdf_sidecar(&format!("v1 argon2id {salt} 19456 2 0"), "x").is_err());
3745 assert!(parse_kdf_sidecar(&format!("v1 argon2id {salt} 19456 2 17"), "x").is_err());
3746 }
3747
3748 #[test]
3751 fn derive_key_rejects_empty_or_whitespace_passphrase() {
3752 let dir = TempDir::new().unwrap();
3753 let path = dir.path().join("k.db");
3754 let path = path.to_str().unwrap();
3755 assert!(DejaDB::derive_key_for(path, "").is_err());
3756 assert!(DejaDB::derive_key_for(path, " ").is_err());
3757 assert!(DejaDB::derive_key_for(path, "\t\n ").is_err());
3758 assert!(!std::path::Path::new(&format!("{path}.kdf")).exists());
3760 }
3761
3762 #[test]
3763 fn derive_key_is_deterministic_for_same_salt() {
3764 let dir = TempDir::new().unwrap();
3765 let path = dir.path().join("k.db");
3766 let path = path.to_str().unwrap();
3767 let k1 = DejaDB::derive_key_for(path, "correct horse battery staple").unwrap();
3769 let k2 = DejaDB::derive_key_for(path, "correct horse battery staple").unwrap();
3770 assert_eq!(*k1, *k2);
3771 let k3 = DejaDB::derive_key_for(path, "a different passphrase").unwrap();
3773 assert_ne!(*k1, *k3);
3774 }
3775
3776 #[test]
3777 fn derive_key_differs_across_salts() {
3778 let dir = TempDir::new().unwrap();
3779 let p1 = dir.path().join("a.db");
3780 let p2 = dir.path().join("b.db");
3781 let (p1, p2) = (p1.to_str().unwrap(), p2.to_str().unwrap());
3782 let k1 = DejaDB::derive_key_for(p1, "same-pass").unwrap();
3785 let k2 = DejaDB::derive_key_for(p2, "same-pass").unwrap();
3786 assert_ne!(*k1, *k2);
3787 assert!(std::path::Path::new(&format!("{p1}.kdf")).exists());
3788 assert!(std::path::Path::new(&format!("{p2}.kdf")).exists());
3789 }
3790
3791 fn rrf_fuse(legs: &[&[i64]]) -> Vec<(i64, f64)> {
3800 let mut scores: HashMap<i64, f64> = HashMap::new();
3801 for leg in legs {
3802 for (rank, seq) in leg.iter().enumerate() {
3803 *scores.entry(*seq).or_insert(0.0) += 1.0 / (RRF_K0 + rank as f64);
3804 }
3805 }
3806 let mut ranked: Vec<(i64, f64)> = scores.into_iter().collect();
3807 ranked.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap().then(b.0.cmp(&a.0)));
3808 ranked
3809 }
3810
3811 #[test]
3812 fn rrf_k0_constant_is_pinned() {
3813 assert!((RRF_K0 - 60.0).abs() < f64::EPSILON);
3815 }
3816
3817 #[test]
3818 fn rrf_single_leg_preserves_rank_order() {
3819 let leg = [10i64, 20, 30];
3820 let fused = rrf_fuse(&[&leg]);
3821 assert_eq!(fused.iter().map(|(s, _)| *s).collect::<Vec<_>>(), vec![10, 20, 30]);
3822 assert!((fused[0].1 - 1.0 / 60.0).abs() < 1e-12);
3823 assert!((fused[1].1 - 1.0 / 61.0).abs() < 1e-12);
3824 assert!((fused[2].1 - 1.0 / 62.0).abs() < 1e-12);
3825 assert!(fused[0].1 > fused[1].1 && fused[1].1 > fused[2].1);
3827 }
3828
3829 #[test]
3830 fn rrf_rewards_agreement_across_legs() {
3831 let a = [1i64, 2];
3833 let b = [1i64, 3];
3834 let fused = rrf_fuse(&[&a, &b]);
3835 assert_eq!(fused[0].0, 1);
3837 let top = fused.iter().find(|(s, _)| *s == 1).unwrap().1;
3838 assert!((top - 2.0 / 60.0).abs() < 1e-12);
3839 let two = fused.iter().find(|(s, _)| *s == 2).unwrap().1;
3841 assert!(top > two);
3842 }
3843
3844 #[test]
3845 fn rrf_breaks_ties_by_seq_desc() {
3846 let l1 = [5i64];
3849 let l2 = [9i64];
3850 let fused = rrf_fuse(&[&l1, &l2]);
3851 assert!((fused[0].1 - fused[1].1).abs() < 1e-12);
3852 assert_eq!(fused[0].0, 9);
3853 assert_eq!(fused[1].0, 5);
3854 }
3855
3856 #[test]
3859 fn english_expander_substitutes_synonyms() {
3860 let ex = EnglishExpander::default();
3861 let v = ex.expand("cell");
3862 assert!(v.contains(&"mobile".to_string()));
3863 assert!(v.contains(&"phone".to_string()));
3864 assert!(!v.contains(&"cell".to_string()));
3866 }
3867
3868 #[test]
3869 fn english_expander_stems_and_is_bounded() {
3870 let ex = EnglishExpander::new(4);
3871 assert!(ex.expand("cars").contains(&"car".to_string()));
3873 assert!(ex.expand("").is_empty());
3875 assert!(ex.expand("cell phone email car").len() <= 4);
3877 }
3878
3879 fn open_tmp() -> (DejaDB, TempDir) {
3882 let dir = TempDir::new().unwrap();
3883 let path = dir.path().join("unit.db");
3884 let db = DejaDB::open(path.to_str().unwrap()).unwrap();
3885 (db, dir)
3886 }
3887
3888 #[test]
3889 fn next_hlc_is_strictly_monotonic_within_one_ms() {
3890 let (mut db, _d) = open_tmp();
3891 db.hlc_last = (now_ms() + 1_000_000) << 16;
3896 let a = db.next_hlc();
3897 let b = db.next_hlc();
3898 let c = db.next_hlc();
3899 assert_eq!(b, a + 1);
3900 assert_eq!(c, b + 1);
3901 }
3902
3903 #[test]
3904 fn next_hlc_tracks_wall_clock_when_it_advances() {
3905 let (mut db, _d) = open_tmp();
3906 let before = now_ms();
3907 db.hlc_last = 0;
3908 let first = db.next_hlc();
3909 let after = now_ms();
3910 assert!(first >> 16 >= before && first >> 16 <= after);
3913 }
3914
3915 #[test]
3916 fn next_hlc_never_repeats_over_many_calls() {
3917 let (mut db, _d) = open_tmp();
3918 let mut last = db.next_hlc();
3919 for _ in 0..5000 {
3920 let n = db.next_hlc();
3921 assert!(n > last, "HLC must strictly increase: {n} !> {last}");
3922 last = n;
3923 }
3924 }
3925
3926 #[test]
3927 fn term_id_interns_and_reverse_scans() {
3928 let (mut db, _d) = open_tmp();
3929 let a = db.term_id("alice").unwrap();
3930 let b = db.term_id("bob").unwrap();
3931 assert_ne!(a, b);
3932 assert_eq!(db.term_id("alice").unwrap(), a);
3934 assert_eq!(db.term_lookup("alice"), Some(a));
3936 assert_eq!(db.term_lookup("bob"), Some(b));
3937 assert_eq!(db.term_lookup("nobody"), None);
3938 assert_eq!(db.term_str(a).as_deref(), Some("alice"));
3940 assert_eq!(db.term_str(b).as_deref(), Some("bob"));
3941 assert_eq!(db.term_str(999_999), None);
3942 }
3943
3944 #[test]
3945 fn term_ids_persist_and_continue_across_reopen() {
3946 let dir = TempDir::new().unwrap();
3947 let path = dir.path().join("dict.db");
3948 let path = path.to_str().unwrap();
3949 let (a, b);
3950 {
3951 let mut db = DejaDB::open(path).unwrap();
3952 a = db.term_id("x").unwrap();
3953 b = db.term_id("y").unwrap();
3954 assert!(b > a);
3955 }
3956 {
3959 let mut db = DejaDB::open(path).unwrap();
3960 assert_eq!(db.term_lookup("x"), Some(a));
3961 assert_eq!(db.term_lookup("y"), Some(b));
3962 let c = db.term_id("z").unwrap();
3963 assert!(c > b);
3964 assert_eq!(db.term_str(c).as_deref(), Some("z"));
3965 }
3966 }
3967}