1use crate::db::Db;
52use serde::{Deserialize, Serialize};
53use serde_json::Value;
54use std::collections::BTreeSet;
55use std::sync::atomic::Ordering;
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
59#[serde(rename_all = "lowercase")]
60pub enum ChangeKind {
61 Added,
63 Removed,
65 Modified,
67}
68
69#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
71pub struct FieldDelta {
72 pub added: Vec<String>,
74 pub removed: Vec<String>,
76 pub changed: Vec<String>,
78}
79
80impl FieldDelta {
81 pub fn is_empty(&self) -> bool {
87 self.added.is_empty() && self.removed.is_empty() && self.changed.is_empty()
88 }
89}
90
91#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
98pub struct TemporalDelta {
99 pub valid_from_before: Option<String>,
100 pub valid_from_after: Option<String>,
101 pub valid_to_before: Option<String>,
102 pub valid_to_after: Option<String>,
103}
104
105#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
107pub struct DocChange {
108 pub coll: String,
109 pub id: String,
110 pub kind: ChangeKind,
111 pub before: Option<Value>,
113 pub after: Option<Value>,
115 pub fields: Option<FieldDelta>,
120 pub temporal: Option<TemporalDelta>,
123}
124
125#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
131pub struct CollChange {
132 pub name: String,
133 pub kind: ChangeKind,
134}
135
136#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
138pub struct StateDiff {
139 pub from_seq: u64,
140 pub to_seq: u64,
141 pub collections: Vec<CollChange>,
143 pub documents: Vec<DocChange>,
145}
146
147impl StateDiff {
148 pub fn is_empty(&self) -> bool {
150 self.collections.is_empty() && self.documents.is_empty()
151 }
152}
153
154pub const HISTORY_PRUNED: &str = "HISTORY_PRUNED";
158
159pub const REVERSED_RANGE: &str = "REVERSED_RANGE";
161
162pub fn diff(db: &Db, from_seq: u64, to_seq: u64) -> Result<StateDiff, String> {
186 if from_seq > to_seq {
187 return Err(format!(
188 "{}: from_seq {} is after to_seq {}; a diff's argument order fixes the \
189 sign of every change in it. Ask for diff({}, {}) if you want the \
190 reverse.",
191 REVERSED_RANGE, from_seq, to_seq, to_seq, from_seq
192 ));
193 }
194
195 let floor = db.history_floor();
196 if from_seq < floor || to_seq < floor {
197 let which = if from_seq < floor { "from_seq" } else { "to_seq" };
198 let bad = if from_seq < floor { from_seq } else { to_seq };
199 return Err(format!(
200 "{}: {} {} is below the history floor {}. compact() discarded the \
201 superseded versions needed to reconstruct that state, so a diff there \
202 could not distinguish an unchanged document from an unreadable one. \
203 The oldest diffable sequence is {}.",
204 HISTORY_PRUNED, which, bad, floor, floor
205 ));
206 }
207
208 if from_seq == to_seq {
212 return Ok(StateDiff { from_seq, to_seq, collections: vec![], documents: vec![] });
213 }
214
215 let before_colls: BTreeSet<String> = live_collections(db, from_seq);
218 let after_colls: BTreeSet<String> = live_collections(db, to_seq);
219
220 let mut collections = Vec::new();
221 for name in after_colls.difference(&before_colls) {
222 collections.push(CollChange { name: name.clone(), kind: ChangeKind::Added });
223 }
224 for name in before_colls.difference(&after_colls) {
225 collections.push(CollChange { name: name.clone(), kind: ChangeKind::Removed });
226 }
227 collections.sort_by(|a, b| a.name.cmp(&b.name));
230
231 let mut documents = Vec::new();
232 for coll in before_colls.union(&after_colls) {
235 let live_before = before_colls.contains(coll);
244 let live_after = after_colls.contains(coll);
245
246 for id in db.list_ids_including_deleted(coll) {
252 let before = live_before.then(|| db.get_as_of(coll, &id, from_seq)).flatten();
253 let after = live_after.then(|| db.get_as_of(coll, &id, to_seq)).flatten();
254 if let Some(change) = classify(coll, &id, before, after) {
255 documents.push(change);
256 }
257 }
258 }
259 documents.sort_by(|a, b| a.coll.cmp(&b.coll).then_with(|| a.id.cmp(&b.id)));
260
261 Ok(StateDiff { from_seq, to_seq, collections, documents })
262}
263
264fn live_collections(db: &Db, seq: u64) -> BTreeSet<String> {
271 db.collections_as_of(seq)
272 .into_iter()
273 .filter(|c| !crate::namespace::is_reserved(c))
274 .collect()
275}
276
277fn classify(
279 coll: &str,
280 id: &str,
281 before: Option<crate::store::Node>,
282 after: Option<crate::store::Node>,
283) -> Option<DocChange> {
284 match (before, after) {
285 (None, None) => None,
286 (None, Some(a)) => Some(DocChange {
287 coll: coll.to_string(),
288 id: id.to_string(),
289 kind: ChangeKind::Added,
290 before: None,
291 after: Some(a.data),
292 fields: None,
293 temporal: None,
294 }),
295 (Some(b), None) => Some(DocChange {
296 coll: coll.to_string(),
297 id: id.to_string(),
298 kind: ChangeKind::Removed,
299 before: Some(b.data),
300 after: None,
301 fields: None,
302 temporal: None,
303 }),
304 (Some(b), Some(a)) => {
305 let temporal_moved =
306 b.valid_from != a.valid_from || b.valid_to != a.valid_to;
307 if b.data == a.data && !temporal_moved {
318 return None;
319 }
320 let fields = field_delta(&b.data, &a.data);
321 Some(DocChange {
322 coll: coll.to_string(),
323 id: id.to_string(),
324 kind: ChangeKind::Modified,
325 before: Some(b.data),
326 after: Some(a.data),
327 fields,
328 temporal: temporal_moved.then(|| TemporalDelta {
329 valid_from_before: b.valid_from,
330 valid_from_after: a.valid_from,
331 valid_to_before: b.valid_to,
332 valid_to_after: a.valid_to,
333 }),
334 })
335 }
336 }
337}
338
339fn field_delta(before: &Value, after: &Value) -> Option<FieldDelta> {
346 let (b, a) = match (before.as_object(), after.as_object()) {
347 (Some(b), Some(a)) => (b, a),
348 _ => return None,
349 };
350 let mut delta = FieldDelta::default();
351 let keys: BTreeSet<&String> = b.keys().chain(a.keys()).collect();
352 for k in keys {
353 match (b.get(k), a.get(k)) {
354 (None, Some(_)) => delta.added.push(k.clone()),
355 (Some(_), None) => delta.removed.push(k.clone()),
356 (Some(bv), Some(av)) if bv != av => delta.changed.push(k.clone()),
357 _ => {}
358 }
359 }
360 Some(delta)
361}
362
363pub fn tip(db: &Db) -> u64 {
367 db.seq.load(Ordering::SeqCst).saturating_sub(1)
368}
369
370#[cfg(test)]
371mod tests {
372 use super::*;
373 use serde_json::json;
374
375 fn db() -> Db {
376 Db::in_memory()
377 }
378
379 fn put(db: &Db, coll: &str, id: &str, data: Value) -> u64 {
380 db.put(coll, id, data, vec![], None, None)
381 .expect("put should succeed")
382 .seq
383 }
384
385 fn find<'a>(d: &'a StateDiff, coll: &str, id: &str) -> Option<&'a DocChange> {
386 d.documents.iter().find(|c| c.coll == coll && c.id == id)
387 }
388
389 #[test]
390 fn added_document_is_detected() {
391 let db = db();
392 let from = put(&db, "users", "a", json!({"n": 1}));
393 put(&db, "users", "b", json!({"n": 2}));
394 let d = diff(&db, from, tip(&db)).unwrap();
395 let c = find(&d, "users", "b").expect("b should appear");
396 assert_eq!(c.kind, ChangeKind::Added);
397 assert_eq!(c.before, None);
398 assert_eq!(c.after, Some(json!({"n": 2})));
399 assert!(c.fields.is_none(), "an add has no field delta; `after` is the whole story");
400 }
401
402 #[test]
403 fn removed_document_is_detected() {
404 let db = db();
405 put(&db, "users", "a", json!({"n": 1}));
406 let from = tip(&db);
407 db.delete("users", "a").unwrap();
408 let d = diff(&db, from, tip(&db)).unwrap();
409 let c = find(&d, "users", "a").expect("a should appear");
410 assert_eq!(c.kind, ChangeKind::Removed);
411 assert_eq!(c.before, Some(json!({"n": 1})));
412 assert_eq!(c.after, None);
413 }
414
415 #[test]
416 fn modified_document_is_detected() {
417 let db = db();
418 put(&db, "users", "a", json!({"n": 1}));
419 let from = tip(&db);
420 put(&db, "users", "a", json!({"n": 2}));
421 let d = diff(&db, from, tip(&db)).unwrap();
422 let c = find(&d, "users", "a").expect("a should appear");
423 assert_eq!(c.kind, ChangeKind::Modified);
424 assert_eq!(c.before, Some(json!({"n": 1})));
425 assert_eq!(c.after, Some(json!({"n": 2})));
426 }
427
428 #[test]
429 fn document_created_and_deleted_inside_the_range_does_not_appear() {
430 let db = db();
431 put(&db, "users", "keep", json!({"n": 0}));
432 let from = tip(&db);
433 put(&db, "users", "ghost", json!({"n": 1}));
434 db.delete("users", "ghost").unwrap();
435 let d = diff(&db, from, tip(&db)).unwrap();
436 assert!(
437 find(&d, "users", "ghost").is_none(),
438 "absent on both sides is not a state change: {:?}",
439 d.documents
440 );
441 assert!(d.documents.is_empty(), "{:?}", d.documents);
442 }
443
444 #[test]
445 fn unchanged_document_does_not_appear() {
446 let db = db();
447 put(&db, "users", "a", json!({"n": 1}));
448 let from = tip(&db);
449 put(&db, "users", "b", json!({"n": 2}));
450 let d = diff(&db, from, tip(&db)).unwrap();
451 assert!(find(&d, "users", "a").is_none());
452 assert_eq!(d.documents.len(), 1);
453 }
454
455 #[test]
456 fn rewriting_identical_content_is_not_a_change() {
457 let db = db();
459 put(&db, "users", "a", json!({"n": 1}));
460 let from = tip(&db);
461 for _ in 0..5 {
462 put(&db, "users", "a", json!({"n": 1}));
463 }
464 let d = diff(&db, from, tip(&db)).unwrap();
465 assert!(d.is_empty(), "{:?}", d);
466 }
467
468 #[test]
469 fn value_restored_to_its_original_is_not_a_change() {
470 let db = db();
471 put(&db, "users", "a", json!({"n": 1}));
472 let from = tip(&db);
473 put(&db, "users", "a", json!({"n": 99}));
474 put(&db, "users", "a", json!({"n": 1}));
475 let d = diff(&db, from, tip(&db)).unwrap();
476 assert!(d.is_empty(), "net state is identical: {:?}", d);
477 }
478
479 #[test]
480 fn created_collection_appears_as_added() {
481 let db = db();
482 put(&db, "users", "a", json!({"n": 1}));
483 let from = tip(&db);
484 put(&db, "orders", "o1", json!({"total": 10}));
485 let d = diff(&db, from, tip(&db)).unwrap();
486 assert_eq!(
487 d.collections,
488 vec![CollChange { name: "orders".into(), kind: ChangeKind::Added }]
489 );
490 assert_eq!(find(&d, "orders", "o1").unwrap().kind, ChangeKind::Added);
491 }
492
493 #[test]
494 fn emptied_but_live_collection_is_not_a_removed_collection() {
495 let db = db();
496 put(&db, "orders", "o1", json!({"total": 10}));
497 let from = tip(&db);
498 db.delete("orders", "o1").unwrap();
499 let d = diff(&db, from, tip(&db)).unwrap();
500 assert!(
501 d.collections.is_empty(),
502 "an empty collection still exists; only a drop removes it: {:?}",
503 d.collections
504 );
505 assert_eq!(find(&d, "orders", "o1").unwrap().kind, ChangeKind::Removed);
506 }
507
508 #[test]
509 fn dropped_collection_appears_as_removed_with_its_rows() {
510 let db = db();
511 put(&db, "orders", "o1", json!({"total": 10}));
512 put(&db, "orders", "o2", json!({"total": 20}));
513 let from = tip(&db);
514 assert!(db.drop_collection("orders").unwrap());
515 let d = diff(&db, from, tip(&db)).unwrap();
516 assert_eq!(
517 d.collections,
518 vec![CollChange { name: "orders".into(), kind: ChangeKind::Removed }]
519 );
520 assert_eq!(d.documents.len(), 2);
523 assert!(d.documents.iter().all(|c| c.kind == ChangeKind::Removed));
524 }
525
526 #[test]
527 fn reserved_collections_never_appear() {
528 let db = db();
529 put(&db, "seed", "s", json!({}));
533 let from = tip(&db);
534 put(&db, "users", "a", json!({"n": 1}));
537 db.create_root().unwrap();
538 let d = diff(&db, from, tip(&db)).unwrap();
539 assert!(
540 d.collections.iter().all(|c| !c.name.starts_with("_nedb")),
541 "{:?}",
542 d.collections
543 );
544 assert!(
545 d.documents.iter().all(|c| !c.coll.starts_with("_nedb")),
546 "{:?}",
547 d.documents
548 );
549 assert_eq!(
550 d.collections,
551 vec![CollChange { name: "users".into(), kind: ChangeKind::Added }]
552 );
553 }
554
555 #[test]
556 fn below_the_history_floor_is_refused_not_silently_empty() {
557 let db = db();
558 put(&db, "users", "a", json!({"n": 1}));
559 put(&db, "users", "a", json!({"n": 2}));
560 db.set_history_floor(tip(&db)).unwrap();
566 let floor = db.history_floor();
567 assert!(floor > 0, "precondition: the database is in the pruned state");
568
569 let err = diff(&db, 0, tip(&db)).unwrap_err();
570 assert!(err.starts_with(HISTORY_PRUNED), "{}", err);
571 assert!(err.contains(&floor.to_string()), "the reason must name the floor: {}", err);
572
573 assert!(diff(&db, floor, tip(&db)).is_ok());
575 }
576
577 #[test]
578 fn history_floor_refusal_also_covers_to_seq() {
579 let db = db();
580 put(&db, "users", "a", json!({"n": 1}));
581 put(&db, "users", "a", json!({"n": 2}));
582 db.set_history_floor(tip(&db)).unwrap();
583 let floor = db.history_floor();
584 let err = diff(&db, 0, 0).unwrap_err();
587 assert!(err.starts_with(HISTORY_PRUNED), "{}", err);
588 assert!(err.contains("from_seq"), "{}", err);
589 let err = diff(&db, floor, floor - 1).unwrap_err();
590 assert!(err.starts_with(REVERSED_RANGE), "{}", err);
591 }
592
593 #[test]
594 fn reversed_range_is_refused() {
595 let db = db();
596 put(&db, "users", "a", json!({"n": 1}));
597 let t = tip(&db);
598 let err = diff(&db, t, 0).unwrap_err();
599 assert!(err.starts_with(REVERSED_RANGE), "{}", err);
600 assert!(err.contains(&format!("diff({}, {})", 0, t)), "must name the fix: {}", err);
601 }
602
603 #[test]
604 fn field_delta_names_added_removed_and_changed_keys() {
605 let db = db();
606 put(&db, "users", "a", json!({"keep": 1, "drop": 2, "move": 3}));
607 let from = tip(&db);
608 put(&db, "users", "a", json!({"keep": 1, "move": 4, "new": 5}));
609 let d = diff(&db, from, tip(&db)).unwrap();
610 let f = find(&d, "users", "a").unwrap().fields.as_ref().expect("objects get a delta");
611 assert_eq!(f.added, vec!["new".to_string()]);
612 assert_eq!(f.removed, vec!["drop".to_string()]);
613 assert_eq!(f.changed, vec!["move".to_string()]);
614 }
615
616 #[test]
617 fn no_field_delta_when_either_side_is_not_an_object() {
618 let db = db();
619 put(&db, "vals", "scalar", json!(1));
620 put(&db, "vals", "arr", json!([1, 2]));
621 let from = tip(&db);
622 put(&db, "vals", "scalar", json!({"n": 1}));
623 put(&db, "vals", "arr", json!([1, 2, 3]));
624 let d = diff(&db, from, tip(&db)).unwrap();
625 assert!(find(&d, "vals", "scalar").unwrap().fields.is_none());
626 assert!(find(&d, "vals", "arr").unwrap().fields.is_none());
627 }
628
629 #[test]
630 fn valid_from_change_alone_counts_as_modified() {
631 let db = db();
632 db.put("users", "a", json!({"n": 1}), vec![], Some("2020-01-01".into()), None)
633 .unwrap();
634 let from = tip(&db);
635 db.put("users", "a", json!({"n": 1}), vec![], Some("2021-01-01".into()), None)
636 .unwrap();
637 let d = diff(&db, from, tip(&db)).unwrap();
638 let c = find(&d, "users", "a").expect("a temporal move is a state change");
639 assert_eq!(c.kind, ChangeKind::Modified);
640 assert_eq!(c.before, c.after, "payload identical; only the window moved");
641 let t = c.temporal.as_ref().expect("the reason must be reported");
642 assert_eq!(t.valid_from_before.as_deref(), Some("2020-01-01"));
643 assert_eq!(t.valid_from_after.as_deref(), Some("2021-01-01"));
644 assert!(
645 c.fields.as_ref().unwrap().is_empty(),
646 "validity is not a field, so no key moved"
647 );
648 }
649
650 #[test]
651 fn valid_to_change_alone_counts_as_modified() {
652 let db = db();
653 db.put("users", "a", json!({"n": 1}), vec![], None, None).unwrap();
654 let from = tip(&db);
655 db.put("users", "a", json!({"n": 1}), vec![], None, Some("2030-01-01".into()))
656 .unwrap();
657 let d = diff(&db, from, tip(&db)).unwrap();
658 let c = find(&d, "users", "a").unwrap();
659 assert_eq!(c.kind, ChangeKind::Modified);
660 let t = c.temporal.as_ref().unwrap();
661 assert_eq!(t.valid_to_before, None);
662 assert_eq!(t.valid_to_after.as_deref(), Some("2030-01-01"));
663 }
664
665 #[test]
666 fn no_temporal_delta_when_the_window_did_not_move() {
667 let db = db();
668 db.put("users", "a", json!({"n": 1}), vec![], Some("2020-01-01".into()), None)
669 .unwrap();
670 let from = tip(&db);
671 db.put("users", "a", json!({"n": 2}), vec![], Some("2020-01-01".into()), None)
672 .unwrap();
673 let d = diff(&db, from, tip(&db)).unwrap();
674 assert!(find(&d, "users", "a").unwrap().temporal.is_none());
675 }
676
677 #[test]
678 fn diff_of_a_point_with_itself_is_empty() {
679 let db = db();
680 put(&db, "users", "a", json!({"n": 1}));
681 put(&db, "orders", "o1", json!({"n": 2}));
682 let t = tip(&db);
683 let d = diff(&db, t, t).unwrap();
684 assert!(d.is_empty(), "{:?}", d);
685 assert_eq!((d.from_seq, d.to_seq), (t, t));
686 assert!(diff(&db, 1, 1).unwrap().is_empty());
688 }
689
690 #[test]
691 fn output_ordering_is_deterministic() {
692 let db = db();
693 put(&db, "seed", "s", json!({}));
694 let from = tip(&db);
695 for (coll, id) in [
699 ("zeta", "m"), ("alpha", "z"), ("zeta", "a"),
700 ("alpha", "b"), ("mid", "q"), ("alpha", "a"),
701 ] {
702 put(&db, coll, id, json!({"v": id}));
703 }
704 let to = tip(&db);
705 let first = diff(&db, from, to).unwrap();
706 let second = diff(&db, from, to).unwrap();
707 assert_eq!(first, second, "two runs over the same range must agree");
708
709 let colls: Vec<&str> = first.collections.iter().map(|c| c.name.as_str()).collect();
710 assert_eq!(colls, vec!["alpha", "mid", "zeta"]);
711 let docs: Vec<(&str, &str)> = first
712 .documents
713 .iter()
714 .map(|c| (c.coll.as_str(), c.id.as_str()))
715 .collect();
716 assert_eq!(
717 docs,
718 vec![
719 ("alpha", "a"), ("alpha", "b"), ("alpha", "z"),
720 ("mid", "q"),
721 ("zeta", "a"), ("zeta", "m"),
722 ]
723 );
724 }
725
726 #[test]
727 fn recreated_document_is_modified_not_added() {
728 let db = db();
731 put(&db, "users", "a", json!({"n": 1}));
732 let from = tip(&db);
733 db.delete("users", "a").unwrap();
734 put(&db, "users", "a", json!({"n": 2}));
735 let d = diff(&db, from, tip(&db)).unwrap();
736 let c = find(&d, "users", "a").unwrap();
737 assert_eq!(c.kind, ChangeKind::Modified);
738 assert_eq!(c.before, Some(json!({"n": 1})));
739 assert_eq!(c.after, Some(json!({"n": 2})));
740 }
741
742 #[test]
743 fn an_empty_diff_implies_equal_state_roots() {
744 let db = db();
747 put(&db, "users", "a", json!({"n": 1}));
748 let from = tip(&db);
749 put(&db, "users", "a", json!({"n": 99}));
750 put(&db, "users", "a", json!({"n": 1}));
751 let to = tip(&db);
752 assert!(diff(&db, from, to).unwrap().is_empty());
753 assert_eq!(
754 db.state_root_as_of(from).unwrap().state_root,
755 db.state_root_as_of(to).unwrap().state_root
756 );
757 }
758}