fraiseql_cli/commands/
explain.rs1use anyhow::Result;
6use fraiseql_core::graphql::{DEFAULT_MAX_ALIASES, complexity::RequestValidator, parse_query};
7use serde::Serialize;
8
9use crate::output::CommandResult;
10
11#[derive(Debug, Serialize)]
13pub struct ExplainResponse {
14 pub query: String,
16 #[serde(skip_serializing_if = "Option::is_none")]
18 pub sql: Option<String>,
19 pub estimated_cost: usize,
21 pub complexity: ComplexityInfo,
23 #[serde(skip_serializing_if = "Vec::is_empty")]
25 pub warnings: Vec<String>,
26}
27
28#[derive(Debug, Serialize)]
30pub struct ComplexityInfo {
31 pub depth: usize,
33 pub score: usize,
35 pub alias_count: usize,
37}
38
39pub fn run(query: &str) -> Result<CommandResult> {
46 let parsed = parse_query(query)?;
48
49 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 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 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)] #[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}