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::{DEFAULT_MAX_ALIASES, complexity::RequestValidator, 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    /// Overall complexity score (accounts for pagination multipliers)
34    pub score:       usize,
35    /// Number of aliased fields
36    pub alias_count: usize,
37}
38
39/// Run explain command
40///
41/// # Errors
42///
43/// Returns an error if the query cannot be parsed or if complexity analysis
44/// fails. Also propagates errors from JSON serialization of the response.
45pub fn run(query: &str) -> Result<CommandResult> {
46    // Parse the query to validate syntax
47    let parsed = parse_query(query)?;
48
49    // Analyze complexity using the AST-based validator.
50    let validator = RequestValidator::default();
51    let metrics = validator.analyze(query)?;
52
53    let depth = metrics.depth;
54    let score = metrics.complexity;
55    let alias_count = metrics.alias_count;
56
57    // Generate warnings for unusual patterns
58    let mut warnings = Vec::new();
59
60    if depth > 10 {
61        warnings.push(format!(
62            "Query depth {depth} exceeds recommended maximum of 10 - consider breaking into multiple queries"
63        ));
64    }
65
66    if score > 100 {
67        warnings.push(format!(
68            "Query complexity score {score} is high - consider optimizing query structure"
69        ));
70    }
71
72    if alias_count > DEFAULT_MAX_ALIASES {
73        warnings.push(format!("Query has {alias_count} aliases — consider reducing alias count"));
74    }
75
76    // Generate SQL representation (simplified for now)
77    // In a real implementation, this would use the QueryPlanner
78    let sql = format!(
79        "-- Query execution plan for: {}\n-- Depth: {}, Score: {}, Aliases: {}\nSELECT data FROM v_table LIMIT 1000;",
80        parsed.root_field, depth, score, alias_count
81    );
82
83    let has_warnings = !warnings.is_empty();
84
85    let response = ExplainResponse {
86        query:          query.to_string(),
87        sql:            Some(sql),
88        estimated_cost: score,
89        complexity:     ComplexityInfo {
90            depth,
91            score,
92            alias_count,
93        },
94        warnings:       warnings.clone(),
95    };
96
97    let result = if has_warnings {
98        CommandResult::success_with_warnings("explain", serde_json::to_value(&response)?, warnings)
99    } else {
100        CommandResult::success("explain", serde_json::to_value(&response)?)
101    };
102
103    Ok(result)
104}
105
106#[allow(clippy::unwrap_used)] // Reason: test code, panics are acceptable
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    #[test]
112    fn test_explain_simple_query() {
113        let query = "query { users { id } }";
114        let result = run(query);
115
116        let cmd_result = result.unwrap_or_else(|e| panic!("expected Ok for simple query: {e}"));
117        assert_eq!(cmd_result.status, "success");
118    }
119
120    #[test]
121    fn test_explain_invalid_query_fails() {
122        let query = "query { invalid {";
123        let result = run(query);
124
125        assert!(result.is_err(), "expected Err for invalid query, got: {result:?}");
126    }
127
128    #[test]
129    fn test_explain_detects_deep_nesting() {
130        let query = "query { a { b { c { d { e { f { g { h { i { j { k { l } } } } } } } } } } } }";
131        let result = run(query);
132
133        let cmd_result =
134            result.unwrap_or_else(|e| panic!("expected Ok for deep nesting query: {e}"));
135        if let Some(warnings) = cmd_result.data {
136            assert!(!warnings.to_string().is_empty());
137        }
138    }
139}