1pub mod bootstrap;
20
21use std::collections::{BTreeMap, BTreeSet};
22use std::sync::{Arc, OnceLock};
23
24use corium_core::{
25 AttrId, Cardinality, Datom, EntityId, IndexOrder, Keyword, KeywordInterner, Partition, Schema,
26 Unique, Value, encoding::Encodable,
27};
28use rpds::{RedBlackTreeMapSync, VectorSync};
29
30pub const FIRST_USER_ID: u64 = 1_000;
32
33#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
35pub enum DbView {
36 #[default]
38 Current,
39 AsOf(u64),
41 Since(u64),
43 History,
45}
46
47#[derive(Clone, Debug, Default)]
49pub struct Idents {
50 by_keyword: BTreeMap<Keyword, EntityId>,
51 by_id: BTreeMap<EntityId, Keyword>,
52}
53
54impl Idents {
55 pub fn insert(&mut self, keyword: Keyword, id: EntityId) {
57 self.by_id.insert(id, keyword.clone());
58 self.by_keyword.insert(keyword, id);
59 }
60
61 #[must_use]
63 pub fn entid(&self, keyword: &Keyword) -> Option<EntityId> {
64 self.by_keyword.get(keyword).copied()
65 }
66
67 #[must_use]
69 pub fn ident(&self, id: EntityId) -> Option<&Keyword> {
70 self.by_id.get(&id)
71 }
72
73 pub fn iter(&self) -> impl Iterator<Item = (&Keyword, &EntityId)> {
75 self.by_keyword.iter()
76 }
77}
78
79#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
81pub struct AttrStats {
82 pub count: usize,
84 pub distinct_values: usize,
86 pub distinct_entities: usize,
88}
89
90#[derive(Clone, Debug, Default)]
92pub struct PlannerStats {
93 pub per_attr: BTreeMap<AttrId, AttrStats>,
95 pub total_datoms: usize,
97 pub entity_count: usize,
99}
100
101impl PlannerStats {
102 #[must_use]
104 pub fn estimate(&self, e_bound: bool, a: Option<AttrId>, v_bound: bool) -> usize {
105 let attr = a.and_then(|a| self.per_attr.get(&a));
106 match (e_bound, attr) {
107 (true, Some(stats)) => (stats.count / stats.distinct_entities.max(1)).max(1),
109 (true, None) => (self.total_datoms / self.entity_count.max(1)).max(1),
110 (false, Some(stats)) if v_bound => (stats.count / stats.distinct_values.max(1)).max(1),
111 (false, Some(stats)) => stats.count.max(1),
112 (false, None) if a.is_some() => 1,
114 (false, None) => self.total_datoms.max(1),
115 }
116 }
117}
118
119type Index = RedBlackTreeMapSync<Vec<u8>, Arc<Datom>>;
136
137const ORDERS: [IndexOrder; 4] = [
138 IndexOrder::Eavt,
139 IndexOrder::Aevt,
140 IndexOrder::Avet,
141 IndexOrder::Vaet,
142];
143
144const fn slot(order: IndexOrder) -> usize {
145 match order {
146 IndexOrder::Eavt => 0,
147 IndexOrder::Aevt => 1,
148 IndexOrder::Avet => 2,
149 IndexOrder::Vaet => 3,
150 }
151}
152
153#[must_use]
158pub fn key_prefix(
159 order: IndexOrder,
160 e: Option<EntityId>,
161 a: Option<AttrId>,
162 v: Option<&Value>,
163) -> Vec<u8> {
164 enum C {
165 E,
166 A,
167 V,
168 }
169 let components = match order {
170 IndexOrder::Eavt => [C::E, C::A, C::V],
171 IndexOrder::Aevt => [C::A, C::E, C::V],
172 IndexOrder::Avet => [C::A, C::V, C::E],
173 IndexOrder::Vaet => [C::V, C::A, C::E],
174 };
175 let mut out = Vec::new();
176 for component in components {
177 match component {
178 C::E => match e {
179 Some(e) => e.encode_into(&mut out),
180 None => break,
181 },
182 C::A => match a {
183 Some(a) => a.encode_into(&mut out),
184 None => break,
185 },
186 C::V => match v {
187 Some(v) => v.encode_into(&mut out),
188 None => break,
189 },
190 }
191 }
192 out
193}
194
195#[derive(Clone, Debug, Default)]
207pub struct TxInstants {
208 by_t: RedBlackTreeMapSync<u64, i64>,
209 by_instant: RedBlackTreeMapSync<i64, u64>,
210}
211
212impl TxInstants {
213 fn record(&mut self, t: u64, instant: i64) {
214 self.by_t.insert_mut(t, instant);
215 if self.by_instant.get(&instant).is_none_or(|held| *held < t) {
220 self.by_instant.insert_mut(instant, t);
221 }
222 }
223
224 #[must_use]
226 pub fn instant(&self, t: u64) -> Option<i64> {
227 self.by_t.get(&t).copied()
228 }
229
230 #[must_use]
233 pub fn t_at(&self, instant: i64) -> u64 {
234 self.by_instant
235 .range(..=instant)
236 .next_back()
237 .map_or(0, |(_, t)| *t)
238 }
239
240 #[must_use]
242 pub fn len(&self) -> usize {
243 self.by_t.size()
244 }
245
246 #[must_use]
248 pub fn is_empty(&self) -> bool {
249 self.by_t.is_empty()
250 }
251}
252
253#[derive(Clone, Debug, Default)]
300pub struct Db {
301 basis_t: u64,
302 schema: Schema,
303 recorded: VectorSync<Arc<Datom>>,
304 idents: Arc<Idents>,
305 interner: Arc<KeywordInterner>,
306 view: DbView,
307 instants: TxInstants,
308 indexes: Arc<OnceLock<[Index; 4]>>,
309 stats: Arc<OnceLock<PlannerStats>>,
310}
311
312impl Db {
313 #[must_use]
319 pub fn new(mut schema: Schema) -> Self {
320 bootstrap::install_schema(&mut schema);
321 Self {
322 schema,
323 ..Self::default()
324 }
325 }
326
327 #[must_use]
341 pub fn from_current_snapshot(
342 basis_t: u64,
343 mut schema: Schema,
344 mut idents: Idents,
345 interner: KeywordInterner,
346 datoms: Vec<Datom>,
347 ) -> Self {
348 bootstrap::install(&mut schema, &mut idents);
349 let mut instants = TxInstants::default();
350 for datom in &datoms {
351 if let Datom {
352 e,
353 a,
354 v: Value::Instant(instant),
355 tx,
356 added: true,
357 ..
358 } = datom
359 && *a == bootstrap::TX_INSTANT
360 && *e == *tx
361 && tx.partition() == Partition::Tx as u32
362 {
363 instants.record(tx.sequence(), *instant);
364 }
365 }
366 Self {
367 basis_t,
368 schema,
369 recorded: datoms.into_iter().map(Arc::new).collect(),
370 idents: Arc::new(idents),
371 interner: Arc::new(interner),
372 view: DbView::Current,
373 instants,
374 indexes: Arc::new(OnceLock::new()),
375 stats: Arc::new(OnceLock::new()),
376 }
377 }
378
379 #[must_use]
381 pub fn with_naming(mut self, mut idents: Idents, interner: KeywordInterner) -> Self {
382 bootstrap::install(&mut self.schema, &mut idents);
383 self.idents = Arc::new(idents);
384 self.interner = Arc::new(interner);
385 self
386 }
387
388 #[must_use]
390 pub const fn basis_t(&self) -> u64 {
391 self.basis_t
392 }
393
394 #[must_use]
396 pub const fn schema(&self) -> &Schema {
397 &self.schema
398 }
399
400 #[must_use]
402 pub fn idents(&self) -> &Idents {
403 &self.idents
404 }
405
406 #[must_use]
408 pub fn interner(&self) -> &KeywordInterner {
409 &self.interner
410 }
411
412 #[must_use]
414 pub const fn view(&self) -> DbView {
415 self.view
416 }
417
418 pub fn recorded_datoms(&self) -> impl Iterator<Item = &Datom> {
420 self.recorded.iter().map(AsRef::as_ref)
421 }
422
423 #[must_use]
425 pub fn recorded_len(&self) -> usize {
426 self.recorded.len()
427 }
428
429 pub fn recorded_since(&self, t: u64) -> impl Iterator<Item = &Datom> {
444 let mut start = self.recorded.len();
445 while start > 0
446 && self
447 .recorded
448 .get(start - 1)
449 .is_some_and(|datom| datom.tx.sequence() > t)
450 {
451 start -= 1;
452 }
453 (start..self.recorded.len())
457 .filter_map(|index| self.recorded.get(index))
458 .map(AsRef::as_ref)
459 }
460
461 #[must_use]
463 pub fn as_of(&self, t: u64) -> Self {
464 self.with_view(DbView::AsOf(t))
465 }
466
467 #[must_use]
469 pub fn since(&self, t: u64) -> Self {
470 self.with_view(DbView::Since(t))
471 }
472
473 #[must_use]
475 pub fn history(&self) -> Self {
476 self.with_view(DbView::History)
477 }
478
479 #[must_use]
481 pub const fn instants(&self) -> &TxInstants {
482 &self.instants
483 }
484
485 #[must_use]
487 pub fn tx_instant(&self, t: u64) -> Option<i64> {
488 self.instants.instant(t)
489 }
490
491 #[must_use]
498 pub fn t_at_instant(&self, instant: i64) -> u64 {
499 self.instants.t_at(instant)
500 }
501
502 #[must_use]
506 pub fn as_of_instant(&self, instant: i64) -> Self {
507 self.as_of(self.t_at_instant(instant))
508 }
509
510 #[must_use]
514 pub fn since_instant(&self, instant: i64) -> Self {
515 self.since(self.t_at_instant(instant))
516 }
517
518 fn with_view(&self, view: DbView) -> Self {
519 if view == self.view {
520 return self.clone();
521 }
522 let shared = self.fold_class(view) == self.fold_class(self.view);
528 Self {
529 view,
530 indexes: if shared {
531 Arc::clone(&self.indexes)
532 } else {
533 Arc::new(OnceLock::new())
534 },
535 stats: if shared {
536 Arc::clone(&self.stats)
537 } else {
538 Arc::new(OnceLock::new())
539 },
540 ..self.clone()
541 }
542 }
543
544 const fn fold_class(&self, view: DbView) -> DbView {
552 match view {
553 DbView::AsOf(t) if t >= self.basis_t => DbView::Current,
554 DbView::Since(0) => DbView::Current,
555 other => other,
556 }
557 }
558
559 #[must_use]
561 pub fn tx_range(&self, start: u64, end: Option<u64>) -> Vec<(u64, Vec<Datom>)> {
562 let mut by_t: BTreeMap<u64, Vec<Datom>> = BTreeMap::new();
563 for datom in &self.recorded {
564 let t = datom.tx.sequence();
565 if t >= start && end.is_none_or(|end| t < end) {
566 by_t.entry(t).or_default().push((**datom).clone());
567 }
568 }
569 by_t.into_iter().collect()
570 }
571
572 #[must_use]
574 pub fn datoms(&self) -> Vec<Datom> {
575 self.datoms_at(IndexOrder::Eavt).cloned().collect()
576 }
577
578 pub fn datoms_at(&self, order: IndexOrder) -> impl Iterator<Item = &Datom> {
583 self.indexes()[slot(order)].values().map(AsRef::as_ref)
584 }
585
586 pub fn datoms_for_attribute(&self, a: AttrId) -> impl Iterator<Item = &Datom> {
592 let prefix = key_prefix(IndexOrder::Aevt, None, Some(a), None);
593 self.indexes()[slot(IndexOrder::Aevt)]
594 .range(prefix.clone()..)
595 .take_while(move |(key, _)| key.starts_with(&prefix))
596 .map(|(_, datom)| datom.as_ref())
597 }
598
599 pub fn datoms_prefix<'a>(
601 &'a self,
602 order: IndexOrder,
603 prefix: &'a [u8],
604 ) -> impl Iterator<Item = &'a Datom> {
605 self.indexes()[slot(order)]
606 .range(prefix.to_vec()..)
607 .take_while(move |(key, _)| key.starts_with(prefix))
608 .map(|(_, datom)| datom.as_ref())
609 }
610
611 pub fn seek_datoms<'a>(
613 &'a self,
614 order: IndexOrder,
615 start: &[u8],
616 ) -> impl Iterator<Item = &'a Datom> {
617 self.indexes()[slot(order)]
618 .range(start.to_vec()..)
619 .map(|(_, datom)| datom.as_ref())
620 }
621
622 pub fn index_range<'a>(
626 &'a self,
627 a: AttrId,
628 start: Option<&Value>,
629 end: Option<&'a Value>,
630 ) -> impl Iterator<Item = &'a Datom> {
631 let a_prefix = key_prefix(IndexOrder::Avet, None, Some(a), None);
632 let start_key = key_prefix(IndexOrder::Avet, None, Some(a), start);
633 self.indexes()[slot(IndexOrder::Avet)]
634 .range(start_key..)
635 .take_while(move |(key, _)| key.starts_with(&a_prefix))
636 .map(|(_, datom)| datom.as_ref())
637 .take_while(move |datom| end.is_none_or(|end| datom.v < *end))
638 }
639
640 #[must_use]
642 pub fn values(&self, e: EntityId, a: AttrId) -> Vec<Value> {
643 let prefix = key_prefix(IndexOrder::Eavt, Some(e), Some(a), None);
644 self.datoms_prefix(IndexOrder::Eavt, &prefix)
645 .map(|datom| datom.v.clone())
646 .collect()
647 }
648
649 #[must_use]
651 pub fn lookup(&self, a: AttrId, v: &Value) -> Option<EntityId> {
652 if avet_covered(&self.schema, a) {
653 let prefix = key_prefix(IndexOrder::Avet, None, Some(a), Some(v));
654 self.datoms_prefix(IndexOrder::Avet, &prefix)
655 .next()
656 .map(|datom| datom.e)
657 } else {
658 let prefix = key_prefix(IndexOrder::Aevt, None, Some(a), None);
659 self.datoms_prefix(IndexOrder::Aevt, &prefix)
660 .find(|datom| datom.v == *v)
661 .map(|datom| datom.e)
662 }
663 }
664
665 #[must_use]
673 pub fn with_transaction(&self, t: u64, datoms: &[Datom]) -> Self {
674 match bootstrap::asserted_instant(t, datoms) {
675 Some(instant) => self.with_transaction_at(t, instant, datoms),
676 None => self.apply_transaction(t, datoms),
677 }
678 }
679
680 #[must_use]
687 pub fn with_transaction_at(&self, t: u64, instant: i64, datoms: &[Datom]) -> Self {
688 let asserted = bootstrap::asserted_instant(t, datoms);
689 let mut next = if asserted.is_some() {
690 self.apply_transaction(t, datoms)
691 } else {
692 let mut with_instant = Vec::with_capacity(datoms.len() + 1);
693 with_instant.extend_from_slice(datoms);
694 with_instant.push(bootstrap::tx_instant_datom(t, instant));
695 self.apply_transaction(t, &with_instant)
696 };
697 next.instants.record(t, asserted.unwrap_or(instant));
698 next
699 }
700
701 fn apply_transaction(&self, t: u64, datoms: &[Datom]) -> Self {
702 debug_assert!(
703 self.view == DbView::Current,
704 "with_transaction applies only to the current view"
705 );
706 let mut next = self.clone();
707 next.basis_t = t;
708 let arrived: Vec<Arc<Datom>> = datoms.iter().cloned().map(Arc::new).collect();
712 for datom in &arrived {
717 next.recorded.push_back_mut(Arc::clone(datom));
718 }
719 next.indexes = Arc::new(OnceLock::new());
720 next.stats = Arc::new(OnceLock::new());
721 if let Some(parent) = self.indexes.get() {
724 let mut derived = parent.clone();
725 apply_current(&mut derived, arrived.iter(), &self.schema);
726 let _ = next.indexes.set(derived);
727 }
728 next
729 }
730
731 #[must_use]
733 pub fn stats(&self) -> DbStats {
734 let planner = self.planner_stats();
735 DbStats {
736 datoms: planner.total_datoms,
737 entities: planner.entity_count,
738 attributes: planner.per_attr.len(),
739 }
740 }
741
742 #[must_use]
744 pub fn planner_stats(&self) -> &PlannerStats {
745 self.stats.get_or_init(|| {
746 let mut stats = PlannerStats::default();
747 let mut values: BTreeMap<AttrId, BTreeSet<&Value>> = BTreeMap::new();
748 let mut attr_entities: BTreeMap<AttrId, BTreeSet<EntityId>> = BTreeMap::new();
749 let mut entities: BTreeSet<EntityId> = BTreeSet::new();
750 for datom in self.datoms_at(IndexOrder::Eavt) {
751 stats.total_datoms += 1;
752 stats.per_attr.entry(datom.a).or_default().count += 1;
753 values.entry(datom.a).or_default().insert(&datom.v);
754 attr_entities.entry(datom.a).or_default().insert(datom.e);
755 entities.insert(datom.e);
756 }
757 for (a, entry) in &mut stats.per_attr {
758 entry.distinct_values = values.get(a).map_or(0, BTreeSet::len);
759 entry.distinct_entities = attr_entities.get(a).map_or(0, BTreeSet::len);
760 }
761 stats.entity_count = entities.len();
762 stats
763 })
764 }
765
766 fn indexes(&self) -> &[Index; 4] {
767 self.indexes.get_or_init(|| {
768 let mut indexes: [Index; 4] = Default::default();
769 match self.view {
770 DbView::History => {
771 for datom in &self.recorded {
772 if self.schema.get(datom.a).is_some_and(|a| a.no_history) {
773 continue;
774 }
775 insert_datom(&mut indexes, datom, &self.schema, true);
776 }
777 }
778 DbView::Current | DbView::AsOf(_) | DbView::Since(_) => {
779 let cutoff = match self.view {
788 DbView::AsOf(t) => Some(t),
789 _ => None,
790 };
791 let mut live = Index::new_sync();
792 for datom in &self.recorded {
793 if cutoff.is_some_and(|t| datom.tx.sequence() > t) {
794 continue;
795 }
796 let key = key_prefix(
797 IndexOrder::Eavt,
798 Some(datom.e),
799 Some(datom.a),
800 Some(&datom.v),
801 );
802 if datom.added {
803 live.insert_mut(key, Arc::clone(datom));
804 } else {
805 live.remove_mut(&key);
806 }
807 }
808 if let DbView::Since(t) = self.view {
815 live = live
816 .iter()
817 .filter(|(_, datom)| datom.tx.sequence() > t)
818 .map(|(key, datom)| (key.clone(), Arc::clone(datom)))
819 .collect();
820 }
821 for datom in live.values() {
822 for order in [IndexOrder::Aevt, IndexOrder::Avet, IndexOrder::Vaet] {
823 if covered(&self.schema, order, datom) {
824 let key =
825 key_prefix(order, Some(datom.e), Some(datom.a), Some(&datom.v));
826 indexes[slot(order)].insert_mut(key, Arc::clone(datom));
827 }
828 }
829 }
830 indexes[slot(IndexOrder::Eavt)] = live;
831 }
832 }
833 indexes
834 })
835 }
836}
837
838fn apply_current<'a>(
844 indexes: &mut [Index; 4],
845 datoms: impl Iterator<Item = &'a Arc<Datom>>,
846 schema: &Schema,
847) {
848 for datom in datoms {
849 if datom.added {
850 insert_datom(indexes, datom, schema, false);
851 } else {
852 for order in ORDERS {
853 if covered(schema, order, datom) {
854 let key = key_prefix(order, Some(datom.e), Some(datom.a), Some(&datom.v));
855 indexes[slot(order)].remove_mut(&key);
856 }
857 }
858 }
859 }
860}
861
862fn insert_datom(indexes: &mut [Index; 4], datom: &Arc<Datom>, schema: &Schema, with_tx: bool) {
863 for order in ORDERS {
864 if covered(schema, order, datom) {
865 let key = if with_tx {
866 datom.key(order)
867 } else {
868 key_prefix(order, Some(datom.e), Some(datom.a), Some(&datom.v))
869 };
870 indexes[slot(order)].insert_mut(key, Arc::clone(datom));
871 }
872 }
873}
874
875#[must_use]
883pub fn covered(schema: &Schema, order: IndexOrder, datom: &Datom) -> bool {
884 match order {
885 IndexOrder::Eavt | IndexOrder::Aevt => true,
886 IndexOrder::Avet => avet_covered(schema, datom.a),
887 IndexOrder::Vaet => matches!(datom.v, Value::Ref(_)),
888 }
889}
890
891#[must_use]
893pub fn avet_covered(schema: &Schema, a: AttrId) -> bool {
894 schema
895 .get(a)
896 .is_some_and(|attr| attr.indexed || attr.unique.is_some())
897}
898
899#[derive(Clone, Copy, Debug, Eq, PartialEq)]
901pub struct DbStats {
902 pub datoms: usize,
904 pub entities: usize,
906 pub attributes: usize,
908}
909
910#[must_use]
912pub const fn attribute(
913 id: u64,
914 value_type: corium_core::ValueType,
915 cardinality: Cardinality,
916 unique: Option<Unique>,
917) -> corium_core::Attribute {
918 corium_core::Attribute {
919 id: EntityId::new(Partition::Db as u32, id),
920 value_type,
921 cardinality,
922 unique,
923 is_component: false,
924 indexed: unique.is_some(),
925 no_history: false,
926 }
927}
928
929#[cfg(test)]
930mod tests {
931 use super::*;
932 use corium_core::ValueType;
933
934 fn schema() -> Schema {
935 let mut schema = Schema::default();
936 schema.insert(attribute(1, ValueType::Str, Cardinality::One, None));
937 schema.insert(attribute(
938 2,
939 ValueType::Long,
940 Cardinality::One,
941 Some(Unique::Identity),
942 ));
943 schema.insert(attribute(3, ValueType::Ref, Cardinality::Many, None));
944 schema
945 }
946
947 fn attr(id: u64) -> AttrId {
948 EntityId::new(Partition::Db as u32, id)
949 }
950
951 fn entity(id: u64) -> EntityId {
952 EntityId::new(Partition::User as u32, id)
953 }
954
955 fn tx_entity(t: u64) -> EntityId {
956 EntityId::new(Partition::Tx as u32, t)
957 }
958
959 fn datom(e: u64, a: u64, v: Value, t: u64, added: bool) -> Datom {
960 Datom {
961 e: entity(e),
962 a: attr(a),
963 v,
964 tx: tx_entity(t),
965 added,
966 }
967 }
968
969 fn sample() -> Db {
970 Db::new(schema())
971 .with_transaction(
972 1,
973 &[
974 datom(1, 1, Value::Str("alice".into()), 1, true),
975 datom(1, 2, Value::Long(7), 1, true),
976 ],
977 )
978 .with_transaction(
979 2,
980 &[
981 datom(1, 1, Value::Str("alice".into()), 2, false),
982 datom(1, 1, Value::Str("alicia".into()), 2, true),
983 datom(2, 3, Value::Ref(entity(1)), 2, true),
984 ],
985 )
986 }
987
988 #[test]
989 fn current_view_folds_retractions() {
990 let db = sample();
991 assert_eq!(
992 db.values(entity(1), attr(1)),
993 vec![Value::Str("alicia".into())]
994 );
995 assert_eq!(db.stats().datoms, 3);
996 }
997
998 #[test]
999 fn recorded_since_returns_the_transaction_tail() {
1000 let db = sample();
1001 assert_eq!(db.recorded_since(2).count(), 0);
1002 let tail: Vec<_> = db.recorded_since(1).collect();
1003 assert_eq!(tail.len(), 3);
1004 assert!(tail.iter().all(|datom| datom.tx.sequence() == 2));
1005 assert_eq!(db.recorded_since(0).count(), db.recorded_len());
1006
1007 let snapshot = Db::from_current_snapshot(
1010 2,
1011 schema(),
1012 Idents::default(),
1013 KeywordInterner::default(),
1014 db.datoms(),
1015 )
1016 .with_transaction(3, &[datom(3, 1, Value::Str("carol".into()), 3, true)]);
1017 let tail: Vec<_> = snapshot.recorded_since(2).collect();
1018 assert_eq!(tail.len(), 1);
1019 assert!(tail.iter().all(|datom| datom.tx.sequence() == 3));
1020 }
1021
1022 #[test]
1023 fn attribute_scan_uses_complete_aevt_coverage() {
1024 let db = sample();
1025 let datoms = db.datoms_for_attribute(attr(1)).collect::<Vec<_>>();
1026 assert_eq!(datoms.len(), 1);
1027 assert_eq!(datoms[0].v, Value::Str("alicia".into()));
1028 assert!(datoms.iter().all(|datom| datom.a == attr(1)));
1029 }
1030
1031 #[test]
1032 fn as_of_reconstructs_past_basis() {
1033 let db = sample().as_of(1);
1034 assert_eq!(
1035 db.values(entity(1), attr(1)),
1036 vec![Value::Str("alice".into())]
1037 );
1038 assert_eq!(db.stats().datoms, 2);
1039 }
1040
1041 #[test]
1042 fn since_excludes_older_live_facts() {
1043 let db = sample().since(1);
1044 assert_eq!(db.values(entity(1), attr(2)), Vec::<Value>::new());
1046 assert_eq!(
1047 db.values(entity(1), attr(1)),
1048 vec![Value::Str("alicia".into())]
1049 );
1050 }
1051
1052 #[test]
1053 fn history_exposes_assertions_and_retractions() {
1054 let db = sample().history();
1055 let names: Vec<_> = db
1056 .datoms_prefix(
1057 IndexOrder::Eavt,
1058 &key_prefix(IndexOrder::Eavt, Some(entity(1)), Some(attr(1)), None),
1059 )
1060 .map(|d| (d.v.clone(), d.added))
1061 .collect();
1062 assert_eq!(names.len(), 3);
1063 assert!(names.contains(&(Value::Str("alice".into()), false)));
1064 }
1065
1066 #[test]
1067 fn since_filters_the_live_set_in_every_order() {
1068 let db = sample().since(1);
1071 assert_eq!(db.datoms_at(IndexOrder::Eavt).count(), 2);
1072 assert_eq!(db.datoms_at(IndexOrder::Aevt).count(), 2);
1073 assert_eq!(db.datoms_at(IndexOrder::Avet).count(), 0);
1075 assert_eq!(db.datoms_at(IndexOrder::Vaet).count(), 1);
1076 for order in ORDERS {
1077 assert!(db.datoms_at(order).all(|datom| datom.tx.sequence() > 1));
1078 }
1079 }
1080
1081 #[test]
1082 fn a_datom_is_allocated_once_and_shared_by_the_log_and_every_index() {
1083 let db = sample();
1084 let indexes = db.indexes();
1085 for (_, eavt) in &indexes[slot(IndexOrder::Eavt)] {
1086 let aevt = indexes[slot(IndexOrder::Aevt)]
1089 .get(&key_prefix(
1090 IndexOrder::Aevt,
1091 Some(eavt.e),
1092 Some(eavt.a),
1093 Some(&eavt.v),
1094 ))
1095 .expect("AEVT covers every datom");
1096 assert!(Arc::ptr_eq(eavt, aevt), "AEVT copied the datom");
1097 assert!(
1098 db.recorded.iter().any(|entry| Arc::ptr_eq(entry, eavt)),
1099 "the index copied the datom instead of sharing the log's"
1100 );
1101 }
1102 }
1103
1104 #[test]
1105 fn views_that_name_the_current_value_share_its_fold() {
1106 let db = sample();
1107 let _ = db.datoms();
1108 for view in [
1109 db.as_of(db.basis_t()),
1110 db.as_of(db.basis_t() + 10),
1111 db.since(0),
1112 ] {
1113 assert!(
1114 Arc::ptr_eq(&db.indexes, &view.indexes),
1115 "{:?} refolded the whole history to reach the current value",
1116 view.view()
1117 );
1118 assert!(Arc::ptr_eq(&db.stats, &view.stats));
1119 assert_eq!(view.datoms(), db.datoms());
1120 }
1121 for view in [db.as_of(1), db.since(1), db.history()] {
1123 assert!(!Arc::ptr_eq(&db.indexes, &view.indexes));
1124 }
1125 }
1126
1127 #[test]
1128 fn a_view_reports_the_time_view_it_was_asked_for() {
1129 let db = sample();
1133 assert_eq!(db.view(), DbView::Current);
1134 assert_eq!(db.as_of(db.basis_t()).view(), DbView::AsOf(db.basis_t()));
1135 assert_eq!(db.since(0).view(), DbView::Since(0));
1136 }
1137
1138 #[test]
1139 fn avet_only_covers_indexed_attributes() {
1140 let db = sample();
1141 assert_eq!(db.datoms_at(IndexOrder::Avet).count(), 1);
1142 assert_eq!(db.datoms_at(IndexOrder::Vaet).count(), 1);
1143 }
1144
1145 #[test]
1146 fn index_range_scans_value_bounds() {
1147 let db = Db::new(schema()).with_transaction(
1148 1,
1149 &[
1150 datom(1, 2, Value::Long(1), 1, true),
1151 datom(2, 2, Value::Long(5), 1, true),
1152 datom(3, 2, Value::Long(9), 1, true),
1153 ],
1154 );
1155 let hits: Vec<_> = db
1156 .index_range(attr(2), Some(&Value::Long(2)), Some(&Value::Long(9)))
1157 .map(|d| d.e)
1158 .collect();
1159 assert_eq!(hits, vec![entity(2)]);
1160 }
1161
1162 fn timed_sample() -> Db {
1165 Db::new(schema())
1166 .with_transaction_at(
1167 1,
1168 1_000,
1169 &[datom(1, 1, Value::Str("alice".into()), 1, true)],
1170 )
1171 .with_transaction_at(
1172 2,
1173 2_000,
1174 &[
1175 datom(1, 1, Value::Str("alice".into()), 2, false),
1176 datom(1, 1, Value::Str("alicia".into()), 2, true),
1177 ],
1178 )
1179 }
1180
1181 #[test]
1182 fn commit_instants_are_recorded_in_both_directions() {
1183 let db = timed_sample();
1184 assert_eq!(db.tx_instant(1), Some(1_000));
1185 assert_eq!(db.tx_instant(2), Some(2_000));
1186 assert_eq!(db.tx_instant(3), None);
1187 assert_eq!(db.t_at_instant(1_000), 1);
1189 assert_eq!(db.t_at_instant(999), 0);
1190 assert_eq!(db.t_at_instant(1_999), 1);
1191 assert_eq!(db.t_at_instant(9_999), 2);
1192 }
1193
1194 #[test]
1195 fn instant_named_views_match_their_basis_named_equivalents() {
1196 let db = timed_sample();
1197 assert_eq!(db.as_of_instant(1_500).datoms(), db.as_of(1).datoms());
1198 assert_eq!(db.since_instant(1_500).datoms(), db.since(1).datoms());
1199 assert!(db.as_of_instant(0).datoms().is_empty());
1202 assert_eq!(db.since_instant(0).datoms(), db.since(0).datoms());
1203 }
1204
1205 #[test]
1206 fn transaction_time_is_queryable_data() {
1207 let db = timed_sample();
1208 let instants: Vec<_> = db
1209 .datoms_for_attribute(bootstrap::TX_INSTANT)
1210 .map(|datom| (datom.e, datom.v.clone()))
1211 .collect();
1212 assert_eq!(
1213 instants,
1214 vec![
1215 (tx_entity(1), Value::Instant(1_000)),
1216 (tx_entity(2), Value::Instant(2_000)),
1217 ]
1218 );
1219 assert!(avet_covered(db.schema(), bootstrap::TX_INSTANT));
1221 }
1222
1223 #[test]
1224 fn derived_views_keep_the_whole_transaction_time_correspondence() {
1225 let db = timed_sample();
1229 for view in [db.as_of(1), db.since(2), db.history()] {
1230 assert_eq!(view.tx_instant(1), Some(1_000));
1231 assert_eq!(view.t_at_instant(2_500), 2);
1232 }
1233 }
1234
1235 #[test]
1236 fn an_instant_supplied_with_the_datoms_wins_over_the_replayed_one() {
1237 let db = Db::new(schema()).with_transaction_at(
1240 1,
1241 5_000,
1242 &[
1243 datom(1, 1, Value::Str("alice".into()), 1, true),
1244 bootstrap::tx_instant_datom(1, 1_000),
1245 ],
1246 );
1247 assert_eq!(db.values(tx_entity(1), bootstrap::TX_INSTANT).len(), 1);
1248 assert_eq!(db.tx_instant(1), Some(1_000));
1249 }
1250
1251 #[test]
1252 fn snapshot_ignores_tx_instant_values_on_non_transaction_entities() {
1253 let user = entity(1);
1254 let db = Db::from_current_snapshot(
1255 1,
1256 schema(),
1257 Idents::default(),
1258 KeywordInterner::default(),
1259 vec![
1260 bootstrap::tx_instant_datom(1, 1_000),
1261 Datom {
1262 e: user,
1263 a: bootstrap::TX_INSTANT,
1264 v: Value::Instant(9_000),
1265 tx: tx_entity(1),
1266 added: true,
1267 },
1268 ],
1269 );
1270 assert_eq!(db.tx_instant(1), Some(1_000));
1271 }
1272
1273 #[test]
1274 fn tx_range_groups_by_transaction() {
1275 let ranged = sample().tx_range(2, None);
1276 assert_eq!(ranged.len(), 1);
1277 assert_eq!(ranged[0].0, 2);
1278 assert_eq!(ranged[0].1.len(), 3);
1279 }
1280
1281 #[test]
1282 fn incremental_indexes_match_rebuilt_indexes() {
1283 let base = Db::new(schema());
1284 let tx1 = [datom(1, 1, Value::Str("a".into()), 1, true)];
1285 let tx2 = [
1286 datom(1, 1, Value::Str("a".into()), 2, false),
1287 datom(1, 1, Value::Str("b".into()), 2, true),
1288 ];
1289 let warm = base.with_transaction(1, &tx1);
1291 let _ = warm.datoms();
1292 let incremental = warm.with_transaction(2, &tx2);
1293 let cold = base.with_transaction(1, &tx1).with_transaction(2, &tx2);
1294 assert_eq!(incremental.datoms(), cold.datoms());
1295 }
1296
1297 #[test]
1298 fn with_transaction_leaves_parent_value_unchanged() {
1299 let parent = Db::new(schema())
1303 .with_transaction(1, &[datom(1, 1, Value::Str("alice".into()), 1, true)]);
1304 let parent_datoms = parent.datoms();
1306 let parent_recorded = parent.recorded_len();
1307
1308 let child = parent.with_transaction(
1309 2,
1310 &[
1311 datom(1, 1, Value::Str("alice".into()), 2, false),
1312 datom(1, 1, Value::Str("alicia".into()), 2, true),
1313 ],
1314 );
1315
1316 assert_eq!(parent.basis_t(), 1);
1318 assert_eq!(parent.recorded_len(), parent_recorded);
1319 assert_eq!(parent.datoms(), parent_datoms);
1320 assert_eq!(
1321 parent.values(entity(1), attr(1)),
1322 vec![Value::Str("alice".into())]
1323 );
1324 assert_eq!(child.basis_t(), 2);
1326 assert_eq!(
1327 child.values(entity(1), attr(1)),
1328 vec![Value::Str("alicia".into())]
1329 );
1330 }
1331
1332 #[test]
1333 fn planner_stats_count_attributes() {
1334 let stats_owner = sample();
1335 let stats = stats_owner.planner_stats();
1336 assert_eq!(stats.total_datoms, 3);
1337 assert_eq!(stats.per_attr[&attr(1)].count, 1);
1338 assert!(stats.estimate(false, Some(attr(1)), false) <= stats.total_datoms);
1339 }
1340}