use crate::adapter::{AstAdapter, Conjunct, Estimate, PlanAxis, StepPlan};
use crate::ast::{Anchor, Axis, Matcher, PathElem, Predicate, Query, Reach};
use crate::value::Value;
use std::fmt::Write as _;
#[derive(Debug, Clone, Default, PartialEq)]
pub struct Budget {
pub rows: Option<u64>,
pub bytes: Option<u64>,
pub requests: Option<u32>,
pub strict: bool,
}
impl Budget {
pub fn exceeded_by(&self, plan: &Plan) -> Option<String> {
let Some(e) = plan.estimate() else {
return self.strict.then(|| {
"the plan's cost is unknown (no estimator answers) and the budget is strict"
.to_string()
});
};
if let Some(max) = self.rows
&& e.rows.1 > max
{
return Some(format!(
"the plan estimates up to {} rows; the budget allows {max}",
e.rows.1
));
}
if let Some(max) = self.bytes
&& let Some(bytes) = e.bytes
&& bytes.1 > max
{
return Some(format!(
"the plan estimates up to {} bytes; the budget allows {max}",
bytes.1
));
}
if let Some(max) = self.requests
&& let Some(n) = e.requests
&& n > max
{
return Some(format!(
"the plan makes {n} requests; the budget allows {max}"
));
}
None
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct PlannedStep {
pub axis: Option<PlanAxis>,
pub name: Option<String>,
pub conjuncts: Vec<(String, Value)>,
pub predicates: usize,
pub scope: Option<Vec<String>>,
pub pushed: Option<Vec<bool>>,
pub estimate: Option<Estimate>,
}
impl PlannedStep {
pub fn residual(&self) -> usize {
let unclaimed = match &self.pushed {
Some(p) => p.iter().filter(|&&b| !b).count(),
None => self.conjuncts.len(),
};
unclaimed + self.predicates.saturating_sub(self.conjuncts.len())
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct BranchPlan {
pub anchored: bool,
pub steps: Vec<PlannedStep>,
}
impl BranchPlan {
pub fn estimate(&self) -> Option<Estimate> {
let mut acc: Option<Estimate> = None;
for s in &self.steps {
let e = s.estimate.as_ref()?;
acc = Some(match acc {
Some(a) => a.then(e),
None => e.clone(),
});
}
acc
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Plan {
pub correlations: Vec<Plan>,
pub branches: Vec<BranchPlan>,
pub stages: usize,
}
impl Plan {
pub fn estimate(&self) -> Option<Estimate> {
let mut acc: Option<Estimate> = None;
let parts = self
.correlations
.iter()
.map(Plan::estimate)
.chain(self.branches.iter().map(BranchPlan::estimate));
for e in parts {
let e = e?;
acc = Some(match acc {
Some(a) => a.beside(&e),
None => e,
});
}
acc
}
pub fn to_kaiv(&self, query: &str) -> String {
let mut w = String::new();
let _ = writeln!(
w,
".!kaiv\n\n# quarb/plan payload — engine-emitted.\n(/meta)"
);
let _ = writeln!(w, "contract={}", kaiv_str("quarb/plan"));
let _ = writeln!(w, "version={}", kaiv_str("1"));
let _ = writeln!(w, "query={}", kaiv_str(query));
let _ = writeln!(w, "()\n");
let mut n = 0;
self.write_steps(&mut w, "", &mut n);
if n > 0 {
let _ = writeln!(w, "[]\n");
}
if let Some(e) = self.estimate() {
let _ = writeln!(w, "(/plan/estimate)");
write_estimate(&mut w, &e);
let _ = writeln!(w, "()");
}
w
}
fn write_steps(&self, w: &mut String, branch: &str, n: &mut usize) {
for (ci, c) in self.correlations.iter().enumerate() {
c.write_steps(w, &format!("{branch}{ci}."), n);
}
for (bi, b) in self.branches.iter().enumerate() {
for (si, s) in b.steps.iter().enumerate() {
*n += 1;
let _ = writeln!(w, "[/plan/@steps]");
let _ = writeln!(w, "branch={}", kaiv_str(&format!("{branch}{bi}")));
let _ = writeln!(w, "!int\nstep={si}");
let _ = writeln!(w, "anchored={}", if b.anchored { "true" } else { "false" });
let _ = writeln!(
w,
"axis={}",
kaiv_str(match s.axis {
Some(PlanAxis::Child) => "child",
Some(PlanAxis::Descendant) => "descendant",
None => "walk",
})
);
if let Some(name) = &s.name {
let _ = writeln!(w, "name={}", kaiv_str(name));
}
if let Some(scope) = &s.scope {
let _ = writeln!(w, "scope={}", kaiv_str(&format!("/{}", scope.join("/"))));
}
for (i, (p, v)) in s.conjuncts.iter().enumerate() {
let pushed = s.pushed.as_ref().and_then(|p| p.get(i)).copied();
let _ = writeln!(
w,
"conjunct={}",
kaiv_str(&format!(
"::{p} = {} ({})",
crate::unparse::operand_text(&crate::ast::Operand::Lit(v.clone())),
match pushed {
Some(true) => "pushed",
Some(false) => "residual",
None => "unknown",
}
))
);
}
let _ = writeln!(w, "!int\nresidual={}", s.residual());
if let Some(e) = &s.estimate {
write_estimate(w, e);
}
}
}
}
}
fn write_estimate(w: &mut String, e: &Estimate) {
let _ = writeln!(
w,
"!int\nrows-min={}\n!int\nrows-max={}",
e.rows.0, e.rows.1
);
if let Some((lo, hi)) = e.bytes {
let _ = writeln!(w, "!int\nbytes-min={lo}\n!int\nbytes-max={hi}");
}
if let Some(n) = e.requests {
let _ = writeln!(w, "!int\nrequests={n}");
}
let _ = writeln!(
w,
"latency={}",
kaiv_str(match e.latency {
crate::adapter::Latency::Memory => "memory",
crate::adapter::Latency::Local => "local",
crate::adapter::Latency::Remote => "remote",
})
);
let _ = writeln!(
w,
"confidence={}",
kaiv_str(match e.confidence {
crate::adapter::Confidence::Guess => "guess",
crate::adapter::Confidence::Bound => "bound",
crate::adapter::Confidence::Exact => "exact",
})
);
}
fn kaiv_str(s: &str) -> String {
if !s.is_empty()
&& s.chars()
.all(|c| c.is_alphanumeric() || matches!(c, '/' | '.' | '-' | '_' | ':'))
{
s.to_string()
} else {
format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\""))
}
}
impl Plan {
pub fn roots(&self) -> Vec<Option<String>> {
let mut out = Vec::new();
for b in self
.branches
.iter()
.chain(self.correlations.iter().flat_map(|c| c.branches.iter()))
{
let first = b.steps.first();
let root = match first {
Some(s) if b.anchored && s.axis == Some(PlanAxis::Child) => s.name.clone(),
_ => None,
};
if !out.contains(&root) {
out.push(root);
}
}
out
}
}
pub fn of(query: &Query, adapter: &dyn AstAdapter) -> Plan {
Plan {
correlations: query.correlations.iter().map(|c| of(c, adapter)).collect(),
branches: query
.branches
.iter()
.map(|b| {
let anchored = matches!(b.anchor, Anchor::Root | Anchor::Current);
BranchPlan {
anchored,
steps: plan_steps(&b.steps, anchored, adapter),
}
})
.collect(),
stages: query.pipeline.len(),
}
}
fn plan_steps(elems: &[PathElem], anchored: bool, adapter: &dyn AstAdapter) -> Vec<PlannedStep> {
let mut scope: Option<Vec<String>> = anchored.then(Vec::new);
let mut out = Vec::new();
for elem in elems {
let PathElem::Step(step) = elem else {
scope = None;
continue;
};
let axis = match step.axis {
Axis::Child => Some(PlanAxis::Child),
Axis::Descendant(Reach::All) => Some(PlanAxis::Descendant),
_ => None,
};
let name = match &step.matcher {
Matcher::Name(n) => Some(n.clone()),
_ => None,
};
let lowered = match step.predicates.first() {
Some(Predicate::Expr(e)) => crate::exec::indexable_equality(e),
_ => None,
};
let conjuncts: Vec<(String, Value)> = lowered
.map(|(p, v)| vec![(p.to_string(), v.clone())])
.unwrap_or_default();
let (pushed, estimate) = match (axis, &scope) {
(Some(axis), Some(scope)) => {
let plan = StepPlan {
axis,
name: name.as_deref(),
conjuncts: conjuncts
.iter()
.map(|(p, v)| Conjunct::Equals(p, v))
.collect(),
limit: None,
order: None,
};
let scope: Vec<&str> = scope.iter().map(String::as_str).collect();
(
adapter.select_plan(&scope, &plan),
adapter.estimate(&scope, &plan),
)
}
_ => (None, None),
};
out.push(PlannedStep {
axis,
name: name.clone(),
conjuncts,
predicates: step.predicates.len(),
scope: scope.clone(),
pushed,
estimate,
});
scope = match (
axis,
name,
step.predicates.is_empty() && step.traits.is_empty(),
scope,
) {
(Some(PlanAxis::Child), Some(n), true, Some(mut s)) => {
s.push(n);
Some(s)
}
_ => None,
};
}
out
}