Skip to main content

mz_expr/
interpret.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
10use std::collections::BTreeMap;
11use std::fmt::Debug;
12
13use mz_repr::{Datum, ReprColumnType, ReprRelationType, ReprScalarType, Row, RowArena};
14
15use crate::scalar::func::variadic::And;
16use crate::{
17    BinaryFunc, Eval, EvalError, MapFilterProject, MfpPlan, MirScalarExpr, UnaryFunc,
18    UnmaterializableFunc, VariadicFunc, func,
19};
20/// Whether a datum is a floating-point or numeric `NaN`.
21///
22/// `NaN` sorts as the maximum of the numeric/float [Datum] ordering, but it is
23/// a fixed point of most functions we treat as monotone (e.g. negation maps
24/// `NaN` to `NaN` while flipping the sign of every other value). A range whose
25/// bounds include `NaN` therefore breaks the monotonicity assumption that
26/// [ResultSpec::flat_map] relies on, so the range-narrowing shortcut must be
27/// skipped in that case.
28fn datum_is_nan(datum: Datum) -> bool {
29    match datum {
30        Datum::Float32(f) => f.is_nan(),
31        Datum::Float64(f) => f.is_nan(),
32        Datum::Numeric(n) => n.0.is_nan(),
33        _ => false,
34    }
35}
36
37/// Whether a datum is a floating-point or numeric infinity.
38fn datum_is_infinite(datum: Datum) -> bool {
39    match datum {
40        Datum::Float32(f) => f.is_infinite(),
41        Datum::Float64(f) => f.is_infinite(),
42        Datum::Numeric(n) => n.0.is_infinite(),
43        _ => false,
44    }
45}
46
47/// An inclusive range of non-null datum values.
48#[derive(Clone, Eq, PartialEq, Debug)]
49enum Values<'a> {
50    /// This range contains no values.
51    Empty,
52    /// An inclusive range. Invariant: the first element is always <= the second.
53    // TODO: a variant for small sets of data would avoid losing precision here.
54    Within(Datum<'a>, Datum<'a>),
55    /// Constraints on structured fields, useful for recursive structures like maps.
56    /// Fields that are not present in the map default to Values::All.
57    // TODO: consider using this variant, or similar, for Datum::List.
58    Nested(BTreeMap<Datum<'a>, ResultSpec<'a>>),
59    /// This range might contain any value. Since we're overapproximating, this is often used
60    /// as a "safe fallback" when we can't determine the right boundaries for a range.
61    All,
62}
63
64impl<'a> Values<'a> {
65    fn just(a: Datum<'a>) -> Values<'a> {
66        match a {
67            Datum::Map(datum_map) => Values::Nested(
68                datum_map
69                    .iter()
70                    .map(|(key, val)| (key.into(), ResultSpec::value(val)))
71                    .collect(),
72            ),
73            other => Self::Within(other, other),
74        }
75    }
76
77    fn union(self, other: Values<'a>) -> Values<'a> {
78        match (self, other) {
79            (Values::Empty, r) => r,
80            (r, Values::Empty) => r,
81            (Values::Within(a0, a1), Values::Within(b0, b1)) => {
82                Values::Within(a0.min(b0), a1.max(b1))
83            }
84            (Values::Nested(a), Values::Nested(mut b)) => {
85                // `Nested(map)` treats keys missing from `map` as fully unconstrained, so a
86                // key present in only one side of the union must be treated as `anything`
87                // on the other side. Because `x ∪ anything = anything`, such keys drop
88                // out of the merged map (the Nested default is already "anything").
89                let mut merged = BTreeMap::new();
90                for (key, a_spec) in a {
91                    if let Some(b_spec) = b.remove(&key) {
92                        let unioned = a_spec.union(b_spec);
93                        if unioned != ResultSpec::anything() {
94                            merged.insert(key, unioned);
95                        }
96                    }
97                }
98                if merged.is_empty() {
99                    Values::All
100                } else {
101                    Values::Nested(merged)
102                }
103            }
104            _ => Values::All,
105        }
106    }
107
108    fn intersect(self, other: Values<'a>) -> Values<'a> {
109        match (self, other) {
110            (Values::Empty, _) => Values::Empty,
111            (_, Values::Empty) => Values::Empty,
112            (Values::Within(a0, a1), Values::Within(b0, b1)) => {
113                let min = a0.max(b0);
114                let max = a1.min(b1);
115                if min <= max {
116                    Values::Within(min, max)
117                } else {
118                    Values::Empty
119                }
120            }
121            (Values::Nested(mut a), Values::Nested(b)) => {
122                for (datum, other_spec) in b {
123                    let spec = a.entry(datum).or_insert_with(ResultSpec::anything);
124                    *spec = spec.clone().intersect(other_spec);
125                }
126                Values::Nested(a)
127            }
128            (Values::All, v) => v,
129            (v, Values::All) => v,
130            // A `Within` range and a `Nested` (map) spec can genuinely overlap:
131            // `Datum` order places `Map` between `List` and `Numeric`, so a
132            // range straddling those tags contains map values. We can't compute
133            // the precise intersection, so return the (more structured) `Nested`
134            // side as a sound over-approximation. Returning `Empty` here would
135            // drop real values and let pushdown wrongly discard a part.
136            (nested @ Values::Nested(_), Values::Within(_, _))
137            | (Values::Within(_, _), nested @ Values::Nested(_)) => nested,
138        }
139    }
140
141    fn may_contain(&self, value: Datum<'a>) -> bool {
142        match self {
143            Values::Empty => false,
144            Values::Within(min, max) => *min <= value && value <= *max,
145            Values::All => true,
146            Values::Nested(field_map) => match value {
147                Datum::Map(datum_map) => {
148                    datum_map
149                        .iter()
150                        .all(|(key, val)| match field_map.get(&key.into()) {
151                            None => true,
152                            Some(nested) => nested.may_contain(val),
153                        })
154                }
155                _ => false,
156            },
157        }
158    }
159
160    /// Returns the sole datum in this value set, if it is known to be a single
161    /// value. Returns `None` otherwise (for empty sets, ranges with distinct
162    /// endpoints, structured constraints, and the unconstrained set).
163    ///
164    /// Prefer this over pattern-matching on [Values::Within] directly when you
165    /// only need the "single known value" case: it's robust against future
166    /// variants of [Values] (e.g. a small-set representation) automatically
167    /// degrading to "not a single value" rather than silently mis-matching.
168    fn as_single(&self) -> Option<Datum<'a>> {
169        match self {
170            Values::Within(a, b) if a == b => Some(*a),
171            _ => None,
172        }
173    }
174}
175
176/// An approximation of the set of values an expression might have, including whether or not it
177/// might be null or an error value. This is generally an _overapproximation_, in the sense that
178/// [ResultSpec::may_contain] may return true even if the argument is not included in the set.
179/// (However, it should never return false when the value _is_ included!)
180#[derive(Debug, Clone, Eq, PartialEq)]
181pub struct ResultSpec<'a> {
182    /// True if the expression may evaluate to [Datum::Null].
183    nullable: bool,
184    /// True if the expression may evaluate to an error.
185    fallible: bool,
186    /// The range of possible (non-null) values that the expression may evaluate to.
187    values: Values<'a>,
188}
189
190impl<'a> ResultSpec<'a> {
191    /// No results match this spec. (For example, an empty table.)
192    pub fn nothing() -> Self {
193        ResultSpec {
194            nullable: false,
195            fallible: false,
196            values: Values::Empty,
197        }
198    }
199
200    /// Every result matches this spec.
201    pub fn anything() -> Self {
202        ResultSpec {
203            nullable: true,
204            fallible: true,
205            values: Values::All,
206        }
207    }
208
209    /// Every result matches this spec.
210    pub fn any_infallible() -> Self {
211        ResultSpec {
212            nullable: true,
213            fallible: false,
214            values: Values::All,
215        }
216    }
217
218    /// A spec that only matches null.
219    pub fn null() -> Self {
220        ResultSpec {
221            nullable: true,
222            ..Self::nothing()
223        }
224    }
225
226    /// A spec that only matches error values.
227    pub fn fails() -> Self {
228        ResultSpec {
229            fallible: true,
230            ..Self::nothing()
231        }
232    }
233
234    /// A spec that matches all values of a given type.
235    pub fn has_type(col: &ReprColumnType, fallible: bool) -> ResultSpec<'a> {
236        let values = match &col.scalar_type {
237            ReprScalarType::Bool => Values::Within(Datum::False, Datum::True),
238            // TODO: add bounds for other bounded types, like integers
239            _ => Values::All,
240        };
241        ResultSpec {
242            nullable: col.nullable,
243            fallible,
244            values,
245        }
246    }
247
248    /// A spec that only matches the given value.
249    pub fn value(value: Datum<'a>) -> ResultSpec<'a> {
250        match value {
251            Datum::Null => Self::null(),
252            nonnull => ResultSpec {
253                values: Values::just(nonnull),
254                ..Self::nothing()
255            },
256        }
257    }
258
259    /// A spec for the values between `min` and `max` inclusive.
260    ///
261    /// Unordered bounds widen to [`ResultSpec::value_all`] instead of collapsing
262    /// to [`ResultSpec::nothing`]: they mean the bounds are unusable, not that
263    /// the column holds nothing. Persist float stats produce them, because arrow
264    /// orders floats totally, putting `-NaN` below `-Infinity`, while the
265    /// [`Datum`] order compared here ranks every NaN above every finite value.
266    /// Collapsing lost every other row in such a part (PER-53).
267    pub fn value_between(min: Datum<'a>, max: Datum<'a>) -> ResultSpec<'a> {
268        assert!(!min.is_null());
269        assert!(!max.is_null());
270        if min <= max {
271            ResultSpec {
272                values: Values::Within(min, max),
273                ..ResultSpec::nothing()
274            }
275        } else {
276            ResultSpec::value_all()
277        }
278    }
279
280    /// A spec that matches any non-null value.
281    pub fn value_all() -> ResultSpec<'a> {
282        ResultSpec {
283            values: Values::All,
284            ..ResultSpec::nothing()
285        }
286    }
287
288    /// A spec that matches Datum::Maps of the given type.
289    pub fn map_spec(map: BTreeMap<Datum<'a>, ResultSpec<'a>>) -> ResultSpec<'a> {
290        ResultSpec {
291            values: Values::Nested(map),
292            ..ResultSpec::nothing()
293        }
294    }
295
296    /// Given two specs, returns a new spec that matches anything that either original spec would match.
297    pub fn union(self, other: ResultSpec<'a>) -> ResultSpec<'a> {
298        ResultSpec {
299            nullable: self.nullable || other.nullable,
300            fallible: self.fallible || other.fallible,
301            values: self.values.union(other.values),
302        }
303    }
304
305    /// Given two specs, returns a new spec that only matches things that both original specs would match.
306    pub fn intersect(self, other: ResultSpec<'a>) -> ResultSpec<'a> {
307        ResultSpec {
308            nullable: self.nullable && other.nullable,
309            fallible: self.fallible && other.fallible,
310            values: self.values.intersect(other.values),
311        }
312    }
313
314    /// Check if a particular value matches the spec.
315    pub fn may_contain(&self, value: Datum<'a>) -> bool {
316        if value == Datum::Null {
317            return self.nullable;
318        }
319
320        self.values.may_contain(value)
321    }
322
323    /// Check if an error value matches the spec.
324    pub fn may_fail(&self) -> bool {
325        self.fallible
326    }
327
328    /// Whether this spec is pinned to a single concrete (non-null) value.
329    ///
330    /// When it is, evaluating a function on the input is exact rather than an
331    /// endpoint-sampled approximation of a range, so the interpreter can trust
332    /// the sampled fallibility. When it isn't, endpoint sampling cannot prove a
333    /// `could_error` function infallible over the range (see the fallibility
334    /// handling in [`ColumnSpecs::unary`] and friends).
335    fn is_single_value(&self) -> bool {
336        self.values.as_single().is_some()
337    }
338
339    /// Whether the value range might include a floating-point or numeric
340    /// infinity. Infinities sort at the extremes of the order, so a `Within`
341    /// range includes one only as an endpoint.
342    fn may_be_infinite(&self) -> bool {
343        match &self.values {
344            Values::Within(min, max) => datum_is_infinite(*min) || datum_is_infinite(*max),
345            Values::All => true,
346            Values::Empty | Values::Nested(_) => false,
347        }
348    }
349
350    /// This method "maps" a function across the `ResultSpec`.
351    ///
352    /// As mentioned above, `ResultSpec` represents an approximate set of results.
353    /// If we actually stored each result in the set, `flat_map` could be implemented by passing
354    /// each result to the function one-by-one and unioning the resulting sets. This is possible
355    /// when our values set is empty or contains a single datum, but when it contains a range,
356    /// we can't enumerate all possible values of the set. We handle this by:
357    /// - tracking whether the function is monotone, in which case we can map the range by just
358    ///   mapping the endpoints;
359    /// - using a safe default when we can't infer a tighter bound on the set, eg. [Self::anything].
360    fn flat_map(
361        &self,
362        is_monotone: bool,
363        mut result_map: impl FnMut(Result<Datum<'a>, EvalError>) -> ResultSpec<'a>,
364    ) -> ResultSpec<'a> {
365        let null_spec = if self.nullable {
366            result_map(Ok(Datum::Null))
367        } else {
368            ResultSpec::nothing()
369        };
370
371        let error_spec = if self.fallible {
372            // Since we only care about whether / not an error is possible, and not the specific
373            // error, create an arbitrary error here.
374            // NOTE! This assumes that functions do not discriminate on the type of the error.
375            let map_err = result_map(Err(EvalError::Internal("".into())));
376            let raise_err = ResultSpec::fails();
377            // SQL has a very loose notion of evaluation order: https://www.postgresql.org/docs/current/sql-expressions.html#SYNTAX-EXPRESS-EVAL
378            // Here, we account for the possibility that the expression is evaluated strictly,
379            // raising the error, or that it's evaluated lazily by the result_map function
380            // (which may return a non-error result even when given an error as input).
381            raise_err.union(map_err)
382        } else {
383            ResultSpec::nothing()
384        };
385
386        let values_spec = match self.values {
387            Values::Empty => ResultSpec::nothing(),
388            // If this range contains a single datum, just call the function.
389            Values::Within(min, max) if min == max => result_map(Ok(min)),
390            // If this is a range of booleans, we know all the values... just try them.
391            Values::Within(Datum::False, Datum::True) => {
392                result_map(Ok(Datum::False)).union(result_map(Ok(Datum::True)))
393            }
394            // Otherwise, if our function is monotonic, we can try mapping the input
395            // range to an output range. A range whose bounds include `NaN` is
396            // excluded: `NaN` is ordered as the maximum but is a fixed point of
397            // most monotone functions, so evaluating the endpoints does not
398            // bound the interior. Such ranges fall through to the
399            // overapproximation below.
400            Values::Within(min, max) if is_monotone && !datum_is_nan(min) && !datum_is_nan(max) => {
401                let min_result = result_map(Ok(min));
402                let max_result = result_map(Ok(max));
403                // Value, null, and error are orthogonal channels. Monotonicity
404                // lets us bound the *values* by the endpoints, but only when both
405                // endpoints actually produced a value; null and error can't be
406                // bounded from value endpoints, so we just union whatever the
407                // endpoints reported on those channels. (A function that errors
408                // on an interior value while the endpoints don't is handled by
409                // the fallibility guard in `unary`/`binary`/`variadic`, not
410                // here.)
411                match (min_result, max_result) {
412                    // Both endpoints produced a value: bound the interior values
413                    // by their union, and carry any null/error from the endpoints.
414                    (
415                        ResultSpec {
416                            nullable: n1,
417                            fallible: f1,
418                            values: a_values @ Values::Within(..),
419                        },
420                        ResultSpec {
421                            nullable: n2,
422                            fallible: f2,
423                            values: b_values @ Values::Within(..),
424                        },
425                    ) => ResultSpec {
426                        nullable: n1 || n2,
427                        fallible: f1 || f2,
428                        values: a_values.union(b_values),
429                    },
430                    // If both endpoints map purely to null, assume the whole
431                    // range maps to null. (Both endpoints *erroring* is NOT
432                    // enough: an interior input can still produce a value, e.g.
433                    // a cast that rejects both bounds but accepts a value
434                    // between them.)
435                    (
436                        ResultSpec {
437                            nullable: true,
438                            fallible: false,
439                            values: Values::Empty,
440                        },
441                        ResultSpec {
442                            nullable: true,
443                            fallible: false,
444                            values: Values::Empty,
445                        },
446                    ) => ResultSpec::null(),
447                    // Otherwise we can't bound the interior values.
448                    _ => ResultSpec::anything(),
449                }
450            }
451            // TODO: we could return a narrower result for eg. `Values::Nested` with all-`Within` fields.
452            Values::Within(_, _) | Values::Nested(_) | Values::All => ResultSpec::anything(),
453        };
454
455        null_spec.union(error_spec).union(values_spec)
456    }
457}
458
459/// [Abstract interpretation](https://en.wikipedia.org/wiki/Abstract_interpretation) for
460/// [MirScalarExpr].
461///
462/// [MirScalarExpr::eval] implements a "concrete interpreter" for the expression type: given an
463/// expression and specific column values as input, it returns a specific value for the output.
464/// This could be reimplemented using this trait... but it's most useful for "abstract"
465/// interpretations of the expression, where we generalize about sets of possible inputs and outputs.
466/// See [Trace] and [ColumnSpecs] for how this can be useful in practice.
467pub trait Interpreter {
468    type Summary: Clone + Debug + Sized;
469
470    /// A column of the input row.
471    fn column(&self, id: usize) -> Self::Summary;
472
473    /// A literal value.
474    /// (Stored as a row, because we can't own a Datum.)
475    fn literal(&self, result: &Result<Row, EvalError>, col_type: &ReprColumnType) -> Self::Summary;
476    /// A call to an unmaterializable function.
477    ///
478    /// These functions cannot be evaluated by `MirScalarExpr::eval`. They must
479    /// be transformed away by a higher layer.
480    fn unmaterializable(&self, func: &UnmaterializableFunc) -> Self::Summary;
481
482    /// A function call that takes one expression as an argument.
483    fn unary(&self, func: &UnaryFunc, expr: Self::Summary) -> Self::Summary;
484
485    /// A function call that takes two expressions as arguments.
486    fn binary(&self, func: &BinaryFunc, left: Self::Summary, right: Self::Summary)
487    -> Self::Summary;
488
489    /// A function call that takes an arbitrary number of arguments.
490    fn variadic(&self, func: &VariadicFunc, exprs: Vec<Self::Summary>) -> Self::Summary;
491
492    /// Conditionally evaluated expressions.
493    fn cond(&self, cond: Self::Summary, then: Self::Summary, els: Self::Summary) -> Self::Summary;
494
495    /// Evaluate an entire expression, by delegating to the fine-grained methods on [Interpreter].
496    fn expr(&self, expr: &MirScalarExpr) -> Self::Summary {
497        match expr {
498            MirScalarExpr::Column(id, _name) => self.column(*id),
499            MirScalarExpr::Literal(value, col_type) => self.literal(value, col_type),
500            MirScalarExpr::CallUnmaterializable(func) => self.unmaterializable(func),
501            MirScalarExpr::CallUnary { func, expr } => {
502                let expr_range = self.expr(expr);
503                self.unary(func, expr_range)
504            }
505            MirScalarExpr::CallBinary { func, expr1, expr2 } => {
506                let expr1_range = self.expr(expr1);
507                let expr2_range = self.expr(expr2);
508                self.binary(func, expr1_range, expr2_range)
509            }
510            MirScalarExpr::CallVariadic { func, exprs } => {
511                let exprs: Vec<_> = exprs.into_iter().map(|e| self.expr(e)).collect();
512                self.variadic(func, exprs)
513            }
514            MirScalarExpr::If { cond, then, els } => {
515                let cond_range = self.expr(cond);
516                let then_range = self.expr(then);
517                let els_range = self.expr(els);
518                self.cond(cond_range, then_range, els_range)
519            }
520        }
521    }
522
523    /// Specifically, this evaluates the map and filters stages of an MFP: summarize each of the
524    /// map expressions, then `and` together all of the filters.
525    fn mfp_filter(&self, mfp: &MapFilterProject) -> Self::Summary {
526        let mfp_eval = MfpEval::new(self, mfp.input_arity, &mfp.expressions);
527        // NB: self should not be used after this point!
528        let predicates = mfp
529            .predicates
530            .iter()
531            .map(|(_, e)| mfp_eval.expr(e))
532            .collect();
533        mfp_eval.variadic(&And.into(), predicates)
534    }
535
536    /// Similar to [Self::mfp_filter], but includes the additional temporal filters that have been
537    /// broken out.
538    fn mfp_plan_filter(&self, plan: &MfpPlan) -> Self::Summary {
539        let mfp_eval = MfpEval::new(self, plan.mfp.input_arity, &plan.mfp.expressions);
540        // NB: self should not be used after this point!
541        let mut results: Vec<_> = plan
542            .mfp
543            .predicates
544            .iter()
545            .map(|(_, e)| mfp_eval.expr(e))
546            .collect();
547        let mz_now = mfp_eval.unmaterializable(&UnmaterializableFunc::MzNow);
548        for bound in &plan.lower_bounds {
549            let bound_range = mfp_eval.expr(bound);
550            let result = mfp_eval.binary(&BinaryFunc::Lte(func::Lte), bound_range, mz_now.clone());
551            results.push(result);
552        }
553        for bound in &plan.upper_bounds {
554            let bound_range = mfp_eval.expr(bound);
555            let result = mfp_eval.binary(&BinaryFunc::Gte(func::Gte), bound_range, mz_now.clone());
556            results.push(result);
557        }
558        self.variadic(&And.into(), results)
559    }
560}
561
562/// Wrap another interpreter, but tack a few extra columns on at the end. An internal implementation
563/// detail of `eval_mfp` and `eval_mfp_plan`.
564pub(crate) struct MfpEval<'a, E: Interpreter + ?Sized> {
565    evaluator: &'a E,
566    input_arity: usize,
567    expressions: Vec<E::Summary>,
568}
569
570impl<'a, E: Interpreter + ?Sized> MfpEval<'a, E> {
571    pub(crate) fn new(evaluator: &'a E, input_arity: usize, expressions: &[MirScalarExpr]) -> Self {
572        let mut mfp_eval = MfpEval {
573            evaluator,
574            input_arity,
575            expressions: vec![],
576        };
577        for expr in expressions {
578            let result = mfp_eval.expr(expr);
579            mfp_eval.expressions.push(result);
580        }
581        mfp_eval
582    }
583}
584
585impl<'a, E: Interpreter + ?Sized> Interpreter for MfpEval<'a, E> {
586    type Summary = E::Summary;
587
588    fn column(&self, id: usize) -> Self::Summary {
589        if id < self.input_arity {
590            self.evaluator.column(id)
591        } else {
592            self.expressions[id - self.input_arity].clone()
593        }
594    }
595
596    fn literal(&self, result: &Result<Row, EvalError>, col_type: &ReprColumnType) -> Self::Summary {
597        self.evaluator.literal(result, col_type)
598    }
599
600    fn unmaterializable(&self, func: &UnmaterializableFunc) -> Self::Summary {
601        self.evaluator.unmaterializable(func)
602    }
603
604    fn unary(&self, func: &UnaryFunc, expr: Self::Summary) -> Self::Summary {
605        self.evaluator.unary(func, expr)
606    }
607
608    fn binary(
609        &self,
610        func: &BinaryFunc,
611        left: Self::Summary,
612        right: Self::Summary,
613    ) -> Self::Summary {
614        self.evaluator.binary(func, left, right)
615    }
616
617    fn variadic(&self, func: &VariadicFunc, exprs: Vec<Self::Summary>) -> Self::Summary {
618        self.evaluator.variadic(func, exprs)
619    }
620
621    fn cond(&self, cond: Self::Summary, then: Self::Summary, els: Self::Summary) -> Self::Summary {
622        self.evaluator.cond(cond, then, els)
623    }
624}
625
626/// A unary function we've added special-case handling for; including:
627/// - A three-argument function, taking and returning [ResultSpec]s. This
628///   overrides the default function-handling logic entirely.
629/// - Metadata on whether / not this function is pushdownable. See [Trace].
630struct SpecialUnary {
631    map_fn: for<'a, 'b> fn(&'b ColumnSpecs<'a>, ResultSpec<'a>) -> ResultSpec<'a>,
632    pushdownable: bool,
633}
634
635impl SpecialUnary {
636    /// Returns the special-case handling for a particular function, if it exists.
637    fn for_func(func: &UnaryFunc) -> Option<SpecialUnary> {
638        /// Eager in the same sense as `func.rs` uses the term; this assumes that
639        /// nulls and errors propagate up, and we only need to define the behaviour
640        /// on values.
641        fn eagerly<'b>(
642            spec: ResultSpec<'b>,
643            value_fn: impl FnOnce(Values<'b>) -> ResultSpec<'b>,
644        ) -> ResultSpec<'b> {
645            let result = match spec.values {
646                Values::Empty => ResultSpec::nothing(),
647                other => value_fn(other),
648            };
649            ResultSpec {
650                fallible: spec.fallible || result.fallible,
651                nullable: spec.nullable || result.nullable,
652                values: result.values,
653            }
654        }
655        match func {
656            UnaryFunc::TryParseMonotonicIso8601Timestamp(_) => Some(SpecialUnary {
657                map_fn: |specs, range| {
658                    let expr = MirScalarExpr::CallUnary {
659                        func: UnaryFunc::TryParseMonotonicIso8601Timestamp(
660                            crate::func::TryParseMonotonicIso8601Timestamp,
661                        ),
662                        expr: Box::new(MirScalarExpr::column(0)),
663                    };
664                    let eval = |d| specs.eval_result(expr.eval(&[d], specs.arena));
665
666                    eagerly(range, |values| {
667                        match values {
668                            Values::Within(a, b) if a == b => eval(a),
669                            Values::Within(a, b) => {
670                                let spec = eval(a).union(eval(b));
671                                let values_spec = if spec.nullable {
672                                    // At least one of the endpoints of the range wasn't a valid
673                                    // timestamp. We can't compute a precise range in this case.
674                                    // If we used the general is_monotone handling, that code would
675                                    // incorrectly assume the whole range mapped to null if each
676                                    // endpoint did.
677                                    ResultSpec::value_all()
678                                } else {
679                                    spec
680                                };
681                                // A range of strings will always contain strings that don't parse
682                                // as timestamps - so unlike the general case, we'll assume null
683                                // is present in every range of output values.
684                                values_spec.union(ResultSpec::null())
685                            }
686                            // Otherwise, assume the worst: this function may return either a valid
687                            // value or null.
688                            _ => ResultSpec::any_infallible(),
689                        }
690                    })
691                },
692                pushdownable: true,
693            }),
694            _ => None,
695        }
696    }
697}
698
699/// The abstract-domain counterpart of a [BinaryFunc]: a binary function
700/// we've added special-case handling for; including:
701/// - Either a complete override of [ResultSpec] computation, or a way to
702///   compute monotonicity dynamically from the input specs.
703/// - Metadata on whether / not this function is pushdownable. See [Trace].
704///
705/// Note: today a function can have *either* a handler override *or* a
706/// dynamic-monotonicity verdict, but not both. If a future function wants
707/// both, promote [AbstractFuncHandler] from an enum to a struct with two
708/// optional fields.
709struct AbstractFunc {
710    handler: AbstractFuncHandler,
711    /// `(left, right)`: per-argument pushdownability hint consumed by
712    /// [Trace]. `true` for an argument means the function preserves enough
713    /// structure that, with sufficient information about that argument's
714    /// range, the output spec can be predicted — i.e. the predicate is a
715    /// pushdown candidate when that argument is constant or a tight range.
716    pushdownable: (bool, bool),
717}
718
719/// How an [AbstractFunc] computes the output [ResultSpec].
720enum AbstractFuncHandler {
721    /// Completely override the spec computation; the default flat-map machinery
722    /// is bypassed.
723    Override(for<'a> fn(ResultSpec<'a>, ResultSpec<'a>) -> ResultSpec<'a>),
724    /// Use the default flat-map machinery, but with a monotonicity verdict that
725    /// depends on the input specs. This lets us claim monotonicity for cases
726    /// the static `LazyBinaryFunc::is_monotone` annotation can't safely claim:
727    /// for instance, `t + INTERVAL '1' day` is monotone in `t`, but `t + i`
728    /// generally isn't (the calendar-month / day-clamping arithmetic in
729    /// `add_timestamp_interval` is non-monotone when `i.months != 0`).
730    DynamicMonotone(fn(&ResultSpec<'_>, &ResultSpec<'_>) -> (bool, bool)),
731}
732
733impl AbstractFunc {
734    /// Returns the special-case handling for a particular function, if it exists.
735    fn for_func(func: &BinaryFunc) -> Option<AbstractFunc> {
736        /// Eager in the same sense as `func.rs` uses the term; this assumes that
737        /// nulls and errors propagate up, and we only need to define the behaviour
738        /// on values.
739        fn eagerly<'b>(
740            left: ResultSpec<'b>,
741            right: ResultSpec<'b>,
742            value_fn: impl FnOnce(Values<'b>, Values<'b>) -> ResultSpec<'b>,
743        ) -> ResultSpec<'b> {
744            let result = match (left.values, right.values) {
745                (Values::Empty, _) | (_, Values::Empty) => ResultSpec::nothing(),
746                (l, r) => value_fn(l, r),
747            };
748            ResultSpec {
749                fallible: left.fallible || right.fallible || result.fallible,
750                nullable: left.nullable || right.nullable || result.nullable,
751                values: result.values,
752            }
753        }
754
755        fn jsonb_get_string<'b>(
756            left: ResultSpec<'b>,
757            right: ResultSpec<'b>,
758            stringify: bool,
759        ) -> ResultSpec<'b> {
760            eagerly(left, right, |left, right| {
761                let nested_spec = match (left, right) {
762                    (Values::Nested(mut map_spec), Values::Within(key, key2)) if key == key2 => {
763                        map_spec.remove(&key)
764                    }
765                    _ => None,
766                };
767
768                if let Some(field_spec) = nested_spec {
769                    if stringify {
770                        // We only preserve value-range information when stringification
771                        // is a noop. (Common in real queries.)
772                        let values = match field_spec.values {
773                            Values::Empty => Values::Empty,
774                            Values::Within(min @ Datum::String(_), max @ Datum::String(_)) => {
775                                Values::Within(min, max)
776                            }
777                            Values::Within(_, _) | Values::Nested(_) | Values::All => Values::All,
778                        };
779                        ResultSpec {
780                            values,
781                            ..field_spec
782                        }
783                    } else {
784                        field_spec
785                    }
786                } else {
787                    // The implementation of `jsonb_get_string` always returns
788                    // `Ok(...)`. Morally, everything has a string
789                    // representation, and the worst you can get is a NULL,
790                    // which maps to a NULL.
791                    ResultSpec::any_infallible()
792                }
793            })
794        }
795
796        fn eq<'b>(left: ResultSpec<'b>, right: ResultSpec<'b>) -> ResultSpec<'b> {
797            eagerly(left, right, |left, right| {
798                // `eq` might return true if there's any overlap between the range of its two arguments...
799                let maybe_true = match left.clone().intersect(right.clone()) {
800                    Values::Empty => ResultSpec::nothing(),
801                    _ => ResultSpec::value(Datum::True),
802                };
803
804                // ...and may return false if the union contains at least two distinct values.
805                // Note that the `Empty` case is handled by `eagerly` above.
806                let maybe_false = match left.union(right) {
807                    Values::Within(a, b) if a == b => ResultSpec::nothing(),
808                    _ => ResultSpec::value(Datum::False),
809                };
810
811                maybe_true.union(maybe_false)
812            })
813        }
814
815        /// `add_timestamp_interval` and friends do calendar-month arithmetic
816        /// with day-clamping, which is non-monotone in either argument when
817        /// `interval.months != 0`. But when `interval.months == 0` the
818        /// operation reduces to adding a fixed number of microseconds, which
819        /// *is* monotone in both arguments. The static `is_monotone`
820        /// annotation has to pick the conservative answer; this dynamic check
821        /// recovers filter pushdown for the common case of literal
822        /// `INTERVAL '<N>' day`-style predicates.
823        fn timestamp_plus_interval_monotone(
824            _left: &ResultSpec<'_>,
825            right: &ResultSpec<'_>,
826        ) -> (bool, bool) {
827            let months_zero = matches!(
828                right.values.as_single(),
829                Some(Datum::Interval(i)) if i.months == 0,
830            );
831            (months_zero, months_zero)
832        }
833
834        match func {
835            BinaryFunc::JsonbGetString(_) => Some(AbstractFunc {
836                handler: AbstractFuncHandler::Override(|l, r| jsonb_get_string(l, r, false)),
837                pushdownable: (true, false),
838            }),
839            BinaryFunc::JsonbGetStringStringify(_) => Some(AbstractFunc {
840                handler: AbstractFuncHandler::Override(|l, r| jsonb_get_string(l, r, true)),
841                pushdownable: (true, false),
842            }),
843            BinaryFunc::Eq(_) => Some(AbstractFunc {
844                handler: AbstractFuncHandler::Override(eq),
845                pushdownable: (true, true),
846            }),
847            BinaryFunc::AddTimestampInterval(_)
848            | BinaryFunc::AddTimestampTzInterval(_)
849            | BinaryFunc::SubTimestampInterval(_)
850            | BinaryFunc::SubTimestampTzInterval(_) => Some(AbstractFunc {
851                handler: AbstractFuncHandler::DynamicMonotone(timestamp_plus_interval_monotone),
852                // For [Trace]: we *might* be pushdownable in the first argument
853                // (we are when the interval is a literal with no months). The
854                // interval argument is reported as non-pushdownable so that
855                // `t_col +/- col_interval` doesn't get routed through pushdown
856                // for no benefit; if both sides are constants the predicate
857                // collapses anyway.
858                pushdownable: (true, false),
859            }),
860            _ => None,
861        }
862    }
863}
864
865#[derive(Clone, Debug)]
866pub struct ColumnSpec<'a> {
867    pub col_type: ReprColumnType,
868    pub range: ResultSpec<'a>,
869}
870
871/// An interpreter that:
872/// - stores both the type and the range of possible values for every column and
873///   unmaterializable function. (See the `push_` methods.)
874/// - given an expression (or MFP, etc.), returns the range of possible results that evaluating that
875///   expression might have. (See the `eval_` methods.)
876#[derive(Clone, Debug)]
877pub struct ColumnSpecs<'a> {
878    pub relation: &'a ReprRelationType,
879    pub columns: Vec<ResultSpec<'a>>,
880    pub unmaterializables: BTreeMap<UnmaterializableFunc, ResultSpec<'a>>,
881    pub arena: &'a RowArena,
882}
883
884impl<'a> ColumnSpecs<'a> {
885    // Interpreting a variadic function can lead to exponential blowup: there are up to 4 possibly-
886    // interesting values for each argument (error, null, range bounds...) and in the worst case
887    // we may need to test every combination. We mitigate that here in two ways:
888    // - Adding a linear-time optimization for associative functions like AND, OR, COALESCE.
889    // - Limiting the number of arguments we'll pass on to eval. If this limit is crossed, we'll
890    //   return our default / safe overapproximation instead.
891    const MAX_EVAL_ARGS: usize = 6;
892
893    /// Create a new, empty set of column specs. (Initially, the only assumption we make about the
894    /// data in the column is that it matches the type.)
895    pub fn new(relation: &'a ReprRelationType, arena: &'a RowArena) -> Self {
896        let columns = relation
897            .column_types
898            .iter()
899            .map(|ct| ResultSpec::has_type(ct, false))
900            .collect();
901        ColumnSpecs {
902            relation,
903            columns,
904            unmaterializables: Default::default(),
905            arena,
906        }
907    }
908
909    /// Restrict the set of possible values in a given column. (By intersecting it with the existing
910    /// spec.)
911    pub fn push_column(&mut self, id: usize, update: ResultSpec<'a>) {
912        let range = self.columns.get_mut(id).expect("valid column id");
913        *range = range.clone().intersect(update);
914    }
915
916    /// Restrict the set of possible values a given unmaterializable func might return. (By
917    /// intersecting it with the existing spec.)
918    pub fn push_unmaterializable(&mut self, func: UnmaterializableFunc, update: ResultSpec<'a>) {
919        let range = self
920            .unmaterializables
921            .entry(func.clone())
922            .or_insert_with(|| ResultSpec::has_type(&func.output_type(), true));
923        *range = range.clone().intersect(update);
924    }
925
926    fn eval_result<'b, E>(&self, result: Result<Datum<'b>, E>) -> ResultSpec<'a> {
927        match result {
928            Ok(Datum::Null) => ResultSpec {
929                nullable: true,
930                ..ResultSpec::nothing()
931            },
932            Ok(d) => ResultSpec {
933                values: Values::just(self.arena.make_datum(|packer| packer.push(d))),
934                ..ResultSpec::nothing()
935            },
936            Err(_) => ResultSpec {
937                fallible: true,
938                ..ResultSpec::nothing()
939            },
940        }
941    }
942
943    fn set_literal(expr: &mut MirScalarExpr, update: Result<Datum, EvalError>) {
944        match expr {
945            MirScalarExpr::Literal(literal, col_type) => match update {
946                Err(error) => *literal = Err(error),
947                Ok(datum) => {
948                    assert!(
949                        datum.is_instance_of(col_type),
950                        "{datum:?} must be an instance of {col_type:?}"
951                    );
952                    match literal {
953                        // Reuse the allocation if we can
954                        Ok(row) => row.packer().push(datum),
955                        literal => *literal = Ok(Row::pack_slice(&[datum])),
956                    }
957                }
958            },
959            _ => panic!("not a literal"),
960        }
961    }
962
963    fn set_argument(expr: &mut MirScalarExpr, arg: usize, value: Result<Datum, EvalError>) {
964        match (expr, arg) {
965            (MirScalarExpr::CallUnary { expr, .. }, 0) => Self::set_literal(expr, value),
966            (MirScalarExpr::CallBinary { expr1, .. }, 0) => Self::set_literal(expr1, value),
967            (MirScalarExpr::CallBinary { expr2, .. }, 1) => Self::set_literal(expr2, value),
968            (MirScalarExpr::CallVariadic { exprs, .. }, n) if n < exprs.len() => {
969                Self::set_literal(&mut exprs[n], value)
970            }
971            _ => panic!("illegal argument for expression"),
972        }
973    }
974
975    /// A literal with the given type and a trivial default value. Callers should ensure that
976    /// [Self::set_literal] is called on the resulting expression to give it a meaningful value
977    /// before evaluating.
978    fn placeholder(col_type: ReprColumnType) -> MirScalarExpr {
979        MirScalarExpr::Literal(Err(EvalError::Internal("".into())), col_type)
980    }
981}
982
983impl<'a> Interpreter for ColumnSpecs<'a> {
984    type Summary = ColumnSpec<'a>;
985
986    fn column(&self, id: usize) -> Self::Summary {
987        let col_type = self.relation.column_types[id].clone();
988        let range = self.columns[id].clone();
989        ColumnSpec { col_type, range }
990    }
991
992    fn literal(&self, result: &Result<Row, EvalError>, col_type: &ReprColumnType) -> Self::Summary {
993        let col_type = col_type.clone();
994        let range = self.eval_result(result.as_ref().map(|row| {
995            self.arena
996                .make_datum(|packer| packer.push(row.unpack_first()))
997        }));
998        ColumnSpec { col_type, range }
999    }
1000
1001    fn unmaterializable(&self, func: &UnmaterializableFunc) -> Self::Summary {
1002        let col_type = func.output_type();
1003        let range = self
1004            .unmaterializables
1005            .get(func)
1006            .cloned()
1007            .unwrap_or_else(|| ResultSpec::has_type(&func.output_type(), true));
1008        ColumnSpec { col_type, range }
1009    }
1010
1011    fn unary(&self, func: &UnaryFunc, summary: Self::Summary) -> Self::Summary {
1012        let fallible = func.could_error() || summary.range.fallible;
1013        // Endpoint sampling proves a monotone function's output value range, but
1014        // it cannot prove the function never errors on an interior value of a
1015        // multi-valued range: monotonicity says nothing about where errors
1016        // occur (e.g. `round(_, scale)` overflows on inputs whose exponent hits
1017        // a bad branch, `numeric::mz_timestamp` rejects fractional inputs). So
1018        // we do not let it conclude a could-error function is infallible over
1019        // such a range.
1020        let input_multivalued = !summary.range.is_single_value();
1021        let mapped_spec = if let Some(special) = SpecialUnary::for_func(func) {
1022            (special.map_fn)(self, summary.range)
1023        } else {
1024            let is_monotone = func.is_monotone();
1025            let mut expr = MirScalarExpr::CallUnary {
1026                func: func.clone(),
1027                expr: Box::new(Self::placeholder(summary.col_type.clone())),
1028            };
1029            summary.range.flat_map(is_monotone, |datum| {
1030                Self::set_argument(&mut expr, 0, datum);
1031                self.eval_result(expr.eval(&[], self.arena))
1032            })
1033        };
1034
1035        let col_type = func.output_type(summary.col_type);
1036
1037        let mut range = mapped_spec.intersect(ResultSpec::has_type(&col_type, fallible));
1038        // `intersect` only ANDs the fallible flag, so `has_type` above narrows
1039        // the value and null domain but cannot surface an error that endpoint
1040        // sampling missed. Force it for a could-error function over a range.
1041        if fallible && input_multivalued {
1042            range.fallible = true;
1043        }
1044        ColumnSpec { col_type, range }
1045    }
1046
1047    fn binary(
1048        &self,
1049        func: &BinaryFunc,
1050        left: Self::Summary,
1051        right: Self::Summary,
1052    ) -> Self::Summary {
1053        let fallible = func.could_error() || left.range.fallible || right.range.fallible;
1054        // See the note in `unary`: endpoint sampling cannot rule out an interior
1055        // error, so a could-error function is fallible over a multi-valued range.
1056        let inputs_multivalued = !left.range.is_single_value() || !right.range.is_single_value();
1057        let operand_may_be_infinite = left.range.may_be_infinite() || right.range.may_be_infinite();
1058
1059        let special = AbstractFunc::for_func(func);
1060        let (left_monotonic, right_monotonic) = match &special {
1061            Some(AbstractFunc {
1062                handler: AbstractFuncHandler::DynamicMonotone(monotone_fn),
1063                ..
1064            }) => monotone_fn(&left.range, &right.range),
1065            _ => func.is_monotone(),
1066        };
1067
1068        let mapped_spec = match special {
1069            Some(AbstractFunc {
1070                handler: AbstractFuncHandler::Override(f),
1071                ..
1072            }) => f(left.range, right.range),
1073            _ => {
1074                let mut expr = MirScalarExpr::CallBinary {
1075                    func: func.clone(),
1076                    expr1: Box::new(Self::placeholder(left.col_type.clone())),
1077                    expr2: Box::new(Self::placeholder(right.col_type.clone())),
1078                };
1079                left.range.flat_map(left_monotonic, |left_result| {
1080                    Self::set_argument(&mut expr, 0, left_result);
1081                    right.range.flat_map(right_monotonic, |right_result| {
1082                        Self::set_argument(&mut expr, 1, right_result);
1083                        self.eval_result(expr.eval(&[], self.arena))
1084                    })
1085                })
1086            }
1087        };
1088
1089        let col_type = func.output_type(&[left.col_type, right.col_type]);
1090
1091        let mut range = mapped_spec.intersect(ResultSpec::has_type(&col_type, fallible));
1092        // `intersect` only ANDs the fallible flag, so force the interior
1093        // fallibility it cannot add (see the note in `unary`).
1094        if fallible && inputs_multivalued {
1095            range.fallible = true;
1096        }
1097        // Some functions (multiplication, division) are not corner-sampleable
1098        // when an operand may be infinite: their indeterminate forms (`∞ * 0`,
1099        // `∞ / ∞`) evaluate to a value the endpoints don't bound, and that value
1100        // can be reached only from the interior (e.g. `[-∞, +∞] * 0` maps both
1101        // endpoints to `NaN` while its finite interior maps to `0`, and
1102        // `finite / ∞ = 0` is stepped over when both endpoints are `∞ / ∞ =
1103        // NaN`). Fall back to the full value domain for them.
1104        if operand_may_be_infinite && !func.is_infinity_monotone() {
1105            range.values = Values::All;
1106        }
1107        ColumnSpec { col_type, range }
1108    }
1109
1110    fn variadic(&self, func: &VariadicFunc, args: Vec<Self::Summary>) -> Self::Summary {
1111        let fallible = func.could_error() || args.iter().any(|s| s.range.fallible);
1112        let inputs_multivalued = args.iter().any(|s| !s.range.is_single_value());
1113        if func.is_associative() && args.len() > 2 {
1114            // To avoid a combinatorial explosion, evaluate large variadic calls as a series of
1115            // smaller ones, since associativity guarantees we'll get compatible results.
1116            return args
1117                .into_iter()
1118                .reduce(|a, b| self.variadic(func, vec![a, b]))
1119                .expect("reducing over a non-empty argument list");
1120        }
1121
1122        let mapped_spec = if args.len() >= Self::MAX_EVAL_ARGS {
1123            ResultSpec::anything()
1124        } else {
1125            fn eval_loop<'a>(
1126                is_monotonic: bool,
1127                expr: &mut MirScalarExpr,
1128                args: &[ColumnSpec<'a>],
1129                index: usize,
1130                datum_map: &mut impl FnMut(&MirScalarExpr) -> ResultSpec<'a>,
1131            ) -> ResultSpec<'a> {
1132                if index >= args.len() {
1133                    datum_map(expr)
1134                } else {
1135                    args[index].range.flat_map(is_monotonic, |datum| {
1136                        ColumnSpecs::set_argument(expr, index, datum);
1137                        eval_loop(is_monotonic, expr, args, index + 1, datum_map)
1138                    })
1139                }
1140            }
1141
1142            let mut fn_expr = MirScalarExpr::CallVariadic {
1143                func: func.clone(),
1144                exprs: args
1145                    .iter()
1146                    .map(|spec| Self::placeholder(spec.col_type.clone()))
1147                    .collect(),
1148            };
1149            eval_loop(func.is_monotone(), &mut fn_expr, &args, 0, &mut |expr| {
1150                self.eval_result(expr.eval(&[], self.arena))
1151            })
1152        };
1153
1154        let col_types = args.into_iter().map(|spec| spec.col_type).collect();
1155        let col_type = func.output_type(col_types);
1156
1157        let mut range = mapped_spec.intersect(ResultSpec::has_type(&col_type, fallible));
1158        // `intersect` only ANDs the fallible flag, so force the interior
1159        // fallibility it cannot add (see the note in `unary`).
1160        if fallible && inputs_multivalued {
1161            range.fallible = true;
1162        }
1163
1164        ColumnSpec { col_type, range }
1165    }
1166
1167    fn cond(&self, cond: Self::Summary, then: Self::Summary, els: Self::Summary) -> Self::Summary {
1168        let col_type = then
1169            .col_type
1170            .union(&els.col_type)
1171            .expect("failed type union for cond during abstract interpretation");
1172
1173        let range = cond
1174            .range
1175            .flat_map(true, |datum| match datum {
1176                Ok(Datum::True) => then.range.clone(),
1177                // A false OR null condition takes the `els` branch, matching
1178                // `MirScalarExpr::eval` (`Datum::False | Datum::Null => els`).
1179                // Mapping null to `fails()` here would drop `els` from the value
1180                // channel and let pushdown wrongly rule out the else result.
1181                Ok(Datum::False) | Ok(Datum::Null) => els.range.clone(),
1182                _ => ResultSpec::fails(),
1183            })
1184            .intersect(ResultSpec::has_type(&col_type, true));
1185
1186        ColumnSpec { col_type, range }
1187    }
1188
1189    /// Override the default implementations of [Self::mfp_filter] and
1190    /// [Self::mfp_plan_filter] so that the fallibility of MFP expressions
1191    /// surfaces in the result, even when the expression's result column isn't
1192    /// referenced by a predicate or temporal bound.
1193    ///
1194    /// The runtime MFP evaluator runs every expression once all the preceding
1195    /// predicates pass (see [`crate::SafeMfpPlan::evaluate_inner`]), so an
1196    /// expression that errors on the actual data will turn the whole row into
1197    /// an `Err` — even if no predicate or bound mentions that expression. The
1198    /// default `mfp_filter` / `mfp_plan_filter` only AND together the
1199    /// predicates and bounds, so the AND result misses the expression's
1200    /// `fallible` flag and persist filter pushdown can wrongly discard a part
1201    /// that actually produces error rows. See database-issues#9656.
1202    fn mfp_filter(&self, mfp: &MapFilterProject) -> Self::Summary {
1203        let mfp_eval = MfpEval::new(self, mfp.input_arity, &mfp.expressions);
1204        let predicates = mfp
1205            .predicates
1206            .iter()
1207            .map(|(_, e)| mfp_eval.expr(e))
1208            .collect();
1209        let mut result = self.variadic(&And.into(), predicates);
1210        if mfp_eval.expressions.iter().any(|s| s.range.fallible) {
1211            result.range.fallible = true;
1212        }
1213        result
1214    }
1215
1216    fn mfp_plan_filter(&self, plan: &MfpPlan) -> Self::Summary {
1217        let mfp_eval = MfpEval::new(self, plan.mfp.input_arity, &plan.mfp.expressions);
1218        let mut results: Vec<_> = plan
1219            .mfp
1220            .predicates
1221            .iter()
1222            .map(|(_, e)| mfp_eval.expr(e))
1223            .collect();
1224        let mz_now = mfp_eval.unmaterializable(&UnmaterializableFunc::MzNow);
1225        for bound in &plan.lower_bounds {
1226            let bound_range = mfp_eval.expr(bound);
1227            let result = mfp_eval.binary(&BinaryFunc::Lte(func::Lte), bound_range, mz_now.clone());
1228            results.push(result);
1229        }
1230        for bound in &plan.upper_bounds {
1231            let bound_range = mfp_eval.expr(bound);
1232            let result = mfp_eval.binary(&BinaryFunc::Gte(func::Gte), bound_range, mz_now.clone());
1233            results.push(result);
1234        }
1235        let mut result = self.variadic(&And.into(), results);
1236        if mfp_eval.expressions.iter().any(|s| s.range.fallible) {
1237            result.range.fallible = true;
1238        }
1239        result
1240    }
1241}
1242
1243/// An interpreter that returns whether or not a particular expression is "pushdownable".
1244/// Broadly speaking, an expression is pushdownable if the result of evaluating the expression
1245/// depends on the range of possible column values in a way that `ColumnSpecs` is able to reason about.
1246///
1247/// In practice, we internally need to distinguish between expressions that are trivially predicable
1248/// (because they're constant) and expressions that depend on the column ranges themselves.
1249/// See the [TraceSummary] variants for those distinctions, and [TraceSummary::pushdownable] for
1250/// the overall assessment.
1251#[derive(Debug)]
1252pub struct Trace;
1253
1254/// A summary type for the [Trace] interpreter.
1255///
1256/// The ordering of this type is meaningful: the "smaller" the summary, the more information we have
1257/// about the possible values of the expression. This means we can eg. use `max` in the
1258/// interpreter below to find the summary for a function-call expression based on the summaries
1259/// of its arguments.
1260#[derive(Copy, Clone, Debug, PartialOrd, PartialEq, Ord, Eq)]
1261pub enum TraceSummary {
1262    /// The expression is constant: we can evaluate it without any runtime information.
1263    /// This corresponds to a `ResultSpec` of a single value.
1264    Constant,
1265    /// The expression depends on runtime information, but in "predictable" way... ie. if we know
1266    /// the range of possible values for all columns and unmaterializable functions, we can
1267    /// predict the possible values of the output.
1268    /// This corresponds to a `ResultSpec` of a perhaps range of values.
1269    Dynamic,
1270    /// The expression depends on runtime information in an unpredictable way.
1271    /// This corresponds to a `ResultSpec::value_all()` or something similarly vague.
1272    Unknown,
1273}
1274
1275impl TraceSummary {
1276    /// We say that a function is "pushdownable" for a particular
1277    /// argument if `ColumnSpecs` can determine the spec of the function's output given the input spec for
1278    /// that argument. (In practice, this is true when either the function is monotone in that argument
1279    /// or it's been special-cased in the interpreter.)
1280    fn apply_fn(self, pushdownable: bool) -> Self {
1281        match self {
1282            TraceSummary::Constant => TraceSummary::Constant,
1283            TraceSummary::Dynamic => match pushdownable {
1284                true => TraceSummary::Dynamic,
1285                false => TraceSummary::Unknown,
1286            },
1287            TraceSummary::Unknown => TraceSummary::Unknown,
1288        }
1289    }
1290
1291    /// We say that an expression is "pushdownable" if it's either constant or dynamic.
1292    pub fn pushdownable(self) -> bool {
1293        match self {
1294            TraceSummary::Constant | TraceSummary::Dynamic => true,
1295            TraceSummary::Unknown => false,
1296        }
1297    }
1298}
1299
1300impl Interpreter for Trace {
1301    type Summary = TraceSummary;
1302
1303    fn column(&self, _id: usize) -> Self::Summary {
1304        TraceSummary::Dynamic
1305    }
1306
1307    fn literal(
1308        &self,
1309        _result: &Result<Row, EvalError>,
1310        _col_type: &ReprColumnType,
1311    ) -> Self::Summary {
1312        TraceSummary::Constant
1313    }
1314
1315    fn unmaterializable(&self, _func: &UnmaterializableFunc) -> Self::Summary {
1316        TraceSummary::Dynamic
1317    }
1318
1319    fn unary(&self, func: &UnaryFunc, expr: Self::Summary) -> Self::Summary {
1320        let pushdownable = match SpecialUnary::for_func(func) {
1321            None => func.is_monotone(),
1322            Some(special) => special.pushdownable,
1323        };
1324        expr.apply_fn(pushdownable)
1325    }
1326
1327    fn binary(
1328        &self,
1329        func: &BinaryFunc,
1330        left: Self::Summary,
1331        right: Self::Summary,
1332    ) -> Self::Summary {
1333        let (left_pushdownable, right_pushdownable) = match AbstractFunc::for_func(func) {
1334            None => func.is_monotone(),
1335            Some(special) => special.pushdownable,
1336        };
1337        left.apply_fn(left_pushdownable)
1338            .max(right.apply_fn(right_pushdownable))
1339    }
1340
1341    fn variadic(&self, func: &VariadicFunc, exprs: Vec<Self::Summary>) -> Self::Summary {
1342        if !func.is_associative() && exprs.len() >= ColumnSpecs::MAX_EVAL_ARGS {
1343            // We can't efficiently evaluate functions with very large argument lists;
1344            // see the comment on ColumnSpecs::MAX_EVAL_ARGS for details.
1345            return TraceSummary::Unknown;
1346        }
1347
1348        let pushdownable_fn = func.is_monotone();
1349        exprs
1350            .into_iter()
1351            .map(|pushdownable_arg| pushdownable_arg.apply_fn(pushdownable_fn))
1352            .max()
1353            .unwrap_or(TraceSummary::Constant)
1354    }
1355
1356    fn cond(&self, cond: Self::Summary, then: Self::Summary, els: Self::Summary) -> Self::Summary {
1357        // We don't actually need to be able to predict the condition precisely to predict the output,
1358        // since we can union the ranges of the two branches for a conservative estimate.
1359        let cond = cond.min(TraceSummary::Dynamic);
1360        cond.max(then).max(els)
1361    }
1362}
1363
1364#[cfg(test)]
1365mod tests {
1366    use itertools::Itertools;
1367    use mz_repr::adt::datetime::DateTimeUnits;
1368    use mz_repr::{Datum, PropDatum, RowArena, SqlScalarType};
1369    use proptest::prelude::*;
1370    use proptest::sample::{Index, select};
1371
1372    use crate::func::*;
1373    use crate::scalar::func::variadic::Concat;
1374    use crate::{BinaryFunc, MirScalarExpr, UnaryFunc};
1375
1376    use super::*;
1377
1378    #[derive(Debug)]
1379    struct ExpressionData {
1380        relation_type: ReprRelationType,
1381        specs: Vec<ResultSpec<'static>>,
1382        rows: Vec<Row>,
1383        expr: MirScalarExpr,
1384    }
1385
1386    // Currently there's no good way to check whether a particular function accepts a particular
1387    // type as argument, which means we need to list everything out explicitly here. Restrict our interest
1388    // to a reasonable number of functions, to keep things tractable
1389    // TODO: replace this with function-level info once it's available.
1390    const NUM_TYPE: ReprScalarType = ReprScalarType::Numeric;
1391    static SCALAR_TYPES: &[ReprScalarType] = &[
1392        ReprScalarType::Bool,
1393        ReprScalarType::Jsonb,
1394        NUM_TYPE,
1395        ReprScalarType::Int16,
1396        ReprScalarType::Int32,
1397        ReprScalarType::Int64,
1398        ReprScalarType::UInt16,
1399        ReprScalarType::UInt32,
1400        ReprScalarType::UInt64,
1401        ReprScalarType::Float32,
1402        ReprScalarType::Float64,
1403        ReprScalarType::Date,
1404        ReprScalarType::Time,
1405        ReprScalarType::Timestamp,
1406        ReprScalarType::TimestampTz,
1407        ReprScalarType::MzTimestamp,
1408        ReprScalarType::Interval,
1409        ReprScalarType::String,
1410    ];
1411
1412    const INTERESTING_UNARY_FUNCS: &[UnaryFunc] = {
1413        &[
1414            UnaryFunc::CastNumericToMzTimestamp(CastNumericToMzTimestamp),
1415            UnaryFunc::CastTimestampToMzTimestamp(CastTimestampToMzTimestamp),
1416            UnaryFunc::NegNumeric(NegNumeric),
1417            UnaryFunc::NegFloat64(NegFloat64),
1418            UnaryFunc::CastJsonbToNumeric(CastJsonbToNumeric(None)),
1419            UnaryFunc::CastJsonbToBool(CastJsonbToBool),
1420            UnaryFunc::CastJsonbToString(CastJsonbToString),
1421            UnaryFunc::DateTruncTimestamp(DateTruncTimestamp(DateTimeUnits::Epoch)),
1422            UnaryFunc::ExtractTimestamp(ExtractTimestamp(DateTimeUnits::Epoch)),
1423            UnaryFunc::ExtractDate(ExtractDate(DateTimeUnits::Epoch)),
1424            UnaryFunc::Not(Not),
1425            UnaryFunc::IsNull(IsNull),
1426            UnaryFunc::IsFalse(IsFalse),
1427            UnaryFunc::TryParseMonotonicIso8601Timestamp(TryParseMonotonicIso8601Timestamp),
1428            // Declared-monotone functions whose claims are otherwise
1429            // unvalidated, chosen for fallible or lossy interiors: the
1430            // equivalence proptests catch a wrong claim as a spec that fails
1431            // to contain the evaluated result.
1432            UnaryFunc::NegInt32(NegInt32),
1433            UnaryFunc::NegInt64(NegInt64),
1434            UnaryFunc::CastInt32ToUint32(CastInt32ToUint32),
1435            UnaryFunc::CastInt64ToInt32(CastInt64ToInt32),
1436            UnaryFunc::CastInt64ToNumeric(CastInt64ToNumeric(None)),
1437            UnaryFunc::CastFloat64ToInt64(CastFloat64ToInt64),
1438            UnaryFunc::CastFloat64ToFloat32(CastFloat64ToFloat32),
1439            UnaryFunc::CastFloat32ToFloat64(CastFloat32ToFloat64),
1440            UnaryFunc::CastNumericToInt64(CastNumericToInt64),
1441            UnaryFunc::CeilNumeric(CeilNumeric),
1442            UnaryFunc::FloorNumeric(FloorNumeric),
1443            UnaryFunc::CastDateToTimestamp(CastDateToTimestamp(None)),
1444            UnaryFunc::CastTimestampToTimestampTz(CastTimestampToTimestampTz {
1445                from: None,
1446                to: None,
1447            }),
1448            UnaryFunc::CastTimestampTzToTimestamp(CastTimestampTzToTimestamp {
1449                from: None,
1450                to: None,
1451            }),
1452            // Conditionally monotone (most significant unit) and its
1453            // non-monotone sibling.
1454            UnaryFunc::ExtractTimestamp(ExtractTimestamp(DateTimeUnits::Year)),
1455            UnaryFunc::ExtractTimestamp(ExtractTimestamp(DateTimeUnits::Month)),
1456            UnaryFunc::ExtractTimestampTz(ExtractTimestampTz(DateTimeUnits::Epoch)),
1457            UnaryFunc::ExtractTimestampTz(ExtractTimestampTz(DateTimeUnits::Year)),
1458            // Batch 2 of the declared-monotone sweep: the remaining cast
1459            // families, ordered-domain arithmetic helpers, and functions with
1460            // partial domains (errors on part of the range).
1461            UnaryFunc::CastBoolToInt32(CastBoolToInt32),
1462            UnaryFunc::CastBoolToString(CastBoolToString),
1463            UnaryFunc::NegInt16(NegInt16),
1464            UnaryFunc::CastInt16ToInt32(CastInt16ToInt32),
1465            UnaryFunc::CastInt16ToInt64(CastInt16ToInt64),
1466            UnaryFunc::CastInt16ToFloat32(CastInt16ToFloat32),
1467            UnaryFunc::CastInt16ToFloat64(CastInt16ToFloat64),
1468            UnaryFunc::CastInt16ToUint16(CastInt16ToUint16),
1469            UnaryFunc::CastInt16ToNumeric(CastInt16ToNumeric(None)),
1470            UnaryFunc::CastInt32ToInt16(CastInt32ToInt16),
1471            UnaryFunc::CastInt32ToInt64(CastInt32ToInt64),
1472            UnaryFunc::CastInt32ToFloat32(CastInt32ToFloat32),
1473            UnaryFunc::CastInt32ToFloat64(CastInt32ToFloat64),
1474            UnaryFunc::CastInt32ToUint16(CastInt32ToUint16),
1475            UnaryFunc::CastInt32ToNumeric(CastInt32ToNumeric(None)),
1476            UnaryFunc::CastInt32ToMzTimestamp(CastInt32ToMzTimestamp),
1477            UnaryFunc::CastInt64ToInt16(CastInt64ToInt16),
1478            UnaryFunc::CastInt64ToFloat32(CastInt64ToFloat32),
1479            UnaryFunc::CastInt64ToFloat64(CastInt64ToFloat64),
1480            UnaryFunc::CastInt64ToUint64(CastInt64ToUint64),
1481            UnaryFunc::CastInt64ToMzTimestamp(CastInt64ToMzTimestamp),
1482            UnaryFunc::CastUint64ToUint32(CastUint64ToUint32),
1483            UnaryFunc::CastUint64ToInt32(CastUint64ToInt32),
1484            UnaryFunc::CastUint64ToNumeric(CastUint64ToNumeric(None)),
1485            UnaryFunc::CastUint64ToMzTimestamp(CastUint64ToMzTimestamp),
1486            UnaryFunc::NegFloat32(NegFloat32),
1487            UnaryFunc::FloorFloat32(FloorFloat32),
1488            UnaryFunc::CastFloat32ToInt32(CastFloat32ToInt32),
1489            UnaryFunc::CastFloat32ToNumeric(CastFloat32ToNumeric(None)),
1490            UnaryFunc::FloorFloat64(FloorFloat64),
1491            UnaryFunc::CastFloat64ToInt32(CastFloat64ToInt32),
1492            UnaryFunc::CastFloat64ToUint64(CastFloat64ToUint64),
1493            UnaryFunc::CastFloat64ToNumeric(CastFloat64ToNumeric(None)),
1494            UnaryFunc::RoundNumeric(RoundNumeric),
1495            UnaryFunc::TruncNumeric(TruncNumeric),
1496            UnaryFunc::Log10Numeric(Log10Numeric),
1497            UnaryFunc::CastNumericToFloat64(CastNumericToFloat64),
1498            UnaryFunc::CastNumericToInt32(CastNumericToInt32),
1499            UnaryFunc::CastTimestampToDate(CastTimestampToDate),
1500            UnaryFunc::CastDateToMzTimestamp(CastDateToMzTimestamp),
1501            UnaryFunc::StepMzTimestamp(StepMzTimestamp),
1502            // Batch 3: every remaining declared-monotone cast family, the
1503            // anti-monotone bitwise complements, and the conditional
1504            // most-significant-unit extracts for date and timestamptz.
1505            UnaryFunc::CastBoolToStringNonstandard(CastBoolToStringNonstandard),
1506            UnaryFunc::CastBoolToInt64(CastBoolToInt64),
1507            UnaryFunc::CastInt16ToUint32(CastInt16ToUint32),
1508            UnaryFunc::CastInt16ToUint64(CastInt16ToUint64),
1509            UnaryFunc::CastInt32ToUint64(CastInt32ToUint64),
1510            UnaryFunc::CastInt64ToUint16(CastInt64ToUint16),
1511            UnaryFunc::CastInt64ToUint32(CastInt64ToUint32),
1512            UnaryFunc::CastUint16ToUint32(CastUint16ToUint32),
1513            UnaryFunc::CastUint16ToUint64(CastUint16ToUint64),
1514            UnaryFunc::CastUint16ToInt16(CastUint16ToInt16),
1515            UnaryFunc::CastUint16ToInt32(CastUint16ToInt32),
1516            UnaryFunc::CastUint16ToFloat32(CastUint16ToFloat32),
1517            UnaryFunc::CastUint16ToFloat64(CastUint16ToFloat64),
1518            UnaryFunc::CastUint16ToNumeric(CastUint16ToNumeric(None)),
1519            UnaryFunc::CastUint16ToInt64(CastUint16ToInt64),
1520            UnaryFunc::BitNotUint16(BitNotUint16),
1521            UnaryFunc::CastUint32ToUint16(CastUint32ToUint16),
1522            UnaryFunc::CastUint32ToUint64(CastUint32ToUint64),
1523            UnaryFunc::CastUint32ToInt32(CastUint32ToInt32),
1524            UnaryFunc::CastUint32ToInt64(CastUint32ToInt64),
1525            UnaryFunc::CastUint32ToFloat32(CastUint32ToFloat32),
1526            UnaryFunc::CastUint32ToFloat64(CastUint32ToFloat64),
1527            UnaryFunc::CastUint32ToNumeric(CastUint32ToNumeric(None)),
1528            UnaryFunc::CastUint32ToInt16(CastUint32ToInt16),
1529            UnaryFunc::CastUint32ToMzTimestamp(CastUint32ToMzTimestamp),
1530            UnaryFunc::BitNotUint32(BitNotUint32),
1531            UnaryFunc::CastUint64ToUint16(CastUint64ToUint16),
1532            UnaryFunc::CastUint64ToInt16(CastUint64ToInt16),
1533            UnaryFunc::CastUint64ToInt64(CastUint64ToInt64),
1534            UnaryFunc::CastUint64ToFloat32(CastUint64ToFloat32),
1535            UnaryFunc::CastUint64ToFloat64(CastUint64ToFloat64),
1536            UnaryFunc::BitNotUint64(BitNotUint64),
1537            UnaryFunc::CastFloat32ToInt16(CastFloat32ToInt16),
1538            UnaryFunc::CastFloat32ToInt64(CastFloat32ToInt64),
1539            UnaryFunc::CastFloat32ToUint16(CastFloat32ToUint16),
1540            UnaryFunc::CastFloat32ToUint32(CastFloat32ToUint32),
1541            UnaryFunc::CastFloat32ToUint64(CastFloat32ToUint64),
1542            UnaryFunc::CastFloat64ToInt16(CastFloat64ToInt16),
1543            UnaryFunc::CastFloat64ToUint16(CastFloat64ToUint16),
1544            UnaryFunc::CastFloat64ToUint32(CastFloat64ToUint32),
1545            UnaryFunc::CastJsonbToInt16(CastJsonbToInt16),
1546            UnaryFunc::CastJsonbToInt32(CastJsonbToInt32),
1547            UnaryFunc::CastJsonbToInt64(CastJsonbToInt64),
1548            UnaryFunc::CastJsonbToFloat32(CastJsonbToFloat32),
1549            UnaryFunc::CastJsonbToFloat64(CastJsonbToFloat64),
1550            UnaryFunc::CastNumericToInt16(CastNumericToInt16),
1551            UnaryFunc::CastNumericToFloat32(CastNumericToFloat32),
1552            UnaryFunc::CastNumericToUint16(CastNumericToUint16),
1553            UnaryFunc::CastNumericToUint32(CastNumericToUint32),
1554            UnaryFunc::CastNumericToUint64(CastNumericToUint64),
1555            UnaryFunc::CastTimestampTzToDate(CastTimestampTzToDate),
1556            UnaryFunc::CastTimestampTzToMzTimestamp(CastTimestampTzToMzTimestamp),
1557            UnaryFunc::DateTruncTimestampTz(DateTruncTimestampTz(DateTimeUnits::Epoch)),
1558            UnaryFunc::CastDateToTimestampTz(CastDateToTimestampTz(None)),
1559            UnaryFunc::ExtractDate(ExtractDate(DateTimeUnits::Year)),
1560            UnaryFunc::ExtractDate(ExtractDate(DateTimeUnits::Day)),
1561        ]
1562    };
1563
1564    fn unary_typecheck(func: &UnaryFunc, arg: &ReprColumnType) -> bool {
1565        use UnaryFunc::*;
1566        match func {
1567            CastNumericToMzTimestamp(_)
1568            | NegNumeric(_)
1569            | CastNumericToInt64(_)
1570            | CeilNumeric(_)
1571            | FloorNumeric(_)
1572            | RoundNumeric(_)
1573            | TruncNumeric(_)
1574            | Log10Numeric(_)
1575            | CastNumericToFloat64(_)
1576            | CastNumericToInt32(_)
1577            | CastNumericToInt16(_)
1578            | CastNumericToFloat32(_)
1579            | CastNumericToUint16(_)
1580            | CastNumericToUint32(_)
1581            | CastNumericToUint64(_) => arg.scalar_type == NUM_TYPE,
1582            NegFloat64(_)
1583            | CastFloat64ToInt64(_)
1584            | CastFloat64ToFloat32(_)
1585            | FloorFloat64(_)
1586            | CastFloat64ToInt32(_)
1587            | CastFloat64ToUint64(_)
1588            | CastFloat64ToNumeric(_)
1589            | CastFloat64ToInt16(_)
1590            | CastFloat64ToUint16(_)
1591            | CastFloat64ToUint32(_) => arg.scalar_type == ReprScalarType::Float64,
1592            CastFloat32ToFloat64(_)
1593            | NegFloat32(_)
1594            | FloorFloat32(_)
1595            | CastFloat32ToInt32(_)
1596            | CastFloat32ToNumeric(_)
1597            | CastFloat32ToInt16(_)
1598            | CastFloat32ToInt64(_)
1599            | CastFloat32ToUint16(_)
1600            | CastFloat32ToUint32(_)
1601            | CastFloat32ToUint64(_) => arg.scalar_type == ReprScalarType::Float32,
1602            NegInt16(_)
1603            | CastInt16ToInt32(_)
1604            | CastInt16ToInt64(_)
1605            | CastInt16ToFloat32(_)
1606            | CastInt16ToFloat64(_)
1607            | CastInt16ToUint16(_)
1608            | CastInt16ToNumeric(_)
1609            | CastInt16ToUint32(_)
1610            | CastInt16ToUint64(_) => arg.scalar_type == ReprScalarType::Int16,
1611            NegInt32(_)
1612            | CastInt32ToUint32(_)
1613            | CastInt32ToInt16(_)
1614            | CastInt32ToInt64(_)
1615            | CastInt32ToFloat32(_)
1616            | CastInt32ToFloat64(_)
1617            | CastInt32ToUint16(_)
1618            | CastInt32ToNumeric(_)
1619            | CastInt32ToMzTimestamp(_)
1620            | CastInt32ToUint64(_) => arg.scalar_type == ReprScalarType::Int32,
1621            NegInt64(_)
1622            | CastInt64ToInt32(_)
1623            | CastInt64ToNumeric(_)
1624            | CastInt64ToInt16(_)
1625            | CastInt64ToFloat32(_)
1626            | CastInt64ToFloat64(_)
1627            | CastInt64ToUint64(_)
1628            | CastInt64ToMzTimestamp(_)
1629            | CastInt64ToUint16(_)
1630            | CastInt64ToUint32(_) => arg.scalar_type == ReprScalarType::Int64,
1631            CastUint16ToUint32(_)
1632            | CastUint16ToUint64(_)
1633            | CastUint16ToInt16(_)
1634            | CastUint16ToInt32(_)
1635            | CastUint16ToFloat32(_)
1636            | CastUint16ToFloat64(_)
1637            | CastUint16ToNumeric(_)
1638            | CastUint16ToInt64(_)
1639            | BitNotUint16(_) => arg.scalar_type == ReprScalarType::UInt16,
1640            CastUint32ToUint16(_)
1641            | CastUint32ToUint64(_)
1642            | CastUint32ToInt32(_)
1643            | CastUint32ToInt64(_)
1644            | CastUint32ToFloat32(_)
1645            | CastUint32ToFloat64(_)
1646            | CastUint32ToNumeric(_)
1647            | CastUint32ToInt16(_)
1648            | CastUint32ToMzTimestamp(_)
1649            | BitNotUint32(_) => arg.scalar_type == ReprScalarType::UInt32,
1650            CastUint64ToUint32(_)
1651            | CastUint64ToInt32(_)
1652            | CastUint64ToNumeric(_)
1653            | CastUint64ToMzTimestamp(_)
1654            | CastUint64ToUint16(_)
1655            | CastUint64ToInt16(_)
1656            | CastUint64ToInt64(_)
1657            | CastUint64ToFloat32(_)
1658            | CastUint64ToFloat64(_)
1659            | BitNotUint64(_) => arg.scalar_type == ReprScalarType::UInt64,
1660            StepMzTimestamp(_) => arg.scalar_type == ReprScalarType::MzTimestamp,
1661            CastBoolToInt32(_)
1662            | CastBoolToString(_)
1663            | CastBoolToStringNonstandard(_)
1664            | CastBoolToInt64(_) => arg.scalar_type == ReprScalarType::Bool,
1665            CastTimestampToMzTimestamp(_)
1666            | CastTimestampToTimestampTz(_)
1667            | CastTimestampToDate(_) => arg.scalar_type == ReprScalarType::Timestamp,
1668            CastTimestampTzToTimestamp(_)
1669            | ExtractTimestampTz(_)
1670            | CastTimestampTzToDate(_)
1671            | CastTimestampTzToMzTimestamp(_)
1672            | DateTruncTimestampTz(_) => arg.scalar_type == ReprScalarType::TimestampTz,
1673            CastJsonbToNumeric(_)
1674            | CastJsonbToBool(_)
1675            | CastJsonbToString(_)
1676            | CastJsonbToInt16(_)
1677            | CastJsonbToInt32(_)
1678            | CastJsonbToInt64(_)
1679            | CastJsonbToFloat32(_)
1680            | CastJsonbToFloat64(_) => arg.scalar_type == ReprScalarType::Jsonb,
1681            ExtractTimestamp(_) | DateTruncTimestamp(_) => {
1682                arg.scalar_type == ReprScalarType::Timestamp
1683            }
1684            ExtractDate(_)
1685            | CastDateToTimestamp(_)
1686            | CastDateToMzTimestamp(_)
1687            | CastDateToTimestampTz(_) => arg.scalar_type == ReprScalarType::Date,
1688            Not(_) => arg.scalar_type == ReprScalarType::Bool,
1689            IsNull(_) => true,
1690            TryParseMonotonicIso8601Timestamp(_) => arg.scalar_type == ReprScalarType::String,
1691            _ => false,
1692        }
1693    }
1694
1695    fn interesting_binary_funcs() -> Vec<BinaryFunc> {
1696        vec![
1697            AddTimestampInterval.into(),
1698            AddNumeric.into(),
1699            SubNumeric.into(),
1700            MulNumeric.into(),
1701            DivNumeric.into(),
1702            AddFloat64.into(),
1703            SubFloat64.into(),
1704            MulFloat64.into(),
1705            DivFloat64.into(),
1706            MulFloat32.into(),
1707            DivFloat32.into(),
1708            RoundNumericBinary.into(),
1709            Eq.into(),
1710            Lt.into(),
1711            Gt.into(),
1712            Lte.into(),
1713            Gte.into(),
1714            DateTruncUnitsTimestamp.into(),
1715            JsonbGetString.into(),
1716            JsonbGetStringStringify.into(),
1717            // Declared-monotone integer arithmetic: overflow and
1718            // division-by-zero are interior error conditions the endpoints
1719            // need not reveal.
1720            AddInt32.into(),
1721            SubInt32.into(),
1722            MulInt32.into(),
1723            DivInt32.into(),
1724            AddInt64.into(),
1725            MulInt64.into(),
1726            AddFloat32.into(),
1727            SubFloat32.into(),
1728            // Monotone in the right argument only.
1729            TextConcatBinary.into(),
1730            // Monotone left, and a declared non-monotone control.
1731            AddDateInterval.into(),
1732            AddTimeInterval.into(),
1733            // Batch 2: remaining ordered-domain arithmetic.
1734            SubInt64.into(),
1735            DivInt64.into(),
1736            SubTimestamp.into(),
1737            SubDate.into(),
1738            AddInterval.into(),
1739            SubInterval.into(),
1740            // Batch 3: the int16 and unsigned arithmetic families, remaining
1741            // date/time arithmetic, and binary date_bin.
1742            AddInt16.into(),
1743            SubInt16.into(),
1744            MulInt16.into(),
1745            DivInt16.into(),
1746            AddUint16.into(),
1747            SubUint16.into(),
1748            MulUint16.into(),
1749            DivUint16.into(),
1750            AddUint32.into(),
1751            SubUint32.into(),
1752            MulUint32.into(),
1753            DivUint32.into(),
1754            AddUint64.into(),
1755            SubUint64.into(),
1756            MulUint64.into(),
1757            DivUint64.into(),
1758            SubTime.into(),
1759            SubTimestampTz.into(),
1760            AddDateTime.into(),
1761            SubDateInterval.into(),
1762            DateBinTimestamp.into(),
1763        ]
1764    }
1765
1766    fn binary_typecheck(func: &BinaryFunc, arg0: &ReprColumnType, arg1: &ReprColumnType) -> bool {
1767        use BinaryFunc::*;
1768        match func {
1769            AddTimestampInterval(_) => {
1770                arg0.scalar_type == ReprScalarType::Timestamp
1771                    && arg1.scalar_type == ReprScalarType::Interval
1772            }
1773            AddNumeric(_) | SubNumeric(_) | MulNumeric(_) | DivNumeric(_) => {
1774                arg0.scalar_type == NUM_TYPE && arg1.scalar_type == NUM_TYPE
1775            }
1776            AddFloat64(_) | SubFloat64(_) | MulFloat64(_) | DivFloat64(_) => {
1777                arg0.scalar_type == ReprScalarType::Float64
1778                    && arg1.scalar_type == ReprScalarType::Float64
1779            }
1780            MulFloat32(_) | DivFloat32(_) => {
1781                arg0.scalar_type == ReprScalarType::Float32
1782                    && arg1.scalar_type == ReprScalarType::Float32
1783            }
1784            RoundNumeric(_) => {
1785                arg0.scalar_type == NUM_TYPE && arg1.scalar_type == ReprScalarType::Int32
1786            }
1787            Eq(_) | Lt(_) | Gt(_) | Lte(_) | Gte(_) => arg0.scalar_type == arg1.scalar_type,
1788            DateTruncTimestamp(_) => {
1789                arg0.scalar_type == ReprScalarType::String
1790                    && arg1.scalar_type == ReprScalarType::Timestamp
1791            }
1792            JsonbGetString(_) | JsonbGetStringStringify(_) => {
1793                arg0.scalar_type == ReprScalarType::Jsonb
1794                    && arg1.scalar_type == ReprScalarType::String
1795            }
1796            AddInt32(_) | SubInt32(_) | MulInt32(_) | DivInt32(_) => {
1797                arg0.scalar_type == ReprScalarType::Int32
1798                    && arg1.scalar_type == ReprScalarType::Int32
1799            }
1800            AddInt64(_) | MulInt64(_) | SubInt64(_) | DivInt64(_) => {
1801                arg0.scalar_type == ReprScalarType::Int64
1802                    && arg1.scalar_type == ReprScalarType::Int64
1803            }
1804            SubTimestamp(_) => {
1805                arg0.scalar_type == ReprScalarType::Timestamp
1806                    && arg1.scalar_type == ReprScalarType::Timestamp
1807            }
1808            SubDate(_) => {
1809                arg0.scalar_type == ReprScalarType::Date && arg1.scalar_type == ReprScalarType::Date
1810            }
1811            AddInterval(_) | SubInterval(_) => {
1812                arg0.scalar_type == ReprScalarType::Interval
1813                    && arg1.scalar_type == ReprScalarType::Interval
1814            }
1815            AddInt16(_) | SubInt16(_) | MulInt16(_) | DivInt16(_) => {
1816                arg0.scalar_type == ReprScalarType::Int16
1817                    && arg1.scalar_type == ReprScalarType::Int16
1818            }
1819            AddUint16(_) | SubUint16(_) | MulUint16(_) | DivUint16(_) => {
1820                arg0.scalar_type == ReprScalarType::UInt16
1821                    && arg1.scalar_type == ReprScalarType::UInt16
1822            }
1823            AddUint32(_) | SubUint32(_) | MulUint32(_) | DivUint32(_) => {
1824                arg0.scalar_type == ReprScalarType::UInt32
1825                    && arg1.scalar_type == ReprScalarType::UInt32
1826            }
1827            AddUint64(_) | SubUint64(_) | MulUint64(_) | DivUint64(_) => {
1828                arg0.scalar_type == ReprScalarType::UInt64
1829                    && arg1.scalar_type == ReprScalarType::UInt64
1830            }
1831            SubTime(_) => {
1832                arg0.scalar_type == ReprScalarType::Time && arg1.scalar_type == ReprScalarType::Time
1833            }
1834            SubTimestampTz(_) => {
1835                arg0.scalar_type == ReprScalarType::TimestampTz
1836                    && arg1.scalar_type == ReprScalarType::TimestampTz
1837            }
1838            AddDateTime(_) => {
1839                arg0.scalar_type == ReprScalarType::Date && arg1.scalar_type == ReprScalarType::Time
1840            }
1841            SubDateInterval(_) => {
1842                arg0.scalar_type == ReprScalarType::Date
1843                    && arg1.scalar_type == ReprScalarType::Interval
1844            }
1845            DateBinTimestamp(_) => {
1846                arg0.scalar_type == ReprScalarType::Interval
1847                    && arg1.scalar_type == ReprScalarType::Timestamp
1848            }
1849            AddFloat32(_) | SubFloat32(_) => {
1850                arg0.scalar_type == ReprScalarType::Float32
1851                    && arg1.scalar_type == ReprScalarType::Float32
1852            }
1853            TextConcat(_) => {
1854                arg0.scalar_type == ReprScalarType::String
1855                    && arg1.scalar_type == ReprScalarType::String
1856            }
1857            AddDateInterval(_) => {
1858                arg0.scalar_type == ReprScalarType::Date
1859                    && arg1.scalar_type == ReprScalarType::Interval
1860            }
1861            AddTimeInterval(_) => {
1862                arg0.scalar_type == ReprScalarType::Time
1863                    && arg1.scalar_type == ReprScalarType::Interval
1864            }
1865            _ => false,
1866        }
1867    }
1868
1869    const INTERESTING_VARIADIC_FUNCS: &[VariadicFunc] = {
1870        use crate::scalar::func::variadic as v;
1871        use VariadicFunc::*;
1872        &[
1873            Coalesce(v::Coalesce),
1874            Greatest(v::Greatest),
1875            Least(v::Least),
1876            And(v::And),
1877            Or(v::Or),
1878            Concat(v::Concat),
1879            ConcatWs(v::ConcatWs),
1880        ]
1881    };
1882
1883    fn variadic_typecheck(func: &VariadicFunc, args: &[ReprColumnType]) -> bool {
1884        use VariadicFunc::*;
1885        fn all_eq<'a>(
1886            iter: impl IntoIterator<Item = &'a ReprColumnType>,
1887            other: &ReprScalarType,
1888        ) -> bool {
1889            iter.into_iter().all(|t| t.scalar_type == *other)
1890        }
1891        match func {
1892            Coalesce(_) | Greatest(_) | Least(_) => match args {
1893                [] => true,
1894                [first, rest @ ..] => all_eq(rest, &first.scalar_type),
1895            },
1896            And(_) | Or(_) => all_eq(args, &ReprScalarType::Bool),
1897            Concat(_) => all_eq(args, &ReprScalarType::String),
1898            ConcatWs(_) => args.len() > 1 && all_eq(args, &ReprScalarType::String),
1899            _ => false,
1900        }
1901    }
1902
1903    fn gen_datums_for_type(typ: &ReprColumnType) -> BoxedStrategy<Datum<'static>> {
1904        let mut values: Vec<Datum<'static>> = SqlScalarType::from_repr(&typ.scalar_type)
1905            .interesting_datums()
1906            .collect();
1907        if typ.nullable {
1908            values.push(Datum::Null)
1909        }
1910        select(values).boxed()
1911    }
1912
1913    fn gen_column() -> impl Strategy<Value = (ReprColumnType, Datum<'static>, ResultSpec<'static>)>
1914    {
1915        let col_type = (select(SCALAR_TYPES), any::<bool>())
1916            .prop_map(|(t, b)| t.nullable(b))
1917            .prop_filter("need at least one value", |c| {
1918                SqlScalarType::from_repr(&c.scalar_type)
1919                    .interesting_datums()
1920                    .count()
1921                    > 0
1922            });
1923
1924        let result_spec = select(vec![
1925            ResultSpec::nothing(),
1926            ResultSpec::null(),
1927            ResultSpec::anything(),
1928            ResultSpec::value_all(),
1929        ]);
1930
1931        (col_type, result_spec).prop_flat_map(|(col, result_spec)| {
1932            gen_datums_for_type(&col).prop_map(move |datum| {
1933                let result_spec = result_spec.clone().union(ResultSpec::value(datum));
1934                (col.clone(), datum, result_spec)
1935            })
1936        })
1937    }
1938
1939    fn gen_expr_for_relation(
1940        relation: &ReprRelationType,
1941    ) -> BoxedStrategy<(MirScalarExpr, ReprColumnType)> {
1942        let column_gen = {
1943            let column_types = relation.column_types.clone();
1944            any::<Index>()
1945                .prop_map(move |idx| {
1946                    let id = idx.index(column_types.len());
1947                    (MirScalarExpr::column(id), column_types[id].clone())
1948                })
1949                .boxed()
1950        };
1951
1952        let literal_gen = (select(SCALAR_TYPES), any::<bool>())
1953            .prop_map(|(s, b)| s.nullable(b))
1954            .prop_flat_map(|ct| {
1955                let error_gen = any::<EvalError>().prop_map(Err).boxed();
1956                let value_gen = gen_datums_for_type(&ct)
1957                    .prop_map(move |datum| Ok(Row::pack_slice(&[datum])))
1958                    .boxed();
1959                error_gen.prop_union(value_gen).prop_map(move |result| {
1960                    (MirScalarExpr::Literal(result, ct.clone()), ct.clone())
1961                })
1962            })
1963            .boxed();
1964
1965        column_gen
1966            .prop_union(literal_gen)
1967            .prop_recursive(4, 64, 8, |self_gen| {
1968                let unary_gen = (select(INTERESTING_UNARY_FUNCS), self_gen.clone())
1969                    .prop_filter_map("unary func", |(func, (expr_in, type_in))| {
1970                        if !unary_typecheck(&func, &type_in) {
1971                            return None;
1972                        }
1973                        let type_out = func.output_type(type_in);
1974                        let expr_out = MirScalarExpr::CallUnary {
1975                            func,
1976                            expr: Box::new(expr_in),
1977                        };
1978                        Some((expr_out, type_out))
1979                    })
1980                    .boxed();
1981                let binary_gen = (
1982                    select(interesting_binary_funcs()),
1983                    self_gen.clone(),
1984                    self_gen.clone(),
1985                )
1986                    .prop_filter_map(
1987                        "binary func",
1988                        |(func, (expr_left, type_left), (expr_right, type_right))| {
1989                            if !binary_typecheck(&func, &type_left, &type_right) {
1990                                return None;
1991                            }
1992                            let type_out = func.output_type(&[type_left, type_right]);
1993                            let expr_out = MirScalarExpr::CallBinary {
1994                                func,
1995                                expr1: Box::new(expr_left),
1996                                expr2: Box::new(expr_right),
1997                            };
1998                            Some((expr_out, type_out))
1999                        },
2000                    )
2001                    .boxed();
2002                let variadic_gen = (
2003                    select(INTERESTING_VARIADIC_FUNCS),
2004                    prop::collection::vec(self_gen.clone(), 1..4),
2005                )
2006                    .prop_filter_map("variadic func", |(func, exprs)| {
2007                        let (exprs_in, type_in): (_, Vec<_>) = exprs.into_iter().unzip();
2008                        if !variadic_typecheck(&func, &type_in) {
2009                            return None;
2010                        }
2011                        let type_out = func.output_type(type_in);
2012                        let expr_out = MirScalarExpr::CallVariadic {
2013                            func,
2014                            exprs: exprs_in,
2015                        };
2016                        Some((expr_out, type_out))
2017                    })
2018                    .boxed();
2019                // Generate `If` nodes without the heavy rejection that filtering
2020                // three independent subexprs for a bool condition and matching
2021                // branch types would incur. The condition is a boolean literal
2022                // (so it can be `True`, `False`, or `Null` — exercising `cond`'s
2023                // value channel, including the null-takes-`els` path), and `els`
2024                // is a literal of `then`'s type so the branches always unify.
2025                let if_gen = {
2026                    let bool_type = ReprScalarType::Bool.nullable(true);
2027                    let cond_gen = gen_datums_for_type(&bool_type).prop_map(move |datum| {
2028                        MirScalarExpr::Literal(Ok(Row::pack_slice(&[datum])), bool_type.clone())
2029                    });
2030                    (cond_gen, self_gen.clone())
2031                        .prop_flat_map(|(cond_expr, (then_expr, then_type))| {
2032                            let out_type = then_type.clone();
2033                            gen_datums_for_type(&then_type).prop_map(move |datum| {
2034                                let els_expr = MirScalarExpr::Literal(
2035                                    Ok(Row::pack_slice(&[datum])),
2036                                    out_type.clone(),
2037                                );
2038                                let expr_out = MirScalarExpr::If {
2039                                    cond: Box::new(cond_expr.clone()),
2040                                    then: Box::new(then_expr.clone()),
2041                                    els: Box::new(els_expr),
2042                                };
2043                                (expr_out, out_type.clone())
2044                            })
2045                        })
2046                        .boxed()
2047                };
2048
2049                unary_gen
2050                    .prop_union(binary_gen)
2051                    .boxed()
2052                    .prop_union(variadic_gen)
2053                    .boxed()
2054                    .prop_union(if_gen)
2055            })
2056            .boxed()
2057    }
2058
2059    fn gen_expr_data() -> impl Strategy<Value = ExpressionData> {
2060        let columns = prop::collection::vec(gen_column(), 1..10);
2061        columns.prop_flat_map(|data| {
2062            let (columns, datums, specs): (Vec<_>, Vec<_>, Vec<_>) = data.into_iter().multiunzip();
2063            let relation = ReprRelationType::new(columns);
2064            let row = Row::pack_slice(&datums);
2065            gen_expr_for_relation(&relation).prop_map(move |(expr, _)| ExpressionData {
2066                relation_type: relation.clone(),
2067                specs: specs.clone(),
2068                rows: vec![row.clone()],
2069                expr,
2070            })
2071        })
2072    }
2073
2074    #[mz_ore::test]
2075    #[cfg_attr(miri, ignore)] // error: unsupported operation: can't call foreign function `decContextDefault` on OS `linux`
2076    fn test_trivial_spec_matches() {
2077        fn check(datum: PropDatum) -> Result<(), TestCaseError> {
2078            let datum: Datum = (&datum).into();
2079            let spec = if datum.is_null() {
2080                ResultSpec::null()
2081            } else {
2082                ResultSpec::value(datum)
2083            };
2084            assert!(spec.may_contain(datum));
2085            Ok(())
2086        }
2087
2088        proptest!(|(datum in mz_repr::arb_datum(true))| {
2089            check(datum)?;
2090        });
2091
2092        assert!(ResultSpec::fails().may_fail());
2093    }
2094
2095    #[mz_ore::test]
2096    #[cfg_attr(miri, ignore)] // error: unsupported operation: can't call foreign function `decContextDefault` on OS `linux`
2097    fn test_equivalence() {
2098        fn check(data: ExpressionData) -> Result<(), TestCaseError> {
2099            let ExpressionData {
2100                relation_type,
2101                specs,
2102                rows,
2103                expr,
2104            } = data;
2105
2106            // We want to ensure that the spec we get when evaluating an expression using
2107            // `ColumnSpecs` always contains the _actual_ value of that column when evaluated with
2108            // eval. (This is an important correctness property of abstract interpretation.)
2109            let arena = RowArena::new();
2110            let mut interpreter = ColumnSpecs::new(&relation_type, &arena);
2111            for (id, spec) in specs.into_iter().enumerate() {
2112                interpreter.push_column(id, spec);
2113            }
2114
2115            let spec = interpreter.expr(&expr);
2116
2117            for row in &rows {
2118                let datums: Vec<_> = row.iter().collect();
2119                let eval_result = expr.eval(&datums, &arena);
2120                match eval_result {
2121                    Ok(value) => {
2122                        assert!(spec.range.may_contain(value))
2123                    }
2124                    Err(_) => {
2125                        assert!(spec.range.may_fail());
2126                    }
2127                }
2128            }
2129
2130            Ok(())
2131        }
2132
2133        proptest!(|(data in gen_expr_data())| {
2134            check(data)?;
2135        });
2136    }
2137
2138    /// A column whose spec is a genuine `value_between(lo, hi)` range with
2139    /// `lo < hi`, paired with a concrete row value `mid` drawn from strictly
2140    /// inside `[lo, hi]`. This is the shape persist filter pushdown actually
2141    /// feeds the interpreter: min/max stats become a `Values::Within` range,
2142    /// and the interpreter narrows it through each function (relying on
2143    /// monotonicity) without ever seeing the interior values. `gen_column`
2144    /// only ever produces single-value or `anything` specs, so it never
2145    /// exercises the range-narrowing path.
2146    fn gen_range_column()
2147    -> impl Strategy<Value = (ReprColumnType, Datum<'static>, ResultSpec<'static>)> {
2148        select(SCALAR_TYPES)
2149            .prop_map(|t| t.nullable(false))
2150            .prop_filter("need at least two distinct values for a range", |c| {
2151                let mut datums: Vec<Datum> = SqlScalarType::from_repr(&c.scalar_type)
2152                    .interesting_datums()
2153                    .filter(|d| !d.is_null())
2154                    .collect();
2155                datums.sort();
2156                datums.dedup();
2157                datums.len() >= 2
2158            })
2159            .prop_flat_map(|col| {
2160                let mut datums: Vec<Datum<'static>> = SqlScalarType::from_repr(&col.scalar_type)
2161                    .interesting_datums()
2162                    .filter(|d| !d.is_null())
2163                    .collect();
2164                datums.sort();
2165                datums.dedup();
2166                (
2167                    Just(col),
2168                    Just(datums),
2169                    any::<Index>(),
2170                    any::<Index>(),
2171                    any::<Index>(),
2172                )
2173                    .prop_map(|(col, datums, a, b, c)| {
2174                        let n = datums.len();
2175                        let mut idxs = [a.index(n), b.index(n), c.index(n)];
2176                        idxs.sort();
2177                        let lo = datums[idxs[0]];
2178                        let mid = datums[idxs[1]];
2179                        let hi = datums[idxs[2]];
2180                        let spec = ResultSpec::value_between(lo, hi);
2181                        (col, mid, spec)
2182                    })
2183            })
2184    }
2185
2186    fn gen_range_expr_data() -> impl Strategy<Value = ExpressionData> {
2187        let columns = prop::collection::vec(gen_range_column(), 1..10);
2188        columns.prop_flat_map(|data| {
2189            let (columns, datums, specs): (Vec<_>, Vec<_>, Vec<_>) = data.into_iter().multiunzip();
2190            let relation = ReprRelationType::new(columns);
2191            let row = Row::pack_slice(&datums);
2192            gen_expr_for_relation(&relation).prop_map(move |(expr, _)| ExpressionData {
2193                relation_type: relation.clone(),
2194                specs: specs.clone(),
2195                rows: vec![row.clone()],
2196                expr,
2197            })
2198        })
2199    }
2200
2201    /// Regression test for database-issues#9656 (PER-50).
2202    ///
2203    /// Like [`test_equivalence`], but the column specs are genuine
2204    /// `value_between(lo, hi)` ranges rather than single values. This is the
2205    /// input shape persist filter pushdown produces from min/max stats, and it
2206    /// is the one that drives the interpreter's monotonicity-based range
2207    /// narrowing. If a function narrows a range to a spec that does not contain
2208    /// the value the concrete evaluator produces for an interior input, the
2209    /// interpreter can wrongly rule out a matching row and pushdown discards a
2210    /// part it should have kept, triggering the audit panic.
2211    #[mz_ore::test]
2212    #[cfg_attr(miri, ignore)] // error: unsupported operation: can't call foreign function `decContextDefault` on OS `linux`
2213    fn test_equivalence_ranges() {
2214        fn check(data: ExpressionData) -> Result<(), TestCaseError> {
2215            let ExpressionData {
2216                relation_type,
2217                specs,
2218                rows,
2219                expr,
2220            } = data;
2221
2222            let arena = RowArena::new();
2223            let mut interpreter = ColumnSpecs::new(&relation_type, &arena);
2224            for (id, spec) in specs.into_iter().enumerate() {
2225                interpreter.push_column(id, spec);
2226            }
2227
2228            let spec = interpreter.expr(&expr);
2229
2230            for row in &rows {
2231                let datums: Vec<_> = row.iter().collect();
2232                let eval_result = expr.eval(&datums, &arena);
2233                match eval_result {
2234                    Ok(value) => {
2235                        prop_assert!(
2236                            spec.range.may_contain(value),
2237                            "interpreter ruled out a value the evaluator produced \
2238                             for an interior input: expr={expr:?} row={row:?} \
2239                             value={value:?} spec={:?}",
2240                            spec.range,
2241                        );
2242                    }
2243                    Err(_) => {
2244                        prop_assert!(
2245                            spec.range.may_fail(),
2246                            "interpreter ruled out an error the evaluator produced \
2247                             for an interior input: expr={expr:?} row={row:?}",
2248                        );
2249                    }
2250                }
2251            }
2252
2253            Ok(())
2254        }
2255
2256        // The expression generator rejects many function/type combinations
2257        // (see the `prop_filter_map`s in `gen_expr_for_relation`), so the
2258        // per-run local-reject budget has to be raised well above proptest's
2259        // default to let enough cases through.
2260        // An explicit PROPTEST_CASES (already parsed into the default config)
2261        // wins, for long local or nightly runs. The generator rejects at a
2262        // roughly fixed rate per case, so the reject budget scales with the
2263        // case count.
2264        let default = ProptestConfig::default();
2265        let cases = if std::env::var_os("PROPTEST_CASES").is_some() {
2266            default.cases
2267        } else {
2268            2048
2269        };
2270        let config = ProptestConfig {
2271            cases,
2272            max_local_rejects: cases.saturating_mul(512),
2273            ..default
2274        };
2275        proptest!(config, |(data in gen_range_expr_data())| {
2276            check(data)?;
2277        });
2278    }
2279
2280    /// The abstract-domain lattice laws `ColumnSpecs` relies on: `union` must
2281    /// over-approximate (contain everything either operand contains), and
2282    /// `intersect` must contain every value BOTH operands contain. An
2283    /// intersect-law violation is a false negative — a value the interpreter
2284    /// silently drops. The interpreter forms straddling `Within` ranges
2285    /// internally (e.g. `Values::union` of differently-typed endpoints in `cond`
2286    /// branches or `eq`), so this is not purely hypothetical. See
2287    /// database-issues#9656.
2288    #[mz_ore::test]
2289    #[cfg_attr(miri, ignore)] // error: unsupported operation: can't call foreign function `decContextDefault` on OS `linux`
2290    fn test_result_spec_lattice_laws() {
2291        // A recipe for a `ResultSpec`, carrying owned `PropDatum`s so the spec
2292        // (which borrows `Datum`s) can be materialized inside the test.
2293        #[derive(Debug, Clone)]
2294        enum Recipe {
2295            Nothing,
2296            Null,
2297            Fails,
2298            Anything,
2299            ValueAll,
2300            Value(PropDatum),
2301            Between(PropDatum, PropDatum),
2302            Map(Vec<(PropDatum, Recipe)>),
2303            Union(Box<Recipe>, Box<Recipe>),
2304        }
2305
2306        fn materialize(recipe: &Recipe) -> ResultSpec<'_> {
2307            match recipe {
2308                Recipe::Nothing => ResultSpec::nothing(),
2309                Recipe::Null => ResultSpec::null(),
2310                Recipe::Fails => ResultSpec::fails(),
2311                Recipe::Anything => ResultSpec::anything(),
2312                Recipe::ValueAll => ResultSpec::value_all(),
2313                Recipe::Value(pd) => ResultSpec::value(pd.into()),
2314                Recipe::Between(a, b) => {
2315                    let (a, b): (Datum, Datum) = (a.into(), b.into());
2316                    if a.is_null() || b.is_null() {
2317                        ResultSpec::nothing()
2318                    } else if a <= b {
2319                        ResultSpec::value_between(a, b)
2320                    } else {
2321                        ResultSpec::value_between(b, a)
2322                    }
2323                }
2324                Recipe::Map(entries) => {
2325                    let mut map = BTreeMap::new();
2326                    for (key, val) in entries {
2327                        let key: Datum = key.into();
2328                        if !key.is_null() {
2329                            map.insert(key, materialize(val));
2330                        }
2331                    }
2332                    ResultSpec::map_spec(map)
2333                }
2334                Recipe::Union(a, b) => materialize(a).union(materialize(b)),
2335            }
2336        }
2337
2338        fn recipe_strategy() -> impl Strategy<Value = Recipe> {
2339            let leaf = proptest::strategy::Union::new(vec![
2340                Just(Recipe::Nothing).boxed(),
2341                Just(Recipe::Null).boxed(),
2342                Just(Recipe::Fails).boxed(),
2343                Just(Recipe::Anything).boxed(),
2344                Just(Recipe::ValueAll).boxed(),
2345                mz_repr::arb_datum(false).prop_map(Recipe::Value).boxed(),
2346                (mz_repr::arb_datum(false), mz_repr::arb_datum(false))
2347                    .prop_map(|(a, b)| Recipe::Between(a, b))
2348                    .boxed(),
2349            ]);
2350            leaf.prop_recursive(3, 24, 4, |inner| {
2351                proptest::strategy::Union::new(vec![
2352                    prop::collection::vec((mz_repr::arb_datum(false), inner.clone()), 0..3)
2353                        .prop_map(Recipe::Map)
2354                        .boxed(),
2355                    (inner.clone(), inner.clone())
2356                        .prop_map(|(a, b)| Recipe::Union(Box::new(a), Box::new(b)))
2357                        .boxed(),
2358                ])
2359            })
2360        }
2361
2362        fn check(a: Recipe, b: Recipe, v: PropDatum) -> Result<(), TestCaseError> {
2363            let a_spec = materialize(&a);
2364            let b_spec = materialize(&b);
2365            let v: Datum = (&v).into();
2366
2367            let in_a = a_spec.may_contain(v);
2368            let in_b = b_spec.may_contain(v);
2369
2370            if in_a || in_b {
2371                prop_assert!(
2372                    a_spec.clone().union(b_spec.clone()).may_contain(v),
2373                    "union dropped a value: a={a:?} b={b:?} v={v:?}",
2374                );
2375            }
2376            if in_a && in_b {
2377                prop_assert!(
2378                    a_spec.intersect(b_spec).may_contain(v),
2379                    "intersect dropped a common value: a={a:?} b={b:?} v={v:?}",
2380                );
2381            }
2382            Ok(())
2383        }
2384
2385        proptest!(
2386            ProptestConfig::with_cases(4096),
2387            |(a in recipe_strategy(), b in recipe_strategy(), v in mz_repr::arb_datum(true))| {
2388                check(a, b, v)?;
2389            }
2390        );
2391    }
2392
2393    /// Deterministic regression test for database-issues#9656 (PER-50), the
2394    /// minimal case [`test_equivalence_ranges`] shrinks to.
2395    ///
2396    /// A numeric column whose stats range spans `[-Infinity, NaN]` (NaN is the
2397    /// maximum of the numeric Datum order) feeds `-column`. `NegNumeric` is
2398    /// declared monotone, so the interpreter would narrow the output to
2399    /// `[neg(-Infinity), neg(NaN)] = [Infinity, NaN]` and wrongly rule out the
2400    /// value `1` that the evaluator produces for the interior input `-1`. That
2401    /// false negative is exactly what makes persist filter pushdown discard a
2402    /// part it should keep, tripping the audit panic.
2403    #[mz_ore::test]
2404    #[cfg_attr(miri, ignore)] // error: unsupported operation: can't call foreign function `decContextDefault` on OS `linux`
2405    fn test_neg_numeric_nan_range() {
2406        use mz_repr::adt::numeric::Numeric;
2407
2408        let neg = MirScalarExpr::CallUnary {
2409            func: UnaryFunc::NegNumeric(NegNumeric),
2410            expr: Box::new(MirScalarExpr::column(0)),
2411        };
2412
2413        let relation = ReprRelationType::new(vec![ReprScalarType::Numeric.nullable(false)]);
2414        let arena = RowArena::new();
2415        let mut interpreter = ColumnSpecs::new(&relation, &arena);
2416        interpreter.push_column(
2417            0,
2418            ResultSpec::value_between(
2419                Datum::from(Numeric::from(f64::NEG_INFINITY)),
2420                Datum::from(Numeric::from(f64::NAN)),
2421            ),
2422        );
2423
2424        let spec = interpreter.expr(&neg);
2425
2426        // `-1` is an interior value of the input range, and `-(-1) = 1`.
2427        let actual = neg
2428            .eval(&[Datum::from(Numeric::from(-1.0f64))], &arena)
2429            .expect("eval succeeds");
2430        assert!(
2431            spec.range.may_contain(actual),
2432            "interpreter must not rule out {actual:?}, which the evaluator \
2433             produces for an interior input; got spec {:?}",
2434            spec.range,
2435        );
2436    }
2437
2438    /// Deterministic regression test for database-issues#9656 (PER-50), the
2439    /// fallibility variant [`test_equivalence_ranges`] also surfaces.
2440    ///
2441    /// `cast_numeric_to_mz_timestamp` is declared monotone but errors on
2442    /// fractional inputs, which are dense in the interior of any range. The
2443    /// range `[0, 2]` has integer endpoints that both cast cleanly, so the
2444    /// interpreter's endpoint sampling never observes an error. It must instead
2445    /// surface the function's own `could_error`, otherwise persist filter
2446    /// pushdown discards a part whose interior rows (e.g. `1.5`) produce error
2447    /// rows.
2448    #[mz_ore::test]
2449    #[cfg_attr(miri, ignore)] // error: unsupported operation: can't call foreign function `decContextDefault` on OS `linux`
2450    fn test_fallible_monotone_interior_error() {
2451        use mz_repr::adt::numeric::Numeric;
2452
2453        let cast = MirScalarExpr::CallUnary {
2454            func: UnaryFunc::CastNumericToMzTimestamp(CastNumericToMzTimestamp),
2455            expr: Box::new(MirScalarExpr::column(0)),
2456        };
2457
2458        let relation = ReprRelationType::new(vec![ReprScalarType::Numeric.nullable(false)]);
2459        let arena = RowArena::new();
2460        let mut interpreter = ColumnSpecs::new(&relation, &arena);
2461        interpreter.push_column(
2462            0,
2463            ResultSpec::value_between(
2464                Datum::from(Numeric::from(0.0f64)),
2465                Datum::from(Numeric::from(2.0f64)),
2466            ),
2467        );
2468
2469        let spec = interpreter.expr(&cast);
2470
2471        // `1.5` is an interior value of `[0, 2]`, and casting a fractional
2472        // numeric to mz_timestamp errors.
2473        let interior = Datum::from(Numeric::from(1.5f64));
2474        assert!(
2475            cast.eval(&[interior], &arena).is_err(),
2476            "precondition: a fractional numeric fails to cast to mz_timestamp",
2477        );
2478        assert!(
2479            spec.range.may_fail(),
2480            "interpreter must surface that a monotone-but-fallible function may \
2481             error on an interior value it never sampled; got spec {:?}",
2482            spec.range,
2483        );
2484    }
2485
2486    /// Regression test for database-issues#9656.
2487    ///
2488    /// The interpreter must surface the fallibility of MFP expressions that
2489    /// aren't referenced by any predicate or temporal bound. The runtime MFP
2490    /// evaluator runs every expression once predicates pass, so an expression
2491    /// that errors on the actual data makes the whole row an `Err` — and
2492    /// `filter_result` must keep the part to emit that error.
2493    #[mz_ore::test]
2494    #[cfg_attr(miri, ignore)]
2495    fn test_mfp_unreferenced_fallible_expression() {
2496        use crate::scalar::func::CastStringToUuid;
2497
2498        // MFP: one expression that always errors on the input range, and one
2499        // predicate that always passes. The expression's result column is
2500        // *not* referenced by the predicate, so the default interpreter
2501        // implementation would AND together just `True` and miss the
2502        // fallibility.
2503        let mfp = MapFilterProject {
2504            expressions: vec![MirScalarExpr::CallUnary {
2505                func: UnaryFunc::CastStringToUuid(CastStringToUuid),
2506                expr: Box::new(MirScalarExpr::column(0)),
2507            }],
2508            predicates: vec![(
2509                1,
2510                MirScalarExpr::literal_ok(Datum::True, ReprScalarType::Bool),
2511            )],
2512            projection: vec![0, 1],
2513            input_arity: 1,
2514        };
2515
2516        let relation = ReprRelationType::new(vec![ReprScalarType::String.nullable(false)]);
2517        let arena = RowArena::new();
2518        let mut interpreter = ColumnSpecs::new(&relation, &arena);
2519        // "not-a-uuid" is in the stats range and definitely doesn't parse as a UUID.
2520        interpreter.push_column(
2521            0,
2522            ResultSpec::value_between(Datum::String("not-a-uuid"), Datum::String("not-a-uuid")),
2523        );
2524        let spec = interpreter.mfp_filter(&mfp);
2525        assert!(
2526            spec.range.may_fail(),
2527            "an MFP expression that errors on the stats range must propagate \
2528             fallibility, otherwise persist filter pushdown can wrongly discard \
2529             a part that produces error rows",
2530        );
2531    }
2532
2533    /// Proptest companion to [`test_mfp_unreferenced_fallible_expression`]:
2534    /// directly verifies the fallibility claim of [`ColumnSpecs::mfp_filter`]
2535    /// against the runtime MFP semantics. For a random expression placed in
2536    /// `MapFilterProject::expressions` (i.e. as an unreferenced Map step), if
2537    /// evaluating the expression on a row drawn from the stats range produces
2538    /// an error at runtime, then the interpreter's summary must report
2539    /// `may_fail()`. Without the `expressions.any(|s| s.range.fallible)` patch
2540    /// in `mfp_filter`, the AND over an empty predicate list collapses to
2541    /// `True` and the runtime error is wrongly ruled out.
2542    #[mz_ore::test]
2543    #[cfg_attr(miri, ignore)]
2544    fn test_mfp_filter_fallibility_equivalence() {
2545        fn check(data: ExpressionData) -> Result<(), TestCaseError> {
2546            let ExpressionData {
2547                relation_type,
2548                specs,
2549                rows,
2550                expr,
2551            } = data;
2552
2553            let input_arity = relation_type.column_types.len();
2554            let mfp = MapFilterProject {
2555                expressions: vec![expr.clone()],
2556                predicates: vec![],
2557                projection: (0..input_arity).collect(),
2558                input_arity,
2559            };
2560
2561            let arena = RowArena::new();
2562            let mut interpreter = ColumnSpecs::new(&relation_type, &arena);
2563            for (id, spec) in specs.into_iter().enumerate() {
2564                interpreter.push_column(id, spec);
2565            }
2566            let summary = interpreter.mfp_filter(&mfp);
2567
2568            for row in &rows {
2569                let datums: Vec<_> = row.iter().collect();
2570                if expr.eval(&datums, &arena).is_err() {
2571                    prop_assert!(
2572                        summary.range.may_fail(),
2573                        "mfp_filter must surface the fallibility of an \
2574                         unreferenced MFP expression: row {:?} errored at \
2575                         runtime but the interpreter ruled out errors",
2576                        row,
2577                    );
2578                }
2579            }
2580            Ok(())
2581        }
2582
2583        proptest!(|(data in gen_expr_data())| {
2584            check(data)?;
2585        });
2586    }
2587
2588    #[mz_ore::test]
2589    fn test_mfp() {
2590        // Regression test for https://github.com/MaterializeInc/database-issues/issues/5736
2591        use MirScalarExpr::*;
2592
2593        let mfp = MapFilterProject {
2594            expressions: vec![],
2595            predicates: vec![
2596                // Always fails on the known input range
2597                (
2598                    1,
2599                    CallUnary {
2600                        func: UnaryFunc::IsNull(IsNull),
2601                        expr: Box::new(CallBinary {
2602                            func: MulInt32.into(),
2603                            expr1: Box::new(MirScalarExpr::column(0)),
2604                            expr2: Box::new(MirScalarExpr::column(0)),
2605                        }),
2606                    },
2607                ),
2608                // Always returns false on the known input range
2609                (
2610                    1,
2611                    CallBinary {
2612                        func: Eq.into(),
2613                        expr1: Box::new(MirScalarExpr::column(0)),
2614                        expr2: Box::new(MirScalarExpr::literal_ok(
2615                            Datum::Int32(1727694505),
2616                            ReprScalarType::Int32,
2617                        )),
2618                    },
2619                ),
2620            ],
2621            projection: vec![],
2622            input_arity: 1,
2623        };
2624
2625        let relation = ReprRelationType::new(vec![ReprScalarType::Int32.nullable(true)]);
2626        let arena = RowArena::new();
2627        let mut interpreter = ColumnSpecs::new(&relation, &arena);
2628        interpreter.push_column(0, ResultSpec::value(Datum::Int32(-1294725158)));
2629        let spec = interpreter.mfp_filter(&mfp);
2630        assert!(spec.range.may_fail());
2631    }
2632
2633    #[mz_ore::test]
2634    fn test_concat() {
2635        let expr = MirScalarExpr::call_variadic(
2636            Concat,
2637            vec![
2638                MirScalarExpr::column(0),
2639                MirScalarExpr::literal_ok(Datum::String("a"), ReprScalarType::String),
2640                MirScalarExpr::literal_ok(Datum::String("b"), ReprScalarType::String),
2641            ],
2642        );
2643
2644        let relation = ReprRelationType::new(vec![ReprScalarType::String.nullable(false)]);
2645        let arena = RowArena::new();
2646        let interpreter = ColumnSpecs::new(&relation, &arena);
2647        let spec = interpreter.expr(&expr);
2648        assert!(spec.range.may_contain(Datum::String("blab")));
2649    }
2650
2651    #[mz_ore::test]
2652    fn test_eval_range() {
2653        // Example inspired by the tumbling windows temporal filter in the docs.
2654        //
2655        // NOTE: `may_fail()` is `true` in both cases below. `DivInt64`,
2656        // `MulInt64`, and `CastInt64ToMzTimestamp` can all error, and the
2657        // interpreter no longer infers infallibility from endpoint sampling for
2658        // an erroring function over a multi-valued range (it cannot prove the
2659        // function doesn't error on an interior value). For this data none of
2660        // them actually errors, so this is a conservative over-approximation
2661        // that keeps the part rather than pruning it. The value channel is still
2662        // narrowed precisely (`may_contain` below is exact), so a follow-up that
2663        // makes the fallibility flag argument-aware could recover pruning here.
2664        let period_ms = MirScalarExpr::literal_ok(Datum::Int64(10), ReprScalarType::Int64);
2665        let expr = MirScalarExpr::CallBinary {
2666            func: Gte.into(),
2667            expr1: Box::new(MirScalarExpr::CallUnmaterializable(
2668                UnmaterializableFunc::MzNow,
2669            )),
2670            expr2: Box::new(MirScalarExpr::CallUnary {
2671                func: UnaryFunc::CastInt64ToMzTimestamp(CastInt64ToMzTimestamp),
2672                expr: Box::new(MirScalarExpr::CallBinary {
2673                    func: MulInt64.into(),
2674                    expr1: Box::new(period_ms.clone()),
2675                    expr2: Box::new(MirScalarExpr::CallBinary {
2676                        func: DivInt64.into(),
2677                        expr1: Box::new(MirScalarExpr::column(0)),
2678                        expr2: Box::new(period_ms),
2679                    }),
2680                }),
2681            }),
2682        };
2683        let relation = ReprRelationType::new(vec![ReprScalarType::Int64.nullable(false)]);
2684
2685        {
2686            // Non-overlapping windows
2687            let arena = RowArena::new();
2688            let mut interpreter = ColumnSpecs::new(&relation, &arena);
2689            interpreter.push_unmaterializable(
2690                UnmaterializableFunc::MzNow,
2691                ResultSpec::value_between(
2692                    Datum::MzTimestamp(10.into()),
2693                    Datum::MzTimestamp(20.into()),
2694                ),
2695            );
2696            interpreter.push_column(0, ResultSpec::value_between(30i64.into(), 40i64.into()));
2697
2698            let range_out = interpreter.expr(&expr).range;
2699            assert!(range_out.may_contain(Datum::False));
2700            assert!(!range_out.may_contain(Datum::True));
2701            assert!(!range_out.may_contain(Datum::Null));
2702            assert!(range_out.may_fail());
2703        }
2704
2705        {
2706            // Overlapping windows
2707            let arena = RowArena::new();
2708            let mut interpreter = ColumnSpecs::new(&relation, &arena);
2709            interpreter.push_unmaterializable(
2710                UnmaterializableFunc::MzNow,
2711                ResultSpec::value_between(
2712                    Datum::MzTimestamp(10.into()),
2713                    Datum::MzTimestamp(35.into()),
2714                ),
2715            );
2716            interpreter.push_column(0, ResultSpec::value_between(30i64.into(), 40i64.into()));
2717
2718            let range_out = interpreter.expr(&expr).range;
2719            assert!(range_out.may_contain(Datum::False));
2720            assert!(range_out.may_contain(Datum::True));
2721            assert!(!range_out.may_contain(Datum::Null));
2722            assert!(range_out.may_fail());
2723        }
2724    }
2725
2726    #[mz_ore::test]
2727    #[cfg_attr(miri, ignore)] // error: unsupported operation: can't call foreign function `decNumberFromInt32` on OS `linux`
2728    fn test_jsonb() {
2729        let arena = RowArena::new();
2730
2731        let expr = MirScalarExpr::column(0)
2732            .call_binary(
2733                MirScalarExpr::literal_ok(Datum::from("ts"), ReprScalarType::String),
2734                JsonbGetString,
2735            )
2736            .call_unary(CastJsonbToNumeric(None));
2737
2738        let relation = ReprRelationType::new(vec![ReprScalarType::Jsonb.nullable(true)]);
2739        let mut interpreter = ColumnSpecs::new(&relation, &arena);
2740        interpreter.push_column(
2741            0,
2742            ResultSpec::map_spec(
2743                [(
2744                    "ts".into(),
2745                    ResultSpec::value_between(
2746                        Datum::Numeric(100.into()),
2747                        Datum::Numeric(300.into()),
2748                    ),
2749                )]
2750                .into_iter()
2751                .collect(),
2752            ),
2753        );
2754
2755        let range_out = interpreter.expr(&expr).range;
2756        assert!(!range_out.may_contain(Datum::Numeric(0.into())));
2757        assert!(range_out.may_contain(Datum::Numeric(200.into())));
2758        assert!(!range_out.may_contain(Datum::Numeric(400.into())));
2759    }
2760
2761    #[mz_ore::test]
2762    fn test_nested_union_partial_overlap() {
2763        // `Nested(map)` constrains a key only when the key is present in `map`; absent
2764        // keys mean "anything". So the union of two Nested specs must drop any key
2765        // that's missing from one side, because `x ∪ anything = anything`. Only keys
2766        // present in *both* sides survive (with their per-key specs unioned).
2767        let a = ResultSpec::map_spec(
2768            [
2769                ("x".into(), ResultSpec::value(Datum::String("a"))),
2770                ("y".into(), ResultSpec::value(Datum::String("b"))),
2771                ("c".into(), ResultSpec::value(Datum::String("c"))),
2772            ]
2773            .into_iter()
2774            .collect(),
2775        );
2776        let b = ResultSpec::map_spec(
2777            [
2778                ("x".into(), ResultSpec::value(Datum::String("a2"))),
2779                ("y".into(), ResultSpec::value(Datum::String("b2"))),
2780                ("z".into(), ResultSpec::value(Datum::String("z"))),
2781            ]
2782            .into_iter()
2783            .collect(),
2784        );
2785
2786        let unioned = a.union(b);
2787
2788        // Push the unioned spec through `->> <key>`: keys only in one side must
2789        // admit NULL (the other side is unconstrained, so the field could be absent
2790        // there); shared keys must include both observed values.
2791        let arena = RowArena::new();
2792        let relation = ReprRelationType::new(vec![ReprScalarType::Jsonb.nullable(false)]);
2793
2794        // Key only in `a`: the union must admit NULL.
2795        {
2796            let mut interpreter = ColumnSpecs::new(&relation, &arena);
2797            interpreter.push_column(0, unioned.clone());
2798            let expr = MirScalarExpr::column(0).call_binary(
2799                MirScalarExpr::literal_ok(Datum::from("c"), ReprScalarType::String),
2800                JsonbGetStringStringify,
2801            );
2802            assert!(interpreter.expr(&expr).range.may_contain(Datum::Null));
2803        }
2804
2805        // Key only in `b`: symmetric.
2806        {
2807            let mut interpreter = ColumnSpecs::new(&relation, &arena);
2808            interpreter.push_column(0, unioned.clone());
2809            let expr = MirScalarExpr::column(0).call_binary(
2810                MirScalarExpr::literal_ok(Datum::from("z"), ReprScalarType::String),
2811                JsonbGetStringStringify,
2812            );
2813            assert!(interpreter.expr(&expr).range.may_contain(Datum::Null));
2814        }
2815
2816        // Key in both: result must include both observed values.
2817        {
2818            let mut interpreter = ColumnSpecs::new(&relation, &arena);
2819            interpreter.push_column(0, unioned);
2820            let expr = MirScalarExpr::column(0).call_binary(
2821                MirScalarExpr::literal_ok(Datum::from("x"), ReprScalarType::String),
2822                JsonbGetStringStringify,
2823            );
2824            let x_range = interpreter.expr(&expr).range;
2825            assert!(x_range.may_contain(Datum::String("a")));
2826            assert!(x_range.may_contain(Datum::String("a2")));
2827        }
2828    }
2829
2830    #[mz_ore::test]
2831    #[cfg_attr(miri, ignore)] // unsupported foreign call in numeric decoding
2832    fn test_case_over_jsonb_columns() {
2833        // Regression test for PER-6: when CASE picks between two JSON columns whose
2834        // observed keys are disjoint, filter pushdown must not prune parts where
2835        // accessing a key only present in one branch might yield NULL in the other.
2836        let arena = RowArena::new();
2837
2838        // `(CASE WHEN col0 THEN col1 ELSE col2 END) ->> 'y' IS NULL`
2839        let expr = MirScalarExpr::If {
2840            cond: Box::new(MirScalarExpr::column(0)),
2841            then: Box::new(MirScalarExpr::column(1)),
2842            els: Box::new(MirScalarExpr::column(2)),
2843        }
2844        .call_binary(
2845            MirScalarExpr::literal_ok(Datum::from("y"), ReprScalarType::String),
2846            JsonbGetStringStringify,
2847        )
2848        .call_unary(UnaryFunc::IsNull(IsNull));
2849
2850        let relation = ReprRelationType::new(vec![
2851            ReprScalarType::Bool.nullable(false),
2852            ReprScalarType::Jsonb.nullable(false),
2853            ReprScalarType::Jsonb.nullable(false),
2854        ]);
2855        let mut interpreter = ColumnSpecs::new(&relation, &arena);
2856        interpreter.push_column(0, ResultSpec::value_between(Datum::False, Datum::True));
2857        interpreter.push_column(
2858            1,
2859            ResultSpec::map_spec(
2860                [("x".into(), ResultSpec::value(Datum::String("a")))]
2861                    .into_iter()
2862                    .collect(),
2863            ),
2864        );
2865        interpreter.push_column(
2866            2,
2867            ResultSpec::map_spec(
2868                [("y".into(), ResultSpec::value(Datum::String("b")))]
2869                    .into_iter()
2870                    .collect(),
2871            ),
2872        );
2873
2874        let range_out = interpreter.expr(&expr).range;
2875        // When the CASE selects column 1, "y" is absent and `->> 'y'` yields NULL, so
2876        // `IS NULL` is True. The filter must not prune a part that could match.
2877        assert!(range_out.may_contain(Datum::True));
2878    }
2879
2880    #[mz_ore::test]
2881    fn test_like() {
2882        let arena = RowArena::new();
2883
2884        let expr = MirScalarExpr::CallUnary {
2885            func: UnaryFunc::IsLikeMatch(IsLikeMatch(
2886                crate::like_pattern::compile("%whatever%", true).unwrap(),
2887            )),
2888            expr: Box::new(MirScalarExpr::column(0)),
2889        };
2890
2891        let relation = ReprRelationType::new(vec![ReprScalarType::String.nullable(true)]);
2892        let mut interpreter = ColumnSpecs::new(&relation, &arena);
2893        interpreter.push_column(
2894            0,
2895            ResultSpec::value_between(Datum::String("aardvark"), Datum::String("zebra")),
2896        );
2897
2898        let range_out = interpreter.expr(&expr).range;
2899        assert!(
2900            !range_out.fallible,
2901            "like function should not error on non-error input"
2902        );
2903        assert!(range_out.may_contain(Datum::True));
2904        assert!(range_out.may_contain(Datum::False));
2905        assert!(range_out.may_contain(Datum::Null));
2906    }
2907
2908    #[mz_ore::test]
2909    fn test_try_parse_monotonic_iso8601_timestamp() {
2910        use chrono::NaiveDateTime;
2911
2912        let arena = RowArena::new();
2913
2914        let expr = MirScalarExpr::CallUnary {
2915            func: UnaryFunc::TryParseMonotonicIso8601Timestamp(TryParseMonotonicIso8601Timestamp),
2916            expr: Box::new(MirScalarExpr::column(0)),
2917        };
2918
2919        let relation = ReprRelationType::new(vec![ReprScalarType::String.nullable(true)]);
2920        // Test the case where we have full timestamps as bounds.
2921        let mut interpreter = ColumnSpecs::new(&relation, &arena);
2922        interpreter.push_column(
2923            0,
2924            ResultSpec::value_between(
2925                Datum::String("2024-01-11T00:00:00.000Z"),
2926                Datum::String("2024-01-11T20:00:00.000Z"),
2927            ),
2928        );
2929
2930        let timestamp = |ts| {
2931            Datum::Timestamp(
2932                NaiveDateTime::parse_from_str(ts, "%Y-%m-%dT%H:%M:%S")
2933                    .unwrap()
2934                    .try_into()
2935                    .unwrap(),
2936            )
2937        };
2938
2939        let range_out = interpreter.expr(&expr).range;
2940        assert!(!range_out.fallible);
2941        assert!(range_out.nullable);
2942        assert!(!range_out.may_contain(timestamp("2024-01-10T10:00:00")));
2943        assert!(range_out.may_contain(timestamp("2024-01-11T10:00:00")));
2944        assert!(!range_out.may_contain(timestamp("2024-01-12T10:00:00")));
2945
2946        // Test the case where we have truncated / useless bounds.
2947        let mut interpreter = ColumnSpecs::new(&relation, &arena);
2948        interpreter.push_column(
2949            0,
2950            ResultSpec::value_between(Datum::String("2024-01-1"), Datum::String("2024-01-2")),
2951        );
2952
2953        let range_out = interpreter.expr(&expr).range;
2954        assert!(!range_out.fallible);
2955        assert!(range_out.nullable);
2956        assert!(range_out.may_contain(timestamp("2024-01-10T10:00:00")));
2957        assert!(range_out.may_contain(timestamp("2024-01-11T10:00:00")));
2958        assert!(range_out.may_contain(timestamp("2024-01-12T10:00:00")));
2959
2960        // Test the case where only one bound is truncated
2961        let mut interpreter = ColumnSpecs::new(&relation, &arena);
2962        interpreter.push_column(
2963            0,
2964            ResultSpec::value_between(
2965                Datum::String("2024-01-1"),
2966                Datum::String("2024-01-12T10:00:00"),
2967            )
2968            .union(ResultSpec::null()),
2969        );
2970
2971        let range_out = interpreter.expr(&expr).range;
2972        assert!(!range_out.fallible);
2973        assert!(range_out.nullable);
2974        assert!(range_out.may_contain(timestamp("2024-01-10T10:00:00")));
2975        assert!(range_out.may_contain(timestamp("2024-01-11T10:00:00")));
2976        assert!(range_out.may_contain(timestamp("2024-01-12T10:00:00")));
2977
2978        // Test the case where the upper and lower bound are identical
2979        let mut interpreter = ColumnSpecs::new(&relation, &arena);
2980        interpreter.push_column(
2981            0,
2982            ResultSpec::value_between(
2983                Datum::String("2024-01-11T10:00:00.000Z"),
2984                Datum::String("2024-01-11T10:00:00.000Z"),
2985            ),
2986        );
2987
2988        let range_out = interpreter.expr(&expr).range;
2989        assert!(!range_out.fallible);
2990        assert!(!range_out.nullable);
2991        assert!(!range_out.may_contain(timestamp("2024-01-10T10:00:00")));
2992        assert!(range_out.may_contain(timestamp("2024-01-11T10:00:00")));
2993        assert!(!range_out.may_contain(timestamp("2024-01-12T10:00:00")));
2994    }
2995
2996    #[mz_ore::test]
2997    fn test_inequality() {
2998        let arena = RowArena::new();
2999
3000        let expr = MirScalarExpr::column(0).call_binary(
3001            MirScalarExpr::CallUnmaterializable(UnmaterializableFunc::MzNow),
3002            Gte,
3003        );
3004
3005        let relation = ReprRelationType::new(vec![ReprScalarType::MzTimestamp.nullable(true)]);
3006        let mut interpreter = ColumnSpecs::new(&relation, &arena);
3007        interpreter.push_column(
3008            0,
3009            ResultSpec::value_between(
3010                Datum::MzTimestamp(1704736444949u64.into()),
3011                Datum::MzTimestamp(1704736444949u64.into()),
3012            )
3013            .union(ResultSpec::null()),
3014        );
3015        interpreter.push_unmaterializable(
3016            UnmaterializableFunc::MzNow,
3017            ResultSpec::value_between(
3018                Datum::MzTimestamp(1704738791000u64.into()),
3019                Datum::MzTimestamp(18446744073709551615u64.into()),
3020            ),
3021        );
3022
3023        let range_out = interpreter.expr(&expr).range;
3024        assert!(
3025            !range_out.fallible,
3026            "<= function should not error on non-error input"
3027        );
3028        assert!(!range_out.may_contain(Datum::True));
3029        assert!(range_out.may_contain(Datum::False));
3030        assert!(range_out.may_contain(Datum::Null));
3031    }
3032
3033    /// Regression test for database-issues#9656.
3034    ///
3035    /// Adding an `Interval` to a `Timestamp` is non-monotone in the interval
3036    /// argument: the lex order of intervals (months, days, micros) does not
3037    /// respect calendar-month arithmetic with day-clamping. The interpreter
3038    /// must therefore not assume monotonicity, otherwise persist filter
3039    /// pushdown can incorrectly conclude that a part has no matching rows.
3040    #[mz_ore::test]
3041    #[cfg_attr(miri, ignore)]
3042    fn test_add_timestamp_interval_non_monotone() {
3043        use chrono::NaiveDateTime;
3044        use mz_repr::adt::interval::Interval;
3045        use mz_repr::adt::timestamp::CheckedTimestamp;
3046        use mz_repr::{Datum, Row};
3047
3048        let arena = RowArena::new();
3049
3050        // The setup: a timestamp literal `t = 2024-01-31 00:00:00`, and an
3051        // interval column whose stats-range spans
3052        // `[{0 months, 31 days, 0 us}, {1 month, 0 days, 0 us}]`. In lex order,
3053        // the 31-day interval is the lower bound and the 1-month interval is
3054        // the upper bound. The function values at the endpoints are:
3055        //   t + {0,31,0} = 2024-03-02
3056        //   t + {1, 0,0} = 2024-02-29
3057        // But an *interior* interval like {0, 60, 0} maps to 2024-03-31, which
3058        // lies far outside `[Feb 29, Mar 2]`. Under the (incorrect) monotone
3059        // assumption, the interpreter would conclude the output is in that
3060        // narrow window, and rule out predicates like `>= 2024-03-15`.
3061        let ts_lit = |s: &str| {
3062            let mut row = Row::default();
3063            row.packer().push(Datum::Timestamp(
3064                CheckedTimestamp::from_timestamplike(
3065                    NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S").unwrap(),
3066                )
3067                .unwrap(),
3068            ));
3069            MirScalarExpr::Literal(Ok(row), ReprScalarType::Timestamp.nullable(false))
3070        };
3071        let interval = |months: i32, days: i32, micros: i64| {
3072            Datum::Interval(Interval {
3073                months,
3074                days,
3075                micros,
3076            })
3077        };
3078
3079        // Expression: `(timestamp_lit + interval_col) >= 2024-03-15`.
3080        let expr = ts_lit("2024-01-31T00:00:00")
3081            .call_binary(MirScalarExpr::column(0), AddTimestampInterval)
3082            .call_binary(ts_lit("2024-03-15T00:00:00"), Gte);
3083
3084        let relation = ReprRelationType::new(vec![ReprScalarType::Interval.nullable(false)]);
3085        let mut interpreter = ColumnSpecs::new(&relation, &arena);
3086        interpreter.push_column(
3087            0,
3088            ResultSpec::value_between(interval(0, 31, 0), interval(1, 0, 0)),
3089        );
3090
3091        let range_out = interpreter.expr(&expr).range;
3092        // The actual data may include e.g. `{0, 60, 0}` → 2024-03-31, which
3093        // satisfies `>= 2024-03-15`. The interpreter must admit `True` so that
3094        // filter pushdown does not skip the part. Under the buggy
3095        // `(true, true)` annotation, the output range would be
3096        // `[Feb 29, Mar 2]`, all of which is `< Mar 15`, and the interpreter
3097        // would (wrongly) admit only `False`.
3098        assert!(
3099            range_out.may_contain(Datum::True),
3100            "interpreter incorrectly ruled out matching rows; \
3101             add_timestamp_interval is not monotone in the interval argument",
3102        );
3103    }
3104
3105    /// Companion test to `test_add_timestamp_interval_non_monotone`: when the
3106    /// interval argument is a literal with `months == 0`, the function reduces
3107    /// to a pure linear shift in microseconds and *is* monotone in the
3108    /// timestamp. The dynamic-monotonicity handler in `AbstractFunc` should
3109    /// recover the tight output range in that case, so that filter pushdown
3110    /// can still narrow predicates like `t - INTERVAL '1' day < literal`.
3111    #[mz_ore::test]
3112    #[cfg_attr(miri, ignore)]
3113    fn test_timestamp_plus_interval_dynamic_monotone() {
3114        use chrono::NaiveDateTime;
3115        use mz_repr::adt::interval::Interval;
3116        use mz_repr::adt::timestamp::CheckedTimestamp;
3117        use mz_repr::{Datum, Row};
3118
3119        let arena = RowArena::new();
3120
3121        let ts = |s: &str| {
3122            Datum::Timestamp(
3123                CheckedTimestamp::from_timestamplike(
3124                    NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S").unwrap(),
3125                )
3126                .unwrap(),
3127            )
3128        };
3129        let interval_lit = |months: i32, days: i32, micros: i64| {
3130            let mut row = Row::default();
3131            row.packer().push(Datum::Interval(Interval {
3132                months,
3133                days,
3134                micros,
3135            }));
3136            MirScalarExpr::Literal(Ok(row), ReprScalarType::Interval.nullable(false))
3137        };
3138
3139        let relation = ReprRelationType::new(vec![ReprScalarType::Timestamp.nullable(false)]);
3140
3141        // (a) `t_col - INTERVAL '1' day < 2024-01-15`, with `t_col` ranging
3142        // over `[2024-01-15, 2024-01-20]`. With the days-only interval, the
3143        // subtraction is monotone, so endpoints alone determine the output:
3144        // [2024-01-14, 2024-01-19]. Only `2024-01-14` satisfies `< 2024-01-15`,
3145        // so both True and False are reachable.
3146        {
3147            let expr = MirScalarExpr::column(0)
3148                .call_binary(interval_lit(0, 1, 0), SubTimestampInterval)
3149                .call_binary(
3150                    MirScalarExpr::Literal(
3151                        Ok({
3152                            let mut r = Row::default();
3153                            r.packer().push(ts("2024-01-15T00:00:00"));
3154                            r
3155                        }),
3156                        ReprScalarType::Timestamp.nullable(false),
3157                    ),
3158                    Lt,
3159                );
3160            let mut interpreter = ColumnSpecs::new(&relation, &arena);
3161            interpreter.push_column(
3162                0,
3163                ResultSpec::value_between(ts("2024-01-15T00:00:00"), ts("2024-01-20T00:00:00")),
3164            );
3165            let range_out = interpreter.expr(&expr).range;
3166            assert!(
3167                range_out.may_contain(Datum::True),
3168                "day-only interval should preserve tight bounds",
3169            );
3170            assert!(
3171                range_out.may_contain(Datum::False),
3172                "day-only interval should preserve tight bounds",
3173            );
3174        }
3175
3176        // (b) Same predicate, but with `t_col` strictly *after* the literal:
3177        // `[2024-01-17, 2024-01-20]`. Output of `t - 1 day`:
3178        // `[2024-01-16, 2024-01-19]`, none of which is `< 2024-01-15`. The
3179        // interpreter must rule out `True`.
3180        {
3181            let expr = MirScalarExpr::column(0)
3182                .call_binary(interval_lit(0, 1, 0), SubTimestampInterval)
3183                .call_binary(
3184                    MirScalarExpr::Literal(
3185                        Ok({
3186                            let mut r = Row::default();
3187                            r.packer().push(ts("2024-01-15T00:00:00"));
3188                            r
3189                        }),
3190                        ReprScalarType::Timestamp.nullable(false),
3191                    ),
3192                    Lt,
3193                );
3194            let mut interpreter = ColumnSpecs::new(&relation, &arena);
3195            interpreter.push_column(
3196                0,
3197                ResultSpec::value_between(ts("2024-01-17T00:00:00"), ts("2024-01-20T00:00:00")),
3198            );
3199            let range_out = interpreter.expr(&expr).range;
3200            assert!(
3201                !range_out.may_contain(Datum::True),
3202                "day-only interval should narrow out impossible matches",
3203            );
3204        }
3205
3206        // (c) With a *month*-bearing literal interval, the operation is no
3207        // longer monotone (day-clamping), so the dynamic-monotonicity handler
3208        // must fall back to `anything()` — the interpreter cannot rule out
3209        // either outcome even when the column range is narrow.
3210        {
3211            let expr = MirScalarExpr::column(0)
3212                .call_binary(interval_lit(1, 0, 0), SubTimestampInterval)
3213                .call_binary(
3214                    MirScalarExpr::Literal(
3215                        Ok({
3216                            let mut r = Row::default();
3217                            r.packer().push(ts("2024-01-15T00:00:00"));
3218                            r
3219                        }),
3220                        ReprScalarType::Timestamp.nullable(false),
3221                    ),
3222                    Lt,
3223                );
3224            let mut interpreter = ColumnSpecs::new(&relation, &arena);
3225            interpreter.push_column(
3226                0,
3227                ResultSpec::value_between(ts("2024-01-17T00:00:00"), ts("2024-01-20T00:00:00")),
3228            );
3229            let range_out = interpreter.expr(&expr).range;
3230            assert!(
3231                range_out.may_contain(Datum::True),
3232                "month-bearing interval must conservatively admit True",
3233            );
3234            assert!(
3235                range_out.may_contain(Datum::False),
3236                "month-bearing interval must conservatively admit False",
3237            );
3238        }
3239    }
3240
3241    /// Proptest companion to [`test_timestamp_plus_interval_dynamic_monotone`]:
3242    /// the dynamic-monotonicity handler in [`AbstractFunc`] claims that
3243    /// `add_timestamp_interval(t, i)` is monotone in `t` whenever `i.months == 0`
3244    /// (the only case it actually claims monotonicity for at runtime: the
3245    /// matches above require the right argument to be a single value with
3246    /// `months == 0`). This proptest verifies that claim directly against the
3247    /// function impl by sampling random timestamps and zero-month intervals
3248    /// and checking that input ordering is preserved in the output.
3249    #[mz_ore::test]
3250    #[cfg_attr(miri, ignore)]
3251    fn proptest_timestamp_plus_interval_monotone_when_months_zero() {
3252        use mz_repr::adt::interval::Interval;
3253        use mz_repr::{Datum, RowArena, SqlScalarType, arb_datum_for_scalar};
3254        use proptest::prelude::*;
3255
3256        let timestamp_strat = || arb_datum_for_scalar(SqlScalarType::Timestamp { precision: None });
3257        // Lex order on `Interval` does *not* match total-microseconds order when
3258        // both days and micros vary independently (e.g. `{0, 0, 86_400_000_001}`
3259        // is lex-less than `{0, 1, 0}` but evaluates to a strictly larger
3260        // timestamp), so we only claim monotonicity for *fixed* zero-month
3261        // intervals — which is exactly what the DynamicMonotone handler does.
3262        // The proptest accordingly varies `t` with `i` held constant.
3263        let zero_month_interval_strat =
3264            (any::<i32>(), any::<i64>()).prop_map(|(days, micros)| Interval {
3265                months: 0,
3266                days,
3267                micros,
3268            });
3269
3270        let expr = MirScalarExpr::CallBinary {
3271            func: AddTimestampInterval.into(),
3272            expr1: Box::new(MirScalarExpr::column(0)),
3273            expr2: Box::new(MirScalarExpr::column(1)),
3274        };
3275        let arena = RowArena::new();
3276
3277        proptest!(|(
3278            t1 in timestamp_strat(),
3279            t2 in timestamp_strat(),
3280            i in zero_month_interval_strat,
3281        )| {
3282            let t1 = match t1 { PropDatum::Timestamp(t) => t, _ => unreachable!() };
3283            let t2 = match t2 { PropDatum::Timestamp(t) => t, _ => unreachable!() };
3284            let i = Datum::Interval(i);
3285            let r1 = expr.eval(&[Datum::Timestamp(t1), i], &arena);
3286            let r2 = expr.eval(&[Datum::Timestamp(t2), i], &arena);
3287            // Only compare when both calls succeed; the monotonicity claim
3288            // applies only within the success domain.
3289            if let (Ok(Datum::Timestamp(r1)), Ok(Datum::Timestamp(r2))) = (r1, r2) {
3290                prop_assert_eq!(t1.cmp(&t2), r1.cmp(&r2));
3291            }
3292        });
3293    }
3294
3295    /// Regression test for `date_bin_timestamp`, which is non-monotone in the
3296    /// `stride` argument: a larger stride can bin a source timestamp to an
3297    /// *earlier* result than a smaller stride, because the bin alignment to
3298    /// the unix epoch depends on the stride magnitude rather than on lex order.
3299    #[mz_ore::test]
3300    #[cfg_attr(miri, ignore)]
3301    fn test_date_bin_timestamp_non_monotone() {
3302        use chrono::NaiveDateTime;
3303        use mz_repr::adt::interval::Interval;
3304        use mz_repr::adt::timestamp::CheckedTimestamp;
3305        use mz_repr::{Datum, Row};
3306
3307        let arena = RowArena::new();
3308
3309        let ts_lit = |s: &str| {
3310            let mut row = Row::default();
3311            row.packer().push(Datum::Timestamp(
3312                CheckedTimestamp::from_timestamplike(
3313                    NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S").unwrap(),
3314                )
3315                .unwrap(),
3316            ));
3317            MirScalarExpr::Literal(Ok(row), ReprScalarType::Timestamp.nullable(false))
3318        };
3319        let interval = |months: i32, days: i32, micros: i64| {
3320            Datum::Interval(Interval {
3321                months,
3322                days,
3323                micros,
3324            })
3325        };
3326
3327        // Expression: `date_bin(stride_col, 2024-01-01 12:00:00) > 2024-01-01 06:00:00`.
3328        // stride_col ranges over `[1 day, 2 days]`.
3329        //
3330        // Endpoint evaluations:
3331        //   1 day stride → bins to 2024-01-01 00:00:00
3332        //   2 day stride → bins to 2023-12-31 00:00:00
3333        //
3334        // Interior strides produce results *outside* that endpoint box. For
3335        // example, a 1.5-day stride (i.e. `{0 months, 1 day, 12 h micros}`,
3336        // which sorts between the two endpoints in lex order) bins
3337        // 2024-01-01 12:00:00 to exactly 2024-01-01 12:00:00 — well above the
3338        // endpoint maximum of 2024-01-01 00:00:00. With the buggy
3339        // `(true, true)` annotation, the interpreter narrows the output to
3340        // `[Dec 31 00:00, Jan 1 00:00]`, both of which are `<= Jan 1 06:00`,
3341        // so the predicate is wrongly proved `False`. With the non-monotone
3342        // fix the output is `anything()`, so `True` is correctly admitted.
3343        let expr = MirScalarExpr::column(0)
3344            .call_binary(ts_lit("2024-01-01T12:00:00"), DateBinTimestamp)
3345            .call_binary(ts_lit("2024-01-01T06:00:00"), Gt);
3346
3347        let relation = ReprRelationType::new(vec![ReprScalarType::Interval.nullable(false)]);
3348        let mut interpreter = ColumnSpecs::new(&relation, &arena);
3349        interpreter.push_column(
3350            0,
3351            ResultSpec::value_between(interval(0, 1, 0), interval(0, 2, 0)),
3352        );
3353
3354        let range_out = interpreter.expr(&expr).range;
3355        assert!(
3356            range_out.may_contain(Datum::True),
3357            "date_bin is not monotone in the stride argument; \
3358             interior strides can produce outputs outside the endpoint-bounded \
3359             box, so the interpreter must admit True for `>`-style predicates",
3360        );
3361    }
3362
3363    /// `round` must not depend on a numeric's exponent: `Row` encoding folds
3364    /// trailing zeroes into it, so the interpreter, which reads its datums back
3365    /// out of a `Row`, would otherwise disagree with the evaluator and pushdown
3366    /// could discard a part it has to keep.
3367    #[mz_ore::test]
3368    #[cfg_attr(miri, ignore)]
3369    fn test_round_numeric_representation_independent() {
3370        use mz_repr::adt::date::Date;
3371
3372        let arena = RowArena::new();
3373        let lit = |d: Datum, ty: ReprScalarType| {
3374            let mut row = Row::default();
3375            row.packer().push(d);
3376            MirScalarExpr::Literal(Ok(row), ty.nullable(false))
3377        };
3378
3379        // `extract` hands `round` a `946684800` whose exponent is zero, where
3380        // the same value read out of a `Row` is `9.466848E+8`.
3381        let expr = lit(
3382            Datum::Date(Date::from_pg_epoch(0).unwrap()),
3383            ReprScalarType::Date,
3384        )
3385        .call_unary(UnaryFunc::ExtractDate(ExtractDate(DateTimeUnits::Epoch)))
3386        .call_binary(
3387            lit(Datum::Int32(i32::MAX), ReprScalarType::Int32),
3388            BinaryFunc::from(RoundNumericBinary),
3389        );
3390
3391        let relation = ReprRelationType::new(vec![]);
3392        let range = ColumnSpecs::new(&relation, &arena).expr(&expr).range;
3393        match expr.eval(&[], &arena) {
3394            Ok(value) => assert!(
3395                range.may_contain(value),
3396                "interpreter ruled out {value:?}, which the evaluator produced: {range:?}",
3397            ),
3398            Err(_) => assert!(
3399                range.may_fail(),
3400                "interpreter ruled out the error the evaluator produced: {range:?}",
3401            ),
3402        }
3403    }
3404
3405    #[mz_ore::test]
3406    fn test_trace() {
3407        use super::Trace;
3408
3409        let expr = MirScalarExpr::column(0).call_binary(
3410            MirScalarExpr::column(1)
3411                .call_binary(MirScalarExpr::column(3).call_unary(NegInt64), AddInt64),
3412            Gte,
3413        );
3414        let summary = Trace.expr(&expr);
3415        assert!(summary.pushdownable());
3416    }
3417}