Skip to main content

triblespace_core/trible/
tribleset.rs

1mod triblesetconstraint;
2pub mod triblesetidrangeconstraint;
3pub mod triblesetrangeconstraint;
4
5use triblesetconstraint::*;
6
7use crate::query::TriblePattern;
8use crate::inline::Inline;
9
10use crate::id::Id;
11use crate::patch::ArchiveEntry;
12use crate::patch::Entry;
13use crate::patch::PATCH;
14use crate::query::Variable;
15use crate::trible::AEVOrder;
16use crate::trible::AVEOrder;
17use crate::trible::EAVOrder;
18use crate::trible::EVAOrder;
19use crate::trible::Trible;
20use crate::trible::VAEOrder;
21use crate::trible::VEAOrder;
22use crate::trible::TRIBLE_LEN;
23use crate::inline::encodings::genid::GenId;
24use crate::inline::InlineEncoding;
25
26use std::iter::FromIterator;
27use std::iter::Map;
28use std::ops::Add;
29use std::ops::AddAssign;
30
31/// A collection of [`Trible`]s.
32///
33/// A [`TribleSet`] is a collection of [`Trible`]s that can be queried and manipulated.
34/// It supports efficient set operations like union, intersection, and difference.
35///
36/// The stored [`Trible`]s are indexed by the six possible orderings of their fields
37/// in corresponding [`PATCH`]es.
38///
39/// Clone is extremely cheap and can be used to create a snapshot of the current state of the [`TribleSet`].
40///
41/// Note that the [`TribleSet`] does not support an explicit `delete`/`remove` operation,
42/// as this would conflict with the CRDT semantics of the [`TribleSet`] and CALM principles as a whole.
43/// It does allow for set subtraction, but that operation is meant to compute the difference between two sets
44/// and not to remove elements from the set. A subtle but important distinction.
45#[derive(Debug, Clone)]
46pub struct TribleSet {
47    /// Entity → Attribute → Inline index.
48    pub eav: PATCH<TRIBLE_LEN, EAVOrder, ()>,
49    /// Inline → Entity → Attribute index.
50    pub vea: PATCH<TRIBLE_LEN, VEAOrder, ()>,
51    /// Attribute → Inline → Entity index.
52    pub ave: PATCH<TRIBLE_LEN, AVEOrder, ()>,
53    /// Inline → Attribute → Entity index.
54    pub vae: PATCH<TRIBLE_LEN, VAEOrder, ()>,
55    /// Entity → Inline → Attribute index.
56    pub eva: PATCH<TRIBLE_LEN, EVAOrder, ()>,
57    /// Attribute → Entity → Inline index.
58    pub aev: PATCH<TRIBLE_LEN, AEVOrder, ()>,
59}
60
61/// O(1) fingerprint for a [`TribleSet`], derived from the PATCH root hash.
62///
63/// This matches the equality semantics of [`TribleSet`], but it is not stable
64/// across process boundaries because [`PATCH`] uses a per-process hash key.
65#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
66pub struct TribleSetFingerprint(Option<u128>);
67
68impl TribleSetFingerprint {
69    /// Fingerprint of an empty set.
70    pub const EMPTY: Self = Self(None);
71
72    /// Returns `true` for the empty-set fingerprint.
73    pub fn is_empty(self) -> bool {
74        self.0.is_none()
75    }
76
77    /// Returns the raw 128-bit hash, or `None` for an empty set.
78    pub fn as_u128(self) -> Option<u128> {
79        self.0
80    }
81}
82
83type TribleSetInner<'a> =
84    Map<crate::patch::PATCHIterator<'a, 64, EAVOrder, ()>, fn(&[u8; 64]) -> &Trible>;
85
86/// Iterator over the tribles in a [`TribleSet`], yielded in EAV order.
87pub struct TribleSetIterator<'a> {
88    inner: TribleSetInner<'a>,
89}
90
91/// Minimum `other.len()` at which [`TribleSet::union`] fans out across
92/// rayon. Below this, the nested-join overhead dominates the saved
93/// per-index work. Tuned for the `entities/union*/5M` bench family.
94#[cfg(feature = "parallel")]
95pub const PARALLEL_UNION_THRESHOLD: usize = 4096;
96
97impl TribleSet {
98    /// Union of two [`TribleSet`]s.
99    ///
100    /// The other [`TribleSet`] is consumed, and this [`TribleSet`] is updated
101    /// in place.
102    ///
103    /// With the `parallel` feature enabled and `other` above
104    /// [`PARALLEL_UNION_THRESHOLD`] tribles, the six index unions
105    /// (`eav`/`eva`/`aev`/`ave`/`vea`/`vae`) fan out via nested
106    /// [`rayon::join`] — they touch disjoint memory so there's no
107    /// contention. The threshold gates on `other.len()` because PATCH
108    /// union work is bounded by the smaller side (each key from `other`
109    /// is inserted into `self`); when `other` is tiny (e.g. the per-
110    /// `entity!{}` `+=` in a serial fold) the rayon overhead would
111    /// dominate even at large `self`.
112    pub fn union(&mut self, other: Self) {
113        #[cfg(feature = "parallel")]
114        {
115            if other.len() >= PARALLEL_UNION_THRESHOLD {
116                let Self {
117                    eav,
118                    eva,
119                    aev,
120                    ave,
121                    vea,
122                    vae,
123                } = self;
124                let Self {
125                    eav: oeav,
126                    eva: oeva,
127                    aev: oaev,
128                    ave: oave,
129                    vea: ovea,
130                    vae: ovae,
131                } = other;
132                // Nested join trees the six tasks across rayon workers
133                // with much lower per-call overhead than `scope`.
134                rayon::join(
135                    || rayon::join(
136                        || eav.union(oeav),
137                        || eva.union(oeva),
138                    ),
139                    || rayon::join(
140                        || rayon::join(
141                            || aev.union(oaev),
142                            || ave.union(oave),
143                        ),
144                        || rayon::join(
145                            || vea.union(ovea),
146                            || vae.union(ovae),
147                        ),
148                    ),
149                );
150                return;
151            }
152        }
153
154        self.eav.union(other.eav);
155        self.eva.union(other.eva);
156        self.aev.union(other.aev);
157        self.ave.union(other.ave);
158        self.vea.union(other.vea);
159        self.vae.union(other.vae);
160    }
161
162    /// Returns a new set containing only tribles present in both sets.
163    ///
164    /// With the `parallel` feature enabled and either side above
165    /// [`PARALLEL_UNION_THRESHOLD`] tribles, the six index intersects
166    /// fan out via nested [`rayon::join`] on the same disjoint-memory
167    /// property as `union`. Threshold gates on `min(self, other)`
168    /// because intersect work is bounded by the smaller side.
169    pub fn intersect(&self, other: &Self) -> Self {
170        #[cfg(feature = "parallel")]
171        {
172            if self.len().min(other.len()) >= PARALLEL_UNION_THRESHOLD {
173                let ((eav, eva), ((aev, ave), (vea, vae))) = rayon::join(
174                    || {
175                        rayon::join(
176                            || self.eav.intersect(&other.eav),
177                            || self.eva.intersect(&other.eva),
178                        )
179                    },
180                    || {
181                        rayon::join(
182                            || {
183                                rayon::join(
184                                    || self.aev.intersect(&other.aev),
185                                    || self.ave.intersect(&other.ave),
186                                )
187                            },
188                            || {
189                                rayon::join(
190                                    || self.vea.intersect(&other.vea),
191                                    || self.vae.intersect(&other.vae),
192                                )
193                            },
194                        )
195                    },
196                );
197                return Self {
198                    eav,
199                    eva,
200                    aev,
201                    ave,
202                    vea,
203                    vae,
204                };
205            }
206        }
207        Self {
208            eav: self.eav.intersect(&other.eav),
209            eva: self.eva.intersect(&other.eva),
210            aev: self.aev.intersect(&other.aev),
211            ave: self.ave.intersect(&other.ave),
212            vea: self.vea.intersect(&other.vea),
213            vae: self.vae.intersect(&other.vae),
214        }
215    }
216
217    /// Returns a new set containing tribles in `self` but not in `other`.
218    ///
219    /// With the `parallel` feature enabled and `self` above
220    /// [`PARALLEL_UNION_THRESHOLD`] tribles, the six index differences
221    /// fan out via nested [`rayon::join`]. Threshold gates on
222    /// `self.len()` because difference work is bounded by the left
223    /// side (each key from `self` is either kept or filtered).
224    pub fn difference(&self, other: &Self) -> Self {
225        #[cfg(feature = "parallel")]
226        {
227            if self.len() >= PARALLEL_UNION_THRESHOLD {
228                let ((eav, eva), ((aev, ave), (vea, vae))) = rayon::join(
229                    || {
230                        rayon::join(
231                            || self.eav.difference(&other.eav),
232                            || self.eva.difference(&other.eva),
233                        )
234                    },
235                    || {
236                        rayon::join(
237                            || {
238                                rayon::join(
239                                    || self.aev.difference(&other.aev),
240                                    || self.ave.difference(&other.ave),
241                                )
242                            },
243                            || {
244                                rayon::join(
245                                    || self.vea.difference(&other.vea),
246                                    || self.vae.difference(&other.vae),
247                                )
248                            },
249                        )
250                    },
251                );
252                return Self {
253                    eav,
254                    eva,
255                    aev,
256                    ave,
257                    vea,
258                    vae,
259                };
260            }
261        }
262        Self {
263            eav: self.eav.difference(&other.eav),
264            eva: self.eva.difference(&other.eva),
265            aev: self.aev.difference(&other.aev),
266            ave: self.ave.difference(&other.ave),
267            vea: self.vea.difference(&other.vea),
268            vae: self.vae.difference(&other.vae),
269        }
270    }
271
272    /// Creates an empty set.
273    pub fn new() -> TribleSet {
274        TribleSet {
275            eav: PATCH::<TRIBLE_LEN, EAVOrder, ()>::new(),
276            eva: PATCH::<TRIBLE_LEN, EVAOrder, ()>::new(),
277            aev: PATCH::<TRIBLE_LEN, AEVOrder, ()>::new(),
278            ave: PATCH::<TRIBLE_LEN, AVEOrder, ()>::new(),
279            vea: PATCH::<TRIBLE_LEN, VEAOrder, ()>::new(),
280            vae: PATCH::<TRIBLE_LEN, VAEOrder, ()>::new(),
281        }
282    }
283
284    /// Returns the number of tribles in the set.
285    pub fn len(&self) -> usize {
286        self.eav.len() as usize
287    }
288
289    /// Returns `true` when the set contains no tribles.
290    pub fn is_empty(&self) -> bool {
291        self.len() == 0
292    }
293
294    /// Returns a fast fingerprint suitable for in-memory caching.
295    ///
296    /// The fingerprint matches [`TribleSet`] equality, but it is not stable
297    /// across process boundaries because [`PATCH`] uses a per-process hash key.
298    pub fn fingerprint(&self) -> TribleSetFingerprint {
299        TribleSetFingerprint(self.eav.root_hash())
300    }
301
302    /// Inserts a trible into all six covering indexes.
303    pub fn insert(&mut self, trible: &Trible) {
304        let key = Entry::new(&trible.data);
305        self.eav.insert(&key);
306        self.eva.insert(&key);
307        self.aev.insert(&key);
308        self.ave.insert(&key);
309        self.vea.insert(&key);
310        self.vae.insert(&key);
311    }
312
313    /// Inserts an archive-backed trible into all six covering indexes
314    /// using [`PATCH::insert_archive`], so each index may land the new
315    /// entry as a `LocalLeaf` instead of a freshly allocated heap
316    /// `Leaf`. The receiving Branches' `owner` fields keep the
317    /// underlying archive bytes alive.
318    pub fn insert_archive(&mut self, entry: &ArchiveEntry<'_, TRIBLE_LEN>) {
319        self.eav.insert_archive(entry);
320        self.eva.insert_archive(entry);
321        self.aev.insert_archive(entry);
322        self.ave.insert_archive(entry);
323        self.vea.insert_archive(entry);
324        self.vae.insert_archive(entry);
325    }
326
327    /// Returns `true` when the exact trible is present in the set.
328    pub fn contains(&self, trible: &Trible) -> bool {
329        self.eav.has_prefix(&trible.data)
330    }
331
332    /// Creates a constraint that proposes only values in the byte range
333    /// `[min, max]` (inclusive) using the VEA index with `infixes_range`.
334    ///
335    /// Use with `and!` alongside a `pattern!` for efficient range queries:
336    ///
337    /// ```rust,ignore
338    /// find!(ts: Inline<NsTAIInterval>,
339    ///     and!(
340    ///         pattern!(&data, [{ ?id @ attr: ?ts }]),
341    ///         data.value_in_range(ts, min_ts, max_ts),
342    ///     )
343    /// )
344    /// ```
345    pub fn value_in_range<V: InlineEncoding>(
346        &self,
347        variable: Variable<V>,
348        min: Inline<V>,
349        max: Inline<V>,
350    ) -> triblesetrangeconstraint::TribleSetRangeConstraint {
351        triblesetrangeconstraint::TribleSetRangeConstraint::new(variable, min, max, self.clone())
352    }
353
354    /// Creates a constraint that proposes only entity IDs in the byte range
355    /// `[min, max]` (inclusive) using the EAV index with `infixes_range`.
356    ///
357    /// ```rust,ignore
358    /// find!(id: Id,
359    ///     and!(
360    ///         pattern!(&data, [{ ?id @ attr: value }]),
361    ///         data.entity_in_range(id, min_id, max_id),
362    ///     )
363    /// )
364    /// ```
365    pub fn entity_in_range(
366        &self,
367        variable: Variable<GenId>,
368        min: Id,
369        max: Id,
370    ) -> triblesetidrangeconstraint::EntityRangeConstraint {
371        triblesetidrangeconstraint::EntityRangeConstraint::new(variable, min, max, self.clone())
372    }
373
374    /// Creates a constraint that proposes only attribute IDs in the byte range
375    /// `[min, max]` (inclusive) using the AEV index with `infixes_range`.
376    ///
377    /// ```rust,ignore
378    /// find!(attr: Id,
379    ///     and!(
380    ///         pattern!(&data, [{ entity @ ?attr: _ }]),
381    ///         data.attribute_in_range(attr, min_attr, max_attr),
382    ///     )
383    /// )
384    /// ```
385    pub fn attribute_in_range(
386        &self,
387        variable: Variable<GenId>,
388        min: Id,
389        max: Id,
390    ) -> triblesetidrangeconstraint::AttributeRangeConstraint {
391        triblesetidrangeconstraint::AttributeRangeConstraint::new(variable, min, max, self.clone())
392    }
393
394    /// Iterates over all tribles in EAV order.
395    pub fn iter(&self) -> TribleSetIterator<'_> {
396        TribleSetIterator {
397            inner: self
398                .eav
399                .iter()
400                .map(|data| Trible::as_transmute_raw_unchecked(data)),
401        }
402    }
403}
404
405impl PartialEq for TribleSet {
406    fn eq(&self, other: &Self) -> bool {
407        self.eav == other.eav
408    }
409}
410
411impl Eq for TribleSet {}
412
413impl Default for TribleSetFingerprint {
414    fn default() -> Self {
415        Self::EMPTY
416    }
417}
418
419impl From<&TribleSet> for TribleSetFingerprint {
420    fn from(set: &TribleSet) -> Self {
421        set.fingerprint()
422    }
423}
424
425impl AddAssign for TribleSet {
426    fn add_assign(&mut self, rhs: Self) {
427        self.union(rhs);
428    }
429}
430
431impl Add for TribleSet {
432    type Output = Self;
433
434    fn add(mut self, rhs: Self) -> Self::Output {
435        self.union(rhs);
436        self
437    }
438}
439
440impl FromIterator<Trible> for TribleSet {
441    fn from_iter<I: IntoIterator<Item = Trible>>(iter: I) -> Self {
442        let mut set = TribleSet::new();
443
444        for t in iter {
445            set.insert(&t);
446        }
447
448        set
449    }
450}
451
452impl TriblePattern for TribleSet {
453    type PatternConstraint<'a> = TribleSetConstraint;
454
455    fn pattern<V: InlineEncoding>(
456        &self,
457        e: Variable<GenId>,
458        a: Variable<GenId>,
459        v: Variable<V>,
460    ) -> Self::PatternConstraint<'static> {
461        TribleSetConstraint::new(e, a, v, self.clone())
462    }
463}
464
465impl<'a> Iterator for TribleSetIterator<'a> {
466    type Item = &'a Trible;
467
468    fn next(&mut self) -> Option<Self::Item> {
469        self.inner.next()
470    }
471}
472
473impl<'a> IntoIterator for &'a TribleSet {
474    type Item = &'a Trible;
475    type IntoIter = TribleSetIterator<'a>;
476
477    fn into_iter(self) -> Self::IntoIter {
478        self.iter()
479    }
480}
481
482impl Default for TribleSet {
483    fn default() -> Self {
484        Self::new()
485    }
486}
487
488#[cfg(test)]
489mod tests {
490    use crate::examples::literature;
491    use crate::prelude::*;
492
493    use super::*;
494    use fake::faker::lorem::en::Words;
495    use fake::faker::name::raw::FirstName;
496    use fake::faker::name::raw::LastName;
497    use fake::locales::EN;
498    use fake::Fake;
499
500    use rayon::iter::IntoParallelIterator;
501    use rayon::iter::ParallelIterator;
502
503    #[test]
504    fn union() {
505        let mut kb = TribleSet::new();
506        for _i in 0..100 {
507            let author = ufoid();
508            let book = ufoid();
509            kb += entity! { &author @
510               literature::firstname: FirstName(EN).fake::<String>(),
511               literature::lastname: LastName(EN).fake::<String>(),
512            };
513            kb += entity! { &book @
514               literature::title: Words(1..3).fake::<Vec<String>>().join(" "),
515               literature::author: &author
516            };
517        }
518        assert_eq!(kb.len(), 400);
519    }
520
521    #[test]
522    fn union_parallel() {
523        let kb = (0..1000)
524            .into_par_iter()
525            .flat_map(|_| {
526                let author = ufoid();
527                let book = ufoid();
528                [
529                    entity! { &author @
530                       literature::firstname: FirstName(EN).fake::<String>(),
531                       literature::lastname: LastName(EN).fake::<String>(),
532                    },
533                    entity! { &book @
534                       literature::title: Words(1..3).fake::<Vec<String>>().join(" "),
535                       literature::author: &author
536                    },
537                ]
538            })
539            .reduce(Fragment::default, |a, b| a + b);
540        assert_eq!(kb.len(), 4000);
541    }
542
543    #[test]
544    fn intersection() {
545        let mut kb1 = TribleSet::new();
546        let mut kb2 = TribleSet::new();
547        for _i in 0..100 {
548            let author = ufoid();
549            let book = ufoid();
550            kb1 += entity! { &author @
551               literature::firstname: FirstName(EN).fake::<String>(),
552               literature::lastname: LastName(EN).fake::<String>(),
553            };
554            kb1 += entity! { &book @
555               literature::title: Words(1..3).fake::<Vec<String>>().join(" "),
556               literature::author: &author
557            };
558            kb2 += entity! { &author @
559               literature::firstname: FirstName(EN).fake::<String>(),
560               literature::lastname: LastName(EN).fake::<String>(),
561            };
562            kb2 += entity! { &book @
563               literature::title: Words(1..3).fake::<Vec<String>>().join(" "),
564               literature::author: &author
565            };
566        }
567        let intersection = kb1.intersect(&kb2);
568        // Verify that the intersection contains only elements present in both kb1 and kb2
569        for trible in &intersection {
570            assert!(kb1.contains(trible));
571            assert!(kb2.contains(trible));
572        }
573    }
574
575    #[test]
576    fn difference() {
577        let mut kb1 = TribleSet::new();
578        let mut kb2 = TribleSet::new();
579        for _i in 0..100 {
580            let author = ufoid();
581            let book = ufoid();
582            kb1 += entity! { &author @
583               literature::firstname: FirstName(EN).fake::<String>(),
584               literature::lastname: LastName(EN).fake::<String>(),
585            };
586            kb1 += entity! { &book @
587               literature::title: Words(1..3).fake::<Vec<String>>().join(" "),
588               literature::author: &author
589            };
590            if _i % 2 == 0 {
591                kb2 += entity! { &author @
592                   literature::firstname: FirstName(EN).fake::<String>(),
593                   literature::lastname: LastName(EN).fake::<String>(),
594                };
595                kb2 += entity! { &book @
596                   literature::title: Words(1..3).fake::<Vec<String>>().join(" "),
597                   literature::author: &author
598                };
599            }
600        }
601        let difference = kb1.difference(&kb2);
602        // Verify that the difference contains only elements present in kb1 but not in kb2
603        for trible in &difference {
604            assert!(kb1.contains(trible));
605            assert!(!kb2.contains(trible));
606        }
607    }
608
609    #[test]
610    fn test_contains() {
611        let mut kb = TribleSet::new();
612        let author = ufoid();
613        let book = ufoid();
614        let author_tribles = entity! { &author @
615           literature::firstname: FirstName(EN).fake::<String>(),
616           literature::lastname: LastName(EN).fake::<String>(),
617        };
618        let book_tribles = entity! { &book @
619           literature::title: Words(1..3).fake::<Vec<String>>().join(" "),
620           literature::author: &author
621        };
622
623        kb += author_tribles.clone();
624        kb += book_tribles.clone();
625
626        for trible in &author_tribles {
627            assert!(kb.contains(trible));
628        }
629        for trible in &book_tribles {
630            assert!(kb.contains(trible));
631        }
632
633        let non_existent_trible = entity! { &ufoid() @
634           literature::firstname: FirstName(EN).fake::<String>(),
635           literature::lastname: LastName(EN).fake::<String>(),
636        };
637
638        for trible in &non_existent_trible {
639            assert!(!kb.contains(trible));
640        }
641    }
642}