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;
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    fn walk_relation(
175        expr: &mut HirRelationExpr,
176        outers: &[SqlRelationType],
177        simplify_join_on: bool,
178    ) -> Result<(), RecursionLimitError> {
179        match expr {
180            HirRelationExpr::Map { scalars, input } => {
181                walk_relation(input, outers, simplify_join_on)?;
182                let mut outers = outers.to_vec();
183                outers.insert(0, input.typ(&outers, &NO_PARAMS));
184                for scalar in scalars {
185                    walk_scalar(scalar, &outers, false, simplify_join_on)?;
186                    let (inner, outers) = outers
187                        .split_first_mut()
188                        .expect("outers known to have at least one element");
189                    let scalar_type = scalar.typ(outers, inner, &NO_PARAMS);
190                    inner.column_types.push(scalar_type);
191                }
192            }
193            HirRelationExpr::Filter { predicates, input } => {
194                walk_relation(input, outers, simplify_join_on)?;
195                let mut outers = outers.to_vec();
196                outers.insert(0, input.typ(&outers, &NO_PARAMS));
197                for pred in predicates {
198                    walk_scalar(pred, &outers, true, simplify_join_on)?;
199                }
200            }
201            HirRelationExpr::CallTable { exprs, .. } => {
202                let mut outers = outers.to_vec();
203                outers.insert(0, SqlRelationType::empty());
204                for scalar in exprs {
205                    walk_scalar(scalar, &outers, false, simplify_join_on)?;
206                }
207            }
208            HirRelationExpr::Join {
209                left, right, on, ..
210            } => {
211                walk_relation(left, outers, simplify_join_on)?;
212                let left_type = left.typ(outers, &NO_PARAMS);
213                let mut outers = outers.to_vec();
214                outers.insert(0, left_type);
215                walk_relation(right, &outers, simplify_join_on)?;
216                if simplify_join_on {
217                    // Build outers with the full join output type, since the
218                    // ON clause can reference columns from both sides.
219                    let right_type = right.typ(&outers, &NO_PARAMS);
220                    let mut join_columns = outers[0].column_types.clone();
221                    join_columns.extend(right_type.column_types);
222                    outers[0] = SqlRelationType::new(join_columns);
223                    walk_scalar(on, &outers, true, simplify_join_on)?;
224                }
225            }
226            expr => {
227                #[allow(deprecated)]
228                let _ = expr.visit1_mut(0, &mut |expr, _| -> Result<(), RecursionLimitError> {
229                    walk_relation(expr, outers, simplify_join_on)
230                });
231            }
232        }
233        Ok(())
234    }
235
236    fn walk_scalar(
237        expr: &mut HirScalarExpr,
238        outers: &[SqlRelationType],
239        mut in_filter: bool,
240        simplify_join_on: bool,
241    ) -> Result<(), RecursionLimitError> {
242        expr.try_visit_mut_pre(&mut |e| {
243            match e {
244                HirScalarExpr::Exists(input, _name) => {
245                    walk_relation(input, outers, simplify_join_on)?
246                }
247                HirScalarExpr::Select(input, _name) => {
248                    walk_relation(input, outers, simplify_join_on)?;
249
250                    // We're inside a `(SELECT ...)` subquery. Now let's see if
251                    // it has the form `(SELECT <any|all>(...) FROM <input>)`.
252                    // Ideally we could do this with one pattern, but Rust's pattern
253                    // matching engine is not powerful enough, so we have to do this
254                    // in stages; the early returns avoid brutal nesting.
255
256                    let (func, expr, input) = match &mut **input {
257                        HirRelationExpr::Reduce {
258                            group_key,
259                            aggregates,
260                            input,
261                            expected_group_size: _,
262                        } if group_key.is_empty() && aggregates.len() == 1 => {
263                            let agg = &mut aggregates[0];
264                            (&agg.func, &mut agg.expr, input)
265                        }
266                        _ => return Ok(()),
267                    };
268
269                    if !in_filter && column_type(outers, input, expr).nullable {
270                        // Unless we're directly inside a WHERE, this
271                        // transformation is only valid if the expression involved
272                        // is non-nullable.
273                        return Ok(());
274                    }
275
276                    match func {
277                        AggregateFunc::Any => {
278                            // Found `(SELECT any(<expr>) FROM <input>)`. Rewrite to
279                            // `EXISTS(SELECT 1 FROM <input> WHERE <expr>)`.
280                            *e = input.take().filter(vec![expr.take()]).exists();
281                        }
282                        AggregateFunc::All => {
283                            // Found `(SELECT all(<expr>) FROM <input>)`. Rewrite to
284                            // `NOT EXISTS(SELECT 1 FROM <input> WHERE NOT <expr> OR <expr> IS NULL)`.
285                            //
286                            // Note that negation of <expr> alone is insufficient.
287                            // Consider that `WHERE <pred>` filters out rows if
288                            // `<pred>` is false *or* null. To invert the test, we
289                            // need `NOT <pred> OR <pred> IS NULL`.
290                            let expr = expr.take();
291                            let filter = expr.clone().not().or(expr.call_is_null());
292                            *e = input.take().filter(vec![filter]).exists().not();
293                        }
294                        _ => (),
295                    }
296                }
297                _ => {
298                    // As soon as we see *any* scalar expression, we are no longer
299                    // directly inside a filter.
300                    in_filter = false;
301                }
302            }
303            Ok(())
304        })
305    }
306
307    walk_relation(expr, &[], simplify_join_on)
308}
309
310/// Collapses `EXISTS` over a FROM-less subquery into an equivalent scalar
311/// predicate on the outer row, so that decorrelation produces a plain `Filter`
312/// rather than a semijoin (for `EXISTS`) or an antijoin (for `NOT EXISTS`).
313///
314/// A FROM-less subquery is a chain of `Map`, `Project`, and `Filter` nodes over
315/// a single-row `Constant` (the join identity of a query with no `FROM`
316/// clause). Such a subquery yields exactly one row when every `Filter`
317/// predicate is `TRUE` and zero rows otherwise, so
318///
319/// ```text
320/// EXISTS(<from-less subquery with predicates p1, p2, ...>) == (p1 AND p2 AND ...) IS TRUE
321/// ```
322///
323/// evaluated on the outer row. The `IS TRUE` is mandatory for null safety. An
324/// empty subquery (some predicate `FALSE` or `NULL`) must make `EXISTS` return
325/// `FALSE`, which `IS TRUE` reproduces while a bare predicate would leak `NULL`.
326/// `NOT EXISTS` then becomes `NOT ((...) IS TRUE)`, which is `... IS NOT TRUE`
327/// and likewise null-safe.
328///
329/// The rewrite fires only on correlated subqueries, where the predicate
330/// references at least one outer column. This keeps it to the pure existence
331/// check that a genuine anti/semi-join would otherwise be lowered to, and it
332/// avoids changing whether an uncorrelated erroring subquery is evaluated when
333/// the outer relation is empty.
334///
335/// This closes database-issues#2613 (`x IN (SELECT ... WHERE p)`, which
336/// [`try_simplify_quantified_comparisons`] has already turned into an `EXISTS`)
337/// and database-issues#2969 (`NOT EXISTS (SELECT ... WHERE p)`). It must run
338/// after [`try_simplify_quantified_comparisons`].
339pub fn simplify_from_less_existence_subqueries(
340    expr: &mut HirRelationExpr,
341) -> Result<(), RecursionLimitError> {
342    // `try_visit_mut_post` walks every relation node, and because
343    // `VisitChildren<Self>` for `HirRelationExpr` descends into the bodies of
344    // `Exists`/`Select` subqueries, it reaches existence checks at every nesting
345    // level. Post-order guarantees a subquery body is simplified before the
346    // `Exists` that encloses it.
347    expr.try_visit_mut_post(&mut |rel| {
348        rel.try_visit_mut_children(|scalar: &mut HirScalarExpr| {
349            scalar.try_visit_mut_pre(&mut |e| {
350                if let HirScalarExpr::Exists(input, _name) = e {
351                    if let Some(pred) = from_less_existence_predicate(input) {
352                        *e = pred.call_unary(UnaryFunc::IsTrue(mz_expr::func::IsTrue));
353                    }
354                }
355                Ok(())
356            })
357        })
358    })
359}
360
361/// If `sub` is a FROM-less subquery (see
362/// [`simplify_from_less_existence_subqueries`]) whose existence check is
363/// correlated on the outer row, returns the predicate `p1 AND p2 AND ...`
364/// expressed in the outer row's frame. Returns `None` otherwise.
365fn from_less_existence_predicate(sub: &HirRelationExpr) -> Option<HirScalarExpr> {
366    // A FROM-less subquery is a linear Map/Project/Filter chain over a single-row
367    // `Constant`. Both properties of the base are load-bearing for soundness: the
368    // single row is what lets EXISTS reduce to "the predicate holds on that row",
369    // and the constant is what lets its columns be inlined into the lifted
370    // predicate below. A 0-row, multi-row, or non-constant base is a genuine
371    // anti/semi-join and bails at the `_` arm.
372    //
373    // Record the chain top to bottom here; it is replayed bottom to top below.
374    let mut chain = Vec::new();
375    let mut cur = sub;
376    let (row, typ) = loop {
377        match cur {
378            HirRelationExpr::Filter { input, .. }
379            | HirRelationExpr::Map { input, .. }
380            | HirRelationExpr::Project { input, .. } => {
381                chain.push(cur);
382                cur = input.as_ref();
383            }
384            HirRelationExpr::Constant { rows, typ } if rows.len() == 1 => break (&rows[0], typ),
385            _ => return None,
386        }
387    };
388
389    // `env` holds the value of each column of the current relation, expressed in
390    // the subquery's own frame. Because level-0 references are resolved as we go,
391    // env entries only ever contain constants and outer (level >= 1) references.
392    let mut env: Vec<HirScalarExpr> = row
393        .iter()
394        .zip_eq(typ.column_types.iter())
395        .map(|(datum, col_type)| HirScalarExpr::literal(datum, col_type.scalar_type.clone()))
396        .collect();
397
398    // Replay the chain bottom to top so each node sees the `env` built by the nodes
399    // beneath it: `Map` extends `env`, `Filter` reads it, `Project` permutes it.
400    let mut preds: Vec<HirScalarExpr> = Vec::new();
401    for node in chain.iter().rev() {
402        match node {
403            HirRelationExpr::Filter { predicates, .. } => {
404                for predicate in predicates {
405                    preds.push(resolve_local_columns(predicate, &env)?);
406                }
407            }
408            HirRelationExpr::Map { scalars, .. } => {
409                for scalar in scalars {
410                    let resolved = resolve_local_columns(scalar, &env)?;
411                    env.push(resolved);
412                }
413            }
414            HirRelationExpr::Project { outputs, .. } => {
415                env = outputs
416                    .iter()
417                    .map(|i| env.get(*i).cloned())
418                    .collect::<Option<Vec<_>>>()?;
419            }
420            _ => unreachable!("chain only contains Filter, Map, and Project nodes"),
421        }
422    }
423
424    let mut pred = HirScalarExpr::variadic_and(preds);
425
426    // `pred` is built only from predicates that `resolve_local_columns` accepted,
427    // and that rejects any subquery, so `pred` contains no nested subqueries. Every
428    // column reference is therefore in the subquery's own frame at nesting depth 0,
429    // and an outer reference is exactly one with `level > 0`.
430
431    // Require correlation: the predicate must reference an outer column. Without
432    // correlation this is not the existence check a genuine anti/semi-join lowers
433    // to, and firing would risk changing when a constant erroring predicate is
434    // evaluated.
435    let mut correlated = false;
436    pred.visit_post(&mut |e| {
437        if let HirScalarExpr::Column(col, _name) = e {
438            if col.level > 0 {
439                correlated = true;
440            }
441        }
442    });
443    if !correlated {
444        return None;
445    }
446
447    // Lift the predicate out of the subquery: references to the immediately
448    // enclosing (outer) scope move down one level.
449    pred.visit_mut_post(&mut |e| {
450        if let HirScalarExpr::Column(col, _name) = e {
451            if col.level > 0 {
452                col.level -= 1;
453            }
454        }
455    });
456
457    Some(pred)
458}
459
460/// Returns `expr` with every reference to the current scope (a [`ColumnRef`]
461/// with `level == 0`) replaced by its value from `env`. Returns `None` if `expr`
462/// cannot be soundly lifted into the outer scope, or references a column absent
463/// from `env`.
464fn resolve_local_columns(expr: &HirScalarExpr, env: &[HirScalarExpr]) -> Option<HirScalarExpr> {
465    // Every scalar in the FROM-less body is substituted into the outer scope, so
466    // reject any that cannot be evaluated equivalently there. The match is
467    // exhaustive on purpose: a new `HirScalarExpr` variant must be classified here
468    // rather than silently treated as liftable.
469    let mut unliftable = false;
470    expr.visit_post(&mut |e| {
471        let liftable = match e {
472            // Row-local: the value depends only on the row, so it is the same in
473            // the subquery's frame and the outer frame.
474            HirScalarExpr::Column(..)
475            | HirScalarExpr::Parameter(..)
476            | HirScalarExpr::Literal(..)
477            | HirScalarExpr::CallUnmaterializable(..)
478            | HirScalarExpr::CallUnary { .. }
479            | HirScalarExpr::CallBinary { .. }
480            | HirScalarExpr::CallVariadic { .. }
481            | HirScalarExpr::If { .. } => true,
482            // A subquery carries its own nested scopes that this flat substitution
483            // does not handle. A window function over the single-row body (e.g.
484            // `row_number() OVER ()` is always 1) is not the same function over the
485            // multi-row outer relation. Neither may cross the subquery boundary.
486            HirScalarExpr::Exists(..)
487            | HirScalarExpr::Select(..)
488            | HirScalarExpr::Windowing(..) => false,
489        };
490        unliftable |= !liftable;
491    });
492    if unliftable {
493        return None;
494    }
495
496    let mut expr = expr.clone();
497    let mut ok = true;
498    expr.visit_mut_post(&mut |e| {
499        if let HirScalarExpr::Column(ColumnRef { level: 0, column }, _name) = e {
500            match env.get(*column) {
501                Some(value) => *e = value.clone(),
502                None => ok = false,
503            }
504        }
505    });
506    ok.then_some(expr)
507}
508
509/// An empty parameter type map.
510///
511/// These transformations are expected to run after parameters are bound, so
512/// there is no need to provide any parameter type information.
513static NO_PARAMS: LazyLock<BTreeMap<usize, SqlScalarType>> = LazyLock::new(BTreeMap::new);
514
515fn column_type(
516    outers: &[SqlRelationType],
517    inner: &HirRelationExpr,
518    expr: &HirScalarExpr,
519) -> SqlColumnType {
520    let inner_type = inner.typ(outers, &NO_PARAMS);
521    expr.typ(outers, &inner_type, &NO_PARAMS)
522}
523
524impl HirScalarExpr {
525    /// Similar to `MirScalarExpr::support`, but adapted to `HirScalarExpr` in a special way: it
526    /// considers column references that target the root level.
527    /// (See `visit_columns_referring_to_root_level`.)
528    fn support(&self) -> Vec<usize> {
529        let mut result = Vec::new();
530        self.visit_columns_referring_to_root_level(&mut |c| result.push(c));
531        result
532    }
533
534    /// Changes column references in `self` by the given remapping.
535    /// Panics if a referred column is not present in `idx_map`!
536    fn remap(mut self, idx_map: &BTreeMap<usize, usize>) -> HirScalarExpr {
537        self.visit_columns_referring_to_root_level_mut(&mut |c| {
538            *c = idx_map[c];
539        });
540        self
541    }
542}
543
544/// # Aims and scope
545///
546/// The aim here is to amortize the overhead of the MIR window function pattern
547/// (see `window_func_applied_to`) by fusing groups of window function calls such
548/// that each group can be performed by one instance of the window function MIR
549/// pattern.
550///
551/// For now, we fuse only value window function calls and window aggregations.
552/// (We probably won't need to fuse scalar window functions for a long time.)
553///
554/// For now, we can fuse value window function calls and window aggregations where the
555/// A. partition by
556/// B. order by
557/// C. window frame
558/// D. ignore nulls for value window functions and distinct for window aggregations
559/// are all the same. (See `extract_options`.)
560/// (Later, we could improve this to only need A. to be the same. This would require
561/// much more code changes, because then we'd have to blow up `ValueWindowExpr`.
562/// TODO: As a much simpler intermediate step, at least we should ignore options that
563/// don't matter. For example, we should be able to fuse a `lag` that has a default
564/// frame with a `first_value` that has some custom frame, because `lag` is not
565/// affected by the frame.)
566/// Note that we fuse value window function calls and window aggregations separately.
567///
568/// # Implementation
569///
570/// At a high level, what we are going to do is look for Maps with more than one window function
571/// calls, and for each Map
572/// - remove some groups of window function call expressions from the Map's `scalars`;
573/// - insert a fused version of each group;
574/// - insert some expressions that decompose the results of the fused calls;
575/// - update some column references in `scalars`: those that refer to window function results that
576///   participated in fusion, as well as those that refer to columns that moved around due to
577///   removing and inserting expressions.
578/// - insert a Project above the matched Map to permute columns back to their original places.
579///
580/// It would be tempting to find groups simply by taking a list of all window function calls
581/// and calling `group_by` with a key function that extracts the above A. B. C. D. properties,
582/// but a complication is that the possible groups that we could theoretically fuse overlap.
583/// This is because when forming groups we need to also take into account column references
584/// that point inside the same Map. For example, imagine a Map with the following scalar
585/// expressions:
586/// C1, E1, C2, C3, where
587/// - E1 refers to C1
588/// - C3 refers to E1.
589/// In this situation, we could either
590/// - fuse C1 and C2, and put the fused expression in the place of C1 (so that E1 can keep referring
591///   to it);
592/// - or fuse C2 and C3.
593/// However, we can't fuse all of C1, C2, C3 into one call, because then there would be
594/// no appropriate place for the fused expression: it would have to be both before and after E1.
595///
596/// So, how we actually form the groups is that, keeping track of a list of non-overlapping groups,
597/// we go through `scalars`, try to put each expression into each of our groups, and the first of
598/// these succeed. When trying to put an expression into a group, we need to be mindful about column
599/// references inside the same Map, as noted above. A constraint that we impose on ourselves for
600/// sanity is that the fused version of each group will be inserted at the place where the first
601/// element of the group originally was. This means that the only condition that we need to check on
602/// column references when adding an expression to a group is that all column references in a group
603/// should be to columns that are earlier than the first element of the group. (No need to check
604/// column references in the other direction, i.e., references in other expressions that refer to
605/// columns in the group.)
606pub fn fuse_window_functions(
607    root: &mut HirRelationExpr,
608    _context: &crate::plan::lowering::Context,
609) -> Result<(), RecursionLimitError> {
610    /// Those options of a window function call that are relevant for fusion.
611    #[derive(PartialEq, Eq)]
612    enum WindowFuncCallOptions {
613        Value(ValueWindowFuncCallOptions),
614        Agg(AggregateWindowFuncCallOptions),
615    }
616    #[derive(PartialEq, Eq)]
617    struct ValueWindowFuncCallOptions {
618        partition_by: Vec<HirScalarExpr>,
619        outer_order_by: Vec<HirScalarExpr>,
620        inner_order_by: Vec<ColumnOrder>,
621        window_frame: WindowFrame,
622        ignore_nulls: bool,
623    }
624    #[derive(PartialEq, Eq)]
625    struct AggregateWindowFuncCallOptions {
626        partition_by: Vec<HirScalarExpr>,
627        outer_order_by: Vec<HirScalarExpr>,
628        inner_order_by: Vec<ColumnOrder>,
629        window_frame: WindowFrame,
630        distinct: bool,
631    }
632
633    /// Helper function to extract the above options.
634    fn extract_options(call: &HirScalarExpr) -> WindowFuncCallOptions {
635        match call {
636            HirScalarExpr::Windowing(
637                WindowExpr {
638                    func:
639                        WindowExprType::Value(ValueWindowExpr {
640                            order_by: inner_order_by,
641                            window_frame,
642                            ignore_nulls,
643                            func: _,
644                            args: _,
645                        }),
646                    partition_by,
647                    order_by: outer_order_by,
648                },
649                _name,
650            ) => WindowFuncCallOptions::Value(ValueWindowFuncCallOptions {
651                partition_by: partition_by.clone(),
652                outer_order_by: outer_order_by.clone(),
653                inner_order_by: inner_order_by.clone(),
654                window_frame: window_frame.clone(),
655                ignore_nulls: ignore_nulls.clone(),
656            }),
657            HirScalarExpr::Windowing(
658                WindowExpr {
659                    func:
660                        WindowExprType::Aggregate(AggregateWindowExpr {
661                            aggregate_expr:
662                                AggregateExpr {
663                                    distinct,
664                                    func: _,
665                                    expr: _,
666                                },
667                            order_by: inner_order_by,
668                            window_frame,
669                        }),
670                    partition_by,
671                    order_by: outer_order_by,
672                },
673                _name,
674            ) => WindowFuncCallOptions::Agg(AggregateWindowFuncCallOptions {
675                partition_by: partition_by.clone(),
676                outer_order_by: outer_order_by.clone(),
677                inner_order_by: inner_order_by.clone(),
678                window_frame: window_frame.clone(),
679                distinct: distinct.clone(),
680            }),
681            _ => panic!(
682                "extract_options should only be called on value window functions or window aggregations"
683            ),
684        }
685    }
686
687    struct FusionGroup {
688        /// The original column index of the first element of the group. (This is an index into the
689        /// Map's `scalars` plus the arity of the Map's input.)
690        first_col: usize,
691        /// The options of all the window function calls in the group. (Must be the same for all the
692        /// calls.)
693        options: WindowFuncCallOptions,
694        /// The calls in the group, with their original column indexes.
695        calls: Vec<(usize, HirScalarExpr)>,
696    }
697
698    impl FusionGroup {
699        /// Creates a window function call that is a fused version of all the calls in the group.
700        /// `new_col` is the column index where the fused call will be inserted at.
701        fn fuse(self, new_col: usize) -> (HirScalarExpr, Vec<HirScalarExpr>) {
702            let fused = match self.options {
703                WindowFuncCallOptions::Value(options) => {
704                    let (fused_funcs, fused_args): (Vec<_>, Vec<_>) = self
705                        .calls
706                        .iter()
707                        .map(|(_idx, call)| {
708                            if let HirScalarExpr::Windowing(
709                                WindowExpr {
710                                    func:
711                                        WindowExprType::Value(ValueWindowExpr {
712                                            func,
713                                            args,
714                                            order_by: _,
715                                            window_frame: _,
716                                            ignore_nulls: _,
717                                        }),
718                                    partition_by: _,
719                                    order_by: _,
720                                },
721                                _name,
722                            ) = call
723                            {
724                                (func.clone(), (**args).clone())
725                            } else {
726                                panic!("unknown window function in FusionGroup")
727                            }
728                        })
729                        .unzip();
730                    let fused_args = HirScalarExpr::call_variadic(
731                        RecordCreate {
732                            // These field names are not important, because this record will only be an
733                            // intermediate expression, which we'll manipulate further before it ends up
734                            // anywhere where a column name would be visible.
735                            field_names: iter::repeat(ColumnName::from(""))
736                                .take(fused_args.len())
737                                .collect(),
738                        },
739                        fused_args,
740                    );
741                    HirScalarExpr::windowing(WindowExpr {
742                        func: WindowExprType::Value(ValueWindowExpr {
743                            func: ValueWindowFunc::Fused(fused_funcs),
744                            args: Box::new(fused_args),
745                            order_by: options.inner_order_by,
746                            window_frame: options.window_frame,
747                            ignore_nulls: options.ignore_nulls,
748                        }),
749                        partition_by: options.partition_by,
750                        order_by: options.outer_order_by,
751                    })
752                }
753                WindowFuncCallOptions::Agg(options) => {
754                    let (fused_funcs, fused_args): (Vec<_>, Vec<_>) = self
755                        .calls
756                        .iter()
757                        .map(|(_idx, call)| {
758                            if let HirScalarExpr::Windowing(
759                                WindowExpr {
760                                    func:
761                                        WindowExprType::Aggregate(AggregateWindowExpr {
762                                            aggregate_expr:
763                                                AggregateExpr {
764                                                    func,
765                                                    expr,
766                                                    distinct: _,
767                                                },
768                                            order_by: _,
769                                            window_frame: _,
770                                        }),
771                                    partition_by: _,
772                                    order_by: _,
773                                },
774                                _name,
775                            ) = call
776                            {
777                                (func.clone(), (**expr).clone())
778                            } else {
779                                panic!("unknown window function in FusionGroup")
780                            }
781                        })
782                        .unzip();
783                    let fused_args = HirScalarExpr::call_variadic(
784                        RecordCreate {
785                            field_names: iter::repeat(ColumnName::from(""))
786                                .take(fused_args.len())
787                                .collect(),
788                        },
789                        fused_args,
790                    );
791                    HirScalarExpr::windowing(WindowExpr {
792                        func: WindowExprType::Aggregate(AggregateWindowExpr {
793                            aggregate_expr: AggregateExpr {
794                                func: AggregateFunc::FusedWindowAgg { funcs: fused_funcs },
795                                expr: Box::new(fused_args),
796                                distinct: options.distinct,
797                            },
798                            order_by: options.inner_order_by,
799                            window_frame: options.window_frame,
800                        }),
801                        partition_by: options.partition_by,
802                        order_by: options.outer_order_by,
803                    })
804                }
805            };
806
807            let decompositions = (0..self.calls.len())
808                .map(|field| {
809                    HirScalarExpr::column(new_col)
810                        .call_unary(UnaryFunc::RecordGet(mz_expr::func::RecordGet(field)))
811                })
812                .collect();
813
814            (fused, decompositions)
815        }
816    }
817
818    let is_value_or_agg_window_func_call = |scalar_expr: &HirScalarExpr| -> bool {
819        // Look for calls only at the root of scalar expressions. This is enough
820        // because they are always there, see 72e84bb78.
821        match scalar_expr {
822            HirScalarExpr::Windowing(
823                WindowExpr {
824                    func: WindowExprType::Value(ValueWindowExpr { func, .. }),
825                    ..
826                },
827                _name,
828            ) => {
829                // Exclude those calls that are already fused. (We shouldn't currently
830                // encounter these, because we just do one pass, but it's better to be
831                // robust against future code changes.)
832                !matches!(func, ValueWindowFunc::Fused(..))
833            }
834            HirScalarExpr::Windowing(
835                WindowExpr {
836                    func:
837                        WindowExprType::Aggregate(AggregateWindowExpr {
838                            aggregate_expr: AggregateExpr { func, .. },
839                            ..
840                        }),
841                    ..
842                },
843                _name,
844            ) => !matches!(func, AggregateFunc::FusedWindowAgg { .. }),
845            _ => false,
846        }
847    };
848
849    root.try_visit_mut_post(&mut |rel_expr| {
850        match rel_expr {
851            HirRelationExpr::Map { input, scalars } => {
852                // There will be various variable names involving `idx` or `col`:
853                // - `idx` will always be an index into `scalars` or something similar,
854                // - `col` will always be a column index,
855                //   which is often `arity_before_map` + an index into `scalars`.
856                let arity_before_map = input.arity();
857                let orig_num_scalars = scalars.len();
858
859                // Collect all value window function calls and window aggregations with their column
860                // indexes.
861                let value_or_agg_window_func_calls = scalars
862                    .iter()
863                    .enumerate()
864                    .filter(|(_idx, scalar_expr)| is_value_or_agg_window_func_call(scalar_expr))
865                    .map(|(idx, call)| (idx + arity_before_map, call.clone()))
866                    .collect_vec();
867                // Exit early if obviously no chance for fusion.
868                if value_or_agg_window_func_calls.len() <= 1 {
869                    // Note that we are doing this only for performance. All plans should be exactly
870                    // the same even if we comment out the following line.
871                    return Ok(());
872                }
873
874                // Determine the fusion groups. (Each group will later be fused into one window
875                // function call.)
876                // Note that this has a quadratic run time with value_or_agg_window_func_calls in
877                // the worst case. However, this is fine even with 1000 window function calls.
878                let mut groups: Vec<FusionGroup> = Vec::new();
879                for (col, call) in value_or_agg_window_func_calls {
880                    let options = extract_options(&call);
881                    let support = call.support();
882                    let to_fuse_with = groups
883                        .iter_mut()
884                        .filter(|group| {
885                            group.options == options && support.iter().all(|c| *c < group.first_col)
886                        })
887                        .next();
888                    if let Some(group) = to_fuse_with {
889                        group.calls.push((col, call.clone()));
890                    } else {
891                        groups.push(FusionGroup {
892                            first_col: col,
893                            options,
894                            calls: vec![(col, call.clone())],
895                        });
896                    }
897                }
898
899                // No fusion to do on groups of 1.
900                groups.retain(|g| g.calls.len() > 1);
901
902                let removals: BTreeSet<usize> = groups
903                    .iter()
904                    .flat_map(|g| g.calls.iter().map(|(col, _)| *col))
905                    .collect();
906
907                // Mutate `scalars`.
908                // We do this by simultaneously iterating through `scalars` and `groups`. (Note that
909                // `groups` is already sorted by `first_col` due to the way it was constructed.)
910                // We also compute a remapping of old indexes to new indexes as we go.
911                let mut groups_it = groups.drain(..).peekable();
912                let mut group = groups_it.next();
913                let mut remap = BTreeMap::new();
914                remap.extend((0..arity_before_map).map(|col| (col, col)));
915                let mut new_col: usize = arity_before_map;
916                let mut new_scalars = Vec::new();
917                for (old_col, e) in scalars
918                    .drain(..)
919                    .enumerate()
920                    .map(|(idx, e)| (idx + arity_before_map, e))
921                {
922                    if group.as_ref().is_some_and(|g| g.first_col == old_col) {
923                        // The current expression will be fused away, and a fused expression will
924                        // appear in its place. Additionally, some new expressions will be inserted
925                        // after the fused expression, to decompose the record that is the result of
926                        // the fused call.
927                        assert!(removals.contains(&old_col));
928                        let group_unwrapped = group.expect("checked above");
929                        let calls_cols = group_unwrapped
930                            .calls
931                            .iter()
932                            .map(|(col, _call)| *col)
933                            .collect_vec();
934                        let (fused, decompositions) = group_unwrapped.fuse(new_col);
935                        new_scalars.push(fused.remap(&remap));
936                        new_scalars.extend(decompositions); // (no remapping needed)
937                        new_col += 1;
938                        for call_old_col in calls_cols {
939                            let present = remap.insert(call_old_col, new_col);
940                            assert!(present.is_none());
941                            new_col += 1;
942                        }
943                        group = groups_it.next();
944                    } else if removals.contains(&old_col) {
945                        assert!(remap.contains_key(&old_col));
946                    } else {
947                        new_scalars.push(e.remap(&remap));
948                        let present = remap.insert(old_col, new_col);
949                        assert!(present.is_none());
950                        new_col += 1;
951                    }
952                }
953                *scalars = new_scalars;
954                assert_eq!(remap.len(), arity_before_map + orig_num_scalars);
955
956                // Add a project to permute columns back to their original places.
957                *rel_expr = rel_expr.take().project(
958                    (0..arity_before_map)
959                        .chain((0..orig_num_scalars).map(|idx| {
960                            *remap
961                                .get(&(idx + arity_before_map))
962                                .expect("all columns should be present by now")
963                        }))
964                        .collect(),
965                );
966
967                assert_eq!(rel_expr.arity(), arity_before_map + orig_num_scalars);
968            }
969            _ => {}
970        }
971        Ok(())
972    })
973}