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