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