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