Skip to main content

lxdb_engine/
relation.rs

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/// A relation whose source and target tokens have been resolved directly
9/// against the binary dataset.
10#[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/// Lazily resolves relation records into high-level relation views.
46#[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}