Skip to main content

lxdb_engine/
query.rs

1use lxdb_core::ids::TokenId;
2use lxdb_storage::BinaryDataset;
3
4use crate::{BinaryDatasetExt, BinaryRelationIter, BinaryToken, EngineError, RelationRecordIter};
5
6/// Read-only query interface over a binary LXDB dataset.
7///
8/// The query borrows the dataset and does not allocate an intermediate graph.
9#[derive(Debug, Clone, Copy)]
10pub struct DatasetQuery<'a> {
11    dataset: &'a BinaryDataset,
12}
13
14impl<'a> DatasetQuery<'a> {
15    pub const fn new(dataset: &'a BinaryDataset) -> Self {
16        Self { dataset }
17    }
18
19    pub const fn dataset(&self) -> &'a BinaryDataset {
20        self.dataset
21    }
22
23    /// Finds the first token whose text exactly matches `text`.
24    ///
25    /// This performs a linear scan over the token records.
26    pub fn token_by_text(&self, text: &str) -> Result<Option<BinaryToken<'a>>, EngineError> {
27        for token in self.dataset.resolved_tokens() {
28            let token = token?;
29
30            if token.text() == text {
31                return Ok(Some(token));
32            }
33        }
34
35        Ok(None)
36    }
37
38    pub fn token_by_id(&self, token_id: TokenId) -> Result<Option<BinaryToken<'a>>, EngineError> {
39        for token in self.dataset.resolved_tokens() {
40            let token = token?;
41
42            if token.id() == token_id {
43                return Ok(Some(token));
44            }
45        }
46
47        Ok(None)
48    }
49
50    pub fn outgoing(&self, token_id: TokenId) -> Result<RelationRecordIter<'a>, EngineError> {
51        self.dataset.outgoing(token_id)
52    }
53
54    /// Resolves all outgoing relations for a token.
55    pub fn resolved_outgoing(
56        &self,
57        token_id: TokenId,
58    ) -> Result<BinaryRelationIter<'a>, EngineError> {
59        let records = self.dataset.outgoing(token_id)?;
60
61        Ok(BinaryRelationIter::new(self.dataset, records))
62    }
63
64    /// Finds a token by text and resolves its outgoing relations.
65    ///
66    /// `Ok(None)` means that no token exists with the provided text.
67    pub fn related_to(&self, text: &str) -> Result<Option<BinaryRelationIter<'a>>, EngineError> {
68        let Some(token) = self.token_by_text(text)? else {
69            return Ok(None);
70        };
71
72        let relations = self.resolved_outgoing(token.id())?;
73
74        Ok(Some(relations))
75    }
76}
77
78#[cfg(test)]
79mod tests {
80    use lxdb_core::ids::TokenId;
81    use lxdb_format::{AdjacencyRecord, RelationRecord};
82
83    use crate::{BinaryDatasetExt, DatasetQuery, EngineError, test_support};
84
85    #[test]
86    fn finds_token_by_exact_text() {
87        let dataset = test_support::dataset(
88            &["rust", "language", "compiler"],
89            &[],
90            &[AdjacencyRecord::new(0, 0), AdjacencyRecord::new(0, 0), AdjacencyRecord::new(0, 0)],
91        );
92
93        let query = DatasetQuery::new(&dataset);
94
95        let token = query
96            .token_by_text("language")
97            .expect("token lookup should succeed")
98            .expect("token should exist");
99
100        assert_eq!(token.id().value(), 1);
101        assert_eq!(token.text(), "language");
102    }
103
104    #[test]
105    fn returns_none_for_unknown_token_text() {
106        let dataset = test_support::dataset(&["rust"], &[], &[AdjacencyRecord::new(0, 0)]);
107
108        let token = dataset.query().token_by_text("python").expect("token lookup should succeed");
109
110        assert!(token.is_none());
111    }
112
113    #[test]
114    fn finds_token_by_id() {
115        let dataset = test_support::dataset(
116            &["rust", "memory"],
117            &[],
118            &[AdjacencyRecord::new(0, 0), AdjacencyRecord::new(0, 0)],
119        );
120
121        let token = dataset
122            .query()
123            .token_by_id(TokenId::new(1))
124            .expect("token lookup should succeed")
125            .expect("token should exist");
126
127        assert_eq!(token.text(), "memory");
128    }
129
130    #[test]
131    fn exposes_outgoing_relations() {
132        let relation = RelationRecord::new(0, 0, 1, 0.9);
133
134        let dataset = test_support::dataset(
135            &["rust", "language"],
136            &[relation],
137            &[AdjacencyRecord::new(0, 1), AdjacencyRecord::new(1, 0)],
138        );
139
140        let mut outgoing =
141            dataset.query().outgoing(TokenId::new(0)).expect("outgoing lookup should succeed");
142
143        let relation =
144            outgoing.next().expect("relation should exist").expect("relation should decode");
145
146        assert_eq!(relation.source(), 0);
147        assert_eq!(relation.target(), 1);
148        assert_eq!(outgoing.len(), 0);
149    }
150
151    #[test]
152    fn rejects_relations_with_missing_target_tokens() {
153        let relation = RelationRecord::new(7, 0, 99, 1.0);
154
155        let dataset = test_support::dataset(&["rust"], &[relation], &[AdjacencyRecord::new(0, 1)]);
156
157        let mut relations = dataset
158            .query()
159            .related_to("rust")
160            .expect("related token query should initialize")
161            .expect("source token should exist");
162
163        let error = relations
164            .next()
165            .expect("relation should exist")
166            .expect_err("missing target should fail");
167
168        assert!(matches!(
169            error,
170            EngineError::MissingRelationTarget { relation_id: 7, token_id: 99 }
171        ));
172    }
173
174    #[test]
175    fn resolves_empty_relation_set() {
176        let dataset = test_support::dataset(&["rust"], &[], &[AdjacencyRecord::new(0, 0)]);
177
178        let relations = dataset
179            .query()
180            .related_to("rust")
181            .expect("related token query should succeed")
182            .expect("source token should exist");
183
184        assert_eq!(relations.len(), 0);
185    }
186
187    #[test]
188    fn returns_none_when_related_source_does_not_exist() {
189        let dataset = test_support::dataset(&["rust"], &[], &[AdjacencyRecord::new(0, 0)]);
190
191        let relations =
192            dataset.query().related_to("python").expect("related token query should succeed");
193
194        assert!(relations.is_none());
195    }
196
197    #[test]
198    fn resolves_related_tokens_by_text() {
199        let relation_to_language = RelationRecord::new(0, 0, 1, 0.9);
200
201        let relation_to_compiler = RelationRecord::new(1, 0, 2, 0.7);
202
203        let dataset = test_support::dataset(
204            &["rust", "language", "compiler"],
205            &[relation_to_language, relation_to_compiler],
206            &[AdjacencyRecord::new(0, 2), AdjacencyRecord::new(2, 0), AdjacencyRecord::new(2, 0)],
207        );
208
209        let mut relations = dataset
210            .query()
211            .related_to("rust")
212            .expect("related token query should succeed")
213            .expect("source token should exist");
214
215        assert_eq!(relations.len(), 2);
216
217        let first = relations
218            .next()
219            .expect("first relation should exist")
220            .expect("first relation should resolve");
221
222        assert_eq!(first.id(), 0);
223        assert_eq!(first.source().text(), "rust");
224        assert_eq!(first.target().text(), "language");
225
226        assert_eq!(first.weight().to_bits(), 0.9_f32.to_bits(),);
227
228        let second = relations
229            .next()
230            .expect("second relation should exist")
231            .expect("second relation should resolve");
232
233        assert_eq!(second.id(), 1);
234        assert_eq!(second.source().text(), "rust");
235        assert_eq!(second.target().text(), "compiler");
236
237        assert_eq!(second.weight().to_bits(), 0.7_f32.to_bits(),);
238
239        assert!(relations.next().is_none());
240    }
241}