Skip to main content

mz_transform/
typecheck.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10//! Check that the visible type of each query has not been changed
11
12use std::collections::BTreeMap;
13use std::fmt::Write;
14use std::sync::{Arc, Mutex};
15
16use itertools::Itertools;
17use mz_expr::explain::{HumanizedExplain, HumanizerMode};
18use mz_expr::visit::Visit;
19use mz_expr::{
20    AggregateExpr, ColumnOrder, Id, JoinImplementation, LocalId, MirRelationExpr, MirScalarExpr,
21    RECURSION_LIMIT, non_nullable_columns,
22};
23use mz_ore::soft_panic_or_log;
24use mz_ore::stack::{CheckedRecursion, RecursionGuard, RecursionLimitError};
25use mz_repr::adt::range::Range;
26use mz_repr::explain::{DummyHumanizer, ExprHumanizer};
27use mz_repr::{
28    ColumnName, Datum, ReprColumnType, ReprRelationType, ReprScalarBaseType, ReprScalarType,
29};
30
31/// Typechecking contexts as shared by various typechecking passes.
32///
33/// We use a `RefCell` to ensure that contexts are shared by multiple typechecker passes.
34/// Shared contexts help catch consistency issues.
35pub type SharedTypecheckingContext = Arc<Mutex<Context>>;
36
37/// Generates an empty context
38pub fn empty_typechecking_context() -> SharedTypecheckingContext {
39    Arc::new(Mutex::new(BTreeMap::new()))
40}
41
42/// The possible forms of inconsistency/errors discovered during typechecking.
43///
44/// Every variant has a `source` field identifying the MIR term that is home
45/// to the error (though not necessarily the root cause of the error).
46#[derive(Debug)]
47pub enum TypeError<'a> {
48    /// Unbound identifiers (local or global)
49    Unbound {
50        /// Expression with the bug
51        source: &'a MirRelationExpr,
52        /// The (unbound) identifier referenced
53        id: Id,
54        /// The type `id` was expected to have
55        typ: ReprRelationType,
56    },
57    /// Dereference of a non-existent column
58    NoSuchColumn {
59        /// Expression with the bug
60        source: &'a MirRelationExpr,
61        /// Scalar expression that references an invalid column
62        expr: &'a MirScalarExpr,
63        /// The invalid column referenced
64        col: usize,
65    },
66    /// A single column type does not match
67    MismatchColumn {
68        /// Expression with the bug
69        source: &'a MirRelationExpr,
70        /// The column type we found (`sub` type)
71        got: ReprColumnType,
72        /// The column type we expected (`sup` type)
73        expected: ReprColumnType,
74        /// The difference between these types
75        diffs: Vec<ReprColumnTypeDifference>,
76        /// An explanatory message
77        message: String,
78    },
79    /// Relation column types do not match
80    MismatchColumns {
81        /// Expression with the bug
82        source: &'a MirRelationExpr,
83        /// The column types we found (`sub` type)
84        got: Vec<ReprColumnType>,
85        /// The column types we expected (`sup` type)
86        expected: Vec<ReprColumnType>,
87        /// The difference between these types
88        diffs: Vec<ReprRelationTypeDifference>,
89        /// An explanatory message
90        message: String,
91    },
92    /// A constant row does not have the correct type
93    BadConstantRowLen {
94        /// Expression with the bug
95        source: &'a MirRelationExpr,
96        /// A constant row
97        got: usize,
98        /// The expected type (which that row does not have)
99        expected: Vec<ReprColumnType>,
100    },
101    /// A constant row does not have the correct type
102    BadConstantRow {
103        /// Expression with the bug
104        source: &'a MirRelationExpr,
105        /// The columns that mismatched, with a debug rendering of the datum we got and the expected type
106        mismatches: Vec<(usize, DatumTypeDifference)>,
107        /// The expected type (which that row does not have)
108        expected: Vec<ReprColumnType>,
109        // TODO(mgree) with a good way to get the type of a Datum, we could give a detailed diff here
110        // this is difficult for empty structures where we may not know the element type
111    },
112    /// Projection of a non-existent column
113    BadProject {
114        /// Expression with the bug
115        source: &'a MirRelationExpr,
116        /// The column projected
117        got: Vec<usize>,
118        /// The input columns (which don't have that column)
119        input_type: Vec<ReprColumnType>,
120    },
121    /// An equivalence class in a join was malformed
122    BadJoinEquivalence {
123        /// Expression with the bug
124        source: &'a MirRelationExpr,
125        /// The join equivalences
126        got: Vec<ReprColumnType>,
127        /// The problem with the join equivalences
128        message: String,
129    },
130    /// TopK grouping by non-existent column
131    BadTopKGroupKey {
132        /// Expression with the bug
133        source: &'a MirRelationExpr,
134        /// The bad column reference in the group key
135        k: usize,
136        /// The input columns (which don't have that column)
137        input_type: Vec<ReprColumnType>,
138    },
139    /// TopK ordering by non-existent column
140    BadTopKOrdering {
141        /// Expression with the bug
142        source: &'a MirRelationExpr,
143        /// The ordering used
144        order: ColumnOrder,
145        /// The input columns (which don't work for that ordering)
146        input_type: Vec<ReprColumnType>,
147    },
148    /// LetRec bindings are malformed
149    BadLetRecBindings {
150        /// Expression with the bug
151        source: &'a MirRelationExpr,
152    },
153    /// Local identifiers are shadowed
154    Shadowing {
155        /// Expression with the bug
156        source: &'a MirRelationExpr,
157        /// The id that was shadowed
158        id: Id,
159    },
160    /// Recursion depth exceeded
161    Recursion {
162        /// The error that aborted recursion
163        error: RecursionLimitError,
164    },
165    /// A dummy value was found
166    DisallowedDummy {
167        /// The expression with the dummy value
168        source: &'a MirRelationExpr,
169    },
170}
171
172impl<'a> From<RecursionLimitError> for TypeError<'a> {
173    fn from(error: RecursionLimitError) -> Self {
174        TypeError::Recursion { error }
175    }
176}
177
178type Context = BTreeMap<Id, Vec<ReprColumnType>>;
179
180/// Characterizes differences between relation types
181///
182/// Each constructor indicates a reason why some type `sub` was not a subtype of another type `sup`
183#[derive(Clone, Debug, Hash)]
184pub enum ReprRelationTypeDifference {
185    /// `sub` and `sup` don't have the same number of columns
186    Length {
187        /// Length of `sub`
188        len_sub: usize,
189        /// Length of `sup`
190        len_sup: usize,
191    },
192    /// `sub` and `sup` differ at the indicated column
193    Column {
194        /// The column at which `sub` and `sup` differ
195        col: usize,
196        /// The difference between `sub` and `sup`
197        diff: ReprColumnTypeDifference,
198    },
199}
200
201/// Characterizes differences between individual column types
202///
203/// Each constructor indicates a reason why some type `sub` was not a subtype of another type `sup`
204/// There may be multiple reasons, e.g., `sub` may be missing fields and have fields of different types
205#[derive(Clone, Debug, Hash)]
206pub enum ReprColumnTypeDifference {
207    /// The `ReprScalarBaseType` of `sub` doesn't match that of `sup`
208    NotSubtype {
209        /// Would-be subtype
210        sub: ReprScalarType,
211        /// Would-be supertype
212        sup: ReprScalarType,
213    },
214    /// `sub` was nullable but `sup` was not
215    Nullability {
216        /// Would-be subtype
217        sub: ReprColumnType,
218        /// Would-be supertype
219        sup: ReprColumnType,
220    },
221    /// Both `sub` and `sup` are a list, map, array, or range, but `sub`'s element type differed from `sup`s
222    ElementType {
223        /// The type constructor (list, array, etc.)
224        ctor: String,
225        /// The difference in the element type
226        element_type: Box<ReprColumnTypeDifference>,
227    },
228    /// `sub` and `sup` are both records, but `sub` is missing fields present in `sup`
229    RecordMissingFields {
230        /// The missing fields
231        missing: Vec<ColumnName>,
232    },
233    /// `sub` and `sup` are both records, but some fields in `sub` are not subtypes of fields in `sup`
234    RecordFields {
235        /// The differences, by field
236        fields: Vec<ReprColumnTypeDifference>,
237    },
238}
239
240impl ReprRelationTypeDifference {
241    /// Returns the same type difference, but ignoring nullability
242    ///
243    /// Returns `None` when _all_ of the differences are due to nullability
244    pub fn ignore_nullability(self) -> Option<Self> {
245        use ReprRelationTypeDifference::*;
246
247        match self {
248            Length { .. } => Some(self),
249            Column { col, diff } => diff.ignore_nullability().map(|diff| Column { col, diff }),
250        }
251    }
252}
253
254impl ReprColumnTypeDifference {
255    /// Returns the same type difference, but ignoring nullability
256    ///
257    /// Returns `None` when _all_ of the differences are due to nullability
258    pub fn ignore_nullability(self) -> Option<Self> {
259        use ReprColumnTypeDifference::*;
260
261        match self {
262            Nullability { .. } => None,
263            NotSubtype { .. } | RecordMissingFields { .. } => Some(self),
264            ElementType { ctor, element_type } => {
265                element_type
266                    .ignore_nullability()
267                    .map(|element_type| ElementType {
268                        ctor,
269                        element_type: Box::new(element_type),
270                    })
271            }
272            RecordFields { fields } => {
273                let fields = fields
274                    .into_iter()
275                    .flat_map(|diff| diff.ignore_nullability())
276                    .collect::<Vec<_>>();
277
278                if fields.is_empty() {
279                    None
280                } else {
281                    Some(RecordFields { fields })
282                }
283            }
284        }
285    }
286}
287
288/// Returns a list of differences that make `sub` not a subtype of `sup`
289///
290/// This function returns an empty list when `sub` is a subtype of `sup`
291pub fn relation_subtype_difference(
292    sub: &[ReprColumnType],
293    sup: &[ReprColumnType],
294) -> Vec<ReprRelationTypeDifference> {
295    let mut diffs = Vec::new();
296
297    if sub.len() != sup.len() {
298        diffs.push(ReprRelationTypeDifference::Length {
299            len_sub: sub.len(),
300            len_sup: sup.len(),
301        });
302
303        // TODO(mgree) we could do an edit-distance computation to report more errors
304        return diffs;
305    }
306
307    diffs.extend(
308        sub.iter()
309            .zip_eq(sup.iter())
310            .enumerate()
311            .flat_map(|(col, (sub_ty, sup_ty))| {
312                column_subtype_difference(sub_ty, sup_ty)
313                    .into_iter()
314                    .map(move |diff| ReprRelationTypeDifference::Column { col, diff })
315            }),
316    );
317
318    diffs
319}
320
321/// Returns a list of differences that make `sub` not a subtype of `sup`
322///
323/// This function returns an empty list when `sub` is a subtype of `sup`
324pub fn column_subtype_difference(
325    sub: &ReprColumnType,
326    sup: &ReprColumnType,
327) -> Vec<ReprColumnTypeDifference> {
328    let mut diffs = scalar_subtype_difference(&sub.scalar_type, &sup.scalar_type);
329
330    if sub.nullable && !sup.nullable {
331        diffs.push(ReprColumnTypeDifference::Nullability {
332            sub: sub.clone(),
333            sup: sup.clone(),
334        });
335    }
336
337    diffs
338}
339
340/// Returns a list of differences that make `sub` not a subtype of `sup`
341///
342/// This function returns an empty list when `sub` is a subtype of `sup`
343pub fn scalar_subtype_difference(
344    sub: &ReprScalarType,
345    sup: &ReprScalarType,
346) -> Vec<ReprColumnTypeDifference> {
347    use ReprScalarType::*;
348
349    let mut diffs = Vec::new();
350
351    match (sub, sup) {
352        (
353            List {
354                element_type: sub_elt,
355                ..
356            },
357            List {
358                element_type: sup_elt,
359                ..
360            },
361        )
362        | (
363            Map {
364                value_type: sub_elt,
365                ..
366            },
367            Map {
368                value_type: sup_elt,
369                ..
370            },
371        )
372        | (
373            Range {
374                element_type: sub_elt,
375                ..
376            },
377            Range {
378                element_type: sup_elt,
379                ..
380            },
381        )
382        | (Array(sub_elt), Array(sup_elt)) => {
383            let ctor = format!("{:?}", ReprScalarBaseType::from(sub));
384            diffs.extend(
385                scalar_subtype_difference(sub_elt, sup_elt)
386                    .into_iter()
387                    .map(|diff| ReprColumnTypeDifference::ElementType {
388                        ctor: ctor.clone(),
389                        element_type: Box::new(diff),
390                    }),
391            );
392        }
393        (
394            Record {
395                fields: sub_fields, ..
396            },
397            Record {
398                fields: sup_fields, ..
399            },
400        ) => {
401            if sub_fields.len() != sup_fields.len() {
402                diffs.push(ReprColumnTypeDifference::NotSubtype {
403                    sub: sub.clone(),
404                    sup: sup.clone(),
405                });
406                return diffs;
407            }
408
409            for (sub_ty, sup_ty) in sub_fields.iter().zip_eq(sup_fields.iter()) {
410                diffs.extend(column_subtype_difference(sub_ty, sup_ty));
411            }
412        }
413        (_, _) => {
414            if ReprScalarBaseType::from(sub) != ReprScalarBaseType::from(sup) {
415                diffs.push(ReprColumnTypeDifference::NotSubtype {
416                    sub: sub.clone(),
417                    sup: sup.clone(),
418                })
419            }
420        }
421    };
422
423    diffs
424}
425
426/// Unions `other` into `typ`, returning a list of differences on failure
427///
428/// This function returns an empty list when `typ` and `other` are a union
429pub fn scalar_union(
430    typ: &mut ReprScalarType,
431    other: &ReprScalarType,
432) -> Vec<ReprColumnTypeDifference> {
433    use ReprScalarType::*;
434
435    let mut diffs = Vec::new();
436
437    // precomputing to appease the borrow checker
438    let ctor = ReprScalarBaseType::from(&*typ);
439    match (typ, other) {
440        (
441            List {
442                element_type: typ_elt,
443            },
444            List {
445                element_type: other_elt,
446            },
447        )
448        | (
449            Map {
450                value_type: typ_elt,
451            },
452            Map {
453                value_type: other_elt,
454            },
455        )
456        | (
457            Range {
458                element_type: typ_elt,
459            },
460            Range {
461                element_type: other_elt,
462            },
463        )
464        | (Array(typ_elt), Array(other_elt)) => {
465            let res = scalar_union(typ_elt.as_mut(), other_elt.as_ref());
466            diffs.extend(
467                res.into_iter()
468                    .map(|diff| ReprColumnTypeDifference::ElementType {
469                        ctor: format!("{ctor:?}"),
470                        element_type: Box::new(diff),
471                    }),
472            );
473        }
474        (
475            Record { fields: typ_fields },
476            Record {
477                fields: other_fields,
478            },
479        ) => {
480            if typ_fields.len() != other_fields.len() {
481                diffs.push(ReprColumnTypeDifference::NotSubtype {
482                    sub: ReprScalarType::Record {
483                        fields: typ_fields.clone(),
484                    },
485                    sup: other.clone(),
486                });
487                return diffs;
488            }
489
490            for (typ_ty, other_ty) in typ_fields.iter_mut().zip_eq(other_fields.iter()) {
491                diffs.extend(column_union(typ_ty, other_ty));
492            }
493        }
494        (typ, _) => {
495            if ctor != ReprScalarBaseType::from(other) {
496                diffs.push(ReprColumnTypeDifference::NotSubtype {
497                    sub: typ.clone(),
498                    sup: other.clone(),
499                })
500            }
501        }
502    };
503
504    diffs
505}
506
507/// Unions `other` into `typ`, returning a list of differences on failure
508///
509/// This function returns an empty list when `typ` and `other` are a union
510pub fn column_union(
511    typ: &mut ReprColumnType,
512    other: &ReprColumnType,
513) -> Vec<ReprColumnTypeDifference> {
514    let diffs = scalar_union(&mut typ.scalar_type, &other.scalar_type);
515
516    if diffs.is_empty() {
517        typ.nullable |= other.nullable;
518    }
519
520    diffs
521}
522
523/// Returns true when it is safe to treat a `sub` row as an `sup` row
524///
525/// In particular, the core types must be equal, and if a column in `sup` is nullable, that column should also be nullable in `sub`
526/// Conversely, it is okay to treat a known non-nullable column as nullable: `sub` may be nullable when `sup` is not
527pub fn is_subtype_of(sub: &[ReprColumnType], sup: &[ReprColumnType]) -> bool {
528    if sub.len() != sup.len() {
529        return false;
530    }
531
532    sub.iter().zip_eq(sup.iter()).all(|(got, known)| {
533        (!known.nullable || got.nullable) && got.scalar_type == known.scalar_type
534    })
535}
536
537/// Characterizes how a Datum differs from a ReprColumnType
538#[derive(Clone, Debug)]
539pub enum DatumTypeDifference {
540    /// Datum was null, but expected a non-null value (i.e., a ReprScalarType)
541    Null {
542        /// The representation scalar type that we expected
543        expected: ReprScalarType,
544    },
545    /// Datum's kind doesn't match the expected ReprScalarType
546    Mismatch {
547        /// The debug rendering of the Datum that we got (we don't store the Datum itself to avoid borrowing issues)
548        /// This debug rendering includes the DatumKind.
549        got_debug: String,
550        /// The representation scalar type that we expected
551        expected: ReprScalarType,
552    },
553    /// Datum's dimensions didn't match the type's dimensions
554    MismatchDimensions {
555        /// The type constructor (list, array, int2vector, etc.)
556        ctor: String,
557        /// The dimension of the datum that we got
558        got: usize,
559        /// The dimension of the type that we expected
560        expected: usize,
561    },
562    /// There was a nested difference in the element type of some container type
563    ElementType {
564        /// The type constructor (list, array, int2vector, etc.)
565        ctor: String,
566        /// The difference in the element type
567        element_type: Box<DatumTypeDifference>,
568    },
569}
570
571/// Computes the difference between this datum and the specified (representation) column type.
572///
573/// See [`Datum<'a>::is_instance_of`] for just getting a yes/no answer.
574///
575/// See [`Datum<'a>::is_instance_of_sql`] for comparing `Datum`s to `SqlColumnType`s.
576fn datum_difference_with_column_type(
577    datum: &Datum<'_>,
578    column_type: &ReprColumnType,
579) -> Result<(), DatumTypeDifference> {
580    fn difference_with_scalar_type(
581        datum: &Datum<'_>,
582        scalar_type: &ReprScalarType,
583    ) -> Result<(), DatumTypeDifference> {
584        fn mismatch(got: &Datum<'_>, expected: &ReprScalarType) -> Result<(), DatumTypeDifference> {
585            Err(DatumTypeDifference::Mismatch {
586                // this will be redacted as appropriate
587                got_debug: format!("{got:?}"),
588                expected: expected.clone(),
589            })
590        }
591
592        if let ReprScalarType::Jsonb = scalar_type {
593            // json type checking
594            match datum {
595                Datum::Dummy => Ok(()), // allow dummys (unlike is_instance_of)
596                Datum::Null => Err(DatumTypeDifference::Null {
597                    expected: ReprScalarType::Jsonb,
598                }),
599                Datum::JsonNull
600                | Datum::False
601                | Datum::True
602                | Datum::Numeric(_)
603                | Datum::String(_) => Ok(()),
604                Datum::List(list) => {
605                    for elem in list.iter() {
606                        difference_with_scalar_type(&elem, scalar_type)?;
607                    }
608                    Ok(())
609                }
610                Datum::Map(dict) => {
611                    for (_, val) in dict.iter() {
612                        difference_with_scalar_type(&val, scalar_type)?;
613                    }
614                    Ok(())
615                }
616                _ => mismatch(datum, scalar_type),
617            }
618        } else {
619            fn element_type_difference(
620                ctor: &str,
621                element_type: DatumTypeDifference,
622            ) -> DatumTypeDifference {
623                DatumTypeDifference::ElementType {
624                    ctor: ctor.to_string(),
625                    element_type: Box::new(element_type),
626                }
627            }
628            match (datum, scalar_type) {
629                (Datum::Dummy, _) => Ok(()), // allow dummys (unlike is_instance_of)
630                (Datum::Null, _) => Err(DatumTypeDifference::Null {
631                    expected: scalar_type.clone(),
632                }),
633                (Datum::False, ReprScalarType::Bool) => Ok(()),
634                (Datum::False, _) => mismatch(datum, scalar_type),
635                (Datum::True, ReprScalarType::Bool) => Ok(()),
636                (Datum::True, _) => mismatch(datum, scalar_type),
637                (Datum::Int16(_), ReprScalarType::Int16) => Ok(()),
638                (Datum::Int16(_), _) => mismatch(datum, scalar_type),
639                (Datum::Int32(_), ReprScalarType::Int32) => Ok(()),
640                (Datum::Int32(_), _) => mismatch(datum, scalar_type),
641                (Datum::Int64(_), ReprScalarType::Int64) => Ok(()),
642                (Datum::Int64(_), _) => mismatch(datum, scalar_type),
643                (Datum::UInt8(_), ReprScalarType::UInt8) => Ok(()),
644                (Datum::UInt8(_), _) => mismatch(datum, scalar_type),
645                (Datum::UInt16(_), ReprScalarType::UInt16) => Ok(()),
646                (Datum::UInt16(_), _) => mismatch(datum, scalar_type),
647                (Datum::UInt32(_), ReprScalarType::UInt32) => Ok(()),
648                (Datum::UInt32(_), _) => mismatch(datum, scalar_type),
649                (Datum::UInt64(_), ReprScalarType::UInt64) => Ok(()),
650                (Datum::UInt64(_), _) => mismatch(datum, scalar_type),
651                (Datum::Float32(_), ReprScalarType::Float32) => Ok(()),
652                (Datum::Float32(_), _) => mismatch(datum, scalar_type),
653                (Datum::Float64(_), ReprScalarType::Float64) => Ok(()),
654                (Datum::Float64(_), _) => mismatch(datum, scalar_type),
655                (Datum::Date(_), ReprScalarType::Date) => Ok(()),
656                (Datum::Date(_), _) => mismatch(datum, scalar_type),
657                (Datum::Time(_), ReprScalarType::Time) => Ok(()),
658                (Datum::Time(_), _) => mismatch(datum, scalar_type),
659                (Datum::Timestamp(_), ReprScalarType::Timestamp { .. }) => Ok(()),
660                (Datum::Timestamp(_), _) => mismatch(datum, scalar_type),
661                (Datum::TimestampTz(_), ReprScalarType::TimestampTz { .. }) => Ok(()),
662                (Datum::TimestampTz(_), _) => mismatch(datum, scalar_type),
663                (Datum::Interval(_), ReprScalarType::Interval) => Ok(()),
664                (Datum::Interval(_), _) => mismatch(datum, scalar_type),
665                (Datum::Bytes(_), ReprScalarType::Bytes) => Ok(()),
666                (Datum::Bytes(_), _) => mismatch(datum, scalar_type),
667                (Datum::String(_), ReprScalarType::String) => Ok(()),
668                (Datum::String(_), _) => mismatch(datum, scalar_type),
669                (Datum::Uuid(_), ReprScalarType::Uuid) => Ok(()),
670                (Datum::Uuid(_), _) => mismatch(datum, scalar_type),
671                (Datum::Array(array), ReprScalarType::Array(t)) => {
672                    for e in array.elements().iter() {
673                        if let Datum::Null = e {
674                            continue;
675                        }
676
677                        difference_with_scalar_type(&e, t)
678                            .map_err(|e| element_type_difference("array", e))?;
679                    }
680                    Ok(())
681                }
682                (Datum::Array(array), ReprScalarType::Int2Vector) => {
683                    if !array.has_int2vector_dims() {
684                        // Int2Vector is 1-D, but empty arrays use 0 dimensions (PostgreSQL convention,
685                        // the error message just mentions 1 dimension, but 0 dimensions are allowed when empty.
686                        return Err(DatumTypeDifference::MismatchDimensions {
687                            ctor: "int2vector".to_string(),
688                            got: array.dims().len(),
689                            expected: 1,
690                        });
691                    }
692
693                    for e in array.elements().iter() {
694                        difference_with_scalar_type(&e, &ReprScalarType::Int16)
695                            .map_err(|e| element_type_difference("int2vector", e))?;
696                    }
697
698                    Ok(())
699                }
700                (Datum::Array(_), _) => mismatch(datum, scalar_type),
701                (Datum::List(list), ReprScalarType::List { element_type, .. }) => {
702                    for e in list.iter() {
703                        if let Datum::Null = e {
704                            continue;
705                        }
706
707                        difference_with_scalar_type(&e, element_type)
708                            .map_err(|e| element_type_difference("list", e))?;
709                    }
710                    Ok(())
711                }
712                (Datum::List(list), ReprScalarType::Record { fields, .. }) => {
713                    let len = list.iter().count();
714                    if len != fields.len() {
715                        return Err(DatumTypeDifference::MismatchDimensions {
716                            ctor: "record".to_string(),
717                            got: len,
718                            expected: fields.len(),
719                        });
720                    }
721
722                    for (e, t) in list.iter().zip_eq(fields) {
723                        if let Datum::Null = e {
724                            if t.nullable {
725                                continue;
726                            } else {
727                                return Err(DatumTypeDifference::Null {
728                                    expected: t.scalar_type.clone(),
729                                });
730                            }
731                        }
732
733                        difference_with_scalar_type(&e, &t.scalar_type)
734                            .map_err(|e| element_type_difference("record", e))?;
735                    }
736                    Ok(())
737                }
738                (Datum::List(_), _) => mismatch(datum, scalar_type),
739                (Datum::Map(map), ReprScalarType::Map { value_type, .. }) => {
740                    for (_, v) in map.iter() {
741                        if let Datum::Null = v {
742                            continue;
743                        }
744
745                        difference_with_scalar_type(&v, value_type)
746                            .map_err(|e| element_type_difference("map", e))?;
747                    }
748                    Ok(())
749                }
750                (Datum::Map(_), _) => mismatch(datum, scalar_type),
751                (Datum::JsonNull, _) => mismatch(datum, scalar_type),
752                (Datum::Numeric(_), ReprScalarType::Numeric) => Ok(()),
753                (Datum::Numeric(_), _) => mismatch(datum, scalar_type),
754                (Datum::MzTimestamp(_), ReprScalarType::MzTimestamp) => Ok(()),
755                (Datum::MzTimestamp(_), _) => mismatch(datum, scalar_type),
756                (Datum::Range(Range { inner }), ReprScalarType::Range { element_type }) => {
757                    match inner {
758                        None => Ok(()),
759                        Some(inner) => {
760                            if let Some(b) = inner.lower.bound {
761                                difference_with_scalar_type(&b.datum(), element_type)
762                                    .map_err(|e| element_type_difference("range", e))?;
763                            }
764                            if let Some(b) = inner.upper.bound {
765                                difference_with_scalar_type(&b.datum(), element_type)
766                                    .map_err(|e| element_type_difference("range", e))?;
767                            }
768                            Ok(())
769                        }
770                    }
771                }
772                (Datum::Range(_), _) => mismatch(datum, scalar_type),
773                (Datum::MzAclItem(_), ReprScalarType::MzAclItem) => Ok(()),
774                (Datum::MzAclItem(_), _) => mismatch(datum, scalar_type),
775                (Datum::AclItem(_), ReprScalarType::AclItem) => Ok(()),
776                (Datum::AclItem(_), _) => mismatch(datum, scalar_type),
777            }
778        }
779    }
780    if column_type.nullable {
781        if let Datum::Null = datum {
782            return Ok(());
783        }
784    }
785    difference_with_scalar_type(datum, &column_type.scalar_type)
786}
787
788fn row_difference_with_column_types<'a>(
789    source: &'a MirRelationExpr,
790    datums: &[Datum<'_>],
791    column_types: &[ReprColumnType],
792) -> Result<(), TypeError<'a>> {
793    // correct length
794    if datums.len() != column_types.len() {
795        return Err(TypeError::BadConstantRowLen {
796            source,
797            got: datums.len(),
798            expected: column_types.to_vec(),
799        });
800    }
801
802    // correct types
803    let mut mismatches = Vec::new();
804    for (i, (d, ty)) in datums.iter().zip_eq(column_types.iter()).enumerate() {
805        if let Err(e) = datum_difference_with_column_type(d, ty) {
806            mismatches.push((i, e));
807        }
808    }
809    if !mismatches.is_empty() {
810        return Err(TypeError::BadConstantRow {
811            source,
812            mismatches,
813            expected: column_types.to_vec(),
814        });
815    }
816
817    Ok(())
818}
819/// Check that the visible type of each query has not been changed
820#[derive(Debug)]
821pub struct Typecheck {
822    /// The known types of the queries so far
823    ctx: SharedTypecheckingContext,
824    /// Whether or not this is the first run of the transform
825    disallow_new_globals: bool,
826    /// Whether or not to be strict about join equivalences having the same nullability
827    strict_join_equivalences: bool,
828    /// Whether or not to disallow dummy values
829    disallow_dummy: bool,
830    /// Recursion guard for checked recursion
831    recursion_guard: RecursionGuard,
832}
833
834impl CheckedRecursion for Typecheck {
835    fn recursion_guard(&self) -> &RecursionGuard {
836        &self.recursion_guard
837    }
838}
839
840impl Typecheck {
841    /// Creates a typechecking consistency checking pass using a given shared context
842    pub fn new(ctx: SharedTypecheckingContext) -> Self {
843        Self {
844            ctx,
845            disallow_new_globals: false,
846            strict_join_equivalences: false,
847            disallow_dummy: false,
848            recursion_guard: RecursionGuard::with_limit(RECURSION_LIMIT),
849        }
850    }
851
852    /// New non-transient global IDs will be treated as an error
853    ///
854    /// Only turn this on after the context has been appropriately populated by, e.g., an earlier run
855    pub fn disallow_new_globals(mut self) -> Self {
856        self.disallow_new_globals = true;
857        self
858    }
859
860    /// Equivalence classes in joins must not only agree on scalar type, but also on nullability
861    ///
862    /// Only turn this on before `JoinImplementation`
863    pub fn strict_join_equivalences(mut self) -> Self {
864        self.strict_join_equivalences = true;
865
866        self
867    }
868
869    /// Disallow dummy values
870    pub fn disallow_dummy(mut self) -> Self {
871        self.disallow_dummy = true;
872        self
873    }
874
875    /// Returns the type of a relation expression or a type error.
876    ///
877    /// This function is careful to check validity, not just find out the type.
878    ///
879    /// It should be linear in the size of the AST.
880    ///
881    /// ??? should we also compute keys and return a `ReprRelationType`?
882    ///   ggevay: Checking keys would have the same problem as checking nullability: key inference
883    ///   is very heuristic (even more so than nullability inference), so it's almost impossible to
884    ///   reliably keep it stable across transformations.
885    pub fn typecheck<'a>(
886        &self,
887        expr: &'a MirRelationExpr,
888        ctx: &Context,
889    ) -> Result<Vec<ReprColumnType>, TypeError<'a>> {
890        use MirRelationExpr::*;
891
892        self.checked_recur(|tc| match expr {
893            Constant { typ, rows } => {
894                if let Ok(rows) = rows {
895                    for (row, _id) in rows {
896                        let datums = row.unpack();
897
898                        let col_types = typ
899                            .column_types
900                            .iter()
901                            .cloned()
902                            .collect_vec();
903                        row_difference_with_column_types(
904                            expr, &datums, &col_types,
905                        )?;
906
907                        if self.disallow_dummy
908                            && datums.iter().any(|d| d == &mz_repr::Datum::Dummy)
909                        {
910                            return Err(TypeError::DisallowedDummy {
911                                source: expr,
912                            });
913                        }
914                    }
915                }
916
917                Ok(typ.column_types.iter().cloned().collect_vec())
918            }
919            Get { typ, id, .. } => {
920                if let Id::Global(_global_id) = id {
921                    if !ctx.contains_key(id) {
922                        // TODO(mgree) pass QueryContext through to check these types
923                        return Ok(typ.column_types.iter().cloned().collect_vec());
924                    }
925                }
926
927                let ctx_typ = ctx.get(id).ok_or_else(|| TypeError::Unbound {
928                    source: expr,
929                    id: id.clone(),
930                    typ: typ.clone(),
931                })?;
932
933                let column_types = typ.column_types.iter().cloned().collect_vec();
934
935                // covariant: the ascribed type must be a subtype of the actual type in the context
936                let diffs = relation_subtype_difference(&column_types, ctx_typ)
937                    .into_iter()
938                    .flat_map(|diff| diff.ignore_nullability())
939                    .collect::<Vec<_>>();
940
941                if !diffs.is_empty() {
942                    return Err(TypeError::MismatchColumns {
943                        source: expr,
944                        got: column_types,
945                        expected: ctx_typ.clone(),
946                        diffs,
947                        message: "annotation did not match context type".to_string(),
948                    });
949                }
950
951                Ok(column_types)
952            }
953            Project { input, outputs } => {
954                let t_in = tc.typecheck(input, ctx)?;
955
956                for x in outputs {
957                    if *x >= t_in.len() {
958                        return Err(TypeError::BadProject {
959                            source: expr,
960                            got: outputs.clone(),
961                            input_type: t_in,
962                        });
963                    }
964                }
965
966                Ok(outputs.iter().map(|col| t_in[*col].clone()).collect())
967            }
968            Map { input, scalars } => {
969                let mut t_in = tc.typecheck(input, ctx)?;
970
971                for scalar_expr in scalars.iter() {
972                    t_in.push(tc.typecheck_scalar(scalar_expr, expr, &t_in)?);
973
974                    if self.disallow_dummy && scalar_expr.contains_dummy() {
975                        return Err(TypeError::DisallowedDummy {
976                            source: expr,
977                        });
978                    }
979                }
980
981                Ok(t_in)
982            }
983            FlatMap { input, func, exprs } => {
984                let mut t_in = tc.typecheck(input, ctx)?;
985
986                for scalar_expr in exprs {
987                    // TODO(mgree) check result agrees with `func`'s input type
988                    let _t_expr = tc.typecheck_scalar(scalar_expr, expr, &t_in)?;
989
990                    if self.disallow_dummy && scalar_expr.contains_dummy() {
991                        return Err(TypeError::DisallowedDummy {
992                            source: expr,
993                        });
994                    }
995                }
996
997                let t_out: Vec<ReprColumnType> = func
998                    .output_type().column_types;
999
1000                // FlatMap extends the existing columns
1001                t_in.extend(t_out);
1002                Ok(t_in)
1003            }
1004            Filter { input, predicates } => {
1005                let mut t_in = tc.typecheck(input, ctx)?;
1006
1007                // Set as nonnull any columns where null values would cause
1008                // any predicate to evaluate to null.
1009                for column in non_nullable_columns(predicates) {
1010                    t_in[column].nullable = false;
1011                }
1012
1013                for scalar_expr in predicates {
1014                    let t = tc.typecheck_scalar(scalar_expr, expr, &t_in)?;
1015
1016                    // filter condition must be boolean
1017                    // ignoring nullability: null is treated as false
1018                    // NB this behavior is slightly different from columns_match (for which we would set nullable to false in the expected type)
1019                    if t.scalar_type != ReprScalarType::Bool {
1020                        let sub = t.scalar_type.clone();
1021
1022                        return Err(TypeError::MismatchColumn {
1023                            source: expr,
1024                            got: t,
1025                            expected: ReprColumnType {
1026                                scalar_type: ReprScalarType::Bool,
1027                                nullable: true,
1028                            },
1029                            diffs: vec![ReprColumnTypeDifference::NotSubtype {
1030                                sub,
1031                                sup: ReprScalarType::Bool,
1032                            }],
1033                            message: "expected boolean condition".to_string(),
1034                        });
1035                    }
1036
1037                    if self.disallow_dummy && scalar_expr.contains_dummy() {
1038                        return Err(TypeError::DisallowedDummy {
1039                            source: expr,
1040                        });
1041                    }
1042                }
1043
1044                Ok(t_in)
1045            }
1046            Join {
1047                inputs,
1048                equivalences,
1049                implementation,
1050            } => {
1051                let mut t_in_global = Vec::new();
1052                let mut t_in_local = vec![Vec::new(); inputs.len()];
1053
1054                for (i, input) in inputs.iter().enumerate() {
1055                    let input_t = tc.typecheck(input, ctx)?;
1056                    t_in_global.extend(input_t.clone());
1057                    t_in_local[i] = input_t;
1058                }
1059
1060                for eq_class in equivalences {
1061                    let mut t_exprs: Vec<ReprColumnType> = Vec::with_capacity(eq_class.len());
1062
1063                    let mut all_nullable = true;
1064
1065                    for scalar_expr in eq_class {
1066                        // Note: the equivalences have global column references
1067                        let t_expr = tc.typecheck_scalar(scalar_expr, expr, &t_in_global)?;
1068
1069                        if !t_expr.nullable {
1070                            all_nullable = false;
1071                        }
1072
1073                        if let Some(t_first) = t_exprs.get(0) {
1074                            let diffs = scalar_subtype_difference(
1075                                &t_expr.scalar_type,
1076                                &t_first.scalar_type,
1077                            ).into_iter().filter_map(|d| d.ignore_nullability()).collect_vec();
1078                            if !diffs.is_empty() {
1079                                return Err(TypeError::MismatchColumn {
1080                                    source: expr,
1081                                    got: t_expr,
1082                                    expected: t_first.clone(),
1083                                    diffs,
1084                                    message: "equivalence class members \
1085                                        have different scalar types"
1086                                        .to_string(),
1087                                });
1088                            }
1089
1090                            // equivalences may or may not match on nullability
1091                            // before JoinImplementation runs, nullability should match.
1092                            // but afterwards, some nulls may appear that are actually being filtered out elsewhere
1093                            if self.strict_join_equivalences {
1094                                if t_expr.nullable != t_first.nullable {
1095                                    let sub = t_expr.clone();
1096                                    let sup = t_first.clone();
1097
1098                                    let err = TypeError::MismatchColumn {
1099                                        source: expr,
1100                                        got: t_expr.clone(),
1101                                        expected: t_first.clone(),
1102                                        diffs: vec![
1103                                            ReprColumnTypeDifference::Nullability { sub, sup },
1104                                        ],
1105                                        message: "equivalence class members have \
1106                                            different nullability (and join \
1107                                            equivalence checking is strict)"
1108                                            .to_string(),
1109                                    };
1110
1111                                    // TODO(mgree) this imprecision should be resolved, but we need to fix the optimizer
1112                                    ::tracing::debug!("{err}");
1113                                }
1114                            }
1115                        }
1116
1117                        if self.disallow_dummy && scalar_expr.contains_dummy() {
1118                            return Err(TypeError::DisallowedDummy {
1119                                source: expr,
1120                            });
1121                        }
1122
1123                        t_exprs.push(t_expr);
1124                    }
1125
1126                    if self.strict_join_equivalences && all_nullable {
1127                        let err = TypeError::BadJoinEquivalence {
1128                            source: expr,
1129                            got: t_exprs,
1130                            message: "all expressions were nullable (and join equivalence checking is strict)".to_string(),
1131                        };
1132
1133                        // TODO(mgree) this imprecision should be resolved, but we need to fix the optimizer
1134                        ::tracing::debug!("{err}");
1135                    }
1136                }
1137
1138                // check that the join implementation is consistent
1139                match implementation {
1140                    JoinImplementation::Differential((start_idx, first_key, _), others) => {
1141                        if let Some(key) = first_key {
1142                            for k in key {
1143                                let _ = tc.typecheck_scalar(k, expr, &t_in_local[*start_idx])?;
1144                            }
1145                        }
1146
1147                        for (idx, key, _) in others {
1148                            for k in key {
1149                                let _ = tc.typecheck_scalar(k, expr, &t_in_local[*idx])?;
1150                            }
1151                        }
1152                    }
1153                    JoinImplementation::DeltaQuery(plans) => {
1154                        for plan in plans {
1155                            for (idx, key, _) in plan {
1156                                for k in key {
1157                                    let _ = tc.typecheck_scalar(k, expr, &t_in_local[*idx])?;
1158                                }
1159                            }
1160                        }
1161                    }
1162                    JoinImplementation::IndexedFilter(_coll_id, _idx_id, key, consts) => {
1163                        let typ: Vec<ReprColumnType> = key
1164                            .iter()
1165                            .map(|k| tc.typecheck_scalar(k, expr, &t_in_global))
1166                            .collect::<Result<Vec<ReprColumnType>, TypeError>>()?;
1167
1168                        for row in consts {
1169                            let datums = row.unpack();
1170
1171                            row_difference_with_column_types(expr, &datums, &typ)?;
1172                        }
1173                    }
1174                    JoinImplementation::Unimplemented => (),
1175                }
1176
1177                Ok(t_in_global)
1178            }
1179            Reduce {
1180                input,
1181                group_key,
1182                aggregates,
1183                monotonic: _,
1184                expected_group_size: _,
1185            } => {
1186                let t_in = tc.typecheck(input, ctx)?;
1187
1188                let mut t_out = group_key
1189                    .iter()
1190                    .map(|scalar_expr| tc.typecheck_scalar(scalar_expr, expr, &t_in))
1191                    .collect::<Result<Vec<_>, _>>()?;
1192
1193                    if self.disallow_dummy
1194                        && group_key
1195                            .iter()
1196                            .any(|scalar_expr| scalar_expr.contains_dummy())
1197                    {
1198                        return Err(TypeError::DisallowedDummy {
1199                            source: expr,
1200                        });
1201                    }
1202
1203                for agg in aggregates {
1204                    t_out.push(tc.typecheck_aggregate(agg, expr, &t_in)?);
1205                }
1206
1207                Ok(t_out)
1208            }
1209            TopK {
1210                input,
1211                group_key,
1212                order_key,
1213                limit: _,
1214                offset: _,
1215                monotonic: _,
1216                expected_group_size: _,
1217            } => {
1218                let t_in = tc.typecheck(input, ctx)?;
1219
1220                for &k in group_key {
1221                    if k >= t_in.len() {
1222                        return Err(TypeError::BadTopKGroupKey {
1223                            source: expr,
1224                            k,
1225                            input_type: t_in,
1226                        });
1227                    }
1228                }
1229
1230                for order in order_key {
1231                    if order.column >= t_in.len() {
1232                        return Err(TypeError::BadTopKOrdering {
1233                            source: expr,
1234                            order: order.clone(),
1235                            input_type: t_in,
1236                        });
1237                    }
1238                }
1239
1240                Ok(t_in)
1241            }
1242            Negate { input } => tc.typecheck(input, ctx),
1243            Threshold { input } => tc.typecheck(input, ctx),
1244            Union { base, inputs } => {
1245                let mut t_base = tc.typecheck(base, ctx)?;
1246
1247                for input in inputs {
1248                    let t_input = tc.typecheck(input, ctx)?;
1249
1250                    let len_sub = t_base.len();
1251                    let len_sup = t_input.len();
1252                    if len_sub != len_sup {
1253                        return Err(TypeError::MismatchColumns {
1254                            source: expr,
1255                            got: t_base.clone(),
1256                            expected: t_input,
1257                            diffs: vec![ReprRelationTypeDifference::Length {
1258                                len_sub,
1259                                len_sup,
1260                            }],
1261                            message: "Union branches have different numbers of columns".to_string(),
1262                        });
1263                    }
1264
1265                    for (base_col, input_col) in t_base.iter_mut().zip_eq(t_input) {
1266                        let diffs = column_union(base_col, &input_col);
1267                        if !diffs.is_empty() {
1268                            return Err(TypeError::MismatchColumn {
1269                                    source: expr,
1270                                    got: input_col,
1271                                    expected: base_col.clone(),
1272                                    diffs,
1273                                    message:
1274                                        "couldn't compute union of column types in Union"
1275                                    .to_string(),
1276                            });
1277                        }
1278
1279                    }
1280                }
1281
1282                Ok(t_base)
1283            }
1284            Let { id, value, body } => {
1285                let t_value = tc.typecheck(value, ctx)?;
1286
1287                let binding = Id::Local(*id);
1288                if ctx.contains_key(&binding) {
1289                    return Err(TypeError::Shadowing {
1290                        source: expr,
1291                        id: binding,
1292                    });
1293                }
1294
1295                let mut body_ctx = ctx.clone();
1296                body_ctx.insert(Id::Local(*id), t_value);
1297
1298                tc.typecheck(body, &body_ctx)
1299            }
1300            LetRec { ids, values, body, limits: _ } => {
1301                if ids.len() != values.len() {
1302                    return Err(TypeError::BadLetRecBindings { source: expr });
1303                }
1304
1305                // temporary hack: steal info from the Gets inside to learn the expected types
1306                // if no get occurs in any definition or the body, that means that relation is dead code (which is okay)
1307                let mut ctx = ctx.clone();
1308                // calling tc.collect_recursive_variable_types(expr, ...) triggers a panic due to nested letrecs with shadowing IDs
1309                for inner_expr in values.iter().chain(std::iter::once(body.as_ref())) {
1310                    tc.collect_recursive_variable_types(inner_expr, ids, &mut ctx)?;
1311                }
1312
1313                for (id, value) in ids.iter().zip_eq(values.iter()) {
1314                    let typ = tc.typecheck(value, &ctx)?;
1315
1316                    let id = Id::Local(id.clone());
1317                    if let Some(ctx_typ) = ctx.get_mut(&id) {
1318                        for (base_col, input_col) in ctx_typ.iter_mut().zip_eq(typ) {
1319                            // we expect an EXACT match, but don't care about nullability
1320                            let diffs = column_union(base_col, &input_col);
1321                            if !diffs.is_empty() {
1322                                 return Err(TypeError::MismatchColumn {
1323                                        source: expr,
1324                                        got: input_col,
1325                                        expected: base_col.clone(),
1326                                        diffs,
1327                                        message:
1328                                            "couldn't compute union of column types in LetRec"
1329                                        .to_string(),
1330                                    })
1331                            }
1332                        }
1333                    } else {
1334                        // dead code: no `Get` references this relation anywhere. we record the type anyway
1335                        ctx.insert(id, typ);
1336                    }
1337                }
1338
1339                tc.typecheck(body, &ctx)
1340            }
1341            ArrangeBy { input, keys } => {
1342                let t_in = tc.typecheck(input, ctx)?;
1343
1344                for key in keys {
1345                    for k in key {
1346                        let _ = tc.typecheck_scalar(k, expr, &t_in)?;
1347                    }
1348                }
1349
1350                Ok(t_in)
1351            }
1352        })
1353    }
1354
1355    /// Traverses a term to collect the types of given ids.
1356    ///
1357    /// LetRec doesn't have type info stored in it. Until we change the MIR to track that information explicitly, we have to rebuild it from looking at the term.
1358    fn collect_recursive_variable_types<'a>(
1359        &self,
1360        expr: &'a MirRelationExpr,
1361        ids: &[LocalId],
1362        ctx: &mut Context,
1363    ) -> Result<(), TypeError<'a>> {
1364        use MirRelationExpr::*;
1365
1366        self.checked_recur(|tc| {
1367            match expr {
1368                Get {
1369                    id: Id::Local(id),
1370                    typ,
1371                    ..
1372                } => {
1373                    if !ids.contains(id) {
1374                        return Ok(());
1375                    }
1376
1377                    let id = Id::Local(id.clone());
1378                    if let Some(ctx_typ) = ctx.get_mut(&id) {
1379                        let typ = typ.column_types.iter().cloned().collect_vec();
1380
1381                        if ctx_typ.len() != typ.len() {
1382                            let diffs = relation_subtype_difference(&typ, ctx_typ);
1383
1384                            return Err(TypeError::MismatchColumns {
1385                                source: expr,
1386                                got: typ,
1387                                expected: ctx_typ.clone(),
1388                                diffs,
1389                                message: "environment and type annotation did not match"
1390                                    .to_string(),
1391                            });
1392                        }
1393
1394                        for (base_col, input_col) in ctx_typ.iter_mut().zip_eq(typ) {
1395                            let diffs = column_union(base_col, &input_col);
1396                            if !diffs.is_empty() {
1397                                return Err(TypeError::MismatchColumn {
1398                                    source: expr,
1399                                    got: input_col,
1400                                    expected: base_col.clone(),
1401                                    diffs,
1402                                    message:
1403                                        "couldn't compute union of column types in Get and context"
1404                                            .to_string(),
1405                                });
1406                            }
1407                        }
1408                    } else {
1409                        ctx.insert(id, typ.column_types.iter().cloned().collect_vec());
1410                    }
1411                }
1412                Get {
1413                    id: Id::Global(..), ..
1414                }
1415                | Constant { .. } => (),
1416                Let { id, value, body } => {
1417                    tc.collect_recursive_variable_types(value, ids, ctx)?;
1418
1419                    // we've shadowed the id
1420                    if ids.contains(id) {
1421                        return Err(TypeError::Shadowing {
1422                            source: expr,
1423                            id: Id::Local(*id),
1424                        });
1425                    }
1426
1427                    tc.collect_recursive_variable_types(body, ids, ctx)?;
1428                }
1429                LetRec {
1430                    ids: inner_ids,
1431                    values,
1432                    body,
1433                    limits: _,
1434                } => {
1435                    for inner_id in inner_ids {
1436                        if ids.contains(inner_id) {
1437                            return Err(TypeError::Shadowing {
1438                                source: expr,
1439                                id: Id::Local(*inner_id),
1440                            });
1441                        }
1442                    }
1443
1444                    for value in values {
1445                        tc.collect_recursive_variable_types(value, ids, ctx)?;
1446                    }
1447
1448                    tc.collect_recursive_variable_types(body, ids, ctx)?;
1449                }
1450                Project { input, .. }
1451                | Map { input, .. }
1452                | FlatMap { input, .. }
1453                | Filter { input, .. }
1454                | Reduce { input, .. }
1455                | TopK { input, .. }
1456                | Negate { input }
1457                | Threshold { input }
1458                | ArrangeBy { input, .. } => {
1459                    tc.collect_recursive_variable_types(input, ids, ctx)?;
1460                }
1461                Join { inputs, .. } => {
1462                    for input in inputs {
1463                        tc.collect_recursive_variable_types(input, ids, ctx)?;
1464                    }
1465                }
1466                Union { base, inputs } => {
1467                    tc.collect_recursive_variable_types(base, ids, ctx)?;
1468
1469                    for input in inputs {
1470                        tc.collect_recursive_variable_types(input, ids, ctx)?;
1471                    }
1472                }
1473            }
1474
1475            Ok(())
1476        })
1477    }
1478
1479    /// Typecheck a `MirScalarExpr`.
1480    ///
1481    /// The walk is iterative, so it is safe at any expression depth. `types`
1482    /// carries the results of already-visited subexpressions: post-order
1483    /// guarantees that a node with `n` children finds their types as the top
1484    /// `n` entries, so each arm drains that many and pushes its own result.
1485    /// One entry remains when the walk finishes, and it is the answer.
1486    fn typecheck_scalar<'a>(
1487        &self,
1488        expr: &'a MirScalarExpr,
1489        source: &'a MirRelationExpr,
1490        column_types: &[ReprColumnType],
1491    ) -> Result<ReprColumnType, TypeError<'a>> {
1492        use MirScalarExpr::*;
1493
1494        let mut types = Vec::<ReprColumnType>::new();
1495
1496        expr.try_visit_post(&mut |e: &'a MirScalarExpr| -> Result<(), TypeError<'a>> {
1497            let typ = match e {
1498                Column(i, _) => match column_types.get(*i) {
1499                    Some(ty) => ty.clone(),
1500                    None => {
1501                        return Err(TypeError::NoSuchColumn {
1502                            source,
1503                            expr: e,
1504                            col: *i,
1505                        });
1506                    }
1507                },
1508                Literal(row, typ) => {
1509                    let typ = typ.clone();
1510                    if let Ok(row) = row {
1511                        let datums = row.unpack();
1512                        row_difference_with_column_types(
1513                            source,
1514                            &datums,
1515                            std::slice::from_ref(&typ),
1516                        )?;
1517                    }
1518
1519                    typ
1520                }
1521                CallUnmaterializable(func) => func.output_type(),
1522                CallUnary { expr: _, func } => {
1523                    let typ_in = types.pop().expect("CallUnary child");
1524                    func.output_type(typ_in)
1525                }
1526                CallBinary {
1527                    expr1: _,
1528                    expr2: _,
1529                    func,
1530                } => {
1531                    let typ_in2 = types.pop().expect("CallBinary children");
1532                    let typ_in1 = types.pop().expect("CallBinary children");
1533                    func.output_type(&[typ_in1, typ_in2])
1534                }
1535                CallVariadic { exprs, func } => {
1536                    assert!(types.len() >= exprs.len(), "CallVariadic children");
1537                    let typ_in = types.split_off(types.len() - exprs.len());
1538                    func.output_type(typ_in)
1539                }
1540                If {
1541                    cond: _,
1542                    then: _,
1543                    els: _,
1544                } => {
1545                    let else_type = types.pop().expect("If children");
1546                    let mut then_type = types.pop().expect("If children");
1547                    let cond_type = types.pop().expect("If children");
1548
1549                    // condition must be boolean
1550                    // ignoring nullability: null is treated as false
1551                    // NB this behavior is slightly different from columns_match (for which we would set nullable to false in the expected type)
1552                    if cond_type.scalar_type != ReprScalarType::Bool {
1553                        let sub = cond_type.scalar_type.clone();
1554
1555                        return Err(TypeError::MismatchColumn {
1556                            source,
1557                            got: cond_type,
1558                            expected: ReprColumnType {
1559                                scalar_type: ReprScalarType::Bool,
1560                                nullable: true,
1561                            },
1562                            diffs: vec![ReprColumnTypeDifference::NotSubtype {
1563                                sub,
1564                                sup: ReprScalarType::Bool,
1565                            }],
1566                            message: "expected boolean condition".to_string(),
1567                        });
1568                    }
1569
1570                    let diffs = column_union(&mut then_type, &else_type);
1571                    if !diffs.is_empty() {
1572                        return Err(TypeError::MismatchColumn {
1573                            source,
1574                            got: then_type,
1575                            expected: else_type,
1576                            diffs,
1577                            message: "couldn't compute union of column types for If".to_string(),
1578                        });
1579                    }
1580
1581                    then_type
1582                }
1583            };
1584
1585            types.push(typ);
1586            Ok(())
1587        })?;
1588
1589        let typ = types.pop().expect("root type");
1590        assert!(
1591            types.is_empty(),
1592            "typecheck_scalar left {} types unconsumed",
1593            types.len()
1594        );
1595        Ok(typ)
1596    }
1597
1598    /// Typecheck an `AggregateExpr`
1599    pub fn typecheck_aggregate<'a>(
1600        &self,
1601        expr: &'a AggregateExpr,
1602        source: &'a MirRelationExpr,
1603        column_types: &[ReprColumnType],
1604    ) -> Result<ReprColumnType, TypeError<'a>> {
1605        let t_in = self.typecheck_scalar(&expr.expr, source, column_types)?;
1606
1607        // TODO check that t_in is actually acceptable for `func`
1608
1609        Ok(expr.func.output_type(t_in))
1610    }
1611}
1612
1613/// Detailed type error logging as a warning, with failures in CI and a logged error in production
1614///
1615/// type_error(severity, ...) logs a type warning; if `severity` is `true`, it will also log an error (visible in Sentry)
1616macro_rules! type_error {
1617    ($severity:expr, $($arg:tt)+) => {{
1618        if $severity {
1619          soft_panic_or_log!($($arg)+);
1620        } else {
1621          ::tracing::debug!($($arg)+);
1622        }
1623    }}
1624}
1625
1626impl crate::Transform for Typecheck {
1627    fn name(&self) -> &'static str {
1628        "Typecheck"
1629    }
1630
1631    fn actually_perform_transform(
1632        &self,
1633        relation: &mut MirRelationExpr,
1634        transform_ctx: &mut crate::TransformCtx,
1635    ) -> Result<(), crate::TransformError> {
1636        let mut typecheck_ctx = self.ctx.lock().expect("typecheck ctx");
1637
1638        let expected = transform_ctx
1639            .global_id
1640            .map_or_else(|| None, |id| typecheck_ctx.get(&Id::Global(id)));
1641
1642        if let Some(id) = transform_ctx.global_id {
1643            if self.disallow_new_globals
1644                && expected.is_none()
1645                && transform_ctx.global_id.is_some()
1646                && !id.is_transient()
1647            {
1648                type_error!(
1649                    false, // not severe
1650                    "type warning: new non-transient global id {id}\n{}",
1651                    relation.pretty()
1652                );
1653            }
1654        }
1655
1656        let got = self.typecheck(relation, &typecheck_ctx);
1657
1658        let humanizer = mz_repr::explain::DummyHumanizer;
1659
1660        match (got, expected) {
1661            (Ok(got), Some(expected)) => {
1662                let id = transform_ctx.global_id.unwrap();
1663
1664                // contravariant: global types can be updated
1665                let diffs = relation_subtype_difference(expected, &got);
1666                if !diffs.is_empty() {
1667                    // SEVERE only if got and expected have true differences, not just nullability
1668                    let severity = diffs
1669                        .iter()
1670                        .any(|diff| diff.clone().ignore_nullability().is_some());
1671
1672                    let err = TypeError::MismatchColumns {
1673                        source: relation,
1674                        got,
1675                        expected: expected.clone(),
1676                        diffs,
1677                        message: format!(
1678                            "a global id {id}'s type changed (was `expected` which should be a subtype of `got`) "
1679                        ),
1680                    };
1681
1682                    type_error!(severity, "type error in known global id {id}:\n{err}");
1683                }
1684            }
1685            (Ok(got), None) => {
1686                if let Some(id) = transform_ctx.global_id {
1687                    typecheck_ctx.insert(Id::Global(id), got);
1688                }
1689            }
1690            (Err(err), _) => {
1691                let (expected, binding) = match expected {
1692                    Some(expected) => {
1693                        let id = transform_ctx.global_id.unwrap();
1694                        (
1695                            format!("expected type {}\n", columns_pretty(expected, &humanizer)),
1696                            format!("known global id {id}"),
1697                        )
1698                    }
1699                    None => ("".to_string(), "transient query".to_string()),
1700                };
1701
1702                type_error!(
1703                    true, // SEVERE: the transformed code is inconsistent
1704                    "type error in {binding}:\n{err}\n{expected}{}",
1705                    relation.pretty()
1706                );
1707            }
1708        }
1709
1710        Ok(())
1711    }
1712}
1713
1714/// Prints a type prettily with a given `ExprHumanizer`
1715pub fn columns_pretty<H>(cols: &[ReprColumnType], humanizer: &H) -> String
1716where
1717    H: ExprHumanizer,
1718{
1719    let mut s = String::with_capacity(2 + 3 * cols.len());
1720
1721    s.push('(');
1722
1723    let mut it = cols.iter().peekable();
1724    while let Some(col) = it.next() {
1725        s.push_str(&humanizer.humanize_column_type(col));
1726
1727        if it.peek().is_some() {
1728            s.push_str(", ");
1729        }
1730    }
1731
1732    s.push(')');
1733
1734    s
1735}
1736
1737impl ReprRelationTypeDifference {
1738    /// Pretty prints a type difference
1739    ///
1740    /// Always indents two spaces
1741    pub fn humanize<H>(&self, h: &H, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result
1742    where
1743        H: ExprHumanizer,
1744    {
1745        use ReprRelationTypeDifference::*;
1746        match self {
1747            Length { len_sub, len_sup } => {
1748                writeln!(
1749                    f,
1750                    "  number of columns do not match ({len_sub} != {len_sup})"
1751                )
1752            }
1753            Column { col, diff } => {
1754                writeln!(f, "  column {col} differs:")?;
1755                diff.humanize(4, h, f)
1756            }
1757        }
1758    }
1759}
1760
1761impl ReprColumnTypeDifference {
1762    /// Pretty prints a type difference at a given indentation level
1763    pub fn humanize<H>(
1764        &self,
1765        indent: usize,
1766        h: &H,
1767        f: &mut std::fmt::Formatter<'_>,
1768    ) -> std::fmt::Result
1769    where
1770        H: ExprHumanizer,
1771    {
1772        use ReprColumnTypeDifference::*;
1773
1774        // indent
1775        write!(f, "{:indent$}", "")?;
1776
1777        match self {
1778            NotSubtype { sub, sup } => {
1779                let sub = h.humanize_scalar_type(sub);
1780                let sup = h.humanize_scalar_type(sup);
1781
1782                writeln!(f, "{sub} is a not a subtype of {sup}")
1783            }
1784            Nullability { sub, sup } => {
1785                let sub = h.humanize_column_type(sub);
1786                let sup = h.humanize_column_type(sup);
1787
1788                writeln!(f, "{sub} is nullable but {sup} is not")
1789            }
1790            ElementType { ctor, element_type } => {
1791                writeln!(f, "{ctor} element types differ:")?;
1792
1793                element_type.humanize(indent + 2, h, f)
1794            }
1795            RecordMissingFields { missing } => {
1796                write!(f, "missing column fields:")?;
1797                for col in missing {
1798                    write!(f, " {col}")?;
1799                }
1800                f.write_char('\n')
1801            }
1802            RecordFields { fields } => {
1803                writeln!(f, "{} record fields differ:", fields.len())?;
1804
1805                for (i, diff) in fields.iter().enumerate() {
1806                    writeln!(f, "{:indent$}  field {i}:", "")?;
1807                    diff.humanize(indent + 4, h, f)?;
1808                }
1809                Ok(())
1810            }
1811        }
1812    }
1813}
1814
1815impl DatumTypeDifference {
1816    /// Pretty prints a type difference at a given indentation level
1817    pub fn humanize<H>(
1818        &self,
1819        indent: usize,
1820        h: &H,
1821        f: &mut std::fmt::Formatter<'_>,
1822    ) -> std::fmt::Result
1823    where
1824        H: ExprHumanizer,
1825    {
1826        // indent
1827        write!(f, "{:indent$}", "")?;
1828
1829        match self {
1830            DatumTypeDifference::Null { expected } => {
1831                let expected = h.humanize_scalar_type(expected);
1832                writeln!(
1833                    f,
1834                    "unexpected null, expected representation type {expected}"
1835                )?
1836            }
1837            DatumTypeDifference::Mismatch {
1838                got_debug,
1839                expected,
1840            } => {
1841                let expected = h.humanize_scalar_type(expected);
1842                // NB `got_debug` will be redacted as appropriate
1843                writeln!(
1844                    f,
1845                    "got datum {got_debug}, expected representation type {expected}"
1846                )?;
1847            }
1848            DatumTypeDifference::MismatchDimensions {
1849                ctor,
1850                got,
1851                expected,
1852            } => {
1853                writeln!(
1854                    f,
1855                    "{ctor} dimensions differ: got datum with dimension {got}, expected dimension {expected}"
1856                )?;
1857            }
1858            DatumTypeDifference::ElementType { ctor, element_type } => {
1859                writeln!(f, "{ctor} element types differ:")?;
1860                element_type.humanize(indent + 4, h, f)?;
1861            }
1862        }
1863
1864        Ok(())
1865    }
1866}
1867
1868/// Wrapper struct for a `Display` instance for `TypeError`s with a given `ExprHumanizer`
1869#[allow(missing_debug_implementations)]
1870pub struct TypeErrorHumanizer<'a, 'b, H>
1871where
1872    H: ExprHumanizer,
1873{
1874    err: &'a TypeError<'a>,
1875    humanizer: &'b H,
1876}
1877
1878impl<'a, 'b, H> TypeErrorHumanizer<'a, 'b, H>
1879where
1880    H: ExprHumanizer,
1881{
1882    /// Create a `Display`-shim struct for a given `TypeError`/`ExprHumanizer` pair
1883    pub fn new(err: &'a TypeError, humanizer: &'b H) -> Self {
1884        Self { err, humanizer }
1885    }
1886}
1887
1888impl<'a, 'b, H> std::fmt::Display for TypeErrorHumanizer<'a, 'b, H>
1889where
1890    H: ExprHumanizer,
1891{
1892    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1893        self.err.humanize(self.humanizer, f)
1894    }
1895}
1896
1897impl<'a> std::fmt::Display for TypeError<'a> {
1898    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1899        TypeErrorHumanizer {
1900            err: self,
1901            humanizer: &DummyHumanizer,
1902        }
1903        .fmt(f)
1904    }
1905}
1906
1907impl<'a> TypeError<'a> {
1908    /// The source of the type error
1909    pub fn source(&self) -> Option<&'a MirRelationExpr> {
1910        use TypeError::*;
1911        match self {
1912            Unbound { source, .. }
1913            | NoSuchColumn { source, .. }
1914            | MismatchColumn { source, .. }
1915            | MismatchColumns { source, .. }
1916            | BadConstantRowLen { source, .. }
1917            | BadConstantRow { source, .. }
1918            | BadProject { source, .. }
1919            | BadJoinEquivalence { source, .. }
1920            | BadTopKGroupKey { source, .. }
1921            | BadTopKOrdering { source, .. }
1922            | BadLetRecBindings { source }
1923            | Shadowing { source, .. }
1924            | DisallowedDummy { source, .. } => Some(source),
1925            Recursion { .. } => None,
1926        }
1927    }
1928
1929    fn humanize<H>(&self, humanizer: &H, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result
1930    where
1931        H: ExprHumanizer,
1932    {
1933        if let Some(source) = self.source() {
1934            writeln!(f, "In the MIR term:\n{}\n", source.pretty())?;
1935        }
1936
1937        use TypeError::*;
1938        match self {
1939            Unbound { source: _, id, typ } => {
1940                let typ = columns_pretty(&typ.column_types, humanizer);
1941                writeln!(f, "{id} is unbound\ndeclared type {typ}")?
1942            }
1943            NoSuchColumn {
1944                source: _,
1945                expr,
1946                col,
1947            } => writeln!(f, "{expr} references non-existent column {col}")?,
1948            MismatchColumn {
1949                source: _,
1950                got,
1951                expected,
1952                diffs,
1953                message,
1954            } => {
1955                let got = humanizer.humanize_column_type(got);
1956                let expected = humanizer.humanize_column_type(expected);
1957                writeln!(
1958                    f,
1959                    "mismatched column types: {message}\n      got {got}\nexpected {expected}"
1960                )?;
1961
1962                for diff in diffs {
1963                    diff.humanize(2, humanizer, f)?;
1964                }
1965            }
1966            MismatchColumns {
1967                source: _,
1968                got,
1969                expected,
1970                diffs,
1971                message,
1972            } => {
1973                let got = columns_pretty(got, humanizer);
1974                let expected = columns_pretty(expected, humanizer);
1975
1976                writeln!(
1977                    f,
1978                    "mismatched relation types: {message}\n      got {got}\nexpected {expected}"
1979                )?;
1980
1981                for diff in diffs {
1982                    diff.humanize(humanizer, f)?;
1983                }
1984            }
1985            BadConstantRowLen {
1986                source: _,
1987                got,
1988                expected,
1989            } => {
1990                let expected = columns_pretty(expected, humanizer);
1991                writeln!(
1992                    f,
1993                    "bad constant row\n      row has length {got}\nexpected row of type {expected}"
1994                )?
1995            }
1996            BadConstantRow {
1997                source: _,
1998                mismatches,
1999                expected,
2000            } => {
2001                let expected = columns_pretty(expected, humanizer);
2002
2003                let num_mismatches = mismatches.len();
2004                let plural = if num_mismatches == 1 { "" } else { "es" };
2005                writeln!(
2006                    f,
2007                    "bad constant row\n      got {num_mismatches} mismatch{plural}\nexpected row of type {expected}"
2008                )?;
2009
2010                if num_mismatches > 0 {
2011                    writeln!(f, "")?;
2012                    for (col, diff) in mismatches.iter() {
2013                        writeln!(f, "      column #{col}:")?;
2014                        diff.humanize(8, humanizer, f)?;
2015                    }
2016                }
2017            }
2018            BadProject {
2019                source: _,
2020                got,
2021                input_type,
2022            } => {
2023                let input_type = columns_pretty(input_type, humanizer);
2024
2025                writeln!(
2026                    f,
2027                    "projection of non-existant columns {got:?} from type {input_type}"
2028                )?
2029            }
2030            BadJoinEquivalence {
2031                source: _,
2032                got,
2033                message,
2034            } => {
2035                let got = columns_pretty(got, humanizer);
2036
2037                writeln!(f, "bad join equivalence {got}: {message}")?
2038            }
2039            BadTopKGroupKey {
2040                source: _,
2041                k,
2042                input_type,
2043            } => {
2044                let input_type = columns_pretty(input_type, humanizer);
2045
2046                writeln!(
2047                    f,
2048                    "TopK group key component references invalid column {k} in columns: {input_type}"
2049                )?
2050            }
2051            BadTopKOrdering {
2052                source: _,
2053                order,
2054                input_type,
2055            } => {
2056                let col = order.column;
2057                let num_cols = input_type.len();
2058                let are = if num_cols == 1 { "is" } else { "are" };
2059                let s = if num_cols == 1 { "" } else { "s" };
2060                let input_type = columns_pretty(input_type, humanizer);
2061
2062                // TODO(cloud#8196)
2063                let mode = HumanizedExplain::new(false);
2064                let order = mode.expr(order, None);
2065
2066                writeln!(
2067                    f,
2068                    "TopK ordering {order} references invalid column {col}\nthere {are} {num_cols} column{s}: {input_type}"
2069                )?
2070            }
2071            BadLetRecBindings { source: _ } => {
2072                writeln!(f, "LetRec ids and definitions don't line up")?
2073            }
2074            Shadowing { source: _, id } => writeln!(f, "id {id} is shadowed")?,
2075            DisallowedDummy { source: _ } => writeln!(f, "contains a dummy value")?,
2076            Recursion { error } => writeln!(f, "{error}")?,
2077        }
2078
2079        Ok(())
2080    }
2081}
2082
2083#[cfg(test)]
2084mod tests {
2085    use mz_ore::{assert_err, assert_ok};
2086    use mz_repr::{SqlColumnType, arb_datum, arb_datum_for_column};
2087    use proptest::prelude::*;
2088
2089    use super::*;
2090
2091    #[mz_ore::test]
2092    fn test_datum_type_difference() {
2093        let datum = Datum::Int16(1);
2094
2095        assert_ok!(datum_difference_with_column_type(
2096            &datum,
2097            &ReprColumnType {
2098                scalar_type: ReprScalarType::Int16,
2099                nullable: true,
2100            }
2101        ));
2102
2103        assert_err!(datum_difference_with_column_type(
2104            &datum,
2105            &ReprColumnType {
2106                scalar_type: ReprScalarType::Int32,
2107                nullable: false,
2108            }
2109        ));
2110    }
2111
2112    proptest! {
2113        #![proptest_config(ProptestConfig {
2114            cases: 5000,
2115            max_global_rejects: 2500,
2116            ..Default::default()
2117        })]
2118        #[mz_ore::test]
2119        #[cfg_attr(miri, ignore)]
2120        fn datum_type_difference_with_instance_of_on_valid_data(
2121            (src, datum) in any::<SqlColumnType>()
2122                .prop_flat_map(|src| {
2123                    let datum = arb_datum_for_column(src.clone());
2124                    (Just(src), datum)
2125                })
2126        ) {
2127            let typ = ReprColumnType::from(&src);
2128            let datum = Datum::from(&datum);
2129
2130            if datum.contains_dummy() {
2131                return Err(TestCaseError::reject("datum contains a dummy"));
2132            }
2133
2134            let diff = datum_difference_with_column_type(&datum, &typ);
2135            if datum.is_instance_of(&typ) {
2136                assert_ok!(diff);
2137            } else {
2138                assert_err!(diff);
2139            }
2140        }
2141    }
2142
2143    proptest! {
2144        // We run many cases because the data are _random_, and we want to be sure
2145        // that we have covered sufficient cases. We drop dummy data (behavior is different!) so we allow many more global rejects.
2146        #![proptest_config(ProptestConfig::with_cases(10000))]
2147        #[mz_ore::test]
2148        #[cfg_attr(miri, ignore)]
2149        fn datum_type_difference_agrees_with_is_instance_of_on_random_data(
2150            src in any::<SqlColumnType>(),
2151            datum in arb_datum(false),
2152        ) {
2153            let typ = ReprColumnType::from(&src);
2154            let datum = Datum::from(&datum);
2155
2156            assert!(!datum.contains_dummy(), "datum contains a dummy (bug in arb_datum)");
2157
2158            let diff = datum_difference_with_column_type(&datum, &typ);
2159            if datum.is_instance_of(&typ) {
2160                assert_ok!(diff);
2161            } else {
2162                assert_err!(diff);
2163            }
2164        }
2165    }
2166
2167    #[mz_ore::test]
2168    fn datum_type_difference_github_10039() {
2169        let typ = ReprColumnType {
2170            scalar_type: ReprScalarType::Record {
2171                fields: Box::new([ReprColumnType {
2172                    scalar_type: ReprScalarType::UInt32,
2173                    nullable: false,
2174                }]),
2175            },
2176            nullable: false,
2177        };
2178
2179        let mut row = mz_repr::Row::default();
2180        row.packer()
2181            .push_list(std::iter::once(mz_repr::Datum::Null));
2182        let datum = row.unpack_first();
2183
2184        assert!(!datum.is_instance_of(&typ));
2185        let diff = datum_difference_with_column_type(&datum, &typ);
2186        assert_err!(diff);
2187    }
2188    /// A scalar expression far deeper than `RECURSION_LIMIT` typechecks, on a
2189    /// thread whose stack could not hold one frame per node. The walk keeps its
2190    /// worklist on the heap, so neither the recursion guard nor the native stack
2191    /// bounds the depth it accepts.
2192    ///
2193    /// `MirScalarExpr`'s `Drop` is recursive, which shapes the rest of the test:
2194    /// the expression is built and dismantled with loops, and the outcome is
2195    /// reduced to shallow values before anything can panic. An assertion that
2196    /// unwound past a `DEPTH`-deep expression would overflow this thread while
2197    /// dropping it and report that instead of the failure it caught. A stack
2198    /// overflow aborts the process rather than unwinding, so the `join` below
2199    /// can only ever report a panic inside the thread.
2200    #[mz_ore::test]
2201    #[cfg_attr(miri, ignore)] // the 40k-node walk is slow under Miri
2202    fn deep_scalar_typechecks_without_recursing() {
2203        const DEPTH: usize = 20 * RECURSION_LIMIT;
2204        const THREAD_STACK_SIZE: usize = 256 << 10;
2205
2206        std::thread::Builder::new()
2207            .stack_size(THREAD_STACK_SIZE)
2208            .spawn(|| {
2209                let mut expr = MirScalarExpr::column(0);
2210                for _ in 0..DEPTH {
2211                    expr = expr.not();
2212                }
2213
2214                let source = MirRelationExpr::constant(vec![], ReprRelationType::empty());
2215                let column_types = vec![ReprColumnType {
2216                    scalar_type: ReprScalarType::Bool,
2217                    nullable: false,
2218                }];
2219
2220                let outcome = Typecheck::new(empty_typechecking_context())
2221                    .typecheck_scalar(&expr, &source, &column_types)
2222                    .map(|typ| typ.scalar_type)
2223                    .map_err(|err| match err {
2224                        TypeError::Recursion { .. } => "recursion limit",
2225                        _ => "type error",
2226                    });
2227
2228                while let MirScalarExpr::CallUnary { expr: inner, .. } = expr {
2229                    expr = *inner;
2230                }
2231
2232                assert_eq!(outcome, Ok(ReprScalarType::Bool));
2233            })
2234            .expect("spawn")
2235            .join()
2236            .expect("deep scalar typecheck panicked");
2237    }
2238}