use std::collections::BTreeMap;
use std::fmt;
use arrow_schema::Schema;
use serde::{Deserialize, Serialize};
use super::ScandalError;
use super::profile::{EXECUTION_OPTIONS_KEY, ExecutionProfile, PROFILE_METADATA_KEY};
pub const PLAN_METADATA_KEY: &str = "spiral.execution.plan";
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PlanNode {
pub id: String,
pub operator: String,
pub inputs: Vec<String>,
pub properties: BTreeMap<String, String>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PlanDecision {
pub node: Option<String>,
pub candidate: String,
pub outcome: String,
pub reason: String,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PlanDescription {
pub version: u32,
pub semantic_id: String,
pub physical_id: String,
pub execution_id: Option<String>,
pub logical: Vec<PlanNode>,
pub physical: Vec<PlanNode>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub profile: Option<ExecutionProfile>,
pub decisions: Vec<PlanDecision>,
}
impl PlanDescription {
pub fn from_schema(schema: &Schema) -> Result<Option<Self>, ScandalError> {
let Some(encoded) = schema.metadata().get(PLAN_METADATA_KEY) else {
return Ok(None);
};
if encoded.len() > 4 * 1024 * 1024 {
return Err(invalid("plan description exceeds 4 MiB"));
}
let plan: Self = serde_json::from_str(encoded).map_err(invalid)?;
if plan.version != 1 {
return Err(invalid(format!(
"unsupported plan description version {}",
plan.version
)));
}
Ok(Some(plan))
}
pub fn with_schema(&self, schema: &Schema) -> Result<Schema, ScandalError> {
let mut metadata = schema.metadata().clone();
if self.profile.is_some() {
metadata.insert(PROFILE_METADATA_KEY.into(), "1".into());
} else {
metadata.remove(PROFILE_METADATA_KEY);
}
let encoded = serde_json::to_string(self).map_err(invalid)?;
if encoded.len() > 4 * 1024 * 1024 {
return Err(invalid("plan description exceeds 4 MiB"));
}
metadata.insert(PLAN_METADATA_KEY.into(), encoded);
Ok(schema.clone().with_metadata(metadata))
}
pub fn row_schema(schema: &Schema) -> Schema {
let metadata = schema
.metadata()
.iter()
.filter(|(key, _)| {
key.as_str() != PLAN_METADATA_KEY
&& key.as_str() != EXECUTION_OPTIONS_KEY
&& key.as_str() != PROFILE_METADATA_KEY
})
.map(|(key, value)| (key.clone(), value.clone()))
.collect();
Schema::new_with_metadata(schema.fields().clone(), metadata)
}
}
impl fmt::Display for PlanDescription {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "plan={} semantic={}", self.physical_id, self.semantic_id)?;
match &self.execution_id {
Some(id) => write!(f, " execution={id}")?,
None => f.write_str(" preview")?,
}
for node in &self.physical {
write!(f, "\n {} {}", node.id, node.operator)?;
if !node.inputs.is_empty() {
write!(f, " <- [{}]", node.inputs.join(", "))?;
}
for (name, value) in &node.properties {
write!(f, " {name}=\"{}\"", value.escape_debug())?;
}
}
if let Some(profile) = &self.profile {
write!(f, "\n{profile}")?;
}
if f.alternate() {
for node in &self.logical {
write!(
f,
"\n logical {} {} <- [{}]",
node.id,
node.operator,
node.inputs.join(", ")
)?;
for (name, value) in &node.properties {
write!(f, " {name}=\"{}\"", value.escape_debug())?;
}
}
for decision in &self.decisions {
write!(
f,
"\n candidate {} {} {} ({})",
decision.node.as_deref().unwrap_or("unmapped"),
decision.candidate,
decision.outcome,
decision.reason
)?;
}
}
Ok(())
}
}
fn invalid(error: impl fmt::Display) -> ScandalError {
ScandalError::InvalidArgument {
message: error.to_string(),
}
}