fraiseql_cli/commands/
explain.rs1use anyhow::Result;
6use fraiseql_core::graphql::{complexity::ComplexityAnalyzer, parse_query};
7use serde::{Deserialize, Serialize};
8
9use crate::output::CommandResult;
10
11#[derive(Debug, Deserialize)]
13#[allow(dead_code)]
14pub struct ExplainRequest {
15 pub query: String,
17}
18
19#[derive(Debug, Serialize)]
21pub struct ExplainResponse {
22 pub query: String,
24 #[serde(skip_serializing_if = "Option::is_none")]
26 pub sql: Option<String>,
27 pub estimated_cost: usize,
29 pub complexity: ComplexityInfo,
31 #[serde(skip_serializing_if = "Vec::is_empty")]
33 pub warnings: Vec<String>,
34}
35
36#[derive(Debug, Serialize)]
38pub struct ComplexityInfo {
39 pub depth: usize,
41 pub field_count: usize,
43 pub score: usize,
45}
46
47pub fn run(query: &str) -> Result<CommandResult> {
49 let parsed = parse_query(query)?;
51
52 let analyzer = ComplexityAnalyzer::new();
54 let (depth, field_count, score) = analyzer.analyze_complexity(query);
55
56 let mut warnings = Vec::new();
58
59 if depth > 10 {
60 warnings.push(format!(
61 "Query depth {depth} exceeds recommended maximum of 10 - consider breaking into multiple queries"
62 ));
63 }
64
65 if field_count > 50 {
66 warnings.push(format!(
67 "Query requests {field_count} fields - consider using pagination or field selection"
68 ));
69 }
70
71 if score > 500 {
72 warnings.push(format!(
73 "Query complexity score {score} is high - consider optimizing query structure"
74 ));
75 }
76
77 let sql = format!(
80 "-- Query execution plan for: {}\n-- Depth: {}, Fields: {}, Cost: {}\nSELECT data FROM v_table LIMIT 1000;",
81 parsed.root_field, depth, field_count, score
82 );
83
84 let has_warnings = !warnings.is_empty();
85
86 let response = ExplainResponse {
87 query: query.to_string(),
88 sql: Some(sql),
89 estimated_cost: score,
90 complexity: ComplexityInfo {
91 depth,
92 field_count,
93 score,
94 },
95 warnings: warnings.clone(),
96 };
97
98 let result = if has_warnings {
99 CommandResult::success_with_warnings("explain", serde_json::to_value(&response)?, warnings)
100 } else {
101 CommandResult::success("explain", serde_json::to_value(&response)?)
102 };
103
104 Ok(result)
105}
106
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 assert!(result.is_ok());
117 let cmd_result = result.unwrap();
118 assert_eq!(cmd_result.status, "success");
119 }
120
121 #[test]
122 fn test_explain_invalid_query_fails() {
123 let query = "query { invalid {";
124 let result = run(query);
125
126 assert!(result.is_err());
127 }
128
129 #[test]
130 fn test_explain_detects_deep_nesting() {
131 let query = "query { a { b { c { d { e { f { g { h { i { j { k { l } } } } } } } } } } } }";
132 let result = run(query);
133
134 assert!(result.is_ok());
135 let cmd_result = result.unwrap();
136 if let Some(warnings) = cmd_result.data {
137 assert!(!warnings.to_string().is_empty());
140 }
141 }
142}