1use std::fs;
4use std::path::{Path, PathBuf};
5use std::sync::Arc;
6use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
7use anyhow::Result;
8use dashmap::DashMap;
9use serde_json::Value;
10use parking_lot::RwLock;
11
12use crate::store::{Dek, Node, ObjectStore};
13use crate::index::{IdIndex, OrderedValue, SortedIndexes};
14use crate::graph::GraphStore;
15use crate::migrate;
16
17#[derive(serde::Serialize, serde::Deserialize)]
21struct Manifest {
22 seq: u64,
23 head: String,
24 #[serde(default)]
28 tip_hash: String,
29 #[serde(default)]
35 coll_tips: std::collections::HashMap<String, String>,
36}
37
38pub const DEFAULT_SINCE_LIMIT: usize = 10_000;
42
43#[derive(Debug, Clone, serde::Serialize)]
49pub struct SinceBatch {
50 pub nodes: Vec<Node>,
52 pub from_seq: u64,
54 pub to_seq: u64,
56 pub head_seq: u64,
58 pub has_more: bool,
60}
61
62#[derive(Debug, Clone, serde::Serialize)]
69pub struct ScanStatus {
70 pub scan_complete: bool,
72 pub tip_seq: u64,
74 pub indexed_seq_min: u64,
76 pub indexed_seq_max: u64,
78 pub indexed_count: usize,
80}
81
82pub struct Db {
83 pub objects: ObjectStore,
84 pub id_index: IdIndex,
85 pub sorted_indexes: SortedIndexes,
86 pub graph: GraphStore,
87 pub root: PathBuf,
88 _dir_lock: Option<std::fs::File>,
98 manifest_dirty: Arc<AtomicBool>,
102 pub seq: AtomicU64,
103 head: RwLock<String>,
105 tip_hash: RwLock<(u64, String)>,
112 coll_tip_hash: Arc<DashMap<String, (u64, String)>>,
118 pub startup_ready: Arc<AtomicBool>,
123 seq_index: Arc<DashMap<u64, String>>,
127}
128
129impl Db {
130 pub fn in_memory() -> Self {
134 Self {
135 objects: ObjectStore::in_memory(),
136 id_index: IdIndex::in_memory(),
137 sorted_indexes: SortedIndexes::new(),
138 graph: GraphStore::in_memory(),
139 root: std::path::PathBuf::from(":memory:"),
140 _dir_lock: None,
141 seq: AtomicU64::new(0),
142 head: RwLock::new(String::new()),
143 tip_hash: RwLock::new((0, String::new())),
144 coll_tip_hash: Arc::new(DashMap::new()),
145 startup_ready: Arc::new(AtomicBool::new(true)), manifest_dirty: Arc::new(AtomicBool::new(false)),
147 seq_index: Arc::new(DashMap::new()),
148 }
149 }
150
151 fn acquire_dir_lock(db_root: &Path) -> Result<Option<std::fs::File>> {
156 if std::env::var("NEDB_SHARED_OPEN").map(|v| v.trim() == "1").unwrap_or(false) {
157 return Ok(None);
158 }
159 use fs2::FileExt as _;
160 use std::io::Write as _;
161 let lock_path = db_root.join("LOCK");
162 let lock_file = std::fs::OpenOptions::new()
163 .create(true).read(true).write(true).open(&lock_path)?;
164 if lock_file.try_lock_exclusive().is_err() {
165 let holder = std::fs::read_to_string(&lock_path).unwrap_or_default();
166 let holder = holder.trim();
167 anyhow::bail!(
168 "data directory {:?} is locked by another process{} — refusing a \
169 split-brain open: a second engine on the same files cannot see this \
170 process's writes (invisible sessions, CAS races). Stop the other \
171 process, or set NEDB_SHARED_OPEN=1 only if you accept that risk.",
172 db_root,
173 if holder.is_empty() { String::new() } else { format!(" (pid {holder})") }
174 );
175 }
176 let _ = lock_file.set_len(0);
178 let _ = writeln!(&lock_file, "{}", std::process::id());
179 let _ = lock_file.sync_all();
180 Ok(Some(lock_file))
181 }
182
183 pub fn open(db_root: &Path, dek: Option<Dek>) -> Result<Self> {
185 std::fs::create_dir_all(db_root)?;
186
187 let dir_lock = Self::acquire_dir_lock(db_root)?;
189
190 let objects = ObjectStore::new(db_root, dek.clone())?;
191 let id_index = IdIndex::new(db_root)?;
192 let sorted_indexes = SortedIndexes::new();
193 let graph = GraphStore::new(db_root)?;
194
195 let mut db = Self {
196 objects,
197 id_index,
198 sorted_indexes,
199 graph,
200 root: db_root.to_path_buf(),
201 _dir_lock: dir_lock,
202 seq: AtomicU64::new(0),
203 head: RwLock::new(String::new()),
204 tip_hash: RwLock::new((0, String::new())),
205 coll_tip_hash: Arc::new(DashMap::new()),
206 startup_ready: Arc::new(AtomicBool::new(false)),
207 manifest_dirty: Arc::new(AtomicBool::new(false)),
208 seq_index: Arc::new(DashMap::new()),
209 };
210
211 migrate::migrate_if_needed(
213 db_root,
214 &db.objects,
215 &db.id_index,
216 &db.sorted_indexes,
217 &db.graph,
218 dek.as_ref(),
219 )?;
220
221 db.startup_rebuild()?;
224
225 Ok(db)
226 }
227
228 fn startup_rebuild(&mut self) -> Result<()> {
233 let manifest_path = self.root.join("MANIFEST");
234 let needs_index_rebuild = !self.sorted_indexes.is_empty();
235
236 if manifest_path.exists() && !needs_index_rebuild {
238 if let Some(m) = fs::read_to_string(&manifest_path)
239 .ok()
240 .and_then(|s| serde_json::from_str::<Manifest>(&s).ok())
241 {
242 if m.head.len() < 8 {
245 eprintln!(" [nedbd] MANIFEST head invalid (len={}), self-healing via cold scan", m.head.len());
246 } else {
247 if m.tip_hash.is_empty() {
265 eprintln!(" [nedbd] MANIFEST predates durable tip() — warm boot; tip()/tip_collection() heal on first flush (no forced scan)");
266 }
267 self.seq.store(m.seq, Ordering::SeqCst); *self.head.write() = m.head.clone();
269 *self.tip_hash.write() = (m.seq.saturating_sub(1), m.tip_hash.clone());
271 for (coll, hash) in &m.coll_tips {
272 self.coll_tip_hash.insert(coll.clone(), (0, hash.clone()));
277 }
278 self.startup_ready.store(true, Ordering::SeqCst);
279 println!(" [nedbd] warm start — seq={} head={}... tip={}...",
280 m.seq, &m.head[..8],
281 if m.tip_hash.is_empty() { "(pre-2.5.43, heals on flush)" }
282 else { &m.tip_hash[..8.min(m.tip_hash.len())] });
283 return Ok(());
284 }
285 } else {
286 eprintln!(" [nedbd] MANIFEST corrupt or missing, falling back to cold scan");
287 }
288 }
289
290 println!(" [nedbd] cold start — background scan will start after heap allocation");
296 Ok(())
297 }
298
299 pub fn start_cold_scan(self_arc: Arc<Self>) {
303 if self_arc.startup_ready.load(Ordering::SeqCst) {
304 return; }
306 if self_arc.objects.all_hashes().next().is_none() {
309 self_arc.startup_ready.store(true, Ordering::SeqCst);
310 return;
311 }
312 println!(" [nedbd] cold start — background scan starting, server accepting reads now");
313 std::thread::spawn(move || {
314 let db = self_arc;
315 cold_scan_background_arc(db);
316 });
317 }
318
319 pub fn put(
321 &self,
322 coll: &str,
323 id: &str,
324 data: Value,
325 caused_by: Vec<String>,
326 valid_from: Option<String>,
327 valid_to: Option<String>,
328 ) -> Result<Node> {
329 let seq = self.seq.fetch_add(1, Ordering::SeqCst);
330 let prev = self.id_index.get(coll, id);
331
332 if !self.sorted_indexes.is_empty() {
338 if let Some(old_hash) = &prev {
339 if let Ok(old_node) = self.objects.read(old_hash) {
340 if let Value::Object(ref obj) = old_node.data {
341 for (field, value) in obj {
342 self.sorted_indexes.remove(coll, field, value, old_hash);
343 }
344 }
345 }
346 }
347 }
348
349 let mut node = Node {
350 id: id.to_string(),
351 coll: coll.to_string(),
352 seq,
353 data: data.clone(),
354 prev,
355 caused_by: caused_by.clone(),
356 ts: now(),
357 valid_from,
358 valid_to,
359 hash: String::new(),
360 };
361
362 let hash = self.objects.write(&mut node)?;
364 self.seq_index.insert(seq, hash.clone());
365
366 self.id_index.set(coll, id, &hash)?;
368
369 if let Value::Object(ref obj) = data {
371 for (field, value) in obj {
372 if self.sorted_indexes.has(coll, field) {
373 self.sorted_indexes.insert(coll, field, value, &hash);
374 }
375 }
376 }
377
378 for cause in &caused_by {
380 self.graph.add_edge(&hash, "caused_by", cause)?;
381 self.graph.add_edge(cause, "caused_by_rev", &hash)?;
382 }
383
384 self.update_head(coll, seq, &hash);
387
388 Ok(node)
389 }
390
391 pub fn put_batch(
396 &self,
397 ops: Vec<(String, String, Value, Vec<String>, Option<String>, Option<String>)>,
398 ) -> Result<Vec<Node>> {
400 use rayon::prelude::*;
401
402 if ops.is_empty() { return Ok(vec![]); }
403 let n = ops.len() as u64;
404
405 let base_seq = self.seq.fetch_add(n, Ordering::SeqCst);
407 let ts = now();
408
409 let index_live = !self.sorted_indexes.is_empty();
411 let mut nodes: Vec<Node> = ops.into_iter().enumerate().map(|(i, (coll, id, data, caused_by, valid_from, valid_to))| {
412 let prev = self.id_index.get(&coll, &id);
413 if index_live {
419 if let Some(old_hash) = &prev {
420 if let Ok(old_node) = self.objects.read(old_hash) {
421 if let Value::Object(ref obj) = old_node.data {
422 for (field, value) in obj {
423 self.sorted_indexes.remove(&coll, field, value, old_hash);
424 }
425 }
426 }
427 }
428 }
429 Node {
430 id, coll, seq: base_seq + i as u64,
431 data, prev, caused_by,
432 ts, valid_from, valid_to,
433 hash: String::new(),
434 }
435 }).collect();
436
437 let write_errors: Vec<anyhow::Error> = nodes.par_iter_mut()
439 .filter_map(|node| self.objects.write(node).err())
440 .collect();
441 if let Some(e) = write_errors.into_iter().next() { return Err(e); }
442
443 let index_errors: Vec<anyhow::Error> = nodes.par_iter()
445 .filter_map(|node| self.id_index.set(&node.coll, &node.id, &node.hash).err())
446 .collect();
447 if let Some(e) = index_errors.into_iter().next() { return Err(e); }
448
449 for node in &nodes {
451 self.seq_index.insert(node.seq, node.hash.clone());
452 if let Value::Object(ref obj) = node.data {
453 for (field, value) in obj {
454 if self.sorted_indexes.has(&node.coll, field) {
455 self.sorted_indexes.insert(&node.coll, field, value, &node.hash);
456 }
457 }
458 }
459 for cause in &node.caused_by {
460 self.graph.add_edge(&node.hash, "caused_by", cause).ok();
461 self.graph.add_edge(cause, "caused_by_rev", &node.hash).ok();
462 }
463 }
464
465 for node in &nodes {
467 self.update_head(&node.coll, node.seq, &node.hash);
468 }
469
470 Ok(nodes)
471 }
472
473 fn update_head(&self, coll: &str, seq: u64, new_hash: &str) {
489 use blake2::{Blake2b512, Digest};
490 {
491 let mut head = self.head.write();
492 let mut h = Blake2b512::new();
493 h.update(head.as_bytes());
494 h.update(seq.to_le_bytes());
495 h.update(new_hash.as_bytes());
496 *head = hex::encode(&h.finalize()[..32]);
497 }
498 {
499 let mut tip = self.tip_hash.write();
500 if seq >= tip.0 {
501 *tip = (seq, new_hash.to_string());
502 }
503 }
504 self.coll_tip_hash
505 .entry(coll.to_string())
506 .and_modify(|t| {
507 if seq >= t.0 {
508 *t = (seq, new_hash.to_string());
509 }
510 })
511 .or_insert_with(|| (seq, new_hash.to_string()));
512 self.manifest_dirty.store(true, Ordering::Release);
514 }
515
516 pub fn flush_all(&self) {
518 self.id_index.flush_write_buf();
519 if let Err(e) = self.objects.sync() {
522 eprintln!("nedb: segment sync failed: {}", e);
523 }
524 self.flush_manifest();
525 }
526
527 pub fn compact(&self) -> Result<crate::segment::CompactStats> {
536 self.flush_all();
537 let mut live: std::collections::HashSet<String> = std::collections::HashSet::new();
538 for coll in self.id_index.collections() {
539 for id in self.id_index.list_ids(&coll) {
540 if let Some(h) = self.id_index.get(&coll, &id) {
541 live.insert(h);
542 }
543 }
544 }
545 self.objects.compact(&live)
546 }
547
548 pub fn flush_manifest_if_dirty(&self) {
550 if self.root == std::path::PathBuf::from(":memory:") { return; }
551 if self.manifest_dirty.compare_exchange(
552 true, false, Ordering::AcqRel, Ordering::Relaxed
553 ).is_ok() {
554 self.flush_manifest();
555 }
556 }
557
558 pub fn flush_manifest(&self) {
560 if self.root == std::path::PathBuf::from(":memory:") { return; }
561 let seq = self.seq.load(Ordering::SeqCst);
562 let head = self.head.read().clone();
563 let tip_hash = self.tip_hash.read().1.clone();
564 let coll_tips: std::collections::HashMap<String, String> = self.coll_tip_hash
565 .iter()
566 .map(|kv| (kv.key().clone(), kv.value().1.clone()))
567 .collect();
568 let m = Manifest { seq, head, tip_hash, coll_tips };
569 if let Ok(json) = serde_json::to_string(&m) {
570 let path = self.root.join("MANIFEST");
571 let tmp = self.root.join("MANIFEST.tmp");
572 let wrote = (|| -> std::io::Result<()> {
579 use std::io::Write;
580 let mut f = fs::File::create(&tmp)?;
581 f.write_all(json.as_bytes())?;
582 f.sync_all()
583 })();
584 if wrote.is_ok() && fs::rename(&tmp, &path).is_ok() {
585 #[cfg(unix)]
589 if let Ok(dir) = fs::File::open(&self.root) {
590 let _ = dir.sync_all();
591 }
592 }
593 }
594 }
595
596 pub fn embedded_flush_interval_ms() -> Option<u64> {
612 match std::env::var("NEDB_FLUSH_MS") {
613 Err(_) => Some(1000),
614 Ok(v) => {
615 let v = v.trim().to_ascii_lowercase();
616 if v.is_empty() { return Some(1000); }
617 if v == "0" || v == "off" || v == "false" || v == "no" { return None; }
618 match v.parse::<u64>() {
619 Ok(ms) => Some(ms.max(50)),
620 Err(_) => { eprintln!("nedb: NEDB_FLUSH_MS={:?} is not a number — using 1000", v); Some(1000) }
621 }
622 }
623 }
624 }
625
626 pub fn start_manifest_ticker(self_arc: Arc<Self>, interval_ms: u64) {
627 let db = self_arc;
628 std::thread::spawn(move || {
629 loop {
630 std::thread::sleep(std::time::Duration::from_millis(interval_ms));
631 db.id_index.flush_write_buf();
633 if db.manifest_dirty.load(Ordering::Acquire) {
643 if let Err(e) = db.objects.sync() {
644 eprintln!("nedb: segment sync failed: {}", e);
645 }
646 db.flush_manifest_if_dirty();
647 }
648 }
649 });
650 }
651
652 pub fn head(&self) -> String {
654 self.head.read().clone()
655 }
656
657 pub fn delete(&self, coll: &str, id: &str) -> Result<bool> {
660 let prev = match self.id_index.get(coll, id) {
661 None => return Ok(false), Some(h) => h,
663 };
664 let seq = self.seq.fetch_add(1, Ordering::SeqCst);
665 let mut tombstone = Node {
666 id: format!("_del_{}", id),
667 coll: coll.to_string(),
668 seq,
669 data: serde_json::json!({"_deleted": id, "_prev": prev}),
670 prev: Some(prev),
671 caused_by: vec![],
672 ts: now(),
673 valid_from: None,
674 valid_to: None,
675 hash: String::new(),
676 };
677 let hash = self.objects.write(&mut tombstone)?;
678 self.update_head(coll, seq, &hash);
679 self.id_index.remove(coll, id)?;
681 Ok(true)
682 }
683
684 pub fn get(&self, coll: &str, id: &str) -> Option<Node> {
686 let hash = self.id_index.get(coll, id)?;
687 self.objects.read(&hash).ok()
688 }
689
690 pub fn get_by_hash(&self, hash: &str) -> Option<Node> {
692 self.objects.read(hash).ok()
693 }
694
695 pub fn get_as_of(&self, coll: &str, id: &str, target_seq: u64) -> Option<Node> {
698 let hash = self.id_index.get(coll, id)?;
699 let mut current = self.objects.read(&hash).ok()?;
700 loop {
701 if current.seq <= target_seq {
702 return Some(current);
703 }
704 let prev_hash = current.prev.as_deref()?;
705 current = self.objects.read(prev_hash).ok()?;
706 }
707 }
708
709 pub fn list(&self, coll: &str) -> Vec<Node> {
711 self.id_index
712 .list_ids(coll)
713 .into_iter()
714 .filter_map(|id| self.get(coll, &id))
715 .collect()
716 }
717
718 pub fn order_by_asc(&self, coll: &str, field: &str, limit: usize) -> Vec<Node> {
720 if self.sorted_indexes.has(coll, field) {
721 self.sorted_indexes
722 .top_k_asc(coll, field, limit)
723 .into_iter()
724 .filter_map(|h| self.objects.read(&h).ok())
725 .collect()
726 } else {
727 let mut docs = self.list(coll);
728 docs.sort_by(|a, b| {
729 let av = a.data.get(field).map(OrderedValue::from).unwrap_or(OrderedValue::Null);
730 let bv = b.data.get(field).map(OrderedValue::from).unwrap_or(OrderedValue::Null);
731 av.cmp(&bv)
732 });
733 docs.truncate(limit);
734 docs
735 }
736 }
737
738 pub fn order_by_desc(&self, coll: &str, field: &str, limit: usize) -> Vec<Node> {
740 if self.sorted_indexes.has(coll, field) {
741 self.sorted_indexes
742 .top_k_desc(coll, field, limit)
743 .into_iter()
744 .filter_map(|h| self.objects.read(&h).ok())
745 .collect()
746 } else {
747 let mut docs = self.list(coll);
748 docs.sort_by(|a, b| {
749 let av = a.data.get(field).map(OrderedValue::from).unwrap_or(OrderedValue::Null);
750 let bv = b.data.get(field).map(OrderedValue::from).unwrap_or(OrderedValue::Null);
751 bv.cmp(&av)
752 });
753 docs.truncate(limit);
754 docs
755 }
756 }
757
758 pub fn trace(&self, hash: &str, reverse: bool, limit: usize) -> Vec<Node> {
760 self.graph
761 .trace(hash, "caused_by", reverse, limit)
762 .into_iter()
763 .filter_map(|h| self.objects.read(&h).ok())
764 .collect()
765 }
766
767 pub fn verify(&self) -> (usize, Vec<String>) {
769 self.objects.verify_all()
770 }
771
772 pub fn create_sorted_index(&self, coll: &str, field: &str) {
774 self.sorted_indexes.ensure(coll, field);
775 for id in self.id_index.list_ids(coll) {
777 if let Some(node) = self.get(coll, &id) {
778 if let Value::Object(ref obj) = node.data {
779 if let Some(value) = obj.get(field) {
780 self.sorted_indexes.insert(coll, field, value, &node.hash);
781 }
782 }
783 }
784 }
785 }
786
787 pub fn get_hash_by_seq(&self, seq: u64) -> Option<String> {
790 self.seq_index.get(&seq).map(|r| r.clone())
791 }
792
793 pub fn tip(&self) -> Option<Node> {
801 let next = self.seq.load(Ordering::SeqCst);
802 if next == 0 {
803 return None; }
805 if let Some(hash) = self.get_hash_by_seq(next - 1) {
808 return self.get_by_hash(&hash);
809 }
810 let th = self.tip_hash.read().1.clone();
814 if !th.is_empty() {
815 return self.get_by_hash(&th);
816 }
817 None
818 }
819
820 pub fn tip_collection(&self, coll: &str) -> Option<Node> {
831 let hash = self.coll_tip_hash.get(coll)?.1.clone();
832 self.get_by_hash(&hash)
833 }
834
835 pub fn since(&self, after_seq: u64, limit: usize) -> SinceBatch {
846 let next = self.seq.load(Ordering::SeqCst); let head_seq = next.saturating_sub(1);
848 let cap = if limit == 0 { DEFAULT_SINCE_LIMIT } else { limit };
849 let mut nodes: Vec<Node> = Vec::new();
850 let mut to_seq = after_seq;
851 let mut hit_limit = false;
852 let mut s = after_seq.saturating_add(1);
853 while s < next {
854 if nodes.len() >= cap { hit_limit = true; break; }
855 if let Some(hash) = self.get_hash_by_seq(s) {
856 if let Some(node) = self.get_by_hash(&hash) {
857 to_seq = node.seq;
858 nodes.push(node);
859 }
860 }
861 s += 1;
862 }
863 SinceBatch { nodes, from_seq: after_seq, to_seq, head_seq, has_more: hit_limit }
864 }
865
866 pub fn scan_status(&self) -> ScanStatus {
873 let next = self.seq.load(Ordering::SeqCst);
874 let mut min = u64::MAX;
875 let mut max = 0u64;
876 let mut count = 0usize;
877 for kv in self.seq_index.iter() {
878 let s = *kv.key();
879 if s < min { min = s; }
880 if s > max { max = s; }
881 count += 1;
882 }
883 if count == 0 { min = 0; }
884 ScanStatus {
885 scan_complete: self.startup_ready.load(Ordering::SeqCst),
886 tip_seq: next.saturating_sub(1),
887 indexed_seq_min: min,
888 indexed_seq_max: max,
889 indexed_count: count,
890 }
891 }
892
893 pub fn link(&self, frm: &str, rel: &str, to: &str) -> Result<()> {
898 let (frm_coll, frm_id) = frm.split_once(':')
899 .ok_or_else(|| anyhow::anyhow!("link frm must be 'coll:id', got: {}", frm))?;
900 let (to_coll, to_id) = to.split_once(':')
901 .ok_or_else(|| anyhow::anyhow!("link to must be 'coll:id', got: {}", to))?;
902 if self.id_index.get(frm_coll, frm_id).is_none() {
903 anyhow::bail!("link: frm not found: {}", frm);
904 }
905 if self.id_index.get(to_coll, to_id).is_none() {
906 anyhow::bail!("link: to not found: {}", to);
907 }
908 let link_id = format!("{}|{}|{}", frm, rel, to);
909 let doc = serde_json::json!({"_from": frm, "_rel": rel, "_to": to});
910 self.put("__links__", &link_id, doc, vec![], None, None)?;
911 Ok(())
912 }
913
914 pub fn unlink(&self, frm: &str, rel: &str, to: &str) -> Result<bool> {
916 let link_id = format!("{}|{}|{}", frm, rel, to);
917 self.delete("__links__", &link_id)
918 }
919
920 pub fn neighbors(&self, frm: &str, rel: &str) -> Vec<Node> {
923 self.id_index
924 .list_ids("__links__")
925 .into_iter()
926 .filter_map(|id| self.get("__links__", &id))
927 .filter(|node| {
928 node.data.get("_from").and_then(|v| v.as_str()) == Some(frm)
929 && node.data.get("_rel").and_then(|v| v.as_str()) == Some(rel)
930 })
931 .filter_map(|node| {
932 let to = node.data.get("_to")?.as_str()?;
933 let (to_coll, to_id) = to.split_once(':')?;
934 self.get(to_coll, to_id)
935 })
936 .collect()
937 }
938}
939
940impl Drop for Db {
941 fn drop(&mut self) {
957 self.flush_all();
958 }
959}
960
961fn cold_scan_background_arc(db: Arc<Db>) {
963 use rayon::prelude::*;
964 use blake2::{Blake2b512, Digest};
965
966 let objects = &db.objects;
967 let head = &db.head;
968 let seq_atomic = &db.seq;
969 let sorted_indexes = &db.sorted_indexes;
970 let seq_index = &db.seq_index;
971 let ready_flag = Arc::clone(&db.startup_ready);
972
973 let hashes: Vec<String> = objects.all_hashes().collect();
974 let total = hashes.len();
975
976 if total == 0 {
977 ready_flag.store(true, Ordering::SeqCst);
978 return;
979 }
980
981 println!(" [nedbd] background scan — {} objects...", total);
982 let t0 = std::time::Instant::now();
983 let step = (total / 10).max(1000);
984
985 let nodes: Vec<Node> = hashes.par_iter()
995 .enumerate()
996 .filter_map(|(i, h)| {
997 if i > 0 && i % step == 0 {
998 let pct = i * 100 / total;
999 let elapsed = t0.elapsed().as_secs_f32();
1000 let rate = i as f32 / elapsed;
1001 let eta = (total - i) as f32 / rate;
1002 eprint!("\r [nedbd] {:>3}% {:>8} / {:>8} ({:>8.0}/s eta {:.0}s) ",
1003 pct, i, total, rate, eta);
1004 }
1005 let node = objects.read(h).ok()?;
1006 seq_index.insert(node.seq, node.hash.clone());
1007 Some(node)
1008 })
1009 .collect();
1010
1011 eprintln!("\r [nedbd] 100% {:>8} / {:>8} ({:.1}s) ",
1012 total, total, t0.elapsed().as_secs_f32());
1013
1014 let max_seq = nodes.iter().map(|n| n.seq).max().unwrap_or(0);
1015 seq_atomic.store(max_seq + 1, Ordering::SeqCst);
1016
1017 let mut coll_max: std::collections::HashMap<String, (u64, String)> = std::collections::HashMap::new();
1022
1023 for node in &nodes {
1024 coll_max.entry(node.coll.clone())
1026 .and_modify(|(s, h)| if node.seq > *s { *s = node.seq; *h = node.hash.clone(); })
1027 .or_insert_with(|| (node.seq, node.hash.clone()));
1028 if let Value::Object(ref obj) = node.data {
1029 for (field, value) in obj {
1030 if sorted_indexes.has(&node.coll, field) {
1031 sorted_indexes.insert(&node.coll, field, value, &node.hash);
1032 }
1033 }
1034 }
1035 }
1036
1037 for (coll, (seq, hash)) in coll_max {
1038 db.coll_tip_hash.insert(coll, (seq, hash));
1039 }
1040
1041 let mut sorted_hashes = hashes;
1043 sorted_hashes.sort();
1044 let mut h = Blake2b512::new();
1045 h.update(max_seq.to_le_bytes());
1046 for hash_str in &sorted_hashes {
1047 h.update(hash_str.as_bytes());
1048 }
1049 let new_head = hex::encode(&h.finalize()[..32]);
1050 *head.write() = new_head;
1051
1052 let tip_hash = db.seq_index.iter()
1055 .max_by_key(|kv| *kv.key())
1056 .map(|kv| kv.value().clone())
1057 .unwrap_or_default();
1058 *db.tip_hash.write() = (max_seq, tip_hash);
1059
1060 db.flush_manifest();
1067
1068 ready_flag.store(true, Ordering::SeqCst);
1070 println!(" [nedbd] background scan complete — seq={} objects={} MANIFEST written", max_seq, total);
1071}
1072
1073fn now() -> f64 {
1074 std::time::SystemTime::now()
1075 .duration_since(std::time::UNIX_EPOCH)
1076 .map(|d| d.as_secs_f64())
1077 .unwrap_or(0.0)
1078}
1079
1080#[cfg(test)]
1081mod tests {
1082 use super::*;
1083 use tempfile::tempdir;
1084
1085 #[test]
1086 fn put_and_get() {
1087 let dir = tempdir().unwrap();
1088 let db = Db::open(dir.path(), None).unwrap();
1089 db.put(
1090 "blocks", "618000",
1091 serde_json::json!({"height": 618000, "hash": "0000abc"}),
1092 vec![], None, None,
1093 ).unwrap();
1094 let node = db.get("blocks", "618000").unwrap();
1095 assert_eq!(node.id, "618000");
1096 assert_eq!(node.data["height"], 618000);
1097 }
1098
1099 #[test]
1100 fn order_by_with_sorted_index() {
1101 let dir = tempdir().unwrap();
1102 let db = Db::open(dir.path(), None).unwrap();
1103 db.create_sorted_index("blocks", "height");
1104 for h in [3u64, 1, 5, 2, 4] {
1105 db.put("blocks", &h.to_string(),
1106 serde_json::json!({"height": h}),
1107 vec![], None, None).unwrap();
1108 }
1109 let asc = db.order_by_asc("blocks", "height", 3);
1110 let heights: Vec<u64> = asc.iter()
1111 .filter_map(|n| n.data["height"].as_u64())
1112 .collect();
1113 assert_eq!(heights, vec![1, 2, 3]);
1114 }
1115
1116 #[test]
1117 fn causal_trace() {
1118 let dir = tempdir().unwrap();
1119 let db = Db::open(dir.path(), None).unwrap();
1120 let a = db.put("ops", "a", serde_json::json!({"op": "create"}), vec![], None, None).unwrap();
1121 let b = db.put("ops", "b", serde_json::json!({"op": "transfer"}), vec![a.hash.clone()], None, None).unwrap();
1122 let c = db.put("ops", "c", serde_json::json!({"op": "burn"}), vec![b.hash.clone()], None, None).unwrap();
1123
1124 let trace = db.trace(&c.hash, false, 10);
1125 assert_eq!(trace.len(), 3); }
1127
1128 #[test]
1129 fn as_of() {
1130 let dir = tempdir().unwrap();
1131 let db = Db::open(dir.path(), None).unwrap();
1132 let v1 = db.put("docs", "x", serde_json::json!({"v": 1}), vec![], None, None).unwrap();
1133 let _v2 = db.put("docs", "x", serde_json::json!({"v": 2}), vec![], None, None).unwrap();
1134
1135 let at_v1 = db.get_as_of("docs", "x", v1.seq).unwrap();
1136 assert_eq!(at_v1.data["v"], 1);
1137 let current = db.get("docs", "x").unwrap();
1138 assert_eq!(current.data["v"], 2);
1139 }
1140}
1141
1142#[cfg(test)]
1143mod tests_v2 {
1144 use super::*;
1145 use tempfile::tempdir;
1146
1147 #[test]
1148 fn seq_index_populated_on_put() {
1149 let db = Db::in_memory();
1150 let a = db.put("item", "a", serde_json::json!({"x": 1}), vec![], None, None).unwrap();
1151 let b = db.put("item", "b", serde_json::json!({"x": 2}), vec![], None, None).unwrap();
1152 assert_eq!(db.get_hash_by_seq(a.seq), Some(a.hash.clone()));
1153 assert_eq!(db.get_hash_by_seq(b.seq), Some(b.hash.clone()));
1154 assert_eq!(db.get_hash_by_seq(9999), None);
1155 }
1156
1157 #[test]
1158 fn tip_and_since() {
1159 let db = Db::in_memory();
1160 assert!(db.tip().is_none());
1162 assert!(db.since(0, 0).nodes.is_empty());
1163
1164 let a = db.put("item", "a", serde_json::json!({"x": 1}), vec![], None, None).unwrap();
1165 let b = db.put("item", "b", serde_json::json!({"x": 2}), vec![], None, None).unwrap();
1166
1167 let t = db.tip().expect("tip after writes");
1169 assert_eq!(t.seq, b.seq);
1170 assert_eq!(t.id, "b");
1171 assert_eq!(t.hash, b.hash);
1172
1173 let after_a = db.since(a.seq, 0);
1175 assert_eq!(after_a.nodes.len(), 1);
1176 assert_eq!(after_a.nodes[0].id, "b");
1177 assert_eq!(after_a.from_seq, a.seq);
1178 assert_eq!(after_a.to_seq, b.seq);
1179 assert_eq!(after_a.head_seq, b.seq);
1180 assert!(!after_a.has_more);
1181
1182 assert!(db.since(b.seq, 0).nodes.is_empty());
1184
1185 let c = db.put("item", "c", serde_json::json!({"x": 3}), vec![], None, None).unwrap();
1187 let page = db.since(a.seq, 1); assert_eq!(page.nodes.len(), 1);
1189 assert_eq!(page.nodes[0].id, "b");
1190 assert_eq!(page.to_seq, b.seq);
1191 assert!(page.has_more);
1192 let page2 = db.since(page.to_seq, 1); assert_eq!(page2.nodes.len(), 1);
1194 assert_eq!(page2.nodes[0].id, "c");
1195 assert_eq!(page2.to_seq, c.seq);
1196 assert!(!page2.has_more);
1197 }
1198
1199 #[test]
1200 fn tip_collection_per_chain() {
1201 let db = Db::in_memory();
1204 assert!(db.tip_collection("blocks").is_none());
1205
1206 db.put("blocks", "b0", serde_json::json!({"h": 0}), vec![], None, None).unwrap();
1207 db.put("tx", "t0", serde_json::json!({"v": 1}), vec![], None, None).unwrap();
1208 let b1 = db.put("blocks", "b1", serde_json::json!({"h": 1}), vec![], None, None).unwrap();
1209 let t1 = db.put("tx", "t1", serde_json::json!({"v": 2}), vec![], None, None).unwrap();
1210
1211 assert_eq!(db.tip().unwrap().id, "t1");
1213 let bt = db.tip_collection("blocks").expect("blocks tip");
1215 assert_eq!(bt.id, "b1");
1216 assert_eq!(bt.seq, b1.seq);
1217 assert_eq!(db.tip_collection("tx").unwrap().seq, t1.seq);
1218 assert!(db.tip_collection("absent").is_none());
1219 }
1220
1221 #[test]
1222 fn seq_index_survives_batch() {
1223 let db = Db::in_memory();
1224 let nodes = db.put_batch(vec![
1225 ("item".into(), "x".into(), serde_json::json!({"v": 1}), vec![], None, None),
1226 ("item".into(), "y".into(), serde_json::json!({"v": 2}), vec![], None, None),
1227 ]).unwrap();
1228 for node in &nodes {
1229 assert_eq!(db.get_hash_by_seq(node.seq), Some(node.hash.clone()));
1230 }
1231 }
1232
1233 #[test]
1239 fn put_batch_removes_superseded_sorted_index_entries() {
1240 let db = Db::in_memory();
1241 db.create_sorted_index("blocks", "height");
1242 db.put("blocks", "x", serde_json::json!({"height": 1}), vec![], None, None).unwrap();
1243 db.put_batch(vec![
1244 ("blocks".into(), "x".into(), serde_json::json!({"height": 99}), vec![], None, None),
1245 ]).unwrap();
1246
1247 let asc = db.order_by_asc("blocks", "height", 10);
1248 assert_eq!(asc.len(), 1, "stale index entry for the superseded version must be gone");
1249 assert_eq!(asc[0].data["height"], 99);
1250 assert_eq!(asc[0].id, "x");
1251 }
1252
1253 #[test]
1256 fn update_without_indexes_preserves_chain() {
1257 let db = Db::in_memory();
1258 let v1 = db.put("docs", "x", serde_json::json!({"v": 1}), vec![], None, None).unwrap();
1259 let v2 = db.put("docs", "x", serde_json::json!({"v": 2}), vec![], None, None).unwrap();
1260 assert_eq!(v2.prev.as_deref(), Some(v1.hash.as_str()), "prev chain must survive the fast path");
1261 assert_eq!(db.get("docs", "x").unwrap().data["v"], 2);
1262 assert_eq!(db.get_as_of("docs", "x", v1.seq).unwrap().data["v"], 1);
1263 }
1264
1265 #[test]
1266 fn link_and_neighbors() {
1267 let db = Db::in_memory();
1268 db.put("driver", "d1", serde_json::json!({"name": "Bob"}), vec![], None, None).unwrap();
1269 db.put("driver", "d2", serde_json::json!({"name": "Carol"}), vec![], None, None).unwrap();
1270 db.put("trip", "t1", serde_json::json!({"status": "req"}), vec![], None, None).unwrap();
1271 db.put("trip", "t2", serde_json::json!({"status": "req"}), vec![], None, None).unwrap();
1272
1273 db.link("driver:d1", "handles", "trip:t1").unwrap();
1274 db.link("driver:d1", "handles", "trip:t2").unwrap();
1275 db.link("driver:d2", "handles", "trip:t1").unwrap();
1276
1277 let d1_trips = db.neighbors("driver:d1", "handles");
1278 assert_eq!(d1_trips.len(), 2);
1279 let ids: std::collections::HashSet<&str> = d1_trips.iter().map(|n| n.id.as_str()).collect();
1280 assert!(ids.contains("t1") && ids.contains("t2"));
1281
1282 let d2_trips = db.neighbors("driver:d2", "handles");
1283 assert_eq!(d2_trips.len(), 1);
1284 assert_eq!(d2_trips[0].id, "t1");
1285 }
1286
1287 #[test]
1288 fn link_stored_in_links_collection() {
1289 let db = Db::in_memory();
1292 db.put("driver", "d1", serde_json::json!({"name": "Bob"}), vec![], None, None).unwrap();
1293 db.put("trip", "t1", serde_json::json!({"status": "req"}), vec![], None, None).unwrap();
1294 db.link("driver:d1", "handles", "trip:t1").unwrap();
1295 let link_doc = db.get("__links__", "driver:d1|handles|trip:t1");
1297 assert!(link_doc.is_some(), "__links__ doc should exist");
1298 let doc = link_doc.unwrap();
1299 assert_eq!(doc.data["_from"], "driver:d1");
1300 assert_eq!(doc.data["_rel"], "handles");
1301 assert_eq!(doc.data["_to"], "trip:t1");
1302 let nb = db.neighbors("driver:d1", "handles");
1304 assert_eq!(nb.len(), 1);
1305 assert_eq!(nb[0].id, "t1");
1306 }
1307
1308 #[test]
1309 fn link_missing_node_errors() {
1310 let db = Db::in_memory();
1311 db.put("driver", "d1", serde_json::json!({}), vec![], None, None).unwrap();
1312 assert!(db.link("driver:d1", "handles", "trip:ghost").is_err());
1313 }
1314
1315 #[test]
1316 fn link_durable_survives_reopen() {
1317 let dir = tempdir().unwrap();
1318 {
1319 let db = Db::open(dir.path(), None).unwrap();
1320 db.put("driver", "d1", serde_json::json!({"name": "Bob"}), vec![], None, None).unwrap();
1321 db.put("trip", "t1", serde_json::json!({"status": "req"}), vec![], None, None).unwrap();
1322 db.link("driver:d1", "handles", "trip:t1").unwrap();
1323 }
1324 let db2 = Db::open(dir.path(), None).unwrap();
1325 db2.startup_ready.store(true, std::sync::atomic::Ordering::SeqCst);
1326 let trips = db2.neighbors("driver:d1", "handles");
1327 assert_eq!(trips.len(), 1);
1328 assert_eq!(trips[0].id, "t1");
1329 }
1330
1331 #[test]
1332 fn tip_survives_warm_restart() {
1333 let dir = tempdir().unwrap();
1337 {
1338 let db = Db::open(dir.path(), None).unwrap();
1339 db.put("blocks", "b1", serde_json::json!({"h": 1}), vec![], None, None).unwrap();
1340 db.put("blocks", "b2", serde_json::json!({"h": 2}), vec![], None, None).unwrap();
1341 db.flush_all(); assert_eq!(db.tip().expect("tip in-session").id, "b2");
1343 }
1344 let db2 = Db::open(dir.path(), None).unwrap();
1346 assert!(db2.get_hash_by_seq(1).is_none(), "seq_index is cold on a warm boot");
1347 let tip = db2.tip().expect("tip() must survive a warm restart");
1348 assert_eq!(tip.id, "b2");
1349 assert_eq!(tip.data.get("h").and_then(|v| v.as_i64()), Some(2));
1350 }
1351
1352 #[test]
1353 fn tip_collection_survives_warm_restart() {
1354 let dir = tempdir().unwrap();
1358 {
1359 let db = Db::open(dir.path(), None).unwrap();
1360 db.put("blocks", "b1", serde_json::json!({"h": 1}), vec![], None, None).unwrap();
1361 db.put("tx", "t1", serde_json::json!({"v": 1}), vec![], None, None).unwrap();
1362 let b2 = db.put("blocks", "b2", serde_json::json!({"h": 2}), vec![], None, None).unwrap();
1363 db.flush_all(); assert_eq!(db.tip_collection("blocks").unwrap().id, "b2");
1365 assert_eq!(db.tip_collection("blocks").unwrap().seq, b2.seq);
1366 }
1367 let db2 = Db::open(dir.path(), None).unwrap();
1369 assert!(db2.get_hash_by_seq(0).is_none(), "seq_index is cold on a warm boot");
1370 let blocks_tip = db2.tip_collection("blocks").expect("tip_collection must survive a warm restart");
1371 assert_eq!(blocks_tip.id, "b2");
1372 assert_eq!(blocks_tip.data.get("h").and_then(|v| v.as_i64()), Some(2));
1373 let tx_tip = db2.tip_collection("tx").expect("tx tip must also survive");
1374 assert_eq!(tx_tip.id, "t1");
1375 assert!(db2.tip_collection("absent").is_none());
1376 }
1377
1378 #[test]
1379 fn cold_scan_indexes_every_object_and_reports_completion() {
1380 let dir = tempdir().unwrap();
1387 let n = 25u64;
1388 {
1389 let db = Db::open(dir.path(), None).unwrap();
1390 for i in 0..n {
1391 db.put("things", &i.to_string(), serde_json::json!({"i": i}), vec![], None, None).unwrap();
1392 }
1393 db.flush_all();
1394 }
1395 std::fs::remove_file(dir.path().join("MANIFEST")).unwrap();
1400
1401 let db = Db::open(dir.path(), None).unwrap();
1402 assert!(!db.scan_status().scan_complete, "should be cold immediately after open");
1403 let db = std::sync::Arc::new(db);
1404 Db::start_cold_scan(std::sync::Arc::clone(&db));
1405
1406 let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10);
1407 while !db.scan_status().scan_complete {
1408 assert!(std::time::Instant::now() < deadline, "cold scan did not complete in time");
1409 std::thread::sleep(std::time::Duration::from_millis(5));
1410 }
1411
1412 let status = db.scan_status();
1413 assert_eq!(status.indexed_count, n as usize, "every written object must be indexed");
1414 assert!(status.scan_complete);
1415
1416 let tip = db.tip().expect("tip resolves after cold scan");
1417 assert_eq!(tip.data.get("i").and_then(|v| v.as_u64()), Some(n - 1));
1418 let coll_tip = db.tip_collection("things").expect("tip_collection resolves after cold scan");
1419 assert_eq!(coll_tip.id, tip.id);
1420 }
1421
1422 #[test]
1429 fn concurrent_puts_tip_resolves_to_highest_seq_after_warm_restart() {
1430 let dir = tempdir().unwrap();
1431 let total: u64 = 100;
1432 {
1433 let db = std::sync::Arc::new(Db::open(dir.path(), None).unwrap());
1434 let mut handles = vec![];
1435 for t in 0..4u64 {
1436 let db2 = std::sync::Arc::clone(&db);
1437 handles.push(std::thread::spawn(move || {
1438 for i in 0..25u64 {
1439 db2.put("c", &format!("{}-{}", t, i),
1440 serde_json::json!({"t": t, "i": i}),
1441 vec![], None, None).unwrap();
1442 }
1443 }));
1444 }
1445 for h in handles { h.join().unwrap(); }
1446 let expected = db.seq.load(std::sync::atomic::Ordering::SeqCst) - 1;
1448 assert_eq!(expected, total - 1, "exactly {} writes expected", total);
1449 assert_eq!(db.tip().expect("in-session tip").seq, expected);
1450 db.flush_all(); }
1452 let db2 = Db::open(dir.path(), None).unwrap();
1454 let tip = db2.tip().expect("tip must survive warm restart after concurrent writes");
1455 assert_eq!(tip.seq, total - 1, "warm-boot tip must be the highest-seq write");
1456 let ct = db2.tip_collection("c").expect("coll tip survives");
1458 assert_eq!(ct.seq, total - 1);
1459 }
1460
1461 #[test]
1468 fn pre_durable_tip_manifest_warm_boots_and_heals_lazily() {
1469 let dir = tempdir().unwrap();
1470 {
1471 let db = Db::open(dir.path(), None).unwrap();
1472 for i in 0..5u64 {
1473 db.put("things", &i.to_string(), serde_json::json!({"i": i}), vec![], None, None).unwrap();
1474 }
1475 db.flush_all();
1476 }
1477 let manifest_path = dir.path().join("MANIFEST");
1479 let m: serde_json::Value =
1480 serde_json::from_str(&std::fs::read_to_string(&manifest_path).unwrap()).unwrap();
1481 let old_format = serde_json::json!({ "seq": m["seq"], "head": m["head"] });
1482 std::fs::write(&manifest_path, serde_json::to_string(&old_format).unwrap()).unwrap();
1483
1484 let db2 = Db::open(dir.path(), None).unwrap();
1486 assert!(db2.startup_ready.load(std::sync::atomic::Ordering::SeqCst),
1487 "pre-2.5.43 MANIFEST must warm-boot, not fall to a cold scan");
1488 assert!(db2.tip().is_none(), "tip() is None until the manifest heals");
1490 let n = db2.put("things", "next", serde_json::json!({"fresh": true}), vec![], None, None).unwrap();
1492 assert_eq!(n.seq, m["seq"].as_u64().unwrap(), "next write takes the persisted next-to-assign seq");
1493 db2.flush_all(); drop(db2);
1495
1496 let db3 = Db::open(dir.path(), None).unwrap();
1498 assert!(db3.startup_ready.load(std::sync::atomic::Ordering::SeqCst));
1499 let tip = db3.tip().expect("tip() must resolve after the organic upgrade");
1500 assert_eq!(tip.id, "next");
1501 }
1502
1503 #[test]
1511 fn manifest_after_cold_scan_does_not_reuse_tip_seq() {
1512 let dir = tempdir().unwrap();
1513 let old_tip_seq;
1514 {
1515 let db = Db::open(dir.path(), None).unwrap();
1516 for i in 0..5u64 {
1517 db.put("things", &i.to_string(), serde_json::json!({"i": i}), vec![], None, None).unwrap();
1518 }
1519 db.flush_all();
1520 old_tip_seq = db.tip().unwrap().seq;
1521 }
1522 std::fs::remove_file(dir.path().join("MANIFEST")).unwrap();
1525 {
1526 let db = std::sync::Arc::new(Db::open(dir.path(), None).unwrap());
1527 Db::start_cold_scan(std::sync::Arc::clone(&db));
1528 let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10);
1529 while !db.scan_status().scan_complete {
1530 assert!(std::time::Instant::now() < deadline, "cold scan did not complete");
1531 std::thread::sleep(std::time::Duration::from_millis(5));
1532 }
1533 }
1535 let db3 = Db::open(dir.path(), None).unwrap();
1538 let tip_before = db3.tip().expect("tip survives scan-written MANIFEST");
1539 assert_eq!(tip_before.seq, old_tip_seq, "tip identity preserved across the scan");
1540 let new_node = db3.put("things", "next", serde_json::json!({"fresh": true}),
1541 vec![], None, None).unwrap();
1542 assert!(new_node.seq > old_tip_seq,
1543 "new write reused seq {} (tip was {}) — duplicate seq in the log",
1544 new_node.seq, old_tip_seq);
1545 }
1546}