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]
192 pub fn with_naming(mut self, idents: Idents, interner: KeywordInterner) -> Self {
193 self.idents = Arc::new(idents);
194 self.interner = Arc::new(interner);
195 self
196 }
197
198 #[must_use]
200 pub const fn basis_t(&self) -> u64 {
201 self.basis_t
202 }
203
204 #[must_use]
206 pub const fn schema(&self) -> &Schema {
207 &self.schema
208 }
209
210 #[must_use]
212 pub fn idents(&self) -> &Idents {
213 &self.idents
214 }
215
216 #[must_use]
218 pub fn interner(&self) -> &KeywordInterner {
219 &self.interner
220 }
221
222 #[must_use]
224 pub const fn view(&self) -> DbView {
225 self.view
226 }
227
228 #[must_use]
230 pub fn recorded_datoms(&self) -> &[Datom] {
231 &self.recorded
232 }
233
234 #[must_use]
236 pub fn as_of(&self, t: u64) -> Self {
237 self.with_view(DbView::AsOf(t))
238 }
239
240 #[must_use]
242 pub fn since(&self, t: u64) -> Self {
243 self.with_view(DbView::Since(t))
244 }
245
246 #[must_use]
248 pub fn history(&self) -> Self {
249 self.with_view(DbView::History)
250 }
251
252 fn with_view(&self, view: DbView) -> Self {
253 if view == self.view {
254 return self.clone();
255 }
256 Self {
257 view,
258 indexes: Arc::new(OnceLock::new()),
259 stats: Arc::new(OnceLock::new()),
260 ..self.clone()
261 }
262 }
263
264 #[must_use]
266 pub fn tx_range(&self, start: u64, end: Option<u64>) -> Vec<(u64, Vec<Datom>)> {
267 let mut by_t: BTreeMap<u64, Vec<Datom>> = BTreeMap::new();
268 for datom in self.recorded.iter() {
269 let t = datom.tx.sequence();
270 if t >= start && end.is_none_or(|end| t < end) {
271 by_t.entry(t).or_default().push(datom.clone());
272 }
273 }
274 by_t.into_iter().collect()
275 }
276
277 #[must_use]
279 pub fn datoms(&self) -> Vec<Datom> {
280 self.datoms_at(IndexOrder::Eavt).cloned().collect()
281 }
282
283 pub fn datoms_at(&self, order: IndexOrder) -> impl Iterator<Item = &Datom> {
288 self.indexes()[slot(order)].values()
289 }
290
291 pub fn datoms_prefix<'a>(
293 &'a self,
294 order: IndexOrder,
295 prefix: &'a [u8],
296 ) -> impl Iterator<Item = &'a Datom> {
297 self.indexes()[slot(order)]
298 .range(prefix.to_vec()..)
299 .take_while(move |(key, _)| key.starts_with(prefix))
300 .map(|(_, datom)| datom)
301 }
302
303 pub fn seek_datoms<'a>(
305 &'a self,
306 order: IndexOrder,
307 start: &[u8],
308 ) -> impl Iterator<Item = &'a Datom> {
309 self.indexes()[slot(order)]
310 .range(start.to_vec()..)
311 .map(|(_, datom)| datom)
312 }
313
314 pub fn index_range<'a>(
318 &'a self,
319 a: AttrId,
320 start: Option<&Value>,
321 end: Option<&'a Value>,
322 ) -> impl Iterator<Item = &'a Datom> {
323 let a_prefix = key_prefix(IndexOrder::Avet, None, Some(a), None);
324 let start_key = key_prefix(IndexOrder::Avet, None, Some(a), start);
325 self.indexes()[slot(IndexOrder::Avet)]
326 .range(start_key..)
327 .take_while(move |(key, _)| key.starts_with(&a_prefix))
328 .map(|(_, datom)| datom)
329 .take_while(move |datom| end.is_none_or(|end| datom.v < *end))
330 }
331
332 #[must_use]
334 pub fn values(&self, e: EntityId, a: AttrId) -> Vec<Value> {
335 let prefix = key_prefix(IndexOrder::Eavt, Some(e), Some(a), None);
336 self.datoms_prefix(IndexOrder::Eavt, &prefix)
337 .map(|datom| datom.v.clone())
338 .collect()
339 }
340
341 #[must_use]
343 pub fn lookup(&self, a: AttrId, v: &Value) -> Option<EntityId> {
344 if avet_covered(&self.schema, a) {
345 let prefix = key_prefix(IndexOrder::Avet, None, Some(a), Some(v));
346 self.datoms_prefix(IndexOrder::Avet, &prefix)
347 .next()
348 .map(|datom| datom.e)
349 } else {
350 let prefix = key_prefix(IndexOrder::Aevt, None, Some(a), None);
351 self.datoms_prefix(IndexOrder::Aevt, &prefix)
352 .find(|datom| datom.v == *v)
353 .map(|datom| datom.e)
354 }
355 }
356
357 #[must_use]
361 pub fn with_transaction(&self, t: u64, datoms: &[Datom]) -> Self {
362 debug_assert!(
363 self.view == DbView::Current,
364 "with_transaction applies only to the current view"
365 );
366 let mut next = self.clone();
367 next.basis_t = t;
368 Arc::make_mut(&mut next.recorded).extend_from_slice(datoms);
369 next.indexes = Arc::new(OnceLock::new());
370 next.stats = Arc::new(OnceLock::new());
371 if let Some(parent) = self.indexes.get() {
374 let mut derived = parent.clone();
375 apply_current(&mut derived, datoms.iter(), &self.schema);
376 let _ = next.indexes.set(derived);
377 }
378 next
379 }
380
381 #[must_use]
383 pub fn stats(&self) -> DbStats {
384 let planner = self.planner_stats();
385 DbStats {
386 datoms: planner.total_datoms,
387 entities: planner.entity_count,
388 attributes: planner.per_attr.len(),
389 }
390 }
391
392 #[must_use]
394 pub fn planner_stats(&self) -> &PlannerStats {
395 self.stats.get_or_init(|| {
396 let mut stats = PlannerStats::default();
397 let mut values: BTreeMap<AttrId, BTreeSet<&Value>> = BTreeMap::new();
398 let mut attr_entities: BTreeMap<AttrId, BTreeSet<EntityId>> = BTreeMap::new();
399 let mut entities: BTreeSet<EntityId> = BTreeSet::new();
400 for datom in self.datoms_at(IndexOrder::Eavt) {
401 stats.total_datoms += 1;
402 stats.per_attr.entry(datom.a).or_default().count += 1;
403 values.entry(datom.a).or_default().insert(&datom.v);
404 attr_entities.entry(datom.a).or_default().insert(datom.e);
405 entities.insert(datom.e);
406 }
407 for (a, entry) in &mut stats.per_attr {
408 entry.distinct_values = values.get(a).map_or(0, BTreeSet::len);
409 entry.distinct_entities = attr_entities.get(a).map_or(0, BTreeSet::len);
410 }
411 stats.entity_count = entities.len();
412 stats
413 })
414 }
415
416 fn indexes(&self) -> &[Index; 4] {
417 self.indexes.get_or_init(|| {
418 let mut indexes: [Index; 4] = Default::default();
419 match self.view {
420 DbView::History => {
421 for datom in self.recorded.iter() {
422 if self.schema.get(datom.a).is_some_and(|a| a.no_history) {
423 continue;
424 }
425 insert_datom(&mut indexes, datom, &self.schema, true);
426 }
427 }
428 DbView::Current | DbView::AsOf(_) | DbView::Since(_) => {
429 let cutoff = match self.view {
430 DbView::AsOf(t) => Some(t),
431 _ => None,
432 };
433 let filtered = self
434 .recorded
435 .iter()
436 .filter(|d| cutoff.is_none_or(|t| d.tx.sequence() <= t));
437 apply_current(&mut indexes, filtered, &self.schema);
438 if let DbView::Since(t) = self.view {
439 for index in &mut indexes {
440 index.retain(|_, datom| datom.tx.sequence() > t);
441 }
442 }
443 }
444 }
445 indexes
446 })
447 }
448}
449
450fn apply_current<'a>(
456 indexes: &mut [Index; 4],
457 datoms: impl Iterator<Item = &'a Datom>,
458 schema: &Schema,
459) {
460 for datom in datoms {
461 if datom.added {
462 insert_datom(indexes, datom, schema, false);
463 } else {
464 for order in ORDERS {
465 if covered(schema, order, datom) {
466 let key = key_prefix(order, Some(datom.e), Some(datom.a), Some(&datom.v));
467 indexes[slot(order)].remove(&key);
468 }
469 }
470 }
471 }
472}
473
474fn insert_datom(indexes: &mut [Index; 4], datom: &Datom, schema: &Schema, with_tx: bool) {
475 for order in ORDERS {
476 if covered(schema, order, datom) {
477 let key = if with_tx {
478 datom.key(order)
479 } else {
480 key_prefix(order, Some(datom.e), Some(datom.a), Some(&datom.v))
481 };
482 indexes[slot(order)].insert(key, datom.clone());
483 }
484 }
485}
486
487fn covered(schema: &Schema, order: IndexOrder, datom: &Datom) -> bool {
488 match order {
489 IndexOrder::Eavt | IndexOrder::Aevt => true,
490 IndexOrder::Avet => avet_covered(schema, datom.a),
491 IndexOrder::Vaet => matches!(datom.v, Value::Ref(_)),
492 }
493}
494
495#[must_use]
497pub fn avet_covered(schema: &Schema, a: AttrId) -> bool {
498 schema
499 .get(a)
500 .is_some_and(|attr| attr.indexed || attr.unique.is_some())
501}
502
503#[derive(Clone, Copy, Debug, Eq, PartialEq)]
505pub struct DbStats {
506 pub datoms: usize,
508 pub entities: usize,
510 pub attributes: usize,
512}
513
514#[must_use]
516pub const fn attribute(
517 id: u64,
518 value_type: corium_core::ValueType,
519 cardinality: Cardinality,
520 unique: Option<Unique>,
521) -> corium_core::Attribute {
522 corium_core::Attribute {
523 id: EntityId::new(Partition::Db as u32, id),
524 value_type,
525 cardinality,
526 unique,
527 is_component: false,
528 indexed: unique.is_some(),
529 no_history: false,
530 }
531}
532
533#[cfg(test)]
534mod tests {
535 use super::*;
536 use corium_core::ValueType;
537
538 fn schema() -> Schema {
539 let mut schema = Schema::default();
540 schema.insert(attribute(1, ValueType::Str, Cardinality::One, None));
541 schema.insert(attribute(
542 2,
543 ValueType::Long,
544 Cardinality::One,
545 Some(Unique::Identity),
546 ));
547 schema.insert(attribute(3, ValueType::Ref, Cardinality::Many, None));
548 schema
549 }
550
551 fn attr(id: u64) -> AttrId {
552 EntityId::new(Partition::Db as u32, id)
553 }
554
555 fn entity(id: u64) -> EntityId {
556 EntityId::new(Partition::User as u32, id)
557 }
558
559 fn tx_entity(t: u64) -> EntityId {
560 EntityId::new(Partition::Tx as u32, t)
561 }
562
563 fn datom(e: u64, a: u64, v: Value, t: u64, added: bool) -> Datom {
564 Datom {
565 e: entity(e),
566 a: attr(a),
567 v,
568 tx: tx_entity(t),
569 added,
570 }
571 }
572
573 fn sample() -> Db {
574 Db::new(schema())
575 .with_transaction(
576 1,
577 &[
578 datom(1, 1, Value::Str("alice".into()), 1, true),
579 datom(1, 2, Value::Long(7), 1, true),
580 ],
581 )
582 .with_transaction(
583 2,
584 &[
585 datom(1, 1, Value::Str("alice".into()), 2, false),
586 datom(1, 1, Value::Str("alicia".into()), 2, true),
587 datom(2, 3, Value::Ref(entity(1)), 2, true),
588 ],
589 )
590 }
591
592 #[test]
593 fn current_view_folds_retractions() {
594 let db = sample();
595 assert_eq!(
596 db.values(entity(1), attr(1)),
597 vec![Value::Str("alicia".into())]
598 );
599 assert_eq!(db.stats().datoms, 3);
600 }
601
602 #[test]
603 fn as_of_reconstructs_past_basis() {
604 let db = sample().as_of(1);
605 assert_eq!(
606 db.values(entity(1), attr(1)),
607 vec![Value::Str("alice".into())]
608 );
609 assert_eq!(db.stats().datoms, 2);
610 }
611
612 #[test]
613 fn since_excludes_older_live_facts() {
614 let db = sample().since(1);
615 assert_eq!(db.values(entity(1), attr(2)), Vec::<Value>::new());
617 assert_eq!(
618 db.values(entity(1), attr(1)),
619 vec![Value::Str("alicia".into())]
620 );
621 }
622
623 #[test]
624 fn history_exposes_assertions_and_retractions() {
625 let db = sample().history();
626 let names: Vec<_> = db
627 .datoms_prefix(
628 IndexOrder::Eavt,
629 &key_prefix(IndexOrder::Eavt, Some(entity(1)), Some(attr(1)), None),
630 )
631 .map(|d| (d.v.clone(), d.added))
632 .collect();
633 assert_eq!(names.len(), 3);
634 assert!(names.contains(&(Value::Str("alice".into()), false)));
635 }
636
637 #[test]
638 fn avet_only_covers_indexed_attributes() {
639 let db = sample();
640 assert_eq!(db.datoms_at(IndexOrder::Avet).count(), 1);
641 assert_eq!(db.datoms_at(IndexOrder::Vaet).count(), 1);
642 }
643
644 #[test]
645 fn index_range_scans_value_bounds() {
646 let db = Db::new(schema()).with_transaction(
647 1,
648 &[
649 datom(1, 2, Value::Long(1), 1, true),
650 datom(2, 2, Value::Long(5), 1, true),
651 datom(3, 2, Value::Long(9), 1, true),
652 ],
653 );
654 let hits: Vec<_> = db
655 .index_range(attr(2), Some(&Value::Long(2)), Some(&Value::Long(9)))
656 .map(|d| d.e)
657 .collect();
658 assert_eq!(hits, vec![entity(2)]);
659 }
660
661 #[test]
662 fn tx_range_groups_by_transaction() {
663 let ranged = sample().tx_range(2, None);
664 assert_eq!(ranged.len(), 1);
665 assert_eq!(ranged[0].0, 2);
666 assert_eq!(ranged[0].1.len(), 3);
667 }
668
669 #[test]
670 fn incremental_indexes_match_rebuilt_indexes() {
671 let base = Db::new(schema());
672 let tx1 = [datom(1, 1, Value::Str("a".into()), 1, true)];
673 let tx2 = [
674 datom(1, 1, Value::Str("a".into()), 2, false),
675 datom(1, 1, Value::Str("b".into()), 2, true),
676 ];
677 let warm = base.with_transaction(1, &tx1);
679 let _ = warm.datoms();
680 let incremental = warm.with_transaction(2, &tx2);
681 let cold = base.with_transaction(1, &tx1).with_transaction(2, &tx2);
682 assert_eq!(incremental.datoms(), cold.datoms());
683 }
684
685 #[test]
686 fn planner_stats_count_attributes() {
687 let stats_owner = sample();
688 let stats = stats_owner.planner_stats();
689 assert_eq!(stats.total_datoms, 3);
690 assert_eq!(stats.per_attr[&attr(1)].count, 1);
691 assert!(stats.estimate(false, Some(attr(1)), false) <= stats.total_datoms);
692 }
693}