1use std::collections::HashSet;
2
3use crate::expr::{Expr as E, Operator};
4use crate::lazy::{LogicalPlan, ProjectionKind};
5use crate::Expr;
6
7pub struct Optimizer;
9
10impl Optimizer {
11 pub fn optimize(plan: &LogicalPlan) -> LogicalPlan {
13 let plan = predicate_pushdown(plan.clone());
14 projection_pushdown(plan)
15 }
16}
17
18fn predicate_pushdown(plan: LogicalPlan) -> LogicalPlan {
19 match plan {
20 LogicalPlan::Filter { input, predicate } => {
21 let input = predicate_pushdown(*input);
22
23 match input {
24 LogicalPlan::Filter {
25 input: inner,
26 predicate: inner_predicate,
27 } => {
28 let combined = and_expr(inner_predicate, predicate);
29 predicate_pushdown(LogicalPlan::Filter {
30 input: inner,
31 predicate: combined,
32 })
33 }
34 LogicalPlan::Projection { input, exprs, kind } => {
35 if can_push_filter_through_projection(&predicate, &exprs, &kind) {
36 predicate_pushdown(LogicalPlan::Projection {
37 input: Box::new(LogicalPlan::Filter { input, predicate }),
38 exprs,
39 kind,
40 })
41 } else {
42 LogicalPlan::Filter {
43 input: Box::new(LogicalPlan::Projection { input, exprs, kind }),
44 predicate,
45 }
46 }
47 }
48 LogicalPlan::CsvScan {
49 path,
50 predicate: existing,
51 projection,
52 } => LogicalPlan::CsvScan {
53 path,
54 predicate: Some(match existing {
55 Some(existing) => and_expr(existing, predicate),
56 None => predicate,
57 }),
58 projection,
59 },
60 LogicalPlan::ParquetScan {
61 path,
62 predicate: existing,
63 projection,
64 } => LogicalPlan::ParquetScan {
65 path,
66 predicate: Some(match existing {
67 Some(existing) => and_expr(existing, predicate),
68 None => predicate,
69 }),
70 projection,
71 },
72 other => LogicalPlan::Filter {
73 input: Box::new(other),
74 predicate,
75 },
76 }
77 }
78 LogicalPlan::Projection { input, exprs, kind } => LogicalPlan::Projection {
79 input: Box::new(predicate_pushdown(*input)),
80 exprs,
81 kind,
82 },
83 LogicalPlan::Aggregate {
84 input,
85 group_by,
86 aggs,
87 } => LogicalPlan::Aggregate {
88 input: Box::new(predicate_pushdown(*input)),
89 group_by,
90 aggs,
91 },
92 LogicalPlan::Join {
93 left,
94 right,
95 keys,
96 how,
97 } => LogicalPlan::Join {
98 left: Box::new(predicate_pushdown(*left)),
99 right: Box::new(predicate_pushdown(*right)),
100 keys,
101 how,
102 },
103 LogicalPlan::Sort { input, options } => LogicalPlan::Sort {
104 input: Box::new(predicate_pushdown(*input)),
105 options,
106 },
107 LogicalPlan::Slice {
108 input,
109 offset,
110 len,
111 from_end,
112 } => LogicalPlan::Slice {
113 input: Box::new(predicate_pushdown(*input)),
114 offset,
115 len,
116 from_end,
117 },
118 LogicalPlan::Unique { input, subset } => LogicalPlan::Unique {
119 input: Box::new(predicate_pushdown(*input)),
120 subset,
121 },
122 LogicalPlan::FillNull { input, fill } => LogicalPlan::FillNull {
123 input: Box::new(predicate_pushdown(*input)),
124 fill,
125 },
126 LogicalPlan::DropNulls { input, subset } => LogicalPlan::DropNulls {
127 input: Box::new(predicate_pushdown(*input)),
128 subset,
129 },
130 LogicalPlan::NullCount { input } => LogicalPlan::NullCount {
131 input: Box::new(predicate_pushdown(*input)),
132 },
133 LogicalPlan::Explode { input, column } => LogicalPlan::Explode {
134 input: Box::new(predicate_pushdown(*input)),
135 column,
136 },
137 LogicalPlan::Implode { input } => LogicalPlan::Implode {
138 input: Box::new(predicate_pushdown(*input)),
139 },
140 other => other,
141 }
142}
143
144fn and_expr(left: Expr, right: Expr) -> Expr {
145 let mut conjuncts = Vec::new();
146 conjuncts.extend(flatten_and(left));
147 conjuncts.extend(flatten_and(right));
148 build_and(conjuncts)
149}
150
151fn flatten_and(expr: Expr) -> Vec<Expr> {
152 match expr {
153 E::BinaryOp {
154 left,
155 op: Operator::And,
156 right,
157 } => {
158 let mut out = flatten_and(*left);
159 out.extend(flatten_and(*right));
160 out
161 }
162 other => vec![other],
163 }
164}
165
166fn build_and(mut conjuncts: Vec<Expr>) -> Expr {
167 let first = conjuncts
168 .pop()
169 .expect("build_and must be called with non-empty conjuncts");
170 conjuncts
171 .into_iter()
172 .rev()
173 .fold(first, |acc, expr| E::BinaryOp {
174 left: Box::new(expr),
175 op: Operator::And,
176 right: Box::new(acc),
177 })
178}
179
180fn can_push_filter_through_projection(
181 predicate: &Expr,
182 exprs: &[Expr],
183 kind: &ProjectionKind,
184) -> bool {
185 let referenced = referenced_columns(predicate);
186
187 match kind {
188 ProjectionKind::Select => match projection_select_output_columns(exprs) {
189 OutputColumns::Some(cols) => referenced.is_subset(&cols),
190 OutputColumns::All | OutputColumns::Unknown => false,
191 },
192 ProjectionKind::WithColumns => {
193 let assigned = projection_assigned_columns(exprs);
194 !referenced.iter().any(|c| assigned.contains(c))
195 }
196 }
197}
198
199fn referenced_columns(expr: &Expr) -> HashSet<String> {
200 let mut out = HashSet::new();
201 collect_referenced_columns(expr, &mut out);
202 out
203}
204
205fn collect_referenced_columns(expr: &Expr, out: &mut HashSet<String>) {
206 match expr {
207 E::Column(name) => {
208 out.insert(name.clone());
209 }
210 E::Alias { expr, .. } => collect_referenced_columns(expr, out),
211 E::UnaryOp { expr, .. } => collect_referenced_columns(expr, out),
212 E::BinaryOp { left, right, .. } => {
213 collect_referenced_columns(left, out);
214 collect_referenced_columns(right, out);
215 }
216 E::Agg { expr, .. } => collect_referenced_columns(expr, out),
217 E::Function { input, .. } => collect_referenced_columns(input, out),
218 E::Literal(_) | E::Wildcard => {}
219 }
220}
221
222enum OutputColumns {
223 All,
224 Some(HashSet<String>),
225 Unknown,
226}
227
228fn projection_select_output_columns(exprs: &[Expr]) -> OutputColumns {
229 let mut cols = HashSet::new();
230 for expr in exprs {
231 match expr {
232 E::Wildcard => return OutputColumns::All,
233 E::Alias { name, .. } => {
234 cols.insert(name.clone());
235 }
236 E::Column(name) => {
237 cols.insert(name.clone());
238 }
239 _ => return OutputColumns::Unknown,
240 }
241 }
242 OutputColumns::Some(cols)
243}
244
245fn projection_assigned_columns(exprs: &[Expr]) -> HashSet<String> {
246 let mut cols = HashSet::new();
247 for expr in exprs {
248 match expr {
249 E::Alias { name, .. } => {
250 cols.insert(name.clone());
251 }
252 E::Column(name) => {
253 cols.insert(name.clone());
254 }
255 _ => {}
256 }
257 }
258 cols
259}
260
261fn projection_pushdown(plan: LogicalPlan) -> LogicalPlan {
262 projection_pushdown_inner(plan, RequiredColumns::All).0
263}
264
265#[derive(Debug, Clone)]
266enum RequiredColumns {
267 All,
268 Some(HashSet<String>),
269}
270
271impl RequiredColumns {
272 fn union(self, other: Self) -> Self {
273 match (self, other) {
274 (RequiredColumns::All, _) | (_, RequiredColumns::All) => RequiredColumns::All,
275 (RequiredColumns::Some(mut a), RequiredColumns::Some(b)) => {
276 a.extend(b);
277 RequiredColumns::Some(a)
278 }
279 }
280 }
281}
282
283fn projection_pushdown_inner(
284 plan: LogicalPlan,
285 required: RequiredColumns,
286) -> (LogicalPlan, RequiredColumns) {
287 match plan {
288 LogicalPlan::Projection { input, exprs, kind } => match kind {
289 ProjectionKind::Select => {
290 let input_required = required_columns_for_select(&exprs);
291 let (new_input, _) = projection_pushdown_inner(*input, input_required);
292 (
293 LogicalPlan::Projection {
294 input: Box::new(new_input),
295 exprs,
296 kind: ProjectionKind::Select,
297 },
298 required,
299 )
300 }
301 ProjectionKind::WithColumns => {
302 let mut needed = HashSet::new();
304 if let RequiredColumns::Some(ref req) = required {
305 needed.extend(req.clone());
306 }
307 for expr in &exprs {
308 match expr {
309 E::Alias { expr, .. } => needed.extend(referenced_columns(expr)),
310 E::Column(_) => {}
311 _ => {}
312 }
313 }
314 let (new_input, _) =
315 projection_pushdown_inner(*input, RequiredColumns::Some(needed));
316 (
317 LogicalPlan::Projection {
318 input: Box::new(new_input),
319 exprs,
320 kind: ProjectionKind::WithColumns,
321 },
322 required,
323 )
324 }
325 },
326 LogicalPlan::Filter { input, predicate } => {
327 let input_required = required
328 .clone()
329 .union(RequiredColumns::Some(referenced_columns(&predicate)));
330 let (new_input, _) = projection_pushdown_inner(*input, input_required);
331 (
332 LogicalPlan::Filter {
333 input: Box::new(new_input),
334 predicate,
335 },
336 required,
337 )
338 }
339 LogicalPlan::Aggregate {
340 input,
341 group_by,
342 aggs,
343 } => {
344 let mut needed = HashSet::new();
345 for e in group_by.iter().chain(aggs.iter()) {
346 needed.extend(referenced_columns(e));
347 }
348 let (new_input, _) = projection_pushdown_inner(*input, RequiredColumns::Some(needed));
349 (
350 LogicalPlan::Aggregate {
351 input: Box::new(new_input),
352 group_by,
353 aggs,
354 },
355 required,
356 )
357 }
358 LogicalPlan::Join {
359 left,
360 right,
361 keys,
362 how,
363 } => {
364 let (new_left, _) = projection_pushdown_inner(*left, RequiredColumns::All);
365 let (new_right, _) = projection_pushdown_inner(*right, RequiredColumns::All);
366 (
367 LogicalPlan::Join {
368 left: Box::new(new_left),
369 right: Box::new(new_right),
370 keys,
371 how,
372 },
373 required,
374 )
375 }
376 LogicalPlan::Sort { input, options } => {
377 let (new_input, _) = projection_pushdown_inner(*input, required.clone());
378 (
379 LogicalPlan::Sort {
380 input: Box::new(new_input),
381 options,
382 },
383 required,
384 )
385 }
386 LogicalPlan::Slice {
387 input,
388 offset,
389 len,
390 from_end,
391 } => {
392 let (new_input, _) = projection_pushdown_inner(*input, required.clone());
393 (
394 LogicalPlan::Slice {
395 input: Box::new(new_input),
396 offset,
397 len,
398 from_end,
399 },
400 required,
401 )
402 }
403 LogicalPlan::Unique { input, subset } => {
404 let (new_input, _) = projection_pushdown_inner(*input, required.clone());
405 (
406 LogicalPlan::Unique {
407 input: Box::new(new_input),
408 subset,
409 },
410 required,
411 )
412 }
413 LogicalPlan::FillNull { input, fill } => {
414 let (new_input, _) = projection_pushdown_inner(*input, required.clone());
415 (
416 LogicalPlan::FillNull {
417 input: Box::new(new_input),
418 fill,
419 },
420 required,
421 )
422 }
423 LogicalPlan::DropNulls { input, subset } => {
424 let (new_input, _) = projection_pushdown_inner(*input, required.clone());
425 (
426 LogicalPlan::DropNulls {
427 input: Box::new(new_input),
428 subset,
429 },
430 required,
431 )
432 }
433 LogicalPlan::NullCount { input } => {
434 let (new_input, _) = projection_pushdown_inner(*input, RequiredColumns::All);
435 (
436 LogicalPlan::NullCount {
437 input: Box::new(new_input),
438 },
439 RequiredColumns::All,
440 )
441 }
442 LogicalPlan::Explode { input, column } => {
443 let (new_input, _) = projection_pushdown_inner(*input, RequiredColumns::All);
444 (
445 LogicalPlan::Explode {
446 input: Box::new(new_input),
447 column,
448 },
449 RequiredColumns::All,
450 )
451 }
452 LogicalPlan::Implode { input } => {
453 let (new_input, _) = projection_pushdown_inner(*input, RequiredColumns::All);
454 (
455 LogicalPlan::Implode {
456 input: Box::new(new_input),
457 },
458 RequiredColumns::All,
459 )
460 }
461 LogicalPlan::CsvScan {
462 path,
463 predicate,
464 projection,
465 } => {
466 let mut needed = match required {
467 RequiredColumns::All => None,
468 RequiredColumns::Some(s) => Some(s),
469 };
470 if let Some(pred) = &predicate {
471 let cols = referenced_columns(pred);
472 needed = Some(match needed {
473 Some(mut s) => {
474 s.extend(cols);
475 s
476 }
477 None => cols,
478 });
479 }
480 (
481 LogicalPlan::CsvScan {
482 path,
483 predicate,
484 projection: merge_projection(projection, needed),
485 },
486 RequiredColumns::All,
487 )
488 }
489 LogicalPlan::ParquetScan {
490 path,
491 predicate,
492 projection,
493 } => {
494 let mut needed = match required {
495 RequiredColumns::All => None,
496 RequiredColumns::Some(s) => Some(s),
497 };
498 if let Some(pred) = &predicate {
499 let cols = referenced_columns(pred);
500 needed = Some(match needed {
501 Some(mut s) => {
502 s.extend(cols);
503 s
504 }
505 None => cols,
506 });
507 }
508 (
509 LogicalPlan::ParquetScan {
510 path,
511 predicate,
512 projection: merge_projection(projection, needed),
513 },
514 RequiredColumns::All,
515 )
516 }
517 other => (other, RequiredColumns::All),
518 }
519}
520
521fn required_columns_for_select(exprs: &[Expr]) -> RequiredColumns {
522 let mut needed = HashSet::new();
523 for expr in exprs {
524 match expr {
525 E::Wildcard => return RequiredColumns::All,
526 other => needed.extend(referenced_columns(other)),
527 }
528 }
529 RequiredColumns::Some(needed)
530}
531
532fn merge_projection(
533 existing: Option<Vec<String>>,
534 needed: Option<HashSet<String>>,
535) -> Option<Vec<String>> {
536 let Some(needed) = needed else {
537 return existing;
538 };
539
540 let mut out = Vec::new();
541 let mut seen = HashSet::new();
542
543 if let Some(existing) = existing {
544 for c in existing {
545 if seen.insert(c.clone()) {
546 out.push(c);
547 }
548 }
549 }
550
551 for c in needed {
552 if seen.insert(c.clone()) {
553 out.push(c);
554 }
555 }
556
557 Some(out)
558}
559
560#[cfg(test)]
561mod tests {
562 use super::Optimizer;
563 use crate::expr::{col, lit};
564 use crate::lazy::LogicalPlan;
565 use crate::lazy::ProjectionKind;
566
567 #[test]
568 fn predicate_pushdown_moves_filter_into_scan() {
569 let plan = LogicalPlan::Filter {
570 input: Box::new(LogicalPlan::CsvScan {
571 path: "data.csv".into(),
572 predicate: None,
573 projection: None,
574 }),
575 predicate: col("a").gt(lit(1_i64)),
576 };
577
578 let optimized = Optimizer::optimize(&plan);
579 match optimized {
580 LogicalPlan::CsvScan { predicate, .. } => assert!(predicate.is_some()),
581 other => panic!("expected CsvScan, got {other:?}"),
582 }
583 }
584
585 #[test]
586 fn predicate_pushdown_combines_multiple_filters_with_and() {
587 let plan = LogicalPlan::Filter {
588 input: Box::new(LogicalPlan::Filter {
589 input: Box::new(LogicalPlan::CsvScan {
590 path: "data.csv".into(),
591 predicate: None,
592 projection: None,
593 }),
594 predicate: col("a").gt(lit(1_i64)),
595 }),
596 predicate: col("b").lt(lit(10_i64)),
597 };
598
599 let optimized = Optimizer::optimize(&plan);
600 match optimized {
601 LogicalPlan::CsvScan {
602 predicate: Some(p), ..
603 } => {
604 let s = format!("{p:?}");
605 assert!(s.contains("And"));
606 }
607 other => panic!("expected CsvScan with predicate, got {other:?}"),
608 }
609 }
610
611 #[test]
612 fn predicate_pushdown_does_not_cross_select_when_column_not_selected() {
613 let plan = LogicalPlan::Filter {
614 input: Box::new(LogicalPlan::Projection {
615 input: Box::new(LogicalPlan::CsvScan {
616 path: "data.csv".into(),
617 predicate: None,
618 projection: None,
619 }),
620 exprs: vec![col("a")],
621 kind: ProjectionKind::Select,
622 }),
623 predicate: col("b").gt(lit(1_i64)),
624 };
625
626 let optimized = Optimizer::optimize(&plan);
627 assert!(matches!(optimized, LogicalPlan::Filter { .. }));
628 }
629
630 #[test]
631 fn projection_pushdown_sets_scan_projection() {
632 let plan = LogicalPlan::Projection {
633 input: Box::new(LogicalPlan::CsvScan {
634 path: "data.csv".into(),
635 predicate: None,
636 projection: None,
637 }),
638 exprs: vec![col("a"), col("b")],
639 kind: ProjectionKind::Select,
640 };
641
642 let optimized = Optimizer::optimize(&plan);
643 let s = optimized.display();
644 assert!(s.contains("projection"));
645 }
646}