1use crate::LogicalPlan;
20use std::collections::HashMap;
21
22#[derive(Debug, Clone, PartialEq)]
26pub struct CostEstimate {
27 pub rows: f64,
29 pub cpu_cost: f64,
31 pub io_cost: f64,
33 pub total_cost: f64,
35}
36
37impl CostEstimate {
38 pub fn zero() -> Self {
40 Self {
41 rows: 0.0,
42 cpu_cost: 0.0,
43 io_cost: 0.0,
44 total_cost: 0.0,
45 }
46 }
47
48 pub fn combine(a: &Self, b: &Self) -> Self {
52 Self {
53 rows: a.rows + b.rows,
54 cpu_cost: a.cpu_cost + b.cpu_cost,
55 io_cost: a.io_cost + b.io_cost,
56 total_cost: a.total_cost + b.total_cost,
57 }
58 }
59}
60
61#[derive(Debug, Clone)]
65pub struct TableStats {
66 pub row_count: u64,
68 pub avg_row_size_bytes: u64,
70 pub index_on: Vec<String>,
72}
73
74impl Default for TableStats {
75 fn default() -> Self {
76 Self {
77 row_count: 10_000,
78 avg_row_size_bytes: 100,
79 index_on: vec![],
80 }
81 }
82}
83
84pub struct CostModel {
90 stats: HashMap<String, TableStats>,
92 cpu_weight: f64,
94 filter_selectivity: f64,
96}
97
98impl Default for CostModel {
99 fn default() -> Self {
100 Self::new()
101 }
102}
103
104impl CostModel {
105 pub fn new() -> Self {
107 Self {
108 stats: HashMap::new(),
109 cpu_weight: 0.01,
110 filter_selectivity: 0.1,
111 }
112 }
113
114 pub fn with_table_stats(mut self, table: &str, stats: TableStats) -> Self {
118 self.stats.insert(table.to_lowercase(), stats);
119 self
120 }
121
122 pub fn with_cpu_weight(mut self, w: f64) -> Self {
124 self.cpu_weight = w;
125 self
126 }
127
128 pub fn with_filter_selectivity(mut self, s: f64) -> Self {
130 self.filter_selectivity = s;
131 self
132 }
133
134 fn table_stats(&self, table: &str) -> TableStats {
136 self.stats
137 .get(&table.to_lowercase())
138 .cloned()
139 .unwrap_or_default()
140 }
141
142 fn total(&self, cpu_cost: f64, io_cost: f64) -> f64 {
144 io_cost + self.cpu_weight * cpu_cost
145 }
146
147 pub fn estimate(&self, plan: &LogicalPlan) -> CostEstimate {
149 match plan {
150 LogicalPlan::Scan { table, .. } => {
152 let s = self.table_stats(table);
153 let rows = s.row_count as f64;
154 let io_cost = rows * s.avg_row_size_bytes as f64 / 8192.0;
155 let cpu_cost = rows * self.cpu_weight;
156 CostEstimate {
157 rows,
158 cpu_cost,
159 io_cost,
160 total_cost: self.total(cpu_cost, io_cost),
161 }
162 }
163
164 LogicalPlan::Filter { input, .. } => {
166 let inp = self.estimate(input);
167 let rows = inp.rows * self.filter_selectivity;
168 let cpu_cost = inp.cpu_cost + inp.rows * 0.1;
169 let io_cost = inp.io_cost;
170 CostEstimate {
171 rows,
172 cpu_cost,
173 io_cost,
174 total_cost: self.total(cpu_cost, io_cost),
175 }
176 }
177
178 LogicalPlan::Project { input, .. } => {
180 let inp = self.estimate(input);
181 let rows = inp.rows;
182 let cpu_cost = inp.cpu_cost + rows * 0.01;
183 let io_cost = inp.io_cost;
184 CostEstimate {
185 rows,
186 cpu_cost,
187 io_cost,
188 total_cost: self.total(cpu_cost, io_cost),
189 }
190 }
191
192 LogicalPlan::Join { left, right, .. } => {
194 let l = self.estimate(left);
195 let r = self.estimate(right);
196 let rows = l.rows * r.rows * 0.1;
197 let cpu_cost = l.cpu_cost + r.cpu_cost + l.rows + r.rows + rows;
198 let io_cost = l.io_cost + r.io_cost;
199 CostEstimate {
200 rows,
201 cpu_cost,
202 io_cost,
203 total_cost: self.total(cpu_cost, io_cost),
204 }
205 }
206
207 LogicalPlan::Aggregate { input, .. } => {
209 let inp = self.estimate(input);
210 let rows = inp.rows * 0.1;
211 let cpu_cost = inp.cpu_cost + inp.rows * 0.5;
212 let io_cost = inp.io_cost;
213 CostEstimate {
214 rows,
215 cpu_cost,
216 io_cost,
217 total_cost: self.total(cpu_cost, io_cost),
218 }
219 }
220
221 LogicalPlan::Sort { input, .. } => {
223 let inp = self.estimate(input);
224 let rows = inp.rows;
225 let sort_cpu = rows * rows.ln().max(1.0) * 0.01;
226 let cpu_cost = inp.cpu_cost + sort_cpu;
227 let io_cost = inp.io_cost;
228 CostEstimate {
229 rows,
230 cpu_cost,
231 io_cost,
232 total_cost: self.total(cpu_cost, io_cost),
233 }
234 }
235
236 LogicalPlan::Limit { input, count, .. } => {
238 let inp = self.estimate(input);
239 let rows = match count {
240 Some(c) => inp.rows.min(*c as f64),
241 None => inp.rows,
242 };
243 let cpu_cost = inp.cpu_cost;
244 let io_cost = inp.io_cost;
245 CostEstimate {
246 rows,
247 cpu_cost,
248 io_cost,
249 total_cost: self.total(cpu_cost, io_cost),
250 }
251 }
252
253 LogicalPlan::Values { .. } | LogicalPlan::Empty => CostEstimate::zero(),
255
256 LogicalPlan::SetOp { left, right, .. } => {
258 let l = self.estimate(left);
259 let r = self.estimate(right);
260 CostEstimate::combine(&l, &r)
261 }
262
263 LogicalPlan::Cte { query, .. } => {
265 let inner = self.estimate(query);
266 let cpu_cost = inner.cpu_cost + inner.rows * 0.05;
267 let io_cost = inner.io_cost;
268 let rows = inner.rows;
269 CostEstimate {
270 rows,
271 cpu_cost,
272 io_cost,
273 total_cost: self.total(cpu_cost, io_cost),
274 }
275 }
276
277 LogicalPlan::CteRef { name } => {
279 let s = self.table_stats(name);
280 let rows = s.row_count as f64;
281 let io_cost = rows * s.avg_row_size_bytes as f64 / 8192.0;
282 let cpu_cost = rows * self.cpu_weight + rows * 0.05;
283 CostEstimate {
284 rows,
285 cpu_cost,
286 io_cost,
287 total_cost: self.total(cpu_cost, io_cost),
288 }
289 }
290
291 LogicalPlan::Window { input, .. } => {
293 let inp = self.estimate(input);
294 let rows = inp.rows;
295 let cpu_cost = inp.cpu_cost + rows * 0.1;
296 let io_cost = inp.io_cost;
297 CostEstimate {
298 rows,
299 cpu_cost,
300 io_cost,
301 total_cost: self.total(cpu_cost, io_cost),
302 }
303 }
304
305 LogicalPlan::Subquery { query, .. } => {
307 let inner = self.estimate(query);
308 let rows = inner.rows;
309 let cpu_cost = inner.cpu_cost + rows * 0.2;
310 let io_cost = inner.io_cost;
311 CostEstimate {
312 rows,
313 cpu_cost,
314 io_cost,
315 total_cost: self.total(cpu_cost, io_cost),
316 }
317 }
318
319 LogicalPlan::Exists { subquery, .. } => {
321 let inner = self.estimate(subquery);
322 let rows = 1.0_f64.min(inner.rows);
324 let cpu_cost = inner.cpu_cost + 0.1;
325 let io_cost = inner.io_cost;
326 CostEstimate {
327 rows,
328 cpu_cost,
329 io_cost,
330 total_cost: self.total(cpu_cost, io_cost),
331 }
332 }
333
334 LogicalPlan::InSubquery { subquery, .. } => {
336 let inner = self.estimate(subquery);
337 let rows = inner.rows * self.filter_selectivity;
338 let cpu_cost = inner.cpu_cost + inner.rows * 0.15;
339 let io_cost = inner.io_cost;
340 CostEstimate {
341 rows,
342 cpu_cost,
343 io_cost,
344 total_cost: self.total(cpu_cost, io_cost),
345 }
346 }
347 }
348 }
349}
350
351#[cfg(test)]
354mod tests {
355 use super::*;
356 use crate::{parse_one, plan_statement};
357
358 #[test]
360 fn test_cost_scan_default() {
361 let stmt = parse_one("SELECT * FROM users").expect("parse");
362 let plan = plan_statement(&stmt).expect("plan");
363 let model = CostModel::new();
364 let est = model.estimate(&plan);
365 assert!(
367 (est.rows - 10_000.0).abs() < 1e-9,
368 "expected 10_000 rows from default stats, got {}",
369 est.rows
370 );
371 }
372
373 #[test]
375 fn test_cost_scan_custom() {
376 let stmt = parse_one("SELECT * FROM orders").expect("parse");
377 let plan = plan_statement(&stmt).expect("plan");
378 let model = CostModel::new().with_table_stats(
379 "orders",
380 TableStats {
381 row_count: 50_000,
382 avg_row_size_bytes: 128,
383 index_on: vec![],
384 },
385 );
386 let est = model.estimate(&plan);
387 assert!(
388 (est.rows - 50_000.0).abs() < 1e-9,
389 "expected 50_000 rows, got {}",
390 est.rows
391 );
392 }
393
394 #[test]
396 fn test_cost_filter_reduces_rows() {
397 let stmt = parse_one("SELECT * FROM users WHERE active = true").expect("parse");
398 let plan = plan_statement(&stmt).expect("plan");
399 let model = CostModel::new();
400 let est = model.estimate(&plan);
401 assert!(
403 est.rows < 10_000.0,
404 "filter should reduce rows below scan rows, got {}",
405 est.rows
406 );
407 }
408
409 #[test]
411 fn test_cost_join_two_scans() {
412 let stmt = parse_one("SELECT a.id, b.name FROM a JOIN b ON a.id = b.a_id").expect("parse");
413 let plan = plan_statement(&stmt).expect("plan");
414 let model = CostModel::new();
415 let est = model.estimate(&plan);
416 let expected = 10_000.0_f64 * 10_000.0 * 0.1;
417 assert!(
418 (est.rows - expected).abs() < 1.0,
419 "join rows expected ~{expected}, got {}",
420 est.rows
421 );
422 }
423
424 #[test]
426 fn test_cost_limit_caps_rows() {
427 let stmt = parse_one("SELECT * FROM users LIMIT 100").expect("parse");
428 let plan = plan_statement(&stmt).expect("plan");
429 let model = CostModel::new();
430 let est = model.estimate(&plan);
431 assert!(
432 (est.rows - 100.0).abs() < 1e-9,
433 "limit should cap rows to 100, got {}",
434 est.rows
435 );
436 }
437
438 #[test]
440 fn test_cost_aggregate_reduces_rows() {
441 let stmt = parse_one("SELECT dept, COUNT(*) FROM employees GROUP BY dept").expect("parse");
442 let plan = plan_statement(&stmt).expect("plan");
443 let model = CostModel::new();
444 let est = model.estimate(&plan);
445 assert!(
446 est.rows < 10_000.0,
447 "aggregate should reduce rows, got {}",
448 est.rows
449 );
450 }
451
452 #[test]
454 fn test_cost_sort_preserves_rows() {
455 let stmt = parse_one("SELECT * FROM products ORDER BY price DESC").expect("parse");
456 let plan = plan_statement(&stmt).expect("plan");
457 let model = CostModel::new();
458 let full_est = model.estimate(&plan);
459
460 let stmt_no_sort = parse_one("SELECT * FROM products").expect("parse_no_sort");
462 let plan_no_sort = plan_statement(&stmt_no_sort).expect("plan_no_sort");
463 let base_est = model.estimate(&plan_no_sort);
464
465 assert!(
467 (full_est.rows - base_est.rows).abs() < 1e-9,
468 "sort should preserve row count: {} vs {}",
469 full_est.rows,
470 base_est.rows
471 );
472 assert!(
474 full_est.cpu_cost > base_est.cpu_cost,
475 "sort should increase cpu_cost: {} vs {}",
476 full_est.cpu_cost,
477 base_est.cpu_cost
478 );
479 }
480
481 #[test]
483 fn test_cost_combines_correctly() {
484 let a = CostEstimate {
485 rows: 100.0,
486 cpu_cost: 5.0,
487 io_cost: 10.0,
488 total_cost: 10.05,
489 };
490 let b = CostEstimate {
491 rows: 200.0,
492 cpu_cost: 3.0,
493 io_cost: 7.0,
494 total_cost: 7.03,
495 };
496 let c = CostEstimate::combine(&a, &b);
497 assert!((c.rows - 300.0).abs() < 1e-9, "rows: {}", c.rows);
498 assert!((c.cpu_cost - 8.0).abs() < 1e-9, "cpu_cost: {}", c.cpu_cost);
499 assert!((c.io_cost - 17.0).abs() < 1e-9, "io_cost: {}", c.io_cost);
500 assert!(
501 (c.total_cost - 17.08).abs() < 1e-9,
502 "total_cost: {}",
503 c.total_cost
504 );
505 }
506}