fraiseql_cli/commands/explain.rs
1//! Explain command - show query complexity analysis
2//!
3//! Usage: fraiseql explain `<query>` `[--json]`
4//!
5//! # What this command does not do
6//!
7//! It does **not** show compiled SQL. It used to publish a `sql` field, documented as
8//! "Compiled SQL representation (if available)", whose value was a hard-coded
9//! `SELECT data FROM v_table LIMIT 1000;` — a relation name that appears nowhere else in the
10//! codebase — with the query text and metrics pasted into a comment header. The command takes
11//! no `--schema` argument, so it could not have produced real SQL even in principle. An
12//! adopter (or an agent consuming `--show-output-schema explain`) who took that as the SQL
13//! FraiseQL would run got `relation "v_table" does not exist` from psql (#868 item 2).
14//!
15//! The field is gone rather than faked. Showing real SQL needs the `--schema` argument the old
16//! module doc already promised plus the `QueryPlanner`; until that exists, this command
17//! reports only what it actually computes: depth, complexity score, alias count and warnings.
18
19use anyhow::Result;
20use fraiseql_core::graphql::{DEFAULT_MAX_ALIASES, complexity::RequestValidator, parse_query};
21use serde::Serialize;
22
23use crate::output::CommandResult;
24
25/// Response with execution plan and complexity info
26#[derive(Debug, Serialize)]
27pub struct ExplainResponse {
28 /// The analyzed query string
29 pub query: String,
30 /// Estimated query execution cost
31 pub estimated_cost: usize,
32 /// Complexity metrics
33 pub complexity: ComplexityInfo,
34 /// Warnings about query structure
35 #[serde(skip_serializing_if = "Vec::is_empty")]
36 pub warnings: Vec<String>,
37}
38
39/// Complexity analysis metrics for a query
40#[derive(Debug, Serialize)]
41pub struct ComplexityInfo {
42 /// Maximum nesting depth of the query
43 pub depth: usize,
44 /// Overall complexity score (accounts for pagination multipliers)
45 pub score: usize,
46 /// Number of aliased fields
47 pub alias_count: usize,
48}
49
50/// Run explain command
51///
52/// # Errors
53///
54/// Returns an error if the query cannot be parsed or if complexity analysis
55/// fails. Also propagates errors from JSON serialization of the response.
56pub fn run(query: &str) -> Result<CommandResult> {
57 // Parse the query to validate syntax
58 let _parsed = parse_query(query)?;
59
60 // Analyze complexity using the AST-based validator.
61 let validator = RequestValidator::default();
62 let metrics = validator.analyze(query)?;
63
64 let depth = metrics.depth;
65 let score = metrics.complexity;
66 let alias_count = metrics.alias_count;
67
68 // Generate warnings for unusual patterns
69 let mut warnings = Vec::new();
70
71 if depth > 10 {
72 warnings.push(format!(
73 "Query depth {depth} exceeds recommended maximum of 10 - consider breaking into multiple queries"
74 ));
75 }
76
77 if score > 100 {
78 warnings.push(format!(
79 "Query complexity score {score} is high - consider optimizing query structure"
80 ));
81 }
82
83 if alias_count > DEFAULT_MAX_ALIASES {
84 warnings.push(format!("Query has {alias_count} aliases — consider reducing alias count"));
85 }
86
87 let has_warnings = !warnings.is_empty();
88
89 let response = ExplainResponse {
90 query: query.to_string(),
91 estimated_cost: score,
92 complexity: ComplexityInfo {
93 depth,
94 score,
95 alias_count,
96 },
97 warnings: warnings.clone(),
98 };
99
100 let result = if has_warnings {
101 CommandResult::success_with_warnings("explain", serde_json::to_value(&response)?, warnings)
102 } else {
103 CommandResult::success("explain", serde_json::to_value(&response)?)
104 };
105
106 Ok(result)
107}