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