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>,
statistics: crate::estimate::Statistics,
}
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 name = name.to_ascii_lowercase();
if !crate::UPSTREAM.contains(&name.as_str()) {
let known: Vec<String> =
crate::UPSTREAM.iter().map(|known| format!("\"{known}\"")).collect();
return Err(Error::parser(format!(
"Optimizer type \"{name}\" not recognized\n\nCandidate optimizers: {}",
known.join(", ")
)));
}
let Some(found) = crate::PASSES.iter().find(|pass| pass.name() == name) else {
return Ok(());
};
if !self.is_disabled(&name) {
self.disabled.push(found.name());
}
Ok(())
}
pub fn tidy(names: &str) -> Result<String> {
let mut kept: Vec<String> = Vec::new();
for name in names.split(',') {
let name = name.trim().to_ascii_lowercase();
if name.is_empty() {
continue;
}
Self::new().disable(&name)?;
if !kept.contains(&name) {
kept.push(name);
}
}
kept.sort();
Ok(kept.join(","))
}
#[must_use]
pub fn is_disabled(&self, name: &str) -> bool {
self.disabled.contains(&name)
}
pub fn measure(&mut self, statistics: crate::estimate::Statistics) {
self.statistics = statistics;
}
#[must_use]
pub fn statistics(&self) -> &crate::estimate::Statistics {
&self.statistics
}
}
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_duckdb_has_and_rudb_has_not_built_turns_nothing_off_and_is_not_an_error() {
let context = Context::without("join_order,unused_columns").expect("both are names");
assert!(context.is_disabled("unused_columns"));
assert!(!context.is_disabled("join_order"), "there is no such pass to have turned off");
}
#[test]
fn the_name_is_matched_without_regard_to_case() {
let context = Context::without("UNUSED_COLUMNS").expect("a name in capitals");
assert!(context.is_disabled("unused_columns"));
}
#[test]
fn the_tidy_text_is_trimmed_lowered_deduplicated_and_sorted() {
assert_eq!(
Context::tidy(" TOP_N , join_order , top_n ,").expect("three names and a comma"),
"join_order,top_n"
);
assert_eq!(Context::tidy("").expect("nothing is nothing"), "");
assert_eq!(Context::tidy(" , ").expect("still nothing"), "");
}
#[test]
fn the_tidy_text_complains_about_the_same_names_the_toggle_does() {
let error = Context::tidy("top_n,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()
);
}
#[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());
}
}