Skip to main content

fraiseql_cli/commands/
explain.rs

1//! Explain command - show query execution plan and complexity analysis
2//!
3//! Usage: fraiseql explain `<query>` --schema `<schema.compiled.json>` `[--json]`
4
5use anyhow::Result;
6use fraiseql_core::graphql::{complexity::ComplexityAnalyzer, parse_query};
7use serde::Serialize;
8
9use crate::output::CommandResult;
10
11/// Response with execution plan and complexity info
12#[derive(Debug, Serialize)]
13pub struct ExplainResponse {
14    /// The analyzed query string
15    pub query:          String,
16    /// Compiled SQL representation (if available)
17    #[serde(skip_serializing_if = "Option::is_none")]
18    pub sql:            Option<String>,
19    /// Estimated query execution cost
20    pub estimated_cost: usize,
21    /// Complexity metrics
22    pub complexity:     ComplexityInfo,
23    /// Warnings about query structure
24    #[serde(skip_serializing_if = "Vec::is_empty")]
25    pub warnings:       Vec<String>,
26}
27
28/// Complexity analysis metrics for a query
29#[derive(Debug, Serialize)]
30pub struct ComplexityInfo {
31    /// Maximum nesting depth of the query
32    pub depth:       usize,
33    /// Total number of fields requested
34    pub field_count: usize,
35    /// Overall complexity score
36    pub score:       usize,
37}
38
39/// Run explain command
40pub fn run(query: &str) -> Result<CommandResult> {
41    // Parse the query to validate syntax
42    let parsed = parse_query(query)?;
43
44    // Analyze complexity
45    let analyzer = ComplexityAnalyzer::new();
46    let (depth, field_count, score) = analyzer.analyze_complexity(query);
47
48    // Generate warnings for unusual patterns
49    let mut warnings = Vec::new();
50
51    if depth > 10 {
52        warnings.push(format!(
53            "Query depth {depth} exceeds recommended maximum of 10 - consider breaking into multiple queries"
54        ));
55    }
56
57    if field_count > 50 {
58        warnings.push(format!(
59            "Query requests {field_count} fields - consider using pagination or field selection"
60        ));
61    }
62
63    if score > 500 {
64        warnings.push(format!(
65            "Query complexity score {score} is high - consider optimizing query structure"
66        ));
67    }
68
69    // Generate SQL representation (simplified for now)
70    // In a real implementation, this would use the QueryPlanner
71    let sql = format!(
72        "-- Query execution plan for: {}\n-- Depth: {}, Fields: {}, Cost: {}\nSELECT data FROM v_table LIMIT 1000;",
73        parsed.root_field, depth, field_count, score
74    );
75
76    let has_warnings = !warnings.is_empty();
77
78    let response = ExplainResponse {
79        query:          query.to_string(),
80        sql:            Some(sql),
81        estimated_cost: score,
82        complexity:     ComplexityInfo {
83            depth,
84            field_count,
85            score,
86        },
87        warnings:       warnings.clone(),
88    };
89
90    let result = if has_warnings {
91        CommandResult::success_with_warnings("explain", serde_json::to_value(&response)?, warnings)
92    } else {
93        CommandResult::success("explain", serde_json::to_value(&response)?)
94    };
95
96    Ok(result)
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102
103    #[test]
104    fn test_explain_simple_query() {
105        let query = "query { users { id } }";
106        let result = run(query);
107
108        assert!(result.is_ok());
109        let cmd_result = result.unwrap();
110        assert_eq!(cmd_result.status, "success");
111    }
112
113    #[test]
114    fn test_explain_invalid_query_fails() {
115        let query = "query { invalid {";
116        let result = run(query);
117
118        assert!(result.is_err());
119    }
120
121    #[test]
122    fn test_explain_detects_deep_nesting() {
123        let query = "query { a { b { c { d { e { f { g { h { i { j { k { l } } } } } } } } } } } }";
124        let result = run(query);
125
126        assert!(result.is_ok());
127        let cmd_result = result.unwrap();
128        if let Some(warnings) = cmd_result.data {
129            // Should have warnings for deep nesting
130            // Response structure: the data field contains ExplainResponse as JSON
131            assert!(!warnings.to_string().is_empty());
132        }
133    }
134}