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::{Eval, 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        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        // Until then the descent recurses once per operator, so a deep plan can
137        // run the thread's stack out. `apply` cannot report an error, which
138        // rules out `CheckedRecursion`, so grow the stack instead of failing.
139        mz_ore::stack::maybe_grow(|| {
140            self.apply_stack_safe(expr, derived, outer_equivalences, get_equivalences, ctx)
141        })
142    }
143
144    fn apply_stack_safe(
145        &self,
146        expr: &mut MirRelationExpr,
147        derived: DerivedView,
148        mut outer_equivalences: EquivalenceClasses,
149        get_equivalences: &mut BTreeMap<Id, EquivalenceClasses>,
150        ctx: &mut TransformCtx,
151    ) {
152        let repr_expr_type = derived
153            .value::<ReprRelationType>()
154            .expect("ReprRelationType required");
155        assert!(repr_expr_type.is_some());
156        let expr_type: Option<&Vec<ReprColumnType>> = repr_expr_type.as_ref();
157        let expr_equivalences = derived
158            .value::<Equivalences>()
159            .expect("Equivalences required");
160
161        // `None` analysis values indicate collections that can be pruned.
162        let expr_equivalences = if let Some(e) = expr_equivalences {
163            e
164        } else {
165            expr.take_safely_with_col_types(expr_type.unwrap().clone());
166            return;
167        };
168
169        // Optimize `outer_equivalences` in the context of `expr_type`.
170        // If it ends up unsatisfiable, we can replace `expr` with an empty constant of the same relation type.
171        let reducer = expr_equivalences.reducer();
172        for class in outer_equivalences.classes.iter_mut() {
173            for expr in class.iter_mut() {
174                reducer.reduce_expr(expr);
175            }
176        }
177
178        outer_equivalences.minimize(expr_type.map(|x| &x[..]));
179        if outer_equivalences.unsatisfiable() {
180            expr.take_safely_with_col_types(expr_type.unwrap().clone());
181            return;
182        }
183
184        match expr {
185            MirRelationExpr::Constant { rows, typ: _ } => {
186                if let Ok(rows) = rows {
187                    let mut datum_vec = mz_repr::DatumVec::new();
188                    // Delete any rows that violate the equivalences.
189                    // Do not delete rows that produce errors, as they are semantically important.
190                    rows.retain(|(row, _count)| {
191                        let temp_storage = mz_repr::RowArena::new();
192                        let datums = datum_vec.borrow_with(row);
193                        outer_equivalences.classes.iter().all(|class| {
194                            // Any subset of `Ok` results must equate, or we can drop the row.
195                            let mut oks = class
196                                .iter()
197                                .filter_map(|e| e.eval(&datums[..], &temp_storage).ok());
198                            if let Some(e1) = oks.next() {
199                                oks.all(|e2| e1 == e2)
200                            } else {
201                                true
202                            }
203                        })
204                    });
205                }
206            }
207            MirRelationExpr::Get { id, .. } => {
208                // Install and intersect with other equivalences from other `Get` sites.
209                // These will be read out by the corresponding `Let` binding's `value`.
210                if let Some(equivs) = get_equivalences.get_mut(id) {
211                    *equivs = equivs.union(&outer_equivalences);
212                } else {
213                    get_equivalences.insert(*id, outer_equivalences);
214                }
215            }
216            MirRelationExpr::Let { id, .. } => {
217                let id = *id;
218                // Traverse `body` first to assemble equivalences to push to `value`.
219                // Descend without a key for `id`, treating the absence as the identity for union.
220                // `Get` nodes with identifier `id` will populate the equivalence classes with the intersection of their guarantees.
221                let mut children_rev = expr.children_mut().rev().zip_eq(derived.children_rev());
222
223                let body = children_rev.next().unwrap();
224                let value = children_rev.next().unwrap();
225
226                self.apply(
227                    body.0,
228                    body.1,
229                    outer_equivalences.clone(),
230                    get_equivalences,
231                    ctx,
232                );
233
234                // We expect to find `id` in `get_equivalences`, as otherwise the binding is
235                // not referenced and can be removed.
236                if let Some(equivalences) = get_equivalences.get(&Id::Local(id)) {
237                    self.apply(
238                        value.0,
239                        value.1,
240                        equivalences.clone(),
241                        get_equivalences,
242                        ctx,
243                    );
244                }
245            }
246            MirRelationExpr::LetRec { .. } => {
247                let mut child_iter = expr.children_mut().rev().zip_eq(derived.children_rev());
248                // Continue in `body` with the outer equivalences.
249                let (body, view) = child_iter.next().unwrap();
250                self.apply(body, view, outer_equivalences, get_equivalences, ctx);
251                // Continue recursively, but without the outer equivalences supplied to `body`.
252                for (child, view) in child_iter {
253                    self.apply(
254                        child,
255                        view,
256                        EquivalenceClasses::default(),
257                        get_equivalences,
258                        ctx,
259                    );
260                }
261            }
262            MirRelationExpr::Project { input, outputs } => {
263                // Transform `outer_equivalences` to one relevant for `input`.
264                outer_equivalences.permute(outputs);
265                self.apply(
266                    input,
267                    derived.last_child(),
268                    outer_equivalences,
269                    get_equivalences,
270                    ctx,
271                );
272            }
273            MirRelationExpr::Map { input, scalars } => {
274                // Optimize `scalars` with respect to input equivalences.
275                let input_equivalences = derived
276                    .last_child()
277                    .value::<Equivalences>()
278                    .expect("Equivalences required");
279
280                if let Some(input_equivalences_orig) = input_equivalences {
281                    // We clone `input_equivalences` only if we want to modify it, which is when
282                    // `enable_dequadratic_eqprop_map` is off. Otherwise, we work with the original
283                    // `input_equivalences`.
284                    let mut input_equivalences_cloned = None;
285                    if !ctx.features.enable_dequadratic_eqprop_map {
286                        // We mutate them for variadic Maps if the feature flag is not set.
287                        input_equivalences_cloned = Some(input_equivalences_orig.clone());
288                    }
289                    // Get all output types, to reveal a prefix to each scaler expr.
290                    let input_types: &Vec<ReprColumnType> = derived
291                        .value::<ReprRelationType>()
292                        .expect("ReprRelationType required")
293                        .as_ref()
294                        .unwrap();
295                    let input_arity = input_types.len() - scalars.len();
296                    for (index, expr) in scalars.iter_mut().enumerate() {
297                        let reducer = if !ctx.features.enable_dequadratic_eqprop_map {
298                            input_equivalences_cloned
299                                .as_ref()
300                                .expect("always filled if feature flag is not set")
301                                .reducer()
302                        } else {
303                            input_equivalences_orig.reducer()
304                        };
305                        let changed = reducer.reduce_expr(expr);
306                        if changed || !ctx.features.enable_less_reduce_in_eqprop {
307                            expr.reduce(&input_types[..(input_arity + index)]);
308                        }
309                        if !ctx.features.enable_dequadratic_eqprop_map {
310                            // Unfortunately, we had to stop doing the following, because it
311                            // was making the `Map` handling quadratic.
312                            // TODO: Get back to this when we have e-graphs.
313                            // https://github.com/MaterializeInc/database-issues/issues/9157
314                            //
315                            // Introduce the fact relating the mapped expression and corresponding
316                            // column. This allows subsequent expressions to be optimized with this
317                            // information.
318                            input_equivalences_cloned
319                                .as_mut()
320                                .expect("always filled if feature flag is not set")
321                                .classes
322                                .push(vec![
323                                    expr.clone(),
324                                    MirScalarExpr::column(input_arity + index),
325                                ]);
326                            input_equivalences_cloned
327                                .as_mut()
328                                .expect("always filled if feature flag is not set")
329                                .minimize(Some(input_types));
330                        }
331                    }
332                    let input_arity = *derived
333                        .last_child()
334                        .value::<Arity>()
335                        .expect("Arity required");
336                    outer_equivalences.project(0..input_arity);
337                    self.apply(
338                        input,
339                        derived.last_child(),
340                        outer_equivalences,
341                        get_equivalences,
342                        ctx,
343                    );
344                }
345            }
346            MirRelationExpr::FlatMap { input, exprs, .. } => {
347                // Transform `exprs` by guarantees from `input` *and* from `outer`???
348                let input_equivalences = derived
349                    .last_child()
350                    .value::<Equivalences>()
351                    .expect("Equivalences required");
352
353                if let Some(input_equivalences) = input_equivalences {
354                    let input_types: &Vec<ReprColumnType> = derived
355                        .last_child()
356                        .value::<ReprRelationType>()
357                        .expect("ReprRelationType required")
358                        .as_ref()
359                        .unwrap();
360                    let reducer = input_equivalences.reducer();
361                    for expr in exprs.iter_mut() {
362                        let changed = reducer.reduce_expr(expr);
363                        if changed || !ctx.features.enable_less_reduce_in_eqprop {
364                            expr.reduce(input_types);
365                        }
366                    }
367                    let input_arity = *derived
368                        .last_child()
369                        .value::<Arity>()
370                        .expect("Arity required");
371                    outer_equivalences.project(0..input_arity);
372                    self.apply(
373                        input,
374                        derived.last_child(),
375                        outer_equivalences,
376                        get_equivalences,
377                        ctx,
378                    );
379                }
380            }
381            MirRelationExpr::Filter { input, predicates } => {
382                // Transform `predicates` by guarantees from `input` *and* from `outer`???
383                // If we reduce based on `input` guarantees, we won't be able to push those
384                // constraints down into input, which may be fine but is worth considering.
385                let input_equivalences = derived
386                    .last_child()
387                    .value::<Equivalences>()
388                    .expect("Equivalences required");
389                if let Some(input_equivalences) = input_equivalences {
390                    let input_types: &Vec<ReprColumnType> = derived
391                        .last_child()
392                        .value::<ReprRelationType>()
393                        .expect("ReprRelationType required")
394                        .as_ref()
395                        .unwrap();
396                    let reducer = input_equivalences.reducer();
397                    for expr in predicates.iter_mut() {
398                        let changed = reducer.reduce_expr(expr);
399                        if changed || !ctx.features.enable_less_reduce_in_eqprop {
400                            expr.reduce(input_types);
401                        }
402                    }
403                    // Incorporate `predicates` into `outer_equivalences`.
404                    let mut class = predicates.clone();
405                    class.push(MirScalarExpr::literal_ok(Datum::True, ReprScalarType::Bool));
406                    outer_equivalences.classes.push(class);
407                    outer_equivalences.minimize(Some(input_types));
408                    self.apply(
409                        input,
410                        derived.last_child(),
411                        outer_equivalences,
412                        get_equivalences,
413                        ctx,
414                    );
415                }
416            }
417
418            MirRelationExpr::Join {
419                inputs,
420                equivalences,
421                ..
422            } => {
423                // Certain equivalences are ensured by each of the inputs.
424                // Other equivalences are imposed by parents of the expression.
425                // We must not weaken the properties provided by the expression to its parents,
426                // meaning we can optimize `equivalences` with respect to input guarantees,
427                // but not with respect to `outer_equivalences`.
428
429                // Each child can be presented with the integration of `join_equivalences`, `outer_equivalences`,
430                // and each input equivalence *other than* their own, projected onto the input's columns.
431
432                // Enumerate locations to find each child's analysis outputs.
433                let mut children: Vec<_> = derived.children_rev().collect::<Vec<_>>();
434                children.reverse();
435
436                // Assemble the appended input types, for use in expression minimization.
437                // Do not use `expr_types`, which may reflect nullability that does not hold for the inputs.
438                let mut input_types = Some(
439                    children
440                        .iter()
441                        .flat_map(|c| {
442                            c.value::<ReprRelationType>()
443                                .expect("ReprRelationType required")
444                                .as_ref()
445                                .unwrap()
446                                .iter()
447                                .cloned()
448                        })
449                        .collect::<Vec<_>>(),
450                );
451
452                // For each child, assemble its equivalences using join-relative column numbers.
453                // Don't do anything with the children yet, as we'll want to revisit each with
454                // this information at hand.
455                let mut columns = 0;
456                let mut input_equivalences = Vec::with_capacity(children.len());
457                for child in children.iter() {
458                    let child_arity = child.value::<Arity>().expect("Arity required");
459                    let equivalences = child
460                        .value::<Equivalences>()
461                        .expect("Equivalences required")
462                        .clone();
463
464                    if let Some(mut equivalences) = equivalences {
465                        let permutation = (columns..(columns + child_arity)).collect::<Vec<_>>();
466                        equivalences.permute(&permutation);
467                        equivalences.minimize(input_types.as_ref().map(|x| &x[..]));
468                        input_equivalences.push(equivalences);
469                    }
470                    columns += child_arity;
471                }
472
473                // Form the equivalences we will use to replace `equivalences`.
474                let mut join_equivalences: EqClassesImpl =
475                    if ctx.features.enable_eq_classes_withholding_errors {
476                        EqClassesImpl::EquivalenceClassesWithholdingErrors(
477                            EquivalenceClassesWithholdingErrors::default(),
478                        )
479                    } else {
480                        EqClassesImpl::EquivalenceClasses(EquivalenceClasses::default())
481                    };
482                join_equivalences.extend_equivalences(equivalences.clone());
483
484                // // Optionally, introduce `outer_equivalences` into `equivalences`.
485                // // This is not required, but it could be very helpful. To be seen.
486                // join_equivalences
487                //     .classes
488                //     .extend(outer_equivalences.classes.clone());
489
490                // Reduce join equivalences by the input equivalences.
491                for input_equivs in input_equivalences.iter() {
492                    let reducer = input_equivs.reducer();
493                    for class in join_equivalences
494                        .equivalence_classes_mut()
495                        .classes
496                        .iter_mut()
497                    {
498                        for expr in class.iter_mut() {
499                            // Semijoin elimination currently fails if you do more advanced simplification than
500                            // literal substitution.
501                            let old = expr.clone();
502                            let changed = reducer.reduce_expr(expr);
503                            let acceptable_sub = literal_domination(&old, expr);
504                            if changed || !ctx.features.enable_less_reduce_in_eqprop {
505                                expr.reduce(input_types.as_ref().unwrap());
506                            }
507                            if !acceptable_sub && !literal_domination(&old, expr)
508                                || expr.contains_err()
509                            {
510                                expr.clone_from(&old);
511                            }
512                        }
513                    }
514                }
515                // Remove nullability information, as it has already been incorporated from input equivalences,
516                // and if it was reduced out relative to input equivalences we don't want to re-introduce it.
517                if let Some(input_types) = input_types.as_mut() {
518                    for col in input_types.iter_mut() {
519                        col.nullable = true;
520                    }
521                }
522                join_equivalences
523                    .equivalence_classes_mut()
524                    .minimize(input_types.as_ref().map(|x| &x[..]));
525
526                // Revisit each child, determining the information to present to it, and recurring.
527                let mut columns = 0;
528                for ((index, child), expr) in
529                    children.into_iter().enumerate().zip_eq(inputs.iter_mut())
530                {
531                    let child_arity = child.value::<Arity>().expect("Arity required");
532
533                    let mut push_equivalences = join_equivalences.clone();
534                    push_equivalences.extend_equivalences(outer_equivalences.classes.clone());
535
536                    for (other, input_equivs) in input_equivalences.iter().enumerate() {
537                        if index != other {
538                            push_equivalences.extend_equivalences(input_equivs.classes.clone());
539                        }
540                    }
541                    push_equivalences.project(columns..(columns + child_arity));
542                    self.apply(
543                        expr,
544                        child,
545                        push_equivalences.equivalence_classes().clone(),
546                        get_equivalences,
547                        ctx,
548                    );
549
550                    columns += child_arity;
551                }
552
553                let extracted_equivalences =
554                    join_equivalences.extract_equivalences(input_types.as_ref().map(|x| &x[..]));
555
556                debug!(
557                    ?inputs,
558                    ?extracted_equivalences,
559                    "Join equivalences extracted"
560                );
561                equivalences.clone_from(&extracted_equivalences);
562            }
563            MirRelationExpr::Reduce {
564                input,
565                group_key,
566                aggregates,
567                ..
568            } => {
569                // TODO: MIN, MAX, ANY, ALL aggregates pass through all certain properties of their columns.
570                // This may involve projection and permutation, to reposition the information appropriately.
571                // TODO: Non-null constraints likely push down into the support of the aggregate expressions.
572
573                // Apply any equivalences about the input to key and aggregate expressions.
574                let input_equivalences = derived
575                    .last_child()
576                    .value::<Equivalences>()
577                    .expect("Equivalences required");
578                if let Some(input_equivalences) = input_equivalences {
579                    let input_type: &Vec<ReprColumnType> = derived
580                        .last_child()
581                        .value::<ReprRelationType>()
582                        .expect("ReprRelationType required")
583                        .as_ref()
584                        .unwrap();
585                    let reducer = input_equivalences.reducer();
586                    for key in group_key.iter_mut() {
587                        // Semijoin elimination currently fails if you do more advanced simplification than
588                        // literal substitution.
589                        let old_key = key.clone();
590                        let changed = reducer.reduce_expr(key);
591                        let acceptable_sub = literal_domination(&old_key, key);
592                        if changed || !ctx.features.enable_less_reduce_in_eqprop {
593                            key.reduce(input_type);
594                        }
595                        if !acceptable_sub && !literal_domination(&old_key, key) {
596                            key.clone_from(&old_key);
597                        }
598                    }
599                    for aggr in aggregates.iter_mut() {
600                        let changed = reducer.reduce_expr(&mut aggr.expr);
601                        if changed || !ctx.features.enable_less_reduce_in_eqprop {
602                            aggr.expr.reduce(input_type);
603                        }
604                        // A count expression over a non-null expression can discard the expression.
605                        if aggr.func == mz_expr::AggregateFunc::Count && !aggr.distinct {
606                            let mut probe = aggr.expr.clone().call_is_null();
607                            reducer.reduce_expr(&mut probe);
608                            if probe.is_literal_false() {
609                                aggr.expr = MirScalarExpr::literal_true();
610                            }
611                        }
612                    }
613                }
614
615                // To transform `outer_equivalences` to one about `input`, we will "pretend" to pre-pend all of
616                // the input columns, introduce equivalences about the evaluation of `group_key` on them
617                // and the key columns themselves, and then project onto these "input" columns.
618                let input_arity = *derived
619                    .last_child()
620                    .value::<Arity>()
621                    .expect("Arity required");
622                let output_arity = *derived.value::<Arity>().expect("Arity required");
623
624                // Permute `outer_equivalences` to reference columns `input_arity` later.
625                let permutation = (input_arity..(input_arity + output_arity)).collect::<Vec<_>>();
626                outer_equivalences.permute(&permutation[..]);
627                for (index, group) in group_key.iter().enumerate() {
628                    outer_equivalences.classes.push(vec![
629                        MirScalarExpr::column(input_arity + index),
630                        group.clone(),
631                    ]);
632                }
633                outer_equivalences.project(0..input_arity);
634                self.apply(
635                    input,
636                    derived.last_child(),
637                    outer_equivalences,
638                    get_equivalences,
639                    ctx,
640                );
641            }
642            MirRelationExpr::TopK {
643                input,
644                group_key,
645                limit,
646                ..
647            } => {
648                // We must be careful when updating `limit` to not install column references
649                // outside of `group_key`. We'll do this for now with `literal_domination`,
650                // which will ensure we only perform substitutions by a literal.
651                let input_equivalences = derived
652                    .last_child()
653                    .value::<Equivalences>()
654                    .expect("Equivalences required");
655                if let Some(input_equivalences) = input_equivalences {
656                    let input_types: &Vec<ReprColumnType> = derived
657                        .last_child()
658                        .value::<ReprRelationType>()
659                        .expect("ReprRelationType required")
660                        .as_ref()
661                        .unwrap();
662                    let reducer = input_equivalences.reducer();
663                    if let Some(expr) = limit {
664                        let old_expr = expr.clone();
665                        let changed = reducer.reduce_expr(expr);
666                        let acceptable_sub = literal_domination(&old_expr, expr);
667                        if changed || !ctx.features.enable_less_reduce_in_eqprop {
668                            expr.reduce(input_types);
669                        }
670                        if !acceptable_sub && !literal_domination(&old_expr, expr) {
671                            expr.clone_from(&old_expr);
672                        }
673                    }
674                }
675
676                // Discard equivalences among non-key columns, as it is not correct that `input` may drop rows
677                // that violate constraints among non-key columns without affecting the result.
678                // Project to the group_key column indices. After project, group_key[i] has been
679                // remapped to position i; permute restores the original indices.
680                outer_equivalences.project(group_key.iter().copied());
681                outer_equivalences.permute(&group_key[..]);
682                self.apply(
683                    input,
684                    derived.last_child(),
685                    outer_equivalences,
686                    get_equivalences,
687                    ctx,
688                );
689            }
690            MirRelationExpr::Negate { input } => {
691                self.apply(
692                    input,
693                    derived.last_child(),
694                    outer_equivalences,
695                    get_equivalences,
696                    ctx,
697                );
698            }
699            MirRelationExpr::Threshold { input } => {
700                self.apply(
701                    input,
702                    derived.last_child(),
703                    outer_equivalences,
704                    get_equivalences,
705                    ctx,
706                );
707            }
708            MirRelationExpr::Union { .. } => {
709                for (child, derived) in expr.children_mut().rev().zip_eq(derived.children_rev()) {
710                    self.apply(
711                        child,
712                        derived,
713                        outer_equivalences.clone(),
714                        get_equivalences,
715                        ctx,
716                    );
717                }
718            }
719            MirRelationExpr::ArrangeBy { input, .. } => {
720                // TODO: Option to alter arrangement keys, though .. terrifying.
721                self.apply(
722                    input,
723                    derived.last_child(),
724                    outer_equivalences,
725                    get_equivalences,
726                    ctx,
727                );
728            }
729        }
730    }
731}
732
733/// Logic encapsulating our willingness to accept an expression simplification.
734///
735/// For reasons of robustness, we cannot yet perform all recommended simplifications.
736/// Certain transforms expect idiomatic expressions, often around precise use of column
737/// identifiers, rather than equivalent identifiers.
738///
739/// The substitutions we are confident with are those that introduce literals for columns,
740/// or which replace column nullability checks with literals.
741fn literal_domination(old: &MirScalarExpr, new: &MirScalarExpr) -> bool {
742    let mut todo = vec![(old, new)];
743    while let Some((old, new)) = todo.pop() {
744        match (old, new) {
745            (_, MirScalarExpr::Literal(_, _)) => {
746                // Substituting a literal is always acceptable; we don't need to consult
747                // the result of the old expression to determine this.
748            }
749            (
750                MirScalarExpr::CallUnary { func: f0, expr: e0 },
751                MirScalarExpr::CallUnary { func: f1, expr: e1 },
752            ) => {
753                if f0 != f1 {
754                    return false;
755                } else {
756                    todo.push((&**e0, &**e1));
757                }
758            }
759            (
760                MirScalarExpr::CallBinary {
761                    func: f0,
762                    expr1: e01,
763                    expr2: e02,
764                },
765                MirScalarExpr::CallBinary {
766                    func: f1,
767                    expr1: e11,
768                    expr2: e12,
769                },
770            ) => {
771                if f0 != f1 {
772                    return false;
773                } else {
774                    todo.push((&**e01, &**e11));
775                    todo.push((&**e02, &**e12));
776                }
777            }
778            (
779                MirScalarExpr::CallVariadic {
780                    func: f0,
781                    exprs: e0s,
782                },
783                MirScalarExpr::CallVariadic {
784                    func: f1,
785                    exprs: e1s,
786                },
787            ) => {
788                use itertools::Itertools;
789                if f0 != f1 || e0s.len() != e1s.len() {
790                    return false;
791                } else {
792                    todo.extend(e0s.iter().zip_eq(e1s));
793                }
794            }
795            (
796                MirScalarExpr::If {
797                    cond: c0,
798                    then: t0,
799                    els: e0,
800                },
801                MirScalarExpr::If {
802                    cond: c1,
803                    then: t1,
804                    els: e1,
805                },
806            ) => {
807                todo.push((&**c0, &**c1));
808                todo.push((&**t0, &**t1));
809                todo.push((&**e0, &**e1))
810            }
811            _ => {
812                if old != new {
813                    return false;
814                }
815            }
816        }
817    }
818    true
819}