Skip to main content

mz_sql/plan/
transform_hir.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//! Transformations of SQL IR, before decorrelation.
11
12use std::collections::{BTreeMap, BTreeSet};
13use std::sync::LazyLock;
14use std::{iter, mem};
15
16use itertools::Itertools;
17use mz_expr::WindowFrame;
18use mz_expr::func::variadic::RecordCreate;
19use mz_expr::visit::{Visit, VisitChildren};
20use mz_expr::{ColumnOrder, UnaryFunc, VariadicFunc};
21use mz_ore::stack::{RecursionLimitError, maybe_grow};
22use mz_repr::{ColumnName, SqlColumnType, SqlRelationType, SqlScalarType};
23
24use crate::plan::hir::{
25    AbstractExpr, AggregateFunc, AggregateWindowExpr, ColumnRef, HirRelationExpr, HirScalarExpr,
26    ValueWindowExpr, ValueWindowFunc, WindowExpr,
27};
28use crate::plan::{AggregateExpr, WindowExprType};
29
30/// Rewrites predicates that contain subqueries so that the subqueries
31/// appear in their own later predicate when possible.
32///
33/// For example, this function rewrites this expression
34///
35/// ```text
36/// Filter {
37///     predicates: [a = b AND EXISTS (<subquery 1>) AND c = d AND (<subquery 2>) = e]
38/// }
39/// ```
40///
41/// like so:
42///
43/// ```text
44/// Filter {
45///     predicates: [
46///         a = b AND c = d,
47///         EXISTS (<subquery>),
48///         (<subquery 2>) = e,
49///     ]
50/// }
51/// ```
52///
53/// The rewrite causes decorrelation to incorporate prior predicates into
54/// the outer relation upon which the subquery is evaluated. In the above
55/// rewritten example, the `EXISTS (<subquery>)` will only be evaluated for
56/// outer rows where `a = b AND c = d`. The second subquery, `(<subquery 2>)
57/// = e`, will be further restricted to outer rows that match `A = b AND c =
58/// d AND EXISTS(<subquery>)`. This can vastly reduce the cost of the
59/// subquery, especially when the original conjunction contains join keys.
60pub fn split_subquery_predicates(expr: &mut HirRelationExpr) -> Result<(), RecursionLimitError> {
61    fn walk_relation(expr: &mut HirRelationExpr) -> Result<(), RecursionLimitError> {
62        #[allow(deprecated)]
63        expr.visit_mut_fallible(0, &mut |expr, _| {
64            match expr {
65                HirRelationExpr::Map { scalars, .. } => {
66                    for scalar in scalars {
67                        walk_scalar(scalar)?;
68                    }
69                }
70                HirRelationExpr::CallTable { exprs, .. } => {
71                    for expr in exprs {
72                        walk_scalar(expr)?;
73                    }
74                }
75                HirRelationExpr::Filter { predicates, .. } => {
76                    let mut subqueries = vec![];
77                    for predicate in &mut *predicates {
78                        walk_scalar(predicate)?;
79                        extract_conjuncted_subqueries(predicate, &mut subqueries)?;
80                    }
81                    // TODO(benesch): we could be smarter about the order in which
82                    // we emit subqueries. At the moment we just emit in the order
83                    // we discovered them, but ideally we'd emit them in an order
84                    // that accounted for their cost/selectivity. E.g., low-cost,
85                    // high-selectivity subqueries should go first.
86                    for subquery in subqueries {
87                        predicates.push(subquery);
88                    }
89                }
90                _ => (),
91            }
92            Ok(())
93        })
94    }
95
96    fn walk_scalar(expr: &mut HirScalarExpr) -> Result<(), RecursionLimitError> {
97        expr.try_visit_direct_subqueries_mut(&mut walk_relation)
98    }
99
100    fn contains_subquery(expr: &HirScalarExpr) -> Result<bool, RecursionLimitError> {
101        let mut found = false;
102        expr.try_visit_direct_subqueries(|_| {
103            found = true;
104            Ok(())
105        })?;
106        Ok(found)
107    }
108
109    /// Extracts subqueries from a conjunction into `out`.
110    ///
111    /// For example, given an expression like
112    ///
113    /// ```text
114    /// a = b AND EXISTS (<subquery 1>) AND c = d AND (<subquery 2>) = e
115    /// ```
116    ///
117    /// this function rewrites the expression to
118    ///
119    /// ```text
120    /// a = b AND true AND c = d AND true
121    /// ```
122    ///
123    /// and returns the expression fragments `EXISTS (<subquery 1>)` and
124    /// `(<subquery 2>) = e` in the `out` vector.
125    fn extract_conjuncted_subqueries(
126        expr: &mut HirScalarExpr,
127        out: &mut Vec<HirScalarExpr>,
128    ) -> Result<(), RecursionLimitError> {
129        match expr {
130            HirScalarExpr::CallVariadic {
131                func: VariadicFunc::And(_),
132                exprs,
133                name: _,
134            } => {
135                exprs
136                    .into_iter()
137                    .try_for_each(|e| extract_conjuncted_subqueries(e, out))?;
138            }
139            expr if contains_subquery(expr)? => {
140                out.push(mem::replace(expr, HirScalarExpr::literal_true()))
141            }
142            _ => (),
143        }
144        Ok(())
145    }
146
147    walk_relation(expr)
148}
149
150/// Rewrites quantified comparisons into simpler EXISTS operators.
151///
152/// Note that this transformation is only valid when the expression is
153/// used in a context where the distinction between `FALSE` and `NULL`
154/// is immaterial, e.g., in a `WHERE` clause or a `CASE` condition, or
155/// when the inputs to the comparison are non-nullable. This function is careful
156/// to only apply the transformation when it is valid to do so.
157///
158/// ```ignore
159/// WHERE (SELECT any(<pred>) FROM <rel>)
160/// =>
161/// WHERE EXISTS(SELECT * FROM <rel> WHERE <pred>)
162///
163/// WHERE (SELECT all(<pred>) FROM <rel>)
164/// =>
165/// WHERE NOT EXISTS(SELECT * FROM <rel> WHERE (NOT <pred>) OR <pred> IS NULL)
166/// ```
167///
168/// See Section 3.5 of "Execution Strategies for SQL Subqueries" by
169/// M. Elhemali, et al.
170pub fn try_simplify_quantified_comparisons(
171    expr: &mut HirRelationExpr,
172    simplify_join_on: bool,
173) -> Result<(), RecursionLimitError> {
174    // There is nothing to simplify unless the query contains a subquery. Bail
175    // early in that common case: `walk_relation` recomputes `input.typ()` at
176    // every level, which is O(depth^2) over a deep relation tree (e.g. a long
177    // JOIN or CTE chain) and would wedge the coordinator.
178    if !relation_contains_subquery(expr) {
179        return Ok(());
180    }
181
182    fn walk_relation(
183        expr: &mut HirRelationExpr,
184        outers: &[SqlRelationType],
185        simplify_join_on: bool,
186    ) -> Result<(), RecursionLimitError> {
187        // Grow the stack: recurses over a user-controlled-depth relation tree.
188        maybe_grow(|| {
189            match expr {
190                HirRelationExpr::Map { scalars, input } => {
191                    walk_relation(input, outers, simplify_join_on)?;
192                    let mut outers = outers.to_vec();
193                    outers.insert(0, input.typ(&outers, &NO_PARAMS));
194                    for scalar in scalars {
195                        walk_scalar(scalar, &outers, false, simplify_join_on)?;
196                        let (inner, outers) = outers
197                            .split_first_mut()
198                            .expect("outers known to have at least one element");
199                        let scalar_type = scalar.typ(outers, inner, &NO_PARAMS);
200                        inner.column_types.push(scalar_type);
201                    }
202                }
203                HirRelationExpr::Filter { predicates, input } => {
204                    walk_relation(input, outers, simplify_join_on)?;
205                    let mut outers = outers.to_vec();
206                    outers.insert(0, input.typ(&outers, &NO_PARAMS));
207                    for pred in predicates {
208                        walk_scalar(pred, &outers, true, simplify_join_on)?;
209                    }
210                }
211                HirRelationExpr::CallTable { exprs, .. } => {
212                    let mut outers = outers.to_vec();
213                    outers.insert(0, SqlRelationType::empty());
214                    for scalar in exprs {
215                        walk_scalar(scalar, &outers, false, simplify_join_on)?;
216                    }
217                }
218                HirRelationExpr::Join {
219                    left, right, on, ..
220                } => {
221                    walk_relation(left, outers, simplify_join_on)?;
222                    let left_type = left.typ(outers, &NO_PARAMS);
223                    let mut outers = outers.to_vec();
224                    outers.insert(0, left_type);
225                    walk_relation(right, &outers, simplify_join_on)?;
226                    if simplify_join_on {
227                        // Build outers with the full join output type, since the
228                        // ON clause can reference columns from both sides.
229                        let right_type = right.typ(&outers, &NO_PARAMS);
230                        let mut join_columns = outers[0].column_types.clone();
231                        join_columns.extend(right_type.column_types);
232                        outers[0] = SqlRelationType::new(join_columns);
233                        walk_scalar(on, &outers, true, simplify_join_on)?;
234                    }
235                }
236                expr => {
237                    #[allow(deprecated)]
238                    let _ = expr.visit1_mut(0, &mut |expr, _| -> Result<(), RecursionLimitError> {
239                        walk_relation(expr, outers, simplify_join_on)
240                    });
241                }
242            }
243            Ok(())
244        })
245    }
246
247    fn walk_scalar(
248        expr: &mut HirScalarExpr,
249        outers: &[SqlRelationType],
250        mut in_filter: bool,
251        simplify_join_on: bool,
252    ) -> Result<(), RecursionLimitError> {
253        expr.try_visit_mut_pre(&mut |e| {
254            match e {
255                HirScalarExpr::Exists(input, _name) => {
256                    walk_relation(input, outers, simplify_join_on)?
257                }
258                HirScalarExpr::Select(input, _name) => {
259                    walk_relation(input, outers, simplify_join_on)?;
260
261                    // We're inside a `(SELECT ...)` subquery. Now let's see if
262                    // it has the form `(SELECT <any|all>(...) FROM <input>)`.
263                    // Ideally we could do this with one pattern, but Rust's pattern
264                    // matching engine is not powerful enough, so we have to do this
265                    // in stages; the early returns avoid brutal nesting.
266
267                    let (func, expr, input) = match &mut **input {
268                        HirRelationExpr::Reduce {
269                            group_key,
270                            aggregates,
271                            input,
272                            expected_group_size: _,
273                        } if group_key.is_empty() && aggregates.len() == 1 => {
274                            let agg = &mut aggregates[0];
275                            (&agg.func, &mut agg.expr, input)
276                        }
277                        _ => return Ok(()),
278                    };
279
280                    if !in_filter && column_type(outers, input, expr).nullable {
281                        // Unless we're directly inside a WHERE, this
282                        // transformation is only valid if the expression involved
283                        // is non-nullable.
284                        return Ok(());
285                    }
286
287                    match func {
288                        AggregateFunc::Any => {
289                            // Found `(SELECT any(<expr>) FROM <input>)`. Rewrite to
290                            // `EXISTS(SELECT 1 FROM <input> WHERE <expr>)`.
291                            *e = input.take().filter(vec![expr.take()]).exists();
292                        }
293                        AggregateFunc::All => {
294                            // Found `(SELECT all(<expr>) FROM <input>)`. Rewrite to
295                            // `NOT EXISTS(SELECT 1 FROM <input> WHERE NOT <expr> OR <expr> IS NULL)`.
296                            //
297                            // Note that negation of <expr> alone is insufficient.
298                            // Consider that `WHERE <pred>` filters out rows if
299                            // `<pred>` is false *or* null. To invert the test, we
300                            // need `NOT <pred> OR <pred> IS NULL`.
301                            let expr = expr.take();
302                            let filter = expr.clone().not().or(expr.call_is_null());
303                            *e = input.take().filter(vec![filter]).exists().not();
304                        }
305                        _ => (),
306                    }
307                }
308                _ => {
309                    // As soon as we see *any* scalar expression, we are no longer
310                    // directly inside a filter.
311                    in_filter = false;
312                }
313            }
314            Ok(())
315        })
316    }
317
318    walk_relation(expr, &[], simplify_join_on)
319}
320
321/// Collapses `EXISTS` over a FROM-less subquery into an equivalent scalar
322/// predicate on the outer row, so that decorrelation produces a plain `Filter`
323/// rather than a semijoin (for `EXISTS`) or an antijoin (for `NOT EXISTS`).
324///
325/// A FROM-less subquery is a chain of `Map`, `Project`, and `Filter` nodes over
326/// a single-row `Constant` (the join identity of a query with no `FROM`
327/// clause). Such a subquery yields exactly one row when every `Filter`
328/// predicate is `TRUE` and zero rows otherwise, so
329///
330/// ```text
331/// EXISTS(<from-less subquery with predicates p1, p2, ...>) == (p1 AND p2 AND ...) IS TRUE
332/// ```
333///
334/// evaluated on the outer row. The `IS TRUE` is mandatory for null safety. An
335/// empty subquery (some predicate `FALSE` or `NULL`) must make `EXISTS` return
336/// `FALSE`, which `IS TRUE` reproduces while a bare predicate would leak `NULL`.
337/// `NOT EXISTS` then becomes `NOT ((...) IS TRUE)`, which is `... IS NOT TRUE`
338/// and likewise null-safe.
339///
340/// The rewrite fires only on correlated subqueries, where the predicate
341/// references at least one outer column. This keeps it to the pure existence
342/// check that a genuine anti/semi-join would otherwise be lowered to, and it
343/// avoids changing whether an uncorrelated erroring subquery is evaluated when
344/// the outer relation is empty.
345///
346/// This closes database-issues#2613 (`x IN (SELECT ... WHERE p)`, which
347/// [`try_simplify_quantified_comparisons`] has already turned into an `EXISTS`)
348/// and database-issues#2969 (`NOT EXISTS (SELECT ... WHERE p)`). It must run
349/// after [`try_simplify_quantified_comparisons`].
350pub fn simplify_from_less_existence_subqueries(
351    expr: &mut HirRelationExpr,
352) -> Result<(), RecursionLimitError> {
353    // `try_visit_mut_post` walks every relation node, and because
354    // `VisitChildren<Self>` for `HirRelationExpr` descends into the bodies of
355    // `Exists`/`Select` subqueries, it reaches existence checks at every nesting
356    // level. Post-order guarantees a subquery body is simplified before the
357    // `Exists` that encloses it.
358    expr.try_visit_mut_post(&mut |rel| {
359        rel.try_visit_mut_children(|scalar: &mut HirScalarExpr| {
360            scalar.try_visit_mut_pre(&mut |e| {
361                if let HirScalarExpr::Exists(input, _name) = e {
362                    if let Some(pred) = from_less_existence_predicate(input) {
363                        *e = pred.call_unary(UnaryFunc::IsTrue(mz_expr::func::IsTrue));
364                    }
365                }
366                Ok(())
367            })
368        })
369    })
370}
371
372/// If `sub` is a FROM-less subquery (see
373/// [`simplify_from_less_existence_subqueries`]) whose existence check is
374/// correlated on the outer row, returns the predicate `p1 AND p2 AND ...`
375/// expressed in the outer row's frame. Returns `None` otherwise.
376fn from_less_existence_predicate(sub: &HirRelationExpr) -> Option<HirScalarExpr> {
377    // A FROM-less subquery is a linear Map/Project/Filter chain over a single-row
378    // `Constant`. Both properties of the base are load-bearing for soundness: the
379    // single row is what lets EXISTS reduce to "the predicate holds on that row",
380    // and the constant is what lets its columns be inlined into the lifted
381    // predicate below. A 0-row, multi-row, or non-constant base is a genuine
382    // anti/semi-join and bails at the `_` arm.
383    //
384    // Record the chain top to bottom here; it is replayed bottom to top below.
385    let mut chain = Vec::new();
386    let mut cur = sub;
387    let (row, typ) = loop {
388        match cur {
389            HirRelationExpr::Filter { input, .. }
390            | HirRelationExpr::Map { input, .. }
391            | HirRelationExpr::Project { input, .. } => {
392                chain.push(cur);
393                cur = input.as_ref();
394            }
395            HirRelationExpr::Constant { rows, typ } if rows.len() == 1 => break (&rows[0], typ),
396            _ => return None,
397        }
398    };
399
400    // `env` holds the value of each column of the current relation, expressed in
401    // the subquery's own frame. Because level-0 references are resolved as we go,
402    // env entries only ever contain constants and outer (level >= 1) references.
403    let mut env: Vec<HirScalarExpr> = row
404        .iter()
405        .zip_eq(typ.column_types.iter())
406        .map(|(datum, col_type)| HirScalarExpr::literal(datum, col_type.scalar_type.clone()))
407        .collect();
408
409    // Replay the chain bottom to top so each node sees the `env` built by the nodes
410    // beneath it: `Map` extends `env`, `Filter` reads it, `Project` permutes it.
411    let mut preds: Vec<HirScalarExpr> = Vec::new();
412    for node in chain.iter().rev() {
413        match node {
414            HirRelationExpr::Filter { predicates, .. } => {
415                for predicate in predicates {
416                    preds.push(resolve_local_columns(predicate, &env)?);
417                }
418            }
419            HirRelationExpr::Map { scalars, .. } => {
420                for scalar in scalars {
421                    let resolved = resolve_local_columns(scalar, &env)?;
422                    env.push(resolved);
423                }
424            }
425            HirRelationExpr::Project { outputs, .. } => {
426                env = outputs
427                    .iter()
428                    .map(|i| env.get(*i).cloned())
429                    .collect::<Option<Vec<_>>>()?;
430            }
431            _ => unreachable!("chain only contains Filter, Map, and Project nodes"),
432        }
433    }
434
435    let mut pred = HirScalarExpr::variadic_and(preds);
436
437    // `pred` is built only from predicates that `resolve_local_columns` accepted,
438    // and that rejects any subquery, so `pred` contains no nested subqueries. Every
439    // column reference is therefore in the subquery's own frame at nesting depth 0,
440    // and an outer reference is exactly one with `level > 0`.
441
442    // Require correlation: the predicate must reference an outer column. Without
443    // correlation this is not the existence check a genuine anti/semi-join lowers
444    // to, and firing would risk changing when a constant erroring predicate is
445    // evaluated.
446    let mut correlated = false;
447    pred.visit_post(&mut |e| {
448        if let HirScalarExpr::Column(col, _name) = e {
449            if col.level > 0 {
450                correlated = true;
451            }
452        }
453    });
454    if !correlated {
455        return None;
456    }
457
458    // Lift the predicate out of the subquery: references to the immediately
459    // enclosing (outer) scope move down one level.
460    pred.visit_mut_post(&mut |e| {
461        if let HirScalarExpr::Column(col, _name) = e {
462            if col.level > 0 {
463                col.level -= 1;
464            }
465        }
466    });
467
468    Some(pred)
469}
470
471/// Returns `expr` with every reference to the current scope (a [`ColumnRef`]
472/// with `level == 0`) replaced by its value from `env`. Returns `None` if `expr`
473/// cannot be soundly lifted into the outer scope, or references a column absent
474/// from `env`.
475fn resolve_local_columns(expr: &HirScalarExpr, env: &[HirScalarExpr]) -> Option<HirScalarExpr> {
476    // Every scalar in the FROM-less body is substituted into the outer scope, so
477    // reject any that cannot be evaluated equivalently there. The match is
478    // exhaustive on purpose: a new `HirScalarExpr` variant must be classified here
479    // rather than silently treated as liftable.
480    let mut unliftable = false;
481    expr.visit_post(&mut |e| {
482        let liftable = match e {
483            // Row-local: the value depends only on the row, so it is the same in
484            // the subquery's frame and the outer frame.
485            HirScalarExpr::Column(..)
486            | HirScalarExpr::Parameter(..)
487            | HirScalarExpr::Literal(..)
488            | HirScalarExpr::CallUnmaterializable(..)
489            | HirScalarExpr::CallUnary { .. }
490            | HirScalarExpr::CallBinary { .. }
491            | HirScalarExpr::CallVariadic { .. }
492            | HirScalarExpr::If { .. } => true,
493            // A subquery carries its own nested scopes that this flat substitution
494            // does not handle. A window function over the single-row body (e.g.
495            // `row_number() OVER ()` is always 1) is not the same function over the
496            // multi-row outer relation. Neither may cross the subquery boundary.
497            HirScalarExpr::Exists(..)
498            | HirScalarExpr::Select(..)
499            | HirScalarExpr::Windowing(..) => false,
500        };
501        unliftable |= !liftable;
502    });
503    if unliftable {
504        return None;
505    }
506
507    let mut expr = expr.clone();
508    let mut ok = true;
509    expr.visit_mut_post(&mut |e| {
510        if let HirScalarExpr::Column(ColumnRef { level: 0, column }, _name) = e {
511            match env.get(*column) {
512                Some(value) => *e = value.clone(),
513                None => ok = false,
514            }
515        }
516    });
517    ok.then_some(expr)
518}
519
520/// Returns whether `expr` contains any subquery (`HirScalarExpr::Exists` or
521/// `HirScalarExpr::Select`). Both the relation tree and the per-node scalars are
522/// traversed iteratively, so this is stack-safe on deeply nested inputs: a long
523/// JOIN/CTE chain grows the relation tree, and a flat `CASE` with many arms
524/// lowers to a deep right-nested `If` chain in a single scalar. `visit_pre` on
525/// `HirScalarExpr` stops at `Exists`/`Select` (they are scalar leaves), so the
526/// scan never descends into subquery bodies. The relation walk already yields
527/// those bodies as its own children.
528fn relation_contains_subquery(expr: &HirRelationExpr) -> bool {
529    let mut found = false;
530    expr.visit_post(&mut |r: &HirRelationExpr| {
531        if !found {
532            VisitChildren::<HirScalarExpr>::visit_children(r, |s| {
533                s.visit_pre(&mut |e: &HirScalarExpr| {
534                    if matches!(e, HirScalarExpr::Exists(..) | HirScalarExpr::Select(..)) {
535                        found = true;
536                    }
537                });
538            });
539        }
540    });
541    found
542}
543
544/// An empty parameter type map.
545///
546/// These transformations are expected to run after parameters are bound, so
547/// there is no need to provide any parameter type information.
548static NO_PARAMS: LazyLock<BTreeMap<usize, SqlScalarType>> = LazyLock::new(BTreeMap::new);
549
550fn column_type(
551    outers: &[SqlRelationType],
552    inner: &HirRelationExpr,
553    expr: &HirScalarExpr,
554) -> SqlColumnType {
555    let inner_type = inner.typ(outers, &NO_PARAMS);
556    expr.typ(outers, &inner_type, &NO_PARAMS)
557}
558
559impl HirScalarExpr {
560    /// Similar to `MirScalarExpr::support`, but adapted to `HirScalarExpr` in a special way: it
561    /// considers column references that target the root level.
562    /// (See `visit_columns_referring_to_root_level`.)
563    fn support(&self) -> Vec<usize> {
564        let mut result = Vec::new();
565        self.visit_columns_referring_to_root_level(&mut |c| result.push(c));
566        result
567    }
568
569    /// Changes column references in `self` by the given remapping.
570    /// Panics if a referred column is not present in `idx_map`!
571    fn remap(mut self, idx_map: &BTreeMap<usize, usize>) -> HirScalarExpr {
572        self.visit_columns_referring_to_root_level_mut(&mut |c| {
573            *c = idx_map[c];
574        });
575        self
576    }
577}
578
579/// # Aims and scope
580///
581/// The aim here is to amortize the overhead of the MIR window function pattern
582/// (see `window_func_applied_to`) by fusing groups of window function calls such
583/// that each group can be performed by one instance of the window function MIR
584/// pattern.
585///
586/// For now, we fuse only value window function calls and window aggregations.
587/// (We probably won't need to fuse scalar window functions for a long time.)
588///
589/// For now, we can fuse value window function calls and window aggregations where the
590/// A. partition by
591/// B. order by
592/// C. window frame
593/// D. ignore nulls for value window functions and distinct for window aggregations
594/// are all the same. (See `extract_options`.)
595/// (Later, we could improve this to only need A. to be the same. This would require
596/// much more code changes, because then we'd have to blow up `ValueWindowExpr`.
597/// TODO: As a much simpler intermediate step, at least we should ignore options that
598/// don't matter. For example, we should be able to fuse a `lag` that has a default
599/// frame with a `first_value` that has some custom frame, because `lag` is not
600/// affected by the frame.)
601/// Note that we fuse value window function calls and window aggregations separately.
602///
603/// # Implementation
604///
605/// At a high level, what we are going to do is look for Maps with more than one window function
606/// calls, and for each Map
607/// - remove some groups of window function call expressions from the Map's `scalars`;
608/// - insert a fused version of each group;
609/// - insert some expressions that decompose the results of the fused calls;
610/// - update some column references in `scalars`: those that refer to window function results that
611///   participated in fusion, as well as those that refer to columns that moved around due to
612///   removing and inserting expressions.
613/// - insert a Project above the matched Map to permute columns back to their original places.
614///
615/// It would be tempting to find groups simply by taking a list of all window function calls
616/// and calling `group_by` with a key function that extracts the above A. B. C. D. properties,
617/// but a complication is that the possible groups that we could theoretically fuse overlap.
618/// This is because when forming groups we need to also take into account column references
619/// that point inside the same Map. For example, imagine a Map with the following scalar
620/// expressions:
621/// C1, E1, C2, C3, where
622/// - E1 refers to C1
623/// - C3 refers to E1.
624/// In this situation, we could either
625/// - fuse C1 and C2, and put the fused expression in the place of C1 (so that E1 can keep referring
626///   to it);
627/// - or fuse C2 and C3.
628/// However, we can't fuse all of C1, C2, C3 into one call, because then there would be
629/// no appropriate place for the fused expression: it would have to be both before and after E1.
630///
631/// So, how we actually form the groups is that, keeping track of a list of non-overlapping groups,
632/// we go through `scalars`, try to put each expression into each of our groups, and the first of
633/// these succeed. When trying to put an expression into a group, we need to be mindful about column
634/// references inside the same Map, as noted above. A constraint that we impose on ourselves for
635/// sanity is that the fused version of each group will be inserted at the place where the first
636/// element of the group originally was. This means that the only condition that we need to check on
637/// column references when adding an expression to a group is that all column references in a group
638/// should be to columns that are earlier than the first element of the group. (No need to check
639/// column references in the other direction, i.e., references in other expressions that refer to
640/// columns in the group.)
641pub fn fuse_window_functions(
642    root: &mut HirRelationExpr,
643    _context: &crate::plan::lowering::Context,
644) -> Result<(), RecursionLimitError> {
645    /// Those options of a window function call that are relevant for fusion.
646    #[derive(PartialEq, Eq)]
647    enum WindowFuncCallOptions {
648        Value(ValueWindowFuncCallOptions),
649        Agg(AggregateWindowFuncCallOptions),
650    }
651    #[derive(PartialEq, Eq)]
652    struct ValueWindowFuncCallOptions {
653        partition_by: Vec<HirScalarExpr>,
654        outer_order_by: Vec<HirScalarExpr>,
655        inner_order_by: Vec<ColumnOrder>,
656        window_frame: WindowFrame,
657        ignore_nulls: bool,
658    }
659    #[derive(PartialEq, Eq)]
660    struct AggregateWindowFuncCallOptions {
661        partition_by: Vec<HirScalarExpr>,
662        outer_order_by: Vec<HirScalarExpr>,
663        inner_order_by: Vec<ColumnOrder>,
664        window_frame: WindowFrame,
665        distinct: bool,
666    }
667
668    /// Helper function to extract the above options.
669    fn extract_options(call: &HirScalarExpr) -> WindowFuncCallOptions {
670        match call {
671            HirScalarExpr::Windowing(
672                WindowExpr {
673                    func:
674                        WindowExprType::Value(ValueWindowExpr {
675                            order_by: inner_order_by,
676                            window_frame,
677                            ignore_nulls,
678                            func: _,
679                            args: _,
680                        }),
681                    partition_by,
682                    order_by: outer_order_by,
683                },
684                _name,
685            ) => WindowFuncCallOptions::Value(ValueWindowFuncCallOptions {
686                partition_by: partition_by.clone(),
687                outer_order_by: outer_order_by.clone(),
688                inner_order_by: inner_order_by.clone(),
689                window_frame: window_frame.clone(),
690                ignore_nulls: ignore_nulls.clone(),
691            }),
692            HirScalarExpr::Windowing(
693                WindowExpr {
694                    func:
695                        WindowExprType::Aggregate(AggregateWindowExpr {
696                            aggregate_expr:
697                                AggregateExpr {
698                                    distinct,
699                                    func: _,
700                                    expr: _,
701                                },
702                            order_by: inner_order_by,
703                            window_frame,
704                        }),
705                    partition_by,
706                    order_by: outer_order_by,
707                },
708                _name,
709            ) => WindowFuncCallOptions::Agg(AggregateWindowFuncCallOptions {
710                partition_by: partition_by.clone(),
711                outer_order_by: outer_order_by.clone(),
712                inner_order_by: inner_order_by.clone(),
713                window_frame: window_frame.clone(),
714                distinct: distinct.clone(),
715            }),
716            _ => panic!(
717                "extract_options should only be called on value window functions or window aggregations"
718            ),
719        }
720    }
721
722    struct FusionGroup {
723        /// The original column index of the first element of the group. (This is an index into the
724        /// Map's `scalars` plus the arity of the Map's input.)
725        first_col: usize,
726        /// The options of all the window function calls in the group. (Must be the same for all the
727        /// calls.)
728        options: WindowFuncCallOptions,
729        /// The calls in the group, with their original column indexes.
730        calls: Vec<(usize, HirScalarExpr)>,
731    }
732
733    impl FusionGroup {
734        /// Creates a window function call that is a fused version of all the calls in the group.
735        /// `new_col` is the column index where the fused call will be inserted at.
736        fn fuse(self, new_col: usize) -> (HirScalarExpr, Vec<HirScalarExpr>) {
737            let fused = match self.options {
738                WindowFuncCallOptions::Value(options) => {
739                    let (fused_funcs, fused_args): (Vec<_>, Vec<_>) = self
740                        .calls
741                        .iter()
742                        .map(|(_idx, call)| {
743                            if let HirScalarExpr::Windowing(
744                                WindowExpr {
745                                    func:
746                                        WindowExprType::Value(ValueWindowExpr {
747                                            func,
748                                            args,
749                                            order_by: _,
750                                            window_frame: _,
751                                            ignore_nulls: _,
752                                        }),
753                                    partition_by: _,
754                                    order_by: _,
755                                },
756                                _name,
757                            ) = call
758                            {
759                                (func.clone(), (**args).clone())
760                            } else {
761                                panic!("unknown window function in FusionGroup")
762                            }
763                        })
764                        .unzip();
765                    let fused_args = HirScalarExpr::call_variadic(
766                        RecordCreate {
767                            // These field names are not important, because this record will only be an
768                            // intermediate expression, which we'll manipulate further before it ends up
769                            // anywhere where a column name would be visible.
770                            field_names: iter::repeat(ColumnName::from(""))
771                                .take(fused_args.len())
772                                .collect(),
773                        },
774                        fused_args,
775                    );
776                    HirScalarExpr::windowing(WindowExpr {
777                        func: WindowExprType::Value(ValueWindowExpr {
778                            func: ValueWindowFunc::Fused(fused_funcs),
779                            args: Box::new(fused_args),
780                            order_by: options.inner_order_by,
781                            window_frame: options.window_frame,
782                            ignore_nulls: options.ignore_nulls,
783                        }),
784                        partition_by: options.partition_by,
785                        order_by: options.outer_order_by,
786                    })
787                }
788                WindowFuncCallOptions::Agg(options) => {
789                    let (fused_funcs, fused_args): (Vec<_>, Vec<_>) = self
790                        .calls
791                        .iter()
792                        .map(|(_idx, call)| {
793                            if let HirScalarExpr::Windowing(
794                                WindowExpr {
795                                    func:
796                                        WindowExprType::Aggregate(AggregateWindowExpr {
797                                            aggregate_expr:
798                                                AggregateExpr {
799                                                    func,
800                                                    expr,
801                                                    distinct: _,
802                                                },
803                                            order_by: _,
804                                            window_frame: _,
805                                        }),
806                                    partition_by: _,
807                                    order_by: _,
808                                },
809                                _name,
810                            ) = call
811                            {
812                                (func.clone(), (**expr).clone())
813                            } else {
814                                panic!("unknown window function in FusionGroup")
815                            }
816                        })
817                        .unzip();
818                    let fused_args = HirScalarExpr::call_variadic(
819                        RecordCreate {
820                            field_names: iter::repeat(ColumnName::from(""))
821                                .take(fused_args.len())
822                                .collect(),
823                        },
824                        fused_args,
825                    );
826                    HirScalarExpr::windowing(WindowExpr {
827                        func: WindowExprType::Aggregate(AggregateWindowExpr {
828                            aggregate_expr: AggregateExpr {
829                                func: AggregateFunc::FusedWindowAgg { funcs: fused_funcs },
830                                expr: Box::new(fused_args),
831                                distinct: options.distinct,
832                            },
833                            order_by: options.inner_order_by,
834                            window_frame: options.window_frame,
835                        }),
836                        partition_by: options.partition_by,
837                        order_by: options.outer_order_by,
838                    })
839                }
840            };
841
842            let decompositions = (0..self.calls.len())
843                .map(|field| {
844                    HirScalarExpr::column(new_col)
845                        .call_unary(UnaryFunc::RecordGet(mz_expr::func::RecordGet(field)))
846                })
847                .collect();
848
849            (fused, decompositions)
850        }
851    }
852
853    let is_value_or_agg_window_func_call = |scalar_expr: &HirScalarExpr| -> bool {
854        // Look for calls only at the root of scalar expressions. This is enough
855        // because they are always there, see 72e84bb78.
856        match scalar_expr {
857            HirScalarExpr::Windowing(
858                WindowExpr {
859                    func: WindowExprType::Value(ValueWindowExpr { func, .. }),
860                    ..
861                },
862                _name,
863            ) => {
864                // Exclude those calls that are already fused. (We shouldn't currently
865                // encounter these, because we just do one pass, but it's better to be
866                // robust against future code changes.)
867                !matches!(func, ValueWindowFunc::Fused(..))
868            }
869            HirScalarExpr::Windowing(
870                WindowExpr {
871                    func:
872                        WindowExprType::Aggregate(AggregateWindowExpr {
873                            aggregate_expr: AggregateExpr { func, .. },
874                            ..
875                        }),
876                    ..
877                },
878                _name,
879            ) => !matches!(func, AggregateFunc::FusedWindowAgg { .. }),
880            _ => false,
881        }
882    };
883
884    root.try_visit_mut_post(&mut |rel_expr| {
885        match rel_expr {
886            HirRelationExpr::Map { input, scalars } => {
887                // There will be various variable names involving `idx` or `col`:
888                // - `idx` will always be an index into `scalars` or something similar,
889                // - `col` will always be a column index,
890                //   which is often `arity_before_map` + an index into `scalars`.
891                let arity_before_map = input.arity();
892                let orig_num_scalars = scalars.len();
893
894                // Collect all value window function calls and window aggregations with their column
895                // indexes.
896                let value_or_agg_window_func_calls = scalars
897                    .iter()
898                    .enumerate()
899                    .filter(|(_idx, scalar_expr)| is_value_or_agg_window_func_call(scalar_expr))
900                    .map(|(idx, call)| (idx + arity_before_map, call.clone()))
901                    .collect_vec();
902                // Exit early if obviously no chance for fusion.
903                if value_or_agg_window_func_calls.len() <= 1 {
904                    // Note that we are doing this only for performance. All plans should be exactly
905                    // the same even if we comment out the following line.
906                    return Ok(());
907                }
908
909                // Determine the fusion groups. (Each group will later be fused into one window
910                // function call.)
911                // Note that this has a quadratic run time with value_or_agg_window_func_calls in
912                // the worst case. However, this is fine even with 1000 window function calls.
913                let mut groups: Vec<FusionGroup> = Vec::new();
914                for (col, call) in value_or_agg_window_func_calls {
915                    let options = extract_options(&call);
916                    let support = call.support();
917                    let to_fuse_with = groups
918                        .iter_mut()
919                        .filter(|group| {
920                            group.options == options && support.iter().all(|c| *c < group.first_col)
921                        })
922                        .next();
923                    if let Some(group) = to_fuse_with {
924                        group.calls.push((col, call.clone()));
925                    } else {
926                        groups.push(FusionGroup {
927                            first_col: col,
928                            options,
929                            calls: vec![(col, call.clone())],
930                        });
931                    }
932                }
933
934                // No fusion to do on groups of 1.
935                groups.retain(|g| g.calls.len() > 1);
936
937                let removals: BTreeSet<usize> = groups
938                    .iter()
939                    .flat_map(|g| g.calls.iter().map(|(col, _)| *col))
940                    .collect();
941
942                // Mutate `scalars`.
943                // We do this by simultaneously iterating through `scalars` and `groups`. (Note that
944                // `groups` is already sorted by `first_col` due to the way it was constructed.)
945                // We also compute a remapping of old indexes to new indexes as we go.
946                let mut groups_it = groups.drain(..).peekable();
947                let mut group = groups_it.next();
948                let mut remap = BTreeMap::new();
949                remap.extend((0..arity_before_map).map(|col| (col, col)));
950                let mut new_col: usize = arity_before_map;
951                let mut new_scalars = Vec::new();
952                for (old_col, e) in scalars
953                    .drain(..)
954                    .enumerate()
955                    .map(|(idx, e)| (idx + arity_before_map, e))
956                {
957                    if group.as_ref().is_some_and(|g| g.first_col == old_col) {
958                        // The current expression will be fused away, and a fused expression will
959                        // appear in its place. Additionally, some new expressions will be inserted
960                        // after the fused expression, to decompose the record that is the result of
961                        // the fused call.
962                        assert!(removals.contains(&old_col));
963                        let group_unwrapped = group.expect("checked above");
964                        let calls_cols = group_unwrapped
965                            .calls
966                            .iter()
967                            .map(|(col, _call)| *col)
968                            .collect_vec();
969                        let (fused, decompositions) = group_unwrapped.fuse(new_col);
970                        new_scalars.push(fused.remap(&remap));
971                        new_scalars.extend(decompositions); // (no remapping needed)
972                        new_col += 1;
973                        for call_old_col in calls_cols {
974                            let present = remap.insert(call_old_col, new_col);
975                            assert!(present.is_none());
976                            new_col += 1;
977                        }
978                        group = groups_it.next();
979                    } else if removals.contains(&old_col) {
980                        assert!(remap.contains_key(&old_col));
981                    } else {
982                        new_scalars.push(e.remap(&remap));
983                        let present = remap.insert(old_col, new_col);
984                        assert!(present.is_none());
985                        new_col += 1;
986                    }
987                }
988                *scalars = new_scalars;
989                assert_eq!(remap.len(), arity_before_map + orig_num_scalars);
990
991                // Add a project to permute columns back to their original places.
992                *rel_expr = rel_expr.take().project(
993                    (0..arity_before_map)
994                        .chain((0..orig_num_scalars).map(|idx| {
995                            *remap
996                                .get(&(idx + arity_before_map))
997                                .expect("all columns should be present by now")
998                        }))
999                        .collect(),
1000                );
1001
1002                assert_eq!(rel_expr.arity(), arity_before_map + orig_num_scalars);
1003            }
1004            _ => {}
1005        }
1006        Ok(())
1007    })
1008}
1009
1010#[cfg(test)]
1011mod tests {
1012    use super::*;
1013
1014    /// A deeply nested, subquery-free relation tree must plan without
1015    /// overflowing the stack. The pre-decorrelation HIR walks
1016    /// (`split_subquery_predicates`, `try_simplify_quantified_comparisons`)
1017    /// recurse over its full, user-controlled depth, and the latter would
1018    /// otherwise recompute `input.typ()` at every level (O(depth^2)).
1019    #[mz_ore::test]
1020    #[cfg_attr(miri, ignore)] // error: unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux`
1021    fn deep_relation_chain_does_not_overflow() {
1022        const DEPTH: usize = 100_000;
1023        let mut expr = HirRelationExpr::constant(vec![], SqlRelationType::empty());
1024        for _ in 0..DEPTH {
1025            expr = HirRelationExpr::Filter {
1026                predicates: vec![],
1027                input: Box::new(expr),
1028            };
1029        }
1030
1031        split_subquery_predicates(&mut expr).unwrap();
1032        try_simplify_quantified_comparisons(&mut expr, false).unwrap();
1033
1034        // Dismantle iteratively: dropping the deep tree recursively would itself
1035        // overflow the stack.
1036        while let HirRelationExpr::Filter { input, .. } = expr {
1037            expr = *input;
1038        }
1039    }
1040
1041    /// A shallow relation whose scalar is a deeply nested `If` chain must plan
1042    /// without overflowing the stack. A flat `CASE` with many arms consumes no
1043    /// per-arm parser recursion but lowers to a right-nested `If` chain of that
1044    /// depth, so the subquery scan in `try_simplify_quantified_comparisons` must
1045    /// scan the scalar iteratively, not once per `If` node.
1046    #[mz_ore::test]
1047    #[cfg_attr(miri, ignore)] // error: unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux`
1048    fn deep_scalar_if_chain_does_not_overflow() {
1049        const DEPTH: usize = 100_000;
1050        let mut scalar = HirScalarExpr::literal_true();
1051        for _ in 0..DEPTH {
1052            scalar = HirScalarExpr::if_then_else(
1053                HirScalarExpr::literal_true(),
1054                HirScalarExpr::literal_true(),
1055                scalar,
1056            );
1057        }
1058        let mut expr = HirRelationExpr::Map {
1059            input: Box::new(HirRelationExpr::constant(vec![], SqlRelationType::empty())),
1060            scalars: vec![scalar],
1061        };
1062
1063        try_simplify_quantified_comparisons(&mut expr, false).unwrap();
1064
1065        // Dismantle the `If` chain iteratively: dropping it recursively would
1066        // itself overflow the stack.
1067        let HirRelationExpr::Map { mut scalars, .. } = expr else {
1068            unreachable!()
1069        };
1070        let mut scalar = scalars.pop().unwrap();
1071        while let HirScalarExpr::If { els, .. } = scalar {
1072            scalar = *els;
1073        }
1074    }
1075
1076    /// Once a subquery defeats the early bail in
1077    /// `try_simplify_quantified_comparisons`, `walk_relation` recurses over the
1078    /// full depth of the relation tree and must not overflow.
1079    ///
1080    /// The depth stays modest because `walk_relation` recomputes `input.typ()`
1081    /// at every level, which is O(depth^2). Running on a thread whose stack is
1082    /// smaller than `mz_ore::stack::STACK_RED_ZONE` is what makes the walk's
1083    /// `maybe_grow` load-bearing at that depth: without it, the walk overflows.
1084    #[mz_ore::test]
1085    #[cfg_attr(miri, ignore)] // error: unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux`
1086    fn deep_relation_chain_with_subquery_does_not_overflow() {
1087        const DEPTH: usize = 3_000;
1088        const THREAD_STACK_SIZE: usize = 256 << 10;
1089
1090        std::thread::Builder::new()
1091            .stack_size(THREAD_STACK_SIZE)
1092            .spawn(|| {
1093                let mut expr = HirRelationExpr::constant(vec![], SqlRelationType::empty());
1094                for _ in 0..DEPTH {
1095                    expr = HirRelationExpr::Filter {
1096                        predicates: vec![],
1097                        input: Box::new(expr),
1098                    };
1099                }
1100                // A single subquery anywhere in the tree is enough to make the
1101                // full-depth walk run.
1102                expr = HirRelationExpr::Filter {
1103                    predicates: vec![
1104                        HirRelationExpr::constant(vec![], SqlRelationType::empty()).exists(),
1105                    ],
1106                    input: Box::new(expr),
1107                };
1108
1109                try_simplify_quantified_comparisons(&mut expr, false).unwrap();
1110
1111                // Dismantle iteratively: dropping the deep tree recursively
1112                // would itself overflow this thread's small stack.
1113                while let HirRelationExpr::Filter { input, .. } = expr {
1114                    expr = *input;
1115                }
1116            })
1117            .unwrap()
1118            .join()
1119            .unwrap();
1120    }
1121}