Skip to main content

sqry_core/query/
plan.rs

1//! Query execution plan types for --explain output
2
3use serde::{Deserialize, Serialize};
4
5/// Query execution plan with optimization details
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct QueryPlan {
8    /// Schema version for forward compatibility
9    pub schema_version: u32,
10    /// Original query string
11    pub original_query: String,
12    /// Optimized query (with predicate reordering)
13    pub optimized_query: String,
14    /// Execution steps with timing
15    pub steps: Vec<ExecutionStep>,
16    /// Total execution time in milliseconds
17    pub execution_time_ms: u64,
18    /// Whether index was used
19    pub used_index: bool,
20    /// Cache hit/miss status
21    pub cache_status: CacheStatus,
22}
23
24/// Single step in query execution
25#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct ExecutionStep {
27    /// Step number (1-indexed)
28    pub step_num: usize,
29    /// Operation description
30    pub operation: String,
31    /// Number of results after this step
32    pub result_count: usize,
33    /// Time taken for this step in milliseconds
34    pub time_ms: u64,
35}
36
37/// Cache status for parse and result caches
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct CacheStatus {
40    /// Parse cache hit
41    pub parse_cache_hit: bool,
42    /// Result cache hit
43    pub result_cache_hit: bool,
44}
45
46impl QueryPlan {
47    /// Current schema version
48    pub const SCHEMA_VERSION: u32 = 1;
49
50    /// Create new query plan
51    #[must_use]
52    pub fn new(
53        original_query: String,
54        optimized_query: String,
55        steps: Vec<ExecutionStep>,
56        execution_time_ms: u64,
57        used_index: bool,
58        cache_status: CacheStatus,
59    ) -> Self {
60        Self {
61            schema_version: Self::SCHEMA_VERSION,
62            original_query,
63            optimized_query,
64            steps,
65            execution_time_ms,
66            used_index,
67            cache_status,
68        }
69    }
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75
76    // QueryPlan tests
77    #[test]
78    fn test_query_plan_creation() {
79        let plan = QueryPlan::new(
80            "kind:function".to_string(),
81            "kind:function".to_string(),
82            vec![],
83            10,
84            true,
85            CacheStatus {
86                parse_cache_hit: false,
87                result_cache_hit: false,
88            },
89        );
90        assert_eq!(plan.schema_version, QueryPlan::SCHEMA_VERSION);
91        assert_eq!(plan.original_query, "kind:function");
92        assert_eq!(plan.execution_time_ms, 10);
93        assert!(plan.used_index);
94    }
95
96    #[test]
97    fn test_query_plan_with_steps() {
98        let steps = vec![
99            ExecutionStep {
100                step_num: 1,
101                operation: "Parse query".to_string(),
102                result_count: 0,
103                time_ms: 2,
104            },
105            ExecutionStep {
106                step_num: 2,
107                operation: "Index lookup".to_string(),
108                result_count: 100,
109                time_ms: 5,
110            },
111            ExecutionStep {
112                step_num: 3,
113                operation: "Filter results".to_string(),
114                result_count: 25,
115                time_ms: 3,
116            },
117        ];
118
119        let plan = QueryPlan::new(
120            "kind:function name:test".to_string(),
121            "name:test kind:function".to_string(),
122            steps.clone(),
123            10,
124            true,
125            CacheStatus {
126                parse_cache_hit: false,
127                result_cache_hit: false,
128            },
129        );
130
131        assert_eq!(plan.steps.len(), 3);
132        assert_eq!(plan.steps[0].step_num, 1);
133        assert_eq!(plan.steps[1].result_count, 100);
134        assert_eq!(plan.steps[2].operation, "Filter results");
135    }
136
137    #[test]
138    fn test_query_plan_optimized_differs() {
139        let plan = QueryPlan::new(
140            "visibility:public name:foo".to_string(),
141            "name:foo visibility:public".to_string(), // Reordered
142            vec![],
143            15,
144            true,
145            CacheStatus {
146                parse_cache_hit: true,
147                result_cache_hit: false,
148            },
149        );
150
151        assert_ne!(plan.original_query, plan.optimized_query);
152        assert!(plan.original_query.starts_with("visibility"));
153        assert!(plan.optimized_query.starts_with("name"));
154    }
155
156    #[test]
157    fn test_query_plan_no_index() {
158        let plan = QueryPlan::new(
159            "content~=/regex/".to_string(),
160            "content~=/regex/".to_string(),
161            vec![],
162            100,
163            false, // Regex search can't use index
164            CacheStatus {
165                parse_cache_hit: false,
166                result_cache_hit: false,
167            },
168        );
169
170        assert!(!plan.used_index);
171    }
172
173    #[test]
174    fn test_query_plan_clone() {
175        let plan = QueryPlan::new(
176            "kind:class".to_string(),
177            "kind:class".to_string(),
178            vec![ExecutionStep {
179                step_num: 1,
180                operation: "test".to_string(),
181                result_count: 10,
182                time_ms: 5,
183            }],
184            5,
185            true,
186            CacheStatus {
187                parse_cache_hit: true,
188                result_cache_hit: true,
189            },
190        );
191
192        let cloned = plan.clone();
193        assert_eq!(plan.original_query, cloned.original_query);
194        assert_eq!(plan.steps.len(), cloned.steps.len());
195        assert_eq!(
196            plan.cache_status.parse_cache_hit,
197            cloned.cache_status.parse_cache_hit
198        );
199    }
200
201    #[test]
202    fn test_query_plan_debug() {
203        let plan = QueryPlan::new(
204            "test".to_string(),
205            "test".to_string(),
206            vec![],
207            0,
208            false,
209            CacheStatus {
210                parse_cache_hit: false,
211                result_cache_hit: false,
212            },
213        );
214
215        let debug_str = format!("{:?}", plan);
216        assert!(debug_str.contains("QueryPlan"));
217        assert!(debug_str.contains("schema_version"));
218    }
219
220    #[test]
221    fn test_query_plan_schema_version_constant() {
222        assert_eq!(QueryPlan::SCHEMA_VERSION, 1);
223    }
224
225    #[test]
226    fn test_query_plan_serde_json() {
227        let plan = QueryPlan::new(
228            "kind:function".to_string(),
229            "kind:function".to_string(),
230            vec![ExecutionStep {
231                step_num: 1,
232                operation: "test".to_string(),
233                result_count: 5,
234                time_ms: 2,
235            }],
236            10,
237            true,
238            CacheStatus {
239                parse_cache_hit: false,
240                result_cache_hit: true,
241            },
242        );
243
244        let json = serde_json::to_string(&plan).unwrap();
245        assert!(json.contains("\"schema_version\":1"));
246        assert!(json.contains("\"original_query\":\"kind:function\""));
247
248        let parsed: QueryPlan = serde_json::from_str(&json).unwrap();
249        assert_eq!(parsed.original_query, plan.original_query);
250        assert_eq!(parsed.steps.len(), 1);
251    }
252
253    // ExecutionStep tests
254    #[test]
255    fn test_execution_step() {
256        let step = ExecutionStep {
257            step_num: 1,
258            operation: "Parse query".to_string(),
259            result_count: 0,
260            time_ms: 1,
261        };
262        assert_eq!(step.step_num, 1);
263        assert_eq!(step.operation, "Parse query");
264    }
265
266    #[test]
267    fn test_execution_step_clone() {
268        let step = ExecutionStep {
269            step_num: 2,
270            operation: "Index lookup".to_string(),
271            result_count: 50,
272            time_ms: 10,
273        };
274
275        let cloned = step.clone();
276        assert_eq!(step.step_num, cloned.step_num);
277        assert_eq!(step.operation, cloned.operation);
278        assert_eq!(step.result_count, cloned.result_count);
279        assert_eq!(step.time_ms, cloned.time_ms);
280    }
281
282    #[test]
283    fn test_execution_step_debug() {
284        let step = ExecutionStep {
285            step_num: 1,
286            operation: "test".to_string(),
287            result_count: 0,
288            time_ms: 0,
289        };
290
291        let debug_str = format!("{:?}", step);
292        assert!(debug_str.contains("ExecutionStep"));
293        assert!(debug_str.contains("step_num"));
294        assert!(debug_str.contains("operation"));
295    }
296
297    #[test]
298    fn test_execution_step_serde_json() {
299        let step = ExecutionStep {
300            step_num: 3,
301            operation: "Filter".to_string(),
302            result_count: 100,
303            time_ms: 15,
304        };
305
306        let json = serde_json::to_string(&step).unwrap();
307        assert!(json.contains("\"step_num\":3"));
308        assert!(json.contains("\"operation\":\"Filter\""));
309
310        let parsed: ExecutionStep = serde_json::from_str(&json).unwrap();
311        assert_eq!(parsed.step_num, step.step_num);
312        assert_eq!(parsed.result_count, step.result_count);
313    }
314
315    // CacheStatus tests
316    #[test]
317    fn test_cache_status() {
318        let status = CacheStatus {
319            parse_cache_hit: true,
320            result_cache_hit: false,
321        };
322        assert!(status.parse_cache_hit);
323        assert!(!status.result_cache_hit);
324    }
325
326    #[test]
327    fn test_cache_status_both_hit() {
328        let status = CacheStatus {
329            parse_cache_hit: true,
330            result_cache_hit: true,
331        };
332        assert!(status.parse_cache_hit);
333        assert!(status.result_cache_hit);
334    }
335
336    #[test]
337    fn test_cache_status_both_miss() {
338        let status = CacheStatus {
339            parse_cache_hit: false,
340            result_cache_hit: false,
341        };
342        assert!(!status.parse_cache_hit);
343        assert!(!status.result_cache_hit);
344    }
345
346    #[test]
347    fn test_cache_status_clone() {
348        let status = CacheStatus {
349            parse_cache_hit: true,
350            result_cache_hit: false,
351        };
352
353        let cloned = status.clone();
354        assert_eq!(status.parse_cache_hit, cloned.parse_cache_hit);
355        assert_eq!(status.result_cache_hit, cloned.result_cache_hit);
356    }
357
358    #[test]
359    fn test_cache_status_debug() {
360        let status = CacheStatus {
361            parse_cache_hit: false,
362            result_cache_hit: true,
363        };
364
365        let debug_str = format!("{:?}", status);
366        assert!(debug_str.contains("CacheStatus"));
367        assert!(debug_str.contains("parse_cache_hit"));
368        assert!(debug_str.contains("result_cache_hit"));
369    }
370
371    #[test]
372    fn test_cache_status_serde_json() {
373        let status = CacheStatus {
374            parse_cache_hit: true,
375            result_cache_hit: false,
376        };
377
378        let json = serde_json::to_string(&status).unwrap();
379        assert!(json.contains("\"parse_cache_hit\":true"));
380        assert!(json.contains("\"result_cache_hit\":false"));
381
382        let parsed: CacheStatus = serde_json::from_str(&json).unwrap();
383        assert_eq!(parsed.parse_cache_hit, status.parse_cache_hit);
384        assert_eq!(parsed.result_cache_hit, status.result_cache_hit);
385    }
386}