1use lxdb_core::ids::TokenId;
2use lxdb_storage::BinaryDataset;
3
4use crate::{BinaryDatasetExt, BinaryRelationIter, BinaryToken, EngineError, RelationRecordIter};
5
6#[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 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 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 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}