fraiseql_core/runtime/executor/
explain.rs1use std::fmt::Write as _;
4
5use super::Executor;
6use crate::{
7 db::{WhereClause, WhereOperator, traits::DatabaseAdapter},
8 error::{FraiseQLError, Result},
9 runtime::explain::ExplainResult,
10};
11
12impl<A: DatabaseAdapter> Executor<A> {
13 pub async fn explain(
33 &self,
34 query_name: &str,
35 variables: Option<&serde_json::Value>,
36 limit: Option<u32>,
37 offset: Option<u32>,
38 ) -> Result<ExplainResult> {
39 if self.schema.mutations.iter().any(|m| m.name == query_name) {
41 return Err(FraiseQLError::Validation {
42 message: format!(
43 "EXPLAIN ANALYZE is not supported for mutations. \
44 '{query_name}' is a mutation; only regular queries are supported."
45 ),
46 path: None,
47 });
48 }
49
50 let query_def =
52 self.schema.queries.iter().find(|q| q.name == query_name).ok_or_else(|| {
53 let candidates: Vec<&str> =
54 self.schema.queries.iter().map(|q| q.name.as_str()).collect();
55 let suggestion = crate::runtime::suggest_similar(query_name, &candidates);
56 let message = match suggestion.as_slice() {
57 [s] => format!("Query '{query_name}' not found in schema. Did you mean '{s}'?"),
58 _ => format!("Query '{query_name}' not found in schema"),
59 };
60 FraiseQLError::Validation {
61 message,
62 path: None,
63 }
64 })?;
65
66 let sql_source =
68 query_def.sql_source.as_ref().ok_or_else(|| FraiseQLError::Validation {
69 message: format!("Query '{query_name}' has no SQL source"),
70 path: None,
71 })?;
72
73 let where_clause = build_where_from_variables(variables);
75
76 let parameters = collect_parameter_values(variables);
78
79 let generated_sql = build_display_sql(sql_source, variables, limit, offset);
81
82 let explain_output = self
84 .adapter
85 .explain_where_query(sql_source, where_clause.as_ref(), limit, offset)
86 .await?;
87
88 Ok(ExplainResult {
89 query_name: query_name.to_string(),
90 sql_source: sql_source.clone(),
91 generated_sql,
92 parameters,
93 explain_output,
94 })
95 }
96}
97
98fn build_where_from_variables(variables: Option<&serde_json::Value>) -> Option<WhereClause> {
103 let map = variables?.as_object()?;
104 if map.is_empty() {
105 return None;
106 }
107 let mut conditions: Vec<WhereClause> = map
108 .iter()
109 .map(|(k, v)| WhereClause::Field {
110 path: vec![k.clone()],
111 operator: WhereOperator::Eq,
112 value: v.clone(),
113 })
114 .collect();
115
116 if conditions.len() == 1 {
117 conditions.pop()
118 } else {
119 Some(WhereClause::And(conditions))
120 }
121}
122
123fn collect_parameter_values(variables: Option<&serde_json::Value>) -> Vec<serde_json::Value> {
125 variables
126 .and_then(|v| v.as_object())
127 .map(|map| map.values().cloned().collect())
128 .unwrap_or_default()
129}
130
131fn build_display_sql(
133 sql_source: &str,
134 variables: Option<&serde_json::Value>,
135 limit: Option<u32>,
136 offset: Option<u32>,
137) -> String {
138 let mut sql =
139 format!("EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) SELECT data FROM \"{sql_source}\"");
140
141 if let Some(map) = variables.and_then(|v| v.as_object()) {
142 if !map.is_empty() {
143 let conditions: Vec<String> = map
144 .keys()
145 .enumerate()
146 .map(|(i, k)| format!("data->>'{}' = ${}", k, i + 1))
147 .collect();
148 sql.push_str(" WHERE ");
149 sql.push_str(&conditions.join(" AND "));
150 }
151 }
152
153 let param_offset = variables.and_then(|v| v.as_object()).map_or(0, |m| m.len());
154
155 if let Some(lim) = limit {
156 let _ = write!(sql, " LIMIT ${}", param_offset + 1);
157 let _ = lim; }
159 if let Some(off) = offset {
160 let limit_added = usize::from(limit.is_some());
161 let _ = write!(sql, " OFFSET ${}", param_offset + limit_added + 1);
162 let _ = off;
163 }
164
165 sql
166}
167
168#[cfg(test)]
169mod tests {
170 #![allow(clippy::unwrap_used)] use std::sync::Arc;
173
174 use async_trait::async_trait;
175 use serde_json::json;
176
177 use crate::{
178 db::{
179 DatabaseType, PoolMetrics, WhereClause,
180 types::{JsonbValue, OrderByClause},
181 },
182 error::{FraiseQLError, Result},
183 runtime::Executor,
184 schema::{CompiledSchema, MutationDefinition, QueryDefinition},
185 };
186
187 struct MockAdapter;
189
190 #[async_trait]
194 impl crate::db::traits::DatabaseAdapter for MockAdapter {
195 async fn execute_where_query(
196 &self,
197 _view: &str,
198 _where_clause: Option<&WhereClause>,
199 _limit: Option<u32>,
200 _offset: Option<u32>,
201 _order_by: Option<&[OrderByClause]>,
202 ) -> Result<Vec<JsonbValue>> {
203 Ok(vec![])
204 }
205
206 async fn execute_with_projection(
207 &self,
208 _view: &str,
209 _projection: Option<&crate::schema::SqlProjectionHint>,
210 _where_clause: Option<&WhereClause>,
211 _limit: Option<u32>,
212 _offset: Option<u32>,
213 _order_by: Option<&[OrderByClause]>,
214 ) -> Result<Vec<JsonbValue>> {
215 Ok(vec![])
216 }
217
218 fn database_type(&self) -> DatabaseType {
219 DatabaseType::SQLite
220 }
221
222 async fn health_check(&self) -> Result<()> {
223 Ok(())
224 }
225
226 fn pool_metrics(&self) -> PoolMetrics {
227 PoolMetrics {
228 total_connections: 1,
229 idle_connections: 1,
230 active_connections: 0,
231 waiting_requests: 0,
232 }
233 }
234
235 async fn execute_raw_query(
236 &self,
237 _sql: &str,
238 ) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>> {
239 Ok(vec![])
240 }
241
242 async fn execute_parameterized_aggregate(
243 &self,
244 _sql: &str,
245 _params: &[serde_json::Value],
246 ) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>> {
247 Ok(vec![])
248 }
249 }
250
251 fn make_schema_with_query(name: &str, sql_source: &str) -> CompiledSchema {
252 let mut schema = CompiledSchema::default();
253 let mut qd = QueryDefinition::new(name, "SomeType");
254 qd.sql_source = Some(sql_source.to_string());
255 schema.queries.push(qd);
256 schema
257 }
258
259 fn make_schema_with_mutation(name: &str) -> CompiledSchema {
260 let mut schema = CompiledSchema::default();
261 let mut md = MutationDefinition::new(name, "MutationResponse");
262 md.sql_source = Some(format!("fn_{name}"));
263 schema.mutations.push(md);
264 schema
265 }
266
267 #[tokio::test]
268 async fn test_explain_unknown_query_returns_error() {
269 let schema = make_schema_with_query("users", "v_user");
270 let executor = Executor::new(schema, Arc::new(MockAdapter));
271
272 let err = executor.explain("nonexistent", None, None, None).await.unwrap_err();
273 assert!(
274 matches!(&err, FraiseQLError::Validation { message, .. } if message.contains("nonexistent")),
275 "expected Validation error mentioning the query name, got: {err:?}"
276 );
277 }
278
279 #[tokio::test]
280 async fn test_explain_mutation_returns_error() {
281 let schema = make_schema_with_mutation("createUser");
282 let executor = Executor::new(schema, Arc::new(MockAdapter));
283
284 let err = executor.explain("createUser", None, None, None).await.unwrap_err();
285 assert!(
286 matches!(&err, FraiseQLError::Validation { message, .. } if message.contains("mutation")),
287 "expected Validation error mentioning mutation, got: {err:?}"
288 );
289 }
290
291 #[tokio::test]
292 async fn test_explain_unsupported_adapter_returns_error() {
293 let schema = make_schema_with_query("users", "v_user");
295 let executor = Executor::new(schema, Arc::new(MockAdapter));
296
297 let err = executor
298 .explain("users", Some(&json!({"status": "active"})), Some(10), None)
299 .await
300 .unwrap_err();
301 assert!(
302 matches!(&err, FraiseQLError::Unsupported { .. }),
303 "expected Unsupported error from mock adapter, got: {err:?}"
304 );
305 }
306
307 #[test]
308 fn test_build_display_sql_no_clause() {
309 let sql = super::build_display_sql("v_user", None, None, None);
310 assert_eq!(sql, "EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) SELECT data FROM \"v_user\"");
311 }
312
313 #[test]
314 fn test_build_display_sql_with_limit_offset() {
315 let vars = json!({"status": "active"});
316 let sql = super::build_display_sql("v_user", Some(&vars), Some(10), Some(20));
317 assert!(sql.contains("LIMIT $2"), "should contain LIMIT $2, got: {sql}");
318 assert!(sql.contains("OFFSET $3"), "should contain OFFSET $3, got: {sql}");
319 }
320}