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 display_names: Vec<String> = self
47                            .schema
48                            .mutations
49                            .iter()
50                            .map(|m| self.schema.display_name(&m.name))
51                            .collect();
52                        let candidate_refs: Vec<&str> =
53                            display_names.iter().map(String::as_str).collect();
54                        let suggestion = suggest_similar(name, &candidate_refs);
55                        let message = match suggestion.as_slice() {
56                            [s] => format!(
57                                "Mutation '{name}' not found in schema. Did you mean '{s}'?"
58                            ),
59                            _ => format!("Mutation '{name}' not found in schema"),
60                        };
61                        FraiseQLError::Validation {
62                            message,
63                            path: None,
64                        }
65                    })?;
66                let fn_name =
67                    mutation_def.sql_source.clone().unwrap_or_else(|| format!("fn_{name}"));
68                Ok(super::super::ExplainPlan {
69                    sql:            format!("SELECT * FROM {fn_name}(...)"),
70                    parameters:     Vec::new(),
71                    estimated_cost: 100,
72                    views_accessed: vec![fn_name],
73                    query_type:     "mutation".to_string(),
74                })
75            },
76            QueryType::Aggregate(ref name) => {
77                let sql_source = self
78                    .schema
79                    .queries
80                    .iter()
81                    .find(|q| q.name == *name)
82                    .and_then(|q| q.sql_source.clone())
83                    .unwrap_or_else(|| "unknown".to_string());
84                Ok(super::super::ExplainPlan {
85                    sql:            format!("SELECT ... FROM {sql_source} -- aggregate"),
86                    parameters:     Vec::new(),
87                    estimated_cost: 200,
88                    views_accessed: vec![sql_source],
89                    query_type:     "aggregate".to_string(),
90                })
91            },
92            QueryType::Window(ref name) => {
93                let sql_source = self
94                    .schema
95                    .queries
96                    .iter()
97                    .find(|q| q.name == *name)
98                    .and_then(|q| q.sql_source.clone())
99                    .unwrap_or_else(|| "unknown".to_string());
100                Ok(super::super::ExplainPlan {
101                    sql:            format!("SELECT ... FROM {sql_source} -- window"),
102                    parameters:     Vec::new(),
103                    estimated_cost: 250,
104                    views_accessed: vec![sql_source],
105                    query_type:     "window".to_string(),
106                })
107            },
108            QueryType::IntrospectionSchema | QueryType::IntrospectionType(_) => {
109                Ok(super::super::ExplainPlan {
110                    sql:            String::new(),
111                    parameters:     Vec::new(),
112                    estimated_cost: 0,
113                    views_accessed: Vec::new(),
114                    query_type:     "introspection".to_string(),
115                })
116            },
117            QueryType::Federation(_) => Ok(super::super::ExplainPlan {
118                sql:            String::new(),
119                parameters:     Vec::new(),
120                estimated_cost: 0,
121                views_accessed: Vec::new(),
122                query_type:     "federation".to_string(),
123            }),
124            QueryType::NodeQuery { .. } => Ok(super::super::ExplainPlan {
125                sql:            String::new(),
126                parameters:     Vec::new(),
127                estimated_cost: 50,
128                views_accessed: Vec::new(),
129                query_type:     "node".to_string(),
130            }),
131        }
132    }
133}