use std::collections::VecDeque;
use rudb_common::{Error, Result};
use rudb_plan::{NodeRef, Plan};
pub trait Pass {
fn name(&self) -> &'static str;
fn run(&self, plan: &mut Plan, context: &Context) -> Result<()>;
}
#[derive(Debug, Clone, Default)]
pub struct Context {
disabled: Vec<&'static str>,
}
impl Context {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn without(names: &str) -> Result<Self> {
let mut context = Self::new();
for name in names.split(',') {
let name = name.trim();
if !name.is_empty() {
context.disable(name)?;
}
}
Ok(context)
}
pub fn disable(&mut self, name: &str) -> Result<()> {
let Some(found) = crate::PASSES.iter().find(|pass| pass.name() == name) else {
let known: Vec<String> =
crate::PASSES.iter().map(|pass| format!("\"{}\"", pass.name())).collect();
return Err(Error::parser(format!(
"Optimizer type \"{name}\" not recognized\n\nCandidate optimizers: {}",
known.join(", ")
)));
};
if !self.is_disabled(name) {
self.disabled.push(found.name());
}
Ok(())
}
#[must_use]
pub fn is_disabled(&self, name: &str) -> bool {
self.disabled.contains(&name)
}
}
pub(crate) fn top_down(plan: &Plan) -> Vec<NodeRef> {
let mut found = Vec::new();
let mut pending = VecDeque::from([plan.root()]);
while let Some(node) = pending.pop_front() {
if found.contains(&node) {
continue;
}
found.push(node);
pending.extend(plan.node(node).children().into_iter().flatten());
}
found
}
#[cfg(test)]
mod tests {
use super::Context;
#[test]
fn every_pass_is_on_unless_it_is_named() {
let context = Context::new();
assert!(!context.is_disabled("expression_rewriter"));
let context = Context::without("expression_rewriter").expect("a name that is a pass");
assert!(context.is_disabled("expression_rewriter"));
assert!(!context.is_disabled("unused_columns"));
}
#[test]
fn a_list_turns_off_each_of_them_and_a_trailing_comma_is_not_an_error() {
let context = Context::without("expression_rewriter, unused_columns,")
.expect("two names and a comma");
assert!(context.is_disabled("expression_rewriter"));
assert!(context.is_disabled("unused_columns"));
}
#[test]
fn naming_the_same_pass_twice_is_naming_it_once() {
let context = Context::without("unused_columns,unused_columns").expect("the same name");
assert!(context.is_disabled("unused_columns"));
}
#[test]
fn a_name_that_is_not_a_pass_is_the_error_duckdb_prints() {
let error = Context::without("bogus").expect_err("not a pass");
assert_eq!(error.code().duckdb_name(), "Parser Error");
assert!(
error.message().starts_with("Optimizer type \"bogus\" not recognized"),
"{}",
error.message()
);
assert!(error.message().contains("Candidate optimizers:"), "{}", error.message());
}
}