1use lxdb_core::ids::TokenId;
2use lxdb_format::{AdjacencyRecord, RelationRecord};
3use lxdb_storage::BinaryDataset;
4use std::iter::FusedIterator;
5
6use crate::{BinaryToken, DatasetQuery, EngineError, RecordIter, RelationRecordIter};
7
8#[derive(Debug, Clone, Copy, PartialEq)]
11pub struct BinaryRelation<'a> {
12 id: u32,
13 source: BinaryToken<'a>,
14 target: BinaryToken<'a>,
15 weight: f32,
16}
17
18impl<'a> BinaryRelation<'a> {
19 pub const fn new(
20 id: u32,
21 source: BinaryToken<'a>,
22 target: BinaryToken<'a>,
23 weight: f32,
24 ) -> Self {
25 Self { id, source, target, weight }
26 }
27
28 pub const fn id(&self) -> u32 {
29 self.id
30 }
31
32 pub const fn source(&self) -> BinaryToken<'a> {
33 self.source
34 }
35
36 pub const fn target(&self) -> BinaryToken<'a> {
37 self.target
38 }
39
40 pub const fn weight(&self) -> f32 {
41 self.weight
42 }
43}
44
45#[derive(Debug, Clone)]
47pub struct BinaryRelationIter<'a> {
48 dataset: &'a BinaryDataset,
49 records: RelationRecordIter<'a>,
50}
51
52impl<'a> BinaryRelationIter<'a> {
53 pub(crate) const fn new(dataset: &'a BinaryDataset, records: RelationRecordIter<'a>) -> Self {
54 Self { dataset, records }
55 }
56
57 fn resolve(&self, record: RelationRecord) -> Result<BinaryRelation<'a>, EngineError> {
58 let query = DatasetQuery::new(self.dataset);
59
60 let source_id = TokenId::new(record.source());
61
62 let source = query.token_by_id(source_id)?.ok_or(EngineError::MissingRelationSource {
63 relation_id: record.id(),
64 token_id: record.source(),
65 })?;
66
67 let target_id = TokenId::new(record.target());
68
69 let target = query.token_by_id(target_id)?.ok_or(EngineError::MissingRelationTarget {
70 relation_id: record.id(),
71 token_id: record.target(),
72 })?;
73
74 Ok(BinaryRelation::new(record.id(), source, target, record.weight()))
75 }
76}
77
78impl<'a> Iterator for BinaryRelationIter<'a> {
79 type Item = Result<BinaryRelation<'a>, EngineError>;
80
81 fn next(&mut self) -> Option<Self::Item> {
82 let record = match self.records.next()? {
83 Ok(record) => record,
84 Err(error) => return Some(Err(error.into())),
85 };
86
87 Some(self.resolve(record))
88 }
89
90 fn size_hint(&self) -> (usize, Option<usize>) {
91 self.records.size_hint()
92 }
93}
94
95impl ExactSizeIterator for BinaryRelationIter<'_> {
96 fn len(&self) -> usize {
97 self.records.len()
98 }
99}
100
101impl FusedIterator for BinaryRelationIter<'_> {}
102
103pub(crate) fn outgoing_relations(
104 dataset: &BinaryDataset,
105 token_id: TokenId,
106) -> Result<RelationRecordIter<'_>, EngineError> {
107 let token_index = usize::try_from(token_id.value()).expect("u32 token id must fit in usize");
108
109 let adjacency_bytes = dataset.adjacency_records();
110
111 let adjacency_start =
112 token_index.checked_mul(AdjacencyRecord::SIZE).ok_or(EngineError::MissingAdjacency {
113 token_id: token_id.value(),
114 adjacency_count: dataset.adjacency_count(),
115 })?;
116
117 let adjacency_end = adjacency_start.checked_add(AdjacencyRecord::SIZE).ok_or(
118 EngineError::MissingAdjacency {
119 token_id: token_id.value(),
120 adjacency_count: dataset.adjacency_count(),
121 },
122 )?;
123
124 let adjacency_slice = adjacency_bytes.get(adjacency_start..adjacency_end).ok_or(
125 EngineError::MissingAdjacency {
126 token_id: token_id.value(),
127 adjacency_count: dataset.adjacency_count(),
128 },
129 )?;
130
131 let adjacency = AdjacencyRecord::decode(adjacency_slice)?;
132
133 relation_range(dataset, token_id, adjacency)
134}
135
136fn relation_range(
137 dataset: &BinaryDataset,
138 token_id: TokenId,
139 adjacency: AdjacencyRecord,
140) -> Result<RelationRecordIter<'_>, EngineError> {
141 let relation_offset = usize::try_from(adjacency.offset())
142 .map_err(|_| relation_range_error(dataset, token_id, adjacency))?;
143
144 let relation_count =
145 usize::try_from(adjacency.count()).expect("u32 relation count must fit in usize");
146
147 let relation_end = relation_offset
148 .checked_add(relation_count)
149 .ok_or_else(|| relation_range_error(dataset, token_id, adjacency))?;
150
151 if relation_end > dataset.relation_count() {
152 return Err(relation_range_error(dataset, token_id, adjacency));
153 }
154
155 let byte_start = relation_offset
156 .checked_mul(RelationRecord::SIZE)
157 .ok_or_else(|| relation_range_error(dataset, token_id, adjacency))?;
158
159 let byte_end = relation_end
160 .checked_mul(RelationRecord::SIZE)
161 .ok_or_else(|| relation_range_error(dataset, token_id, adjacency))?;
162
163 let bytes = dataset
164 .relation_records()
165 .get(byte_start..byte_end)
166 .ok_or_else(|| relation_range_error(dataset, token_id, adjacency))?;
167
168 Ok(RecordIter::<RelationRecord>::new(bytes))
169}
170
171fn relation_range_error(
172 dataset: &BinaryDataset,
173 token_id: TokenId,
174 adjacency: AdjacencyRecord,
175) -> EngineError {
176 EngineError::RelationRangeOutOfBounds {
177 token_id: token_id.value(),
178 offset: adjacency.offset(),
179 count: adjacency.count(),
180 relation_count: dataset.relation_count(),
181 }
182}
183
184#[cfg(test)]
185mod tests {
186 use lxdb_core::ids::TokenId;
187
188 use lxdb_format::{AdjacencyRecord, Header, RelationRecord, Section, SectionHeader, flags};
189
190 use lxdb_storage::DatasetReader;
191
192 use crate::{BinaryDatasetExt, EngineError};
193
194 #[test]
195 fn resolves_only_outgoing_relations_for_token() {
196 let first_relation = RelationRecord::new(0, 0, 1, 0.75);
197
198 let second_relation = RelationRecord::new(1, 0, 2, 0.50);
199
200 let dataset = dataset_with_relations(
201 &[first_relation, second_relation],
202 &[AdjacencyRecord::new(0, 2), AdjacencyRecord::new(2, 0), AdjacencyRecord::new(2, 0)],
203 );
204
205 let mut outgoing =
206 dataset.outgoing(TokenId::new(0)).expect("token adjacency should resolve");
207
208 assert_eq!(outgoing.len(), 2);
209
210 let first = outgoing
211 .next()
212 .expect("first relation should exist")
213 .expect("first relation should decode");
214
215 assert_eq!(first.id(), 0);
216 assert_eq!(first.source(), 0);
217 assert_eq!(first.target(), 1);
218
219 let second = outgoing
220 .next()
221 .expect("second relation should exist")
222 .expect("second relation should decode");
223
224 assert_eq!(second.id(), 1);
225 assert_eq!(second.source(), 0);
226 assert_eq!(second.target(), 2);
227
228 assert!(outgoing.next().is_none());
229 }
230
231 #[test]
232 fn supports_tokens_without_outgoing_relations() {
233 let relation = RelationRecord::new(0, 0, 1, 1.0);
234
235 let dataset = dataset_with_relations(
236 &[relation],
237 &[AdjacencyRecord::new(0, 1), AdjacencyRecord::new(1, 0)],
238 );
239
240 let outgoing = dataset.outgoing(TokenId::new(1)).expect("empty adjacency should be valid");
241
242 assert_eq!(outgoing.len(), 0);
243 }
244
245 #[test]
246 fn rejects_tokens_without_adjacency_records() {
247 let dataset = dataset_with_relations(&[], &[AdjacencyRecord::new(0, 0)]);
248
249 let error =
250 dataset.outgoing(TokenId::new(5)).expect_err("missing adjacency should be rejected");
251
252 assert!(matches!(error, EngineError::MissingAdjacency { token_id: 5, adjacency_count: 1 }));
253 }
254
255 #[test]
256 fn rejects_relation_ranges_outside_table() {
257 let dataset = dataset_with_relations(&[], &[AdjacencyRecord::new(4, 2)]);
258
259 let error =
260 dataset.outgoing(TokenId::new(0)).expect_err("invalid relation range should fail");
261
262 assert!(matches!(
263 error,
264 EngineError::RelationRangeOutOfBounds {
265 token_id: 0,
266 offset: 4,
267 count: 2,
268 relation_count: 0,
269 }
270 ));
271 }
272
273 fn dataset_with_relations(
274 relations: &[RelationRecord],
275 adjacency: &[AdjacencyRecord],
276 ) -> lxdb_storage::BinaryDataset {
277 let mut bytes = Vec::new();
278
279 bytes.extend_from_slice(&Header::current().encode());
280
281 append_section(&mut bytes, Section::Tokens, &[]);
282
283 append_section(&mut bytes, Section::TokenStringTable, &[]);
284
285 let mut relation_bytes = Vec::new();
286
287 for relation in relations {
288 relation_bytes.extend_from_slice(&relation.encode());
289 }
290
291 append_section(&mut bytes, Section::Relations, &relation_bytes);
292
293 let mut adjacency_bytes = Vec::new();
294
295 for entry in adjacency {
296 adjacency_bytes.extend_from_slice(&entry.encode());
297 }
298
299 append_section(&mut bytes, Section::Adjacency, &adjacency_bytes);
300
301 DatasetReader::new().read(bytes).expect("test dataset should be valid")
302 }
303
304 fn append_section(output: &mut Vec<u8>, section: Section, payload: &[u8]) {
305 let length = u64::try_from(payload.len()).expect("test payload length should fit in u64");
306
307 let header = SectionHeader::new(section, flags::NONE, length);
308
309 output.extend_from_slice(&header.encode());
310 output.extend_from_slice(payload);
311 }
312}