1use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, Default)]
11pub struct QueryHints {
12 pub preferred_index: Option<IndexType>,
14 pub limit: Option<usize>,
16 pub offset: Option<usize>,
18 pub use_bloom_filter: Option<bool>,
20 pub enable_caching: Option<bool>,
22 pub estimated_result_size: Option<usize>,
24 pub timeout_ms: Option<u64>,
26 pub collect_stats: bool,
28}
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
32pub enum IndexType {
33 SPO,
35 POS,
37 OSP,
39}
40
41impl QueryHints {
42 pub fn new() -> Self {
44 Self::default()
45 }
46
47 pub fn with_index(mut self, index: IndexType) -> Self {
49 self.preferred_index = Some(index);
50 self
51 }
52
53 pub fn with_limit(mut self, limit: usize) -> Self {
55 self.limit = Some(limit);
56 self
57 }
58
59 pub fn with_offset(mut self, offset: usize) -> Self {
61 self.offset = Some(offset);
62 self
63 }
64
65 pub fn with_bloom_filter(mut self, enable: bool) -> Self {
67 self.use_bloom_filter = Some(enable);
68 self
69 }
70
71 pub fn with_caching(mut self, enable: bool) -> Self {
73 self.enable_caching = Some(enable);
74 self
75 }
76
77 pub fn with_estimated_size(mut self, size: usize) -> Self {
79 self.estimated_result_size = Some(size);
80 self
81 }
82
83 pub fn with_timeout(mut self, timeout_ms: u64) -> Self {
85 self.timeout_ms = Some(timeout_ms);
86 self
87 }
88
89 pub fn with_stats(mut self, collect: bool) -> Self {
91 self.collect_stats = collect;
92 self
93 }
94
95 pub fn auto_select_index(s_bound: bool, p_bound: bool, o_bound: bool) -> IndexType {
102 match (s_bound, p_bound, o_bound) {
103 (true, _, _) => IndexType::SPO, (false, true, _) => IndexType::POS, (false, false, true) => IndexType::OSP, (false, false, false) => IndexType::SPO, }
108 }
109
110 pub fn merge(&self, other: &QueryHints) -> QueryHints {
112 QueryHints {
113 preferred_index: other.preferred_index.or(self.preferred_index),
114 limit: other.limit.or(self.limit),
115 offset: other.offset.or(self.offset),
116 use_bloom_filter: other.use_bloom_filter.or(self.use_bloom_filter),
117 enable_caching: other.enable_caching.or(self.enable_caching),
118 estimated_result_size: other.estimated_result_size.or(self.estimated_result_size),
119 timeout_ms: other.timeout_ms.or(self.timeout_ms),
120 collect_stats: other.collect_stats || self.collect_stats,
121 }
122 }
123
124 pub fn apply_pagination<T>(&self, results: Vec<T>) -> Vec<T> {
126 let offset = self.offset.unwrap_or(0);
127 let limit = self.limit.unwrap_or(usize::MAX);
128
129 results.into_iter().skip(offset).take(limit).collect()
130 }
131}
132
133#[derive(Debug, Clone, Default)]
135pub struct QueryStats {
136 pub pages_accessed: usize,
138 pub index_entries_scanned: usize,
140 pub results_found: usize,
142 pub execution_time_us: u64,
144 pub index_used: Option<IndexType>,
146 pub bloom_filter_used: bool,
148 pub bloom_false_positives: usize,
150 pub cached: bool,
152}
153
154impl QueryStats {
155 pub fn new() -> Self {
157 Self::default()
158 }
159
160 pub fn with_index(mut self, index: IndexType) -> Self {
162 self.index_used = Some(index);
163 self
164 }
165
166 pub fn with_bloom_filter(mut self, used: bool, false_positives: usize) -> Self {
168 self.bloom_filter_used = used;
169 self.bloom_false_positives = false_positives;
170 self
171 }
172
173 pub fn with_execution_time(mut self, time_us: u64) -> Self {
175 self.execution_time_us = time_us;
176 self
177 }
178
179 pub fn avg_time_per_result(&self) -> f64 {
181 if self.results_found == 0 {
182 0.0
183 } else {
184 self.execution_time_us as f64 / self.results_found as f64
185 }
186 }
187
188 pub fn bloom_filter_effectiveness(&self) -> f64 {
190 if !self.bloom_filter_used || self.index_entries_scanned == 0 {
191 0.0
192 } else {
193 1.0 - (self.bloom_false_positives as f64 / self.index_entries_scanned as f64)
194 }
195 }
196}
197
198#[cfg(test)]
199mod tests {
200 use super::*;
201
202 #[test]
203 fn test_query_hints_builder() {
204 let hints = QueryHints::new()
205 .with_index(IndexType::SPO)
206 .with_limit(100)
207 .with_offset(10)
208 .with_bloom_filter(true)
209 .with_caching(false)
210 .with_timeout(5000);
211
212 assert_eq!(hints.preferred_index, Some(IndexType::SPO));
213 assert_eq!(hints.limit, Some(100));
214 assert_eq!(hints.offset, Some(10));
215 assert_eq!(hints.use_bloom_filter, Some(true));
216 assert_eq!(hints.enable_caching, Some(false));
217 assert_eq!(hints.timeout_ms, Some(5000));
218 }
219
220 #[test]
221 fn test_auto_select_index() {
222 assert_eq!(
224 QueryHints::auto_select_index(true, false, false),
225 IndexType::SPO
226 );
227 assert_eq!(
228 QueryHints::auto_select_index(true, true, false),
229 IndexType::SPO
230 );
231 assert_eq!(
232 QueryHints::auto_select_index(true, false, true),
233 IndexType::SPO
234 );
235
236 assert_eq!(
238 QueryHints::auto_select_index(false, true, false),
239 IndexType::POS
240 );
241 assert_eq!(
242 QueryHints::auto_select_index(false, true, true),
243 IndexType::POS
244 );
245
246 assert_eq!(
248 QueryHints::auto_select_index(false, false, true),
249 IndexType::OSP
250 );
251
252 assert_eq!(
254 QueryHints::auto_select_index(false, false, false),
255 IndexType::SPO
256 );
257 }
258
259 #[test]
260 fn test_hints_merge() {
261 let hints1 = QueryHints::new().with_limit(100).with_offset(10);
262
263 let hints2 = QueryHints::new().with_limit(50).with_bloom_filter(true);
264
265 let merged = hints1.merge(&hints2);
266
267 assert_eq!(merged.limit, Some(50)); assert_eq!(merged.offset, Some(10)); assert_eq!(merged.use_bloom_filter, Some(true)); }
271
272 #[test]
273 fn test_apply_pagination() {
274 let hints = QueryHints::new().with_limit(3).with_offset(2);
275
276 let data = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
277 let paginated = hints.apply_pagination(data);
278
279 assert_eq!(paginated, vec![3, 4, 5]); }
281
282 #[test]
283 fn test_apply_pagination_no_offset() {
284 let hints = QueryHints::new().with_limit(3);
285
286 let data = vec![1, 2, 3, 4, 5];
287 let paginated = hints.apply_pagination(data);
288
289 assert_eq!(paginated, vec![1, 2, 3]);
290 }
291
292 #[test]
293 fn test_apply_pagination_no_limit() {
294 let hints = QueryHints::new().with_offset(2);
295
296 let data = vec![1, 2, 3, 4, 5];
297 let paginated = hints.apply_pagination(data);
298
299 assert_eq!(paginated, vec![3, 4, 5]);
300 }
301
302 #[test]
303 fn test_query_stats() {
304 let stats = QueryStats::new()
305 .with_index(IndexType::POS)
306 .with_bloom_filter(true, 5)
307 .with_execution_time(1000);
308
309 assert_eq!(stats.index_used, Some(IndexType::POS));
310 assert!(stats.bloom_filter_used);
311 assert_eq!(stats.bloom_false_positives, 5);
312 assert_eq!(stats.execution_time_us, 1000);
313 }
314
315 #[test]
316 fn test_query_stats_avg_time() {
317 let mut stats = QueryStats::new().with_execution_time(1000);
318 stats.results_found = 10;
319
320 assert_eq!(stats.avg_time_per_result(), 100.0);
321 }
322
323 #[test]
324 fn test_bloom_filter_effectiveness() {
325 let mut stats = QueryStats::new().with_bloom_filter(true, 10);
326 stats.index_entries_scanned = 100;
327
328 assert_eq!(stats.bloom_filter_effectiveness(), 0.9);
330 }
331}