Skip to main content

fraiseql_core/runtime/executor/
planning.rs

1//! Query planning — `plan_query()` without executing against the database.
2
3use super::{Executor, QueryType};
4use crate::{
5    db::traits::DatabaseAdapter,
6    error::{FraiseQLError, Result},
7    runtime::suggest_similar,
8};
9
10impl<A: DatabaseAdapter> Executor<A> {
11    /// Generate an explain plan for a query without executing it.
12    ///
13    /// Returns the SQL that would be generated, parameters, cost estimate,
14    /// and views that would be accessed.
15    ///
16    /// # Errors
17    ///
18    /// Returns error if the query cannot be parsed or matched against the schema.
19    pub fn plan_query(
20        &self,
21        query: &str,
22        variables: Option<&serde_json::Value>,
23    ) -> Result<super::super::ExplainPlan> {
24        let query_type = self.classify_query(query)?;
25
26        match query_type {
27            QueryType::Regular => {
28                let query_match = self.matcher.match_query(query, variables)?;
29                let view = query_match
30                    .query_def
31                    .sql_source
32                    .clone()
33                    .unwrap_or_else(|| "unknown".to_string());
34                let plan = self.planner.plan(&query_match)?;
35                Ok(super::super::ExplainPlan {
36                    sql:            plan.sql,
37                    parameters:     plan.parameters,
38                    estimated_cost: plan.estimated_cost,
39                    views_accessed: vec![view],
40                    query_type:     "regular".to_string(),
41                })
42            },
43            QueryType::Mutation(ref name) => {
44                let mutation_def =
45                    self.schema.mutations.iter().find(|m| m.name == *name).ok_or_else(|| {
46                        let candidates: Vec<&str> =
47                            self.schema.mutations.iter().map(|m| m.name.as_str()).collect();
48                        let suggestion = suggest_similar(name, &candidates);
49                        let message = match suggestion.as_slice() {
50                            [s] => format!(
51                                "Mutation '{name}' not found in schema. Did you mean '{s}'?"
52                            ),
53                            _ => format!("Mutation '{name}' not found in schema"),
54                        };
55                        FraiseQLError::Validation {
56                            message,
57                            path: None,
58                        }
59                    })?;
60                let fn_name =
61                    mutation_def.sql_source.clone().unwrap_or_else(|| format!("fn_{name}"));
62                Ok(super::super::ExplainPlan {
63                    sql:            format!("SELECT * FROM {fn_name}(...)"),
64                    parameters:     Vec::new(),
65                    estimated_cost: 100,
66                    views_accessed: vec![fn_name],
67                    query_type:     "mutation".to_string(),
68                })
69            },
70            QueryType::Aggregate(ref name) => {
71                let sql_source = self
72                    .schema
73                    .queries
74                    .iter()
75                    .find(|q| q.name == *name)
76                    .and_then(|q| q.sql_source.clone())
77                    .unwrap_or_else(|| "unknown".to_string());
78                Ok(super::super::ExplainPlan {
79                    sql:            format!("SELECT ... FROM {sql_source} -- aggregate"),
80                    parameters:     Vec::new(),
81                    estimated_cost: 200,
82                    views_accessed: vec![sql_source],
83                    query_type:     "aggregate".to_string(),
84                })
85            },
86            QueryType::Window(ref name) => {
87                let sql_source = self
88                    .schema
89                    .queries
90                    .iter()
91                    .find(|q| q.name == *name)
92                    .and_then(|q| q.sql_source.clone())
93                    .unwrap_or_else(|| "unknown".to_string());
94                Ok(super::super::ExplainPlan {
95                    sql:            format!("SELECT ... FROM {sql_source} -- window"),
96                    parameters:     Vec::new(),
97                    estimated_cost: 250,
98                    views_accessed: vec![sql_source],
99                    query_type:     "window".to_string(),
100                })
101            },
102            QueryType::IntrospectionSchema | QueryType::IntrospectionType(_) => {
103                Ok(super::super::ExplainPlan {
104                    sql:            String::new(),
105                    parameters:     Vec::new(),
106                    estimated_cost: 0,
107                    views_accessed: Vec::new(),
108                    query_type:     "introspection".to_string(),
109                })
110            },
111            QueryType::Federation(_) => Ok(super::super::ExplainPlan {
112                sql:            String::new(),
113                parameters:     Vec::new(),
114                estimated_cost: 0,
115                views_accessed: Vec::new(),
116                query_type:     "federation".to_string(),
117            }),
118            QueryType::NodeQuery => Ok(super::super::ExplainPlan {
119                sql:            String::new(),
120                parameters:     Vec::new(),
121                estimated_cost: 50,
122                views_accessed: Vec::new(),
123                query_type:     "node".to_string(),
124            }),
125        }
126    }
127}