Skip to main content

mz_expr/scalar/reduce/
variadic.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//! Post-order rewrites for `CallVariadic` nodes.
11
12use std::collections::BTreeSet;
13use std::mem;
14
15use mz_ore::collections::CollectionExt;
16use mz_pgtz::timezone::TimezoneSpec;
17use mz_repr::{Datum, ReprColumnType, ReprScalarType, RowArena, SqlScalarType};
18
19use crate::scalar::func::variadic::{Coalesce, ListCreate, ListIndex};
20use crate::scalar::func::{
21    self, BinaryFunc, UnaryFunc, VariadicFunc, parse_timezone, regexp_replace_parse_flags,
22};
23use crate::{Eval, MirScalarExpr};
24
25pub(super) fn reduce_call_variadic(
26    e: &mut MirScalarExpr,
27    column_types: &[ReprColumnType],
28    temp_storage: &RowArena,
29) {
30    // Flatten chains of associative variadic calls before any per-`func`
31    // dispatch. `undistribute_and_or` below relies on this having run.
32    e.flatten_associative();
33
34    let MirScalarExpr::CallVariadic { func, exprs } = e else {
35        unreachable!("`flatten_associative` shouldn't change node type");
36    };
37
38    // Coalesce has its own simplification routine that handles null/error
39    // propagation internally — bail out to it.
40    if *func == Coalesce.into() {
41        simplify_coalesce(e, column_types);
42        return;
43    }
44
45    // Generic folds: constant-fold, null-propagate, error-propagate.
46    if exprs.iter().all(|x| x.is_literal()) {
47        *e = MirScalarExpr::literal(e.eval(&[], temp_storage), e.typ(column_types).scalar_type);
48        return;
49    }
50    if func.propagates_nulls() && exprs.iter().any(|x| x.is_literal_null()) {
51        *e = MirScalarExpr::literal_null(e.typ(column_types).scalar_type);
52        return;
53    }
54    if let Some(err) = exprs.iter().find_map(|x| x.as_literal_err()) {
55        *e = MirScalarExpr::literal(Err(err.clone()), e.typ(column_types).scalar_type);
56        return;
57    }
58
59    // Per-function dispatch. Arms are mutually exclusive on discriminant; the
60    // bodies only fire when their literal-argument guards hold.
61    match func {
62        VariadicFunc::Greatest(_) | VariadicFunc::Least(_) => {
63            reduce_greatest_least(e, column_types);
64        }
65        VariadicFunc::Substr(_)
66            if exprs.len() == 2 && matches!(exprs[1].as_literal(), Some(Ok(Datum::Int32(1)))) =>
67        {
68            // `substr(s, 1)` — the two-argument form — keeps the entire
69            // string, and its evaluation at a start of one is infallible.
70            *e = exprs.swap_remove(0);
71        }
72        VariadicFunc::RegexpMatch(_)
73            if exprs[1].is_literal() && exprs.get(2).map_or(true, |e| e.is_literal()) =>
74        {
75            let needle = exprs[1].as_literal_str().unwrap();
76            let flags = if exprs.len() == 3 {
77                exprs[2].as_literal_str().unwrap()
78            } else {
79                ""
80            };
81            *e = match func::build_regex(needle, flags) {
82                Ok(regex) => mem::take(exprs)
83                    .into_first()
84                    .call_unary(UnaryFunc::RegexpMatch(func::RegexpMatch(regex))),
85                Err(err) => MirScalarExpr::literal(Err(err), e.typ(column_types).scalar_type),
86            };
87        }
88        VariadicFunc::RegexpReplace(_)
89            if exprs[1].is_literal() && exprs.get(3).map_or(true, |e| e.is_literal()) =>
90        {
91            let pattern = exprs[1].as_literal_str().unwrap();
92            let flags = exprs
93                .get(3)
94                .map_or("", |expr| expr.as_literal_str().unwrap());
95            let (limit, flags) = regexp_replace_parse_flags(flags);
96
97            // The behavior of `regexp_replace` is that if the data is `NULL`, the
98            // function returns `NULL`, independently of whether the pattern or
99            // flags are correct. We need to check for this case and introduce an
100            // if-then-else on the error path to only surface the error if both
101            // the source and replacement inputs are non-NULL.
102            *e = match func::build_regex(pattern, &flags) {
103                Ok(regex) => {
104                    let mut exprs = mem::take(exprs);
105                    let replacement = exprs.swap_remove(2);
106                    let source = exprs.swap_remove(0);
107                    source.call_binary(
108                        replacement,
109                        BinaryFunc::from(func::RegexpReplace { regex, limit }),
110                    )
111                }
112                Err(err) => {
113                    let mut exprs = mem::take(exprs);
114                    let replacement = exprs.swap_remove(2);
115                    let source = exprs.swap_remove(0);
116                    let scalar_type = e.typ(column_types).scalar_type;
117                    // We need to return `NULL` on `NULL` input, and error otherwise.
118                    source
119                        .call_is_null()
120                        .or(replacement.call_is_null())
121                        .if_then_else(
122                            MirScalarExpr::literal_null(scalar_type.clone()),
123                            MirScalarExpr::literal(Err(err), scalar_type),
124                        )
125                }
126            };
127        }
128        VariadicFunc::RegexpSplitToArray(_)
129            if exprs[1].is_literal() && exprs.get(2).map_or(true, |e| e.is_literal()) =>
130        {
131            let needle = exprs[1].as_literal_str().unwrap();
132            let flags = if exprs.len() == 3 {
133                exprs[2].as_literal_str().unwrap()
134            } else {
135                ""
136            };
137            *e = match func::build_regex(needle, flags) {
138                Ok(regex) => {
139                    mem::take(exprs)
140                        .into_first()
141                        .call_unary(UnaryFunc::RegexpSplitToArray(func::RegexpSplitToArray(
142                            regex,
143                        )))
144                }
145                Err(err) => MirScalarExpr::literal(Err(err), e.typ(column_types).scalar_type),
146            };
147        }
148        VariadicFunc::ListIndex(_) if is_list_create_call(&exprs[0]) => {
149            // We are looking for ListIndex(ListCreate, literal), and eliminate
150            // both the ListIndex and the ListCreate. E.g.: `LIST[f1,f2][2]` --> `f2`
151            let ind_exprs = exprs.split_off(1);
152            let top_list_create = exprs.swap_remove(0);
153            *e = reduce_list_create_list_index_literal(top_list_create, ind_exprs);
154        }
155        VariadicFunc::And(_) | VariadicFunc::Or(_) => {
156            // Note: It's important that we have called `flatten_associative` above.
157            e.undistribute_and_or();
158            e.reduce_and_canonicalize_and_or();
159        }
160        VariadicFunc::TimezoneTimeVariadic(_)
161            if exprs[0].is_literal() && exprs[2].is_literal_ok() =>
162        {
163            let tz = exprs[0].as_literal_str().unwrap();
164            *e = match parse_timezone(tz, TimezoneSpec::Posix) {
165                Ok(tz) => MirScalarExpr::CallUnary {
166                    func: UnaryFunc::TimezoneTime(func::TimezoneTime {
167                        tz,
168                        wall_time: exprs[2]
169                            .as_literal()
170                            .unwrap()
171                            .unwrap()
172                            .unwrap_timestamptz()
173                            .naive_utc(),
174                    }),
175                    expr: Box::new(exprs[1].take()),
176                },
177                Err(err) => MirScalarExpr::literal(Err(err), e.typ(column_types).scalar_type),
178            };
179        }
180        _ => {}
181    }
182}
183
184/// Simplifies a `Greatest`/`Least` call:
185/// 1. Deduplicate structurally equal operands, keeping first occurrences.
186///    Scalar evaluation is deterministic (the `And`/`Or` and `Coalesce`
187///    reducers already rely on this when they deduplicate), so duplicates of
188///    an expression contribute the same value — over which `greatest`/`least`
189///    are idempotent — or the same error; keeping the first occurrence leaves
190///    unchanged which error surfaces.
191/// 2. Drop literal null operands: both functions ignore null inputs (they
192///    return the max/min of the non-null inputs, and null only when every
193///    input is null).
194/// 3. A call left with a single operand is the identity on it — the call
195///    evaluates the operand once and returns it, null or not — and a call
196///    left with none is null.
197fn reduce_greatest_least(e: &mut MirScalarExpr, column_types: &[ReprColumnType]) {
198    let typ = e.typ(column_types).scalar_type;
199    let MirScalarExpr::CallVariadic { exprs, .. } = e else {
200        unreachable!()
201    };
202    let mut seen = BTreeSet::new();
203    exprs.retain(|x| seen.insert(x.clone()));
204    exprs.retain(|x| !x.is_literal_null());
205    match exprs.len() {
206        0 => *e = MirScalarExpr::literal_null(typ),
207        1 => *e = exprs.swap_remove(0),
208        _ => {}
209    }
210}
211
212/// Simplifies a `Coalesce`:
213/// 1. If all arguments are null, the result is null.
214/// 2. Drop null arguments (none of them can be the result).
215/// 3. Truncate after the first argument known to be non-null (a literal or a
216///    non-nullable column).
217/// 4. Deduplicate arguments (e.g. `coalesce(#0, #0) → coalesce(#0)`).
218/// 5. Unwrap a single-argument `coalesce`.
219fn simplify_coalesce(e: &mut MirScalarExpr, column_types: &[ReprColumnType]) {
220    let MirScalarExpr::CallVariadic { exprs, .. } = e else {
221        unreachable!()
222    };
223
224    // If all inputs are null, output is null. This check must
225    // be done before `exprs.retain...` because `e.typ` requires
226    // > 0 `exprs` remain.
227    if exprs.iter().all(|x| x.is_literal_null()) {
228        *e = MirScalarExpr::literal_null(e.typ(column_types).scalar_type);
229        return;
230    }
231
232    // Remove any null values if not all values are null.
233    exprs.retain(|x| !x.is_literal_null());
234
235    // Find the first argument that is a literal or non-nullable
236    // column. All arguments after it get ignored, so throw them
237    // away. This intentionally throws away errors that can
238    // never happen.
239    if let Some(i) = exprs
240        .iter()
241        .position(|x| x.is_literal() || !x.typ(column_types).nullable)
242    {
243        exprs.truncate(i + 1);
244    }
245
246    // Deduplicate arguments in cases like `coalesce(#0, #0)`.
247    let mut seen = BTreeSet::new();
248    exprs.retain(|x| seen.insert(x.clone()));
249
250    if exprs.len() == 1 {
251        // Only one argument, so the coalesce is a no-op.
252        *e = exprs[0].take();
253    }
254}
255
256fn is_list_create_call(expr: &MirScalarExpr) -> bool {
257    matches!(
258        expr,
259        MirScalarExpr::CallVariadic {
260            func: VariadicFunc::ListCreate(..),
261            ..
262        }
263    )
264}
265
266fn list_create_type(list_create: &MirScalarExpr) -> ReprScalarType {
267    if let MirScalarExpr::CallVariadic {
268        func: VariadicFunc::ListCreate(ListCreate { elem_type: typ }),
269        ..
270    } = list_create
271    {
272        ReprScalarType::from(typ)
273    } else {
274        unreachable!()
275    }
276}
277
278/// Partial-evaluates a list indexing with a literal directly after a list creation.
279///
280/// Multi-dimensional lists are handled by a single call to this function, with multiple
281/// elements in index_exprs (of which not all need to be literals), and nested ListCreates
282/// in list_create_to_reduce.
283///
284/// # Examples
285///
286/// `LIST[f1,f2][2]` --> `f2`.
287///
288/// A multi-dimensional list, with only some of the indexes being literals:
289/// `LIST[[[f1, f2], [f3, f4]], [[f5, f6], [f7, f8]]] [2][n][2]` --> `LIST[f6, f8] [n]`
290///
291/// See more examples in list.slt.
292fn reduce_list_create_list_index_literal(
293    mut list_create_to_reduce: MirScalarExpr,
294    mut index_exprs: Vec<MirScalarExpr>,
295) -> MirScalarExpr {
296    // We iterate over the index_exprs and remove literals, but keep non-literals.
297    // When we encounter a non-literal, we need to dig into the nested ListCreates:
298    // `list_create_mut_refs` will contain all the ListCreates of the current level. If an
299    // element of `list_create_mut_refs` is not actually a ListCreate, then we break out of
300    // the loop. When we remove a literal, we need to partial-evaluate all ListCreates
301    // that are at the current level (except those that disappeared due to
302    // literals at earlier levels), index into them with the literal, and change each
303    // element in `list_create_mut_refs` to the result.
304    // We also record mut refs to all the earlier `element_type` references that we have
305    // seen in ListCreate calls, because when we process a literal index, we need to remove
306    // one layer of list type from all these earlier ListCreate `element_type`s.
307    let mut list_create_mut_refs = vec![&mut list_create_to_reduce];
308    let mut earlier_list_create_types: Vec<&mut SqlScalarType> = vec![];
309    let mut i = 0;
310    while i < index_exprs.len()
311        && list_create_mut_refs
312            .iter()
313            .all(|lc| is_list_create_call(lc))
314    {
315        if index_exprs[i].is_literal_ok() {
316            // We can remove this index.
317            let removed_index = index_exprs.remove(i);
318            let index_i64 = match removed_index.as_literal().unwrap().unwrap() {
319                Datum::Int64(sql_index_i64) => sql_index_i64 - 1,
320                _ => unreachable!(), // always an Int64, see plan_index_list
321            };
322            // For each list_create referenced by list_create_mut_refs, substitute it by its
323            // `index`th argument (or null).
324            for list_create in &mut list_create_mut_refs {
325                let list_create_args = match list_create {
326                    MirScalarExpr::CallVariadic {
327                        func: VariadicFunc::ListCreate(ListCreate { elem_type: _ }),
328                        exprs,
329                    } => exprs,
330                    _ => unreachable!(), // func cannot be anything else than a ListCreate
331                };
332                // ListIndex gives null on an out-of-bounds index
333                if index_i64 >= 0 && index_i64 < list_create_args.len().try_into().unwrap() {
334                    let index: usize = index_i64.try_into().unwrap();
335                    **list_create = list_create_args.swap_remove(index);
336                } else {
337                    let typ = list_create_type(list_create);
338                    **list_create = MirScalarExpr::literal_null(typ);
339                }
340            }
341            // Peel one layer off of each of the earlier element types.
342            for t in earlier_list_create_types.iter_mut() {
343                if let SqlScalarType::List {
344                    element_type,
345                    custom_id: _,
346                } = t
347                {
348                    **t = *element_type.clone();
349                    // These are not the same types anymore, so remove custom_ids all the
350                    // way down.
351                    let mut u = &mut **t;
352                    while let SqlScalarType::List {
353                        element_type,
354                        custom_id,
355                    } = u
356                    {
357                        *custom_id = None;
358                        u = &mut **element_type;
359                    }
360                } else {
361                    unreachable!("already matched below");
362                }
363            }
364        } else {
365            // We can't remove this index, so we can't reduce any of the ListCreates at this
366            // level. So we change list_create_mut_refs to refer to all the arguments of all
367            // the ListCreates currently referenced by list_create_mut_refs.
368            list_create_mut_refs = list_create_mut_refs
369                .into_iter()
370                .flat_map(|list_create| match list_create {
371                    MirScalarExpr::CallVariadic {
372                        func: VariadicFunc::ListCreate(ListCreate { elem_type }),
373                        exprs: list_create_args,
374                    } => {
375                        earlier_list_create_types.push(elem_type);
376                        list_create_args
377                    }
378                    // func cannot be anything else than a ListCreate
379                    _ => unreachable!(),
380                })
381                .collect();
382            i += 1;
383        }
384    }
385    // If all list indexes have been evaluated, return the reduced expression.
386    // Otherwise, rebuild the ListIndex call with the remaining ListCreates and indexes.
387    if index_exprs.is_empty() {
388        assert_eq!(list_create_mut_refs.len(), 1);
389        list_create_to_reduce
390    } else {
391        MirScalarExpr::call_variadic(
392            ListIndex,
393            std::iter::once(list_create_to_reduce)
394                .chain(index_exprs)
395                .collect(),
396        )
397    }
398}
399
400#[cfg(test)]
401mod tests {
402    use mz_repr::{Datum, ReprScalarType};
403
404    use crate::MirScalarExpr;
405    use crate::scalar::func::variadic::{Greatest, Least, Substr};
406
407    #[mz_ore::test]
408    fn greatest_least_null_operand_drop() {
409        let types = [
410            ReprScalarType::Int32.nullable(true),
411            ReprScalarType::Int32.nullable(true),
412        ];
413        let null = || MirScalarExpr::literal_null(ReprScalarType::Int32);
414        let col = MirScalarExpr::column;
415
416        // Null operands drop; a single survivor is the result.
417        let mut e = MirScalarExpr::call_variadic(Greatest, vec![col(0), null()]);
418        e.reduce(&types);
419        assert_eq!(e, col(0));
420
421        let mut e = MirScalarExpr::call_variadic(Least, vec![col(0), null(), col(1)]);
422        e.reduce(&types);
423        assert_eq!(e, MirScalarExpr::call_variadic(Least, vec![col(0), col(1)]));
424
425        // Structurally equal operands deduplicate (scalar evaluation is
426        // deterministic and greatest/least are idempotent), keeping first
427        // occurrences.
428        let mut e = MirScalarExpr::call_variadic(Greatest, vec![col(0), col(0)]);
429        e.reduce(&types);
430        assert_eq!(e, col(0));
431
432        let mut e = MirScalarExpr::call_variadic(Least, vec![col(1), col(0), col(1)]);
433        e.reduce(&types);
434        assert_eq!(e, MirScalarExpr::call_variadic(Least, vec![col(1), col(0)]));
435
436        // Dedup and null-drop compose down to the bare operand.
437        let mut e = MirScalarExpr::call_variadic(Greatest, vec![col(0), null(), col(0)]);
438        e.reduce(&types);
439        assert_eq!(e, col(0));
440
441        // All-null evaluates to null.
442        let mut e = MirScalarExpr::call_variadic(Greatest, vec![null(), null()]);
443        e.reduce(&types);
444        assert!(e.is_literal_null());
445    }
446
447    #[mz_ore::test]
448    fn substr_from_one() {
449        let types = [ReprScalarType::String.nullable(true)];
450        let col = || MirScalarExpr::column(0);
451        let lit = |v| MirScalarExpr::literal_ok(Datum::Int32(v), ReprScalarType::Int32);
452
453        // The two-argument form starting at one is the identity.
454        let mut e = MirScalarExpr::call_variadic(Substr, vec![col(), lit(1)]);
455        e.reduce(&types);
456        assert_eq!(e, col());
457
458        // The three-argument form truncates and must stay.
459        let mut e = MirScalarExpr::call_variadic(Substr, vec![col(), lit(1), lit(5)]);
460        e.reduce(&types);
461        assert_ne!(e, col());
462    }
463}