Skip to main content

mz_compute_types/plan/
join.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//! Planning of `LirRelationExpr::Join` operators, and supporting types.
11//!
12//! Join planning proceeds by repeatedly introducing collections that
13//! extend the set of available output columns. The expected location
14//! of each output column is determined by the order of inputs to the
15//! join operator: columns are appended in that order.
16//!
17//! While planning the join, we also have access to logic in the form
18//! of expressions, predicates, and projections that we intended to
19//! apply to the output of the join. This logic uses "output column
20//! reckoning" where columns are identified by their intended output
21//! position.
22//!
23//! As we consider applying expressions to partial results, we will
24//! place the results in column locations *after* the intended output
25//! column locations. These output locations in addition to the new
26//! distinct identifiers for constructed expressions is "extended
27//! output column reckoning", as is what we use when reasoning about
28//! work still available to be done on the partial join results.
29
30use std::collections::BTreeMap;
31
32use mz_expr::{Columns, Eval, MapFilterProject, MirScalarExpr};
33use mz_repr::{Datum, Row, RowArena};
34use serde::{Deserialize, Serialize};
35
36pub mod delta_join;
37pub mod linear_join;
38
39pub use delta_join::DeltaJoinPlan;
40pub use linear_join::LinearJoinPlan;
41
42use crate::plan::scalar::{LirScalarExpr, lses_from_mses};
43
44/// A complete enumeration of possible join plans to render.
45#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd)]
46pub enum JoinPlan {
47    /// A join implemented by a linear join.
48    Linear(LinearJoinPlan),
49    /// A join implemented by a delta join.
50    Delta(DeltaJoinPlan),
51}
52
53/// A manual closure implementation of filtering and logic application.
54///
55/// This manual implementation exists to express lifetime constraints clearly,
56/// as there is a relationship between the borrowed lifetime of the closed-over
57/// state and the arguments it takes when invoked. It was not clear how to do
58/// this with a Rust closure (glorious battle was waged, but ultimately lost).
59#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd)]
60pub struct JoinClosure {
61    /// Ordered list of equivalences to evaluate.
62    pub ready_equivalences: Vec<Vec<LirScalarExpr>>,
63    /// MFP to run before the equivalence
64    pub before: mz_expr::SafeMfpPlan<LirScalarExpr>,
65}
66
67impl JoinClosure {
68    /// Applies per-row filtering and logic.
69    #[inline(always)]
70    pub fn apply<'a, 'row>(
71        &'a self,
72        datums: &mut Vec<Datum<'a>>,
73        temp_storage: &'a RowArena,
74        row: &'row mut Row,
75    ) -> Result<Option<&'row Row>, mz_expr::EvalError> {
76        for exprs in self.ready_equivalences.iter() {
77            // Each list of expressions should be equal to the same value.
78            let val = exprs[0].eval(&datums[..], temp_storage)?;
79            for expr in exprs[1..].iter() {
80                if expr.eval(datums, temp_storage)? != val {
81                    return Ok(None);
82                }
83            }
84        }
85        self.before.evaluate_into(datums, temp_storage, row)
86    }
87
88    /// Construct an instance of the closure from available columns.
89    ///
90    /// This method updates the available columns, equivalences, and
91    /// the `MapFilterProject` instance. The columns are updated to
92    /// include reference to any columns added by the application of
93    /// this logic, which might result from partial application of
94    /// the `MapFilterProject` instance.
95    ///
96    /// If all columns are available for `mfp`, this method works
97    /// extra hard to ensure that the closure contains all the work,
98    /// and `mfp` is left as an identity transform (which can then
99    /// be ignored).
100    fn build(
101        columns: &mut BTreeMap<usize, usize>,
102        equivalences: &mut Vec<Vec<LirScalarExpr>>,
103        mfp: &mut MapFilterProject,
104        permutation: BTreeMap<usize, usize>,
105        thinned_arity_with_key: usize,
106    ) -> Self {
107        // First, determine which columns should be compared due to `equivalences`.
108        let mut ready_equivalences = Vec::new();
109        for equivalence in equivalences.iter_mut() {
110            if let Some(pos) = equivalence
111                .iter()
112                .position(|e| e.support().into_iter().all(|c| columns.contains_key(&c)))
113            {
114                let mut should_equate = Vec::new();
115                let mut cursor = pos + 1;
116                while cursor < equivalence.len() {
117                    if equivalence[cursor]
118                        .support()
119                        .into_iter()
120                        .all(|c| columns.contains_key(&c))
121                    {
122                        // Remove expression and equate with the first bound expression.
123                        should_equate.push(equivalence.remove(cursor));
124                    } else {
125                        cursor += 1;
126                    }
127                }
128                if !should_equate.is_empty() {
129                    should_equate.push(equivalence[pos].clone());
130                    ready_equivalences.push(should_equate);
131                }
132            }
133        }
134        equivalences.retain(|e| e.len() > 1);
135        let permuted_columns = columns.iter().map(|(k, v)| (*k, permutation[v])).collect();
136        // Update ready_equivalences to reference correct column locations.
137        for exprs in ready_equivalences.iter_mut() {
138            for expr in exprs.iter_mut() {
139                expr.permute_map(&permuted_columns);
140            }
141        }
142
143        // Next, partition `mfp` into `before` and `after`, the former of which can be
144        // applied now.
145        let (mut before, after) = std::mem::replace(mfp, MapFilterProject::new(mfp.input_arity))
146            .partition(columns.clone(), columns.len());
147
148        // Add any newly created columns to `columns`. These columns may be referenced
149        // by `after`, and it will be important to track their locations.
150        let bonus_columns = before.projection.len() - before.input_arity;
151        for bonus_column in 0..bonus_columns {
152            columns.insert(mfp.input_arity + bonus_column, columns.len());
153        }
154
155        *mfp = after;
156
157        // Before constructing and returning the result, we can remove output columns of `before`
158        // that are not needed in further `equivalences` or by `after` (now `mfp`).
159        let mut demand = Vec::new();
160        demand.extend(mfp.demand());
161        for equivalence in equivalences.iter() {
162            for expr in equivalence.iter() {
163                demand.extend(expr.support());
164            }
165        }
166        demand.sort();
167        demand.dedup();
168        // We only want to remove columns that are presented as outputs (i.e. can be found as in
169        // `columns`). Other columns have yet to be introduced, and we shouldn't have any opinion
170        // about them yet.
171        demand.retain(|column| columns.contains_key(column));
172        // Project `before` output columns using current locations of demanded columns.
173        before = before.project(demand.iter().map(|column| columns[column]));
174        // Update `columns` to reflect location of retained columns.
175        columns.clear();
176        for (index, column) in demand.iter().enumerate() {
177            columns.insert(*column, index);
178        }
179
180        // If `mfp` is a permutation of the columns present in `columns`, then we can
181        // apply that permutation to `before` and `columns`, so that `mfp` becomes the
182        // identity operation.
183        if mfp.expressions.is_empty()
184            && mfp.predicates.is_empty()
185            && mfp.projection.len() == columns.len()
186            && mfp.projection.iter().all(|col| columns.contains_key(col))
187            && columns.keys().all(|col| mfp.projection.contains(col))
188        {
189            // The projection we want to apply to `before`  comes to us from `mfp` in the
190            // extended output column reckoning.
191            let projection = mfp
192                .projection
193                .iter()
194                .map(|col| columns[col])
195                .collect::<Vec<_>>();
196            before = before.project(projection);
197            // Update the physical locations of each output column.
198            columns.clear();
199            for (index, column) in mfp.projection.iter().enumerate() {
200                columns.insert(*column, index);
201            }
202        }
203
204        before.permute_fn(|c| permutation[&c], thinned_arity_with_key);
205
206        // `before` should not be modified after this point.
207        before.optimize();
208
209        // Cons up an instance of the closure with the closed-over state.
210        Self::new(before, ready_equivalences)
211    }
212
213    /// Constructs a `JoinClosure` from an MFP to be run before and an ordered list of equivalences.
214    ///
215    /// You should _probably_ not be calling this function directly---it's a shim for taking MIR forms
216    /// and producing the LIR forms we'll ultimately need.
217    pub(crate) fn new(
218        before: MapFilterProject,
219        ready_equivalences: Vec<Vec<LirScalarExpr>>,
220    ) -> Self {
221        let before = crate::plan::scalar::safe_mfp_mir_to_lir(
222            before.into_plan().unwrap().into_nontemporal().unwrap(),
223        );
224        Self {
225            ready_equivalences,
226            before,
227        }
228    }
229
230    /// True iff the closure neither filters nor transforms records.
231    pub fn is_identity(&self) -> bool {
232        self.ready_equivalences.is_empty() && self.before.is_identity()
233    }
234
235    /// True iff the closure does more than projections.
236    pub fn maps_or_filters(&self) -> bool {
237        !self.before.expressions.is_empty()
238            || !self.before.predicates.is_empty()
239            || !self.ready_equivalences.is_empty()
240    }
241
242    /// Returns true if evaluation could introduce an error on non-error inputs.
243    pub fn could_error(&self) -> bool {
244        self.before.could_error()
245            || self
246                .ready_equivalences
247                .iter()
248                .any(|es| es.iter().any(|e| e.could_error()))
249    }
250}
251
252/// Maintained state as we construct join dataflows.
253///
254/// This state primarily tracks the *remaining* work that has not yet been applied to a
255/// stream of partial results.
256///
257/// This state is meant to reconcile the logical operations that remain to apply (e.g.
258/// filtering, expressions, projection) and the physical organization of the current stream
259/// of data, which columns may be partially assembled in non-standard locations and which
260/// may already have been partially subjected to logic we need to apply.
261#[derive(Debug)]
262pub struct JoinBuildState {
263    /// Map from expected locations in extended output column reckoning to physical locations.
264    column_map: BTreeMap<usize, usize>,
265    /// A list of equivalence classes of expressions.
266    ///
267    /// Within each equivalence class, expressions must evaluate to the same result to pass
268    /// the join expression. Importantly, "the same" should be evaluated with `Datum`s Rust
269    /// equality, rather than the equality presented by the `BinaryFunc` equality operator.
270    /// The distinction is important for null handling, at the least.
271    equivalences: Vec<Vec<LirScalarExpr>>,
272    /// The linear operator logic (maps, filters, and projection) that remains to be applied
273    /// to the output of the join.
274    ///
275    /// When we advance through the construction of the join dataflow, we may be able to peel
276    /// off some of this work, ideally reducing `mfp` to something nearly the identity.
277    mfp: MapFilterProject,
278}
279
280impl JoinBuildState {
281    /// Create a new join state and initial closure from initial values.
282    ///
283    /// The initial closure can be `None` which indicates that it is the identity operator.
284    fn new(
285        columns: std::ops::Range<usize>,
286        equivalences: &[Vec<MirScalarExpr>],
287        mfp: &MapFilterProject,
288    ) -> Self {
289        let mut column_map = BTreeMap::new();
290        for column in columns {
291            column_map.insert(column, column_map.len());
292        }
293        let mut equivalences = equivalences.to_vec();
294        mz_expr::canonicalize::canonicalize_equivalence_classes(&mut equivalences);
295        let equivalences = equivalences
296            .into_iter()
297            .map(|equiv| lses_from_mses(&equiv))
298            .collect();
299        Self {
300            column_map,
301            equivalences,
302            mfp: mfp.clone(),
303        }
304    }
305
306    /// Present new columns and extract any newly available closure.
307    fn add_columns(
308        &mut self,
309        new_columns: std::ops::Range<usize>,
310        bound_expressions: &[LirScalarExpr],
311        thinned_arity_with_key: usize,
312        // The permutation to run on the join of the thinned collections
313        permutation: BTreeMap<usize, usize>,
314    ) -> JoinClosure {
315        // Remove each element of `bound_expressions` from `equivalences`, so that we
316        // avoid redundant predicate work. This removal also paves the way for
317        // more precise "demand" information going forward.
318        for equivalence in self.equivalences.iter_mut() {
319            equivalence.retain(|expr| !bound_expressions.contains(expr));
320        }
321        self.equivalences.retain(|e| e.len() > 1);
322
323        // Update our map of the sources of each column in the update stream.
324        for column in new_columns {
325            self.column_map.insert(column, self.column_map.len());
326        }
327
328        self.extract_closure(permutation, thinned_arity_with_key)
329    }
330
331    /// Extract a final `MapFilterProject` once all columns are available.
332    ///
333    /// If not all columns are available this method will likely panic.
334    /// This method differs from `extract_closure` in that it forcibly
335    /// completes the join, extracting projections and expressions that
336    /// may not be extracted with `extract_closure` (for example, literals,
337    /// permutations, and repetition of output columns).
338    ///
339    /// The resulting closure may be the identity operator, which can be
340    /// checked with the `is_identity()` method.
341    fn complete(self) -> JoinClosure {
342        let Self {
343            column_map,
344            mut equivalences,
345            mut mfp,
346        } = self;
347
348        for equivalence in equivalences.iter_mut() {
349            for expr in equivalence.iter_mut() {
350                expr.permute_map(&column_map);
351            }
352        }
353        let column_map_len = column_map.len();
354        mfp.permute_fn(|c| column_map[&c], column_map_len);
355        mfp.optimize();
356
357        JoinClosure::new(mfp, equivalences)
358    }
359
360    /// A method on `self` that extracts an available closure.
361    ///
362    /// The extracted closure is not guaranteed to be non-trivial. Sensitive users should
363    /// consider using the `.is_identity()` method to determine non-triviality.
364    fn extract_closure(
365        &mut self,
366        permutation: BTreeMap<usize, usize>,
367        thinned_arity_with_key: usize,
368    ) -> JoinClosure {
369        JoinClosure::build(
370            &mut self.column_map,
371            &mut self.equivalences,
372            &mut self.mfp,
373            permutation,
374            thinned_arity_with_key,
375        )
376    }
377}