Skip to main content

triblespace_core/trible/
fragment.rs

1use std::ops::{Add, AddAssign, Deref};
2
3use crate::blob::{BlobEncoding, MemoryBlobStore, IntoBlob};
4use crate::id::Id;
5use crate::id::RawId;
6use crate::patch::Entry;
7use crate::patch::PATCH;
8use crate::inline::encodings::hash::Handle;
9use crate::inline::Inline;
10
11use super::Trible;
12use super::TribleSet;
13
14/// A rooted (or multi-root) fragment of a knowledge graph.
15///
16/// A fragment is a [`TribleSet`] plus a (possibly empty) set of "exported" entity
17/// ids that act as entry points into the contained facts, plus the
18/// [`MemoryBlobStore`] holding any bytes the contained facts reference
19/// by handle. Exports are not privileged in the graph model itself;
20/// they are simply the ids the producer wants to hand back to the
21/// caller as the fragment's interface.
22///
23/// The embedded blob store is what makes a Fragment *self-contained*:
24/// handles in the facts (e.g. `metadata::name: <Inline<Handle</// LongString>>>`) reference bytes that the fragment carries with
25/// itself. An empty `MemoryBlobStore` is structurally a single
26/// PATCH-root pointer — fragments without blobs pay essentially
27/// zero overhead.
28#[derive(Debug, Clone, Default, PartialEq, Eq)]
29pub struct Fragment {
30    exports: PATCH<16>,
31    facts: TribleSet,
32    blobs: MemoryBlobStore,
33}
34
35impl Fragment {
36    /// Creates an empty fragment with no exports and no facts.
37    pub fn empty() -> Self {
38        Self::default()
39    }
40
41    /// Creates a fragment that exports a single root id, with the
42    /// given facts and an empty blob store.
43    pub fn rooted(root: Id, facts: TribleSet) -> Self {
44        let mut exports = PATCH::<16>::new();
45        let raw: RawId = root.into();
46        exports.insert(&Entry::new(&raw));
47        Self {
48            exports,
49            facts,
50            blobs: MemoryBlobStore::new(),
51        }
52    }
53
54    /// Creates a fragment with the given exported ids and an empty blob store.
55    ///
56    /// Export ids are canonicalized as a set (duplicates are ignored). Empty
57    /// exports are allowed.
58    pub fn new<I>(exports: I, facts: TribleSet) -> Self
59    where
60        I: IntoIterator<Item = Id>,
61    {
62        let mut export_set = PATCH::<16>::new();
63        for id in exports {
64            let raw: RawId = id.into();
65            export_set.insert(&Entry::new(&raw));
66        }
67        Self {
68            exports: export_set,
69            facts,
70            blobs: MemoryBlobStore::new(),
71        }
72    }
73
74    /// Creates a fragment with no exports, holding the given facts and
75    /// blob store. Useful when re-wrapping the tail of a destructured
76    /// fragment (e.g. inside `Spread::spread`) where the exports have
77    /// already been consumed.
78    pub fn from_facts_and_blobs(facts: TribleSet, blobs: MemoryBlobStore) -> Self {
79        Self {
80            exports: PATCH::<16>::new(),
81            facts,
82            blobs,
83        }
84    }
85
86    /// Creates a fragment that exports a single root id, with the given
87    /// facts and blob store. The macro-generated `entity!{}` expansion
88    /// uses this to wrap its accumulated state — facts come from per-
89    /// attribute inserts, blobs come from any `field*: spread_source`
90    /// extras the spread sources carried with them.
91    pub fn rooted_with_blobs(
92        root: Id,
93        facts: TribleSet,
94        blobs: MemoryBlobStore,
95    ) -> Self {
96        let mut exports = PATCH::<16>::new();
97        let raw: RawId = root.into();
98        exports.insert(&Entry::new(&raw));
99        Self {
100            exports,
101            facts,
102            blobs,
103        }
104    }
105
106    /// Insert a blob into the fragment's local blob store and return the
107    /// content-addressed handle that references it.
108    ///
109    /// Use this when you want a Fragment to be self-contained — every
110    /// handle in its facts has its bytes available without consulting
111    /// an external blob store. Idempotent under content addressing:
112    /// putting the same bytes twice returns the same handle and
113    /// doesn't grow the store.
114    pub fn put<S, T>(&mut self, item: T) -> Inline<Handle<S>>
115    where
116        S: BlobEncoding,
117        T: IntoBlob<S>,
118    {
119        self.blobs.insert(item.to_blob())
120    }
121
122    /// Returns the exported ids for this fragment, in deterministic (lexicographic) order.
123    pub fn exports(&self) -> impl Iterator<Item = Id> + '_ {
124        self.exports
125            .iter_ordered()
126            .map(|raw| Id::new(*raw).expect("export ids are non-nil"))
127    }
128
129    /// Returns the single exported id if this fragment is rooted.
130    pub fn root(&self) -> Option<Id> {
131        if self.exports.len() == 1 {
132            let raw = self
133                .exports
134                .iter_ordered()
135                .next()
136                .expect("len() == 1 implies a first element exists");
137            Some(Id::new(*raw).expect("export ids are non-nil"))
138        } else {
139            None
140        }
141    }
142
143    pub fn facts(&self) -> &TribleSet {
144        &self.facts
145    }
146
147    /// Mutable access to the fragment's facts, for producers that
148    /// accumulate tribles directly (e.g. importers inserting per-row
149    /// facts alongside `put`-ing the blobs those facts reference).
150    pub fn facts_mut(&mut self) -> &mut TribleSet {
151        &mut self.facts
152    }
153
154    /// Borrow the fragment's local blob store.
155    pub fn blobs(&self) -> &MemoryBlobStore {
156        &self.blobs
157    }
158
159    /// Mutable access to the fragment's local blob store, for
160    /// producers that need to merge an existing store in bulk
161    /// (`blobs_mut().union(other)`) rather than `put` items one at
162    /// a time.
163    pub fn blobs_mut(&mut self) -> &mut MemoryBlobStore {
164        &mut self.blobs
165    }
166
167    pub fn into_facts(self) -> TribleSet {
168        self.facts
169    }
170
171    /// Consume the fragment, yielding its facts and blob store. The
172    /// exports are dropped — most callers want facts/blobs together
173    /// without the rooted-id concern.
174    pub fn into_facts_and_blobs(self) -> (TribleSet, MemoryBlobStore) {
175        (self.facts, self.blobs)
176    }
177
178    pub fn into_parts(self) -> (PATCH<16>, TribleSet, MemoryBlobStore) {
179        (self.exports, self.facts, self.blobs)
180    }
181
182}
183
184impl Deref for Fragment {
185    type Target = TribleSet;
186
187    fn deref(&self) -> &Self::Target {
188        &self.facts
189    }
190}
191
192impl<'a> IntoIterator for &'a Fragment {
193    type Item = &'a Trible;
194    type IntoIter = super::tribleset::TribleSetIterator<'a>;
195
196    fn into_iter(self) -> Self::IntoIter {
197        self.facts.iter()
198    }
199}
200
201impl AddAssign for Fragment {
202    fn add_assign(&mut self, rhs: Self) {
203        self.facts += rhs.facts;
204        self.exports.union(rhs.exports);
205        self.blobs.union(rhs.blobs);
206    }
207}
208
209impl AddAssign<TribleSet> for Fragment {
210    /// Facts-only merge — does not touch exports or blobs.
211    fn add_assign(&mut self, rhs: TribleSet) {
212        self.facts += rhs;
213    }
214}
215
216impl Add for Fragment {
217    type Output = Self;
218
219    fn add(mut self, rhs: Self) -> Self::Output {
220        self += rhs;
221        self
222    }
223}
224
225impl Add<TribleSet> for Fragment {
226    type Output = Self;
227
228    fn add(mut self, rhs: TribleSet) -> Self::Output {
229        self += rhs;
230        self
231    }
232}
233
234impl AddAssign<Fragment> for TribleSet {
235    fn add_assign(&mut self, rhs: Fragment) {
236        self.union(rhs.facts);
237    }
238}
239
240impl Add<Fragment> for TribleSet {
241    type Output = Self;
242
243    fn add(mut self, rhs: Fragment) -> Self::Output {
244        self += rhs;
245        self
246    }
247}
248
249/// Lossless promotion: a `TribleSet` becomes a Fragment with no
250/// exported root and an empty blob store. The reverse direction is
251/// intentionally not implemented — going from `Fragment` to
252/// `TribleSet` discards the embedded blob store and exports, so it
253/// has to be explicit (`Fragment::into_facts`).
254impl From<TribleSet> for Fragment {
255    fn from(facts: TribleSet) -> Self {
256        Self::from_facts_and_blobs(facts, MemoryBlobStore::new())
257    }
258}
259
260impl From<Fragment> for TribleSet {
261    fn from(value: Fragment) -> Self {
262        value.facts
263    }
264}