Skip to main content

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}