mz_transform/equivalence_propagation.rs
1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10//! Propagates expression equivalence from leaves to root, and back down again.
11//!
12//! Expression equivalences are `MirScalarExpr` replacements by simpler expressions.
13//! These equivalences derive from
14//! Filters: predicates must evaluate to `Datum::True`.
15//! Maps: new columns equal the expressions that define them.
16//! Joins: equated columns must be equal.
17//! Others: lots of other predicates we might learn (range constraints on aggregates; non-negativity)
18//!
19//! From leaf to root the equivalences are *enforced*, and communicate that the expression will not produce rows that do not satisfy the equivalence.
20//! From root to leaf the equivalences are *advised*, and communicate that the expression may discard any outputs that do not satisfy the equivalence.
21//!
22//! Importantly, in descent the operator *may not* assume any equivalence filtering will be applied to its results.
23//! It cannot therefore produce rows it would otherwise not, even rows that do not satisfy the equivalence.
24//! Operators *may* introduce filtering in descent, and they must do so to take further advantage of the equivalences.
25//!
26//! The subtlety is due to the expressions themselves causing the equivalences, and allowing more rows may invalidate equivalences.
27//! For example, we might learn that `Column(7)` equals `Literal(3)`, but must refrain from introducing that substitution in descent,
28//! because it is possible that the equivalence derives from restrictions in the expression we are visiting. Were we certain that the
29//! equivalence was independent of the expression (e.g. through a more nuanced expression traversal) we could imaging relaxing this.
30
31use std::collections::BTreeMap;
32
33use itertools::Itertools;
34use mz_expr::{Id, MirRelationExpr, MirScalarExpr};
35use mz_repr::{Datum, ReprScalarType};
36use tracing::debug;
37
38use crate::analysis::equivalences::{
39 EqClassesImpl, EquivalenceClasses, EquivalenceClassesWithholdingErrors, Equivalences,
40 ExpressionReducer,
41};
42use mz_repr::ReprColumnType;
43
44use crate::analysis::{Arity, DerivedView, ReprRelationType};
45
46use crate::{TransformCtx, TransformError};
47
48/// Pulls up and pushes down predicate information represented as equivalences
49#[derive(Debug, Default)]
50pub struct EquivalencePropagation;
51
52impl crate::Transform for EquivalencePropagation {
53 fn name(&self) -> &'static str {
54 "EquivalencePropagation"
55 }
56
57 #[mz_ore::instrument(
58 target = "optimizer"
59 level = "trace",
60 fields(path.segment = "equivalence_propagation")
61 )]
62 fn actually_perform_transform(
63 &self,
64 relation: &mut MirRelationExpr,
65 ctx: &mut TransformCtx,
66 ) -> Result<(), TransformError> {
67 // Perform bottom-up equivalence class analysis.
68 use crate::analysis::DerivedBuilder;
69 let mut builder = DerivedBuilder::new(ctx.features);
70 builder.require(Equivalences);
71 builder.require(ReprRelationType);
72 let derived = builder.visit(relation);
73 let derived = derived.as_view();
74
75 let prior = relation.clone();
76
77 let mut get_equivalences = BTreeMap::default();
78 self.apply(
79 relation,
80 derived,
81 EquivalenceClasses::default(),
82 &mut get_equivalences,
83 ctx,
84 );
85
86 // Trace the plan as the result of `equivalence_propagation` before potentially applying
87 // `ColumnKnowledge`. (If `ColumnKnowledge` runs, it will trace its own result.)
88 mz_repr::explain::trace_plan(&*relation);
89
90 if prior == *relation {
91 let ck = crate::ColumnKnowledge::default();
92 ck.transform(relation, ctx)?;
93 if prior != *relation {
94 // This used to be tracing::error, but it became too common with
95 // dequadratic_eqprop_map.
96 tracing::info!(
97 ?ctx.global_id,
98 "ColumnKnowledge performed work after EquivalencePropagation",
99 );
100 }
101 }
102
103 Ok(())
104 }
105}
106
107impl EquivalencePropagation {
108 /// Provides the opportunity to mutate `relation` in response to equivalences enforced by others.
109 ///
110 /// Provides the opportunity to mutate `relation` in response to equivalences enforced by their children,
111 /// as presented in `derived`, and equivalences enforced of their output (by their ancestors), as presented
112 /// in `outer_equivalences` and `get_equivalences`.
113 ///
114 /// The mutations should never invalidate an equivalence the operator has been reported as providing, as that
115 /// information may have already been acted upon by others.
116 ///
117 /// The `expr_index` argument must equal `expr`s position in post-order, so that it can be used as a reference
118 /// into `derived`. The argument can be used with the `SubtreeSize` analysis to determine the range of values
119 /// associated with `expr`.
120 ///
121 /// After the call, `get_equivalences` will be populated with certainly equivalences that will be certainly
122 /// enforced for all uses of each identifier. The information can be harvested and propagated to the definitions
123 /// of those identifiers.
124 pub fn apply(
125 &self,
126 expr: &mut MirRelationExpr,
127 derived: DerivedView,
128 mut outer_equivalences: EquivalenceClasses,
129 get_equivalences: &mut BTreeMap<Id, EquivalenceClasses>,
130 ctx: &mut TransformCtx,
131 ) {
132 // TODO: The top-down traversal can be coded as a worklist, with arguments tupled and enqueued.
133 // This has the potential to do a lot more cloning (of `outer_equivalences`), and some care is needed
134 // for `get_equivalences` which would be scoped to the whole method rather than tupled and enqueued.
135
136 let repr_expr_type = derived
137 .value::<ReprRelationType>()
138 .expect("ReprRelationType required");
139 assert!(repr_expr_type.is_some());
140 let expr_type: Option<&Vec<ReprColumnType>> = repr_expr_type.as_ref();
141 let expr_equivalences = derived
142 .value::<Equivalences>()
143 .expect("Equivalences required");
144
145 // `None` analysis values indicate collections that can be pruned.
146 let expr_equivalences = if let Some(e) = expr_equivalences {
147 e
148 } else {
149 expr.take_safely_with_col_types(expr_type.unwrap().clone());
150 return;
151 };
152
153 // Optimize `outer_equivalences` in the context of `expr_type`.
154 // If it ends up unsatisfiable, we can replace `expr` with an empty constant of the same relation type.
155 let reducer = expr_equivalences.reducer();
156 for class in outer_equivalences.classes.iter_mut() {
157 for expr in class.iter_mut() {
158 reducer.reduce_expr(expr);
159 }
160 }
161
162 outer_equivalences.minimize(expr_type.map(|x| &x[..]));
163 if outer_equivalences.unsatisfiable() {
164 expr.take_safely_with_col_types(expr_type.unwrap().clone());
165 return;
166 }
167
168 match expr {
169 MirRelationExpr::Constant { rows, typ: _ } => {
170 if let Ok(rows) = rows {
171 let mut datum_vec = mz_repr::DatumVec::new();
172 // Delete any rows that violate the equivalences.
173 // Do not delete rows that produce errors, as they are semantically important.
174 rows.retain(|(row, _count)| {
175 let temp_storage = mz_repr::RowArena::new();
176 let datums = datum_vec.borrow_with(row);
177 outer_equivalences.classes.iter().all(|class| {
178 // Any subset of `Ok` results must equate, or we can drop the row.
179 let mut oks = class
180 .iter()
181 .filter_map(|e| e.eval(&datums[..], &temp_storage).ok());
182 if let Some(e1) = oks.next() {
183 oks.all(|e2| e1 == e2)
184 } else {
185 true
186 }
187 })
188 });
189 }
190 }
191 MirRelationExpr::Get { id, .. } => {
192 // Install and intersect with other equivalences from other `Get` sites.
193 // These will be read out by the corresponding `Let` binding's `value`.
194 if let Some(equivs) = get_equivalences.get_mut(id) {
195 *equivs = equivs.union(&outer_equivalences);
196 } else {
197 get_equivalences.insert(*id, outer_equivalences);
198 }
199 }
200 MirRelationExpr::Let { id, .. } => {
201 let id = *id;
202 // Traverse `body` first to assemble equivalences to push to `value`.
203 // Descend without a key for `id`, treating the absence as the identity for union.
204 // `Get` nodes with identifier `id` will populate the equivalence classes with the intersection of their guarantees.
205 let mut children_rev = expr.children_mut().rev().zip_eq(derived.children_rev());
206
207 let body = children_rev.next().unwrap();
208 let value = children_rev.next().unwrap();
209
210 self.apply(
211 body.0,
212 body.1,
213 outer_equivalences.clone(),
214 get_equivalences,
215 ctx,
216 );
217
218 // We expect to find `id` in `get_equivalences`, as otherwise the binding is
219 // not referenced and can be removed.
220 if let Some(equivalences) = get_equivalences.get(&Id::Local(id)) {
221 self.apply(
222 value.0,
223 value.1,
224 equivalences.clone(),
225 get_equivalences,
226 ctx,
227 );
228 }
229 }
230 MirRelationExpr::LetRec { .. } => {
231 let mut child_iter = expr.children_mut().rev().zip_eq(derived.children_rev());
232 // Continue in `body` with the outer equivalences.
233 let (body, view) = child_iter.next().unwrap();
234 self.apply(body, view, outer_equivalences, get_equivalences, ctx);
235 // Continue recursively, but without the outer equivalences supplied to `body`.
236 for (child, view) in child_iter {
237 self.apply(
238 child,
239 view,
240 EquivalenceClasses::default(),
241 get_equivalences,
242 ctx,
243 );
244 }
245 }
246 MirRelationExpr::Project { input, outputs } => {
247 // Transform `outer_equivalences` to one relevant for `input`.
248 outer_equivalences.permute(outputs);
249 self.apply(
250 input,
251 derived.last_child(),
252 outer_equivalences,
253 get_equivalences,
254 ctx,
255 );
256 }
257 MirRelationExpr::Map { input, scalars } => {
258 // Optimize `scalars` with respect to input equivalences.
259 let input_equivalences = derived
260 .last_child()
261 .value::<Equivalences>()
262 .expect("Equivalences required");
263
264 if let Some(input_equivalences_orig) = input_equivalences {
265 // We clone `input_equivalences` only if we want to modify it, which is when
266 // `enable_dequadratic_eqprop_map` is off. Otherwise, we work with the original
267 // `input_equivalences`.
268 let mut input_equivalences_cloned = None;
269 if !ctx.features.enable_dequadratic_eqprop_map {
270 // We mutate them for variadic Maps if the feature flag is not set.
271 input_equivalences_cloned = Some(input_equivalences_orig.clone());
272 }
273 // Get all output types, to reveal a prefix to each scaler expr.
274 let input_types: &Vec<ReprColumnType> = derived
275 .value::<ReprRelationType>()
276 .expect("ReprRelationType required")
277 .as_ref()
278 .unwrap();
279 let input_arity = input_types.len() - scalars.len();
280 for (index, expr) in scalars.iter_mut().enumerate() {
281 let reducer = if !ctx.features.enable_dequadratic_eqprop_map {
282 input_equivalences_cloned
283 .as_ref()
284 .expect("always filled if feature flag is not set")
285 .reducer()
286 } else {
287 input_equivalences_orig.reducer()
288 };
289 let changed = reducer.reduce_expr(expr);
290 if changed || !ctx.features.enable_less_reduce_in_eqprop {
291 expr.reduce(&input_types[..(input_arity + index)]);
292 }
293 if !ctx.features.enable_dequadratic_eqprop_map {
294 // Unfortunately, we had to stop doing the following, because it
295 // was making the `Map` handling quadratic.
296 // TODO: Get back to this when we have e-graphs.
297 // https://github.com/MaterializeInc/database-issues/issues/9157
298 //
299 // Introduce the fact relating the mapped expression and corresponding
300 // column. This allows subsequent expressions to be optimized with this
301 // information.
302 input_equivalences_cloned
303 .as_mut()
304 .expect("always filled if feature flag is not set")
305 .classes
306 .push(vec![
307 expr.clone(),
308 MirScalarExpr::column(input_arity + index),
309 ]);
310 input_equivalences_cloned
311 .as_mut()
312 .expect("always filled if feature flag is not set")
313 .minimize(Some(input_types));
314 }
315 }
316 let input_arity = *derived
317 .last_child()
318 .value::<Arity>()
319 .expect("Arity required");
320 outer_equivalences.project(0..input_arity);
321 self.apply(
322 input,
323 derived.last_child(),
324 outer_equivalences,
325 get_equivalences,
326 ctx,
327 );
328 }
329 }
330 MirRelationExpr::FlatMap { input, exprs, .. } => {
331 // Transform `exprs` by guarantees from `input` *and* from `outer`???
332 let input_equivalences = derived
333 .last_child()
334 .value::<Equivalences>()
335 .expect("Equivalences required");
336
337 if let Some(input_equivalences) = input_equivalences {
338 let input_types: &Vec<ReprColumnType> = derived
339 .last_child()
340 .value::<ReprRelationType>()
341 .expect("ReprRelationType required")
342 .as_ref()
343 .unwrap();
344 let reducer = input_equivalences.reducer();
345 for expr in exprs.iter_mut() {
346 let changed = reducer.reduce_expr(expr);
347 if changed || !ctx.features.enable_less_reduce_in_eqprop {
348 expr.reduce(input_types);
349 }
350 }
351 let input_arity = *derived
352 .last_child()
353 .value::<Arity>()
354 .expect("Arity required");
355 outer_equivalences.project(0..input_arity);
356 self.apply(
357 input,
358 derived.last_child(),
359 outer_equivalences,
360 get_equivalences,
361 ctx,
362 );
363 }
364 }
365 MirRelationExpr::Filter { input, predicates } => {
366 // Transform `predicates` by guarantees from `input` *and* from `outer`???
367 // If we reduce based on `input` guarantees, we won't be able to push those
368 // constraints down into input, which may be fine but is worth considering.
369 let input_equivalences = derived
370 .last_child()
371 .value::<Equivalences>()
372 .expect("Equivalences required");
373 if let Some(input_equivalences) = input_equivalences {
374 let input_types: &Vec<ReprColumnType> = derived
375 .last_child()
376 .value::<ReprRelationType>()
377 .expect("ReprRelationType required")
378 .as_ref()
379 .unwrap();
380 let reducer = input_equivalences.reducer();
381 for expr in predicates.iter_mut() {
382 let changed = reducer.reduce_expr(expr);
383 if changed || !ctx.features.enable_less_reduce_in_eqprop {
384 expr.reduce(input_types);
385 }
386 }
387 // Incorporate `predicates` into `outer_equivalences`.
388 let mut class = predicates.clone();
389 class.push(MirScalarExpr::literal_ok(Datum::True, ReprScalarType::Bool));
390 outer_equivalences.classes.push(class);
391 outer_equivalences.minimize(Some(input_types));
392 self.apply(
393 input,
394 derived.last_child(),
395 outer_equivalences,
396 get_equivalences,
397 ctx,
398 );
399 }
400 }
401
402 MirRelationExpr::Join {
403 inputs,
404 equivalences,
405 ..
406 } => {
407 // Certain equivalences are ensured by each of the inputs.
408 // Other equivalences are imposed by parents of the expression.
409 // We must not weaken the properties provided by the expression to its parents,
410 // meaning we can optimize `equivalences` with respect to input guarantees,
411 // but not with respect to `outer_equivalences`.
412
413 // Each child can be presented with the integration of `join_equivalences`, `outer_equivalences`,
414 // and each input equivalence *other than* their own, projected onto the input's columns.
415
416 // Enumerate locations to find each child's analysis outputs.
417 let mut children: Vec<_> = derived.children_rev().collect::<Vec<_>>();
418 children.reverse();
419
420 // Assemble the appended input types, for use in expression minimization.
421 // Do not use `expr_types`, which may reflect nullability that does not hold for the inputs.
422 let mut input_types = Some(
423 children
424 .iter()
425 .flat_map(|c| {
426 c.value::<ReprRelationType>()
427 .expect("ReprRelationType required")
428 .as_ref()
429 .unwrap()
430 .iter()
431 .cloned()
432 })
433 .collect::<Vec<_>>(),
434 );
435
436 // For each child, assemble its equivalences using join-relative column numbers.
437 // Don't do anything with the children yet, as we'll want to revisit each with
438 // this information at hand.
439 let mut columns = 0;
440 let mut input_equivalences = Vec::with_capacity(children.len());
441 for child in children.iter() {
442 let child_arity = child.value::<Arity>().expect("Arity required");
443 let equivalences = child
444 .value::<Equivalences>()
445 .expect("Equivalences required")
446 .clone();
447
448 if let Some(mut equivalences) = equivalences {
449 let permutation = (columns..(columns + child_arity)).collect::<Vec<_>>();
450 equivalences.permute(&permutation);
451 equivalences.minimize(input_types.as_ref().map(|x| &x[..]));
452 input_equivalences.push(equivalences);
453 }
454 columns += child_arity;
455 }
456
457 // Form the equivalences we will use to replace `equivalences`.
458 let mut join_equivalences: EqClassesImpl =
459 if ctx.features.enable_eq_classes_withholding_errors {
460 EqClassesImpl::EquivalenceClassesWithholdingErrors(
461 EquivalenceClassesWithholdingErrors::default(),
462 )
463 } else {
464 EqClassesImpl::EquivalenceClasses(EquivalenceClasses::default())
465 };
466 join_equivalences.extend_equivalences(equivalences.clone());
467
468 // // Optionally, introduce `outer_equivalences` into `equivalences`.
469 // // This is not required, but it could be very helpful. To be seen.
470 // join_equivalences
471 // .classes
472 // .extend(outer_equivalences.classes.clone());
473
474 // Reduce join equivalences by the input equivalences.
475 for input_equivs in input_equivalences.iter() {
476 let reducer = input_equivs.reducer();
477 for class in join_equivalences
478 .equivalence_classes_mut()
479 .classes
480 .iter_mut()
481 {
482 for expr in class.iter_mut() {
483 // Semijoin elimination currently fails if you do more advanced simplification than
484 // literal substitution.
485 let old = expr.clone();
486 let changed = reducer.reduce_expr(expr);
487 let acceptable_sub = literal_domination(&old, expr);
488 if changed || !ctx.features.enable_less_reduce_in_eqprop {
489 expr.reduce(input_types.as_ref().unwrap());
490 }
491 if !acceptable_sub && !literal_domination(&old, expr)
492 || expr.contains_err()
493 {
494 expr.clone_from(&old);
495 }
496 }
497 }
498 }
499 // Remove nullability information, as it has already been incorporated from input equivalences,
500 // and if it was reduced out relative to input equivalences we don't want to re-introduce it.
501 if let Some(input_types) = input_types.as_mut() {
502 for col in input_types.iter_mut() {
503 col.nullable = true;
504 }
505 }
506 join_equivalences
507 .equivalence_classes_mut()
508 .minimize(input_types.as_ref().map(|x| &x[..]));
509
510 // Revisit each child, determining the information to present to it, and recurring.
511 let mut columns = 0;
512 for ((index, child), expr) in
513 children.into_iter().enumerate().zip_eq(inputs.iter_mut())
514 {
515 let child_arity = child.value::<Arity>().expect("Arity required");
516
517 let mut push_equivalences = join_equivalences.clone();
518 push_equivalences.extend_equivalences(outer_equivalences.classes.clone());
519
520 for (other, input_equivs) in input_equivalences.iter().enumerate() {
521 if index != other {
522 push_equivalences.extend_equivalences(input_equivs.classes.clone());
523 }
524 }
525 push_equivalences.project(columns..(columns + child_arity));
526 self.apply(
527 expr,
528 child,
529 push_equivalences.equivalence_classes().clone(),
530 get_equivalences,
531 ctx,
532 );
533
534 columns += child_arity;
535 }
536
537 let extracted_equivalences =
538 join_equivalences.extract_equivalences(input_types.as_ref().map(|x| &x[..]));
539
540 debug!(
541 ?inputs,
542 ?extracted_equivalences,
543 "Join equivalences extracted"
544 );
545 equivalences.clone_from(&extracted_equivalences);
546 }
547 MirRelationExpr::Reduce {
548 input,
549 group_key,
550 aggregates,
551 ..
552 } => {
553 // TODO: MIN, MAX, ANY, ALL aggregates pass through all certain properties of their columns.
554 // This may involve projection and permutation, to reposition the information appropriately.
555 // TODO: Non-null constraints likely push down into the support of the aggregate expressions.
556
557 // Apply any equivalences about the input to key and aggregate expressions.
558 let input_equivalences = derived
559 .last_child()
560 .value::<Equivalences>()
561 .expect("Equivalences required");
562 if let Some(input_equivalences) = input_equivalences {
563 let input_type: &Vec<ReprColumnType> = derived
564 .last_child()
565 .value::<ReprRelationType>()
566 .expect("ReprRelationType required")
567 .as_ref()
568 .unwrap();
569 let reducer = input_equivalences.reducer();
570 for key in group_key.iter_mut() {
571 // Semijoin elimination currently fails if you do more advanced simplification than
572 // literal substitution.
573 let old_key = key.clone();
574 let changed = reducer.reduce_expr(key);
575 let acceptable_sub = literal_domination(&old_key, key);
576 if changed || !ctx.features.enable_less_reduce_in_eqprop {
577 key.reduce(input_type);
578 }
579 if !acceptable_sub && !literal_domination(&old_key, key) {
580 key.clone_from(&old_key);
581 }
582 }
583 for aggr in aggregates.iter_mut() {
584 let changed = reducer.reduce_expr(&mut aggr.expr);
585 if changed || !ctx.features.enable_less_reduce_in_eqprop {
586 aggr.expr.reduce(input_type);
587 }
588 // A count expression over a non-null expression can discard the expression.
589 if aggr.func == mz_expr::AggregateFunc::Count && !aggr.distinct {
590 let mut probe = aggr.expr.clone().call_is_null();
591 reducer.reduce_expr(&mut probe);
592 if probe.is_literal_false() {
593 aggr.expr = MirScalarExpr::literal_true();
594 }
595 }
596 }
597 }
598
599 // To transform `outer_equivalences` to one about `input`, we will "pretend" to pre-pend all of
600 // the input columns, introduce equivalences about the evaluation of `group_key` on them
601 // and the key columns themselves, and then project onto these "input" columns.
602 let input_arity = *derived
603 .last_child()
604 .value::<Arity>()
605 .expect("Arity required");
606 let output_arity = *derived.value::<Arity>().expect("Arity required");
607
608 // Permute `outer_equivalences` to reference columns `input_arity` later.
609 let permutation = (input_arity..(input_arity + output_arity)).collect::<Vec<_>>();
610 outer_equivalences.permute(&permutation[..]);
611 for (index, group) in group_key.iter().enumerate() {
612 outer_equivalences.classes.push(vec![
613 MirScalarExpr::column(input_arity + index),
614 group.clone(),
615 ]);
616 }
617 outer_equivalences.project(0..input_arity);
618 self.apply(
619 input,
620 derived.last_child(),
621 outer_equivalences,
622 get_equivalences,
623 ctx,
624 );
625 }
626 MirRelationExpr::TopK {
627 input,
628 group_key,
629 limit,
630 ..
631 } => {
632 // We must be careful when updating `limit` to not install column references
633 // outside of `group_key`. We'll do this for now with `literal_domination`,
634 // which will ensure we only perform substitutions by a literal.
635 let input_equivalences = derived
636 .last_child()
637 .value::<Equivalences>()
638 .expect("Equivalences required");
639 if let Some(input_equivalences) = input_equivalences {
640 let input_types: &Vec<ReprColumnType> = derived
641 .last_child()
642 .value::<ReprRelationType>()
643 .expect("ReprRelationType required")
644 .as_ref()
645 .unwrap();
646 let reducer = input_equivalences.reducer();
647 if let Some(expr) = limit {
648 let old_expr = expr.clone();
649 let changed = reducer.reduce_expr(expr);
650 let acceptable_sub = literal_domination(&old_expr, expr);
651 if changed || !ctx.features.enable_less_reduce_in_eqprop {
652 expr.reduce(input_types);
653 }
654 if !acceptable_sub && !literal_domination(&old_expr, expr) {
655 expr.clone_from(&old_expr);
656 }
657 }
658 }
659
660 // Discard equivalences among non-key columns, as it is not correct that `input` may drop rows
661 // that violate constraints among non-key columns without affecting the result.
662 outer_equivalences.project(0..group_key.len());
663 self.apply(
664 input,
665 derived.last_child(),
666 outer_equivalences,
667 get_equivalences,
668 ctx,
669 );
670 }
671 MirRelationExpr::Negate { input } => {
672 self.apply(
673 input,
674 derived.last_child(),
675 outer_equivalences,
676 get_equivalences,
677 ctx,
678 );
679 }
680 MirRelationExpr::Threshold { input } => {
681 self.apply(
682 input,
683 derived.last_child(),
684 outer_equivalences,
685 get_equivalences,
686 ctx,
687 );
688 }
689 MirRelationExpr::Union { .. } => {
690 for (child, derived) in expr.children_mut().rev().zip_eq(derived.children_rev()) {
691 self.apply(
692 child,
693 derived,
694 outer_equivalences.clone(),
695 get_equivalences,
696 ctx,
697 );
698 }
699 }
700 MirRelationExpr::ArrangeBy { input, .. } => {
701 // TODO: Option to alter arrangement keys, though .. terrifying.
702 self.apply(
703 input,
704 derived.last_child(),
705 outer_equivalences,
706 get_equivalences,
707 ctx,
708 );
709 }
710 }
711 }
712}
713
714/// Logic encapsulating our willingness to accept an expression simplification.
715///
716/// For reasons of robustness, we cannot yet perform all recommended simplifications.
717/// Certain transforms expect idiomatic expressions, often around precise use of column
718/// identifiers, rather than equivalent identifiers.
719///
720/// The substitutions we are confident with are those that introduce literals for columns,
721/// or which replace column nullability checks with literals.
722fn literal_domination(old: &MirScalarExpr, new: &MirScalarExpr) -> bool {
723 let mut todo = vec![(old, new)];
724 while let Some((old, new)) = todo.pop() {
725 match (old, new) {
726 (_, MirScalarExpr::Literal(_, _)) => {
727 // Substituting a literal is always acceptable; we don't need to consult
728 // the result of the old expression to determine this.
729 }
730 (
731 MirScalarExpr::CallUnary { func: f0, expr: e0 },
732 MirScalarExpr::CallUnary { func: f1, expr: e1 },
733 ) => {
734 if f0 != f1 {
735 return false;
736 } else {
737 todo.push((&**e0, &**e1));
738 }
739 }
740 (
741 MirScalarExpr::CallBinary {
742 func: f0,
743 expr1: e01,
744 expr2: e02,
745 },
746 MirScalarExpr::CallBinary {
747 func: f1,
748 expr1: e11,
749 expr2: e12,
750 },
751 ) => {
752 if f0 != f1 {
753 return false;
754 } else {
755 todo.push((&**e01, &**e11));
756 todo.push((&**e02, &**e12));
757 }
758 }
759 (
760 MirScalarExpr::CallVariadic {
761 func: f0,
762 exprs: e0s,
763 },
764 MirScalarExpr::CallVariadic {
765 func: f1,
766 exprs: e1s,
767 },
768 ) => {
769 use itertools::Itertools;
770 if f0 != f1 || e0s.len() != e1s.len() {
771 return false;
772 } else {
773 todo.extend(e0s.iter().zip_eq(e1s));
774 }
775 }
776 (
777 MirScalarExpr::If {
778 cond: c0,
779 then: t0,
780 els: e0,
781 },
782 MirScalarExpr::If {
783 cond: c1,
784 then: t1,
785 els: e1,
786 },
787 ) => {
788 todo.push((&**c0, &**c1));
789 todo.push((&**t0, &**t1));
790 todo.push((&**e0, &**e1))
791 }
792 _ => {
793 if old != new {
794 return false;
795 }
796 }
797 }
798 }
799 true
800}