1use std::collections::{BTreeMap, BTreeSet};
11use std::sync::{Arc, OnceLock};
12
13use corium_core::{
14 AttrId, Cardinality, Datom, EntityId, IndexOrder, Keyword, KeywordInterner, Partition, Schema,
15 Unique, Value, encoding::Encodable,
16};
17
18pub const FIRST_USER_ID: u64 = 1_000;
20
21#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
23pub enum DbView {
24 #[default]
26 Current,
27 AsOf(u64),
29 Since(u64),
31 History,
33}
34
35#[derive(Clone, Debug, Default)]
37pub struct Idents {
38 by_keyword: BTreeMap<Keyword, EntityId>,
39 by_id: BTreeMap<EntityId, Keyword>,
40}
41
42impl Idents {
43 pub fn insert(&mut self, keyword: Keyword, id: EntityId) {
45 self.by_id.insert(id, keyword.clone());
46 self.by_keyword.insert(keyword, id);
47 }
48
49 #[must_use]
51 pub fn entid(&self, keyword: &Keyword) -> Option<EntityId> {
52 self.by_keyword.get(keyword).copied()
53 }
54
55 #[must_use]
57 pub fn ident(&self, id: EntityId) -> Option<&Keyword> {
58 self.by_id.get(&id)
59 }
60
61 pub fn iter(&self) -> impl Iterator<Item = (&Keyword, &EntityId)> {
63 self.by_keyword.iter()
64 }
65}
66
67#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
69pub struct AttrStats {
70 pub count: usize,
72 pub distinct_values: usize,
74 pub distinct_entities: usize,
76}
77
78#[derive(Clone, Debug, Default)]
80pub struct PlannerStats {
81 pub per_attr: BTreeMap<AttrId, AttrStats>,
83 pub total_datoms: usize,
85 pub entity_count: usize,
87}
88
89impl PlannerStats {
90 #[must_use]
92 pub fn estimate(&self, e_bound: bool, a: Option<AttrId>, v_bound: bool) -> usize {
93 let attr = a.and_then(|a| self.per_attr.get(&a));
94 match (e_bound, attr) {
95 (true, Some(stats)) => (stats.count / stats.distinct_entities.max(1)).max(1),
97 (true, None) => (self.total_datoms / self.entity_count.max(1)).max(1),
98 (false, Some(stats)) if v_bound => (stats.count / stats.distinct_values.max(1)).max(1),
99 (false, Some(stats)) => stats.count.max(1),
100 (false, None) if a.is_some() => 1,
102 (false, None) => self.total_datoms.max(1),
103 }
104 }
105}
106
107type Index = BTreeMap<Vec<u8>, Datom>;
108
109const ORDERS: [IndexOrder; 4] = [
110 IndexOrder::Eavt,
111 IndexOrder::Aevt,
112 IndexOrder::Avet,
113 IndexOrder::Vaet,
114];
115
116const fn slot(order: IndexOrder) -> usize {
117 match order {
118 IndexOrder::Eavt => 0,
119 IndexOrder::Aevt => 1,
120 IndexOrder::Avet => 2,
121 IndexOrder::Vaet => 3,
122 }
123}
124
125#[must_use]
130pub fn key_prefix(
131 order: IndexOrder,
132 e: Option<EntityId>,
133 a: Option<AttrId>,
134 v: Option<&Value>,
135) -> Vec<u8> {
136 enum C {
137 E,
138 A,
139 V,
140 }
141 let components = match order {
142 IndexOrder::Eavt => [C::E, C::A, C::V],
143 IndexOrder::Aevt => [C::A, C::E, C::V],
144 IndexOrder::Avet => [C::A, C::V, C::E],
145 IndexOrder::Vaet => [C::V, C::A, C::E],
146 };
147 let mut out = Vec::new();
148 for component in components {
149 match component {
150 C::E => match e {
151 Some(e) => e.encode_into(&mut out),
152 None => break,
153 },
154 C::A => match a {
155 Some(a) => a.encode_into(&mut out),
156 None => break,
157 },
158 C::V => match v {
159 Some(v) => v.encode_into(&mut out),
160 None => break,
161 },
162 }
163 }
164 out
165}
166
167#[derive(Clone, Debug, Default)]
169pub struct Db {
170 basis_t: u64,
171 schema: Schema,
172 recorded: Arc<Vec<Datom>>,
173 idents: Arc<Idents>,
174 interner: Arc<KeywordInterner>,
175 view: DbView,
176 indexes: Arc<OnceLock<[Index; 4]>>,
177 stats: Arc<OnceLock<PlannerStats>>,
178}
179
180impl Db {
181 #[must_use]
183 pub fn new(schema: Schema) -> Self {
184 Self {
185 schema,
186 ..Self::default()
187 }
188 }
189
190 #[must_use]
198 pub fn from_current_snapshot(
199 basis_t: u64,
200 schema: Schema,
201 idents: Idents,
202 interner: KeywordInterner,
203 datoms: Vec<Datom>,
204 ) -> Self {
205 Self {
206 basis_t,
207 schema,
208 recorded: Arc::new(datoms),
209 idents: Arc::new(idents),
210 interner: Arc::new(interner),
211 view: DbView::Current,
212 indexes: Arc::new(OnceLock::new()),
213 stats: Arc::new(OnceLock::new()),
214 }
215 }
216
217 #[must_use]
219 pub fn with_naming(mut self, idents: Idents, interner: KeywordInterner) -> Self {
220 self.idents = Arc::new(idents);
221 self.interner = Arc::new(interner);
222 self
223 }
224
225 #[must_use]
227 pub const fn basis_t(&self) -> u64 {
228 self.basis_t
229 }
230
231 #[must_use]
233 pub const fn schema(&self) -> &Schema {
234 &self.schema
235 }
236
237 #[must_use]
239 pub fn idents(&self) -> &Idents {
240 &self.idents
241 }
242
243 #[must_use]
245 pub fn interner(&self) -> &KeywordInterner {
246 &self.interner
247 }
248
249 #[must_use]
251 pub const fn view(&self) -> DbView {
252 self.view
253 }
254
255 #[must_use]
257 pub fn recorded_datoms(&self) -> &[Datom] {
258 &self.recorded
259 }
260
261 #[must_use]
263 pub fn as_of(&self, t: u64) -> Self {
264 self.with_view(DbView::AsOf(t))
265 }
266
267 #[must_use]
269 pub fn since(&self, t: u64) -> Self {
270 self.with_view(DbView::Since(t))
271 }
272
273 #[must_use]
275 pub fn history(&self) -> Self {
276 self.with_view(DbView::History)
277 }
278
279 fn with_view(&self, view: DbView) -> Self {
280 if view == self.view {
281 return self.clone();
282 }
283 Self {
284 view,
285 indexes: Arc::new(OnceLock::new()),
286 stats: Arc::new(OnceLock::new()),
287 ..self.clone()
288 }
289 }
290
291 #[must_use]
293 pub fn tx_range(&self, start: u64, end: Option<u64>) -> Vec<(u64, Vec<Datom>)> {
294 let mut by_t: BTreeMap<u64, Vec<Datom>> = BTreeMap::new();
295 for datom in self.recorded.iter() {
296 let t = datom.tx.sequence();
297 if t >= start && end.is_none_or(|end| t < end) {
298 by_t.entry(t).or_default().push(datom.clone());
299 }
300 }
301 by_t.into_iter().collect()
302 }
303
304 #[must_use]
306 pub fn datoms(&self) -> Vec<Datom> {
307 self.datoms_at(IndexOrder::Eavt).cloned().collect()
308 }
309
310 pub fn datoms_at(&self, order: IndexOrder) -> impl Iterator<Item = &Datom> {
315 self.indexes()[slot(order)].values()
316 }
317
318 pub fn datoms_for_attribute(&self, a: AttrId) -> impl Iterator<Item = &Datom> {
324 let prefix = key_prefix(IndexOrder::Aevt, None, Some(a), None);
325 self.indexes()[slot(IndexOrder::Aevt)]
326 .range(prefix.clone()..)
327 .take_while(move |(key, _)| key.starts_with(&prefix))
328 .map(|(_, datom)| datom)
329 }
330
331 pub fn datoms_prefix<'a>(
333 &'a self,
334 order: IndexOrder,
335 prefix: &'a [u8],
336 ) -> impl Iterator<Item = &'a Datom> {
337 self.indexes()[slot(order)]
338 .range(prefix.to_vec()..)
339 .take_while(move |(key, _)| key.starts_with(prefix))
340 .map(|(_, datom)| datom)
341 }
342
343 pub fn seek_datoms<'a>(
345 &'a self,
346 order: IndexOrder,
347 start: &[u8],
348 ) -> impl Iterator<Item = &'a Datom> {
349 self.indexes()[slot(order)]
350 .range(start.to_vec()..)
351 .map(|(_, datom)| datom)
352 }
353
354 pub fn index_range<'a>(
358 &'a self,
359 a: AttrId,
360 start: Option<&Value>,
361 end: Option<&'a Value>,
362 ) -> impl Iterator<Item = &'a Datom> {
363 let a_prefix = key_prefix(IndexOrder::Avet, None, Some(a), None);
364 let start_key = key_prefix(IndexOrder::Avet, None, Some(a), start);
365 self.indexes()[slot(IndexOrder::Avet)]
366 .range(start_key..)
367 .take_while(move |(key, _)| key.starts_with(&a_prefix))
368 .map(|(_, datom)| datom)
369 .take_while(move |datom| end.is_none_or(|end| datom.v < *end))
370 }
371
372 #[must_use]
374 pub fn values(&self, e: EntityId, a: AttrId) -> Vec<Value> {
375 let prefix = key_prefix(IndexOrder::Eavt, Some(e), Some(a), None);
376 self.datoms_prefix(IndexOrder::Eavt, &prefix)
377 .map(|datom| datom.v.clone())
378 .collect()
379 }
380
381 #[must_use]
383 pub fn lookup(&self, a: AttrId, v: &Value) -> Option<EntityId> {
384 if avet_covered(&self.schema, a) {
385 let prefix = key_prefix(IndexOrder::Avet, None, Some(a), Some(v));
386 self.datoms_prefix(IndexOrder::Avet, &prefix)
387 .next()
388 .map(|datom| datom.e)
389 } else {
390 let prefix = key_prefix(IndexOrder::Aevt, None, Some(a), None);
391 self.datoms_prefix(IndexOrder::Aevt, &prefix)
392 .find(|datom| datom.v == *v)
393 .map(|datom| datom.e)
394 }
395 }
396
397 #[must_use]
401 pub fn with_transaction(&self, t: u64, datoms: &[Datom]) -> Self {
402 debug_assert!(
403 self.view == DbView::Current,
404 "with_transaction applies only to the current view"
405 );
406 let mut next = self.clone();
407 next.basis_t = t;
408 Arc::make_mut(&mut next.recorded).extend_from_slice(datoms);
409 next.indexes = Arc::new(OnceLock::new());
410 next.stats = Arc::new(OnceLock::new());
411 if let Some(parent) = self.indexes.get() {
414 let mut derived = parent.clone();
415 apply_current(&mut derived, datoms.iter(), &self.schema);
416 let _ = next.indexes.set(derived);
417 }
418 next
419 }
420
421 #[must_use]
423 pub fn stats(&self) -> DbStats {
424 let planner = self.planner_stats();
425 DbStats {
426 datoms: planner.total_datoms,
427 entities: planner.entity_count,
428 attributes: planner.per_attr.len(),
429 }
430 }
431
432 #[must_use]
434 pub fn planner_stats(&self) -> &PlannerStats {
435 self.stats.get_or_init(|| {
436 let mut stats = PlannerStats::default();
437 let mut values: BTreeMap<AttrId, BTreeSet<&Value>> = BTreeMap::new();
438 let mut attr_entities: BTreeMap<AttrId, BTreeSet<EntityId>> = BTreeMap::new();
439 let mut entities: BTreeSet<EntityId> = BTreeSet::new();
440 for datom in self.datoms_at(IndexOrder::Eavt) {
441 stats.total_datoms += 1;
442 stats.per_attr.entry(datom.a).or_default().count += 1;
443 values.entry(datom.a).or_default().insert(&datom.v);
444 attr_entities.entry(datom.a).or_default().insert(datom.e);
445 entities.insert(datom.e);
446 }
447 for (a, entry) in &mut stats.per_attr {
448 entry.distinct_values = values.get(a).map_or(0, BTreeSet::len);
449 entry.distinct_entities = attr_entities.get(a).map_or(0, BTreeSet::len);
450 }
451 stats.entity_count = entities.len();
452 stats
453 })
454 }
455
456 fn indexes(&self) -> &[Index; 4] {
457 self.indexes.get_or_init(|| {
458 let mut indexes: [Index; 4] = Default::default();
459 match self.view {
460 DbView::History => {
461 for datom in self.recorded.iter() {
462 if self.schema.get(datom.a).is_some_and(|a| a.no_history) {
463 continue;
464 }
465 insert_datom(&mut indexes, datom, &self.schema, true);
466 }
467 }
468 DbView::Current | DbView::AsOf(_) | DbView::Since(_) => {
469 let cutoff = match self.view {
470 DbView::AsOf(t) => Some(t),
471 _ => None,
472 };
473 let filtered = self
474 .recorded
475 .iter()
476 .filter(|d| cutoff.is_none_or(|t| d.tx.sequence() <= t));
477 apply_current(&mut indexes, filtered, &self.schema);
478 if let DbView::Since(t) = self.view {
479 for index in &mut indexes {
480 index.retain(|_, datom| datom.tx.sequence() > t);
481 }
482 }
483 }
484 }
485 indexes
486 })
487 }
488}
489
490fn apply_current<'a>(
496 indexes: &mut [Index; 4],
497 datoms: impl Iterator<Item = &'a Datom>,
498 schema: &Schema,
499) {
500 for datom in datoms {
501 if datom.added {
502 insert_datom(indexes, datom, schema, false);
503 } else {
504 for order in ORDERS {
505 if covered(schema, order, datom) {
506 let key = key_prefix(order, Some(datom.e), Some(datom.a), Some(&datom.v));
507 indexes[slot(order)].remove(&key);
508 }
509 }
510 }
511 }
512}
513
514fn insert_datom(indexes: &mut [Index; 4], datom: &Datom, schema: &Schema, with_tx: bool) {
515 for order in ORDERS {
516 if covered(schema, order, datom) {
517 let key = if with_tx {
518 datom.key(order)
519 } else {
520 key_prefix(order, Some(datom.e), Some(datom.a), Some(&datom.v))
521 };
522 indexes[slot(order)].insert(key, datom.clone());
523 }
524 }
525}
526
527fn covered(schema: &Schema, order: IndexOrder, datom: &Datom) -> bool {
528 match order {
529 IndexOrder::Eavt | IndexOrder::Aevt => true,
530 IndexOrder::Avet => avet_covered(schema, datom.a),
531 IndexOrder::Vaet => matches!(datom.v, Value::Ref(_)),
532 }
533}
534
535#[must_use]
537pub fn avet_covered(schema: &Schema, a: AttrId) -> bool {
538 schema
539 .get(a)
540 .is_some_and(|attr| attr.indexed || attr.unique.is_some())
541}
542
543#[derive(Clone, Copy, Debug, Eq, PartialEq)]
545pub struct DbStats {
546 pub datoms: usize,
548 pub entities: usize,
550 pub attributes: usize,
552}
553
554#[must_use]
556pub const fn attribute(
557 id: u64,
558 value_type: corium_core::ValueType,
559 cardinality: Cardinality,
560 unique: Option<Unique>,
561) -> corium_core::Attribute {
562 corium_core::Attribute {
563 id: EntityId::new(Partition::Db as u32, id),
564 value_type,
565 cardinality,
566 unique,
567 is_component: false,
568 indexed: unique.is_some(),
569 no_history: false,
570 }
571}
572
573#[cfg(test)]
574mod tests {
575 use super::*;
576 use corium_core::ValueType;
577
578 fn schema() -> Schema {
579 let mut schema = Schema::default();
580 schema.insert(attribute(1, ValueType::Str, Cardinality::One, None));
581 schema.insert(attribute(
582 2,
583 ValueType::Long,
584 Cardinality::One,
585 Some(Unique::Identity),
586 ));
587 schema.insert(attribute(3, ValueType::Ref, Cardinality::Many, None));
588 schema
589 }
590
591 fn attr(id: u64) -> AttrId {
592 EntityId::new(Partition::Db as u32, id)
593 }
594
595 fn entity(id: u64) -> EntityId {
596 EntityId::new(Partition::User as u32, id)
597 }
598
599 fn tx_entity(t: u64) -> EntityId {
600 EntityId::new(Partition::Tx as u32, t)
601 }
602
603 fn datom(e: u64, a: u64, v: Value, t: u64, added: bool) -> Datom {
604 Datom {
605 e: entity(e),
606 a: attr(a),
607 v,
608 tx: tx_entity(t),
609 added,
610 }
611 }
612
613 fn sample() -> Db {
614 Db::new(schema())
615 .with_transaction(
616 1,
617 &[
618 datom(1, 1, Value::Str("alice".into()), 1, true),
619 datom(1, 2, Value::Long(7), 1, true),
620 ],
621 )
622 .with_transaction(
623 2,
624 &[
625 datom(1, 1, Value::Str("alice".into()), 2, false),
626 datom(1, 1, Value::Str("alicia".into()), 2, true),
627 datom(2, 3, Value::Ref(entity(1)), 2, true),
628 ],
629 )
630 }
631
632 #[test]
633 fn current_view_folds_retractions() {
634 let db = sample();
635 assert_eq!(
636 db.values(entity(1), attr(1)),
637 vec![Value::Str("alicia".into())]
638 );
639 assert_eq!(db.stats().datoms, 3);
640 }
641
642 #[test]
643 fn attribute_scan_uses_complete_aevt_coverage() {
644 let db = sample();
645 let datoms = db.datoms_for_attribute(attr(1)).collect::<Vec<_>>();
646 assert_eq!(datoms.len(), 1);
647 assert_eq!(datoms[0].v, Value::Str("alicia".into()));
648 assert!(datoms.iter().all(|datom| datom.a == attr(1)));
649 }
650
651 #[test]
652 fn as_of_reconstructs_past_basis() {
653 let db = sample().as_of(1);
654 assert_eq!(
655 db.values(entity(1), attr(1)),
656 vec![Value::Str("alice".into())]
657 );
658 assert_eq!(db.stats().datoms, 2);
659 }
660
661 #[test]
662 fn since_excludes_older_live_facts() {
663 let db = sample().since(1);
664 assert_eq!(db.values(entity(1), attr(2)), Vec::<Value>::new());
666 assert_eq!(
667 db.values(entity(1), attr(1)),
668 vec![Value::Str("alicia".into())]
669 );
670 }
671
672 #[test]
673 fn history_exposes_assertions_and_retractions() {
674 let db = sample().history();
675 let names: Vec<_> = db
676 .datoms_prefix(
677 IndexOrder::Eavt,
678 &key_prefix(IndexOrder::Eavt, Some(entity(1)), Some(attr(1)), None),
679 )
680 .map(|d| (d.v.clone(), d.added))
681 .collect();
682 assert_eq!(names.len(), 3);
683 assert!(names.contains(&(Value::Str("alice".into()), false)));
684 }
685
686 #[test]
687 fn avet_only_covers_indexed_attributes() {
688 let db = sample();
689 assert_eq!(db.datoms_at(IndexOrder::Avet).count(), 1);
690 assert_eq!(db.datoms_at(IndexOrder::Vaet).count(), 1);
691 }
692
693 #[test]
694 fn index_range_scans_value_bounds() {
695 let db = Db::new(schema()).with_transaction(
696 1,
697 &[
698 datom(1, 2, Value::Long(1), 1, true),
699 datom(2, 2, Value::Long(5), 1, true),
700 datom(3, 2, Value::Long(9), 1, true),
701 ],
702 );
703 let hits: Vec<_> = db
704 .index_range(attr(2), Some(&Value::Long(2)), Some(&Value::Long(9)))
705 .map(|d| d.e)
706 .collect();
707 assert_eq!(hits, vec![entity(2)]);
708 }
709
710 #[test]
711 fn tx_range_groups_by_transaction() {
712 let ranged = sample().tx_range(2, None);
713 assert_eq!(ranged.len(), 1);
714 assert_eq!(ranged[0].0, 2);
715 assert_eq!(ranged[0].1.len(), 3);
716 }
717
718 #[test]
719 fn incremental_indexes_match_rebuilt_indexes() {
720 let base = Db::new(schema());
721 let tx1 = [datom(1, 1, Value::Str("a".into()), 1, true)];
722 let tx2 = [
723 datom(1, 1, Value::Str("a".into()), 2, false),
724 datom(1, 1, Value::Str("b".into()), 2, true),
725 ];
726 let warm = base.with_transaction(1, &tx1);
728 let _ = warm.datoms();
729 let incremental = warm.with_transaction(2, &tx2);
730 let cold = base.with_transaction(1, &tx1).with_transaction(2, &tx2);
731 assert_eq!(incremental.datoms(), cold.datoms());
732 }
733
734 #[test]
735 fn planner_stats_count_attributes() {
736 let stats_owner = sample();
737 let stats = stats_owner.planner_stats();
738 assert_eq!(stats.total_datoms, 3);
739 assert_eq!(stats.per_attr[&attr(1)].count, 1);
740 assert!(stats.estimate(false, Some(attr(1)), false) <= stats.total_datoms);
741 }
742}