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}