Skip to main content

fraiseql_cli/commands/
cost.rs

1//! Cost command - lightweight complexity scoring for queries
2//!
3//! Usage: fraiseql cost `<query>` `[--json]`
4
5use anyhow::Result;
6// Suppress unused import warning
7#[allow(unused_imports)]
8use fraiseql_core::graphql::complexity;
9use fraiseql_core::graphql::{complexity::ComplexityAnalyzer, parse_query};
10use serde::Serialize;
11
12use crate::output::CommandResult;
13
14/// Response with cost estimation
15#[derive(Debug, Serialize)]
16pub struct CostResponse {
17    /// The GraphQL query being analyzed
18    pub query:            String,
19    /// Complexity score based on query depth and breadth
20    pub complexity_score: usize,
21    /// Estimated execution cost
22    pub estimated_cost:   usize,
23    /// Maximum query depth
24    pub depth:            usize,
25    /// Total number of fields requested
26    pub field_count:      usize,
27}
28
29/// Run cost command (minimal complexity analysis)
30pub fn run(query: &str) -> Result<CommandResult> {
31    // Validate query syntax
32    let _parsed = parse_query(query)?;
33
34    // Quick complexity analysis
35    let analyzer = ComplexityAnalyzer::new();
36    let (depth, field_count, score) = analyzer.analyze_complexity(query);
37
38    let response = CostResponse {
39        query: query.to_string(),
40        complexity_score: score,
41        estimated_cost: depth * 25, // Rough cost estimation
42        depth,
43        field_count,
44    };
45
46    Ok(CommandResult::success("cost", serde_json::to_value(&response)?))
47}
48
49#[cfg(test)]
50mod tests {
51    use super::*;
52
53    #[test]
54    fn test_cost_simple_query() {
55        let query = "query { users { id } }";
56        let result = run(query);
57
58        assert!(result.is_ok());
59        let cmd_result = result.unwrap();
60        assert_eq!(cmd_result.status, "success");
61    }
62
63    #[test]
64    fn test_cost_invalid_query_fails() {
65        let query = "query { invalid {";
66        let result = run(query);
67
68        assert!(result.is_err());
69    }
70
71    #[test]
72    fn test_cost_provides_score() {
73        let query = "query { users { id name } }";
74        let result = run(query);
75
76        assert!(result.is_ok());
77        let cmd_result = result.unwrap();
78        if let Some(data) = cmd_result.data {
79            assert!(data["complexity_score"].is_number());
80        }
81    }
82
83    #[test]
84    fn test_cost_more_fields_higher_score() {
85        let few_fields = run("query { users { id } }").unwrap();
86        let many_fields = run("query { users { id name email phone address } }").unwrap();
87
88        let few_score = few_fields
89            .data
90            .as_ref()
91            .and_then(|d| d["complexity_score"].as_u64())
92            .unwrap_or(0);
93        let many_score = many_fields
94            .data
95            .as_ref()
96            .and_then(|d| d["complexity_score"].as_u64())
97            .unwrap_or(0);
98
99        assert!(many_score >= few_score);
100    }
101
102    #[test]
103    fn test_cost_nested_has_higher_score() {
104        let shallow = run("query { users { id } }").unwrap();
105        let deep = run("query { users { posts { comments { author } } } }").unwrap();
106
107        let shallow_score =
108            shallow.data.as_ref().and_then(|d| d["complexity_score"].as_u64()).unwrap_or(0);
109        let deep_score =
110            deep.data.as_ref().and_then(|d| d["complexity_score"].as_u64()).unwrap_or(0);
111
112        assert!(deep_score > shallow_score);
113    }
114}