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