use alloc::boxed::Box;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use crate::parser::{Expr, OrderBy, SelectStatement};
#[derive(Debug, Clone, Copy)]
pub struct TableStats {
pub row_count: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Dispatch {
Cpu,
Gpu,
}
#[derive(Debug, Clone, PartialEq)]
pub enum PlanNode {
Scan {
table: String,
vectorized: bool,
},
Filter {
expr: Expr,
input: Box<PlanNode>,
},
Project {
columns: Vec<String>,
star: bool,
input: Box<PlanNode>,
},
Limit {
n: u64,
input: Box<PlanNode>,
},
Sort {
columns: Vec<OrderBy>,
input: Box<PlanNode>,
},
Aggregate {
group_by: Vec<String>,
aggregates: Vec<(String, crate::parser::AggregateFunc, String)>,
having: Option<Expr>,
input: Box<PlanNode>,
},
SubqueryScan {
plan: Box<Plan>,
alias: String,
},
}
#[derive(Debug, Clone, PartialEq)]
pub struct Plan {
pub root: PlanNode,
pub estimated_rows: u64,
pub dispatch: Dispatch,
}
const VECTORIZE_ROW_THRESHOLD: u64 = 1024;
const GPU_ROW_THRESHOLD: u64 = 1_000_000;
fn selectivity(expr: &Expr) -> (u64, u64) {
match expr {
Expr::Cmp {
op: crate::parser::CmpOp::Eq,
..
} => (1, 10),
Expr::Cmp {
op: crate::parser::CmpOp::Ne,
..
} => (9, 10),
Expr::Cmp { .. } => (1, 3),
Expr::IsNull { negated: false, .. } => (1, 20),
Expr::IsNull { negated: true, .. } => (19, 20),
Expr::Like { .. } => (1, 5),
Expr::InInt { values, .. } => {
let n = values.len().max(1) as u64;
(n, 100)
}
Expr::BetweenInt { .. } => (1, 3),
Expr::CmpColumn {
op: crate::parser::CmpOp::Eq,
..
} => (1, 10),
Expr::CmpColumn { .. } => (1, 3),
Expr::And(l, r) => {
let (a, b) = selectivity(l);
let (c, d) = selectivity(r);
((a * c).max(1), b * d)
}
Expr::Or(l, r) => {
let (a, b) = selectivity(l);
let (c, d) = selectivity(r);
let num = a * d + c * b - a * c;
let den = b * d;
if num >= den {
(1, 1)
} else {
(num.max(1), den)
}
}
Expr::Not(inner) => {
let (a, b) = selectivity(inner);
(b.saturating_sub(a).max(1), b)
}
Expr::Exists { .. } | Expr::InSubquery { .. } | Expr::ScalarCmp { .. } => (1, 3),
Expr::Agg { .. } | Expr::AggCmp { .. } => (1, 3),
Expr::ExtractCmp { .. } => (1, 3),
}
}
pub fn plan_select(stmt: &SelectStatement, stats: TableStats) -> Plan {
let vectorized = stats.row_count >= VECTORIZE_ROW_THRESHOLD;
let mut node = PlanNode::Scan {
table: stmt.table.name().to_string(),
vectorized,
};
let mut estimated = stats.row_count;
if let Some(expr) = &stmt.filter {
let (num, den) = selectivity(expr);
estimated = (estimated * num).div_ceil(den);
node = PlanNode::Filter {
expr: expr.clone(),
input: Box::new(node),
};
}
if !stmt.group_by.is_empty() || !stmt.aggregates.is_empty() {
node = PlanNode::Aggregate {
group_by: stmt.group_by.clone(),
aggregates: stmt.aggregates.clone(),
having: stmt.having.clone(),
input: Box::new(node),
};
if !stmt.group_by.is_empty() {
estimated = estimated.div_ceil(10).max(1);
}
}
node = PlanNode::Project {
columns: stmt.columns.clone(),
star: stmt.star,
input: Box::new(node),
};
if !stmt.order_by.is_empty() {
node = PlanNode::Sort {
columns: stmt.order_by.clone(),
input: Box::new(node),
};
}
if let Some(n) = stmt.limit {
estimated = estimated.min(n);
node = PlanNode::Limit {
n,
input: Box::new(node),
};
}
let dispatch = if cfg!(feature = "gpu") && stats.row_count >= GPU_ROW_THRESHOLD {
Dispatch::Gpu
} else {
Dispatch::Cpu
};
Plan {
root: node,
estimated_rows: estimated,
dispatch,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parser::parse_select;
#[test]
fn plans_scan_project_for_simple_select() {
let stmt = parse_select("SELECT id FROM t").unwrap();
let plan = plan_select(&stmt, TableStats { row_count: 10 });
assert!(matches!(plan.root, PlanNode::Project { .. }));
assert_eq!(plan.dispatch, Dispatch::Cpu);
assert_eq!(plan.estimated_rows, 10);
}
#[test]
fn filter_reduces_estimated_rows() {
let stmt = parse_select("SELECT * FROM t WHERE x = 5").unwrap();
let plan = plan_select(&stmt, TableStats { row_count: 1000 });
assert!(plan.estimated_rows < 1000);
}
#[test]
fn large_tables_are_vectorized() {
let stmt = parse_select("SELECT * FROM big").unwrap();
let plan = plan_select(&stmt, TableStats { row_count: 10_000 });
if let PlanNode::Project { input, .. } = &plan.root {
assert!(matches!(
**input,
PlanNode::Scan {
vectorized: true,
..
}
));
} else {
panic!("expected project at root");
}
}
#[test]
fn limit_caps_estimate() {
let stmt = parse_select("SELECT * FROM t LIMIT 3").unwrap();
let plan = plan_select(&stmt, TableStats { row_count: 1000 });
assert_eq!(plan.estimated_rows, 3);
}
#[test]
fn cpu_dispatch_without_gpu_feature() {
let stmt = parse_select("SELECT * FROM huge").unwrap();
let plan = plan_select(
&stmt,
TableStats {
row_count: 5_000_000,
},
);
if !cfg!(feature = "gpu") {
assert_eq!(plan.dispatch, Dispatch::Cpu);
}
}
#[test]
fn group_by_plans_aggregate_node() {
let stmt = parse_select("SELECT dept, COUNT(*) FROM t GROUP BY dept").unwrap();
let plan = plan_select(&stmt, TableStats { row_count: 100 });
assert!(matches!(plan.root, PlanNode::Project { .. }));
}
#[test]
fn subquery_scan_node_round_trips() {
let stmt = parse_select("SELECT id FROM t").unwrap();
let inner_plan = plan_select(&stmt, TableStats { row_count: 5 });
let node = PlanNode::SubqueryScan {
plan: alloc::boxed::Box::new(inner_plan),
alias: "sub".to_string(),
};
assert!(matches!(node, PlanNode::SubqueryScan { .. }));
}
#[test]
fn order_by_plans_sort_node() {
let stmt = parse_select("SELECT * FROM t ORDER BY x DESC").unwrap();
let plan = plan_select(&stmt, TableStats { row_count: 100 });
match &plan.root {
PlanNode::Sort { .. } => {}
PlanNode::Project { input, .. } => {
assert!(matches!(**input, PlanNode::Sort { .. }));
}
other => panic!("expected Sort or Project wrapping Sort, got {:?}", other),
}
}
}