Skip to main content

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}