use std::fmt::Write as _;
use super::super::Executor;
use crate::{
db::{WhereClause, WhereOperator, traits::DatabaseAdapter},
error::{FraiseQLError, Result},
runtime::explain::ExplainResult,
};
impl<A: DatabaseAdapter> Executor<A> {
pub async fn explain(
&self,
query_name: &str,
variables: Option<&serde_json::Value>,
limit: Option<u32>,
offset: Option<u32>,
) -> Result<ExplainResult> {
if self.ctx.schema.mutations.iter().any(|m| m.name == query_name) {
return Err(FraiseQLError::Validation {
message: format!(
"EXPLAIN ANALYZE is not supported for mutations. \
'{query_name}' is a mutation; only regular queries are supported."
),
path: None,
});
}
let query_def =
self.ctx.schema.queries.iter().find(|q| q.name == query_name).ok_or_else(|| {
let display_names: Vec<String> = self
.ctx
.schema
.queries
.iter()
.map(|q| self.ctx.schema.display_name(&q.name))
.collect();
let candidate_refs: Vec<&str> = display_names.iter().map(String::as_str).collect();
let suggestion = crate::runtime::suggest_similar(query_name, &candidate_refs);
let message = match suggestion.as_slice() {
[s] => format!("Query '{query_name}' not found in schema. Did you mean '{s}'?"),
_ => format!("Query '{query_name}' not found in schema"),
};
FraiseQLError::Validation {
message,
path: None,
}
})?;
let sql_source =
query_def.sql_source.as_ref().ok_or_else(|| FraiseQLError::Validation {
message: format!("Query '{query_name}' has no SQL source"),
path: None,
})?;
let where_clause = build_where_from_variables(variables);
let parameters = collect_parameter_values(variables);
let generated_sql = build_display_sql(sql_source, variables, limit, offset);
let explain_output = self
.ctx
.adapter
.explain_where_query(sql_source, where_clause.as_ref(), limit, offset)
.await?;
Ok(ExplainResult {
query_name: query_name.to_string(),
sql_source: sql_source.clone(),
generated_sql,
parameters,
explain_output,
})
}
}
fn build_where_from_variables(variables: Option<&serde_json::Value>) -> Option<WhereClause> {
let map = variables?.as_object()?;
if map.is_empty() {
return None;
}
let mut conditions: Vec<WhereClause> = map
.iter()
.map(|(k, v)| WhereClause::Field {
path: vec![k.clone()],
operator: WhereOperator::Eq,
value: v.clone(),
})
.collect();
if conditions.len() == 1 {
conditions.pop()
} else {
Some(WhereClause::And(conditions))
}
}
fn collect_parameter_values(variables: Option<&serde_json::Value>) -> Vec<serde_json::Value> {
variables
.and_then(|v| v.as_object())
.map(|map| map.values().cloned().collect())
.unwrap_or_default()
}
pub fn build_display_sql(
sql_source: &str,
variables: Option<&serde_json::Value>,
limit: Option<u32>,
offset: Option<u32>,
) -> String {
let mut sql =
format!("EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) SELECT data FROM \"{sql_source}\"");
if let Some(map) = variables.and_then(|v| v.as_object()) {
if !map.is_empty() {
let conditions: Vec<String> = map
.keys()
.enumerate()
.map(|(i, k)| format!("data->>'{}' = ${}", k, i + 1))
.collect();
sql.push_str(" WHERE ");
sql.push_str(&conditions.join(" AND "));
}
}
let param_offset = variables.and_then(|v| v.as_object()).map_or(0, |m| m.len());
if let Some(lim) = limit {
let _ = write!(sql, " LIMIT ${}", param_offset + 1);
let _ = lim; }
if let Some(off) = offset {
let limit_added = usize::from(limit.is_some());
let _ = write!(sql, " OFFSET ${}", param_offset + limit_added + 1);
let _ = off;
}
sql
}