mz_transform/fold_constants.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//! Replace operators on constants collections with constant collections.
11
12use std::cmp::Ordering;
13use std::collections::{BTreeMap, BTreeSet};
14use std::convert::TryInto;
15use std::iter;
16
17use mz_expr::visit::Visit;
18use mz_expr::{
19 AggregateExpr, ColumnOrder, Eval, EvalError, MirRelationExpr, MirScalarExpr, RowComparator,
20 TableFunc, UnaryFunc,
21};
22use mz_repr::{Datum, Diff, ReprRelationType, Row, RowArena};
23
24use crate::{TransformCtx, TransformError, any};
25
26/// Replace operators on constant collections with constant collections.
27#[derive(Debug)]
28pub struct FoldConstants {
29 /// An optional maximum size, after which optimization can cease.
30 ///
31 /// The `None` value here indicates no maximum size, but does not
32 /// currently guarantee that any constant expression will be reduced
33 /// to a `MirRelationExpr::Constant` variant.
34 pub limit: Option<usize>,
35}
36
37impl crate::Transform for FoldConstants {
38 fn name(&self) -> &'static str {
39 "FoldConstants"
40 }
41
42 #[mz_ore::instrument(
43 target = "optimizer",
44 level = "debug",
45 fields(path.segment = "fold_constants")
46 )]
47 fn actually_perform_transform(
48 &self,
49 relation: &mut MirRelationExpr,
50 _: &mut TransformCtx,
51 ) -> Result<(), TransformError> {
52 let mut type_stack = Vec::new();
53 let result = relation.try_visit_mut_post(&mut |e| -> Result<(), TransformError> {
54 let num_inputs = e.num_inputs();
55 let input_types = &type_stack[type_stack.len() - num_inputs..];
56 let mut relation_type = e.typ_with_input_types(input_types);
57 self.action(e, &mut relation_type)?;
58 type_stack.truncate(type_stack.len() - num_inputs);
59 type_stack.push(relation_type);
60 Ok(())
61 });
62 mz_repr::explain::trace_plan(&*relation);
63 result
64 }
65}
66
67impl FoldConstants {
68 /// Replace operators on constants collections with constant collections.
69 ///
70 /// This transform will cease optimization if it encounters constant collections
71 /// that are larger than `self.limit`, if that is set. It is not guaranteed that
72 /// a constant input within the limit will be reduced to a `Constant` variant.
73 pub fn action(
74 &self,
75 relation: &mut MirRelationExpr,
76 relation_type: &mut ReprRelationType,
77 ) -> Result<(), TransformError> {
78 match relation {
79 MirRelationExpr::Constant { .. } => { /* handled after match */ }
80 MirRelationExpr::Get { .. } => {}
81 MirRelationExpr::Let { .. } | MirRelationExpr::LetRec { .. } => {
82 // Constant propagation through bindings is currently handled by in NormalizeLets.
83 // Maybe we should move it / replicate it here (see database-issues#5346 for context)?
84 }
85 MirRelationExpr::Reduce {
86 input,
87 group_key,
88 aggregates,
89 monotonic: _,
90 expected_group_size: _,
91 } => {
92 // Guard against evaluating an expression that may contain
93 // unfoldable functions.
94 if group_key.iter().any(|e| e.contains_unfoldable())
95 || aggregates.iter().any(|a| a.expr.contains_unfoldable())
96 {
97 return Ok(());
98 }
99
100 if let Some((rows, ..)) = (**input).as_const() {
101 let new_rows = match rows {
102 Ok(rows) => {
103 if let Some(rows) =
104 Self::fold_reduce_constant(group_key, aggregates, rows, self.limit)
105 {
106 rows
107 } else {
108 return Ok(());
109 }
110 }
111 Err(e) => Err(e.clone()),
112 };
113 *relation = MirRelationExpr::Constant {
114 rows: new_rows,
115 typ: relation_type.clone(),
116 };
117 }
118 }
119 MirRelationExpr::TopK {
120 input,
121 group_key,
122 order_key,
123 limit,
124 offset,
125 ..
126 } => {
127 // Only fold constants when:
128 //
129 // 1. The `limit` value is not set, or
130 // 2. The `limit` value is set to a literal x such that x >= 0.
131 //
132 // We can improve this to arbitrary expressions, but it requires
133 // more typing.
134 if any![
135 limit.is_none(),
136 limit.as_ref().and_then(|l| l.as_literal_int64()) >= Some(0),
137 ] {
138 let limit = limit
139 .as_ref()
140 .and_then(|l| l.as_literal_int64().map(Into::into));
141 if let Some((rows, ..)) = (**input).as_const_mut() {
142 if let Ok(rows) = rows {
143 Self::fold_topk_constant(group_key, order_key, &limit, offset, rows);
144 }
145 *relation = input.take_dangerous();
146 }
147 }
148 }
149 MirRelationExpr::Negate { input } => {
150 if let Some((rows, ..)) = (**input).as_const_mut() {
151 if let Ok(rows) = rows {
152 for (_row, diff) in rows {
153 *diff = -*diff;
154 }
155 }
156 *relation = input.take_dangerous();
157 }
158 }
159 MirRelationExpr::Threshold { input } => {
160 if let Some((rows, ..)) = (**input).as_const_mut() {
161 if let Ok(rows) = rows {
162 rows.retain(|(_, diff)| diff.is_positive());
163 }
164 *relation = input.take_dangerous();
165 }
166 }
167 MirRelationExpr::Map { input, scalars } => {
168 // Guard against evaluating expression that may contain
169 // unfoldable functions.
170 if scalars.iter().any(|e| e.contains_unfoldable()) {
171 return Ok(());
172 }
173
174 if let Some((rows, ..)) = (**input).as_const() {
175 // Do not evaluate calls if:
176 // 1. The input consist of at least one row, and
177 // 2. The scalars is a singleton mz_panic('forced panic') call.
178 // Instead, indicate to the caller to panic.
179 if rows.as_ref().map_or(0, |r| r.len()) > 0 && scalars.len() == 1 {
180 if let MirScalarExpr::CallUnary {
181 func: UnaryFunc::Panic(_),
182 expr,
183 } = &scalars[0]
184 {
185 if let Some("forced panic") = expr.as_literal_str() {
186 let msg = "forced panic".to_string();
187 return Err(TransformError::CallerShouldPanic(msg));
188 }
189 }
190 }
191
192 let new_rows = match rows {
193 Ok(rows) => rows
194 .iter()
195 .map(|(input_row, diff)| {
196 // TODO: reduce allocations to zero.
197 let mut unpacked = input_row.unpack();
198 let temp_storage = RowArena::new();
199 for scalar in scalars.iter() {
200 unpacked.push(scalar.eval(&unpacked, &temp_storage)?)
201 }
202 Ok::<_, EvalError>((Row::pack_slice(&unpacked), *diff))
203 })
204 .collect::<Result<_, _>>(),
205 Err(e) => Err(e.clone()),
206 };
207 *relation = MirRelationExpr::Constant {
208 rows: new_rows,
209 typ: relation_type.clone(),
210 };
211 }
212 }
213 MirRelationExpr::FlatMap { input, func, exprs } => {
214 // Guard against evaluating expression that may contain unfoldable functions.
215 if exprs.iter().any(|e| e.contains_unfoldable()) {
216 return Ok(());
217 }
218
219 if let Some((rows, ..)) = (**input).as_const() {
220 let new_rows = match rows {
221 Ok(rows) => Self::fold_flat_map_constant(func, exprs, rows, self.limit),
222 Err(e) => Err(e.clone()),
223 };
224 match new_rows {
225 Ok(None) => {}
226 Ok(Some(rows)) => {
227 *relation = MirRelationExpr::Constant {
228 rows: Ok(rows),
229 typ: relation_type.clone(),
230 };
231 }
232 Err(err) => {
233 *relation = MirRelationExpr::Constant {
234 rows: Err(err),
235 typ: relation_type.clone(),
236 };
237 }
238 };
239 }
240 }
241 MirRelationExpr::Filter { input, predicates } => {
242 // Guard against evaluating expression that may contain
243 // unfoldable function calls.
244 if predicates.iter().any(|e| e.contains_unfoldable()) {
245 return Ok(());
246 }
247
248 // If any predicate is false, reduce to the empty collection.
249 if predicates
250 .iter()
251 .any(|p| p.is_literal_false() || p.is_literal_null())
252 {
253 relation.take_safely(Some(relation_type.clone()));
254 } else if let Some((rows, ..)) = (**input).as_const() {
255 // Evaluate errors last, to reduce risk of spurious errors.
256 predicates.sort_by_key(|p| p.is_literal_err());
257 let new_rows = match rows {
258 Ok(rows) => Self::fold_filter_constant(predicates, rows),
259 Err(e) => Err(e.clone()),
260 };
261 *relation = MirRelationExpr::Constant {
262 rows: new_rows,
263 typ: relation_type.clone(),
264 };
265 }
266 }
267 MirRelationExpr::Project { input, outputs } => {
268 if let Some((rows, ..)) = (**input).as_const() {
269 let mut row_buf = Row::default();
270 let new_rows = match rows {
271 Ok(rows) => Ok(rows
272 .iter()
273 .map(|(input_row, diff)| {
274 // TODO: reduce allocations to zero.
275 let datums = input_row.unpack();
276 row_buf.packer().extend(outputs.iter().map(|i| &datums[*i]));
277 (row_buf.clone(), *diff)
278 })
279 .collect()),
280 Err(e) => Err(e.clone()),
281 };
282 *relation = MirRelationExpr::Constant {
283 rows: new_rows,
284 typ: relation_type.clone(),
285 };
286 }
287 }
288 MirRelationExpr::Join {
289 inputs,
290 equivalences,
291 ..
292 } => {
293 if inputs.iter().any(|e| e.is_empty()) {
294 relation.take_safely(Some(relation_type.clone()));
295 } else if let Some(e) = inputs.iter().find_map(|i| i.as_const_err()) {
296 *relation = MirRelationExpr::Constant {
297 rows: Err(e.clone()),
298 typ: relation_type.clone(),
299 };
300 } else if inputs
301 .iter()
302 .all(|i| matches!(i.as_const(), Some((Ok(_), ..))))
303 {
304 // Guard against evaluating expression that may contain unfoldable functions.
305 if equivalences
306 .iter()
307 .any(|equiv| equiv.iter().any(|e| e.contains_unfoldable()))
308 {
309 return Ok(());
310 }
311
312 // We can fold all constant inputs together, but must apply the constraints to restrict them.
313 // We start with a single 0-ary row.
314 let mut old_rows = vec![(Row::pack::<_, Datum>(None), Diff::ONE)];
315 let mut row_buf = Row::default();
316 for input in inputs.iter() {
317 if let Some((Ok(rows), ..)) = input.as_const() {
318 if let Some(limit) = self.limit {
319 if old_rows.len() * rows.len() > limit {
320 // Bail out if we have produced too many rows.
321 // TODO: progressively apply equivalences to narrow this count
322 // as we go, rather than at the end.
323 return Ok(());
324 }
325 }
326 let mut next_rows = Vec::new();
327 for (old_row, old_count) in old_rows {
328 for (new_row, new_count) in rows.iter() {
329 let mut packer = row_buf.packer();
330 packer.extend_by_row(&old_row);
331 packer.extend_by_row(new_row);
332 next_rows.push((row_buf.clone(), old_count * *new_count));
333 }
334 }
335 old_rows = next_rows;
336 }
337 }
338
339 // Now throw away anything that doesn't satisfy the requisite constraints.
340 let mut datum_vec = mz_repr::DatumVec::new();
341 old_rows.retain(|(row, _count)| {
342 let datums = datum_vec.borrow_with(row);
343 let temp_storage = RowArena::new();
344 equivalences.iter().all(|equivalence| {
345 let mut values =
346 equivalence.iter().map(|e| e.eval(&datums, &temp_storage));
347 if let Some(value) = values.next() {
348 values.all(|v| v == value)
349 } else {
350 true
351 }
352 })
353 });
354
355 *relation = MirRelationExpr::Constant {
356 rows: Ok(old_rows),
357 typ: relation_type.clone(),
358 };
359 }
360 // TODO: General constant folding for all constant inputs.
361 }
362 MirRelationExpr::Union { base, inputs } => {
363 if let Some(e) = iter::once(&mut **base)
364 .chain(&mut *inputs)
365 .find_map(|i| i.as_const_err())
366 {
367 *relation = MirRelationExpr::Constant {
368 rows: Err(e.clone()),
369 typ: relation_type.clone(),
370 };
371 } else {
372 let mut rows = vec![];
373 let mut new_inputs = vec![];
374
375 for input in iter::once(&mut **base).chain(&mut *inputs) {
376 if let Some((Ok(rs), ..)) = input.as_const() {
377 rows.extend(rs.clone());
378 } else {
379 new_inputs.push(input.clone())
380 }
381 }
382 if !rows.is_empty() {
383 new_inputs.push(MirRelationExpr::Constant {
384 rows: Ok(rows),
385 typ: relation_type.clone(),
386 });
387 }
388
389 *relation = MirRelationExpr::union_many(new_inputs, relation_type.clone());
390 }
391 }
392 MirRelationExpr::ArrangeBy { .. } => {
393 // Don't fold ArrangeBys, because that could result in unarranged Delta join inputs.
394 // See also the comment on `MirRelationExpr::Constant`.
395 }
396 }
397
398 // This transformation maintains the invariant that all constant nodes
399 // will be consolidated. We have to make a separate check for constant
400 // nodes here, since the match arm above might install new constant
401 // nodes.
402 if let Some((Ok(rows), typ)) = relation.as_const_mut() {
403 // Reduce down to canonical representation.
404 differential_dataflow::consolidation::consolidate(rows);
405
406 // Re-establish nullability of each column.
407 for col_type in typ.column_types.iter_mut() {
408 col_type.nullable = false;
409 }
410 for (row, _) in rows.iter_mut() {
411 for (index, datum) in row.iter().enumerate() {
412 if datum.is_null() {
413 typ.column_types[index].nullable = true;
414 }
415 }
416 }
417 *relation_type = typ.clone();
418 }
419
420 Ok(())
421 }
422
423 // TODO(benesch): remove this once this function no longer makes use of
424 // potentially dangerous `as` conversions.
425 #[allow(clippy::as_conversions)]
426 fn fold_reduce_constant(
427 group_key: &[MirScalarExpr],
428 aggregates: &[AggregateExpr],
429 rows: &[(Row, Diff)],
430 limit: Option<usize>,
431 ) -> Option<Result<Vec<(Row, Diff)>, EvalError>> {
432 // Build a map from `group_key` to `Vec<Vec<an, ..., a1>>)`,
433 // where `an` is the input to the nth aggregate function in
434 // `aggregates`.
435 let mut groups = BTreeMap::new();
436 let temp_storage2 = RowArena::new();
437 let mut row_buf = Row::default();
438 let mut limit_remaining =
439 limit.map_or(Diff::MAX, |limit| Diff::try_from(limit).expect("must fit"));
440 for (row, diff) in rows {
441 // We currently maintain the invariant that any negative
442 // multiplicities will be consolidated away before they
443 // arrive at a reduce.
444
445 if *diff <= Diff::ZERO {
446 return Some(Err(EvalError::InvalidParameterValue(
447 "constant folding encountered reduce on collection with non-positive multiplicities".into()
448 )));
449 }
450
451 if limit_remaining < *diff {
452 return None;
453 }
454 limit_remaining -= diff;
455
456 let datums = row.unpack();
457 let temp_storage = RowArena::new();
458 let key = match group_key
459 .iter()
460 .map(|e| e.eval(&datums, &temp_storage2))
461 .collect::<Result<Vec<_>, _>>()
462 {
463 Ok(key) => key,
464 Err(e) => return Some(Err(e)),
465 };
466 let val = match aggregates
467 .iter()
468 .map(|agg| {
469 row_buf
470 .packer()
471 .extend([agg.expr.eval(&datums, &temp_storage)?]);
472 Ok::<_, EvalError>(row_buf.clone())
473 })
474 .collect::<Result<Vec<_>, _>>()
475 {
476 Ok(val) => val,
477 Err(e) => return Some(Err(e)),
478 };
479 // Store the value once alongside its multiplicity rather than
480 // expanding it into `diff` copies. The count-aware aggregate
481 // `eval` consumes the multiplicity directly, so this avoids
482 // materializing a number of rows proportional to the diff.
483 let entry = groups.entry(key).or_insert_with(Vec::new);
484 entry.push((val, *diff));
485 }
486
487 // For each group, apply the aggregate function to the rows
488 // in the group. The output is
489 // `Vec<Vec<k1, ..., kn, r1, ..., rn>>`
490 // where kn is the nth column of the key and rn is the
491 // result of the nth aggregate function for that group.
492 let new_rows = groups
493 .into_iter()
494 .map({
495 let mut row_buf = Row::default();
496 move |(key, vals)| {
497 let temp_storage = RowArena::new();
498 row_buf.packer().extend(key.into_iter().chain(
499 aggregates.iter().enumerate().map(|(i, agg)| {
500 if agg.distinct {
501 // Distinct collapses to one copy per value, so
502 // each distinct datum gets a unit multiplicity.
503 agg.func.eval(
504 vals.iter()
505 .map(|(val, _diff)| val[i].unpack_first())
506 .collect::<BTreeSet<_>>()
507 .into_iter()
508 .map(|datum| (datum, Diff::ONE)),
509 &temp_storage,
510 )
511 } else {
512 agg.func.eval(
513 vals.iter()
514 .map(|(val, diff)| (val[i].unpack_first(), *diff)),
515 &temp_storage,
516 )
517 }
518 }),
519 ));
520 (row_buf.clone(), Diff::ONE)
521 }
522 })
523 .collect();
524 Some(Ok(new_rows))
525 }
526
527 fn fold_topk_constant<'a>(
528 group_key: &[usize],
529 order_key: &[ColumnOrder],
530 limit: &Option<Diff>,
531 offset: &usize,
532 rows: &'a mut [(Row, Diff)],
533 ) {
534 // helper functions for comparing elements by order_key and group_key
535 let comparator = RowComparator::new(order_key);
536
537 let mut cmp_order_key = |lhs: &(Row, Diff), rhs: &(Row, Diff)| {
538 comparator.compare_rows(&lhs.0, &rhs.0, || lhs.cmp(rhs))
539 };
540 let mut cmp_group_key = {
541 let group_key = group_key
542 .iter()
543 .map(|column| ColumnOrder {
544 column: *column,
545 // desc and nulls_last don't matter: the sorting by cmp_group_key is just to
546 // make the elements of each group appear next to each other, but the order of
547 // groups doesn't matter.
548 desc: false,
549 nulls_last: false,
550 })
551 .collect::<Vec<ColumnOrder>>();
552 let comparator = RowComparator::new(group_key);
553 move |lhs: &(Row, Diff), rhs: &(Row, Diff)| {
554 comparator.compare_rows(&lhs.0, &rhs.0, || Ordering::Equal)
555 }
556 };
557
558 // compute Ordering based on the sort_key, otherwise consider all rows equal
559 rows.sort_by(&mut cmp_order_key);
560
561 // sort by the grouping key if not empty, keeping order_key as a secondary sort
562 if !group_key.is_empty() {
563 rows.sort_by(&mut cmp_group_key);
564 };
565
566 let same_group_key =
567 |lhs: &(Row, Diff), rhs: &(Row, Diff)| cmp_group_key(lhs, rhs) == Ordering::Equal;
568
569 let mut cursor = 0;
570 while cursor < rows.len() {
571 // first, reset the remaining limit and offset for the current group
572 let mut offset_rem: Diff = offset.clone().try_into().unwrap();
573 let mut limit_rem: Option<Diff> = limit.clone();
574
575 let mut finger = cursor;
576 while finger < rows.len() && same_group_key(&rows[cursor], &rows[finger]) {
577 if rows[finger].1.is_negative() {
578 // ignore elements with negative diff
579 rows[finger].1 = Diff::ZERO;
580 } else {
581 // determine how many of the leading rows to ignore,
582 // then decrement the diff and remaining offset by that number
583 let rows_to_ignore = std::cmp::min(offset_rem, rows[finger].1);
584 rows[finger].1 -= rows_to_ignore;
585 offset_rem -= rows_to_ignore;
586 // determine how many of the remaining rows to retain,
587 // then update the diff and decrement the remaining limit by that number
588 if let Some(limit_rem) = &mut limit_rem {
589 let rows_to_retain = std::cmp::min(*limit_rem, rows[finger].1);
590 rows[finger].1 = rows_to_retain;
591 *limit_rem -= rows_to_retain;
592 }
593 }
594 finger += 1;
595 }
596 cursor = finger;
597 }
598 }
599
600 fn fold_flat_map_constant(
601 func: &TableFunc,
602 exprs: &[MirScalarExpr],
603 rows: &[(Row, Diff)],
604 limit: Option<usize>,
605 ) -> Result<Option<Vec<(Row, Diff)>>, EvalError> {
606 // We cannot exceed `usize::MAX` in any array, so this is a fine upper bound.
607 let limit = limit.unwrap_or(usize::MAX);
608 let mut new_rows = Vec::new();
609 let mut row_buf = Row::default();
610 let mut datum_vec = mz_repr::DatumVec::new();
611 for (input_row, diff) in rows {
612 let datums = datum_vec.borrow_with(input_row);
613 let temp_storage = RowArena::new();
614 let datums = exprs
615 .iter()
616 .map(|expr| expr.eval(&datums, &temp_storage))
617 .collect::<Result<Vec<_>, _>>()?;
618 let mut output_rows = func.eval(&datums, &temp_storage)?.fuse();
619 for (output_row, diff2) in (&mut output_rows).take(limit - new_rows.len()) {
620 let mut packer = row_buf.packer();
621 packer.extend_by_row(input_row);
622 packer.extend_by_row(&output_row);
623 new_rows.push((row_buf.clone(), diff2 * *diff))
624 }
625 // If we still have records to enumerate, but dropped out of the iteration,
626 // it means we have exhausted `limit` and should stop.
627 if output_rows.next() != None {
628 return Ok(None);
629 }
630 }
631 Ok(Some(new_rows))
632 }
633
634 fn fold_filter_constant(
635 predicates: &[MirScalarExpr],
636 rows: &[(Row, Diff)],
637 ) -> Result<Vec<(Row, Diff)>, EvalError> {
638 let mut new_rows = Vec::new();
639 let mut datum_vec = mz_repr::DatumVec::new();
640 'outer: for (row, diff) in rows {
641 let datums = datum_vec.borrow_with(row);
642 let temp_storage = RowArena::new();
643 for p in &*predicates {
644 if p.eval(&datums, &temp_storage)? != Datum::True {
645 continue 'outer;
646 }
647 }
648 new_rows.push((row.clone(), *diff))
649 }
650 Ok(new_rows)
651 }
652}