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}