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}