1use std::collections::BTreeMap;
11use std::iter;
12use std::sync::LazyLock;
13
14use differential_dataflow::trace::implementations::BatchContainer;
15use differential_dataflow::trace::{BatchReader, Cursor, Navigable};
16use itertools::EitherOrBoth;
17use maplit::btreemap;
18use mz_ore::cast::CastFrom;
19use mz_repr::{
20 CatalogItemId, ColumnName, Datum, Diff, Row, RowPacker, SqlColumnType, SqlScalarType,
21};
22use timely::progress::Antichain;
23
24use crate::avro::DiffPair;
25
26pub fn for_each_diff_pair<B, C, F>(batch: &B, mut on_diff_pair: F)
31where
32 B: BatchReader<Time = C::Time> + Navigable<Cursor = C>,
33 C: Cursor<Storage = B, Diff = Diff>,
34 C::Time: Copy,
35 C::ValOwn: 'static,
36 F: FnMut(&<C::KeyContainer as BatchContainer>::Owned, C::Time, DiffPair<C::ValOwn>),
37{
38 for (key, timed_pairs) in iter_diff_pairs(batch, None, None) {
39 for (time, pair) in timed_pairs {
40 on_diff_pair(&key, time, pair);
41 }
42 }
43}
44
45pub async fn for_each_diff_pair_async<B, C, F, E>(
49 batch: &B,
50 lower: Option<Antichain<C::Time>>,
51 upper: Option<Antichain<C::Time>>,
52 mut on_diff_pair: F,
53) -> Result<(), E>
54where
55 B: BatchReader<Time = C::Time> + Navigable<Cursor = C>,
56 C: Cursor<Storage = B, Diff = Diff>,
57 C::Time: Copy,
58 C::ValOwn: 'static,
59 F: AsyncFnMut(
60 &<C::KeyContainer as BatchContainer>::Owned,
61 C::Time,
62 DiffPair<C::ValOwn>,
63 ) -> Result<(), E>,
64{
65 for (key, timed_pairs) in iter_diff_pairs(batch, lower, upper) {
66 for (time, pair) in timed_pairs {
67 on_diff_pair(&key, time, pair).await?;
68 }
69 }
70
71 Ok(())
72}
73
74pub fn iter_diff_pairs<B, C>(
91 batch: &B,
92 lower: Option<Antichain<C::Time>>,
93 upper: Option<Antichain<C::Time>>,
94) -> impl Iterator<
95 Item = (
96 <C::KeyContainer as BatchContainer>::Owned,
97 impl Iterator<Item = (C::Time, DiffPair<C::ValOwn>)>,
98 ),
99>
100where
101 B: BatchReader<Time = C::Time> + Navigable<Cursor = C>,
102 C: Cursor<Storage = B, Diff = Diff>,
103 C::Time: Copy,
104 C::ValOwn: 'static,
105{
106 let mut cursor = batch.cursor();
107 iter::from_fn(move || {
108 while cursor.key_valid(batch) {
109 let k = cursor.key(batch);
110
111 let mut befores: Vec<(C::Time, C::ValOwn, usize)> = vec![];
115 let mut afters: Vec<(C::Time, C::ValOwn, usize)> = vec![];
116 while cursor.val_valid(batch) {
117 let v = cursor.val(batch);
118 cursor.map_times(batch, |t, diff| {
119 let t = C::owned_time(t);
120 if lower.as_ref().is_some_and(|lower| !lower.less_equal(&t))
121 || upper.as_ref().is_some_and(|upper| upper.less_equal(&t))
122 {
123 return;
125 }
126 let diff = C::owned_diff(diff);
127 let update = (t, C::owned_val(v), usize::cast_from(diff.unsigned_abs()));
128 if diff < Diff::ZERO {
129 befores.push(update);
130 } else {
131 afters.push(update);
132 }
133 });
134 cursor.step_val(batch);
135 }
136
137 if befores.is_empty() && afters.is_empty() {
138 cursor.step_key(batch);
140 continue;
141 }
142
143 befores.sort_by_key(|(t, _v, _diff)| *t);
144 afters.sort_by_key(|(t, _v, _diff)| *t);
145
146 let fan_out = |(t, v, cnt): (C::Time, C::ValOwn, usize)| iter::repeat_n((t, v), cnt);
150 let befores_iter = befores.into_iter().flat_map(fan_out);
151 let afters_iter = afters.into_iter().flat_map(fan_out);
152
153 let key_owned = <C::KeyContainer as BatchContainer>::into_owned(k);
154
155 let pairs =
156 itertools::merge_join_by(befores_iter, afters_iter, |(t1, _v1), (t2, _v2)| {
157 t1.cmp(t2)
158 })
159 .map(|pair| {
160 let (t, before, after) = match pair {
161 EitherOrBoth::Both((t, before), (_t, after)) => {
162 (t, Some(before), Some(after))
163 }
164 EitherOrBoth::Left((t, before)) => (t, Some(before), None),
165 EitherOrBoth::Right((t, after)) => (t, None, Some(after)),
166 };
167 (t, DiffPair { before, after })
168 });
169
170 cursor.step_key(batch);
171 return Some((key_owned, pairs));
172 }
173 None
174 })
175}
176
177pub(crate) const TRANSACTION_TYPE_ID: CatalogItemId = CatalogItemId::Transient(1);
185pub(crate) const DBZ_ROW_TYPE_ID: CatalogItemId = CatalogItemId::Transient(2);
186
187pub static ENVELOPE_CUSTOM_NAMES: LazyLock<BTreeMap<CatalogItemId, String>> = LazyLock::new(|| {
188 btreemap! {
189 TRANSACTION_TYPE_ID => "transaction".into(),
190 DBZ_ROW_TYPE_ID => "row".into(),
191 }
192});
193
194pub(crate) fn dbz_envelope(
195 names_and_types: Vec<(ColumnName, SqlColumnType)>,
196) -> Vec<(ColumnName, SqlColumnType)> {
197 let row = SqlColumnType {
198 nullable: true,
199 scalar_type: SqlScalarType::Record {
200 fields: names_and_types.into(),
201 custom_id: Some(DBZ_ROW_TYPE_ID),
202 },
203 };
204 vec![("before".into(), row.clone()), ("after".into(), row)]
205}
206
207pub fn dbz_format(rp: &mut RowPacker, dp: DiffPair<Row>) {
208 if let Some(before) = dp.before {
209 rp.push_list_with(|rp| rp.extend_by_row(&before));
210 } else {
211 rp.push(Datum::Null);
212 }
213 if let Some(after) = dp.after {
214 rp.push_list_with(|rp| rp.extend_by_row(&after));
215 } else {
216 rp.push(Datum::Null);
217 }
218}
219
220#[cfg(test)]
221mod tests {
222 use std::cell::Cell;
223
224 use differential_dataflow::trace::implementations::chunker::ContainerChunker;
225 use differential_dataflow::trace::implementations::{ValBatcher, ValBuilder};
226 use differential_dataflow::trace::{Batcher, Builder};
227 use timely::container::{ContainerBuilder, PushInto};
228 use timely::progress::Antichain;
229
230 use super::*;
231
232 fn batch_from_tuples_with<V: Ord + Clone + 'static>(
235 mut tuples: Vec<((String, V), u64, Diff)>,
236 upper: u64,
237 ) -> <ValBuilder<String, V, u64, Diff> as Builder>::Output {
238 let mut batcher = ValBatcher::<String, V, u64, Diff>::new(None, 0);
241 let mut chunker = ContainerChunker::<Vec<((String, V), u64, Diff)>>::default();
242 chunker.push_into(&mut tuples);
243 while let Some(chunk) = chunker.extract() {
244 batcher.push_into(std::mem::take(chunk));
245 }
246 while let Some(chunk) = chunker.finish() {
247 batcher.push_into(std::mem::take(chunk));
248 }
249 let (mut chain, description) = batcher.seal(Antichain::from_elem(upper));
250 ValBuilder::<String, V, u64, Diff>::seal(&mut chain, description)
251 }
252
253 fn batch_from_tuples(
256 tuples: Vec<((String, String), u64, Diff)>,
257 upper: u64,
258 ) -> <ValBuilder<String, String, u64, Diff> as Builder>::Output {
259 batch_from_tuples_with(tuples, upper)
260 }
261
262 fn collect_diff_pairs<B, C>(batch: &B) -> Vec<(String, u64, Option<String>, Option<String>)>
265 where
266 B: BatchReader<Time = C::Time> + Navigable<Cursor = C>,
267 C: Cursor<Storage = B, Diff = Diff>,
268 C::Time: Copy + Into<u64>,
269 C::ValOwn: 'static + Into<String>,
270 <C::KeyContainer as BatchContainer>::Owned: Into<String> + Clone,
271 {
272 let mut out = vec![];
273 for_each_diff_pair(batch, |k, t, dp| {
274 out.push((
275 k.clone().into(),
276 t.into(),
277 dp.before.map(Into::into),
278 dp.after.map(Into::into),
279 ));
280 });
281 out.sort();
282 out
283 }
284
285 fn collect_bounded_diff_pairs<B, C>(
288 batch: &B,
289 lower: Option<u64>,
290 upper: Option<u64>,
291 ) -> Vec<(String, Vec<(u64, Option<String>, Option<String>)>)>
292 where
293 B: BatchReader<Time = u64> + Navigable<Cursor = C>,
294 C: Cursor<Storage = B, Diff = Diff, Time = u64>,
295 C::ValOwn: 'static + Into<String>,
296 <C::KeyContainer as BatchContainer>::Owned: Into<String>,
297 {
298 let mut out: Vec<_> = iter_diff_pairs(
299 batch,
300 lower.map(Antichain::from_elem),
301 upper.map(Antichain::from_elem),
302 )
303 .map(|(k, pairs)| {
304 let pairs = pairs
305 .into_iter()
306 .map(|(t, dp)| (t, dp.before.map(Into::into), dp.after.map(Into::into)))
307 .collect();
308 (k.into(), pairs)
309 })
310 .collect();
311 out.sort();
312 out
313 }
314
315 #[mz_ore::test]
316 fn single_insertion() {
317 let batch = batch_from_tuples(vec![(("k1".into(), "v1".into()), 5, Diff::ONE)], 6);
318 let pairs = collect_diff_pairs(&batch);
319 assert_eq!(pairs, vec![("k1".into(), 5, None, Some("v1".into()))]);
320 }
321
322 #[mz_ore::test]
323 fn single_retraction() {
324 let batch = batch_from_tuples(vec![(("k1".into(), "v1".into()), 5, -Diff::ONE)], 6);
325 let pairs = collect_diff_pairs(&batch);
326 assert_eq!(pairs, vec![("k1".into(), 5, Some("v1".into()), None)]);
327 }
328
329 #[mz_ore::test]
330 fn update_at_same_timestamp() {
331 let batch = batch_from_tuples(
334 vec![
335 (("k1".into(), "v1".into()), 5, -Diff::ONE),
336 (("k1".into(), "v2".into()), 5, Diff::ONE),
337 ],
338 6,
339 );
340 let pairs = collect_diff_pairs(&batch);
341 assert_eq!(
342 pairs,
343 vec![("k1".into(), 5, Some("v1".into()), Some("v2".into()))]
344 );
345 }
346
347 #[mz_ore::test]
348 fn update_across_timestamps() {
349 let batch = batch_from_tuples(
351 vec![
352 (("k1".into(), "v1".into()), 5, Diff::ONE),
353 (("k1".into(), "v1".into()), 10, -Diff::ONE),
354 (("k1".into(), "v2".into()), 10, Diff::ONE),
355 ],
356 11,
357 );
358 let pairs = collect_diff_pairs(&batch);
359 assert_eq!(
360 pairs,
361 vec![
362 ("k1".into(), 5, None, Some("v1".into())),
363 ("k1".into(), 10, Some("v1".into()), Some("v2".into())),
364 ]
365 );
366 }
367
368 #[mz_ore::test]
369 fn diff_greater_than_one_fans_out() {
370 let batch = batch_from_tuples(vec![(("k1".into(), "v1".into()), 5, Diff::from(3))], 6);
372 let pairs = collect_diff_pairs(&batch);
373 assert_eq!(
374 pairs,
375 vec![
376 ("k1".into(), 5, None, Some("v1".into())),
377 ("k1".into(), 5, None, Some("v1".into())),
378 ("k1".into(), 5, None, Some("v1".into())),
379 ]
380 );
381 }
382
383 #[mz_ore::test]
384 fn multiple_keys_are_independent() {
385 let batch = batch_from_tuples(
386 vec![
387 (("k1".into(), "v1".into()), 5, Diff::ONE),
388 (("k2".into(), "v2".into()), 5, Diff::ONE),
389 ],
390 6,
391 );
392 let pairs = collect_diff_pairs(&batch);
393 assert_eq!(
394 pairs,
395 vec![
396 ("k1".into(), 5, None, Some("v1".into())),
397 ("k2".into(), 5, None, Some("v2".into())),
398 ]
399 );
400 }
401
402 #[mz_ore::test]
403 fn unpaired_before_and_after_at_different_timestamps() {
404 let batch = batch_from_tuples(
407 vec![
408 (("k1".into(), "v1".into()), 5, -Diff::ONE),
409 (("k1".into(), "v2".into()), 10, Diff::ONE),
410 ],
411 11,
412 );
413 let pairs = collect_diff_pairs(&batch);
414 assert_eq!(
415 pairs,
416 vec![
417 ("k1".into(), 5, Some("v1".into()), None),
418 ("k1".into(), 10, None, Some("v2".into())),
419 ]
420 );
421 }
422
423 #[mz_ore::test]
424 fn pairs_are_grouped_by_key() {
425 let batch = batch_from_tuples(
427 vec![
428 (("k1".into(), "v1".into()), 5, Diff::ONE),
429 (("k1".into(), "v1".into()), 10, -Diff::ONE),
430 (("k1".into(), "v2".into()), 10, Diff::ONE),
431 (("k2".into(), "v3".into()), 7, Diff::ONE),
432 ],
433 11,
434 );
435 let items = collect_bounded_diff_pairs(&batch, None, None);
436 assert_eq!(
437 items,
438 vec![
439 (
440 "k1".into(),
441 vec![
442 (5, None, Some("v1".into())),
443 (10, Some("v1".into()), Some("v2".into())),
444 ]
445 ),
446 ("k2".into(), vec![(7, None, Some("v3".into()))]),
447 ]
448 );
449 }
450
451 #[mz_ore::test]
452 fn lower_bound_is_inclusive() {
453 let batch = batch_from_tuples(
454 vec![
455 (("k1".into(), "v1".into()), 4, Diff::ONE),
456 (("k1".into(), "v2".into()), 5, Diff::ONE),
457 ],
458 6,
459 );
460 let items = collect_bounded_diff_pairs(&batch, Some(5), None);
461 assert_eq!(
462 items,
463 vec![("k1".into(), vec![(5, None, Some("v2".into()))])]
464 );
465 }
466
467 #[mz_ore::test]
468 fn upper_bound_is_exclusive() {
469 let batch = batch_from_tuples(
470 vec![
471 (("k1".into(), "v1".into()), 5, Diff::ONE),
472 (("k1".into(), "v2".into()), 6, Diff::ONE),
473 ],
474 7,
475 );
476 let items = collect_bounded_diff_pairs(&batch, None, Some(6));
477 assert_eq!(
478 items,
479 vec![("k1".into(), vec![(5, None, Some("v1".into()))])]
480 );
481 }
482
483 #[mz_ore::test]
484 fn bounds_filter_within_a_key() {
485 let batch = batch_from_tuples(
488 vec![
489 (("k1".into(), "v1".into()), 5, Diff::ONE),
490 (("k1".into(), "v1".into()), 10, -Diff::ONE),
491 (("k1".into(), "v2".into()), 10, Diff::ONE),
492 ],
493 11,
494 );
495 let items = collect_bounded_diff_pairs(&batch, Some(6), Some(11));
496 assert_eq!(
497 items,
498 vec![(
499 "k1".into(),
500 vec![(10, Some("v1".into()), Some("v2".into()))]
501 )]
502 );
503 }
504
505 #[mz_ore::test]
506 fn fully_filtered_key_emits_no_item() {
507 let batch = batch_from_tuples(
510 vec![
511 (("k1".into(), "v1".into()), 3, Diff::ONE),
512 (("k2".into(), "v2".into()), 5, Diff::ONE),
513 ],
514 6,
515 );
516 let items = collect_bounded_diff_pairs(&batch, Some(5), None);
517 assert_eq!(
518 items,
519 vec![("k2".into(), vec![(5, None, Some("v2".into()))])]
520 );
521 }
522
523 #[mz_ore::test]
524 fn bounds_filter_everything() {
525 let batch = batch_from_tuples(vec![(("k1".into(), "v1".into()), 5, Diff::ONE)], 6);
526 let items = collect_bounded_diff_pairs(&batch, Some(6), None);
527 assert_eq!(items, vec![]);
528 }
529
530 thread_local! {
531 static TRACKED_LIVE: Cell<usize> = const { Cell::new(0) };
532 static TRACKED_PEAK: Cell<usize> = const { Cell::new(0) };
533 }
534
535 #[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
538 struct Tracked(String);
539
540 impl Tracked {
541 fn new(s: &str) -> Self {
542 Self::record_live();
543 Tracked(s.into())
544 }
545
546 fn record_live() {
547 let live = TRACKED_LIVE.with(|l| {
548 l.set(l.get() + 1);
549 l.get()
550 });
551 TRACKED_PEAK.with(|p| p.set(p.get().max(live)));
552 }
553
554 fn reset_peak() -> usize {
556 let live = TRACKED_LIVE.with(Cell::get);
557 TRACKED_PEAK.with(|p| p.set(live));
558 live
559 }
560
561 fn peak() -> usize {
562 TRACKED_PEAK.with(Cell::get)
563 }
564 }
565
566 impl Clone for Tracked {
567 fn clone(&self) -> Self {
568 Self::record_live();
569 Tracked(self.0.clone())
570 }
571 }
572
573 impl Drop for Tracked {
574 fn drop(&mut self) {
575 TRACKED_LIVE.with(|l| l.set(l.get() - 1));
576 }
577 }
578
579 #[mz_ore::test]
580 fn fan_out_clones_lazily() {
581 const FAN_OUT: i64 = 1000;
588 let batch = batch_from_tuples_with(
589 vec![(("k1".into(), Tracked::new("v1")), 5, Diff::from(FAN_OUT))],
590 6,
591 );
592
593 let baseline = Tracked::reset_peak();
594 let mut pair_count = 0i64;
595 for (_key, pairs) in iter_diff_pairs(&batch, None, None) {
596 for (_time, pair) in pairs {
597 pair_count += 1;
598 drop(pair);
599 }
600 }
601 assert_eq!(pair_count, FAN_OUT);
602
603 let peak_extra = Tracked::peak() - baseline;
604 assert!(
605 peak_extra <= 3,
606 "walking the fan-out held {peak_extra} extra live values at peak, \
607 expected O(1) rather than O(FAN_OUT)"
608 );
609 }
610}