1pub mod bootstrap;
15
16use std::collections::{BTreeMap, BTreeSet};
17use std::sync::{Arc, OnceLock};
18
19use corium_core::{
20 AttrId, Cardinality, Datom, EntityId, IndexOrder, Keyword, KeywordInterner, Partition, Schema,
21 Unique, Value, encoding::Encodable,
22};
23use rpds::{RedBlackTreeMapSync, VectorSync};
24
25pub const FIRST_USER_ID: u64 = 1_000;
27
28#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
30pub enum DbView {
31 #[default]
33 Current,
34 AsOf(u64),
36 Since(u64),
38 History,
40}
41
42#[derive(Clone, Debug, Default)]
44pub struct Idents {
45 by_keyword: BTreeMap<Keyword, EntityId>,
46 by_id: BTreeMap<EntityId, Keyword>,
47}
48
49impl Idents {
50 pub fn insert(&mut self, keyword: Keyword, id: EntityId) {
52 self.by_id.insert(id, keyword.clone());
53 self.by_keyword.insert(keyword, id);
54 }
55
56 #[must_use]
58 pub fn entid(&self, keyword: &Keyword) -> Option<EntityId> {
59 self.by_keyword.get(keyword).copied()
60 }
61
62 #[must_use]
64 pub fn ident(&self, id: EntityId) -> Option<&Keyword> {
65 self.by_id.get(&id)
66 }
67
68 pub fn iter(&self) -> impl Iterator<Item = (&Keyword, &EntityId)> {
70 self.by_keyword.iter()
71 }
72}
73
74#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
76pub struct AttrStats {
77 pub count: usize,
79 pub distinct_values: usize,
81 pub distinct_entities: usize,
83}
84
85#[derive(Clone, Debug, Default)]
87pub struct PlannerStats {
88 pub per_attr: BTreeMap<AttrId, AttrStats>,
90 pub total_datoms: usize,
92 pub entity_count: usize,
94}
95
96impl PlannerStats {
97 #[must_use]
99 pub fn estimate(&self, e_bound: bool, a: Option<AttrId>, v_bound: bool) -> usize {
100 let attr = a.and_then(|a| self.per_attr.get(&a));
101 match (e_bound, attr) {
102 (true, Some(stats)) => (stats.count / stats.distinct_entities.max(1)).max(1),
104 (true, None) => (self.total_datoms / self.entity_count.max(1)).max(1),
105 (false, Some(stats)) if v_bound => (stats.count / stats.distinct_values.max(1)).max(1),
106 (false, Some(stats)) => stats.count.max(1),
107 (false, None) if a.is_some() => 1,
109 (false, None) => self.total_datoms.max(1),
110 }
111 }
112}
113
114type Index = RedBlackTreeMapSync<Vec<u8>, Datom>;
123
124const ORDERS: [IndexOrder; 4] = [
125 IndexOrder::Eavt,
126 IndexOrder::Aevt,
127 IndexOrder::Avet,
128 IndexOrder::Vaet,
129];
130
131const fn slot(order: IndexOrder) -> usize {
132 match order {
133 IndexOrder::Eavt => 0,
134 IndexOrder::Aevt => 1,
135 IndexOrder::Avet => 2,
136 IndexOrder::Vaet => 3,
137 }
138}
139
140#[must_use]
145pub fn key_prefix(
146 order: IndexOrder,
147 e: Option<EntityId>,
148 a: Option<AttrId>,
149 v: Option<&Value>,
150) -> Vec<u8> {
151 enum C {
152 E,
153 A,
154 V,
155 }
156 let components = match order {
157 IndexOrder::Eavt => [C::E, C::A, C::V],
158 IndexOrder::Aevt => [C::A, C::E, C::V],
159 IndexOrder::Avet => [C::A, C::V, C::E],
160 IndexOrder::Vaet => [C::V, C::A, C::E],
161 };
162 let mut out = Vec::new();
163 for component in components {
164 match component {
165 C::E => match e {
166 Some(e) => e.encode_into(&mut out),
167 None => break,
168 },
169 C::A => match a {
170 Some(a) => a.encode_into(&mut out),
171 None => break,
172 },
173 C::V => match v {
174 Some(v) => v.encode_into(&mut out),
175 None => break,
176 },
177 }
178 }
179 out
180}
181
182#[derive(Clone, Debug, Default)]
194pub struct TxInstants {
195 by_t: RedBlackTreeMapSync<u64, i64>,
196 by_instant: RedBlackTreeMapSync<i64, u64>,
197}
198
199impl TxInstants {
200 fn record(&mut self, t: u64, instant: i64) {
201 self.by_t.insert_mut(t, instant);
202 if self.by_instant.get(&instant).is_none_or(|held| *held < t) {
207 self.by_instant.insert_mut(instant, t);
208 }
209 }
210
211 #[must_use]
213 pub fn instant(&self, t: u64) -> Option<i64> {
214 self.by_t.get(&t).copied()
215 }
216
217 #[must_use]
220 pub fn t_at(&self, instant: i64) -> u64 {
221 self.by_instant
222 .range(..=instant)
223 .next_back()
224 .map_or(0, |(_, t)| *t)
225 }
226
227 #[must_use]
229 pub fn len(&self) -> usize {
230 self.by_t.size()
231 }
232
233 #[must_use]
235 pub fn is_empty(&self) -> bool {
236 self.by_t.is_empty()
237 }
238}
239
240#[derive(Clone, Debug, Default)]
242pub struct Db {
243 basis_t: u64,
244 schema: Schema,
245 recorded: VectorSync<Datom>,
246 idents: Arc<Idents>,
247 interner: Arc<KeywordInterner>,
248 view: DbView,
249 instants: TxInstants,
250 indexes: Arc<OnceLock<[Index; 4]>>,
251 stats: Arc<OnceLock<PlannerStats>>,
252}
253
254impl Db {
255 #[must_use]
261 pub fn new(mut schema: Schema) -> Self {
262 bootstrap::install_schema(&mut schema);
263 Self {
264 schema,
265 ..Self::default()
266 }
267 }
268
269 #[must_use]
283 pub fn from_current_snapshot(
284 basis_t: u64,
285 mut schema: Schema,
286 mut idents: Idents,
287 interner: KeywordInterner,
288 datoms: Vec<Datom>,
289 ) -> Self {
290 bootstrap::install(&mut schema, &mut idents);
291 let mut instants = TxInstants::default();
292 for datom in &datoms {
293 if let Datom {
294 e,
295 a,
296 v: Value::Instant(instant),
297 tx,
298 added: true,
299 ..
300 } = datom
301 && *a == bootstrap::TX_INSTANT
302 && *e == *tx
303 && tx.partition() == Partition::Tx as u32
304 {
305 instants.record(tx.sequence(), *instant);
306 }
307 }
308 Self {
309 basis_t,
310 schema,
311 recorded: datoms.into_iter().collect(),
312 idents: Arc::new(idents),
313 interner: Arc::new(interner),
314 view: DbView::Current,
315 instants,
316 indexes: Arc::new(OnceLock::new()),
317 stats: Arc::new(OnceLock::new()),
318 }
319 }
320
321 #[must_use]
323 pub fn with_naming(mut self, mut idents: Idents, interner: KeywordInterner) -> Self {
324 bootstrap::install(&mut self.schema, &mut idents);
325 self.idents = Arc::new(idents);
326 self.interner = Arc::new(interner);
327 self
328 }
329
330 #[must_use]
332 pub const fn basis_t(&self) -> u64 {
333 self.basis_t
334 }
335
336 #[must_use]
338 pub const fn schema(&self) -> &Schema {
339 &self.schema
340 }
341
342 #[must_use]
344 pub fn idents(&self) -> &Idents {
345 &self.idents
346 }
347
348 #[must_use]
350 pub fn interner(&self) -> &KeywordInterner {
351 &self.interner
352 }
353
354 #[must_use]
356 pub const fn view(&self) -> DbView {
357 self.view
358 }
359
360 pub fn recorded_datoms(&self) -> impl Iterator<Item = &Datom> {
362 self.recorded.iter()
363 }
364
365 #[must_use]
367 pub fn recorded_len(&self) -> usize {
368 self.recorded.len()
369 }
370
371 #[must_use]
373 pub fn as_of(&self, t: u64) -> Self {
374 self.with_view(DbView::AsOf(t))
375 }
376
377 #[must_use]
379 pub fn since(&self, t: u64) -> Self {
380 self.with_view(DbView::Since(t))
381 }
382
383 #[must_use]
385 pub fn history(&self) -> Self {
386 self.with_view(DbView::History)
387 }
388
389 #[must_use]
391 pub const fn instants(&self) -> &TxInstants {
392 &self.instants
393 }
394
395 #[must_use]
397 pub fn tx_instant(&self, t: u64) -> Option<i64> {
398 self.instants.instant(t)
399 }
400
401 #[must_use]
408 pub fn t_at_instant(&self, instant: i64) -> u64 {
409 self.instants.t_at(instant)
410 }
411
412 #[must_use]
416 pub fn as_of_instant(&self, instant: i64) -> Self {
417 self.as_of(self.t_at_instant(instant))
418 }
419
420 #[must_use]
424 pub fn since_instant(&self, instant: i64) -> Self {
425 self.since(self.t_at_instant(instant))
426 }
427
428 fn with_view(&self, view: DbView) -> Self {
429 if view == self.view {
430 return self.clone();
431 }
432 Self {
433 view,
434 indexes: Arc::new(OnceLock::new()),
435 stats: Arc::new(OnceLock::new()),
436 ..self.clone()
437 }
438 }
439
440 #[must_use]
442 pub fn tx_range(&self, start: u64, end: Option<u64>) -> Vec<(u64, Vec<Datom>)> {
443 let mut by_t: BTreeMap<u64, Vec<Datom>> = BTreeMap::new();
444 for datom in &self.recorded {
445 let t = datom.tx.sequence();
446 if t >= start && end.is_none_or(|end| t < end) {
447 by_t.entry(t).or_default().push(datom.clone());
448 }
449 }
450 by_t.into_iter().collect()
451 }
452
453 #[must_use]
455 pub fn datoms(&self) -> Vec<Datom> {
456 self.datoms_at(IndexOrder::Eavt).cloned().collect()
457 }
458
459 pub fn datoms_at(&self, order: IndexOrder) -> impl Iterator<Item = &Datom> {
464 self.indexes()[slot(order)].values()
465 }
466
467 pub fn datoms_for_attribute(&self, a: AttrId) -> impl Iterator<Item = &Datom> {
473 let prefix = key_prefix(IndexOrder::Aevt, None, Some(a), None);
474 self.indexes()[slot(IndexOrder::Aevt)]
475 .range(prefix.clone()..)
476 .take_while(move |(key, _)| key.starts_with(&prefix))
477 .map(|(_, datom)| datom)
478 }
479
480 pub fn datoms_prefix<'a>(
482 &'a self,
483 order: IndexOrder,
484 prefix: &'a [u8],
485 ) -> impl Iterator<Item = &'a Datom> {
486 self.indexes()[slot(order)]
487 .range(prefix.to_vec()..)
488 .take_while(move |(key, _)| key.starts_with(prefix))
489 .map(|(_, datom)| datom)
490 }
491
492 pub fn seek_datoms<'a>(
494 &'a self,
495 order: IndexOrder,
496 start: &[u8],
497 ) -> impl Iterator<Item = &'a Datom> {
498 self.indexes()[slot(order)]
499 .range(start.to_vec()..)
500 .map(|(_, datom)| datom)
501 }
502
503 pub fn index_range<'a>(
507 &'a self,
508 a: AttrId,
509 start: Option<&Value>,
510 end: Option<&'a Value>,
511 ) -> impl Iterator<Item = &'a Datom> {
512 let a_prefix = key_prefix(IndexOrder::Avet, None, Some(a), None);
513 let start_key = key_prefix(IndexOrder::Avet, None, Some(a), start);
514 self.indexes()[slot(IndexOrder::Avet)]
515 .range(start_key..)
516 .take_while(move |(key, _)| key.starts_with(&a_prefix))
517 .map(|(_, datom)| datom)
518 .take_while(move |datom| end.is_none_or(|end| datom.v < *end))
519 }
520
521 #[must_use]
523 pub fn values(&self, e: EntityId, a: AttrId) -> Vec<Value> {
524 let prefix = key_prefix(IndexOrder::Eavt, Some(e), Some(a), None);
525 self.datoms_prefix(IndexOrder::Eavt, &prefix)
526 .map(|datom| datom.v.clone())
527 .collect()
528 }
529
530 #[must_use]
532 pub fn lookup(&self, a: AttrId, v: &Value) -> Option<EntityId> {
533 if avet_covered(&self.schema, a) {
534 let prefix = key_prefix(IndexOrder::Avet, None, Some(a), Some(v));
535 self.datoms_prefix(IndexOrder::Avet, &prefix)
536 .next()
537 .map(|datom| datom.e)
538 } else {
539 let prefix = key_prefix(IndexOrder::Aevt, None, Some(a), None);
540 self.datoms_prefix(IndexOrder::Aevt, &prefix)
541 .find(|datom| datom.v == *v)
542 .map(|datom| datom.e)
543 }
544 }
545
546 #[must_use]
554 pub fn with_transaction(&self, t: u64, datoms: &[Datom]) -> Self {
555 match bootstrap::asserted_instant(t, datoms) {
556 Some(instant) => self.with_transaction_at(t, instant, datoms),
557 None => self.apply_transaction(t, datoms),
558 }
559 }
560
561 #[must_use]
568 pub fn with_transaction_at(&self, t: u64, instant: i64, datoms: &[Datom]) -> Self {
569 let asserted = bootstrap::asserted_instant(t, datoms);
570 let mut next = if asserted.is_some() {
571 self.apply_transaction(t, datoms)
572 } else {
573 let mut with_instant = Vec::with_capacity(datoms.len() + 1);
574 with_instant.extend_from_slice(datoms);
575 with_instant.push(bootstrap::tx_instant_datom(t, instant));
576 self.apply_transaction(t, &with_instant)
577 };
578 next.instants.record(t, asserted.unwrap_or(instant));
579 next
580 }
581
582 fn apply_transaction(&self, t: u64, datoms: &[Datom]) -> Self {
583 debug_assert!(
584 self.view == DbView::Current,
585 "with_transaction applies only to the current view"
586 );
587 let mut next = self.clone();
588 next.basis_t = t;
589 for datom in datoms {
594 next.recorded.push_back_mut(datom.clone());
595 }
596 next.indexes = Arc::new(OnceLock::new());
597 next.stats = Arc::new(OnceLock::new());
598 if let Some(parent) = self.indexes.get() {
601 let mut derived = parent.clone();
602 apply_current(&mut derived, datoms.iter(), &self.schema);
603 let _ = next.indexes.set(derived);
604 }
605 next
606 }
607
608 #[must_use]
610 pub fn stats(&self) -> DbStats {
611 let planner = self.planner_stats();
612 DbStats {
613 datoms: planner.total_datoms,
614 entities: planner.entity_count,
615 attributes: planner.per_attr.len(),
616 }
617 }
618
619 #[must_use]
621 pub fn planner_stats(&self) -> &PlannerStats {
622 self.stats.get_or_init(|| {
623 let mut stats = PlannerStats::default();
624 let mut values: BTreeMap<AttrId, BTreeSet<&Value>> = BTreeMap::new();
625 let mut attr_entities: BTreeMap<AttrId, BTreeSet<EntityId>> = BTreeMap::new();
626 let mut entities: BTreeSet<EntityId> = BTreeSet::new();
627 for datom in self.datoms_at(IndexOrder::Eavt) {
628 stats.total_datoms += 1;
629 stats.per_attr.entry(datom.a).or_default().count += 1;
630 values.entry(datom.a).or_default().insert(&datom.v);
631 attr_entities.entry(datom.a).or_default().insert(datom.e);
632 entities.insert(datom.e);
633 }
634 for (a, entry) in &mut stats.per_attr {
635 entry.distinct_values = values.get(a).map_or(0, BTreeSet::len);
636 entry.distinct_entities = attr_entities.get(a).map_or(0, BTreeSet::len);
637 }
638 stats.entity_count = entities.len();
639 stats
640 })
641 }
642
643 fn indexes(&self) -> &[Index; 4] {
644 self.indexes.get_or_init(|| {
645 let mut indexes: [Index; 4] = Default::default();
646 match self.view {
647 DbView::History => {
648 for datom in &self.recorded {
649 if self.schema.get(datom.a).is_some_and(|a| a.no_history) {
650 continue;
651 }
652 insert_datom(&mut indexes, datom, &self.schema, true);
653 }
654 }
655 DbView::Current | DbView::AsOf(_) | DbView::Since(_) => {
656 let cutoff = match self.view {
657 DbView::AsOf(t) => Some(t),
658 _ => None,
659 };
660 let filtered = self
661 .recorded
662 .iter()
663 .filter(|d| cutoff.is_none_or(|t| d.tx.sequence() <= t));
664 apply_current(&mut indexes, filtered, &self.schema);
665 if let DbView::Since(t) = self.view {
666 for index in &mut indexes {
667 *index = index
668 .iter()
669 .filter(|(_, datom)| datom.tx.sequence() > t)
670 .map(|(key, datom)| (key.clone(), datom.clone()))
671 .collect();
672 }
673 }
674 }
675 }
676 indexes
677 })
678 }
679}
680
681fn apply_current<'a>(
687 indexes: &mut [Index; 4],
688 datoms: impl Iterator<Item = &'a Datom>,
689 schema: &Schema,
690) {
691 for datom in datoms {
692 if datom.added {
693 insert_datom(indexes, datom, schema, false);
694 } else {
695 for order in ORDERS {
696 if covered(schema, order, datom) {
697 let key = key_prefix(order, Some(datom.e), Some(datom.a), Some(&datom.v));
698 indexes[slot(order)].remove_mut(&key);
699 }
700 }
701 }
702 }
703}
704
705fn insert_datom(indexes: &mut [Index; 4], datom: &Datom, schema: &Schema, with_tx: bool) {
706 for order in ORDERS {
707 if covered(schema, order, datom) {
708 let key = if with_tx {
709 datom.key(order)
710 } else {
711 key_prefix(order, Some(datom.e), Some(datom.a), Some(&datom.v))
712 };
713 indexes[slot(order)].insert_mut(key, datom.clone());
714 }
715 }
716}
717
718fn covered(schema: &Schema, order: IndexOrder, datom: &Datom) -> bool {
719 match order {
720 IndexOrder::Eavt | IndexOrder::Aevt => true,
721 IndexOrder::Avet => avet_covered(schema, datom.a),
722 IndexOrder::Vaet => matches!(datom.v, Value::Ref(_)),
723 }
724}
725
726#[must_use]
728pub fn avet_covered(schema: &Schema, a: AttrId) -> bool {
729 schema
730 .get(a)
731 .is_some_and(|attr| attr.indexed || attr.unique.is_some())
732}
733
734#[derive(Clone, Copy, Debug, Eq, PartialEq)]
736pub struct DbStats {
737 pub datoms: usize,
739 pub entities: usize,
741 pub attributes: usize,
743}
744
745#[must_use]
747pub const fn attribute(
748 id: u64,
749 value_type: corium_core::ValueType,
750 cardinality: Cardinality,
751 unique: Option<Unique>,
752) -> corium_core::Attribute {
753 corium_core::Attribute {
754 id: EntityId::new(Partition::Db as u32, id),
755 value_type,
756 cardinality,
757 unique,
758 is_component: false,
759 indexed: unique.is_some(),
760 no_history: false,
761 }
762}
763
764#[cfg(test)]
765mod tests {
766 use super::*;
767 use corium_core::ValueType;
768
769 fn schema() -> Schema {
770 let mut schema = Schema::default();
771 schema.insert(attribute(1, ValueType::Str, Cardinality::One, None));
772 schema.insert(attribute(
773 2,
774 ValueType::Long,
775 Cardinality::One,
776 Some(Unique::Identity),
777 ));
778 schema.insert(attribute(3, ValueType::Ref, Cardinality::Many, None));
779 schema
780 }
781
782 fn attr(id: u64) -> AttrId {
783 EntityId::new(Partition::Db as u32, id)
784 }
785
786 fn entity(id: u64) -> EntityId {
787 EntityId::new(Partition::User as u32, id)
788 }
789
790 fn tx_entity(t: u64) -> EntityId {
791 EntityId::new(Partition::Tx as u32, t)
792 }
793
794 fn datom(e: u64, a: u64, v: Value, t: u64, added: bool) -> Datom {
795 Datom {
796 e: entity(e),
797 a: attr(a),
798 v,
799 tx: tx_entity(t),
800 added,
801 }
802 }
803
804 fn sample() -> Db {
805 Db::new(schema())
806 .with_transaction(
807 1,
808 &[
809 datom(1, 1, Value::Str("alice".into()), 1, true),
810 datom(1, 2, Value::Long(7), 1, true),
811 ],
812 )
813 .with_transaction(
814 2,
815 &[
816 datom(1, 1, Value::Str("alice".into()), 2, false),
817 datom(1, 1, Value::Str("alicia".into()), 2, true),
818 datom(2, 3, Value::Ref(entity(1)), 2, true),
819 ],
820 )
821 }
822
823 #[test]
824 fn current_view_folds_retractions() {
825 let db = sample();
826 assert_eq!(
827 db.values(entity(1), attr(1)),
828 vec![Value::Str("alicia".into())]
829 );
830 assert_eq!(db.stats().datoms, 3);
831 }
832
833 #[test]
834 fn attribute_scan_uses_complete_aevt_coverage() {
835 let db = sample();
836 let datoms = db.datoms_for_attribute(attr(1)).collect::<Vec<_>>();
837 assert_eq!(datoms.len(), 1);
838 assert_eq!(datoms[0].v, Value::Str("alicia".into()));
839 assert!(datoms.iter().all(|datom| datom.a == attr(1)));
840 }
841
842 #[test]
843 fn as_of_reconstructs_past_basis() {
844 let db = sample().as_of(1);
845 assert_eq!(
846 db.values(entity(1), attr(1)),
847 vec![Value::Str("alice".into())]
848 );
849 assert_eq!(db.stats().datoms, 2);
850 }
851
852 #[test]
853 fn since_excludes_older_live_facts() {
854 let db = sample().since(1);
855 assert_eq!(db.values(entity(1), attr(2)), Vec::<Value>::new());
857 assert_eq!(
858 db.values(entity(1), attr(1)),
859 vec![Value::Str("alicia".into())]
860 );
861 }
862
863 #[test]
864 fn history_exposes_assertions_and_retractions() {
865 let db = sample().history();
866 let names: Vec<_> = db
867 .datoms_prefix(
868 IndexOrder::Eavt,
869 &key_prefix(IndexOrder::Eavt, Some(entity(1)), Some(attr(1)), None),
870 )
871 .map(|d| (d.v.clone(), d.added))
872 .collect();
873 assert_eq!(names.len(), 3);
874 assert!(names.contains(&(Value::Str("alice".into()), false)));
875 }
876
877 #[test]
878 fn avet_only_covers_indexed_attributes() {
879 let db = sample();
880 assert_eq!(db.datoms_at(IndexOrder::Avet).count(), 1);
881 assert_eq!(db.datoms_at(IndexOrder::Vaet).count(), 1);
882 }
883
884 #[test]
885 fn index_range_scans_value_bounds() {
886 let db = Db::new(schema()).with_transaction(
887 1,
888 &[
889 datom(1, 2, Value::Long(1), 1, true),
890 datom(2, 2, Value::Long(5), 1, true),
891 datom(3, 2, Value::Long(9), 1, true),
892 ],
893 );
894 let hits: Vec<_> = db
895 .index_range(attr(2), Some(&Value::Long(2)), Some(&Value::Long(9)))
896 .map(|d| d.e)
897 .collect();
898 assert_eq!(hits, vec![entity(2)]);
899 }
900
901 fn timed_sample() -> Db {
904 Db::new(schema())
905 .with_transaction_at(
906 1,
907 1_000,
908 &[datom(1, 1, Value::Str("alice".into()), 1, true)],
909 )
910 .with_transaction_at(
911 2,
912 2_000,
913 &[
914 datom(1, 1, Value::Str("alice".into()), 2, false),
915 datom(1, 1, Value::Str("alicia".into()), 2, true),
916 ],
917 )
918 }
919
920 #[test]
921 fn commit_instants_are_recorded_in_both_directions() {
922 let db = timed_sample();
923 assert_eq!(db.tx_instant(1), Some(1_000));
924 assert_eq!(db.tx_instant(2), Some(2_000));
925 assert_eq!(db.tx_instant(3), None);
926 assert_eq!(db.t_at_instant(1_000), 1);
928 assert_eq!(db.t_at_instant(999), 0);
929 assert_eq!(db.t_at_instant(1_999), 1);
930 assert_eq!(db.t_at_instant(9_999), 2);
931 }
932
933 #[test]
934 fn instant_named_views_match_their_basis_named_equivalents() {
935 let db = timed_sample();
936 assert_eq!(db.as_of_instant(1_500).datoms(), db.as_of(1).datoms());
937 assert_eq!(db.since_instant(1_500).datoms(), db.since(1).datoms());
938 assert!(db.as_of_instant(0).datoms().is_empty());
941 assert_eq!(db.since_instant(0).datoms(), db.since(0).datoms());
942 }
943
944 #[test]
945 fn transaction_time_is_queryable_data() {
946 let db = timed_sample();
947 let instants: Vec<_> = db
948 .datoms_for_attribute(bootstrap::TX_INSTANT)
949 .map(|datom| (datom.e, datom.v.clone()))
950 .collect();
951 assert_eq!(
952 instants,
953 vec![
954 (tx_entity(1), Value::Instant(1_000)),
955 (tx_entity(2), Value::Instant(2_000)),
956 ]
957 );
958 assert!(avet_covered(db.schema(), bootstrap::TX_INSTANT));
960 }
961
962 #[test]
963 fn derived_views_keep_the_whole_transaction_time_correspondence() {
964 let db = timed_sample();
968 for view in [db.as_of(1), db.since(2), db.history()] {
969 assert_eq!(view.tx_instant(1), Some(1_000));
970 assert_eq!(view.t_at_instant(2_500), 2);
971 }
972 }
973
974 #[test]
975 fn an_instant_supplied_with_the_datoms_wins_over_the_replayed_one() {
976 let db = Db::new(schema()).with_transaction_at(
979 1,
980 5_000,
981 &[
982 datom(1, 1, Value::Str("alice".into()), 1, true),
983 bootstrap::tx_instant_datom(1, 1_000),
984 ],
985 );
986 assert_eq!(db.values(tx_entity(1), bootstrap::TX_INSTANT).len(), 1);
987 assert_eq!(db.tx_instant(1), Some(1_000));
988 }
989
990 #[test]
991 fn snapshot_ignores_tx_instant_values_on_non_transaction_entities() {
992 let user = entity(1);
993 let db = Db::from_current_snapshot(
994 1,
995 schema(),
996 Idents::default(),
997 KeywordInterner::default(),
998 vec![
999 bootstrap::tx_instant_datom(1, 1_000),
1000 Datom {
1001 e: user,
1002 a: bootstrap::TX_INSTANT,
1003 v: Value::Instant(9_000),
1004 tx: tx_entity(1),
1005 added: true,
1006 },
1007 ],
1008 );
1009 assert_eq!(db.tx_instant(1), Some(1_000));
1010 }
1011
1012 #[test]
1013 fn tx_range_groups_by_transaction() {
1014 let ranged = sample().tx_range(2, None);
1015 assert_eq!(ranged.len(), 1);
1016 assert_eq!(ranged[0].0, 2);
1017 assert_eq!(ranged[0].1.len(), 3);
1018 }
1019
1020 #[test]
1021 fn incremental_indexes_match_rebuilt_indexes() {
1022 let base = Db::new(schema());
1023 let tx1 = [datom(1, 1, Value::Str("a".into()), 1, true)];
1024 let tx2 = [
1025 datom(1, 1, Value::Str("a".into()), 2, false),
1026 datom(1, 1, Value::Str("b".into()), 2, true),
1027 ];
1028 let warm = base.with_transaction(1, &tx1);
1030 let _ = warm.datoms();
1031 let incremental = warm.with_transaction(2, &tx2);
1032 let cold = base.with_transaction(1, &tx1).with_transaction(2, &tx2);
1033 assert_eq!(incremental.datoms(), cold.datoms());
1034 }
1035
1036 #[test]
1037 fn with_transaction_leaves_parent_value_unchanged() {
1038 let parent = Db::new(schema())
1042 .with_transaction(1, &[datom(1, 1, Value::Str("alice".into()), 1, true)]);
1043 let parent_datoms = parent.datoms();
1045 let parent_recorded = parent.recorded_len();
1046
1047 let child = parent.with_transaction(
1048 2,
1049 &[
1050 datom(1, 1, Value::Str("alice".into()), 2, false),
1051 datom(1, 1, Value::Str("alicia".into()), 2, true),
1052 ],
1053 );
1054
1055 assert_eq!(parent.basis_t(), 1);
1057 assert_eq!(parent.recorded_len(), parent_recorded);
1058 assert_eq!(parent.datoms(), parent_datoms);
1059 assert_eq!(
1060 parent.values(entity(1), attr(1)),
1061 vec![Value::Str("alice".into())]
1062 );
1063 assert_eq!(child.basis_t(), 2);
1065 assert_eq!(
1066 child.values(entity(1), attr(1)),
1067 vec![Value::Str("alicia".into())]
1068 );
1069 }
1070
1071 #[test]
1072 fn planner_stats_count_attributes() {
1073 let stats_owner = sample();
1074 let stats = stats_owner.planner_stats();
1075 assert_eq!(stats.total_datoms, 3);
1076 assert_eq!(stats.per_attr[&attr(1)].count, 1);
1077 assert!(stats.estimate(false, Some(attr(1)), false) <= stats.total_datoms);
1078 }
1079}