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            TryCast(f) => unary_typecheck(f.inner(), arg),
1692            _ => false,
1693        }
1694    }
1695
1696    /// Fallible casts to wrap in `TryCast`, as the planner does for `TRY_CAST`:
1697    /// narrowing integer casts, float to integer, numeric to integer, and the
1698    /// jsonb casts that reject the wrong kind of value.
1699    const TRY_CAST_INNER_FUNCS: &[UnaryFunc] = &[
1700        UnaryFunc::CastInt64ToInt32(CastInt64ToInt32),
1701        UnaryFunc::CastInt32ToInt16(CastInt32ToInt16),
1702        UnaryFunc::CastInt64ToUint64(CastInt64ToUint64),
1703        UnaryFunc::CastFloat64ToInt64(CastFloat64ToInt64),
1704        UnaryFunc::CastFloat64ToFloat32(CastFloat64ToFloat32),
1705        UnaryFunc::CastNumericToInt32(CastNumericToInt32),
1706        UnaryFunc::CastNumericToMzTimestamp(CastNumericToMzTimestamp),
1707        UnaryFunc::CastJsonbToNumeric(CastJsonbToNumeric(None)),
1708        UnaryFunc::CastJsonbToBool(CastJsonbToBool),
1709        UnaryFunc::CastTimestampToMzTimestamp(CastTimestampToMzTimestamp),
1710    ];
1711
1712    /// The declared unary functions, plus `TryCast` around each fallible cast
1713    /// in `TRY_CAST_INNER_FUNCS`, so the interpreter's properties are checked
1714    /// over the null-fallback wrapper as well.
1715    fn interesting_unary_funcs() -> Vec<UnaryFunc> {
1716        let mut funcs = INTERESTING_UNARY_FUNCS.to_vec();
1717        funcs.extend(TRY_CAST_INNER_FUNCS.iter().map(|cast| {
1718            assert!(
1719                cast.could_error(),
1720                "{cast} never errors, so wrapping it is pointless"
1721            );
1722            UnaryFunc::try_cast_if_fallible(cast.clone())
1723        }));
1724        funcs
1725    }
1726
1727    fn interesting_binary_funcs() -> Vec<BinaryFunc> {
1728        vec![
1729            AddTimestampInterval.into(),
1730            AddNumeric.into(),
1731            SubNumeric.into(),
1732            MulNumeric.into(),
1733            DivNumeric.into(),
1734            AddFloat64.into(),
1735            SubFloat64.into(),
1736            MulFloat64.into(),
1737            DivFloat64.into(),
1738            MulFloat32.into(),
1739            DivFloat32.into(),
1740            RoundNumericBinary.into(),
1741            Eq.into(),
1742            Lt.into(),
1743            Gt.into(),
1744            Lte.into(),
1745            Gte.into(),
1746            DateTruncUnitsTimestamp.into(),
1747            JsonbGetString.into(),
1748            JsonbGetStringStringify.into(),
1749            // Declared-monotone integer arithmetic: overflow and
1750            // division-by-zero are interior error conditions the endpoints
1751            // need not reveal.
1752            AddInt32.into(),
1753            SubInt32.into(),
1754            MulInt32.into(),
1755            DivInt32.into(),
1756            AddInt64.into(),
1757            MulInt64.into(),
1758            AddFloat32.into(),
1759            SubFloat32.into(),
1760            // Monotone in the right argument only.
1761            TextConcatBinary.into(),
1762            // Monotone left, and a declared non-monotone control.
1763            AddDateInterval.into(),
1764            AddTimeInterval.into(),
1765            // Batch 2: remaining ordered-domain arithmetic.
1766            SubInt64.into(),
1767            DivInt64.into(),
1768            SubTimestamp.into(),
1769            SubDate.into(),
1770            AddInterval.into(),
1771            SubInterval.into(),
1772            // Batch 3: the int16 and unsigned arithmetic families, remaining
1773            // date/time arithmetic, and binary date_bin.
1774            AddInt16.into(),
1775            SubInt16.into(),
1776            MulInt16.into(),
1777            DivInt16.into(),
1778            AddUint16.into(),
1779            SubUint16.into(),
1780            MulUint16.into(),
1781            DivUint16.into(),
1782            AddUint32.into(),
1783            SubUint32.into(),
1784            MulUint32.into(),
1785            DivUint32.into(),
1786            AddUint64.into(),
1787            SubUint64.into(),
1788            MulUint64.into(),
1789            DivUint64.into(),
1790            SubTime.into(),
1791            SubTimestampTz.into(),
1792            AddDateTime.into(),
1793            SubDateInterval.into(),
1794            DateBinTimestamp.into(),
1795        ]
1796    }
1797
1798    fn binary_typecheck(func: &BinaryFunc, arg0: &ReprColumnType, arg1: &ReprColumnType) -> bool {
1799        use BinaryFunc::*;
1800        match func {
1801            AddTimestampInterval(_) => {
1802                arg0.scalar_type == ReprScalarType::Timestamp
1803                    && arg1.scalar_type == ReprScalarType::Interval
1804            }
1805            AddNumeric(_) | SubNumeric(_) | MulNumeric(_) | DivNumeric(_) => {
1806                arg0.scalar_type == NUM_TYPE && arg1.scalar_type == NUM_TYPE
1807            }
1808            AddFloat64(_) | SubFloat64(_) | MulFloat64(_) | DivFloat64(_) => {
1809                arg0.scalar_type == ReprScalarType::Float64
1810                    && arg1.scalar_type == ReprScalarType::Float64
1811            }
1812            MulFloat32(_) | DivFloat32(_) => {
1813                arg0.scalar_type == ReprScalarType::Float32
1814                    && arg1.scalar_type == ReprScalarType::Float32
1815            }
1816            RoundNumeric(_) => {
1817                arg0.scalar_type == NUM_TYPE && arg1.scalar_type == ReprScalarType::Int32
1818            }
1819            Eq(_) | Lt(_) | Gt(_) | Lte(_) | Gte(_) => arg0.scalar_type == arg1.scalar_type,
1820            DateTruncTimestamp(_) => {
1821                arg0.scalar_type == ReprScalarType::String
1822                    && arg1.scalar_type == ReprScalarType::Timestamp
1823            }
1824            JsonbGetString(_) | JsonbGetStringStringify(_) => {
1825                arg0.scalar_type == ReprScalarType::Jsonb
1826                    && arg1.scalar_type == ReprScalarType::String
1827            }
1828            AddInt32(_) | SubInt32(_) | MulInt32(_) | DivInt32(_) => {
1829                arg0.scalar_type == ReprScalarType::Int32
1830                    && arg1.scalar_type == ReprScalarType::Int32
1831            }
1832            AddInt64(_) | MulInt64(_) | SubInt64(_) | DivInt64(_) => {
1833                arg0.scalar_type == ReprScalarType::Int64
1834                    && arg1.scalar_type == ReprScalarType::Int64
1835            }
1836            SubTimestamp(_) => {
1837                arg0.scalar_type == ReprScalarType::Timestamp
1838                    && arg1.scalar_type == ReprScalarType::Timestamp
1839            }
1840            SubDate(_) => {
1841                arg0.scalar_type == ReprScalarType::Date && arg1.scalar_type == ReprScalarType::Date
1842            }
1843            AddInterval(_) | SubInterval(_) => {
1844                arg0.scalar_type == ReprScalarType::Interval
1845                    && arg1.scalar_type == ReprScalarType::Interval
1846            }
1847            AddInt16(_) | SubInt16(_) | MulInt16(_) | DivInt16(_) => {
1848                arg0.scalar_type == ReprScalarType::Int16
1849                    && arg1.scalar_type == ReprScalarType::Int16
1850            }
1851            AddUint16(_) | SubUint16(_) | MulUint16(_) | DivUint16(_) => {
1852                arg0.scalar_type == ReprScalarType::UInt16
1853                    && arg1.scalar_type == ReprScalarType::UInt16
1854            }
1855            AddUint32(_) | SubUint32(_) | MulUint32(_) | DivUint32(_) => {
1856                arg0.scalar_type == ReprScalarType::UInt32
1857                    && arg1.scalar_type == ReprScalarType::UInt32
1858            }
1859            AddUint64(_) | SubUint64(_) | MulUint64(_) | DivUint64(_) => {
1860                arg0.scalar_type == ReprScalarType::UInt64
1861                    && arg1.scalar_type == ReprScalarType::UInt64
1862            }
1863            SubTime(_) => {
1864                arg0.scalar_type == ReprScalarType::Time && arg1.scalar_type == ReprScalarType::Time
1865            }
1866            SubTimestampTz(_) => {
1867                arg0.scalar_type == ReprScalarType::TimestampTz
1868                    && arg1.scalar_type == ReprScalarType::TimestampTz
1869            }
1870            AddDateTime(_) => {
1871                arg0.scalar_type == ReprScalarType::Date && arg1.scalar_type == ReprScalarType::Time
1872            }
1873            SubDateInterval(_) => {
1874                arg0.scalar_type == ReprScalarType::Date
1875                    && arg1.scalar_type == ReprScalarType::Interval
1876            }
1877            DateBinTimestamp(_) => {
1878                arg0.scalar_type == ReprScalarType::Interval
1879                    && arg1.scalar_type == ReprScalarType::Timestamp
1880            }
1881            AddFloat32(_) | SubFloat32(_) => {
1882                arg0.scalar_type == ReprScalarType::Float32
1883                    && arg1.scalar_type == ReprScalarType::Float32
1884            }
1885            TextConcat(_) => {
1886                arg0.scalar_type == ReprScalarType::String
1887                    && arg1.scalar_type == ReprScalarType::String
1888            }
1889            AddDateInterval(_) => {
1890                arg0.scalar_type == ReprScalarType::Date
1891                    && arg1.scalar_type == ReprScalarType::Interval
1892            }
1893            AddTimeInterval(_) => {
1894                arg0.scalar_type == ReprScalarType::Time
1895                    && arg1.scalar_type == ReprScalarType::Interval
1896            }
1897            _ => false,
1898        }
1899    }
1900
1901    const INTERESTING_VARIADIC_FUNCS: &[VariadicFunc] = {
1902        use crate::scalar::func::variadic as v;
1903        use VariadicFunc::*;
1904        &[
1905            Coalesce(v::Coalesce),
1906            Greatest(v::Greatest),
1907            Least(v::Least),
1908            And(v::And),
1909            Or(v::Or),
1910            Concat(v::Concat),
1911            ConcatWs(v::ConcatWs),
1912        ]
1913    };
1914
1915    fn variadic_typecheck(func: &VariadicFunc, args: &[ReprColumnType]) -> bool {
1916        use VariadicFunc::*;
1917        fn all_eq<'a>(
1918            iter: impl IntoIterator<Item = &'a ReprColumnType>,
1919            other: &ReprScalarType,
1920        ) -> bool {
1921            iter.into_iter().all(|t| t.scalar_type == *other)
1922        }
1923        match func {
1924            Coalesce(_) | Greatest(_) | Least(_) => match args {
1925                [] => true,
1926                [first, rest @ ..] => all_eq(rest, &first.scalar_type),
1927            },
1928            And(_) | Or(_) => all_eq(args, &ReprScalarType::Bool),
1929            Concat(_) => all_eq(args, &ReprScalarType::String),
1930            ConcatWs(_) => args.len() > 1 && all_eq(args, &ReprScalarType::String),
1931            _ => false,
1932        }
1933    }
1934
1935    fn gen_datums_for_type(typ: &ReprColumnType) -> BoxedStrategy<Datum<'static>> {
1936        let mut values: Vec<Datum<'static>> = SqlScalarType::from_repr(&typ.scalar_type)
1937            .interesting_datums()
1938            .collect();
1939        if typ.nullable {
1940            values.push(Datum::Null)
1941        }
1942        select(values).boxed()
1943    }
1944
1945    fn gen_column() -> impl Strategy<Value = (ReprColumnType, Datum<'static>, ResultSpec<'static>)>
1946    {
1947        let col_type = (select(SCALAR_TYPES), any::<bool>())
1948            .prop_map(|(t, b)| t.nullable(b))
1949            .prop_filter("need at least one value", |c| {
1950                SqlScalarType::from_repr(&c.scalar_type)
1951                    .interesting_datums()
1952                    .count()
1953                    > 0
1954            });
1955
1956        let result_spec = select(vec![
1957            ResultSpec::nothing(),
1958            ResultSpec::null(),
1959            ResultSpec::anything(),
1960            ResultSpec::value_all(),
1961        ]);
1962
1963        (col_type, result_spec).prop_flat_map(|(col, result_spec)| {
1964            gen_datums_for_type(&col).prop_map(move |datum| {
1965                let result_spec = result_spec.clone().union(ResultSpec::value(datum));
1966                (col.clone(), datum, result_spec)
1967            })
1968        })
1969    }
1970
1971    fn gen_expr_for_relation(
1972        relation: &ReprRelationType,
1973    ) -> BoxedStrategy<(MirScalarExpr, ReprColumnType)> {
1974        let column_gen = {
1975            let column_types = relation.column_types.clone();
1976            any::<Index>()
1977                .prop_map(move |idx| {
1978                    let id = idx.index(column_types.len());
1979                    (MirScalarExpr::column(id), column_types[id].clone())
1980                })
1981                .boxed()
1982        };
1983
1984        let literal_gen = (select(SCALAR_TYPES), any::<bool>())
1985            .prop_map(|(s, b)| s.nullable(b))
1986            .prop_flat_map(|ct| {
1987                let error_gen = any::<EvalError>().prop_map(Err).boxed();
1988                let value_gen = gen_datums_for_type(&ct)
1989                    .prop_map(move |datum| Ok(Row::pack_slice(&[datum])))
1990                    .boxed();
1991                error_gen.prop_union(value_gen).prop_map(move |result| {
1992                    (MirScalarExpr::Literal(result, ct.clone()), ct.clone())
1993                })
1994            })
1995            .boxed();
1996
1997        column_gen
1998            .prop_union(literal_gen)
1999            .prop_recursive(4, 64, 8, |self_gen| {
2000                let unary_gen = (select(interesting_unary_funcs()), self_gen.clone())
2001                    .prop_filter_map("unary func", |(func, (expr_in, type_in))| {
2002                        if !unary_typecheck(&func, &type_in) {
2003                            return None;
2004                        }
2005                        let type_out = func.output_type(type_in);
2006                        let expr_out = MirScalarExpr::CallUnary {
2007                            func,
2008                            expr: Box::new(expr_in),
2009                        };
2010                        Some((expr_out, type_out))
2011                    })
2012                    .boxed();
2013                let binary_gen = (
2014                    select(interesting_binary_funcs()),
2015                    self_gen.clone(),
2016                    self_gen.clone(),
2017                )
2018                    .prop_filter_map(
2019                        "binary func",
2020                        |(func, (expr_left, type_left), (expr_right, type_right))| {
2021                            if !binary_typecheck(&func, &type_left, &type_right) {
2022                                return None;
2023                            }
2024                            let type_out = func.output_type(&[type_left, type_right]);
2025                            let expr_out = MirScalarExpr::CallBinary {
2026                                func,
2027                                expr1: Box::new(expr_left),
2028                                expr2: Box::new(expr_right),
2029                            };
2030                            Some((expr_out, type_out))
2031                        },
2032                    )
2033                    .boxed();
2034                let variadic_gen = (
2035                    select(INTERESTING_VARIADIC_FUNCS),
2036                    prop::collection::vec(self_gen.clone(), 1..4),
2037                )
2038                    .prop_filter_map("variadic func", |(func, exprs)| {
2039                        let (exprs_in, type_in): (_, Vec<_>) = exprs.into_iter().unzip();
2040                        if !variadic_typecheck(&func, &type_in) {
2041                            return None;
2042                        }
2043                        let type_out = func.output_type(type_in);
2044                        let expr_out = MirScalarExpr::CallVariadic {
2045                            func,
2046                            exprs: exprs_in,
2047                        };
2048                        Some((expr_out, type_out))
2049                    })
2050                    .boxed();
2051                // Generate `If` nodes without the heavy rejection that filtering
2052                // three independent subexprs for a bool condition and matching
2053                // branch types would incur. The condition is a boolean literal
2054                // (so it can be `True`, `False`, or `Null` — exercising `cond`'s
2055                // value channel, including the null-takes-`els` path), and `els`
2056                // is a literal of `then`'s type so the branches always unify.
2057                let if_gen = {
2058                    let bool_type = ReprScalarType::Bool.nullable(true);
2059                    let cond_gen = gen_datums_for_type(&bool_type).prop_map(move |datum| {
2060                        MirScalarExpr::Literal(Ok(Row::pack_slice(&[datum])), bool_type.clone())
2061                    });
2062                    (cond_gen, self_gen.clone())
2063                        .prop_flat_map(|(cond_expr, (then_expr, then_type))| {
2064                            let out_type = then_type.clone();
2065                            gen_datums_for_type(&then_type).prop_map(move |datum| {
2066                                let els_expr = MirScalarExpr::Literal(
2067                                    Ok(Row::pack_slice(&[datum])),
2068                                    out_type.clone(),
2069                                );
2070                                let expr_out = MirScalarExpr::If {
2071                                    cond: Box::new(cond_expr.clone()),
2072                                    then: Box::new(then_expr.clone()),
2073                                    els: Box::new(els_expr),
2074                                };
2075                                (expr_out, out_type.clone())
2076                            })
2077                        })
2078                        .boxed()
2079                };
2080
2081                unary_gen
2082                    .prop_union(binary_gen)
2083                    .boxed()
2084                    .prop_union(variadic_gen)
2085                    .boxed()
2086                    .prop_union(if_gen)
2087            })
2088            .boxed()
2089    }
2090
2091    fn gen_expr_data() -> impl Strategy<Value = ExpressionData> {
2092        let columns = prop::collection::vec(gen_column(), 1..10);
2093        columns.prop_flat_map(|data| {
2094            let (columns, datums, specs): (Vec<_>, Vec<_>, Vec<_>) = data.into_iter().multiunzip();
2095            let relation = ReprRelationType::new(columns);
2096            let row = Row::pack_slice(&datums);
2097            gen_expr_for_relation(&relation).prop_map(move |(expr, _)| ExpressionData {
2098                relation_type: relation.clone(),
2099                specs: specs.clone(),
2100                rows: vec![row.clone()],
2101                expr,
2102            })
2103        })
2104    }
2105
2106    #[mz_ore::test]
2107    #[cfg_attr(miri, ignore)] // error: unsupported operation: can't call foreign function `decContextDefault` on OS `linux`
2108    fn test_trivial_spec_matches() {
2109        fn check(datum: PropDatum) -> Result<(), TestCaseError> {
2110            let datum: Datum = (&datum).into();
2111            let spec = if datum.is_null() {
2112                ResultSpec::null()
2113            } else {
2114                ResultSpec::value(datum)
2115            };
2116            assert!(spec.may_contain(datum));
2117            Ok(())
2118        }
2119
2120        proptest!(|(datum in mz_repr::arb_datum(true))| {
2121            check(datum)?;
2122        });
2123
2124        assert!(ResultSpec::fails().may_fail());
2125    }
2126
2127    #[mz_ore::test]
2128    #[cfg_attr(miri, ignore)] // error: unsupported operation: can't call foreign function `decContextDefault` on OS `linux`
2129    fn test_equivalence() {
2130        fn check(data: ExpressionData) -> Result<(), TestCaseError> {
2131            let ExpressionData {
2132                relation_type,
2133                specs,
2134                rows,
2135                expr,
2136            } = data;
2137
2138            // We want to ensure that the spec we get when evaluating an expression using
2139            // `ColumnSpecs` always contains the _actual_ value of that column when evaluated with
2140            // eval. (This is an important correctness property of abstract interpretation.)
2141            let arena = RowArena::new();
2142            let mut interpreter = ColumnSpecs::new(&relation_type, &arena);
2143            for (id, spec) in specs.into_iter().enumerate() {
2144                interpreter.push_column(id, spec);
2145            }
2146
2147            let spec = interpreter.expr(&expr);
2148
2149            for row in &rows {
2150                let datums: Vec<_> = row.iter().collect();
2151                let eval_result = expr.eval(&datums, &arena);
2152                match eval_result {
2153                    Ok(value) => {
2154                        assert!(spec.range.may_contain(value))
2155                    }
2156                    Err(_) => {
2157                        assert!(spec.range.may_fail());
2158                    }
2159                }
2160            }
2161
2162            Ok(())
2163        }
2164
2165        proptest!(|(data in gen_expr_data())| {
2166            check(data)?;
2167        });
2168    }
2169
2170    /// A column whose spec is a genuine `value_between(lo, hi)` range with
2171    /// `lo < hi`, paired with a concrete row value `mid` drawn from strictly
2172    /// inside `[lo, hi]`. This is the shape persist filter pushdown actually
2173    /// feeds the interpreter: min/max stats become a `Values::Within` range,
2174    /// and the interpreter narrows it through each function (relying on
2175    /// monotonicity) without ever seeing the interior values. `gen_column`
2176    /// only ever produces single-value or `anything` specs, so it never
2177    /// exercises the range-narrowing path.
2178    fn gen_range_column()
2179    -> impl Strategy<Value = (ReprColumnType, Datum<'static>, ResultSpec<'static>)> {
2180        select(SCALAR_TYPES)
2181            .prop_map(|t| t.nullable(false))
2182            .prop_filter("need at least two distinct values for a range", |c| {
2183                let mut datums: Vec<Datum> = SqlScalarType::from_repr(&c.scalar_type)
2184                    .interesting_datums()
2185                    .filter(|d| !d.is_null())
2186                    .collect();
2187                datums.sort();
2188                datums.dedup();
2189                datums.len() >= 2
2190            })
2191            .prop_flat_map(|col| {
2192                let mut datums: Vec<Datum<'static>> = SqlScalarType::from_repr(&col.scalar_type)
2193                    .interesting_datums()
2194                    .filter(|d| !d.is_null())
2195                    .collect();
2196                datums.sort();
2197                datums.dedup();
2198                (
2199                    Just(col),
2200                    Just(datums),
2201                    any::<Index>(),
2202                    any::<Index>(),
2203                    any::<Index>(),
2204                )
2205                    .prop_map(|(col, datums, a, b, c)| {
2206                        let n = datums.len();
2207                        let mut idxs = [a.index(n), b.index(n), c.index(n)];
2208                        idxs.sort();
2209                        let lo = datums[idxs[0]];
2210                        let mid = datums[idxs[1]];
2211                        let hi = datums[idxs[2]];
2212                        let spec = ResultSpec::value_between(lo, hi);
2213                        (col, mid, spec)
2214                    })
2215            })
2216    }
2217
2218    fn gen_range_expr_data() -> impl Strategy<Value = ExpressionData> {
2219        let columns = prop::collection::vec(gen_range_column(), 1..10);
2220        columns.prop_flat_map(|data| {
2221            let (columns, datums, specs): (Vec<_>, Vec<_>, Vec<_>) = data.into_iter().multiunzip();
2222            let relation = ReprRelationType::new(columns);
2223            let row = Row::pack_slice(&datums);
2224            gen_expr_for_relation(&relation).prop_map(move |(expr, _)| ExpressionData {
2225                relation_type: relation.clone(),
2226                specs: specs.clone(),
2227                rows: vec![row.clone()],
2228                expr,
2229            })
2230        })
2231    }
2232
2233    /// Regression test for database-issues#9656 (PER-50).
2234    ///
2235    /// Like [`test_equivalence`], but the column specs are genuine
2236    /// `value_between(lo, hi)` ranges rather than single values. This is the
2237    /// input shape persist filter pushdown produces from min/max stats, and it
2238    /// is the one that drives the interpreter's monotonicity-based range
2239    /// narrowing. If a function narrows a range to a spec that does not contain
2240    /// the value the concrete evaluator produces for an interior input, the
2241    /// interpreter can wrongly rule out a matching row and pushdown discards a
2242    /// part it should have kept, triggering the audit panic.
2243    #[mz_ore::test]
2244    #[cfg_attr(miri, ignore)] // error: unsupported operation: can't call foreign function `decContextDefault` on OS `linux`
2245    fn test_equivalence_ranges() {
2246        fn check(data: ExpressionData) -> Result<(), TestCaseError> {
2247            let ExpressionData {
2248                relation_type,
2249                specs,
2250                rows,
2251                expr,
2252            } = data;
2253
2254            let arena = RowArena::new();
2255            let mut interpreter = ColumnSpecs::new(&relation_type, &arena);
2256            for (id, spec) in specs.into_iter().enumerate() {
2257                interpreter.push_column(id, spec);
2258            }
2259
2260            let spec = interpreter.expr(&expr);
2261
2262            for row in &rows {
2263                let datums: Vec<_> = row.iter().collect();
2264                let eval_result = expr.eval(&datums, &arena);
2265                match eval_result {
2266                    Ok(value) => {
2267                        prop_assert!(
2268                            spec.range.may_contain(value),
2269                            "interpreter ruled out a value the evaluator produced \
2270                             for an interior input: expr={expr:?} row={row:?} \
2271                             value={value:?} spec={:?}",
2272                            spec.range,
2273                        );
2274                    }
2275                    Err(_) => {
2276                        prop_assert!(
2277                            spec.range.may_fail(),
2278                            "interpreter ruled out an error the evaluator produced \
2279                             for an interior input: expr={expr:?} row={row:?}",
2280                        );
2281                    }
2282                }
2283            }
2284
2285            Ok(())
2286        }
2287
2288        // The expression generator rejects many function/type combinations
2289        // (see the `prop_filter_map`s in `gen_expr_for_relation`), so the
2290        // per-run local-reject budget has to be raised well above proptest's
2291        // default to let enough cases through.
2292        // An explicit PROPTEST_CASES (already parsed into the default config)
2293        // wins, for long local or nightly runs. The generator rejects at a
2294        // roughly fixed rate per case, so the reject budget scales with the
2295        // case count.
2296        let default = ProptestConfig::default();
2297        let cases = if std::env::var_os("PROPTEST_CASES").is_some() {
2298            default.cases
2299        } else {
2300            2048
2301        };
2302        let config = ProptestConfig {
2303            cases,
2304            max_local_rejects: cases.saturating_mul(512),
2305            ..default
2306        };
2307        proptest!(config, |(data in gen_range_expr_data())| {
2308            check(data)?;
2309        });
2310    }
2311
2312    /// The abstract-domain lattice laws `ColumnSpecs` relies on: `union` must
2313    /// over-approximate (contain everything either operand contains), and
2314    /// `intersect` must contain every value BOTH operands contain. An
2315    /// intersect-law violation is a false negative — a value the interpreter
2316    /// silently drops. The interpreter forms straddling `Within` ranges
2317    /// internally (e.g. `Values::union` of differently-typed endpoints in `cond`
2318    /// branches or `eq`), so this is not purely hypothetical. See
2319    /// database-issues#9656.
2320    #[mz_ore::test]
2321    #[cfg_attr(miri, ignore)] // error: unsupported operation: can't call foreign function `decContextDefault` on OS `linux`
2322    fn test_result_spec_lattice_laws() {
2323        // A recipe for a `ResultSpec`, carrying owned `PropDatum`s so the spec
2324        // (which borrows `Datum`s) can be materialized inside the test.
2325        #[derive(Debug, Clone)]
2326        enum Recipe {
2327            Nothing,
2328            Null,
2329            Fails,
2330            Anything,
2331            ValueAll,
2332            Value(PropDatum),
2333            Between(PropDatum, PropDatum),
2334            Map(Vec<(PropDatum, Recipe)>),
2335            Union(Box<Recipe>, Box<Recipe>),
2336        }
2337
2338        fn materialize(recipe: &Recipe) -> ResultSpec<'_> {
2339            match recipe {
2340                Recipe::Nothing => ResultSpec::nothing(),
2341                Recipe::Null => ResultSpec::null(),
2342                Recipe::Fails => ResultSpec::fails(),
2343                Recipe::Anything => ResultSpec::anything(),
2344                Recipe::ValueAll => ResultSpec::value_all(),
2345                Recipe::Value(pd) => ResultSpec::value(pd.into()),
2346                Recipe::Between(a, b) => {
2347                    let (a, b): (Datum, Datum) = (a.into(), b.into());
2348                    if a.is_null() || b.is_null() {
2349                        ResultSpec::nothing()
2350                    } else if a <= b {
2351                        ResultSpec::value_between(a, b)
2352                    } else {
2353                        ResultSpec::value_between(b, a)
2354                    }
2355                }
2356                Recipe::Map(entries) => {
2357                    let mut map = BTreeMap::new();
2358                    for (key, val) in entries {
2359                        let key: Datum = key.into();
2360                        if !key.is_null() {
2361                            map.insert(key, materialize(val));
2362                        }
2363                    }
2364                    ResultSpec::map_spec(map)
2365                }
2366                Recipe::Union(a, b) => materialize(a).union(materialize(b)),
2367            }
2368        }
2369
2370        fn recipe_strategy() -> impl Strategy<Value = Recipe> {
2371            let leaf = proptest::strategy::Union::new(vec![
2372                Just(Recipe::Nothing).boxed(),
2373                Just(Recipe::Null).boxed(),
2374                Just(Recipe::Fails).boxed(),
2375                Just(Recipe::Anything).boxed(),
2376                Just(Recipe::ValueAll).boxed(),
2377                mz_repr::arb_datum(false).prop_map(Recipe::Value).boxed(),
2378                (mz_repr::arb_datum(false), mz_repr::arb_datum(false))
2379                    .prop_map(|(a, b)| Recipe::Between(a, b))
2380                    .boxed(),
2381            ]);
2382            leaf.prop_recursive(3, 24, 4, |inner| {
2383                proptest::strategy::Union::new(vec![
2384                    prop::collection::vec((mz_repr::arb_datum(false), inner.clone()), 0..3)
2385                        .prop_map(Recipe::Map)
2386                        .boxed(),
2387                    (inner.clone(), inner.clone())
2388                        .prop_map(|(a, b)| Recipe::Union(Box::new(a), Box::new(b)))
2389                        .boxed(),
2390                ])
2391            })
2392        }
2393
2394        fn check(a: Recipe, b: Recipe, v: PropDatum) -> Result<(), TestCaseError> {
2395            let a_spec = materialize(&a);
2396            let b_spec = materialize(&b);
2397            let v: Datum = (&v).into();
2398
2399            let in_a = a_spec.may_contain(v);
2400            let in_b = b_spec.may_contain(v);
2401
2402            if in_a || in_b {
2403                prop_assert!(
2404                    a_spec.clone().union(b_spec.clone()).may_contain(v),
2405                    "union dropped a value: a={a:?} b={b:?} v={v:?}",
2406                );
2407            }
2408            if in_a && in_b {
2409                prop_assert!(
2410                    a_spec.intersect(b_spec).may_contain(v),
2411                    "intersect dropped a common value: a={a:?} b={b:?} v={v:?}",
2412                );
2413            }
2414            Ok(())
2415        }
2416
2417        proptest!(
2418            ProptestConfig::with_cases(4096),
2419            |(a in recipe_strategy(), b in recipe_strategy(), v in mz_repr::arb_datum(true))| {
2420                check(a, b, v)?;
2421            }
2422        );
2423    }
2424
2425    /// Deterministic regression test for database-issues#9656 (PER-50), the
2426    /// minimal case [`test_equivalence_ranges`] shrinks to.
2427    ///
2428    /// A numeric column whose stats range spans `[-Infinity, NaN]` (NaN is the
2429    /// maximum of the numeric Datum order) feeds `-column`. `NegNumeric` is
2430    /// declared monotone, so the interpreter would narrow the output to
2431    /// `[neg(-Infinity), neg(NaN)] = [Infinity, NaN]` and wrongly rule out the
2432    /// value `1` that the evaluator produces for the interior input `-1`. That
2433    /// false negative is exactly what makes persist filter pushdown discard a
2434    /// part it should keep, tripping the audit panic.
2435    #[mz_ore::test]
2436    #[cfg_attr(miri, ignore)] // error: unsupported operation: can't call foreign function `decContextDefault` on OS `linux`
2437    fn test_neg_numeric_nan_range() {
2438        use mz_repr::adt::numeric::Numeric;
2439
2440        let neg = MirScalarExpr::CallUnary {
2441            func: UnaryFunc::NegNumeric(NegNumeric),
2442            expr: Box::new(MirScalarExpr::column(0)),
2443        };
2444
2445        let relation = ReprRelationType::new(vec![ReprScalarType::Numeric.nullable(false)]);
2446        let arena = RowArena::new();
2447        let mut interpreter = ColumnSpecs::new(&relation, &arena);
2448        interpreter.push_column(
2449            0,
2450            ResultSpec::value_between(
2451                Datum::from(Numeric::from(f64::NEG_INFINITY)),
2452                Datum::from(Numeric::from(f64::NAN)),
2453            ),
2454        );
2455
2456        let spec = interpreter.expr(&neg);
2457
2458        // `-1` is an interior value of the input range, and `-(-1) = 1`.
2459        let actual = neg
2460            .eval(&[Datum::from(Numeric::from(-1.0f64))], &arena)
2461            .expect("eval succeeds");
2462        assert!(
2463            spec.range.may_contain(actual),
2464            "interpreter must not rule out {actual:?}, which the evaluator \
2465             produces for an interior input; got spec {:?}",
2466            spec.range,
2467        );
2468    }
2469
2470    /// Deterministic regression test for database-issues#9656 (PER-50), the
2471    /// fallibility variant [`test_equivalence_ranges`] also surfaces.
2472    ///
2473    /// `cast_numeric_to_mz_timestamp` is declared monotone but errors on
2474    /// fractional inputs, which are dense in the interior of any range. The
2475    /// range `[0, 2]` has integer endpoints that both cast cleanly, so the
2476    /// interpreter's endpoint sampling never observes an error. It must instead
2477    /// surface the function's own `could_error`, otherwise persist filter
2478    /// pushdown discards a part whose interior rows (e.g. `1.5`) produce error
2479    /// rows.
2480    #[mz_ore::test]
2481    #[cfg_attr(miri, ignore)] // error: unsupported operation: can't call foreign function `decContextDefault` on OS `linux`
2482    fn test_fallible_monotone_interior_error() {
2483        use mz_repr::adt::numeric::Numeric;
2484
2485        let cast = MirScalarExpr::CallUnary {
2486            func: UnaryFunc::CastNumericToMzTimestamp(CastNumericToMzTimestamp),
2487            expr: Box::new(MirScalarExpr::column(0)),
2488        };
2489
2490        let relation = ReprRelationType::new(vec![ReprScalarType::Numeric.nullable(false)]);
2491        let arena = RowArena::new();
2492        let mut interpreter = ColumnSpecs::new(&relation, &arena);
2493        interpreter.push_column(
2494            0,
2495            ResultSpec::value_between(
2496                Datum::from(Numeric::from(0.0f64)),
2497                Datum::from(Numeric::from(2.0f64)),
2498            ),
2499        );
2500
2501        let spec = interpreter.expr(&cast);
2502
2503        // `1.5` is an interior value of `[0, 2]`, and casting a fractional
2504        // numeric to mz_timestamp errors.
2505        let interior = Datum::from(Numeric::from(1.5f64));
2506        assert!(
2507            cast.eval(&[interior], &arena).is_err(),
2508            "precondition: a fractional numeric fails to cast to mz_timestamp",
2509        );
2510        assert!(
2511            spec.range.may_fail(),
2512            "interpreter must surface that a monotone-but-fallible function may \
2513             error on an interior value it never sampled; got spec {:?}",
2514            spec.range,
2515        );
2516    }
2517
2518    /// Regression test for database-issues#9656.
2519    ///
2520    /// The interpreter must surface the fallibility of MFP expressions that
2521    /// aren't referenced by any predicate or temporal bound. The runtime MFP
2522    /// evaluator runs every expression once predicates pass, so an expression
2523    /// that errors on the actual data makes the whole row an `Err` — and
2524    /// `filter_result` must keep the part to emit that error.
2525    #[mz_ore::test]
2526    #[cfg_attr(miri, ignore)]
2527    fn test_mfp_unreferenced_fallible_expression() {
2528        use crate::scalar::func::CastStringToUuid;
2529
2530        // MFP: one expression that always errors on the input range, and one
2531        // predicate that always passes. The expression's result column is
2532        // *not* referenced by the predicate, so the default interpreter
2533        // implementation would AND together just `True` and miss the
2534        // fallibility.
2535        let mfp = MapFilterProject {
2536            expressions: vec![MirScalarExpr::CallUnary {
2537                func: UnaryFunc::CastStringToUuid(CastStringToUuid),
2538                expr: Box::new(MirScalarExpr::column(0)),
2539            }],
2540            predicates: vec![(
2541                1,
2542                MirScalarExpr::literal_ok(Datum::True, ReprScalarType::Bool),
2543            )],
2544            projection: vec![0, 1],
2545            input_arity: 1,
2546        };
2547
2548        let relation = ReprRelationType::new(vec![ReprScalarType::String.nullable(false)]);
2549        let arena = RowArena::new();
2550        let mut interpreter = ColumnSpecs::new(&relation, &arena);
2551        // "not-a-uuid" is in the stats range and definitely doesn't parse as a UUID.
2552        interpreter.push_column(
2553            0,
2554            ResultSpec::value_between(Datum::String("not-a-uuid"), Datum::String("not-a-uuid")),
2555        );
2556        let spec = interpreter.mfp_filter(&mfp);
2557        assert!(
2558            spec.range.may_fail(),
2559            "an MFP expression that errors on the stats range must propagate \
2560             fallibility, otherwise persist filter pushdown can wrongly discard \
2561             a part that produces error rows",
2562        );
2563    }
2564
2565    /// Proptest companion to [`test_mfp_unreferenced_fallible_expression`]:
2566    /// directly verifies the fallibility claim of [`ColumnSpecs::mfp_filter`]
2567    /// against the runtime MFP semantics. For a random expression placed in
2568    /// `MapFilterProject::expressions` (i.e. as an unreferenced Map step), if
2569    /// evaluating the expression on a row drawn from the stats range produces
2570    /// an error at runtime, then the interpreter's summary must report
2571    /// `may_fail()`. Without the `expressions.any(|s| s.range.fallible)` patch
2572    /// in `mfp_filter`, the AND over an empty predicate list collapses to
2573    /// `True` and the runtime error is wrongly ruled out.
2574    #[mz_ore::test]
2575    #[cfg_attr(miri, ignore)]
2576    fn test_mfp_filter_fallibility_equivalence() {
2577        fn check(data: ExpressionData) -> Result<(), TestCaseError> {
2578            let ExpressionData {
2579                relation_type,
2580                specs,
2581                rows,
2582                expr,
2583            } = data;
2584
2585            let input_arity = relation_type.column_types.len();
2586            let mfp = MapFilterProject {
2587                expressions: vec![expr.clone()],
2588                predicates: vec![],
2589                projection: (0..input_arity).collect(),
2590                input_arity,
2591            };
2592
2593            let arena = RowArena::new();
2594            let mut interpreter = ColumnSpecs::new(&relation_type, &arena);
2595            for (id, spec) in specs.into_iter().enumerate() {
2596                interpreter.push_column(id, spec);
2597            }
2598            let summary = interpreter.mfp_filter(&mfp);
2599
2600            for row in &rows {
2601                let datums: Vec<_> = row.iter().collect();
2602                if expr.eval(&datums, &arena).is_err() {
2603                    prop_assert!(
2604                        summary.range.may_fail(),
2605                        "mfp_filter must surface the fallibility of an \
2606                         unreferenced MFP expression: row {:?} errored at \
2607                         runtime but the interpreter ruled out errors",
2608                        row,
2609                    );
2610                }
2611            }
2612            Ok(())
2613        }
2614
2615        proptest!(|(data in gen_expr_data())| {
2616            check(data)?;
2617        });
2618    }
2619
2620    #[mz_ore::test]
2621    fn test_mfp() {
2622        // Regression test for https://github.com/MaterializeInc/database-issues/issues/5736
2623        use MirScalarExpr::*;
2624
2625        let mfp = MapFilterProject {
2626            expressions: vec![],
2627            predicates: vec![
2628                // Always fails on the known input range
2629                (
2630                    1,
2631                    CallUnary {
2632                        func: UnaryFunc::IsNull(IsNull),
2633                        expr: Box::new(CallBinary {
2634                            func: MulInt32.into(),
2635                            expr1: Box::new(MirScalarExpr::column(0)),
2636                            expr2: Box::new(MirScalarExpr::column(0)),
2637                        }),
2638                    },
2639                ),
2640                // Always returns false on the known input range
2641                (
2642                    1,
2643                    CallBinary {
2644                        func: Eq.into(),
2645                        expr1: Box::new(MirScalarExpr::column(0)),
2646                        expr2: Box::new(MirScalarExpr::literal_ok(
2647                            Datum::Int32(1727694505),
2648                            ReprScalarType::Int32,
2649                        )),
2650                    },
2651                ),
2652            ],
2653            projection: vec![],
2654            input_arity: 1,
2655        };
2656
2657        let relation = ReprRelationType::new(vec![ReprScalarType::Int32.nullable(true)]);
2658        let arena = RowArena::new();
2659        let mut interpreter = ColumnSpecs::new(&relation, &arena);
2660        interpreter.push_column(0, ResultSpec::value(Datum::Int32(-1294725158)));
2661        let spec = interpreter.mfp_filter(&mfp);
2662        assert!(spec.range.may_fail());
2663    }
2664
2665    #[mz_ore::test]
2666    fn test_concat() {
2667        let expr = MirScalarExpr::call_variadic(
2668            Concat,
2669            vec![
2670                MirScalarExpr::column(0),
2671                MirScalarExpr::literal_ok(Datum::String("a"), ReprScalarType::String),
2672                MirScalarExpr::literal_ok(Datum::String("b"), ReprScalarType::String),
2673            ],
2674        );
2675
2676        let relation = ReprRelationType::new(vec![ReprScalarType::String.nullable(false)]);
2677        let arena = RowArena::new();
2678        let interpreter = ColumnSpecs::new(&relation, &arena);
2679        let spec = interpreter.expr(&expr);
2680        assert!(spec.range.may_contain(Datum::String("blab")));
2681    }
2682
2683    #[mz_ore::test]
2684    fn test_eval_range() {
2685        // Example inspired by the tumbling windows temporal filter in the docs.
2686        //
2687        // NOTE: `may_fail()` is `true` in both cases below. `DivInt64`,
2688        // `MulInt64`, and `CastInt64ToMzTimestamp` can all error, and the
2689        // interpreter no longer infers infallibility from endpoint sampling for
2690        // an erroring function over a multi-valued range (it cannot prove the
2691        // function doesn't error on an interior value). For this data none of
2692        // them actually errors, so this is a conservative over-approximation
2693        // that keeps the part rather than pruning it. The value channel is still
2694        // narrowed precisely (`may_contain` below is exact), so a follow-up that
2695        // makes the fallibility flag argument-aware could recover pruning here.
2696        let period_ms = MirScalarExpr::literal_ok(Datum::Int64(10), ReprScalarType::Int64);
2697        let expr = MirScalarExpr::CallBinary {
2698            func: Gte.into(),
2699            expr1: Box::new(MirScalarExpr::CallUnmaterializable(
2700                UnmaterializableFunc::MzNow,
2701            )),
2702            expr2: Box::new(MirScalarExpr::CallUnary {
2703                func: UnaryFunc::CastInt64ToMzTimestamp(CastInt64ToMzTimestamp),
2704                expr: Box::new(MirScalarExpr::CallBinary {
2705                    func: MulInt64.into(),
2706                    expr1: Box::new(period_ms.clone()),
2707                    expr2: Box::new(MirScalarExpr::CallBinary {
2708                        func: DivInt64.into(),
2709                        expr1: Box::new(MirScalarExpr::column(0)),
2710                        expr2: Box::new(period_ms),
2711                    }),
2712                }),
2713            }),
2714        };
2715        let relation = ReprRelationType::new(vec![ReprScalarType::Int64.nullable(false)]);
2716
2717        {
2718            // Non-overlapping windows
2719            let arena = RowArena::new();
2720            let mut interpreter = ColumnSpecs::new(&relation, &arena);
2721            interpreter.push_unmaterializable(
2722                UnmaterializableFunc::MzNow,
2723                ResultSpec::value_between(
2724                    Datum::MzTimestamp(10.into()),
2725                    Datum::MzTimestamp(20.into()),
2726                ),
2727            );
2728            interpreter.push_column(0, ResultSpec::value_between(30i64.into(), 40i64.into()));
2729
2730            let range_out = interpreter.expr(&expr).range;
2731            assert!(range_out.may_contain(Datum::False));
2732            assert!(!range_out.may_contain(Datum::True));
2733            assert!(!range_out.may_contain(Datum::Null));
2734            assert!(range_out.may_fail());
2735        }
2736
2737        {
2738            // Overlapping windows
2739            let arena = RowArena::new();
2740            let mut interpreter = ColumnSpecs::new(&relation, &arena);
2741            interpreter.push_unmaterializable(
2742                UnmaterializableFunc::MzNow,
2743                ResultSpec::value_between(
2744                    Datum::MzTimestamp(10.into()),
2745                    Datum::MzTimestamp(35.into()),
2746                ),
2747            );
2748            interpreter.push_column(0, ResultSpec::value_between(30i64.into(), 40i64.into()));
2749
2750            let range_out = interpreter.expr(&expr).range;
2751            assert!(range_out.may_contain(Datum::False));
2752            assert!(range_out.may_contain(Datum::True));
2753            assert!(!range_out.may_contain(Datum::Null));
2754            assert!(range_out.may_fail());
2755        }
2756    }
2757
2758    #[mz_ore::test]
2759    #[cfg_attr(miri, ignore)] // error: unsupported operation: can't call foreign function `decNumberFromInt32` on OS `linux`
2760    fn test_jsonb() {
2761        let arena = RowArena::new();
2762
2763        let expr = MirScalarExpr::column(0)
2764            .call_binary(
2765                MirScalarExpr::literal_ok(Datum::from("ts"), ReprScalarType::String),
2766                JsonbGetString,
2767            )
2768            .call_unary(CastJsonbToNumeric(None));
2769
2770        let relation = ReprRelationType::new(vec![ReprScalarType::Jsonb.nullable(true)]);
2771        let mut interpreter = ColumnSpecs::new(&relation, &arena);
2772        interpreter.push_column(
2773            0,
2774            ResultSpec::map_spec(
2775                [(
2776                    "ts".into(),
2777                    ResultSpec::value_between(
2778                        Datum::Numeric(100.into()),
2779                        Datum::Numeric(300.into()),
2780                    ),
2781                )]
2782                .into_iter()
2783                .collect(),
2784            ),
2785        );
2786
2787        let range_out = interpreter.expr(&expr).range;
2788        assert!(!range_out.may_contain(Datum::Numeric(0.into())));
2789        assert!(range_out.may_contain(Datum::Numeric(200.into())));
2790        assert!(!range_out.may_contain(Datum::Numeric(400.into())));
2791    }
2792
2793    #[mz_ore::test]
2794    fn test_nested_union_partial_overlap() {
2795        // `Nested(map)` constrains a key only when the key is present in `map`; absent
2796        // keys mean "anything". So the union of two Nested specs must drop any key
2797        // that's missing from one side, because `x ∪ anything = anything`. Only keys
2798        // present in *both* sides survive (with their per-key specs unioned).
2799        let a = ResultSpec::map_spec(
2800            [
2801                ("x".into(), ResultSpec::value(Datum::String("a"))),
2802                ("y".into(), ResultSpec::value(Datum::String("b"))),
2803                ("c".into(), ResultSpec::value(Datum::String("c"))),
2804            ]
2805            .into_iter()
2806            .collect(),
2807        );
2808        let b = ResultSpec::map_spec(
2809            [
2810                ("x".into(), ResultSpec::value(Datum::String("a2"))),
2811                ("y".into(), ResultSpec::value(Datum::String("b2"))),
2812                ("z".into(), ResultSpec::value(Datum::String("z"))),
2813            ]
2814            .into_iter()
2815            .collect(),
2816        );
2817
2818        let unioned = a.union(b);
2819
2820        // Push the unioned spec through `->> <key>`: keys only in one side must
2821        // admit NULL (the other side is unconstrained, so the field could be absent
2822        // there); shared keys must include both observed values.
2823        let arena = RowArena::new();
2824        let relation = ReprRelationType::new(vec![ReprScalarType::Jsonb.nullable(false)]);
2825
2826        // Key only in `a`: the union must admit NULL.
2827        {
2828            let mut interpreter = ColumnSpecs::new(&relation, &arena);
2829            interpreter.push_column(0, unioned.clone());
2830            let expr = MirScalarExpr::column(0).call_binary(
2831                MirScalarExpr::literal_ok(Datum::from("c"), ReprScalarType::String),
2832                JsonbGetStringStringify,
2833            );
2834            assert!(interpreter.expr(&expr).range.may_contain(Datum::Null));
2835        }
2836
2837        // Key only in `b`: symmetric.
2838        {
2839            let mut interpreter = ColumnSpecs::new(&relation, &arena);
2840            interpreter.push_column(0, unioned.clone());
2841            let expr = MirScalarExpr::column(0).call_binary(
2842                MirScalarExpr::literal_ok(Datum::from("z"), ReprScalarType::String),
2843                JsonbGetStringStringify,
2844            );
2845            assert!(interpreter.expr(&expr).range.may_contain(Datum::Null));
2846        }
2847
2848        // Key in both: result must include both observed values.
2849        {
2850            let mut interpreter = ColumnSpecs::new(&relation, &arena);
2851            interpreter.push_column(0, unioned);
2852            let expr = MirScalarExpr::column(0).call_binary(
2853                MirScalarExpr::literal_ok(Datum::from("x"), ReprScalarType::String),
2854                JsonbGetStringStringify,
2855            );
2856            let x_range = interpreter.expr(&expr).range;
2857            assert!(x_range.may_contain(Datum::String("a")));
2858            assert!(x_range.may_contain(Datum::String("a2")));
2859        }
2860    }
2861
2862    #[mz_ore::test]
2863    #[cfg_attr(miri, ignore)] // unsupported foreign call in numeric decoding
2864    fn test_case_over_jsonb_columns() {
2865        // Regression test for PER-6: when CASE picks between two JSON columns whose
2866        // observed keys are disjoint, filter pushdown must not prune parts where
2867        // accessing a key only present in one branch might yield NULL in the other.
2868        let arena = RowArena::new();
2869
2870        // `(CASE WHEN col0 THEN col1 ELSE col2 END) ->> 'y' IS NULL`
2871        let expr = MirScalarExpr::If {
2872            cond: Box::new(MirScalarExpr::column(0)),
2873            then: Box::new(MirScalarExpr::column(1)),
2874            els: Box::new(MirScalarExpr::column(2)),
2875        }
2876        .call_binary(
2877            MirScalarExpr::literal_ok(Datum::from("y"), ReprScalarType::String),
2878            JsonbGetStringStringify,
2879        )
2880        .call_unary(UnaryFunc::IsNull(IsNull));
2881
2882        let relation = ReprRelationType::new(vec![
2883            ReprScalarType::Bool.nullable(false),
2884            ReprScalarType::Jsonb.nullable(false),
2885            ReprScalarType::Jsonb.nullable(false),
2886        ]);
2887        let mut interpreter = ColumnSpecs::new(&relation, &arena);
2888        interpreter.push_column(0, ResultSpec::value_between(Datum::False, Datum::True));
2889        interpreter.push_column(
2890            1,
2891            ResultSpec::map_spec(
2892                [("x".into(), ResultSpec::value(Datum::String("a")))]
2893                    .into_iter()
2894                    .collect(),
2895            ),
2896        );
2897        interpreter.push_column(
2898            2,
2899            ResultSpec::map_spec(
2900                [("y".into(), ResultSpec::value(Datum::String("b")))]
2901                    .into_iter()
2902                    .collect(),
2903            ),
2904        );
2905
2906        let range_out = interpreter.expr(&expr).range;
2907        // When the CASE selects column 1, "y" is absent and `->> 'y'` yields NULL, so
2908        // `IS NULL` is True. The filter must not prune a part that could match.
2909        assert!(range_out.may_contain(Datum::True));
2910    }
2911
2912    #[mz_ore::test]
2913    fn test_like() {
2914        let arena = RowArena::new();
2915
2916        let expr = MirScalarExpr::CallUnary {
2917            func: UnaryFunc::IsLikeMatch(IsLikeMatch(
2918                crate::like_pattern::compile("%whatever%", true).unwrap(),
2919            )),
2920            expr: Box::new(MirScalarExpr::column(0)),
2921        };
2922
2923        let relation = ReprRelationType::new(vec![ReprScalarType::String.nullable(true)]);
2924        let mut interpreter = ColumnSpecs::new(&relation, &arena);
2925        interpreter.push_column(
2926            0,
2927            ResultSpec::value_between(Datum::String("aardvark"), Datum::String("zebra")),
2928        );
2929
2930        let range_out = interpreter.expr(&expr).range;
2931        assert!(
2932            !range_out.fallible,
2933            "like function should not error on non-error input"
2934        );
2935        assert!(range_out.may_contain(Datum::True));
2936        assert!(range_out.may_contain(Datum::False));
2937        assert!(range_out.may_contain(Datum::Null));
2938    }
2939
2940    #[mz_ore::test]
2941    fn test_try_parse_monotonic_iso8601_timestamp() {
2942        use chrono::NaiveDateTime;
2943
2944        let arena = RowArena::new();
2945
2946        let expr = MirScalarExpr::CallUnary {
2947            func: UnaryFunc::TryParseMonotonicIso8601Timestamp(TryParseMonotonicIso8601Timestamp),
2948            expr: Box::new(MirScalarExpr::column(0)),
2949        };
2950
2951        let relation = ReprRelationType::new(vec![ReprScalarType::String.nullable(true)]);
2952        // Test the case where we have full timestamps as bounds.
2953        let mut interpreter = ColumnSpecs::new(&relation, &arena);
2954        interpreter.push_column(
2955            0,
2956            ResultSpec::value_between(
2957                Datum::String("2024-01-11T00:00:00.000Z"),
2958                Datum::String("2024-01-11T20:00:00.000Z"),
2959            ),
2960        );
2961
2962        let timestamp = |ts| {
2963            Datum::Timestamp(
2964                NaiveDateTime::parse_from_str(ts, "%Y-%m-%dT%H:%M:%S")
2965                    .unwrap()
2966                    .try_into()
2967                    .unwrap(),
2968            )
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 we have truncated / useless bounds.
2979        let mut interpreter = ColumnSpecs::new(&relation, &arena);
2980        interpreter.push_column(
2981            0,
2982            ResultSpec::value_between(Datum::String("2024-01-1"), Datum::String("2024-01-2")),
2983        );
2984
2985        let range_out = interpreter.expr(&expr).range;
2986        assert!(!range_out.fallible);
2987        assert!(range_out.nullable);
2988        assert!(range_out.may_contain(timestamp("2024-01-10T10:00:00")));
2989        assert!(range_out.may_contain(timestamp("2024-01-11T10:00:00")));
2990        assert!(range_out.may_contain(timestamp("2024-01-12T10:00:00")));
2991
2992        // Test the case where only one bound is truncated
2993        let mut interpreter = ColumnSpecs::new(&relation, &arena);
2994        interpreter.push_column(
2995            0,
2996            ResultSpec::value_between(
2997                Datum::String("2024-01-1"),
2998                Datum::String("2024-01-12T10:00:00"),
2999            )
3000            .union(ResultSpec::null()),
3001        );
3002
3003        let range_out = interpreter.expr(&expr).range;
3004        assert!(!range_out.fallible);
3005        assert!(range_out.nullable);
3006        assert!(range_out.may_contain(timestamp("2024-01-10T10:00:00")));
3007        assert!(range_out.may_contain(timestamp("2024-01-11T10:00:00")));
3008        assert!(range_out.may_contain(timestamp("2024-01-12T10:00:00")));
3009
3010        // Test the case where the upper and lower bound are identical
3011        let mut interpreter = ColumnSpecs::new(&relation, &arena);
3012        interpreter.push_column(
3013            0,
3014            ResultSpec::value_between(
3015                Datum::String("2024-01-11T10:00:00.000Z"),
3016                Datum::String("2024-01-11T10:00:00.000Z"),
3017            ),
3018        );
3019
3020        let range_out = interpreter.expr(&expr).range;
3021        assert!(!range_out.fallible);
3022        assert!(!range_out.nullable);
3023        assert!(!range_out.may_contain(timestamp("2024-01-10T10:00:00")));
3024        assert!(range_out.may_contain(timestamp("2024-01-11T10:00:00")));
3025        assert!(!range_out.may_contain(timestamp("2024-01-12T10:00:00")));
3026    }
3027
3028    #[mz_ore::test]
3029    fn test_inequality() {
3030        let arena = RowArena::new();
3031
3032        let expr = MirScalarExpr::column(0).call_binary(
3033            MirScalarExpr::CallUnmaterializable(UnmaterializableFunc::MzNow),
3034            Gte,
3035        );
3036
3037        let relation = ReprRelationType::new(vec![ReprScalarType::MzTimestamp.nullable(true)]);
3038        let mut interpreter = ColumnSpecs::new(&relation, &arena);
3039        interpreter.push_column(
3040            0,
3041            ResultSpec::value_between(
3042                Datum::MzTimestamp(1704736444949u64.into()),
3043                Datum::MzTimestamp(1704736444949u64.into()),
3044            )
3045            .union(ResultSpec::null()),
3046        );
3047        interpreter.push_unmaterializable(
3048            UnmaterializableFunc::MzNow,
3049            ResultSpec::value_between(
3050                Datum::MzTimestamp(1704738791000u64.into()),
3051                Datum::MzTimestamp(18446744073709551615u64.into()),
3052            ),
3053        );
3054
3055        let range_out = interpreter.expr(&expr).range;
3056        assert!(
3057            !range_out.fallible,
3058            "<= function should not error on non-error input"
3059        );
3060        assert!(!range_out.may_contain(Datum::True));
3061        assert!(range_out.may_contain(Datum::False));
3062        assert!(range_out.may_contain(Datum::Null));
3063    }
3064
3065    /// Regression test for database-issues#9656.
3066    ///
3067    /// Adding an `Interval` to a `Timestamp` is non-monotone in the interval
3068    /// argument: the lex order of intervals (months, days, micros) does not
3069    /// respect calendar-month arithmetic with day-clamping. The interpreter
3070    /// must therefore not assume monotonicity, otherwise persist filter
3071    /// pushdown can incorrectly conclude that a part has no matching rows.
3072    #[mz_ore::test]
3073    #[cfg_attr(miri, ignore)]
3074    fn test_add_timestamp_interval_non_monotone() {
3075        use chrono::NaiveDateTime;
3076        use mz_repr::adt::interval::Interval;
3077        use mz_repr::adt::timestamp::CheckedTimestamp;
3078        use mz_repr::{Datum, Row};
3079
3080        let arena = RowArena::new();
3081
3082        // The setup: a timestamp literal `t = 2024-01-31 00:00:00`, and an
3083        // interval column whose stats-range spans
3084        // `[{0 months, 31 days, 0 us}, {1 month, 0 days, 0 us}]`. In lex order,
3085        // the 31-day interval is the lower bound and the 1-month interval is
3086        // the upper bound. The function values at the endpoints are:
3087        //   t + {0,31,0} = 2024-03-02
3088        //   t + {1, 0,0} = 2024-02-29
3089        // But an *interior* interval like {0, 60, 0} maps to 2024-03-31, which
3090        // lies far outside `[Feb 29, Mar 2]`. Under the (incorrect) monotone
3091        // assumption, the interpreter would conclude the output is in that
3092        // narrow window, and rule out predicates like `>= 2024-03-15`.
3093        let ts_lit = |s: &str| {
3094            let mut row = Row::default();
3095            row.packer().push(Datum::Timestamp(
3096                CheckedTimestamp::from_timestamplike(
3097                    NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S").unwrap(),
3098                )
3099                .unwrap(),
3100            ));
3101            MirScalarExpr::Literal(Ok(row), ReprScalarType::Timestamp.nullable(false))
3102        };
3103        let interval = |months: i32, days: i32, micros: i64| {
3104            Datum::Interval(Interval {
3105                months,
3106                days,
3107                micros,
3108            })
3109        };
3110
3111        // Expression: `(timestamp_lit + interval_col) >= 2024-03-15`.
3112        let expr = ts_lit("2024-01-31T00:00:00")
3113            .call_binary(MirScalarExpr::column(0), AddTimestampInterval)
3114            .call_binary(ts_lit("2024-03-15T00:00:00"), Gte);
3115
3116        let relation = ReprRelationType::new(vec![ReprScalarType::Interval.nullable(false)]);
3117        let mut interpreter = ColumnSpecs::new(&relation, &arena);
3118        interpreter.push_column(
3119            0,
3120            ResultSpec::value_between(interval(0, 31, 0), interval(1, 0, 0)),
3121        );
3122
3123        let range_out = interpreter.expr(&expr).range;
3124        // The actual data may include e.g. `{0, 60, 0}` → 2024-03-31, which
3125        // satisfies `>= 2024-03-15`. The interpreter must admit `True` so that
3126        // filter pushdown does not skip the part. Under the buggy
3127        // `(true, true)` annotation, the output range would be
3128        // `[Feb 29, Mar 2]`, all of which is `< Mar 15`, and the interpreter
3129        // would (wrongly) admit only `False`.
3130        assert!(
3131            range_out.may_contain(Datum::True),
3132            "interpreter incorrectly ruled out matching rows; \
3133             add_timestamp_interval is not monotone in the interval argument",
3134        );
3135    }
3136
3137    /// Companion test to `test_add_timestamp_interval_non_monotone`: when the
3138    /// interval argument is a literal with `months == 0`, the function reduces
3139    /// to a pure linear shift in microseconds and *is* monotone in the
3140    /// timestamp. The dynamic-monotonicity handler in `AbstractFunc` should
3141    /// recover the tight output range in that case, so that filter pushdown
3142    /// can still narrow predicates like `t - INTERVAL '1' day < literal`.
3143    #[mz_ore::test]
3144    #[cfg_attr(miri, ignore)]
3145    fn test_timestamp_plus_interval_dynamic_monotone() {
3146        use chrono::NaiveDateTime;
3147        use mz_repr::adt::interval::Interval;
3148        use mz_repr::adt::timestamp::CheckedTimestamp;
3149        use mz_repr::{Datum, Row};
3150
3151        let arena = RowArena::new();
3152
3153        let ts = |s: &str| {
3154            Datum::Timestamp(
3155                CheckedTimestamp::from_timestamplike(
3156                    NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S").unwrap(),
3157                )
3158                .unwrap(),
3159            )
3160        };
3161        let interval_lit = |months: i32, days: i32, micros: i64| {
3162            let mut row = Row::default();
3163            row.packer().push(Datum::Interval(Interval {
3164                months,
3165                days,
3166                micros,
3167            }));
3168            MirScalarExpr::Literal(Ok(row), ReprScalarType::Interval.nullable(false))
3169        };
3170
3171        let relation = ReprRelationType::new(vec![ReprScalarType::Timestamp.nullable(false)]);
3172
3173        // (a) `t_col - INTERVAL '1' day < 2024-01-15`, with `t_col` ranging
3174        // over `[2024-01-15, 2024-01-20]`. With the days-only interval, the
3175        // subtraction is monotone, so endpoints alone determine the output:
3176        // [2024-01-14, 2024-01-19]. Only `2024-01-14` satisfies `< 2024-01-15`,
3177        // so both True and False are reachable.
3178        {
3179            let expr = MirScalarExpr::column(0)
3180                .call_binary(interval_lit(0, 1, 0), SubTimestampInterval)
3181                .call_binary(
3182                    MirScalarExpr::Literal(
3183                        Ok({
3184                            let mut r = Row::default();
3185                            r.packer().push(ts("2024-01-15T00:00:00"));
3186                            r
3187                        }),
3188                        ReprScalarType::Timestamp.nullable(false),
3189                    ),
3190                    Lt,
3191                );
3192            let mut interpreter = ColumnSpecs::new(&relation, &arena);
3193            interpreter.push_column(
3194                0,
3195                ResultSpec::value_between(ts("2024-01-15T00:00:00"), ts("2024-01-20T00:00:00")),
3196            );
3197            let range_out = interpreter.expr(&expr).range;
3198            assert!(
3199                range_out.may_contain(Datum::True),
3200                "day-only interval should preserve tight bounds",
3201            );
3202            assert!(
3203                range_out.may_contain(Datum::False),
3204                "day-only interval should preserve tight bounds",
3205            );
3206        }
3207
3208        // (b) Same predicate, but with `t_col` strictly *after* the literal:
3209        // `[2024-01-17, 2024-01-20]`. Output of `t - 1 day`:
3210        // `[2024-01-16, 2024-01-19]`, none of which is `< 2024-01-15`. The
3211        // interpreter must rule out `True`.
3212        {
3213            let expr = MirScalarExpr::column(0)
3214                .call_binary(interval_lit(0, 1, 0), SubTimestampInterval)
3215                .call_binary(
3216                    MirScalarExpr::Literal(
3217                        Ok({
3218                            let mut r = Row::default();
3219                            r.packer().push(ts("2024-01-15T00:00:00"));
3220                            r
3221                        }),
3222                        ReprScalarType::Timestamp.nullable(false),
3223                    ),
3224                    Lt,
3225                );
3226            let mut interpreter = ColumnSpecs::new(&relation, &arena);
3227            interpreter.push_column(
3228                0,
3229                ResultSpec::value_between(ts("2024-01-17T00:00:00"), ts("2024-01-20T00:00:00")),
3230            );
3231            let range_out = interpreter.expr(&expr).range;
3232            assert!(
3233                !range_out.may_contain(Datum::True),
3234                "day-only interval should narrow out impossible matches",
3235            );
3236        }
3237
3238        // (c) With a *month*-bearing literal interval, the operation is no
3239        // longer monotone (day-clamping), so the dynamic-monotonicity handler
3240        // must fall back to `anything()` — the interpreter cannot rule out
3241        // either outcome even when the column range is narrow.
3242        {
3243            let expr = MirScalarExpr::column(0)
3244                .call_binary(interval_lit(1, 0, 0), SubTimestampInterval)
3245                .call_binary(
3246                    MirScalarExpr::Literal(
3247                        Ok({
3248                            let mut r = Row::default();
3249                            r.packer().push(ts("2024-01-15T00:00:00"));
3250                            r
3251                        }),
3252                        ReprScalarType::Timestamp.nullable(false),
3253                    ),
3254                    Lt,
3255                );
3256            let mut interpreter = ColumnSpecs::new(&relation, &arena);
3257            interpreter.push_column(
3258                0,
3259                ResultSpec::value_between(ts("2024-01-17T00:00:00"), ts("2024-01-20T00:00:00")),
3260            );
3261            let range_out = interpreter.expr(&expr).range;
3262            assert!(
3263                range_out.may_contain(Datum::True),
3264                "month-bearing interval must conservatively admit True",
3265            );
3266            assert!(
3267                range_out.may_contain(Datum::False),
3268                "month-bearing interval must conservatively admit False",
3269            );
3270        }
3271    }
3272
3273    /// Proptest companion to [`test_timestamp_plus_interval_dynamic_monotone`]:
3274    /// the dynamic-monotonicity handler in [`AbstractFunc`] claims that
3275    /// `add_timestamp_interval(t, i)` is monotone in `t` whenever `i.months == 0`
3276    /// (the only case it actually claims monotonicity for at runtime: the
3277    /// matches above require the right argument to be a single value with
3278    /// `months == 0`). This proptest verifies that claim directly against the
3279    /// function impl by sampling random timestamps and zero-month intervals
3280    /// and checking that input ordering is preserved in the output.
3281    #[mz_ore::test]
3282    #[cfg_attr(miri, ignore)]
3283    fn proptest_timestamp_plus_interval_monotone_when_months_zero() {
3284        use mz_repr::adt::interval::Interval;
3285        use mz_repr::{Datum, RowArena, SqlScalarType, arb_datum_for_scalar};
3286        use proptest::prelude::*;
3287
3288        let timestamp_strat = || arb_datum_for_scalar(SqlScalarType::Timestamp { precision: None });
3289        // Lex order on `Interval` does *not* match total-microseconds order when
3290        // both days and micros vary independently (e.g. `{0, 0, 86_400_000_001}`
3291        // is lex-less than `{0, 1, 0}` but evaluates to a strictly larger
3292        // timestamp), so we only claim monotonicity for *fixed* zero-month
3293        // intervals — which is exactly what the DynamicMonotone handler does.
3294        // The proptest accordingly varies `t` with `i` held constant.
3295        let zero_month_interval_strat =
3296            (any::<i32>(), any::<i64>()).prop_map(|(days, micros)| Interval {
3297                months: 0,
3298                days,
3299                micros,
3300            });
3301
3302        let expr = MirScalarExpr::CallBinary {
3303            func: AddTimestampInterval.into(),
3304            expr1: Box::new(MirScalarExpr::column(0)),
3305            expr2: Box::new(MirScalarExpr::column(1)),
3306        };
3307        let arena = RowArena::new();
3308
3309        proptest!(|(
3310            t1 in timestamp_strat(),
3311            t2 in timestamp_strat(),
3312            i in zero_month_interval_strat,
3313        )| {
3314            let t1 = match t1 { PropDatum::Timestamp(t) => t, _ => unreachable!() };
3315            let t2 = match t2 { PropDatum::Timestamp(t) => t, _ => unreachable!() };
3316            let i = Datum::Interval(i);
3317            let r1 = expr.eval(&[Datum::Timestamp(t1), i], &arena);
3318            let r2 = expr.eval(&[Datum::Timestamp(t2), i], &arena);
3319            // Only compare when both calls succeed; the monotonicity claim
3320            // applies only within the success domain.
3321            if let (Ok(Datum::Timestamp(r1)), Ok(Datum::Timestamp(r2))) = (r1, r2) {
3322                prop_assert_eq!(t1.cmp(&t2), r1.cmp(&r2));
3323            }
3324        });
3325    }
3326
3327    /// Regression test for `date_bin_timestamp`, which is non-monotone in the
3328    /// `stride` argument: a larger stride can bin a source timestamp to an
3329    /// *earlier* result than a smaller stride, because the bin alignment to
3330    /// the unix epoch depends on the stride magnitude rather than on lex order.
3331    #[mz_ore::test]
3332    #[cfg_attr(miri, ignore)]
3333    fn test_date_bin_timestamp_non_monotone() {
3334        use chrono::NaiveDateTime;
3335        use mz_repr::adt::interval::Interval;
3336        use mz_repr::adt::timestamp::CheckedTimestamp;
3337        use mz_repr::{Datum, Row};
3338
3339        let arena = RowArena::new();
3340
3341        let ts_lit = |s: &str| {
3342            let mut row = Row::default();
3343            row.packer().push(Datum::Timestamp(
3344                CheckedTimestamp::from_timestamplike(
3345                    NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S").unwrap(),
3346                )
3347                .unwrap(),
3348            ));
3349            MirScalarExpr::Literal(Ok(row), ReprScalarType::Timestamp.nullable(false))
3350        };
3351        let interval = |months: i32, days: i32, micros: i64| {
3352            Datum::Interval(Interval {
3353                months,
3354                days,
3355                micros,
3356            })
3357        };
3358
3359        // Expression: `date_bin(stride_col, 2024-01-01 12:00:00) > 2024-01-01 06:00:00`.
3360        // stride_col ranges over `[1 day, 2 days]`.
3361        //
3362        // Endpoint evaluations:
3363        //   1 day stride → bins to 2024-01-01 00:00:00
3364        //   2 day stride → bins to 2023-12-31 00:00:00
3365        //
3366        // Interior strides produce results *outside* that endpoint box. For
3367        // example, a 1.5-day stride (i.e. `{0 months, 1 day, 12 h micros}`,
3368        // which sorts between the two endpoints in lex order) bins
3369        // 2024-01-01 12:00:00 to exactly 2024-01-01 12:00:00 — well above the
3370        // endpoint maximum of 2024-01-01 00:00:00. With the buggy
3371        // `(true, true)` annotation, the interpreter narrows the output to
3372        // `[Dec 31 00:00, Jan 1 00:00]`, both of which are `<= Jan 1 06:00`,
3373        // so the predicate is wrongly proved `False`. With the non-monotone
3374        // fix the output is `anything()`, so `True` is correctly admitted.
3375        let expr = MirScalarExpr::column(0)
3376            .call_binary(ts_lit("2024-01-01T12:00:00"), DateBinTimestamp)
3377            .call_binary(ts_lit("2024-01-01T06:00:00"), Gt);
3378
3379        let relation = ReprRelationType::new(vec![ReprScalarType::Interval.nullable(false)]);
3380        let mut interpreter = ColumnSpecs::new(&relation, &arena);
3381        interpreter.push_column(
3382            0,
3383            ResultSpec::value_between(interval(0, 1, 0), interval(0, 2, 0)),
3384        );
3385
3386        let range_out = interpreter.expr(&expr).range;
3387        assert!(
3388            range_out.may_contain(Datum::True),
3389            "date_bin is not monotone in the stride argument; \
3390             interior strides can produce outputs outside the endpoint-bounded \
3391             box, so the interpreter must admit True for `>`-style predicates",
3392        );
3393    }
3394
3395    /// `round` must not depend on a numeric's exponent: `Row` encoding folds
3396    /// trailing zeroes into it, so the interpreter, which reads its datums back
3397    /// out of a `Row`, would otherwise disagree with the evaluator and pushdown
3398    /// could discard a part it has to keep.
3399    #[mz_ore::test]
3400    #[cfg_attr(miri, ignore)]
3401    fn test_round_numeric_representation_independent() {
3402        use mz_repr::adt::date::Date;
3403
3404        let arena = RowArena::new();
3405        let lit = |d: Datum, ty: ReprScalarType| {
3406            let mut row = Row::default();
3407            row.packer().push(d);
3408            MirScalarExpr::Literal(Ok(row), ty.nullable(false))
3409        };
3410
3411        // `extract` hands `round` a `946684800` whose exponent is zero, where
3412        // the same value read out of a `Row` is `9.466848E+8`.
3413        let expr = lit(
3414            Datum::Date(Date::from_pg_epoch(0).unwrap()),
3415            ReprScalarType::Date,
3416        )
3417        .call_unary(UnaryFunc::ExtractDate(ExtractDate(DateTimeUnits::Epoch)))
3418        .call_binary(
3419            lit(Datum::Int32(i32::MAX), ReprScalarType::Int32),
3420            BinaryFunc::from(RoundNumericBinary),
3421        );
3422
3423        let relation = ReprRelationType::new(vec![]);
3424        let range = ColumnSpecs::new(&relation, &arena).expr(&expr).range;
3425        match expr.eval(&[], &arena) {
3426            Ok(value) => assert!(
3427                range.may_contain(value),
3428                "interpreter ruled out {value:?}, which the evaluator produced: {range:?}",
3429            ),
3430            Err(_) => assert!(
3431                range.may_fail(),
3432                "interpreter ruled out the error the evaluator produced: {range:?}",
3433            ),
3434        }
3435    }
3436
3437    #[mz_ore::test]
3438    fn test_trace() {
3439        use super::Trace;
3440
3441        let expr = MirScalarExpr::column(0).call_binary(
3442            MirScalarExpr::column(1)
3443                .call_binary(MirScalarExpr::column(3).call_unary(NegInt64), AddInt64),
3444            Gte,
3445        );
3446        let summary = Trace.expr(&expr);
3447        assert!(summary.pushdownable());
3448    }
3449}