1use std::collections::BTreeMap;
11use std::iter;
12use std::sync::LazyLock;
13
14use differential_dataflow::trace::implementations::BatchContainer;
15use differential_dataflow::trace::{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: 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: 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: 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::batcher::{Batcher, Extract};
225 use differential_dataflow::trace::Builder;
226 use differential_dataflow::trace::implementations::{ValBatcher, ValBuilder};
227 use timely::progress::Antichain;
228
229 use super::*;
230
231 fn batch_from_tuples_with<V: Ord + Clone + 'static>(
234 mut tuples: Vec<((String, V), u64, Diff)>,
235 upper: u64,
236 ) -> <ValBuilder<String, V, u64, Diff> as Builder>::Output {
237 let mut batcher = ValBatcher::<String, V, u64, Diff>::new(None, 0);
238 batcher.insert(&mut tuples);
240 let (batch, _frontier) = batcher.extract(Antichain::from_elem(upper).borrow());
241 batch.expect("updates were pushed, so the batch is non-empty")
242 }
243
244 fn batch_from_tuples(
247 tuples: Vec<((String, String), u64, Diff)>,
248 upper: u64,
249 ) -> <ValBuilder<String, String, u64, Diff> as Builder>::Output {
250 batch_from_tuples_with(tuples, upper)
251 }
252
253 fn collect_diff_pairs<B, C>(batch: &B) -> Vec<(String, u64, Option<String>, Option<String>)>
256 where
257 B: Navigable<Cursor = C>,
258 C: Cursor<Storage = B, Diff = Diff>,
259 C::Time: Copy + Into<u64>,
260 C::ValOwn: 'static + Into<String>,
261 <C::KeyContainer as BatchContainer>::Owned: Into<String> + Clone,
262 {
263 let mut out = vec![];
264 for_each_diff_pair(batch, |k, t, dp| {
265 out.push((
266 k.clone().into(),
267 t.into(),
268 dp.before.map(Into::into),
269 dp.after.map(Into::into),
270 ));
271 });
272 out.sort();
273 out
274 }
275
276 fn collect_bounded_diff_pairs<B, C>(
279 batch: &B,
280 lower: Option<u64>,
281 upper: Option<u64>,
282 ) -> Vec<(String, Vec<(u64, Option<String>, Option<String>)>)>
283 where
284 B: Navigable<Cursor = C>,
285 C: Cursor<Storage = B, Diff = Diff, Time = u64>,
286 C::ValOwn: 'static + Into<String>,
287 <C::KeyContainer as BatchContainer>::Owned: Into<String>,
288 {
289 let mut out: Vec<_> = iter_diff_pairs(
290 batch,
291 lower.map(Antichain::from_elem),
292 upper.map(Antichain::from_elem),
293 )
294 .map(|(k, pairs)| {
295 let pairs = pairs
296 .into_iter()
297 .map(|(t, dp)| (t, dp.before.map(Into::into), dp.after.map(Into::into)))
298 .collect();
299 (k.into(), pairs)
300 })
301 .collect();
302 out.sort();
303 out
304 }
305
306 #[mz_ore::test]
307 fn single_insertion() {
308 let batch = batch_from_tuples(vec![(("k1".into(), "v1".into()), 5, Diff::ONE)], 6);
309 let pairs = collect_diff_pairs(&batch);
310 assert_eq!(pairs, vec![("k1".into(), 5, None, Some("v1".into()))]);
311 }
312
313 #[mz_ore::test]
314 fn single_retraction() {
315 let batch = batch_from_tuples(vec![(("k1".into(), "v1".into()), 5, -Diff::ONE)], 6);
316 let pairs = collect_diff_pairs(&batch);
317 assert_eq!(pairs, vec![("k1".into(), 5, Some("v1".into()), None)]);
318 }
319
320 #[mz_ore::test]
321 fn update_at_same_timestamp() {
322 let batch = batch_from_tuples(
325 vec![
326 (("k1".into(), "v1".into()), 5, -Diff::ONE),
327 (("k1".into(), "v2".into()), 5, Diff::ONE),
328 ],
329 6,
330 );
331 let pairs = collect_diff_pairs(&batch);
332 assert_eq!(
333 pairs,
334 vec![("k1".into(), 5, Some("v1".into()), Some("v2".into()))]
335 );
336 }
337
338 #[mz_ore::test]
339 fn update_across_timestamps() {
340 let batch = batch_from_tuples(
342 vec![
343 (("k1".into(), "v1".into()), 5, Diff::ONE),
344 (("k1".into(), "v1".into()), 10, -Diff::ONE),
345 (("k1".into(), "v2".into()), 10, Diff::ONE),
346 ],
347 11,
348 );
349 let pairs = collect_diff_pairs(&batch);
350 assert_eq!(
351 pairs,
352 vec![
353 ("k1".into(), 5, None, Some("v1".into())),
354 ("k1".into(), 10, Some("v1".into()), Some("v2".into())),
355 ]
356 );
357 }
358
359 #[mz_ore::test]
360 fn diff_greater_than_one_fans_out() {
361 let batch = batch_from_tuples(vec![(("k1".into(), "v1".into()), 5, Diff::from(3))], 6);
363 let pairs = collect_diff_pairs(&batch);
364 assert_eq!(
365 pairs,
366 vec![
367 ("k1".into(), 5, None, Some("v1".into())),
368 ("k1".into(), 5, None, Some("v1".into())),
369 ("k1".into(), 5, None, Some("v1".into())),
370 ]
371 );
372 }
373
374 #[mz_ore::test]
375 fn multiple_keys_are_independent() {
376 let batch = batch_from_tuples(
377 vec![
378 (("k1".into(), "v1".into()), 5, Diff::ONE),
379 (("k2".into(), "v2".into()), 5, Diff::ONE),
380 ],
381 6,
382 );
383 let pairs = collect_diff_pairs(&batch);
384 assert_eq!(
385 pairs,
386 vec![
387 ("k1".into(), 5, None, Some("v1".into())),
388 ("k2".into(), 5, None, Some("v2".into())),
389 ]
390 );
391 }
392
393 #[mz_ore::test]
394 fn unpaired_before_and_after_at_different_timestamps() {
395 let batch = batch_from_tuples(
398 vec![
399 (("k1".into(), "v1".into()), 5, -Diff::ONE),
400 (("k1".into(), "v2".into()), 10, Diff::ONE),
401 ],
402 11,
403 );
404 let pairs = collect_diff_pairs(&batch);
405 assert_eq!(
406 pairs,
407 vec![
408 ("k1".into(), 5, Some("v1".into()), None),
409 ("k1".into(), 10, None, Some("v2".into())),
410 ]
411 );
412 }
413
414 #[mz_ore::test]
415 fn pairs_are_grouped_by_key() {
416 let batch = batch_from_tuples(
418 vec![
419 (("k1".into(), "v1".into()), 5, Diff::ONE),
420 (("k1".into(), "v1".into()), 10, -Diff::ONE),
421 (("k1".into(), "v2".into()), 10, Diff::ONE),
422 (("k2".into(), "v3".into()), 7, Diff::ONE),
423 ],
424 11,
425 );
426 let items = collect_bounded_diff_pairs(&batch, None, None);
427 assert_eq!(
428 items,
429 vec![
430 (
431 "k1".into(),
432 vec![
433 (5, None, Some("v1".into())),
434 (10, Some("v1".into()), Some("v2".into())),
435 ]
436 ),
437 ("k2".into(), vec![(7, None, Some("v3".into()))]),
438 ]
439 );
440 }
441
442 #[mz_ore::test]
443 fn lower_bound_is_inclusive() {
444 let batch = batch_from_tuples(
445 vec![
446 (("k1".into(), "v1".into()), 4, Diff::ONE),
447 (("k1".into(), "v2".into()), 5, Diff::ONE),
448 ],
449 6,
450 );
451 let items = collect_bounded_diff_pairs(&batch, Some(5), None);
452 assert_eq!(
453 items,
454 vec![("k1".into(), vec![(5, None, Some("v2".into()))])]
455 );
456 }
457
458 #[mz_ore::test]
459 fn upper_bound_is_exclusive() {
460 let batch = batch_from_tuples(
461 vec![
462 (("k1".into(), "v1".into()), 5, Diff::ONE),
463 (("k1".into(), "v2".into()), 6, Diff::ONE),
464 ],
465 7,
466 );
467 let items = collect_bounded_diff_pairs(&batch, None, Some(6));
468 assert_eq!(
469 items,
470 vec![("k1".into(), vec![(5, None, Some("v1".into()))])]
471 );
472 }
473
474 #[mz_ore::test]
475 fn bounds_filter_within_a_key() {
476 let batch = batch_from_tuples(
479 vec![
480 (("k1".into(), "v1".into()), 5, Diff::ONE),
481 (("k1".into(), "v1".into()), 10, -Diff::ONE),
482 (("k1".into(), "v2".into()), 10, Diff::ONE),
483 ],
484 11,
485 );
486 let items = collect_bounded_diff_pairs(&batch, Some(6), Some(11));
487 assert_eq!(
488 items,
489 vec![(
490 "k1".into(),
491 vec![(10, Some("v1".into()), Some("v2".into()))]
492 )]
493 );
494 }
495
496 #[mz_ore::test]
497 fn fully_filtered_key_emits_no_item() {
498 let batch = batch_from_tuples(
501 vec![
502 (("k1".into(), "v1".into()), 3, Diff::ONE),
503 (("k2".into(), "v2".into()), 5, Diff::ONE),
504 ],
505 6,
506 );
507 let items = collect_bounded_diff_pairs(&batch, Some(5), None);
508 assert_eq!(
509 items,
510 vec![("k2".into(), vec![(5, None, Some("v2".into()))])]
511 );
512 }
513
514 #[mz_ore::test]
515 fn bounds_filter_everything() {
516 let batch = batch_from_tuples(vec![(("k1".into(), "v1".into()), 5, Diff::ONE)], 6);
517 let items = collect_bounded_diff_pairs(&batch, Some(6), None);
518 assert_eq!(items, vec![]);
519 }
520
521 thread_local! {
522 static TRACKED_LIVE: Cell<usize> = const { Cell::new(0) };
523 static TRACKED_PEAK: Cell<usize> = const { Cell::new(0) };
524 }
525
526 #[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
529 struct Tracked(String);
530
531 impl Tracked {
532 fn new(s: &str) -> Self {
533 Self::record_live();
534 Tracked(s.into())
535 }
536
537 fn record_live() {
538 let live = TRACKED_LIVE.with(|l| {
539 l.set(l.get() + 1);
540 l.get()
541 });
542 TRACKED_PEAK.with(|p| p.set(p.get().max(live)));
543 }
544
545 fn reset_peak() -> usize {
547 let live = TRACKED_LIVE.with(Cell::get);
548 TRACKED_PEAK.with(|p| p.set(live));
549 live
550 }
551
552 fn peak() -> usize {
553 TRACKED_PEAK.with(Cell::get)
554 }
555 }
556
557 impl Clone for Tracked {
558 fn clone(&self) -> Self {
559 Self::record_live();
560 Tracked(self.0.clone())
561 }
562 }
563
564 impl Drop for Tracked {
565 fn drop(&mut self) {
566 TRACKED_LIVE.with(|l| l.set(l.get() - 1));
567 }
568 }
569
570 #[mz_ore::test]
571 fn fan_out_clones_lazily() {
572 const FAN_OUT: i64 = 1000;
579 let batch = batch_from_tuples_with(
580 vec![(("k1".into(), Tracked::new("v1")), 5, Diff::from(FAN_OUT))],
581 6,
582 );
583
584 let baseline = Tracked::reset_peak();
585 let mut pair_count = 0i64;
586 for (_key, pairs) in iter_diff_pairs(&batch, None, None) {
587 for (_time, pair) in pairs {
588 pair_count += 1;
589 drop(pair);
590 }
591 }
592 assert_eq!(pair_count, FAN_OUT);
593
594 let peak_extra = Tracked::peak() - baseline;
595 assert!(
596 peak_extra <= 3,
597 "walking the fan-out held {peak_extra} extra live values at peak, \
598 expected O(1) rather than O(FAN_OUT)"
599 );
600 }
601}