Skip to main content

oxirs_tdb/
query_hints.rs

1//! Query hint support for optimization
2//!
3//! Query hints allow applications to provide guidance to the query optimizer
4//! for better performance. Hints can suggest index usage, join strategies,
5//! and other optimization techniques.
6
7use serde::{Deserialize, Serialize};
8
9/// Query hints for optimization
10#[derive(Debug, Clone, Default)]
11pub struct QueryHints {
12    /// Suggested index to use (SPO, POS, or OSP)
13    pub preferred_index: Option<IndexType>,
14    /// Maximum number of results to return
15    pub limit: Option<usize>,
16    /// Number of results to skip (for pagination)
17    pub offset: Option<usize>,
18    /// Whether to use bloom filter for existence checks
19    pub use_bloom_filter: Option<bool>,
20    /// Whether to enable result caching
21    pub enable_caching: Option<bool>,
22    /// Estimated result size (helps with memory allocation)
23    pub estimated_result_size: Option<usize>,
24    /// Query timeout in milliseconds
25    pub timeout_ms: Option<u64>,
26    /// Whether to collect detailed execution statistics
27    pub collect_stats: bool,
28}
29
30/// Type of index to use for a query
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
32pub enum IndexType {
33    /// Subject-Predicate-Object index (best for S-first patterns)
34    SPO,
35    /// Predicate-Object-Subject index (best for P-first patterns)
36    POS,
37    /// Object-Subject-Predicate index (best for O-first patterns)
38    OSP,
39}
40
41impl QueryHints {
42    /// Create new query hints with defaults
43    pub fn new() -> Self {
44        Self::default()
45    }
46
47    /// Set preferred index
48    pub fn with_index(mut self, index: IndexType) -> Self {
49        self.preferred_index = Some(index);
50        self
51    }
52
53    /// Set result limit
54    pub fn with_limit(mut self, limit: usize) -> Self {
55        self.limit = Some(limit);
56        self
57    }
58
59    /// Set result offset
60    pub fn with_offset(mut self, offset: usize) -> Self {
61        self.offset = Some(offset);
62        self
63    }
64
65    /// Enable/disable bloom filter
66    pub fn with_bloom_filter(mut self, enable: bool) -> Self {
67        self.use_bloom_filter = Some(enable);
68        self
69    }
70
71    /// Enable/disable caching
72    pub fn with_caching(mut self, enable: bool) -> Self {
73        self.enable_caching = Some(enable);
74        self
75    }
76
77    /// Set estimated result size
78    pub fn with_estimated_size(mut self, size: usize) -> Self {
79        self.estimated_result_size = Some(size);
80        self
81    }
82
83    /// Set query timeout
84    pub fn with_timeout(mut self, timeout_ms: u64) -> Self {
85        self.timeout_ms = Some(timeout_ms);
86        self
87    }
88
89    /// Enable statistics collection
90    pub fn with_stats(mut self, collect: bool) -> Self {
91        self.collect_stats = collect;
92        self
93    }
94
95    /// Automatically select the best index based on query pattern
96    ///
97    /// Returns the optimal index type based on which components are bound:
98    /// - If S is bound: use SPO
99    /// - If P is bound but S is not: use POS
100    /// - If O is bound but S and P are not: use OSP
101    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,          // S bound -> SPO
104            (false, true, _) => IndexType::POS,      // P bound, S not -> POS
105            (false, false, true) => IndexType::OSP,  // O bound, S,P not -> OSP
106            (false, false, false) => IndexType::SPO, // All unbound -> SPO (default)
107        }
108    }
109
110    /// Merge with another set of hints (other takes precedence)
111    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    /// Apply pagination to results
125    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/// Query execution statistics
134#[derive(Debug, Clone, Default)]
135pub struct QueryStats {
136    /// Number of pages accessed
137    pub pages_accessed: usize,
138    /// Number of index entries scanned
139    pub index_entries_scanned: usize,
140    /// Number of results found
141    pub results_found: usize,
142    /// Query execution time in microseconds
143    pub execution_time_us: u64,
144    /// Index used for the query
145    pub index_used: Option<IndexType>,
146    /// Whether bloom filter was used
147    pub bloom_filter_used: bool,
148    /// Number of bloom filter false positives
149    pub bloom_false_positives: usize,
150    /// Whether results were cached
151    pub cached: bool,
152}
153
154impl QueryStats {
155    /// Create new query statistics
156    pub fn new() -> Self {
157        Self::default()
158    }
159
160    /// Record the index used
161    pub fn with_index(mut self, index: IndexType) -> Self {
162        self.index_used = Some(index);
163        self
164    }
165
166    /// Record bloom filter usage
167    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    /// Record execution time
174    pub fn with_execution_time(mut self, time_us: u64) -> Self {
175        self.execution_time_us = time_us;
176        self
177    }
178
179    /// Calculate average time per result (in microseconds)
180    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    /// Calculate bloom filter effectiveness (percentage of false positives)
189    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        // S bound -> SPO
223        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        // P bound, S not -> POS
237        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        // O bound, S,P not -> OSP
247        assert_eq!(
248            QueryHints::auto_select_index(false, false, true),
249            IndexType::OSP
250        );
251
252        // All unbound -> SPO (default)
253        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)); // hints2 takes precedence
268        assert_eq!(merged.offset, Some(10)); // from hints1
269        assert_eq!(merged.use_bloom_filter, Some(true)); // from hints2
270    }
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]); // Skip 2, take 3
280    }
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        // 10 false positives out of 100 scans = 90% effectiveness
329        assert_eq!(stats.bloom_filter_effectiveness(), 0.9);
330    }
331}