Skip to main content

mz_timely_util/columnar/
unload.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//! [`UnloadChunk`]: the bulk-read capability for [`Chunk`] families, and the
11//! batch-level driver over it.
12//!
13//! Ported from differential's unmerged bulk-read surface
14//! (TimelyDataflow/differential-dataflow#782) so that consumers here depend
15//! on a local definition rather than a git pin. One deliberate difference
16//! from the upstream shape: the batch-level driver is the [`UnloadBatch`]
17//! extension trait rather than inherent methods on differential's
18//! [`ChunkBatch`] (an inherent impl on a foreign type is not available to
19//! this crate); the method names and signatures match, so call sites survive
20//! a future switch to upstream re-exports unchanged.
21
22use std::cmp::Ordering;
23
24use differential_dataflow::trace::chunk::{Chunk, ChunkBatch};
25
26/// Look up a sorted set of keys in a chunk, copying the matching updates out
27/// into caller-owned staging.
28///
29/// This is the bulk, copy-out way to read a chunk: extraction is finished
30/// with a chunk's body when the call returns, so a spilled body is read for
31/// the scope of one call and no reference into pool memory ever exists
32/// outside it. That contract is what lets a buffer pool evict with no reader
33/// accounting, where navigation-style access would need the body kept
34/// readable for as long as the reader holds it.
35///
36/// Callers usually read whole batches, not single chunks: the [`UnloadBatch`]
37/// extension on [`ChunkBatch`] looks up a probe set across the batch's chunk
38/// sequence, and its `fetch_into` stages the full contents (the scan path).
39/// Results land in a [`Staging`](UnloadChunk::Staging) the caller owns and
40/// consumes at leisure. Staged times are copied verbatim — advancement by a
41/// compaction frontier stays with the consumer.
42///
43/// Like [`Chunk`] itself, the trait has no key, val, time, or diff opinions:
44/// `Staging` and `Probes` are opaque types the implementing family chooses,
45/// and the one key comparison the batch driver needs is delegated to the
46/// chunk via [`locate`](UnloadChunk::locate) — which, like
47/// [`len`](Chunk::len), must be answerable from resident metadata even when
48/// the body is spilled.
49///
50/// # The consume-index protocol (implementors)
51///
52/// A batch's chunks are one globally-sorted sequence cut at arbitrary points,
53/// so a key's updates may straddle consecutive chunks.
54/// [`extract_into`](UnloadChunk::extract_into) carries that invariant as a
55/// protocol: a chunk consumes (advances `*probe_index` past) every probe
56/// strictly below its last key — extracting hits, silently passing over
57/// misses — and *extracts but does not consume* a probe equal to its last
58/// key, so the driver re-offers that probe to the next chunk, whose
59/// continuation lands in staging as a legal straddle. Consumers detect
60/// misses as keys absent from staging.
61pub trait UnloadChunk: Chunk {
62    /// Where extracted updates land: an owned accumulation of resident data,
63    /// chosen by the implementing family.
64    ///
65    /// Appends arrive in global `(key, val, time)` order; a group continuing
66    /// across appends may remain split, exactly as chunk sequences elsewhere
67    /// carry the straddle invariant.
68    type Staging: Default;
69
70    /// A sorted, deduplicated key column, borrowed from the same family —
71    /// another chunk's key column, or a synthesized probe set.
72    type Probes<'a>: Copy;
73
74    /// The number of probe keys.
75    fn probe_count(probes: Self::Probes<'_>) -> usize;
76
77    /// Where `probes[probe_index]` falls relative to this chunk's key span:
78    /// `Less` before the first key, `Equal` within `[first, last]`, `Greater`
79    /// past the last key.
80    ///
81    /// Resident metadata only — must never fetch a spilled body.
82    fn locate(&self, probes: Self::Probes<'_>, probe_index: usize) -> Ordering;
83
84    /// Append this chunk's updates for probes at and after `*probe_index`
85    /// into `staging`, advancing `*probe_index` past every probe strictly
86    /// below this chunk's last key.
87    ///
88    /// A probe *equal* to the last key is extracted but not consumed: its
89    /// group may continue in the next chunk (see the protocol above). Any
90    /// read of a spilled body is scoped to this call.
91    fn extract_into(
92        &self,
93        probes: Self::Probes<'_>,
94        probe_index: &mut usize,
95        staging: &mut Self::Staging,
96    );
97
98    /// Append the whole chunk into `staging` (the scan path).
99    fn fetch_into(&self, staging: &mut Self::Staging);
100}
101
102/// The batch-level driver over [`UnloadChunk`], as an extension of
103/// [`ChunkBatch`].
104pub trait UnloadBatch<C: UnloadChunk> {
105    /// Extract every probe hit in this batch into `staging`.
106    ///
107    /// Gallops the chunk list by [`locate`](UnloadChunk::locate) — resident
108    /// metadata only — and opens (via
109    /// [`extract_into`](UnloadChunk::extract_into)) only the chunks whose key
110    /// span contains a probe. Probes that fall in the gap between two chunks'
111    /// spans are consumed against resident metadata alone. A probe left
112    /// unconsumed at a chunk's last key is re-offered to the next chunk,
113    /// whose continuation follows in staging.
114    fn extract_into(&self, probes: C::Probes<'_>, staging: &mut C::Staging);
115
116    /// Materialize the batch's full contents into `staging` (the scan path).
117    fn fetch_into(&self, staging: &mut C::Staging);
118}
119
120impl<C: UnloadChunk> UnloadBatch<C> for ChunkBatch<C> {
121    fn extract_into(&self, probes: C::Probes<'_>, staging: &mut C::Staging) {
122        let count = C::probe_count(probes);
123        let chunks = &self.chunks[..];
124        let (mut probe_index, mut chunk) = (0usize, 0usize);
125        while probe_index < count && chunk < chunks.len() {
126            // Whether chunk `c` lies entirely below `probes[probe_index]`
127            // (its last key is smaller), read from resident metadata.
128            let below = |c: usize| chunks[c].locate(probes, probe_index) == Ordering::Greater;
129            // Gallop to the first chunk not below the probe: exponential
130            // search from the current chunk, then binary within the bracket.
131            if below(chunk) {
132                let (mut prev, mut step) = (chunk, 1usize);
133                while prev + step < chunks.len() && below(prev + step) {
134                    prev += step;
135                    step <<= 1;
136                }
137                let (mut a, mut b) = (prev + 1, (prev + step).min(chunks.len()));
138                while a < b {
139                    let m = a + (b - a) / 2;
140                    if below(m) { a = m + 1 } else { b = m }
141                }
142                chunk = a;
143            }
144            if chunk >= chunks.len() {
145                return;
146            }
147            // Consume probes in the gap below this chunk's first key: they
148            // match nothing in the batch, and deciding so from resident
149            // metadata is what keeps an untouched body unopened.
150            while probe_index < count && chunks[chunk].locate(probes, probe_index) == Ordering::Less
151            {
152                probe_index += 1;
153            }
154            if probe_index < count && chunks[chunk].locate(probes, probe_index) == Ordering::Equal {
155                chunks[chunk].extract_into(probes, &mut probe_index, staging);
156            }
157            // Everything strictly below this chunk's last key is consumed; a
158            // probe equal to it was extracted but left for the next chunk
159            // (the straddle re-offer).
160            chunk += 1;
161        }
162    }
163
164    fn fetch_into(&self, staging: &mut C::Staging) {
165        for chunk in &self.chunks {
166            chunk.fetch_into(staging);
167        }
168    }
169}
170
171#[cfg(test)]
172mod tests {
173    //! Contract tests for the batch driver over a miniature row family:
174    //! extraction over arbitrary chunk cuts and probe placements — straddled
175    //! keys included — equals the reference filter of the raw rows.
176
177    use std::collections::VecDeque;
178
179    use differential_dataflow::trace::Description;
180    use timely::progress::Antichain;
181    use timely::progress::frontier::AntichainRef;
182
183    use super::*;
184
185    /// A sorted, consolidated run of `(key, val)` rows; the minimal family.
186    /// Only the read surface is exercised: the [`Chunk`] transducers are the
187    /// maintenance half, which the driver never invokes.
188    #[derive(Clone)]
189    struct Rows(Vec<(u64, u64)>);
190
191    impl Chunk for Rows {
192        type Time = u64;
193        const TARGET: usize = 8;
194        fn len(&self) -> usize {
195            self.0.len()
196        }
197        fn merge(_: &mut VecDeque<Self>, _: &mut VecDeque<Self>, _: &mut VecDeque<Self>) {
198            unimplemented!("maintenance is not under test")
199        }
200        fn extract(
201            _: &mut VecDeque<Self>,
202            _: AntichainRef<u64>,
203            _: &mut Antichain<u64>,
204            _: &mut VecDeque<Self>,
205            _: &mut VecDeque<Self>,
206        ) {
207            unimplemented!("maintenance is not under test")
208        }
209        fn advance(_: &mut VecDeque<Self>, _: AntichainRef<u64>, _: bool, _: &mut VecDeque<Self>) {
210            unimplemented!("maintenance is not under test")
211        }
212        fn settle(_: &mut VecDeque<Self>, _: bool, _: &mut VecDeque<Self>) {
213            unimplemented!("maintenance is not under test")
214        }
215    }
216
217    impl UnloadChunk for Rows {
218        type Staging = Vec<(u64, u64)>;
219        type Probes<'a> = &'a [u64];
220
221        fn probe_count(probes: &[u64]) -> usize {
222            probes.len()
223        }
224
225        fn locate(&self, probes: &[u64], probe_index: usize) -> Ordering {
226            let probe = probes[probe_index];
227            if probe < self.0[0].0 {
228                Ordering::Less
229            } else if probe > self.0[self.0.len() - 1].0 {
230                Ordering::Greater
231            } else {
232                Ordering::Equal
233            }
234        }
235
236        fn extract_into(
237            &self,
238            probes: &[u64],
239            probe_index: &mut usize,
240            staging: &mut Self::Staging,
241        ) {
242            let rows = &self.0[..];
243            let last = rows[rows.len() - 1].0;
244            let mut pos = 0;
245            while *probe_index < probes.len() {
246                let probe = probes[*probe_index];
247                if probe > last {
248                    return;
249                }
250                while pos < rows.len() && rows[pos].0 < probe {
251                    pos += 1;
252                }
253                while pos < rows.len() && rows[pos].0 == probe {
254                    staging.push(rows[pos]);
255                    pos += 1;
256                }
257                if probe == last {
258                    return;
259                }
260                *probe_index += 1;
261            }
262        }
263
264        fn fetch_into(&self, staging: &mut Self::Staging) {
265            staging.extend_from_slice(&self.0);
266        }
267    }
268
269    fn batch_of(rows: &[(u64, u64)], cut: usize) -> ChunkBatch<Rows> {
270        let chunks: Vec<Rows> = rows.chunks(cut).map(|c| Rows(c.to_vec())).collect();
271        let description = Description::new(
272            Antichain::from_elem(0u64),
273            Antichain::new(),
274            Antichain::from_elem(0u64),
275        );
276        ChunkBatch::new(chunks, description)
277    }
278
279    /// Every (chunk cut, contiguous probe range) placement over rows with a
280    /// multi-chunk-spanning key equals the reference filter, and the scan
281    /// path reproduces the batch exactly.
282    #[mz_ore::test]
283    fn extract_matches_filter() {
284        // Even keys 0..=16; key 8 carries 6 rows so it spans chunks at every
285        // cut size below 6.
286        let mut rows: Vec<(u64, u64)> = Vec::new();
287        for k in (0..=16u64).step_by(2) {
288            let copies = if k == 8 { 6 } else { 1 };
289            for c in 0..copies {
290                rows.push((k, c));
291            }
292        }
293        for cut in 1..=5 {
294            let batch = batch_of(&rows, cut);
295            for lo in 0..=18u64 {
296                for hi in lo..=18u64 {
297                    let probes: Vec<u64> = (lo..=hi).collect();
298                    let mut staging = Vec::new();
299                    batch.extract_into(&probes[..], &mut staging);
300                    let want: Vec<_> = rows
301                        .iter()
302                        .filter(|r| lo <= r.0 && r.0 <= hi)
303                        .copied()
304                        .collect();
305                    assert_eq!(staging, want, "cut={cut} probes={lo}..={hi}");
306                }
307            }
308            let mut staging = Vec::new();
309            batch.fetch_into(&mut staging);
310            assert_eq!(staging, rows, "cut={cut}");
311        }
312    }
313}