use crate::LogicalPlan;
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq)]
pub struct CostEstimate {
pub rows: f64,
pub cpu_cost: f64,
pub io_cost: f64,
pub total_cost: f64,
}
impl CostEstimate {
pub fn zero() -> Self {
Self {
rows: 0.0,
cpu_cost: 0.0,
io_cost: 0.0,
total_cost: 0.0,
}
}
pub fn combine(a: &Self, b: &Self) -> Self {
Self {
rows: a.rows + b.rows,
cpu_cost: a.cpu_cost + b.cpu_cost,
io_cost: a.io_cost + b.io_cost,
total_cost: a.total_cost + b.total_cost,
}
}
}
#[derive(Debug, Clone)]
pub struct TableStats {
pub row_count: u64,
pub avg_row_size_bytes: u64,
pub index_on: Vec<String>,
}
impl Default for TableStats {
fn default() -> Self {
Self {
row_count: 10_000,
avg_row_size_bytes: 100,
index_on: vec![],
}
}
}
pub struct CostModel {
stats: HashMap<String, TableStats>,
cpu_weight: f64,
filter_selectivity: f64,
}
impl Default for CostModel {
fn default() -> Self {
Self::new()
}
}
impl CostModel {
pub fn new() -> Self {
Self {
stats: HashMap::new(),
cpu_weight: 0.01,
filter_selectivity: 0.1,
}
}
pub fn with_table_stats(mut self, table: &str, stats: TableStats) -> Self {
self.stats.insert(table.to_lowercase(), stats);
self
}
pub fn with_cpu_weight(mut self, w: f64) -> Self {
self.cpu_weight = w;
self
}
pub fn with_filter_selectivity(mut self, s: f64) -> Self {
self.filter_selectivity = s;
self
}
fn table_stats(&self, table: &str) -> TableStats {
self.stats
.get(&table.to_lowercase())
.cloned()
.unwrap_or_default()
}
fn total(&self, cpu_cost: f64, io_cost: f64) -> f64 {
io_cost + self.cpu_weight * cpu_cost
}
pub fn estimate(&self, plan: &LogicalPlan) -> CostEstimate {
match plan {
LogicalPlan::Scan { table, .. } => {
let s = self.table_stats(table);
let rows = s.row_count as f64;
let io_cost = rows * s.avg_row_size_bytes as f64 / 8192.0;
let cpu_cost = rows * self.cpu_weight;
CostEstimate {
rows,
cpu_cost,
io_cost,
total_cost: self.total(cpu_cost, io_cost),
}
}
LogicalPlan::Filter { input, .. } => {
let inp = self.estimate(input);
let rows = inp.rows * self.filter_selectivity;
let cpu_cost = inp.cpu_cost + inp.rows * 0.1;
let io_cost = inp.io_cost;
CostEstimate {
rows,
cpu_cost,
io_cost,
total_cost: self.total(cpu_cost, io_cost),
}
}
LogicalPlan::Project { input, .. } => {
let inp = self.estimate(input);
let rows = inp.rows;
let cpu_cost = inp.cpu_cost + rows * 0.01;
let io_cost = inp.io_cost;
CostEstimate {
rows,
cpu_cost,
io_cost,
total_cost: self.total(cpu_cost, io_cost),
}
}
LogicalPlan::Join { left, right, .. } => {
let l = self.estimate(left);
let r = self.estimate(right);
let rows = l.rows * r.rows * 0.1;
let cpu_cost = l.cpu_cost + r.cpu_cost + l.rows + r.rows + rows;
let io_cost = l.io_cost + r.io_cost;
CostEstimate {
rows,
cpu_cost,
io_cost,
total_cost: self.total(cpu_cost, io_cost),
}
}
LogicalPlan::Aggregate { input, .. } => {
let inp = self.estimate(input);
let rows = inp.rows * 0.1;
let cpu_cost = inp.cpu_cost + inp.rows * 0.5;
let io_cost = inp.io_cost;
CostEstimate {
rows,
cpu_cost,
io_cost,
total_cost: self.total(cpu_cost, io_cost),
}
}
LogicalPlan::Sort { input, .. } => {
let inp = self.estimate(input);
let rows = inp.rows;
let sort_cpu = rows * rows.ln().max(1.0) * 0.01;
let cpu_cost = inp.cpu_cost + sort_cpu;
let io_cost = inp.io_cost;
CostEstimate {
rows,
cpu_cost,
io_cost,
total_cost: self.total(cpu_cost, io_cost),
}
}
LogicalPlan::Limit { input, count, .. } => {
let inp = self.estimate(input);
let rows = match count {
Some(c) => inp.rows.min(*c as f64),
None => inp.rows,
};
let cpu_cost = inp.cpu_cost;
let io_cost = inp.io_cost;
CostEstimate {
rows,
cpu_cost,
io_cost,
total_cost: self.total(cpu_cost, io_cost),
}
}
LogicalPlan::Values { .. } | LogicalPlan::Empty => CostEstimate::zero(),
LogicalPlan::SetOp { left, right, .. } => {
let l = self.estimate(left);
let r = self.estimate(right);
CostEstimate::combine(&l, &r)
}
LogicalPlan::Cte { query, .. } => {
let inner = self.estimate(query);
let cpu_cost = inner.cpu_cost + inner.rows * 0.05;
let io_cost = inner.io_cost;
let rows = inner.rows;
CostEstimate {
rows,
cpu_cost,
io_cost,
total_cost: self.total(cpu_cost, io_cost),
}
}
LogicalPlan::CteRef { name } => {
let s = self.table_stats(name);
let rows = s.row_count as f64;
let io_cost = rows * s.avg_row_size_bytes as f64 / 8192.0;
let cpu_cost = rows * self.cpu_weight + rows * 0.05;
CostEstimate {
rows,
cpu_cost,
io_cost,
total_cost: self.total(cpu_cost, io_cost),
}
}
LogicalPlan::Window { input, .. } => {
let inp = self.estimate(input);
let rows = inp.rows;
let cpu_cost = inp.cpu_cost + rows * 0.1;
let io_cost = inp.io_cost;
CostEstimate {
rows,
cpu_cost,
io_cost,
total_cost: self.total(cpu_cost, io_cost),
}
}
LogicalPlan::Subquery { query, .. } => {
let inner = self.estimate(query);
let rows = inner.rows;
let cpu_cost = inner.cpu_cost + rows * 0.2;
let io_cost = inner.io_cost;
CostEstimate {
rows,
cpu_cost,
io_cost,
total_cost: self.total(cpu_cost, io_cost),
}
}
LogicalPlan::Exists { subquery, .. } => {
let inner = self.estimate(subquery);
let rows = 1.0_f64.min(inner.rows);
let cpu_cost = inner.cpu_cost + 0.1;
let io_cost = inner.io_cost;
CostEstimate {
rows,
cpu_cost,
io_cost,
total_cost: self.total(cpu_cost, io_cost),
}
}
LogicalPlan::InSubquery { subquery, .. } => {
let inner = self.estimate(subquery);
let rows = inner.rows * self.filter_selectivity;
let cpu_cost = inner.cpu_cost + inner.rows * 0.15;
let io_cost = inner.io_cost;
CostEstimate {
rows,
cpu_cost,
io_cost,
total_cost: self.total(cpu_cost, io_cost),
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{parse_one, plan_statement};
#[test]
fn test_cost_scan_default() {
let stmt = parse_one("SELECT * FROM users").expect("parse");
let plan = plan_statement(&stmt).expect("plan");
let model = CostModel::new();
let est = model.estimate(&plan);
assert!(
(est.rows - 10_000.0).abs() < 1e-9,
"expected 10_000 rows from default stats, got {}",
est.rows
);
}
#[test]
fn test_cost_scan_custom() {
let stmt = parse_one("SELECT * FROM orders").expect("parse");
let plan = plan_statement(&stmt).expect("plan");
let model = CostModel::new().with_table_stats(
"orders",
TableStats {
row_count: 50_000,
avg_row_size_bytes: 128,
index_on: vec![],
},
);
let est = model.estimate(&plan);
assert!(
(est.rows - 50_000.0).abs() < 1e-9,
"expected 50_000 rows, got {}",
est.rows
);
}
#[test]
fn test_cost_filter_reduces_rows() {
let stmt = parse_one("SELECT * FROM users WHERE active = true").expect("parse");
let plan = plan_statement(&stmt).expect("plan");
let model = CostModel::new();
let est = model.estimate(&plan);
assert!(
est.rows < 10_000.0,
"filter should reduce rows below scan rows, got {}",
est.rows
);
}
#[test]
fn test_cost_join_two_scans() {
let stmt = parse_one("SELECT a.id, b.name FROM a JOIN b ON a.id = b.a_id").expect("parse");
let plan = plan_statement(&stmt).expect("plan");
let model = CostModel::new();
let est = model.estimate(&plan);
let expected = 10_000.0_f64 * 10_000.0 * 0.1;
assert!(
(est.rows - expected).abs() < 1.0,
"join rows expected ~{expected}, got {}",
est.rows
);
}
#[test]
fn test_cost_limit_caps_rows() {
let stmt = parse_one("SELECT * FROM users LIMIT 100").expect("parse");
let plan = plan_statement(&stmt).expect("plan");
let model = CostModel::new();
let est = model.estimate(&plan);
assert!(
(est.rows - 100.0).abs() < 1e-9,
"limit should cap rows to 100, got {}",
est.rows
);
}
#[test]
fn test_cost_aggregate_reduces_rows() {
let stmt = parse_one("SELECT dept, COUNT(*) FROM employees GROUP BY dept").expect("parse");
let plan = plan_statement(&stmt).expect("plan");
let model = CostModel::new();
let est = model.estimate(&plan);
assert!(
est.rows < 10_000.0,
"aggregate should reduce rows, got {}",
est.rows
);
}
#[test]
fn test_cost_sort_preserves_rows() {
let stmt = parse_one("SELECT * FROM products ORDER BY price DESC").expect("parse");
let plan = plan_statement(&stmt).expect("plan");
let model = CostModel::new();
let full_est = model.estimate(&plan);
let stmt_no_sort = parse_one("SELECT * FROM products").expect("parse_no_sort");
let plan_no_sort = plan_statement(&stmt_no_sort).expect("plan_no_sort");
let base_est = model.estimate(&plan_no_sort);
assert!(
(full_est.rows - base_est.rows).abs() < 1e-9,
"sort should preserve row count: {} vs {}",
full_est.rows,
base_est.rows
);
assert!(
full_est.cpu_cost > base_est.cpu_cost,
"sort should increase cpu_cost: {} vs {}",
full_est.cpu_cost,
base_est.cpu_cost
);
}
#[test]
fn test_cost_combines_correctly() {
let a = CostEstimate {
rows: 100.0,
cpu_cost: 5.0,
io_cost: 10.0,
total_cost: 10.05,
};
let b = CostEstimate {
rows: 200.0,
cpu_cost: 3.0,
io_cost: 7.0,
total_cost: 7.03,
};
let c = CostEstimate::combine(&a, &b);
assert!((c.rows - 300.0).abs() < 1e-9, "rows: {}", c.rows);
assert!((c.cpu_cost - 8.0).abs() < 1e-9, "cpu_cost: {}", c.cpu_cost);
assert!((c.io_cost - 17.0).abs() < 1e-9, "io_cost: {}", c.io_cost);
assert!(
(c.total_cost - 17.08).abs() < 1e-9,
"total_cost: {}",
c.total_cost
);
}
}