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 start_manifest_ticker(self_arc: Arc<Self>, interval_ms: u64) {
600 let db = self_arc;
601 std::thread::spawn(move || {
602 loop {
603 std::thread::sleep(std::time::Duration::from_millis(interval_ms));
604 db.id_index.flush_write_buf();
606 if db.manifest_dirty.load(Ordering::Acquire) {
616 if let Err(e) = db.objects.sync() {
617 eprintln!("nedb: segment sync failed: {}", e);
618 }
619 db.flush_manifest_if_dirty();
620 }
621 }
622 });
623 }
624
625 pub fn head(&self) -> String {
627 self.head.read().clone()
628 }
629
630 pub fn delete(&self, coll: &str, id: &str) -> Result<bool> {
633 let prev = match self.id_index.get(coll, id) {
634 None => return Ok(false), Some(h) => h,
636 };
637 let seq = self.seq.fetch_add(1, Ordering::SeqCst);
638 let mut tombstone = Node {
639 id: format!("_del_{}", id),
640 coll: coll.to_string(),
641 seq,
642 data: serde_json::json!({"_deleted": id, "_prev": prev}),
643 prev: Some(prev),
644 caused_by: vec![],
645 ts: now(),
646 valid_from: None,
647 valid_to: None,
648 hash: String::new(),
649 };
650 let hash = self.objects.write(&mut tombstone)?;
651 self.update_head(coll, seq, &hash);
652 self.id_index.remove(coll, id)?;
654 Ok(true)
655 }
656
657 pub fn get(&self, coll: &str, id: &str) -> Option<Node> {
659 let hash = self.id_index.get(coll, id)?;
660 self.objects.read(&hash).ok()
661 }
662
663 pub fn get_by_hash(&self, hash: &str) -> Option<Node> {
665 self.objects.read(hash).ok()
666 }
667
668 pub fn get_as_of(&self, coll: &str, id: &str, target_seq: u64) -> Option<Node> {
671 let hash = self.id_index.get(coll, id)?;
672 let mut current = self.objects.read(&hash).ok()?;
673 loop {
674 if current.seq <= target_seq {
675 return Some(current);
676 }
677 let prev_hash = current.prev.as_deref()?;
678 current = self.objects.read(prev_hash).ok()?;
679 }
680 }
681
682 pub fn list(&self, coll: &str) -> Vec<Node> {
684 self.id_index
685 .list_ids(coll)
686 .into_iter()
687 .filter_map(|id| self.get(coll, &id))
688 .collect()
689 }
690
691 pub fn order_by_asc(&self, coll: &str, field: &str, limit: usize) -> Vec<Node> {
693 if self.sorted_indexes.has(coll, field) {
694 self.sorted_indexes
695 .top_k_asc(coll, field, limit)
696 .into_iter()
697 .filter_map(|h| self.objects.read(&h).ok())
698 .collect()
699 } else {
700 let mut docs = self.list(coll);
701 docs.sort_by(|a, b| {
702 let av = a.data.get(field).map(OrderedValue::from).unwrap_or(OrderedValue::Null);
703 let bv = b.data.get(field).map(OrderedValue::from).unwrap_or(OrderedValue::Null);
704 av.cmp(&bv)
705 });
706 docs.truncate(limit);
707 docs
708 }
709 }
710
711 pub fn order_by_desc(&self, coll: &str, field: &str, limit: usize) -> Vec<Node> {
713 if self.sorted_indexes.has(coll, field) {
714 self.sorted_indexes
715 .top_k_desc(coll, field, limit)
716 .into_iter()
717 .filter_map(|h| self.objects.read(&h).ok())
718 .collect()
719 } else {
720 let mut docs = self.list(coll);
721 docs.sort_by(|a, b| {
722 let av = a.data.get(field).map(OrderedValue::from).unwrap_or(OrderedValue::Null);
723 let bv = b.data.get(field).map(OrderedValue::from).unwrap_or(OrderedValue::Null);
724 bv.cmp(&av)
725 });
726 docs.truncate(limit);
727 docs
728 }
729 }
730
731 pub fn trace(&self, hash: &str, reverse: bool, limit: usize) -> Vec<Node> {
733 self.graph
734 .trace(hash, "caused_by", reverse, limit)
735 .into_iter()
736 .filter_map(|h| self.objects.read(&h).ok())
737 .collect()
738 }
739
740 pub fn verify(&self) -> (usize, Vec<String>) {
742 self.objects.verify_all()
743 }
744
745 pub fn create_sorted_index(&self, coll: &str, field: &str) {
747 self.sorted_indexes.ensure(coll, field);
748 for id in self.id_index.list_ids(coll) {
750 if let Some(node) = self.get(coll, &id) {
751 if let Value::Object(ref obj) = node.data {
752 if let Some(value) = obj.get(field) {
753 self.sorted_indexes.insert(coll, field, value, &node.hash);
754 }
755 }
756 }
757 }
758 }
759
760 pub fn get_hash_by_seq(&self, seq: u64) -> Option<String> {
763 self.seq_index.get(&seq).map(|r| r.clone())
764 }
765
766 pub fn tip(&self) -> Option<Node> {
774 let next = self.seq.load(Ordering::SeqCst);
775 if next == 0 {
776 return None; }
778 if let Some(hash) = self.get_hash_by_seq(next - 1) {
781 return self.get_by_hash(&hash);
782 }
783 let th = self.tip_hash.read().1.clone();
787 if !th.is_empty() {
788 return self.get_by_hash(&th);
789 }
790 None
791 }
792
793 pub fn tip_collection(&self, coll: &str) -> Option<Node> {
804 let hash = self.coll_tip_hash.get(coll)?.1.clone();
805 self.get_by_hash(&hash)
806 }
807
808 pub fn since(&self, after_seq: u64, limit: usize) -> SinceBatch {
819 let next = self.seq.load(Ordering::SeqCst); let head_seq = next.saturating_sub(1);
821 let cap = if limit == 0 { DEFAULT_SINCE_LIMIT } else { limit };
822 let mut nodes: Vec<Node> = Vec::new();
823 let mut to_seq = after_seq;
824 let mut hit_limit = false;
825 let mut s = after_seq.saturating_add(1);
826 while s < next {
827 if nodes.len() >= cap { hit_limit = true; break; }
828 if let Some(hash) = self.get_hash_by_seq(s) {
829 if let Some(node) = self.get_by_hash(&hash) {
830 to_seq = node.seq;
831 nodes.push(node);
832 }
833 }
834 s += 1;
835 }
836 SinceBatch { nodes, from_seq: after_seq, to_seq, head_seq, has_more: hit_limit }
837 }
838
839 pub fn scan_status(&self) -> ScanStatus {
846 let next = self.seq.load(Ordering::SeqCst);
847 let mut min = u64::MAX;
848 let mut max = 0u64;
849 let mut count = 0usize;
850 for kv in self.seq_index.iter() {
851 let s = *kv.key();
852 if s < min { min = s; }
853 if s > max { max = s; }
854 count += 1;
855 }
856 if count == 0 { min = 0; }
857 ScanStatus {
858 scan_complete: self.startup_ready.load(Ordering::SeqCst),
859 tip_seq: next.saturating_sub(1),
860 indexed_seq_min: min,
861 indexed_seq_max: max,
862 indexed_count: count,
863 }
864 }
865
866 pub fn link(&self, frm: &str, rel: &str, to: &str) -> Result<()> {
871 let (frm_coll, frm_id) = frm.split_once(':')
872 .ok_or_else(|| anyhow::anyhow!("link frm must be 'coll:id', got: {}", frm))?;
873 let (to_coll, to_id) = to.split_once(':')
874 .ok_or_else(|| anyhow::anyhow!("link to must be 'coll:id', got: {}", to))?;
875 if self.id_index.get(frm_coll, frm_id).is_none() {
876 anyhow::bail!("link: frm not found: {}", frm);
877 }
878 if self.id_index.get(to_coll, to_id).is_none() {
879 anyhow::bail!("link: to not found: {}", to);
880 }
881 let link_id = format!("{}|{}|{}", frm, rel, to);
882 let doc = serde_json::json!({"_from": frm, "_rel": rel, "_to": to});
883 self.put("__links__", &link_id, doc, vec![], None, None)?;
884 Ok(())
885 }
886
887 pub fn unlink(&self, frm: &str, rel: &str, to: &str) -> Result<bool> {
889 let link_id = format!("{}|{}|{}", frm, rel, to);
890 self.delete("__links__", &link_id)
891 }
892
893 pub fn neighbors(&self, frm: &str, rel: &str) -> Vec<Node> {
896 self.id_index
897 .list_ids("__links__")
898 .into_iter()
899 .filter_map(|id| self.get("__links__", &id))
900 .filter(|node| {
901 node.data.get("_from").and_then(|v| v.as_str()) == Some(frm)
902 && node.data.get("_rel").and_then(|v| v.as_str()) == Some(rel)
903 })
904 .filter_map(|node| {
905 let to = node.data.get("_to")?.as_str()?;
906 let (to_coll, to_id) = to.split_once(':')?;
907 self.get(to_coll, to_id)
908 })
909 .collect()
910 }
911}
912
913impl Drop for Db {
914 fn drop(&mut self) {
930 self.flush_all();
931 }
932}
933
934fn cold_scan_background_arc(db: Arc<Db>) {
936 use rayon::prelude::*;
937 use blake2::{Blake2b512, Digest};
938
939 let objects = &db.objects;
940 let head = &db.head;
941 let seq_atomic = &db.seq;
942 let sorted_indexes = &db.sorted_indexes;
943 let seq_index = &db.seq_index;
944 let ready_flag = Arc::clone(&db.startup_ready);
945
946 let hashes: Vec<String> = objects.all_hashes().collect();
947 let total = hashes.len();
948
949 if total == 0 {
950 ready_flag.store(true, Ordering::SeqCst);
951 return;
952 }
953
954 println!(" [nedbd] background scan — {} objects...", total);
955 let t0 = std::time::Instant::now();
956 let step = (total / 10).max(1000);
957
958 let nodes: Vec<Node> = hashes.par_iter()
968 .enumerate()
969 .filter_map(|(i, h)| {
970 if i > 0 && i % step == 0 {
971 let pct = i * 100 / total;
972 let elapsed = t0.elapsed().as_secs_f32();
973 let rate = i as f32 / elapsed;
974 let eta = (total - i) as f32 / rate;
975 eprint!("\r [nedbd] {:>3}% {:>8} / {:>8} ({:>8.0}/s eta {:.0}s) ",
976 pct, i, total, rate, eta);
977 }
978 let node = objects.read(h).ok()?;
979 seq_index.insert(node.seq, node.hash.clone());
980 Some(node)
981 })
982 .collect();
983
984 eprintln!("\r [nedbd] 100% {:>8} / {:>8} ({:.1}s) ",
985 total, total, t0.elapsed().as_secs_f32());
986
987 let max_seq = nodes.iter().map(|n| n.seq).max().unwrap_or(0);
988 seq_atomic.store(max_seq + 1, Ordering::SeqCst);
989
990 let mut coll_max: std::collections::HashMap<String, (u64, String)> = std::collections::HashMap::new();
995
996 for node in &nodes {
997 coll_max.entry(node.coll.clone())
999 .and_modify(|(s, h)| if node.seq > *s { *s = node.seq; *h = node.hash.clone(); })
1000 .or_insert_with(|| (node.seq, node.hash.clone()));
1001 if let Value::Object(ref obj) = node.data {
1002 for (field, value) in obj {
1003 if sorted_indexes.has(&node.coll, field) {
1004 sorted_indexes.insert(&node.coll, field, value, &node.hash);
1005 }
1006 }
1007 }
1008 }
1009
1010 for (coll, (seq, hash)) in coll_max {
1011 db.coll_tip_hash.insert(coll, (seq, hash));
1012 }
1013
1014 let mut sorted_hashes = hashes;
1016 sorted_hashes.sort();
1017 let mut h = Blake2b512::new();
1018 h.update(max_seq.to_le_bytes());
1019 for hash_str in &sorted_hashes {
1020 h.update(hash_str.as_bytes());
1021 }
1022 let new_head = hex::encode(&h.finalize()[..32]);
1023 *head.write() = new_head;
1024
1025 let tip_hash = db.seq_index.iter()
1028 .max_by_key(|kv| *kv.key())
1029 .map(|kv| kv.value().clone())
1030 .unwrap_or_default();
1031 *db.tip_hash.write() = (max_seq, tip_hash);
1032
1033 db.flush_manifest();
1040
1041 ready_flag.store(true, Ordering::SeqCst);
1043 println!(" [nedbd] background scan complete — seq={} objects={} MANIFEST written", max_seq, total);
1044}
1045
1046fn now() -> f64 {
1047 std::time::SystemTime::now()
1048 .duration_since(std::time::UNIX_EPOCH)
1049 .map(|d| d.as_secs_f64())
1050 .unwrap_or(0.0)
1051}
1052
1053#[cfg(test)]
1054mod tests {
1055 use super::*;
1056 use tempfile::tempdir;
1057
1058 #[test]
1059 fn put_and_get() {
1060 let dir = tempdir().unwrap();
1061 let db = Db::open(dir.path(), None).unwrap();
1062 db.put(
1063 "blocks", "618000",
1064 serde_json::json!({"height": 618000, "hash": "0000abc"}),
1065 vec![], None, None,
1066 ).unwrap();
1067 let node = db.get("blocks", "618000").unwrap();
1068 assert_eq!(node.id, "618000");
1069 assert_eq!(node.data["height"], 618000);
1070 }
1071
1072 #[test]
1073 fn order_by_with_sorted_index() {
1074 let dir = tempdir().unwrap();
1075 let db = Db::open(dir.path(), None).unwrap();
1076 db.create_sorted_index("blocks", "height");
1077 for h in [3u64, 1, 5, 2, 4] {
1078 db.put("blocks", &h.to_string(),
1079 serde_json::json!({"height": h}),
1080 vec![], None, None).unwrap();
1081 }
1082 let asc = db.order_by_asc("blocks", "height", 3);
1083 let heights: Vec<u64> = asc.iter()
1084 .filter_map(|n| n.data["height"].as_u64())
1085 .collect();
1086 assert_eq!(heights, vec![1, 2, 3]);
1087 }
1088
1089 #[test]
1090 fn causal_trace() {
1091 let dir = tempdir().unwrap();
1092 let db = Db::open(dir.path(), None).unwrap();
1093 let a = db.put("ops", "a", serde_json::json!({"op": "create"}), vec![], None, None).unwrap();
1094 let b = db.put("ops", "b", serde_json::json!({"op": "transfer"}), vec![a.hash.clone()], None, None).unwrap();
1095 let c = db.put("ops", "c", serde_json::json!({"op": "burn"}), vec![b.hash.clone()], None, None).unwrap();
1096
1097 let trace = db.trace(&c.hash, false, 10);
1098 assert_eq!(trace.len(), 3); }
1100
1101 #[test]
1102 fn as_of() {
1103 let dir = tempdir().unwrap();
1104 let db = Db::open(dir.path(), None).unwrap();
1105 let v1 = db.put("docs", "x", serde_json::json!({"v": 1}), vec![], None, None).unwrap();
1106 let _v2 = db.put("docs", "x", serde_json::json!({"v": 2}), vec![], None, None).unwrap();
1107
1108 let at_v1 = db.get_as_of("docs", "x", v1.seq).unwrap();
1109 assert_eq!(at_v1.data["v"], 1);
1110 let current = db.get("docs", "x").unwrap();
1111 assert_eq!(current.data["v"], 2);
1112 }
1113}
1114
1115#[cfg(test)]
1116mod tests_v2 {
1117 use super::*;
1118 use tempfile::tempdir;
1119
1120 #[test]
1121 fn seq_index_populated_on_put() {
1122 let db = Db::in_memory();
1123 let a = db.put("item", "a", serde_json::json!({"x": 1}), vec![], None, None).unwrap();
1124 let b = db.put("item", "b", serde_json::json!({"x": 2}), vec![], None, None).unwrap();
1125 assert_eq!(db.get_hash_by_seq(a.seq), Some(a.hash.clone()));
1126 assert_eq!(db.get_hash_by_seq(b.seq), Some(b.hash.clone()));
1127 assert_eq!(db.get_hash_by_seq(9999), None);
1128 }
1129
1130 #[test]
1131 fn tip_and_since() {
1132 let db = Db::in_memory();
1133 assert!(db.tip().is_none());
1135 assert!(db.since(0, 0).nodes.is_empty());
1136
1137 let a = db.put("item", "a", serde_json::json!({"x": 1}), vec![], None, None).unwrap();
1138 let b = db.put("item", "b", serde_json::json!({"x": 2}), vec![], None, None).unwrap();
1139
1140 let t = db.tip().expect("tip after writes");
1142 assert_eq!(t.seq, b.seq);
1143 assert_eq!(t.id, "b");
1144 assert_eq!(t.hash, b.hash);
1145
1146 let after_a = db.since(a.seq, 0);
1148 assert_eq!(after_a.nodes.len(), 1);
1149 assert_eq!(after_a.nodes[0].id, "b");
1150 assert_eq!(after_a.from_seq, a.seq);
1151 assert_eq!(after_a.to_seq, b.seq);
1152 assert_eq!(after_a.head_seq, b.seq);
1153 assert!(!after_a.has_more);
1154
1155 assert!(db.since(b.seq, 0).nodes.is_empty());
1157
1158 let c = db.put("item", "c", serde_json::json!({"x": 3}), vec![], None, None).unwrap();
1160 let page = db.since(a.seq, 1); assert_eq!(page.nodes.len(), 1);
1162 assert_eq!(page.nodes[0].id, "b");
1163 assert_eq!(page.to_seq, b.seq);
1164 assert!(page.has_more);
1165 let page2 = db.since(page.to_seq, 1); assert_eq!(page2.nodes.len(), 1);
1167 assert_eq!(page2.nodes[0].id, "c");
1168 assert_eq!(page2.to_seq, c.seq);
1169 assert!(!page2.has_more);
1170 }
1171
1172 #[test]
1173 fn tip_collection_per_chain() {
1174 let db = Db::in_memory();
1177 assert!(db.tip_collection("blocks").is_none());
1178
1179 db.put("blocks", "b0", serde_json::json!({"h": 0}), vec![], None, None).unwrap();
1180 db.put("tx", "t0", serde_json::json!({"v": 1}), vec![], None, None).unwrap();
1181 let b1 = db.put("blocks", "b1", serde_json::json!({"h": 1}), vec![], None, None).unwrap();
1182 let t1 = db.put("tx", "t1", serde_json::json!({"v": 2}), vec![], None, None).unwrap();
1183
1184 assert_eq!(db.tip().unwrap().id, "t1");
1186 let bt = db.tip_collection("blocks").expect("blocks tip");
1188 assert_eq!(bt.id, "b1");
1189 assert_eq!(bt.seq, b1.seq);
1190 assert_eq!(db.tip_collection("tx").unwrap().seq, t1.seq);
1191 assert!(db.tip_collection("absent").is_none());
1192 }
1193
1194 #[test]
1195 fn seq_index_survives_batch() {
1196 let db = Db::in_memory();
1197 let nodes = db.put_batch(vec![
1198 ("item".into(), "x".into(), serde_json::json!({"v": 1}), vec![], None, None),
1199 ("item".into(), "y".into(), serde_json::json!({"v": 2}), vec![], None, None),
1200 ]).unwrap();
1201 for node in &nodes {
1202 assert_eq!(db.get_hash_by_seq(node.seq), Some(node.hash.clone()));
1203 }
1204 }
1205
1206 #[test]
1212 fn put_batch_removes_superseded_sorted_index_entries() {
1213 let db = Db::in_memory();
1214 db.create_sorted_index("blocks", "height");
1215 db.put("blocks", "x", serde_json::json!({"height": 1}), vec![], None, None).unwrap();
1216 db.put_batch(vec![
1217 ("blocks".into(), "x".into(), serde_json::json!({"height": 99}), vec![], None, None),
1218 ]).unwrap();
1219
1220 let asc = db.order_by_asc("blocks", "height", 10);
1221 assert_eq!(asc.len(), 1, "stale index entry for the superseded version must be gone");
1222 assert_eq!(asc[0].data["height"], 99);
1223 assert_eq!(asc[0].id, "x");
1224 }
1225
1226 #[test]
1229 fn update_without_indexes_preserves_chain() {
1230 let db = Db::in_memory();
1231 let v1 = db.put("docs", "x", serde_json::json!({"v": 1}), vec![], None, None).unwrap();
1232 let v2 = db.put("docs", "x", serde_json::json!({"v": 2}), vec![], None, None).unwrap();
1233 assert_eq!(v2.prev.as_deref(), Some(v1.hash.as_str()), "prev chain must survive the fast path");
1234 assert_eq!(db.get("docs", "x").unwrap().data["v"], 2);
1235 assert_eq!(db.get_as_of("docs", "x", v1.seq).unwrap().data["v"], 1);
1236 }
1237
1238 #[test]
1239 fn link_and_neighbors() {
1240 let db = Db::in_memory();
1241 db.put("driver", "d1", serde_json::json!({"name": "Bob"}), vec![], None, None).unwrap();
1242 db.put("driver", "d2", serde_json::json!({"name": "Carol"}), vec![], None, None).unwrap();
1243 db.put("trip", "t1", serde_json::json!({"status": "req"}), vec![], None, None).unwrap();
1244 db.put("trip", "t2", serde_json::json!({"status": "req"}), vec![], None, None).unwrap();
1245
1246 db.link("driver:d1", "handles", "trip:t1").unwrap();
1247 db.link("driver:d1", "handles", "trip:t2").unwrap();
1248 db.link("driver:d2", "handles", "trip:t1").unwrap();
1249
1250 let d1_trips = db.neighbors("driver:d1", "handles");
1251 assert_eq!(d1_trips.len(), 2);
1252 let ids: std::collections::HashSet<&str> = d1_trips.iter().map(|n| n.id.as_str()).collect();
1253 assert!(ids.contains("t1") && ids.contains("t2"));
1254
1255 let d2_trips = db.neighbors("driver:d2", "handles");
1256 assert_eq!(d2_trips.len(), 1);
1257 assert_eq!(d2_trips[0].id, "t1");
1258 }
1259
1260 #[test]
1261 fn link_stored_in_links_collection() {
1262 let db = Db::in_memory();
1265 db.put("driver", "d1", serde_json::json!({"name": "Bob"}), vec![], None, None).unwrap();
1266 db.put("trip", "t1", serde_json::json!({"status": "req"}), vec![], None, None).unwrap();
1267 db.link("driver:d1", "handles", "trip:t1").unwrap();
1268 let link_doc = db.get("__links__", "driver:d1|handles|trip:t1");
1270 assert!(link_doc.is_some(), "__links__ doc should exist");
1271 let doc = link_doc.unwrap();
1272 assert_eq!(doc.data["_from"], "driver:d1");
1273 assert_eq!(doc.data["_rel"], "handles");
1274 assert_eq!(doc.data["_to"], "trip:t1");
1275 let nb = db.neighbors("driver:d1", "handles");
1277 assert_eq!(nb.len(), 1);
1278 assert_eq!(nb[0].id, "t1");
1279 }
1280
1281 #[test]
1282 fn link_missing_node_errors() {
1283 let db = Db::in_memory();
1284 db.put("driver", "d1", serde_json::json!({}), vec![], None, None).unwrap();
1285 assert!(db.link("driver:d1", "handles", "trip:ghost").is_err());
1286 }
1287
1288 #[test]
1289 fn link_durable_survives_reopen() {
1290 let dir = tempdir().unwrap();
1291 {
1292 let db = Db::open(dir.path(), None).unwrap();
1293 db.put("driver", "d1", serde_json::json!({"name": "Bob"}), vec![], None, None).unwrap();
1294 db.put("trip", "t1", serde_json::json!({"status": "req"}), vec![], None, None).unwrap();
1295 db.link("driver:d1", "handles", "trip:t1").unwrap();
1296 }
1297 let db2 = Db::open(dir.path(), None).unwrap();
1298 db2.startup_ready.store(true, std::sync::atomic::Ordering::SeqCst);
1299 let trips = db2.neighbors("driver:d1", "handles");
1300 assert_eq!(trips.len(), 1);
1301 assert_eq!(trips[0].id, "t1");
1302 }
1303
1304 #[test]
1305 fn tip_survives_warm_restart() {
1306 let dir = tempdir().unwrap();
1310 {
1311 let db = Db::open(dir.path(), None).unwrap();
1312 db.put("blocks", "b1", serde_json::json!({"h": 1}), vec![], None, None).unwrap();
1313 db.put("blocks", "b2", serde_json::json!({"h": 2}), vec![], None, None).unwrap();
1314 db.flush_all(); assert_eq!(db.tip().expect("tip in-session").id, "b2");
1316 }
1317 let db2 = Db::open(dir.path(), None).unwrap();
1319 assert!(db2.get_hash_by_seq(1).is_none(), "seq_index is cold on a warm boot");
1320 let tip = db2.tip().expect("tip() must survive a warm restart");
1321 assert_eq!(tip.id, "b2");
1322 assert_eq!(tip.data.get("h").and_then(|v| v.as_i64()), Some(2));
1323 }
1324
1325 #[test]
1326 fn tip_collection_survives_warm_restart() {
1327 let dir = tempdir().unwrap();
1331 {
1332 let db = Db::open(dir.path(), None).unwrap();
1333 db.put("blocks", "b1", serde_json::json!({"h": 1}), vec![], None, None).unwrap();
1334 db.put("tx", "t1", serde_json::json!({"v": 1}), vec![], None, None).unwrap();
1335 let b2 = db.put("blocks", "b2", serde_json::json!({"h": 2}), vec![], None, None).unwrap();
1336 db.flush_all(); assert_eq!(db.tip_collection("blocks").unwrap().id, "b2");
1338 assert_eq!(db.tip_collection("blocks").unwrap().seq, b2.seq);
1339 }
1340 let db2 = Db::open(dir.path(), None).unwrap();
1342 assert!(db2.get_hash_by_seq(0).is_none(), "seq_index is cold on a warm boot");
1343 let blocks_tip = db2.tip_collection("blocks").expect("tip_collection must survive a warm restart");
1344 assert_eq!(blocks_tip.id, "b2");
1345 assert_eq!(blocks_tip.data.get("h").and_then(|v| v.as_i64()), Some(2));
1346 let tx_tip = db2.tip_collection("tx").expect("tx tip must also survive");
1347 assert_eq!(tx_tip.id, "t1");
1348 assert!(db2.tip_collection("absent").is_none());
1349 }
1350
1351 #[test]
1352 fn cold_scan_indexes_every_object_and_reports_completion() {
1353 let dir = tempdir().unwrap();
1360 let n = 25u64;
1361 {
1362 let db = Db::open(dir.path(), None).unwrap();
1363 for i in 0..n {
1364 db.put("things", &i.to_string(), serde_json::json!({"i": i}), vec![], None, None).unwrap();
1365 }
1366 db.flush_all();
1367 }
1368 std::fs::remove_file(dir.path().join("MANIFEST")).unwrap();
1373
1374 let db = Db::open(dir.path(), None).unwrap();
1375 assert!(!db.scan_status().scan_complete, "should be cold immediately after open");
1376 let db = std::sync::Arc::new(db);
1377 Db::start_cold_scan(std::sync::Arc::clone(&db));
1378
1379 let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10);
1380 while !db.scan_status().scan_complete {
1381 assert!(std::time::Instant::now() < deadline, "cold scan did not complete in time");
1382 std::thread::sleep(std::time::Duration::from_millis(5));
1383 }
1384
1385 let status = db.scan_status();
1386 assert_eq!(status.indexed_count, n as usize, "every written object must be indexed");
1387 assert!(status.scan_complete);
1388
1389 let tip = db.tip().expect("tip resolves after cold scan");
1390 assert_eq!(tip.data.get("i").and_then(|v| v.as_u64()), Some(n - 1));
1391 let coll_tip = db.tip_collection("things").expect("tip_collection resolves after cold scan");
1392 assert_eq!(coll_tip.id, tip.id);
1393 }
1394
1395 #[test]
1402 fn concurrent_puts_tip_resolves_to_highest_seq_after_warm_restart() {
1403 let dir = tempdir().unwrap();
1404 let total: u64 = 100;
1405 {
1406 let db = std::sync::Arc::new(Db::open(dir.path(), None).unwrap());
1407 let mut handles = vec![];
1408 for t in 0..4u64 {
1409 let db2 = std::sync::Arc::clone(&db);
1410 handles.push(std::thread::spawn(move || {
1411 for i in 0..25u64 {
1412 db2.put("c", &format!("{}-{}", t, i),
1413 serde_json::json!({"t": t, "i": i}),
1414 vec![], None, None).unwrap();
1415 }
1416 }));
1417 }
1418 for h in handles { h.join().unwrap(); }
1419 let expected = db.seq.load(std::sync::atomic::Ordering::SeqCst) - 1;
1421 assert_eq!(expected, total - 1, "exactly {} writes expected", total);
1422 assert_eq!(db.tip().expect("in-session tip").seq, expected);
1423 db.flush_all(); }
1425 let db2 = Db::open(dir.path(), None).unwrap();
1427 let tip = db2.tip().expect("tip must survive warm restart after concurrent writes");
1428 assert_eq!(tip.seq, total - 1, "warm-boot tip must be the highest-seq write");
1429 let ct = db2.tip_collection("c").expect("coll tip survives");
1431 assert_eq!(ct.seq, total - 1);
1432 }
1433
1434 #[test]
1441 fn pre_durable_tip_manifest_warm_boots_and_heals_lazily() {
1442 let dir = tempdir().unwrap();
1443 {
1444 let db = Db::open(dir.path(), None).unwrap();
1445 for i in 0..5u64 {
1446 db.put("things", &i.to_string(), serde_json::json!({"i": i}), vec![], None, None).unwrap();
1447 }
1448 db.flush_all();
1449 }
1450 let manifest_path = dir.path().join("MANIFEST");
1452 let m: serde_json::Value =
1453 serde_json::from_str(&std::fs::read_to_string(&manifest_path).unwrap()).unwrap();
1454 let old_format = serde_json::json!({ "seq": m["seq"], "head": m["head"] });
1455 std::fs::write(&manifest_path, serde_json::to_string(&old_format).unwrap()).unwrap();
1456
1457 let db2 = Db::open(dir.path(), None).unwrap();
1459 assert!(db2.startup_ready.load(std::sync::atomic::Ordering::SeqCst),
1460 "pre-2.5.43 MANIFEST must warm-boot, not fall to a cold scan");
1461 assert!(db2.tip().is_none(), "tip() is None until the manifest heals");
1463 let n = db2.put("things", "next", serde_json::json!({"fresh": true}), vec![], None, None).unwrap();
1465 assert_eq!(n.seq, m["seq"].as_u64().unwrap(), "next write takes the persisted next-to-assign seq");
1466 db2.flush_all(); drop(db2);
1468
1469 let db3 = Db::open(dir.path(), None).unwrap();
1471 assert!(db3.startup_ready.load(std::sync::atomic::Ordering::SeqCst));
1472 let tip = db3.tip().expect("tip() must resolve after the organic upgrade");
1473 assert_eq!(tip.id, "next");
1474 }
1475
1476 #[test]
1484 fn manifest_after_cold_scan_does_not_reuse_tip_seq() {
1485 let dir = tempdir().unwrap();
1486 let old_tip_seq;
1487 {
1488 let db = Db::open(dir.path(), None).unwrap();
1489 for i in 0..5u64 {
1490 db.put("things", &i.to_string(), serde_json::json!({"i": i}), vec![], None, None).unwrap();
1491 }
1492 db.flush_all();
1493 old_tip_seq = db.tip().unwrap().seq;
1494 }
1495 std::fs::remove_file(dir.path().join("MANIFEST")).unwrap();
1498 {
1499 let db = std::sync::Arc::new(Db::open(dir.path(), None).unwrap());
1500 Db::start_cold_scan(std::sync::Arc::clone(&db));
1501 let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10);
1502 while !db.scan_status().scan_complete {
1503 assert!(std::time::Instant::now() < deadline, "cold scan did not complete");
1504 std::thread::sleep(std::time::Duration::from_millis(5));
1505 }
1506 }
1508 let db3 = Db::open(dir.path(), None).unwrap();
1511 let tip_before = db3.tip().expect("tip survives scan-written MANIFEST");
1512 assert_eq!(tip_before.seq, old_tip_seq, "tip identity preserved across the scan");
1513 let new_node = db3.put("things", "next", serde_json::json!({"fresh": true}),
1514 vec![], None, None).unwrap();
1515 assert!(new_node.seq > old_tip_seq,
1516 "new write reused seq {} (tip was {}) — duplicate seq in the log",
1517 new_node.seq, old_tip_seq);
1518 }
1519}