Skip to main content

mz_timely_util/
columnar.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License in the LICENSE file at the
6// root of this repository, or online at
7//
8//     http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16//! Container for columnar data.
17
18#![deny(missing_docs)]
19
20pub mod batcher;
21pub mod body;
22pub mod builder;
23pub mod builder_input;
24pub mod chunk;
25pub mod consolidate;
26pub mod merge_batcher;
27pub mod unload;
28
29use std::hash::{BuildHasher, Hash, Hasher};
30use std::sync::LazyLock;
31
32use columnar::Borrow;
33use columnar::bytes::indexed;
34use columnar::common::IterOwn;
35use columnar::{Clear, FromBytes, Index, Len};
36use columnar::{Columnar, Ref};
37use differential_dataflow::Hashable;
38use differential_dataflow::collection::containers::{Enter, ResultsIn};
39use differential_dataflow::trace::implementations::merge_batcher::MergeBatcher;
40use itertools::Itertools;
41use timely::Accountable;
42use timely::bytes::arc::Bytes;
43use timely::container::{DrainContainer, PushInto, SizableContainer};
44use timely::dataflow::channels::ContainerBytes;
45use timely::progress::Timestamp;
46use timely::progress::timestamp::Refines;
47
48use crate::columnation::ColInternalMerger;
49
50/// A batcher for columnar storage.
51///
52/// `Chu` melds raw input containers into the chunks the batcher merges:
53/// [`ColumnationChunker`](crate::columnation::ColumnationChunker) for `Vec<_>`
54/// input, or [`batcher::Chunker`] (over a `ColumnationStack<_>`) for [`Column`]
55/// input. `Se` seals each extracted chain into a batch. A caller that wants the
56/// chain itself takes [`ConsolidatingBatcher`](crate::operator::ConsolidatingBatcher)
57/// instead, which is this batcher without the seal.
58pub type Col2ValBatcher<K, V, T, R, Chu, Se> =
59    MergeBatcher<Chu, ColInternalMerger<(K, V), T, R>, Se>;
60/// A batcher for columnar storage with unit values.
61pub type Col2KeyBatcher<K, T, R, Chu, Se> = Col2ValBatcher<K, (), T, R, Chu, Se>;
62
63/// Pageable counterpart to [`Col2ValBatcher`]. Routes every chunk produced
64/// by chunking, merging, or extract through a [`crate::column_pager::ColumnPager`],
65/// so memory pressure can spill chains to a backing store without touching
66/// the merge / extract bodies.
67///
68/// Drop-in shape at the type level: both aliases take the same parameters and
69/// produce a `Batcher<C>` over the same chunk type. The pager itself defaults
70/// to [`crate::column_pager::ColumnPager::disabled`]; inject a real one via
71/// [`merge_batcher::ColumnMergeBatcher::set_pager`].
72pub type Col2ValPagedBatcher<K, V, T, R, Chu, Se> =
73    merge_batcher::ColumnMergeBatcher<Chu, (K, V), T, R, Se>;
74
75/// Columnar-native counterpart to [`Col2ValBatcher`], holding
76/// [`body::ColumnBody`] chunks rather than columnation stacks and merging
77/// them through [`batcher::ColumnMerger`].
78///
79/// Pairs with [`batcher::ColumnChunker`] and any builder whose `Input` is
80/// `ColumnBody<((K, V), T, R)>`. Unlike [`Col2ValPagedBatcher`] the chains
81/// stay resident, so this arm carries no pager and no spill budget.
82pub type Col2ValColBatcher<K, V, T, R, Chu, Se> =
83    MergeBatcher<Chu, batcher::ColumnMerger<(K, V), T, R>, Se>;
84
85/// A container based on a columnar store, encoded in aligned bytes: the container on dataflow
86/// edges.
87///
88/// The type can represent typed data, bytes from Timely, or an aligned allocation. The name
89/// is singular to express that the preferred format is [`Column::Align`]. The [`Column::Typed`]
90/// variant is used to construct the container, and it owns potentially multiple columns of data.
91///
92/// Data at rest behind an edge, in a merge batcher's chains, a spill-backed chunk, or a batch
93/// builder, is a [`body::ColumnBody`] instead, which has no channel-bytes form.
94pub enum Column<C: Columnar> {
95    /// The typed variant of the container.
96    Typed(C::Container),
97    /// The binary variant of the container.
98    Bytes(Bytes),
99    /// Relocated, aligned binary data, if `Bytes` doesn't work for some reason.
100    ///
101    /// Reasons could include misalignment, cloning of data, or wanting
102    /// to release the `Bytes` as a scarce resource.
103    ///
104    /// `Vec<u64>` guarantees `u64` alignment for the contained bytes.
105    Align(Vec<u64>),
106}
107
108impl<C: Columnar> Column<C> {
109    /// Empties the column, retaining the `Typed` variant's allocation so the
110    /// caller can refill it.
111    ///
112    /// [`columnar::Clear`] clears the typed container in place without
113    /// releasing its capacity. The serialized variants (`Bytes`/`Align`) own
114    /// no reusable typed buffer, so they are reset to an empty `Typed`.
115    #[inline]
116    pub fn clear(&mut self) {
117        match self {
118            Column::Typed(t) => t.clear(),
119            Column::Bytes(_) | Column::Align(_) => *self = Default::default(),
120        }
121    }
122
123    /// True when the column holds no records.
124    ///
125    /// The `Typed` variant answers from the container itself. The serialized
126    /// variants have to reconstruct their borrowed view to reach a length, so
127    /// there this costs as much as [`Column::borrow`].
128    #[inline]
129    pub fn is_empty(&self) -> bool {
130        match self {
131            Column::Typed(t) => t.is_empty(),
132            Column::Bytes(_) | Column::Align(_) => self.borrow().is_empty(),
133        }
134    }
135
136    /// Borrows the container as a reference.
137    // NOTE: `inline(always)` measured 7.1 ns to 5.8 ns per call for a per-element borrow of an
138    // `Align` column. The header decode stays per call, so loops still hoist the borrow.
139    #[inline(always)]
140    pub fn borrow(&self) -> <C::Container as Borrow>::Borrowed<'_> {
141        match self {
142            Column::Typed(t) => t.borrow(),
143            Column::Bytes(b) => <<C::Container as Borrow>::Borrowed<'_>>::from_bytes(
144                &mut indexed::decode(bytemuck::cast_slice(b)),
145            ),
146            Column::Align(a) => {
147                <<C::Container as Borrow>::Borrowed<'_>>::from_bytes(&mut indexed::decode(a))
148            }
149        }
150    }
151}
152
153impl<C: Columnar> Default for Column<C> {
154    fn default() -> Self {
155        Self::Typed(Default::default())
156    }
157}
158
159impl<C: Columnar> Clone for Column<C>
160where
161    C::Container: Clone,
162{
163    fn clone(&self) -> Self {
164        match self {
165            // Typed stays typed, although we would have the option to move to aligned data.
166            // If we did it might be confusing why we couldn't push into a cloned column.
167            Column::Typed(t) => Column::Typed(t.clone()),
168            Column::Bytes(b) => {
169                assert_eq!(b.len() % 8, 0);
170                Self::Align(bytemuck::allocation::pod_collect_to_vec(b))
171            }
172            Column::Align(a) => Column::Align(a.clone()),
173        }
174    }
175}
176
177impl<C: Columnar> Accountable for Column<C> {
178    #[inline]
179    fn record_count(&self) -> i64 {
180        self.borrow().len().try_into().expect("Must fit")
181    }
182}
183impl<C: Columnar> DrainContainer for Column<C> {
184    type Item<'a> = Ref<'a, C>;
185    type DrainIter<'a> = IterOwn<<C::Container as Borrow>::Borrowed<'a>>;
186    #[inline]
187    fn drain(&mut self) -> Self::DrainIter<'_> {
188        self.borrow().into_index_iter()
189    }
190}
191
192impl<C: Columnar, T> PushInto<T> for Column<C>
193where
194    C::Container: columnar::Push<T>,
195{
196    #[inline]
197    fn push_into(&mut self, item: T) {
198        use columnar::Push;
199        match self {
200            Column::Typed(t) => t.push(item),
201            Column::Align(_) | Column::Bytes(_) => {
202                // We really oughtn't be calling this in this case.
203                // We could convert to owned, but need more constraints on `C`.
204                unimplemented!("Pushing into Column::Bytes without first clearing");
205            }
206        }
207    }
208}
209
210/// Re-encodes a column's times into a refining timestamp, so a columnar collection can enter
211/// an iterative scope.
212impl<D, T1, T2, R> Enter<T1, T2> for Column<(D, T1, R)>
213where
214    D: Columnar,
215    T1: Columnar + Timestamp,
216    T2: Columnar + Refines<T1>,
217    R: Columnar,
218    (D, T1, R): Columnar<Container = (D::Container, T1::Container, R::Container)>,
219    (D, T2, R): Columnar<Container = (D::Container, T2::Container, R::Container)>,
220    for<'a> D::Container: columnar::Push<Ref<'a, D>>,
221    for<'a> T2::Container: columnar::Push<&'a T2>,
222    for<'a> R::Container: columnar::Push<Ref<'a, R>>,
223{
224    type InnerContainer = Column<(D, T2, R)>;
225
226    fn enter(self) -> Self::InnerContainer {
227        use columnar::Push;
228        match self {
229            // Only the times change, so the data and diff columns move across whole.
230            Column::Typed((data, times, diffs)) => {
231                let mut inner = T2::Container::default();
232                for time in times.borrow().into_index_iter() {
233                    inner.push(&T2::to_inner(T1::into_owned(time)));
234                }
235                Column::Typed((data, inner, diffs))
236            }
237            // A serialized column owns no typed sub-containers, so only the times are
238            // materialized. The data and diff columns go from their borrowed views
239            // straight into the output allocation, a copy per column rather than a
240            // decode and re-encode per record.
241            serialized => {
242                let (borrowed_data, borrowed_times, borrowed_diffs) = serialized.borrow();
243                let mut times = T2::Container::default();
244                for time in borrowed_times.into_index_iter() {
245                    times.push(&T2::to_inner(T1::into_owned(time)));
246                }
247                let view = (borrowed_data, times.borrow(), borrowed_diffs);
248                let words = indexed::length_in_words(&view);
249                let mut alloc: Vec<u64> = Vec::with_capacity(words);
250                indexed::encode(&mut alloc, &view);
251                Column::Align(alloc)
252            }
253        }
254    }
255}
256
257/// Advances a column's times by `step`, so a columnar collection can be a loop variable.
258///
259/// A time that the summary does not advance drops its record, so the data and diff columns
260/// cannot move across whole the way [`Enter`] moves them: a record's position in one column
261/// has to keep matching its position in the others. The times are read and advanced first,
262/// and the rest is rebuilt only when the step actually dropped something.
263impl<D, T, R> ResultsIn<T::Summary> for Column<(D, T, R)>
264where
265    D: Columnar,
266    T: Columnar + Timestamp,
267    R: Columnar,
268    (D, T, R): Columnar<Container = (D::Container, T::Container, R::Container)>,
269    for<'a> D::Container: columnar::Push<Ref<'a, D>>,
270    for<'a> T::Container: columnar::Push<&'a T>,
271    for<'a> R::Container: columnar::Push<Ref<'a, R>>,
272{
273    fn results_in(self, step: &T::Summary) -> Self {
274        use columnar::Push;
275        use timely::progress::PathSummary;
276
277        // Advance the times first. Whether the step dropped any record decides whether the
278        // data and diff columns still line up with the new times, which is what lets them
279        // be reused rather than rebuilt.
280        let (times, kept) = {
281            let (_, borrowed_times, _) = self.borrow();
282            let mut times = T::Container::default();
283            let mut time = T::minimum();
284            let kept: Vec<bool> = borrowed_times
285                .into_index_iter()
286                .map(|reference| {
287                    time.copy_from(reference);
288                    match step.results_in(&time) {
289                        Some(time) => {
290                            times.push(&time);
291                            true
292                        }
293                        None => false,
294                    }
295                })
296                .collect();
297            (times, kept)
298        };
299
300        if kept.iter().all(|kept| *kept) {
301            match self {
302                // Only the times changed, so the data and diff columns move across whole.
303                Column::Typed((data, _, diffs)) => Column::Typed((data, times, diffs)),
304                // A serialized column owns no typed sub-containers, so its data and diff
305                // columns go from their borrowed views straight into the output allocation.
306                serialized => {
307                    let (borrowed_data, _, borrowed_diffs) = serialized.borrow();
308                    let view = (borrowed_data, times.borrow(), borrowed_diffs);
309                    let words = indexed::length_in_words(&view);
310                    let mut alloc: Vec<u64> = Vec::with_capacity(words);
311                    indexed::encode(&mut alloc, &view);
312                    Column::Align(alloc)
313                }
314            }
315        } else {
316            let (borrowed_data, _, borrowed_diffs) = self.borrow();
317            let mut data = D::Container::default();
318            let mut diffs = R::Container::default();
319            let records = borrowed_data
320                .into_index_iter()
321                .zip_eq(borrowed_diffs.into_index_iter())
322                .zip_eq(kept.iter());
323            for ((datum, diff), _) in records.filter(|(_, kept)| **kept) {
324                data.push(datum);
325                diffs.push(diff);
326            }
327            Column::Typed((data, times, diffs))
328        }
329    }
330}
331
332/// Words per 2 MiB. `length_in_words` returns serialized size in `u64` units,
333/// so this is the page count we round up to. Picked to match
334/// [`builder::ColumnBuilder`]'s output granularity so chunks shipped from the
335/// merger and chunks shipped from the builder are sized comparably.
336const SHIP_WORDS: usize = 1 << 18;
337
338/// Returns true once the serialized size of `borrow` reaches 10% under
339/// `SHIP_WORDS`.
340///
341/// Monotone in size, deliberately not a window below the boundary. A single
342/// record wider than a window steps clear over it, and a ship signal that
343/// un-fires past the boundary lets a chunk grow until it exceeds the buffer
344/// pool's largest size class, past which a spilled body degrades to
345/// permanently resident. The same heuristic as [`builder::ColumnBuilder`]'s
346/// ship point, lifted out so the builder, the merger, and the
347/// `SizableContainer` impl agree on the signal.
348#[inline]
349pub(crate) fn at_serialized_capacity<'a, A>(borrow: &A) -> bool
350where
351    A: columnar::AsBytes<'a>,
352{
353    indexed::length_in_words(borrow) >= SHIP_WORDS - SHIP_WORDS / 10
354}
355
356impl<C: Columnar> SizableContainer for Column<C> {
357    fn at_capacity(&self) -> bool {
358        // Match `ColumnBuilder`'s ship heuristic: serialized size at the
359        // 2 MiB ship threshold. Aligns chunk-size choices across the two
360        // paths and keeps recipients dealing with a single granularity.
361        //
362        // Serialized chunks (`Bytes` / `Align`) have no typed builder to push
363        // into, so they're trivially "at capacity" — there's no further work
364        // they can absorb.
365        match self {
366            Column::Typed(c) => at_serialized_capacity(&c.borrow()),
367            Column::Bytes(_) | Column::Align(_) => true,
368        }
369    }
370
371    fn ensure_capacity(&mut self, _stash: &mut Option<Self>) {
372        // No pre-reservation: chunks are recycled by the merge framework, so
373        // leaf capacities settle to steady-state after the first round and
374        // there is nothing useful to reserve up front. The `SizableContainer`
375        // impl exists so `at_capacity` is callable on result chunks during
376        // `Merger::merge` orchestration; `ensure_capacity` is a required
377        // method on the trait but has no work to do here.
378    }
379}
380
381impl<C: Columnar> ContainerBytes for Column<C> {
382    #[inline]
383    fn from_bytes(bytes: Bytes) -> Self {
384        // Our expectation / hope is that `bytes` is `u64` aligned and sized.
385        // If the alignment is borked, we can relocate. If the size is borked,
386        // not sure what we do in that case. An incorrect size indicates a problem
387        // of `into_bytes`, or a failure of the communication layer, both of which
388        // are unrecoverable.
389        assert_eq!(bytes.len() % 8, 0);
390        if let Ok(_) = bytemuck::try_cast_slice::<_, u64>(&bytes) {
391            Self::Bytes(bytes)
392        } else {
393            // We failed to cast the slice, so we'll reallocate. `Vec<u64>`
394            // is u64-aligned by construction.
395            Self::Align(bytemuck::allocation::pod_collect_to_vec(&bytes[..]))
396        }
397    }
398
399    #[inline]
400    fn length_in_bytes(&self) -> usize {
401        match self {
402            Column::Typed(t) => indexed::length_in_bytes(&t.borrow()),
403            Column::Bytes(b) => b.len(),
404            Column::Align(a) => 8 * a.len(),
405        }
406    }
407
408    #[inline]
409    fn into_bytes<W: ::std::io::Write>(&self, writer: &mut W) {
410        match self {
411            Column::Typed(t) => indexed::write(writer, &t.borrow()).unwrap(),
412            Column::Bytes(b) => writer.write_all(b).unwrap(),
413            Column::Align(a) => writer.write_all(bytemuck::cast_slice(a)).unwrap(),
414        }
415    }
416}
417
418/// An exchange function for columnar tuples of the form `((K, V), T, D)`. Rust has a hard
419/// time to figure out the lifetimes of the elements when specified as a closure, so we rather
420/// specify it as a function.
421#[inline(always)]
422pub fn columnar_exchange<K, V, T, D>(((k, _), _, _): &Ref<'_, ((K, V), T, D)>) -> u64
423where
424    K: Columnar,
425    for<'a> Ref<'a, K>: Hash,
426    V: Columnar,
427    D: Columnar,
428    T: Columnar,
429{
430    k.hashed()
431}
432
433/// Routes a `(D, T, R)` column by the hash of its data column.
434///
435/// Counterpart to [`columnar_exchange`] for collections whose data is not a
436/// key/value pair. Consolidation sites want [`columnar_consolidate_exchange`]
437/// instead.
438pub fn columnar_exchange_data<D, T, R>((d, _, _): &Ref<'_, (D, T, R)>) -> u64
439where
440    D: Columnar,
441    for<'a> Ref<'a, D>: Hash,
442    T: Columnar,
443    R: Columnar,
444{
445    d.hashed()
446}
447
448/// Routes a `(D, T, R)` column for consolidation, by a fixed-seed AHash of its data column.
449///
450/// Worker assignment is `hash % workers`, so the low bits alone decide the split and the
451/// [`Hashable`] default (FNV) the other exchange functions use diffuses them poorly. The
452/// seed is fixed, so routing is identical across builds and replicas.
453///
454/// Spelled as a function rather than a closure over the hasher state, because the argument
455/// is higher-ranked in its lifetime and closure inference cannot express that.
456pub fn columnar_consolidate_exchange<D, T, R>((d, _, _): &Ref<'_, (D, T, R)>) -> u64
457where
458    D: Columnar,
459    for<'a> Ref<'a, D>: Hash,
460    T: Columnar,
461    R: Columnar,
462{
463    static STATE: LazyLock<ahash::RandomState> = LazyLock::new(crate::hash::fixed_state);
464    let mut hasher = STATE.build_hasher();
465    d.hash(&mut hasher);
466    hasher.finish()
467}
468
469#[cfg(test)]
470mod tests {
471    use timely::bytes::arc::BytesMut;
472    use timely::container::PushInto;
473    use timely::dataflow::channels::ContainerBytes;
474
475    use super::*;
476
477    /// Produce some bytes that are in columnar format.
478    fn raw_columnar_bytes() -> Vec<u8> {
479        let mut raw = Vec::new();
480        raw.extend(16_u64.to_le_bytes()); // offsets
481        raw.extend(28_u64.to_le_bytes()); // length
482        raw.extend(1_i32.to_le_bytes());
483        raw.extend(2_i32.to_le_bytes());
484        raw.extend(3_i32.to_le_bytes());
485        raw.extend([0, 0, 0, 0]); // padding
486        raw
487    }
488
489    #[mz_ore::test]
490    fn test_column_clone() {
491        let columns = Columnar::as_columns([1, 2, 3].iter());
492        let column_typed: Column<i32> = Column::Typed(columns);
493        let column_typed2 = column_typed.clone();
494
495        assert_eq!(
496            column_typed2.borrow().into_index_iter().collect::<Vec<_>>(),
497            vec![&1, &2, &3]
498        );
499
500        let bytes = BytesMut::from(raw_columnar_bytes()).freeze();
501        let column_bytes: Column<i32> = Column::Bytes(bytes);
502        let column_bytes2 = column_bytes.clone();
503
504        assert_eq!(
505            column_bytes2.borrow().into_index_iter().collect::<Vec<_>>(),
506            vec![&1, &2, &3]
507        );
508
509        let raw = raw_columnar_bytes();
510        let mut region: Vec<u64> = vec![0; raw.len() / 8];
511        let region_bytes = bytemuck::cast_slice_mut(&mut region[..]);
512        region_bytes[..raw.len()].copy_from_slice(&raw);
513        let column_align: Column<i32> = Column::Align(region);
514        let column_align2 = column_align.clone();
515
516        assert_eq!(
517            column_align2.borrow().into_index_iter().collect::<Vec<_>>(),
518            vec![&1, &2, &3]
519        );
520    }
521
522    /// Assert the desired contents of raw_columnar_bytes so that diagnosing test failures is
523    /// easier.
524    #[mz_ore::test]
525    fn test_column_known_bytes() {
526        let mut column: Column<i32> = Default::default();
527        column.push_into(1);
528        column.push_into(2);
529        column.push_into(3);
530        let mut data = Vec::new();
531        column.into_bytes(&mut std::io::Cursor::new(&mut data));
532        assert_eq!(data, raw_columnar_bytes());
533    }
534
535    #[mz_ore::test]
536    fn test_column_from_bytes() {
537        let raw = raw_columnar_bytes();
538
539        let buf = vec![0; raw.len() + 8];
540        let align = buf.as_ptr().align_offset(std::mem::size_of::<u64>());
541        let mut bytes_mut = BytesMut::from(buf);
542        let _ = bytes_mut.extract_to(align);
543        bytes_mut[..raw.len()].copy_from_slice(&raw);
544        let aligned_bytes = bytes_mut.extract_to(raw.len());
545
546        let column: Column<i32> = Column::from_bytes(aligned_bytes);
547        assert!(matches!(column, Column::Bytes(_)));
548        assert_eq!(
549            column.borrow().into_index_iter().collect::<Vec<_>>(),
550            vec![&1, &2, &3]
551        );
552
553        let buf = vec![0; raw.len() + 8];
554        let align = buf.as_ptr().align_offset(std::mem::size_of::<u64>());
555        let mut bytes_mut = BytesMut::from(buf);
556        let _ = bytes_mut.extract_to(align + 1);
557        bytes_mut[..raw.len()].copy_from_slice(&raw);
558        let unaligned_bytes = bytes_mut.extract_to(raw.len());
559
560        let column: Column<i32> = Column::from_bytes(unaligned_bytes);
561        assert!(matches!(column, Column::Align(_)));
562        assert_eq!(
563            column.borrow().into_index_iter().collect::<Vec<_>>(),
564            vec![&1, &2, &3]
565        );
566    }
567
568    /// The ship signal is monotone: once it fires it stays fired, even when
569    /// a single wide record steps far past the 2 MiB boundary in one push.
570    #[mz_ore::test]
571    #[cfg_attr(miri, ignore)] // too slow
572    fn ship_threshold_monotone() {
573        use columnar::Push;
574        let mut container = <Vec<u64> as Columnar>::Container::default();
575        // Wider than 10% of any boundary a 25 MiB run can reach.
576        let wide: Vec<u64> = vec![0u64; 50_000];
577        let mut fired = false;
578        for pushes in 1..=64 {
579            container.push(&wide);
580            let now = at_serialized_capacity(&container.borrow());
581            if fired {
582                assert!(now, "ship signal un-fired at {pushes} records");
583            }
584            fired = fired || now;
585        }
586        assert!(fired, "ship signal never fired");
587    }
588}