1use differential_dataflow::consolidation::ConsolidatingContainerBuilder;
13use std::convert::Infallible;
14use std::fmt::Debug;
15use std::future::Future;
16use std::hash::Hash;
17use std::sync::Arc;
18use std::time::Instant;
19
20use differential_dataflow::lattice::Lattice;
21use futures::{StreamExt, future::Either};
22use mz_expr::{ColumnSpecs, EvalError, Interpreter, MfpPlan, ResultSpec, UnmaterializableFunc};
23use mz_ore::cast::CastFrom;
24use mz_ore::collections::CollectionExt;
25use mz_ore::str::redact;
26use mz_persist_client::cache::PersistClientCache;
27use mz_persist_client::cfg::{PersistConfig, RetryParameters};
28use mz_persist_client::fetch::{ExchangeableBatchPart, ShardSourcePart};
29use mz_persist_client::fetch::{FetchedBlob, FetchedPart};
30use mz_persist_client::operators::shard_source::{
31 ErrorHandler, FilterResult, SnapshotMode, shard_source,
32};
33use mz_persist_client::stats::STATS_AUDIT_PANIC;
34use mz_persist_types::Codec64;
35use mz_persist_types::codec_impls::UnitSchema;
36use mz_persist_types::columnar::{ColumnEncoder, Schema};
37use mz_repr::{
38 Datum, DatumVec, Diff, GlobalId, RelationDesc, ReprRelationType, Row, RowArena, Timestamp,
39};
40use mz_storage_types::StorageDiff;
41use mz_storage_types::controller::{CollectionMetadata, TxnsCodecRow};
42use mz_storage_types::errors::DataflowError;
43use mz_storage_types::sources::SourceData;
44use mz_storage_types::stats::RelationPartStats;
45use mz_timely_util::builder_async::{
46 Event, OperatorBuilder as AsyncOperatorBuilder, PressOnDropButton,
47};
48use mz_timely_util::probe::ProbeNotify;
49use mz_txn_wal::operator::{TxnsContext, txns_progress};
50use serde::{Deserialize, Serialize};
51use timely::PartialOrder;
52use timely::container::CapacityContainerBuilder;
53use timely::dataflow::channels::pact::Pipeline;
54use timely::dataflow::operators::generic::builder_rc::OperatorBuilder;
55use timely::dataflow::operators::generic::{OutputBuilder, OutputBuilderSession};
56use timely::dataflow::operators::{Capability, Leave, OkErr};
57use timely::dataflow::operators::{CapabilitySet, ConnectLoop, Feedback};
58use timely::dataflow::{Scope, Stream, StreamVec};
59use timely::order::TotalOrder;
60use timely::progress::Antichain;
61use timely::progress::Timestamp as TimelyTimestamp;
62use timely::progress::timestamp::PathSummary;
63use timely::scheduling::Activator;
64use tokio::sync::mpsc::UnboundedSender;
65use tracing::{error, trace};
66
67use crate::metrics::BackpressureMetrics;
68
69#[derive(
77 Copy,
78 Clone,
79 PartialEq,
80 Default,
81 Eq,
82 PartialOrd,
83 Ord,
84 Debug,
85 Serialize,
86 Deserialize,
87 Hash,
88 columnar::Columnar
89)]
90#[columnar(derive(PartialEq, Eq, PartialOrd, Ord))]
93pub struct Subtime(u64);
94
95impl PartialOrder for Subtime {
96 fn less_equal(&self, other: &Self) -> bool {
97 self.0.less_equal(&other.0)
98 }
99}
100
101impl TotalOrder for Subtime {}
102
103impl PathSummary<Subtime> for Subtime {
104 fn results_in(&self, src: &Subtime) -> Option<Subtime> {
105 self.0.results_in(&src.0).map(Subtime)
106 }
107
108 fn followed_by(&self, other: &Self) -> Option<Self> {
109 self.0.followed_by(&other.0).map(Subtime)
110 }
111}
112
113impl TimelyTimestamp for Subtime {
114 type Summary = Subtime;
115
116 fn minimum() -> Self {
117 Subtime(0)
118 }
119}
120
121impl columnation::Columnation for Subtime {
122 type InnerRegion = columnation::CopyRegion<Subtime>;
123}
124
125impl differential_dataflow::lattice::Lattice for Subtime {
126 fn join(&self, other: &Self) -> Self {
127 Subtime(std::cmp::max(self.0, other.0))
128 }
129 fn meet(&self, other: &Self) -> Self {
130 Subtime(std::cmp::min(self.0, other.0))
131 }
132}
133
134impl differential_dataflow::lattice::Maximum for Subtime {
135 fn maximum() -> Self {
136 Subtime(u64::MAX)
137 }
138}
139
140impl Subtime {
141 pub const fn least_summary() -> Self {
143 Subtime(1)
144 }
145}
146
147pub fn persist_source<'scope, E>(
170 scope: Scope<'scope, mz_repr::Timestamp>,
171 source_id: GlobalId,
172 persist_clients: Arc<PersistClientCache>,
173 txns_ctx: &TxnsContext,
174 metadata: CollectionMetadata,
175 read_schema: Option<RelationDesc>,
176 as_of: Option<Antichain<Timestamp>>,
177 snapshot_mode: SnapshotMode,
178 until: Antichain<Timestamp>,
179 map_filter_project: Option<&mut MfpPlan>,
180 max_inflight_bytes: Option<usize>,
181 start_signal: impl Future<Output = ()> + Send + 'static,
182 error_handler: ErrorHandler,
183) -> (
184 StreamVec<'scope, mz_repr::Timestamp, (Row, Timestamp, Diff)>,
185 StreamVec<'scope, mz_repr::Timestamp, (E, Timestamp, Diff)>,
186 Vec<PressOnDropButton>,
187)
188where
189 E: timely::ExchangeData + Ord + Clone + Debug + From<DataflowError> + From<EvalError>,
190{
191 let shard_metrics = persist_clients.shard_metrics(&metadata.data_shard, &source_id.to_string());
192
193 let mut tokens = vec![];
194
195 let outer = scope.clone();
196 let stream = scope.scoped(&format!("granular_backpressure({})", source_id), |scope| {
197 let (flow_control, flow_control_probe) = match max_inflight_bytes {
198 Some(max_inflight_bytes) => {
199 let backpressure_metrics = BackpressureMetrics {
200 emitted_bytes: Arc::clone(&shard_metrics.backpressure_emitted_bytes),
201 last_backpressured_bytes: Arc::clone(
202 &shard_metrics.backpressure_last_backpressured_bytes,
203 ),
204 retired_bytes: Arc::clone(&shard_metrics.backpressure_retired_bytes),
205 };
206
207 let probe = mz_timely_util::probe::Handle::default();
208 let progress_stream = mz_timely_util::probe::source(
209 scope.clone(),
210 format!("decode_backpressure_probe({source_id})"),
211 probe.clone(),
212 );
213 let flow_control = FlowControl {
214 progress_stream,
215 max_inflight_bytes,
216 summary: (Default::default(), Subtime::least_summary()),
217 metrics: Some(backpressure_metrics),
218 };
219 (Some(flow_control), Some(probe))
220 }
221 None => (None, None),
222 };
223
224 let cfg = Arc::clone(&persist_clients.cfg().configs);
230 let subscribe_sleep = match metadata.txns_shard {
231 Some(_) => Some(move || mz_txn_wal::operator::txns_data_shard_retry_params(&cfg)),
232 None => None,
233 };
234
235 let (stream, source_tokens) = persist_source_core(
236 outer,
237 scope,
238 source_id,
239 Arc::clone(&persist_clients),
240 metadata.clone(),
241 read_schema,
242 as_of.clone(),
243 snapshot_mode,
244 until.clone(),
245 map_filter_project,
246 flow_control,
247 subscribe_sleep,
248 start_signal,
249 error_handler,
250 );
251 tokens.extend(source_tokens);
252
253 let stream = match flow_control_probe {
254 Some(probe) => stream.probe_notify_with(vec![probe]),
255 None => stream,
256 };
257
258 stream.leave(outer)
259 });
260
261 let (stream, txns_tokens) = match metadata.txns_shard {
266 Some(txns_shard) => txns_progress::<SourceData, (), Timestamp, i64, _, TxnsCodecRow, _>(
267 stream,
268 &source_id.to_string(),
269 txns_ctx,
270 move || {
271 let (c, l) = (
272 Arc::clone(&persist_clients),
273 metadata.persist_location.clone(),
274 );
275 async move { c.open(l).await.expect("location is valid") }
276 },
277 txns_shard,
278 metadata.data_shard,
279 as_of
280 .expect("as_of is provided for table sources")
281 .into_option()
282 .expect("shard is not closed"),
283 until,
284 Arc::new(metadata.relation_desc),
285 Arc::new(UnitSchema),
286 ),
287 None => (stream, vec![]),
288 };
289 tokens.extend(txns_tokens);
290 let (ok_stream, err_stream) = stream.ok_err(|(d, t, r)| match d {
291 Ok(row) => Ok((row, t.0, r)),
292 Err(err) => Err((err, t.0, r)),
293 });
294 (ok_stream, err_stream, tokens)
295}
296
297type RefinedScope<'scope, T> = Scope<'scope, (T, Subtime)>;
298
299#[allow(clippy::needless_borrow)]
306pub fn persist_source_core<'g, 'outer, E>(
307 outer: Scope<'outer, mz_repr::Timestamp>,
308 scope: RefinedScope<'g, mz_repr::Timestamp>,
309 source_id: GlobalId,
310 persist_clients: Arc<PersistClientCache>,
311 metadata: CollectionMetadata,
312 read_schema: Option<RelationDesc>,
313 as_of: Option<Antichain<Timestamp>>,
314 snapshot_mode: SnapshotMode,
315 until: Antichain<Timestamp>,
316 map_filter_project: Option<&mut MfpPlan>,
317 flow_control: Option<FlowControl<'g, (mz_repr::Timestamp, Subtime)>>,
318 listen_sleep: Option<impl Fn() -> RetryParameters + Send + 'static>,
320 start_signal: impl Future<Output = ()> + Send + 'static,
321 error_handler: ErrorHandler,
322) -> (
323 Stream<
324 'g,
325 (mz_repr::Timestamp, Subtime),
326 Vec<(Result<Row, E>, (mz_repr::Timestamp, Subtime), Diff)>,
327 >,
328 Vec<PressOnDropButton>,
329)
330where
331 E: timely::ExchangeData + Ord + Clone + Debug + From<DataflowError> + From<EvalError>,
332{
333 let cfg = persist_clients.cfg().clone();
334 let name = source_id.to_string();
335 let filter_plan = map_filter_project.as_ref().map(|p| (*p).clone());
336
337 let read_desc = match read_schema {
339 Some(desc) => desc,
340 None => metadata.relation_desc,
341 };
342
343 let desc_transformer = match flow_control {
344 Some(flow_control) => Some(move |scope, descs, chosen_worker| {
345 let (stream, token) = backpressure(
346 scope,
347 &format!("backpressure({source_id})"),
348 descs,
349 flow_control,
350 chosen_worker,
351 None,
352 );
353 (stream, vec![token])
354 }),
355 None => None,
356 };
357
358 let metrics = Arc::clone(persist_clients.metrics());
359 let filter_name = name.clone();
360 let upper = until.as_option().cloned().unwrap_or(Timestamp::MAX);
364 let (fetched, token) = shard_source(
365 outer,
366 scope,
367 &name,
368 move || {
369 let (c, l) = (
370 Arc::clone(&persist_clients),
371 metadata.persist_location.clone(),
372 );
373 async move { c.open(l).await.unwrap() }
374 },
375 metadata.data_shard,
376 as_of,
377 snapshot_mode,
378 until.clone(),
379 desc_transformer,
380 Arc::new(read_desc.clone()),
381 Arc::new(UnitSchema),
382 move |stats, frontier| {
383 let Some(lower) = frontier.as_option().copied() else {
384 return FilterResult::Discard;
387 };
388
389 if lower > upper {
390 return FilterResult::Discard;
393 }
394
395 let time_range =
396 ResultSpec::value_between(Datum::MzTimestamp(lower), Datum::MzTimestamp(upper));
397 if let Some(plan) = &filter_plan {
398 let metrics = &metrics.pushdown.part_stats;
399 let stats = RelationPartStats::new(&filter_name, metrics, &read_desc, stats);
400 filter_result(&read_desc, time_range, stats, plan)
401 } else {
402 FilterResult::Keep
403 }
404 },
405 listen_sleep,
406 start_signal,
407 error_handler,
408 );
409 let rows = decode_and_mfp(cfg, fetched, &name, until, map_filter_project);
410 (rows, token)
411}
412
413fn filter_result(
414 relation_desc: &RelationDesc,
415 time_range: ResultSpec,
416 stats: RelationPartStats,
417 plan: &MfpPlan,
418) -> FilterResult {
419 let arena = RowArena::new();
420 let relation = ReprRelationType::from(relation_desc.typ());
421 let mut ranges = ColumnSpecs::new(&relation, &arena);
422 ranges.push_unmaterializable(UnmaterializableFunc::MzNow, time_range);
423
424 let may_error = stats.err_count().map_or(true, |count| count > 0);
425
426 for (pos, (idx, _, _)) in relation_desc.iter_all().enumerate() {
429 let result_spec = stats.col_stats(idx, &arena);
430 ranges.push_column(pos, result_spec);
431 }
432 let result = ranges.mfp_plan_filter(plan).range;
433 let may_error = may_error || result.may_fail();
434 let may_keep = result.may_contain(Datum::True);
435 let may_skip = result.may_contain(Datum::False) || result.may_contain(Datum::Null);
436 if relation_desc.len() == 0 && !may_error && !may_skip {
437 let Ok(mut key) = <RelationDesc as Schema<SourceData>>::encoder(relation_desc) else {
438 return FilterResult::Keep;
439 };
440 key.append(&SourceData(Ok(Row::default())));
441 let key = key.finish();
442 let Ok(mut val) = <UnitSchema as Schema<()>>::encoder(&UnitSchema) else {
443 return FilterResult::Keep;
444 };
445 val.append(&());
446 let val = val.finish();
447
448 FilterResult::ReplaceWith {
449 key: Arc::new(key),
450 val: Arc::new(val),
451 }
452 } else if may_error || may_keep {
453 FilterResult::Keep
454 } else {
455 FilterResult::Discard
456 }
457}
458
459pub fn decode_and_mfp<'scope, E>(
460 cfg: PersistConfig,
461 fetched: StreamVec<
462 'scope,
463 (mz_repr::Timestamp, Subtime),
464 FetchedBlob<SourceData, (), Timestamp, StorageDiff>,
465 >,
466 name: &str,
467 until: Antichain<Timestamp>,
468 mut map_filter_project: Option<&mut MfpPlan>,
469) -> StreamVec<
470 'scope,
471 (mz_repr::Timestamp, Subtime),
472 (Result<Row, E>, (mz_repr::Timestamp, Subtime), Diff),
473>
474where
475 E: timely::ExchangeData + Ord + Clone + Debug + From<DataflowError> + From<EvalError>,
476{
477 let scope = fetched.scope();
478 let mut builder = OperatorBuilder::new(
479 format!("persist_source::decode_and_mfp({})", name),
480 scope.clone(),
481 );
482 let operator_info = builder.operator_info();
483
484 let mut fetched_input = builder.new_input(fetched, Pipeline);
485 let (updates_output, updates_stream) = builder.new_output();
486 let mut updates_output = OutputBuilder::from(updates_output);
487
488 let mut datum_vec = mz_repr::DatumVec::new();
490 let mut row_builder = Row::default();
491
492 let map_filter_project = map_filter_project.as_mut().map(|mfp| mfp.take());
494
495 builder.build(move |_caps| {
496 let name = name.to_owned();
497 let activations = scope.activations();
499 let activator = Activator::new(operator_info.address, activations);
500 let mut pending_work = std::collections::VecDeque::new();
502 let panic_on_audit_failure = STATS_AUDIT_PANIC.handle(&cfg);
503
504 move |_frontier| {
505 fetched_input.for_each(|time, data| {
506 let capability = time.retain(0);
507 for fetched_blob in data.drain(..) {
508 pending_work.push_back(PendingWork {
509 panic_on_audit_failure: panic_on_audit_failure.get(),
510 capability: capability.clone(),
511 part: PendingPart::Unparsed(fetched_blob),
512 })
513 }
514 });
515
516 let yield_fuel = cfg.storage_source_decode_fuel();
519 let yield_fn = |_, work| work >= yield_fuel;
520
521 let mut work = 0;
522 let start_time = Instant::now();
523 let mut output = updates_output.activate();
524 while !pending_work.is_empty() && !yield_fn(start_time, work) {
525 let done = pending_work.front_mut().unwrap().do_work(
526 &mut work,
527 &name,
528 start_time,
529 yield_fn,
530 &until,
531 map_filter_project.as_ref(),
532 &mut datum_vec,
533 &mut row_builder,
534 &mut output,
535 );
536 if done {
537 pending_work.pop_front();
538 }
539 }
540 if !pending_work.is_empty() {
541 activator.activate();
542 }
543 }
544 });
545
546 updates_stream
547}
548
549struct PendingWork {
551 panic_on_audit_failure: bool,
553 capability: Capability<(mz_repr::Timestamp, Subtime)>,
555 part: PendingPart,
557}
558
559enum PendingPart {
560 Unparsed(FetchedBlob<SourceData, (), Timestamp, StorageDiff>),
561 Parsed {
562 part: ShardSourcePart<SourceData, (), Timestamp, StorageDiff>,
563 },
564}
565
566impl PendingPart {
567 fn part_mut(&mut self) -> &mut FetchedPart<SourceData, (), Timestamp, StorageDiff> {
574 match self {
575 PendingPart::Unparsed(x) => {
576 *self = PendingPart::Parsed { part: x.parse() };
577 self.part_mut()
579 }
580 PendingPart::Parsed { part } => &mut part.part,
581 }
582 }
583}
584
585impl PendingWork {
586 fn do_work<YFn, E>(
589 &mut self,
590 work: &mut usize,
591 name: &str,
592 start_time: Instant,
593 yield_fn: YFn,
594 until: &Antichain<Timestamp>,
595 map_filter_project: Option<&MfpPlan>,
596 datum_vec: &mut DatumVec,
597 row_builder: &mut Row,
598 output: &mut OutputBuilderSession<
599 '_,
600 (mz_repr::Timestamp, Subtime),
601 ConsolidatingContainerBuilder<
602 Vec<(Result<Row, E>, (mz_repr::Timestamp, Subtime), Diff)>,
603 >,
604 >,
605 ) -> bool
606 where
607 YFn: Fn(Instant, usize) -> bool,
608 E: timely::ExchangeData + Ord + Clone + Debug + From<DataflowError> + From<EvalError>,
609 {
610 let mut session = output.session_with_builder(&self.capability);
611 let fetched_part = self.part.part_mut();
612 let is_filter_pushdown_audit = fetched_part.is_filter_pushdown_audit();
613 let mut row_buf = None;
614 while let Some(((key, val), time, diff)) =
615 fetched_part.next_with_storage(&mut row_buf, &mut None)
616 {
617 if until.less_equal(&time) {
618 continue;
619 }
620 match (key, val) {
621 (SourceData(Ok(row)), ()) => {
622 if let Some(mfp) = map_filter_project {
623 *work += 1;
630 let arena = mz_repr::RowArena::new();
631 let mut datums_local = datum_vec.borrow_with(&row);
632 for result in mfp.evaluate(
633 &mut datums_local,
634 &arena,
635 time,
636 diff.into(),
637 |time| !until.less_equal(time),
638 row_builder,
639 ) {
640 if let Some(stats) = &is_filter_pushdown_audit {
644 sentry::with_scope(
648 |scope| {
649 scope
650 .set_tag("alert_id", "persist_pushdown_audit_violation")
651 },
652 || {
653 error!(
654 ?stats,
655 name,
656 mfp = ?redact(&mfp),
657 result = ?redact(&result),
658 "persist filter pushdown correctness violation!"
659 );
660 if self.panic_on_audit_failure {
661 panic!(
662 "persist filter pushdown correctness violation! {}",
663 name
664 );
665 }
666 },
667 );
668 }
669 match result {
670 Ok((row, time, diff)) => {
671 if !until.less_equal(&time) {
673 let mut emit_time = *self.capability.time();
674 emit_time.0 = time;
675 session.give((Ok(row), emit_time, diff));
676 *work += 1;
677 }
678 }
679 Err((err, time, diff)) => {
680 if !until.less_equal(&time) {
682 let mut emit_time = *self.capability.time();
683 emit_time.0 = time;
684 session.give((Err(err), emit_time, diff));
685 *work += 1;
686 }
687 }
688 }
689 }
690 drop(datums_local);
694 row_buf.replace(SourceData(Ok(row)));
695 } else {
696 let mut emit_time = *self.capability.time();
697 emit_time.0 = time;
698 session.give((Ok(row.clone()), emit_time, diff.into()));
700 row_buf.replace(SourceData(Ok(row)));
701 *work += 1;
702 }
703 }
704 (SourceData(Err(err)), ()) => {
705 if let Some(stats) = &is_filter_pushdown_audit {
711 sentry::with_scope(
712 |scope| scope.set_tag("alert_id", "persist_pushdown_audit_violation"),
713 || {
714 error!(
720 ?stats,
721 name,
722 err = ?redact(&err),
723 "persist filter pushdown correctness violation!"
724 );
725 if self.panic_on_audit_failure {
726 panic!(
727 "persist filter pushdown correctness violation! {}",
728 name
729 );
730 }
731 },
732 );
733 }
734 let mut emit_time = *self.capability.time();
735 emit_time.0 = time;
736 session.give((Err(E::from(err)), emit_time, diff.into()));
737 *work += 1;
738 }
739 }
740 if yield_fn(start_time, *work) {
741 return false;
742 }
743 }
744 true
745 }
746}
747
748pub trait Backpressureable: Clone + 'static {
750 fn byte_size(&self) -> usize;
752}
753
754impl<T: Clone + 'static> Backpressureable for (usize, ExchangeableBatchPart<T>) {
755 fn byte_size(&self) -> usize {
756 self.1.encoded_size_bytes()
757 }
758}
759
760#[derive(Debug)]
762pub struct FlowControl<'scope, T: timely::progress::Timestamp> {
763 pub progress_stream: StreamVec<'scope, T, Infallible>,
769 pub max_inflight_bytes: usize,
771 pub summary: T::Summary,
774
775 pub metrics: Option<BackpressureMetrics>,
777}
778
779pub fn backpressure<'scope, T, O>(
792 scope: Scope<'scope, (T, Subtime)>,
793 name: &str,
794 data: StreamVec<'scope, (T, Subtime), O>,
795 flow_control: FlowControl<'scope, (T, Subtime)>,
796 chosen_worker: usize,
797 probe: Option<UnboundedSender<(Antichain<(T, Subtime)>, usize, usize)>>,
799) -> (StreamVec<'scope, (T, Subtime), O>, PressOnDropButton)
800where
801 T: TimelyTimestamp + Lattice + Codec64 + TotalOrder,
802 O: Backpressureable + std::fmt::Debug,
803{
804 let worker_index = scope.index();
805
806 let (flow_control_stream, flow_control_max_bytes, metrics) = (
807 flow_control.progress_stream,
808 flow_control.max_inflight_bytes,
809 flow_control.metrics,
810 );
811
812 let (handle, summaried_flow) = scope.feedback(flow_control.summary.clone());
817 flow_control_stream.connect_loop(handle);
818
819 let mut builder = AsyncOperatorBuilder::new(
820 format!("persist_source_backpressure({})", name),
821 scope.clone(),
822 );
823 let (data_output, data_stream) = builder.new_output::<CapacityContainerBuilder<Vec<_>>>();
824
825 let mut data_input = builder.new_disconnected_input(data, Pipeline);
826 let mut flow_control_input = builder.new_disconnected_input(summaried_flow, Pipeline);
827
828 fn synthesize_frontiers<T: PartialOrder + Clone>(
830 mut frontier: Antichain<(T, Subtime)>,
831 mut time: (T, Subtime),
832 part_number: &mut u64,
833 ) -> (
834 (T, Subtime),
835 Antichain<(T, Subtime)>,
836 Antichain<(T, Subtime)>,
837 ) {
838 let mut next_frontier = frontier.clone();
839 time.1 = Subtime(*part_number);
840 frontier.insert(time.clone());
841 *part_number += 1;
842 let mut next_time = time.clone();
843 next_time.1 = Subtime(*part_number);
844 next_frontier.insert(next_time);
845 (time, frontier, next_frontier)
846 }
847
848 let data_input = async_stream::stream!({
851 let mut part_number = 0;
852 let mut parts: Vec<((T, Subtime), O)> = Vec::new();
853 loop {
854 match data_input.next().await {
855 None => {
856 let empty = Antichain::new();
857 parts.sort_by_key(|val| val.0.clone());
858 for (part_time, d) in parts.drain(..) {
859 let (part_time, frontier, next_frontier) = synthesize_frontiers(
860 empty.clone(),
861 part_time.clone(),
862 &mut part_number,
863 );
864 yield Either::Right((part_time, d, frontier, next_frontier))
865 }
866 break;
867 }
868 Some(Event::Data(time, data)) => {
869 for d in data {
870 parts.push((time.clone(), d));
871 }
872 }
873 Some(Event::Progress(prog)) => {
874 parts.sort_by_key(|val| val.0.clone());
875 for (part_time, d) in parts.extract_if(.., |p| !prog.less_equal(&p.0)) {
876 let (part_time, frontier, next_frontier) =
877 synthesize_frontiers(prog.clone(), part_time.clone(), &mut part_number);
878 yield Either::Right((part_time, d, frontier, next_frontier))
879 }
880 yield Either::Left(prog)
881 }
882 }
883 }
884 });
885 let shutdown_button = builder.build(move |caps| async move {
886 let mut cap_set = CapabilitySet::from_elem(caps.into_element());
888
889 let mut output_frontier = Antichain::from_elem(TimelyTimestamp::minimum());
891 let mut flow_control_frontier = Antichain::from_elem(TimelyTimestamp::minimum());
893
894 let mut inflight_parts = Vec::new();
896 let mut pending_parts = std::collections::VecDeque::new();
898
899 if worker_index != chosen_worker {
901 trace!(
902 "We are not the chosen worker ({}), exiting...",
903 chosen_worker
904 );
905 return;
906 }
907 tokio::pin!(data_input);
908 'emitting_parts: loop {
909 let inflight_bytes: usize = inflight_parts.iter().map(|(_, size)| size).sum();
912
913 if inflight_bytes < flow_control_max_bytes
921 || !PartialOrder::less_equal(&flow_control_frontier, &output_frontier)
922 {
923 let (time, part, next_frontier) =
924 if let Some((time, part, next_frontier)) = pending_parts.pop_front() {
925 (time, part, next_frontier)
926 } else {
927 match data_input.next().await {
928 Some(Either::Right((time, part, frontier, next_frontier))) => {
929 output_frontier = frontier;
934 cap_set.downgrade(output_frontier.iter());
935
936 if inflight_bytes >= flow_control_max_bytes
941 && !PartialOrder::less_than(
942 &output_frontier,
943 &flow_control_frontier,
944 )
945 {
946 pending_parts.push_back((time, part, next_frontier));
947 continue 'emitting_parts;
948 }
949 (time, part, next_frontier)
950 }
951 Some(Either::Left(prog)) => {
952 output_frontier = prog;
953 cap_set.downgrade(output_frontier.iter());
954 continue 'emitting_parts;
955 }
956 None => {
957 if pending_parts.is_empty() {
958 break 'emitting_parts;
959 } else {
960 continue 'emitting_parts;
961 }
962 }
963 }
964 };
965
966 let byte_size = part.byte_size();
967 if let Some(emission_ts) = flow_control.summary.results_in(&time) {
977 inflight_parts.push((emission_ts, byte_size));
978 }
979
980 data_output.give(&cap_set.delayed(&time), part);
983
984 if let Some(metrics) = &metrics {
985 metrics.emitted_bytes.inc_by(u64::cast_from(byte_size))
986 }
987
988 output_frontier = next_frontier;
989 cap_set.downgrade(output_frontier.iter())
990 } else {
991 if let Some(metrics) = &metrics {
992 metrics
993 .last_backpressured_bytes
994 .set(u64::cast_from(inflight_bytes))
995 }
996 let parts_count = inflight_parts.len();
997 let new_flow_control_frontier = match flow_control_input.next().await {
1002 Some(Event::Progress(frontier)) => frontier,
1003 Some(Event::Data(_, _)) => {
1004 unreachable!("flow_control_input should not contain data")
1005 }
1006 None => Antichain::new(),
1007 };
1008
1009 flow_control_frontier.clone_from(&new_flow_control_frontier);
1011
1012 let retired_parts = inflight_parts
1014 .extract_if(.., |(ts, _size)| !flow_control_frontier.less_equal(ts));
1015 let (retired_size, retired_count): (usize, usize) = retired_parts
1016 .fold((0, 0), |(accum_size, accum_count), (_ts, size)| {
1017 (accum_size + size, accum_count + 1)
1018 });
1019 trace!(
1020 "returning {} parts with {} bytes, frontier: {:?}",
1021 retired_count, retired_size, flow_control_frontier,
1022 );
1023
1024 if let Some(metrics) = &metrics {
1025 metrics.retired_bytes.inc_by(u64::cast_from(retired_size))
1026 }
1027
1028 if let Some(probe) = probe.as_ref() {
1030 let _ = probe.send((new_flow_control_frontier, parts_count, retired_count));
1031 }
1032 }
1033 }
1034 });
1035 (data_stream, shutdown_button.press_on_drop())
1036}
1037
1038#[cfg(test)]
1039mod tests {
1040 use timely::container::CapacityContainerBuilder;
1041 use timely::dataflow::operators::{Enter, Probe};
1042 use tokio::sync::mpsc::unbounded_channel;
1043 use tokio::sync::oneshot;
1044
1045 use super::*;
1046
1047 #[mz_ore::test]
1048 fn test_backpressure_non_granular() {
1049 use Step::*;
1050 backpressure_runner(
1051 vec![(50, Part(101)), (50, Part(102)), (100, Part(1))],
1052 100,
1053 (1, Subtime(0)),
1054 vec![
1055 AssertOutputFrontier((50, Subtime(2))),
1058 AssertBackpressured {
1059 frontier: (1, Subtime(0)),
1060 inflight_parts: 1,
1061 retired_parts: 0,
1062 },
1063 AssertBackpressured {
1064 frontier: (51, Subtime(0)),
1065 inflight_parts: 1,
1066 retired_parts: 0,
1067 },
1068 ProcessXParts(2),
1069 AssertBackpressured {
1070 frontier: (101, Subtime(0)),
1071 inflight_parts: 2,
1072 retired_parts: 2,
1073 },
1074 AssertOutputFrontier((100, Subtime(3))),
1077 ],
1078 true,
1079 );
1080
1081 backpressure_runner(
1082 vec![
1083 (50, Part(10)),
1084 (50, Part(10)),
1085 (51, Part(100)),
1086 (52, Part(1000)),
1087 ],
1088 50,
1089 (1, Subtime(0)),
1090 vec![
1091 AssertOutputFrontier((51, Subtime(3))),
1093 AssertBackpressured {
1094 frontier: (1, Subtime(0)),
1095 inflight_parts: 3,
1096 retired_parts: 0,
1097 },
1098 ProcessXParts(3),
1099 AssertBackpressured {
1100 frontier: (52, Subtime(0)),
1101 inflight_parts: 3,
1102 retired_parts: 2,
1103 },
1104 AssertBackpressured {
1105 frontier: (53, Subtime(0)),
1106 inflight_parts: 1,
1107 retired_parts: 1,
1108 },
1109 AssertOutputFrontier((52, Subtime(4))),
1112 ],
1113 true,
1114 );
1115
1116 backpressure_runner(
1117 vec![
1118 (50, Part(98)),
1119 (50, Part(1)),
1120 (51, Part(10)),
1121 (52, Part(100)),
1122 (52, Part(10)),
1124 (52, Part(10)),
1125 (52, Part(10)),
1126 (52, Part(100)),
1127 (100, Part(100)),
1129 ],
1130 100,
1131 (1, Subtime(0)),
1132 vec![
1133 AssertOutputFrontier((51, Subtime(3))),
1134 AssertBackpressured {
1138 frontier: (1, Subtime(0)),
1139 inflight_parts: 3,
1140 retired_parts: 0,
1141 },
1142 AssertBackpressured {
1143 frontier: (51, Subtime(0)),
1144 inflight_parts: 3,
1145 retired_parts: 0,
1146 },
1147 ProcessXParts(1),
1148 AssertOutputFrontier((51, Subtime(3))),
1151 ProcessXParts(1),
1155 AssertOutputFrontier((52, Subtime(4))),
1156 AssertBackpressured {
1157 frontier: (52, Subtime(0)),
1158 inflight_parts: 3,
1159 retired_parts: 2,
1160 },
1161 ProcessXParts(1),
1165 AssertBackpressured {
1169 frontier: (53, Subtime(0)),
1170 inflight_parts: 2,
1171 retired_parts: 1,
1172 },
1173 ProcessXParts(5),
1175 AssertBackpressured {
1176 frontier: (101, Subtime(0)),
1177 inflight_parts: 5,
1178 retired_parts: 5,
1179 },
1180 AssertOutputFrontier((100, Subtime(9))),
1181 ],
1182 true,
1183 );
1184 }
1185
1186 #[mz_ore::test]
1187 fn test_backpressure_granular() {
1188 use Step::*;
1189 backpressure_runner(
1190 vec![(50, Part(101)), (50, Part(101))],
1191 100,
1192 (0, Subtime(1)),
1193 vec![
1194 AssertOutputFrontier((50, Subtime(1))),
1196 AssertBackpressured {
1199 frontier: (0, Subtime(1)),
1200 inflight_parts: 1,
1201 retired_parts: 0,
1202 },
1203 AssertBackpressured {
1204 frontier: (50, Subtime(1)),
1205 inflight_parts: 1,
1206 retired_parts: 0,
1207 },
1208 ProcessXParts(1),
1210 AssertBackpressured {
1212 frontier: (50, Subtime(2)),
1213 inflight_parts: 1,
1214 retired_parts: 1,
1215 },
1216 AssertOutputFrontier((50, Subtime(2))),
1218 ],
1219 false,
1220 );
1221
1222 backpressure_runner(
1223 vec![
1224 (50, Part(10)),
1225 (50, Part(10)),
1226 (51, Part(35)),
1227 (52, Part(100)),
1228 ],
1229 50,
1230 (0, Subtime(1)),
1231 vec![
1232 AssertOutputFrontier((51, Subtime(3))),
1234 AssertBackpressured {
1235 frontier: (0, Subtime(1)),
1236 inflight_parts: 3,
1237 retired_parts: 0,
1238 },
1239 AssertBackpressured {
1240 frontier: (50, Subtime(1)),
1241 inflight_parts: 3,
1242 retired_parts: 0,
1243 },
1244 ProcessXParts(1),
1246 AssertBackpressured {
1247 frontier: (50, Subtime(2)),
1248 inflight_parts: 3,
1249 retired_parts: 1,
1250 },
1251 AssertOutputFrontier((52, Subtime(4))),
1254 ProcessXParts(2),
1255 AssertBackpressured {
1256 frontier: (52, Subtime(4)),
1257 inflight_parts: 3,
1258 retired_parts: 2,
1259 },
1260 ],
1261 false,
1262 );
1263 }
1264
1265 type Time = (u64, Subtime);
1266 #[derive(Clone, Debug)]
1267 struct Part(usize);
1268 impl Backpressureable for Part {
1269 fn byte_size(&self) -> usize {
1270 self.0
1271 }
1272 }
1273
1274 enum Step {
1276 AssertOutputFrontier(Time),
1279 AssertBackpressured {
1283 frontier: Time,
1284 inflight_parts: usize,
1285 retired_parts: usize,
1286 },
1287 ProcessXParts(usize),
1289 }
1290
1291 fn backpressure_runner(
1293 input: Vec<(u64, Part)>,
1295 max_inflight_bytes: usize,
1297 summary: Time,
1299 steps: Vec<Step>,
1301 non_granular_consumer: bool,
1304 ) {
1305 timely::execute::execute_directly(move |worker| {
1306 let (
1307 backpressure_probe,
1308 consumer_tx,
1309 mut backpressure_status_rx,
1310 finalizer_tx,
1311 _token,
1312 ) =
1313 worker.dataflow::<u64, _, _>(|outer_scope| {
1315 let (non_granular_feedback_handle, non_granular_feedback) =
1316 if non_granular_consumer {
1317 let (h, f) = outer_scope.feedback(Default::default());
1318 (Some(h), Some(f))
1319 } else {
1320 (None, None)
1321 };
1322 let (
1323 backpressure_probe,
1324 consumer_tx,
1325 backpressure_status_rx,
1326 token,
1327 backpressured,
1328 finalizer_tx,
1329 ) = outer_scope.scoped::<(u64, Subtime), _, _>("hybrid", |scope| {
1330 let (input, finalizer_tx) =
1331 iterator_operator(scope.clone(), input.into_iter());
1332
1333 let (flow_control, granular_feedback_handle) = if non_granular_consumer {
1334 (
1335 FlowControl {
1336 progress_stream: non_granular_feedback.unwrap().enter(scope),
1337 max_inflight_bytes,
1338 summary,
1339 metrics: None
1340 },
1341 None,
1342 )
1343 } else {
1344 let (granular_feedback_handle, granular_feedback) =
1345 scope.feedback(Default::default());
1346 (
1347 FlowControl {
1348 progress_stream: granular_feedback,
1349 max_inflight_bytes,
1350 summary,
1351 metrics: None,
1352 },
1353 Some(granular_feedback_handle),
1354 )
1355 };
1356
1357 let (backpressure_status_tx, backpressure_status_rx) = unbounded_channel();
1358
1359 let (backpressured, token) = backpressure(
1360 scope,
1361 "test",
1362 input,
1363 flow_control,
1364 0,
1365 Some(backpressure_status_tx),
1366 );
1367
1368 let tx = if !non_granular_consumer {
1370 Some(consumer_operator(
1371 scope.clone(),
1372 backpressured.clone(),
1373 granular_feedback_handle.unwrap(),
1374 ))
1375 } else {
1376 None
1377 };
1378
1379 let (probe_handle, backpressured) = backpressured.probe();
1380 (
1381 probe_handle,
1382 tx,
1383 backpressure_status_rx,
1384 token,
1385 backpressured.leave(outer_scope),
1386 finalizer_tx,
1387 )
1388 });
1389
1390 let consumer_tx = if non_granular_consumer {
1392 consumer_operator(
1393 outer_scope.clone(),
1394 backpressured,
1395 non_granular_feedback_handle.unwrap(),
1396 )
1397 } else {
1398 consumer_tx.unwrap()
1399 };
1400
1401 (
1402 backpressure_probe,
1403 consumer_tx,
1404 backpressure_status_rx,
1405 finalizer_tx,
1406 token,
1407 )
1408 });
1409
1410 use Step::*;
1411 for step in steps {
1412 match step {
1413 AssertOutputFrontier(time) => {
1414 eprintln!("checking advance to {time:?}");
1415 backpressure_probe.with_frontier(|front| {
1416 eprintln!("current backpressure output frontier: {front:?}");
1417 });
1418 while backpressure_probe.less_than(&time) {
1419 worker.step();
1420 backpressure_probe.with_frontier(|front| {
1421 eprintln!("current backpressure output frontier: {front:?}");
1422 });
1423 std::thread::sleep(std::time::Duration::from_millis(25));
1424 }
1425 }
1426 ProcessXParts(parts) => {
1427 eprintln!("processing {parts:?} parts");
1428 for _ in 0..parts {
1429 consumer_tx.send(()).unwrap();
1430 }
1431 }
1432 AssertBackpressured {
1433 frontier,
1434 inflight_parts,
1435 retired_parts,
1436 } => {
1437 let frontier = Antichain::from_elem(frontier);
1438 eprintln!(
1439 "asserting backpressured at {frontier:?}, with {inflight_parts:?} inflight parts \
1440 and {retired_parts:?} retired"
1441 );
1442 let (new_frontier, new_count, new_retired_count) = loop {
1443 if let Ok(val) = backpressure_status_rx.try_recv() {
1444 break val;
1445 }
1446 worker.step();
1447 std::thread::sleep(std::time::Duration::from_millis(25));
1448 };
1449 assert_eq!(
1450 (frontier, inflight_parts, retired_parts),
1451 (new_frontier, new_count, new_retired_count)
1452 );
1453 }
1454 }
1455 }
1456 let _ = finalizer_tx.send(());
1458 });
1459 }
1460
1461 fn iterator_operator<'scope, I: Iterator<Item = (u64, Part)> + 'static>(
1464 scope: Scope<'scope, (u64, Subtime)>,
1465 mut input: I,
1466 ) -> (StreamVec<'scope, (u64, Subtime), Part>, oneshot::Sender<()>) {
1467 let (finalizer_tx, finalizer_rx) = oneshot::channel();
1468 let mut iterator = AsyncOperatorBuilder::new("iterator".to_string(), scope);
1469 let (output_handle, output) = iterator.new_output::<CapacityContainerBuilder<Vec<Part>>>();
1470
1471 iterator.build(|mut caps| async move {
1472 let mut capability = Some(caps.pop().unwrap());
1473 let mut last = None;
1474 while let Some(element) = input.next() {
1475 let time = element.0.clone();
1476 let part = element.1;
1477 last = Some((time, Subtime(0)));
1478 output_handle.give(&capability.as_ref().unwrap().delayed(&last.unwrap()), part);
1479 }
1480 if let Some(last) = last {
1481 capability
1482 .as_mut()
1483 .unwrap()
1484 .downgrade(&(last.0 + 1, last.1));
1485 }
1486
1487 let _ = finalizer_rx.await;
1488 capability.take();
1489 });
1490
1491 (output, finalizer_tx)
1492 }
1493
1494 fn consumer_operator<
1498 'scope,
1499 T: timely::progress::Timestamp,
1500 O: Backpressureable + std::fmt::Debug,
1501 >(
1502 scope: Scope<'scope, T>,
1503 input: StreamVec<'scope, T, O>,
1504 feedback: timely::dataflow::operators::feedback::Handle<
1505 'scope,
1506 T,
1507 Vec<std::convert::Infallible>,
1508 >,
1509 ) -> UnboundedSender<()> {
1510 let (tx, mut rx) = unbounded_channel::<()>();
1511 let mut consumer = AsyncOperatorBuilder::new("consumer".to_string(), scope);
1512 let (output_handle, output) =
1513 consumer.new_output::<CapacityContainerBuilder<Vec<std::convert::Infallible>>>();
1514 let mut input = consumer.new_input_for(input, Pipeline, &output_handle);
1515
1516 consumer.build(|_caps| async move {
1517 while let Some(()) = rx.recv().await {
1518 while let Some(Event::Progress(_)) = input.next().await {}
1520 }
1521 });
1522 output.connect_loop(feedback);
1523
1524 tx
1525 }
1526
1527 mod filter_pushdown_audit {
1542 use itertools::Itertools;
1543 use mz_expr::func::variadic::{And, Or};
1544 use mz_expr::func::{
1545 AddFloat32, AddTimestampInterval, CastNumericToFloat32, CastNumericToMzTimestamp, Eq,
1546 Gt, Gte, IsNull, JsonbGetString, JsonbGetStringStringify, Lt, Lte, MulFloat32,
1547 MulFloat64, Not, RoundNumericBinary, TryParseMonotonicIso8601Timestamp,
1548 };
1549 use mz_expr::{BinaryFunc, MapFilterProject, MirScalarExpr, UnaryFunc};
1550 use mz_ore::metrics::MetricsRegistry;
1551 use mz_persist_types::part::PartBuilder;
1552 use mz_persist_types::stats::{PartStats, PartStatsMetrics};
1553 use mz_repr::adt::interval::Interval;
1554 use mz_repr::adt::numeric::Numeric;
1555 use mz_repr::{Diff, ReprScalarType, SqlScalarType};
1556 use proptest::prelude::*;
1557 use proptest::sample::{Index, select};
1558 use proptest::strategy::Union;
1559
1560 use super::*;
1561
1562 fn f32_lit(x: f32) -> MirScalarExpr {
1563 MirScalarExpr::literal_ok(Datum::from(x), ReprScalarType::Float32)
1564 }
1565
1566 fn f64_lit(x: f64) -> MirScalarExpr {
1567 MirScalarExpr::literal_ok(Datum::from(x), ReprScalarType::Float64)
1568 }
1569
1570 fn numeric_datum(x: f64) -> Datum<'static> {
1571 Datum::from(Numeric::from(x))
1572 }
1573
1574 fn numeric_desc() -> RelationDesc {
1575 RelationDesc::builder()
1576 .with_column(
1577 "c0",
1578 SqlScalarType::Numeric { max_scale: None }.nullable(false),
1579 )
1580 .finish()
1581 }
1582
1583 fn build_part_stats(desc: &RelationDesc, rows: &[SourceData]) -> PartStats {
1586 let mut builder = PartBuilder::new(desc, &UnitSchema);
1587 for row in rows {
1588 builder.push(row, &(), 1u64, 1i64);
1589 }
1590 let part = builder.finish();
1591 PartStats::new::<SourceData, RelationDesc>(&part, desc).expect("stats")
1592 }
1593
1594 fn mfp_yields_output(plan: &MfpPlan, rows: &[Row]) -> bool {
1598 let arena = RowArena::new();
1599 let mut row_builder = Row::default();
1600 for row in rows {
1601 let mut datums: Vec<Datum> = row.iter().collect();
1602 let mut results = plan.evaluate::<DataflowError, _>(
1603 &mut datums,
1604 &arena,
1605 Timestamp::MIN,
1606 Diff::from(1),
1607 |_| true,
1608 &mut row_builder,
1609 );
1610 if results.next().is_some() {
1611 return true;
1612 }
1613 }
1614 false
1615 }
1616
1617 fn comparison_funcs() -> impl Strategy<Value = BinaryFunc> {
1618 select(vec![
1619 BinaryFunc::Lte(Lte),
1620 BinaryFunc::Lt(Lt),
1621 BinaryFunc::Gte(Gte),
1622 BinaryFunc::Gt(Gt),
1623 BinaryFunc::Eq(Eq),
1624 ])
1625 }
1626
1627 fn f32_consts() -> impl Strategy<Value = f32> {
1628 select(vec![
1629 0.0f32,
1630 1.0,
1631 -1.0,
1632 0.0087531805,
1633 745213.56,
1634 76700000000.0,
1635 f32::MAX,
1636 1e30,
1637 -1e30,
1638 ])
1639 }
1640
1641 fn float_arith_predicate(
1645 scale: i32,
1646 a: f32,
1647 b: f32,
1648 c: f32,
1649 cmp: BinaryFunc,
1650 ) -> MirScalarExpr {
1651 let round = MirScalarExpr::CallBinary {
1652 func: BinaryFunc::RoundNumeric(RoundNumericBinary),
1653 expr1: Box::new(MirScalarExpr::column(0)),
1654 expr2: Box::new(MirScalarExpr::literal_ok(
1655 Datum::from(scale),
1656 ReprScalarType::Int32,
1657 )),
1658 };
1659 let cast = MirScalarExpr::CallUnary {
1660 func: UnaryFunc::CastNumericToFloat32(CastNumericToFloat32),
1661 expr: Box::new(round),
1662 };
1663 let add = MirScalarExpr::CallBinary {
1664 func: BinaryFunc::AddFloat32(AddFloat32),
1665 expr1: Box::new(f32_lit(a)),
1666 expr2: Box::new(cast),
1667 };
1668 let mul = MirScalarExpr::CallBinary {
1669 func: BinaryFunc::MulFloat32(MulFloat32),
1670 expr1: Box::new(add),
1671 expr2: Box::new(f32_lit(b)),
1672 };
1673 MirScalarExpr::CallBinary {
1674 func: cmp,
1675 expr1: Box::new(mul),
1676 expr2: Box::new(f32_lit(c)),
1677 }
1678 }
1679
1680 fn cast_mz_timestamp_predicate(ts: u64, cmp: BinaryFunc) -> MirScalarExpr {
1684 let cast = MirScalarExpr::CallUnary {
1685 func: UnaryFunc::CastNumericToMzTimestamp(CastNumericToMzTimestamp),
1686 expr: Box::new(MirScalarExpr::column(0)),
1687 };
1688 MirScalarExpr::CallBinary {
1689 func: cmp,
1690 expr1: Box::new(cast),
1691 expr2: Box::new(MirScalarExpr::literal_ok(
1692 Datum::MzTimestamp(Timestamp::from(ts)),
1693 ReprScalarType::MzTimestamp,
1694 )),
1695 }
1696 }
1697
1698 fn arb_numeric_rows() -> impl Strategy<Value = Vec<Row>> {
1699 let magnitudes = vec![
1700 0.0f64, 1.0, 2.0, 1.5, 2.5, 0.25, -1.0, 10.0, 100.0, 1e10, -1e10, 3.0e38, 3.4e38,
1701 3.5e38, 1e40, -1e40, 1e300,
1702 ];
1703 prop::collection::vec(
1704 select(magnitudes).prop_map(|x| Row::pack_slice(&[numeric_datum(x)])),
1705 1..8,
1706 )
1707 }
1708
1709 fn arb_predicate() -> impl Strategy<Value = MirScalarExpr> {
1710 let float_arith = (
1711 select(vec![0i32, 2, -5, 24699]),
1712 f32_consts(),
1713 f32_consts(),
1714 f32_consts(),
1715 comparison_funcs(),
1716 )
1717 .prop_map(|(scale, a, b, c, cmp)| float_arith_predicate(scale, a, b, c, cmp));
1718 let cast_ts = (select(vec![0u64, 1, 2, 100, u64::MAX]), comparison_funcs())
1719 .prop_map(|(ts, cmp)| cast_mz_timestamp_predicate(ts, cmp));
1720 proptest::strategy::Union::new(vec![float_arith.boxed(), cast_ts.boxed()])
1721 }
1722
1723 #[mz_ore::test]
1724 #[cfg_attr(miri, ignore)] fn filter_result_never_discards_matching_part() {
1726 fn check(rows: Vec<Row>, predicate: MirScalarExpr) -> Result<(), TestCaseError> {
1727 let desc = numeric_desc();
1728 let plan = MapFilterProject::new(1)
1729 .filter(std::iter::once(predicate))
1730 .into_plan()
1731 .expect("into_plan");
1732
1733 let source_rows: Vec<_> = rows.iter().map(|r| SourceData(Ok(r.clone()))).collect();
1734 let part_stats = build_part_stats(&desc, &source_rows);
1735 let metrics = PartStatsMetrics::new(&MetricsRegistry::new());
1736 let stats = RelationPartStats::new("test", &metrics, &desc, &part_stats);
1737
1738 let decision = filter_result(&desc, ResultSpec::anything(), stats, &plan);
1739
1740 if mfp_yields_output(&plan, &rows) {
1741 prop_assert!(
1742 !matches!(decision, FilterResult::Discard),
1743 "filter pushdown discarded a part whose MFP yields output on a real \
1744 row (wrongly-skipped part; the runtime audit would panic).\n\
1745 rows={rows:?}\nplan={plan:?}",
1746 );
1747 }
1748 Ok(())
1749 }
1750
1751 proptest!(|(rows in arb_numeric_rows(), predicate in arb_predicate())| {
1752 check(rows, predicate)?;
1753 });
1754 }
1755
1756 fn assert_part_kept(desc: &RelationDesc, rows: &[Row], predicate: MirScalarExpr) {
1760 let plan = MapFilterProject::new(1)
1761 .filter(std::iter::once(predicate))
1762 .into_plan()
1763 .expect("into_plan");
1764 assert!(
1765 mfp_yields_output(&plan, rows),
1766 "nothing to keep: the MFP yields no output on any of these rows.\n\
1767 rows={rows:?}\nplan={plan:?}",
1768 );
1769
1770 let source_rows: Vec<_> = rows.iter().map(|r| SourceData(Ok(r.clone()))).collect();
1771 let part_stats = build_part_stats(desc, &source_rows);
1772 let metrics = PartStatsMetrics::new(&MetricsRegistry::new());
1773 let stats = RelationPartStats::new("test", &metrics, desc, &part_stats);
1774 let decision = filter_result(desc, ResultSpec::anything(), stats, &plan);
1775 assert!(
1776 !matches!(decision, FilterResult::Discard),
1777 "filter pushdown discarded a part whose MFP yields output on a real row.\n\
1778 rows={rows:?}\nplan={plan:?}",
1779 );
1780 }
1781
1782 #[mz_ore::test]
1793 #[cfg_attr(miri, ignore)] fn negative_nan_does_not_discard_matching_part() {
1795 let desc = RelationDesc::builder()
1798 .with_column("c0", SqlScalarType::Float32.nullable(false))
1799 .finish();
1800 let rows = [
1801 Row::pack_slice(&[Datum::from(-f32::NAN)]),
1802 Row::pack_slice(&[Datum::from(0.0f32)]),
1803 ];
1804 assert_part_kept(
1805 &desc,
1806 &rows,
1807 MirScalarExpr::CallBinary {
1808 func: BinaryFunc::Lt(Lt),
1809 expr1: Box::new(MirScalarExpr::column(0)),
1810 expr2: Box::new(f32_lit(1.0)),
1811 },
1812 );
1813
1814 let desc = RelationDesc::builder()
1815 .with_column("c0", SqlScalarType::Float64.nullable(false))
1816 .finish();
1817 let rows = [
1818 Row::pack_slice(&[Datum::from(-f64::NAN)]),
1819 Row::pack_slice(&[Datum::from(0.0f64)]),
1820 ];
1821 assert_part_kept(
1822 &desc,
1823 &rows,
1824 MirScalarExpr::CallBinary {
1825 func: BinaryFunc::Lt(Lt),
1826 expr1: Box::new(MirScalarExpr::column(0)),
1827 expr2: Box::new(f64_lit(1.0)),
1828 },
1829 );
1830 }
1831
1832 #[mz_ore::test]
1843 #[cfg_attr(miri, ignore)] fn mixed_sign_nans_do_not_discard_matching_part() {
1845 let desc = RelationDesc::builder()
1846 .with_column("c0", SqlScalarType::Float32.nullable(false))
1847 .finish();
1848 let rows = [
1849 Row::pack_slice(&[Datum::from(-f32::NAN)]),
1850 Row::pack_slice(&[Datum::from(f32::NAN)]),
1851 Row::pack_slice(&[Datum::from(0.0f32)]),
1852 ];
1853 assert_part_kept(
1854 &desc,
1855 &rows,
1856 MirScalarExpr::CallBinary {
1857 func: BinaryFunc::Eq(Eq),
1858 expr1: Box::new(MirScalarExpr::column(0)),
1859 expr2: Box::new(f32_lit(0.0)),
1860 },
1861 );
1862
1863 let desc = RelationDesc::builder()
1864 .with_column("c0", SqlScalarType::Float64.nullable(false))
1865 .finish();
1866 let rows = [
1867 Row::pack_slice(&[Datum::from(-f64::NAN)]),
1868 Row::pack_slice(&[Datum::from(f64::NAN)]),
1869 Row::pack_slice(&[Datum::from(0.0f64)]),
1870 ];
1871 assert_part_kept(
1872 &desc,
1873 &rows,
1874 MirScalarExpr::CallBinary {
1875 func: BinaryFunc::Eq(Eq),
1876 expr1: Box::new(MirScalarExpr::column(0)),
1877 expr2: Box::new(f64_lit(0.0)),
1878 },
1879 );
1880 }
1881
1882 const NUM: usize = 0;
1891 const F32: usize = 1;
1892 const F64: usize = 2;
1893 const STR: usize = 3;
1894 const J1: usize = 4;
1895 const J2: usize = 5;
1896 const BOOL: usize = 6;
1897 const TS: usize = 7;
1898 const MZTS: usize = 8;
1899 const WIDE_ARITY: usize = 9;
1900
1901 fn wide_scalar_type(col: usize) -> SqlScalarType {
1902 match col {
1903 NUM => SqlScalarType::Numeric { max_scale: None },
1904 F32 => SqlScalarType::Float32,
1905 F64 => SqlScalarType::Float64,
1906 STR => SqlScalarType::String,
1907 J1 | J2 => SqlScalarType::Jsonb,
1908 BOOL => SqlScalarType::Bool,
1909 TS => SqlScalarType::Timestamp { precision: None },
1910 MZTS => SqlScalarType::MzTimestamp,
1911 _ => unreachable!("no such column"),
1912 }
1913 }
1914
1915 fn wide_repr_type(col: usize) -> ReprScalarType {
1916 match col {
1917 NUM => ReprScalarType::Numeric,
1918 F32 => ReprScalarType::Float32,
1919 F64 => ReprScalarType::Float64,
1920 STR => ReprScalarType::String,
1921 J1 | J2 => ReprScalarType::Jsonb,
1922 BOOL => ReprScalarType::Bool,
1923 TS => ReprScalarType::Timestamp,
1924 MZTS => ReprScalarType::MzTimestamp,
1925 _ => unreachable!("no such column"),
1926 }
1927 }
1928
1929 fn wide_desc() -> RelationDesc {
1930 let mut builder = RelationDesc::builder();
1931 for col in 0..WIDE_ARITY {
1932 let nullable = col != BOOL;
1936 builder = builder
1937 .with_column(format!("c{col}"), wide_scalar_type(col).nullable(nullable));
1938 }
1939 builder.finish()
1940 }
1941
1942 fn wide_pool(col: usize) -> Vec<Datum<'static>> {
1943 let mut pool: Vec<_> = wide_scalar_type(col).interesting_datums().collect();
1944 if col != BOOL {
1945 pool.push(Datum::Null);
1946 }
1947 pool
1948 }
1949
1950 fn arb_wide_rows() -> impl Strategy<Value = Vec<SourceData>> {
1951 let pools: Vec<Vec<Datum<'static>>> = (0..WIDE_ARITY).map(wide_pool).collect();
1952 let ok_row = prop::collection::vec(any::<Index>(), WIDE_ARITY).prop_map(move |picks| {
1953 let datums = picks
1954 .iter()
1955 .zip_eq(&pools)
1956 .map(|(pick, pool)| pool[pick.index(pool.len())]);
1957 SourceData(Ok(Row::pack(datums)))
1958 });
1959 let err_row = Just(SourceData(Err(DataflowError::from(
1960 EvalError::DivisionByZero,
1961 ))));
1962 let row = Union::new_weighted(vec![(9, ok_row.boxed()), (1, err_row.boxed())]);
1963 prop::collection::vec(row, 2..8)
1964 }
1965
1966 fn lit(datum: Datum<'static>, typ: ReprScalarType) -> MirScalarExpr {
1967 if datum.is_null() {
1968 MirScalarExpr::literal_null(typ)
1969 } else {
1970 MirScalarExpr::literal_ok(datum, typ)
1971 }
1972 }
1973
1974 fn is_null(expr: MirScalarExpr) -> MirScalarExpr {
1975 MirScalarExpr::CallUnary {
1976 func: UnaryFunc::IsNull(IsNull),
1977 expr: Box::new(expr),
1978 }
1979 }
1980
1981 fn not(expr: MirScalarExpr) -> MirScalarExpr {
1982 MirScalarExpr::CallUnary {
1983 func: UnaryFunc::Not(Not),
1984 expr: Box::new(expr),
1985 }
1986 }
1987
1988 fn binary(func: BinaryFunc, a: MirScalarExpr, b: MirScalarExpr) -> MirScalarExpr {
1989 MirScalarExpr::CallBinary {
1990 func,
1991 expr1: Box::new(a),
1992 expr2: Box::new(b),
1993 }
1994 }
1995
1996 fn arb_cmp_col_lit() -> impl Strategy<Value = MirScalarExpr> {
1999 (0..WIDE_ARITY, any::<Index>(), comparison_funcs()).prop_map(|(col, pick, cmp)| {
2000 let pool = wide_pool(col);
2001 let datum = pool[pick.index(pool.len())];
2002 binary(
2003 cmp,
2004 MirScalarExpr::column(col),
2005 lit(datum, wide_repr_type(col)),
2006 )
2007 })
2008 }
2009
2010 fn arb_is_null_pred() -> impl Strategy<Value = MirScalarExpr> {
2011 (0..WIDE_ARITY, any::<bool>()).prop_map(|(col, negate)| {
2012 let expr = is_null(MirScalarExpr::column(col));
2013 if negate { not(expr) } else { expr }
2014 })
2015 }
2016
2017 fn jsonb_keys() -> impl Strategy<Value = &'static str> {
2018 select(vec!["x", "y", "nested", "absent"])
2019 }
2020
2021 fn jsonb_get(expr: MirScalarExpr, key: &'static str, stringify: bool) -> MirScalarExpr {
2022 let func = if stringify {
2023 BinaryFunc::JsonbGetStringStringify(JsonbGetStringStringify)
2024 } else {
2025 BinaryFunc::JsonbGetString(JsonbGetString)
2026 };
2027 binary(
2028 func,
2029 expr,
2030 MirScalarExpr::literal_ok(Datum::String(key), ReprScalarType::String),
2031 )
2032 }
2033
2034 fn arb_jsonb_pred() -> impl Strategy<Value = MirScalarExpr> {
2037 (
2038 select(vec![J1, J2]),
2039 jsonb_keys(),
2040 any::<bool>(),
2041 any::<bool>(),
2042 )
2043 .prop_map(|(col, key, stringify, wrap_eq)| {
2044 let get = jsonb_get(MirScalarExpr::column(col), key, stringify);
2045 if wrap_eq {
2046 let typ = if stringify {
2047 ReprScalarType::String
2048 } else {
2049 ReprScalarType::Jsonb
2050 };
2051 binary(BinaryFunc::Eq(Eq), get, lit(Datum::String("a"), typ))
2052 } else {
2053 is_null(get)
2054 }
2055 })
2056 }
2057
2058 fn arb_case_jsonb_pred() -> impl Strategy<Value = MirScalarExpr> {
2061 (any::<bool>(), jsonb_keys(), any::<bool>()).prop_map(
2062 |(cond_is_col, key, stringify)| {
2063 let cond = if cond_is_col {
2064 MirScalarExpr::column(BOOL)
2065 } else {
2066 is_null(MirScalarExpr::column(STR))
2067 };
2068 let case = MirScalarExpr::If {
2069 cond: Box::new(cond),
2070 then: Box::new(MirScalarExpr::column(J1)),
2071 els: Box::new(MirScalarExpr::column(J2)),
2072 };
2073 is_null(jsonb_get(case, key, stringify))
2074 },
2075 )
2076 }
2077
2078 fn arb_iso_parse_pred() -> impl Strategy<Value = MirScalarExpr> {
2081 (comparison_funcs(), any::<Index>(), any::<bool>()).prop_map(
2082 |(cmp, pick, wrap_null)| {
2083 let parse = MirScalarExpr::CallUnary {
2084 func: UnaryFunc::TryParseMonotonicIso8601Timestamp(
2085 TryParseMonotonicIso8601Timestamp,
2086 ),
2087 expr: Box::new(MirScalarExpr::column(STR)),
2088 };
2089 if wrap_null {
2090 is_null(parse)
2091 } else {
2092 let pool: Vec<_> = SqlScalarType::Timestamp { precision: None }
2093 .interesting_datums()
2094 .collect();
2095 let datum = pool[pick.index(pool.len())];
2096 binary(cmp, parse, lit(datum, ReprScalarType::Timestamp))
2097 }
2098 },
2099 )
2100 }
2101
2102 fn arb_ts_interval_pred() -> impl Strategy<Value = MirScalarExpr> {
2106 let intervals = select(vec![
2107 Interval::new(0, 2, 0),
2108 Interval::new(0, 0, 3_600_000_000),
2109 Interval::new(1, 0, 0),
2110 Interval::new(-1, 0, 0),
2111 ]);
2112 (comparison_funcs(), intervals, any::<Index>()).prop_map(|(cmp, iv, pick)| {
2113 let add = binary(
2114 BinaryFunc::AddTimestampInterval(AddTimestampInterval),
2115 MirScalarExpr::column(TS),
2116 lit(Datum::Interval(iv), ReprScalarType::Interval),
2117 );
2118 let pool: Vec<_> = SqlScalarType::Timestamp { precision: None }
2119 .interesting_datums()
2120 .collect();
2121 let datum = pool[pick.index(pool.len())];
2122 binary(cmp, add, lit(datum, ReprScalarType::Timestamp))
2123 })
2124 }
2125
2126 fn arb_float_mul_pred() -> impl Strategy<Value = MirScalarExpr> {
2130 let consts = || select(vec![0.0f64, 1.0, -1.0, 1e300, -1e300, f64::INFINITY]);
2131 (comparison_funcs(), consts(), consts()).prop_map(|(cmp, a, c)| {
2132 let mul = binary(
2133 BinaryFunc::MulFloat64(MulFloat64),
2134 MirScalarExpr::column(F64),
2135 lit(Datum::from(a), ReprScalarType::Float64),
2136 );
2137 binary(cmp, mul, lit(Datum::from(c), ReprScalarType::Float64))
2138 })
2139 }
2140
2141 fn arb_temporal_pred() -> impl Strategy<Value = MirScalarExpr> {
2145 let cmps = select(vec![
2146 BinaryFunc::Lte(Lte),
2147 BinaryFunc::Lt(Lt),
2148 BinaryFunc::Gte(Gte),
2149 BinaryFunc::Gt(Gt),
2150 ]);
2151 (cmps, any::<bool>(), any::<Index>()).prop_map(|(cmp, use_col, pick)| {
2152 let mz_now = MirScalarExpr::CallUnmaterializable(UnmaterializableFunc::MzNow);
2153 let rhs = if use_col {
2154 MirScalarExpr::column(MZTS)
2155 } else {
2156 let pool = wide_pool(MZTS);
2157 lit(pool[pick.index(pool.len())], ReprScalarType::MzTimestamp)
2158 };
2159 binary(cmp, mz_now, rhs)
2160 })
2161 }
2162
2163 fn arb_wide_predicate() -> impl Strategy<Value = MirScalarExpr> {
2164 let leaf = Union::new(vec![
2165 arb_cmp_col_lit().boxed(),
2166 arb_is_null_pred().boxed(),
2167 arb_jsonb_pred().boxed(),
2168 arb_case_jsonb_pred().boxed(),
2169 arb_iso_parse_pred().boxed(),
2170 arb_ts_interval_pred().boxed(),
2171 arb_float_mul_pred().boxed(),
2172 arb_temporal_pred().boxed(),
2173 (
2176 select(vec![0i32, 2, -5, 24699]),
2177 f32_consts(),
2178 f32_consts(),
2179 f32_consts(),
2180 comparison_funcs(),
2181 )
2182 .prop_map(|(s, a, b, c, cmp)| float_arith_predicate(s, a, b, c, cmp))
2183 .boxed(),
2184 (select(vec![0u64, 1, 2, 100, u64::MAX]), comparison_funcs())
2185 .prop_map(|(ts, cmp)| cast_mz_timestamp_predicate(ts, cmp))
2186 .boxed(),
2187 ])
2188 .boxed();
2189 Union::new_weighted(vec![
2190 (3, leaf.clone()),
2191 (
2192 1,
2193 (leaf.clone(), leaf.clone(), any::<bool>())
2194 .prop_map(|(a, b, is_and)| {
2195 let func = if is_and { And.into() } else { Or.into() };
2196 MirScalarExpr::CallVariadic {
2197 func,
2198 exprs: vec![a, b],
2199 }
2200 })
2201 .boxed(),
2202 ),
2203 (1, leaf.prop_map(not).boxed()),
2204 ])
2205 }
2206
2207 #[mz_ore::test]
2213 #[cfg_attr(miri, ignore)] fn zero_column_relation_replace_with() {
2215 let desc = RelationDesc::empty();
2216 let metrics = PartStatsMetrics::new(&MetricsRegistry::new());
2217 let ok_rows = vec![
2218 SourceData(Ok(Row::default())),
2219 SourceData(Ok(Row::default())),
2220 ];
2221
2222 let plan = MapFilterProject::new(0).into_plan().expect("into_plan");
2225 let part_stats = build_part_stats(&desc, &ok_rows);
2226 let stats = RelationPartStats::new("test", &metrics, &desc, &part_stats);
2227 let decision = filter_result(&desc, ResultSpec::anything(), stats, &plan);
2228 assert!(
2229 matches!(decision, FilterResult::ReplaceWith { .. }),
2230 "expected ReplaceWith, got {decision:?}",
2231 );
2232
2233 let mixed_rows = vec![
2236 SourceData(Ok(Row::default())),
2237 SourceData(Err(DataflowError::from(EvalError::DivisionByZero))),
2238 ];
2239 let part_stats = build_part_stats(&desc, &mixed_rows);
2240 let stats = RelationPartStats::new("test", &metrics, &desc, &part_stats);
2241 let decision = filter_result(&desc, ResultSpec::anything(), stats, &plan);
2242 assert!(
2243 matches!(decision, FilterResult::Keep),
2244 "expected Keep, got {decision:?}",
2245 );
2246
2247 let plan = MapFilterProject::new(0)
2249 .filter(std::iter::once(MirScalarExpr::literal_ok(
2250 Datum::False,
2251 ReprScalarType::Bool,
2252 )))
2253 .into_plan()
2254 .expect("into_plan");
2255 let part_stats = build_part_stats(&desc, &ok_rows);
2256 let stats = RelationPartStats::new("test", &metrics, &desc, &part_stats);
2257 let decision = filter_result(&desc, ResultSpec::anything(), stats, &plan);
2258 assert!(
2259 matches!(decision, FilterResult::Discard),
2260 "expected Discard, got {decision:?}",
2261 );
2262 }
2263
2264 #[mz_ore::test]
2272 #[cfg_attr(miri, ignore)] fn schema_drift_degrades_to_no_stats() {
2274 let metrics = PartStatsMetrics::new(&MetricsRegistry::new());
2275
2276 let write_desc = RelationDesc::builder()
2278 .with_column("a", SqlScalarType::Int32.nullable(false))
2279 .finish();
2280 let rows = vec![SourceData(Ok(Row::pack_slice(&[Datum::Int32(1)])))];
2281 let part_stats = build_part_stats(&write_desc, &rows);
2282
2283 let read_desc = RelationDesc::builder()
2284 .with_column("a", SqlScalarType::Int32.nullable(false))
2285 .with_column("b", SqlScalarType::Float64.nullable(true))
2286 .finish();
2287 let stats = RelationPartStats::new("test", &metrics, &read_desc, &part_stats);
2288 let plan = MapFilterProject::new(2)
2291 .filter(std::iter::once(is_null(MirScalarExpr::column(1))))
2292 .into_plan()
2293 .expect("into_plan");
2294 let decision = filter_result(&read_desc, ResultSpec::anything(), stats, &plan);
2295 assert!(
2296 !matches!(decision, FilterResult::Discard),
2297 "part written before ADD COLUMN was discarded: {decision:?}",
2298 );
2299
2300 let write_desc = RelationDesc::builder()
2304 .with_column("a", SqlScalarType::Int32.nullable(false))
2305 .with_column("b", SqlScalarType::Float64.nullable(true))
2306 .finish();
2307 let rows = vec![SourceData(Ok(Row::pack_slice(&[
2308 Datum::Int32(1),
2309 Datum::from(5.0f64),
2310 ])))];
2311 let part_stats = build_part_stats(&write_desc, &rows);
2312
2313 let read_desc = RelationDesc::builder()
2314 .with_column("b", SqlScalarType::Float64.nullable(true))
2315 .finish();
2316 let stats = RelationPartStats::new("test", &metrics, &read_desc, &part_stats);
2317 let plan = MapFilterProject::new(1)
2318 .filter(std::iter::once(binary(
2319 BinaryFunc::Eq(Eq),
2320 MirScalarExpr::column(0),
2321 lit(Datum::from(5.0f64), ReprScalarType::Float64),
2322 )))
2323 .into_plan()
2324 .expect("into_plan");
2325 let decision = filter_result(&read_desc, ResultSpec::anything(), stats, &plan);
2326 assert!(
2327 !matches!(decision, FilterResult::Discard),
2328 "projected read desc discarded a matching part: {decision:?}",
2329 );
2330 }
2331
2332 fn part_yields_output(
2339 plan: &MfpPlan,
2340 rows: &[SourceData],
2341 eval_time: Timestamp,
2342 until: &Antichain<Timestamp>,
2343 ) -> bool {
2344 if until.less_equal(&eval_time) {
2345 return false;
2346 }
2347 let arena = RowArena::new();
2348 let mut row_builder = Row::default();
2349 for source_data in rows {
2350 match &source_data.0 {
2351 Err(_) => return true,
2352 Ok(row) => {
2353 let mut datums: Vec<Datum> = row.iter().collect();
2354 let mut results = plan.evaluate::<DataflowError, _>(
2355 &mut datums,
2356 &arena,
2357 eval_time,
2358 Diff::from(1),
2359 |time| !until.less_equal(time),
2360 &mut row_builder,
2361 );
2362 if results.next().is_some() {
2363 return true;
2364 }
2365 }
2366 }
2367 }
2368 false
2369 }
2370
2371 #[mz_ore::test]
2372 #[cfg_attr(miri, ignore)] fn wide_filter_result_never_discards_matching_part() {
2374 fn check(
2375 rows: Vec<SourceData>,
2376 predicate: MirScalarExpr,
2377 eval_time: u64,
2378 until: Option<u64>,
2379 ) -> Result<(), TestCaseError> {
2380 let desc = wide_desc();
2381 let Ok(plan) = MapFilterProject::new(desc.arity())
2385 .filter(std::iter::once(predicate))
2386 .into_plan()
2387 else {
2388 return Ok(());
2389 };
2390 let eval_time = Timestamp::from(eval_time);
2391 let until =
2392 until.map_or_else(Antichain::new, |t| Antichain::from_elem(Timestamp::from(t)));
2393
2394 let part_stats = build_part_stats(&desc, &rows);
2395 let metrics = PartStatsMetrics::new(&MetricsRegistry::new());
2396 let stats = RelationPartStats::new("test", &metrics, &desc, &part_stats);
2397
2398 let upper = until.as_option().copied().unwrap_or(Timestamp::MAX);
2403 if eval_time > upper {
2404 return Ok(());
2405 }
2406 let time_range = ResultSpec::value_between(
2407 Datum::MzTimestamp(eval_time),
2408 Datum::MzTimestamp(upper),
2409 );
2410 let decision = filter_result(&desc, time_range, stats, &plan);
2411
2412 if part_yields_output(&plan, &rows, eval_time, &until) {
2413 prop_assert!(
2414 !matches!(decision, FilterResult::Discard),
2415 "filter pushdown discarded a part whose MFP yields output on a real \
2416 row (wrongly-skipped part; the runtime audit would panic).\n\
2417 rows={rows:?}\nplan={plan:?}\neval_time={eval_time}\nuntil={until:?}",
2418 );
2419 }
2420 Ok(())
2421 }
2422
2423 let default = ProptestConfig::default();
2430 let cases = if std::env::var_os("PROPTEST_CASES").is_some() {
2431 default.cases
2432 } else {
2433 4096
2434 };
2435 let config = ProptestConfig { cases, ..default };
2436 proptest!(config, |(
2437 rows in arb_wide_rows(),
2438 predicate in arb_wide_predicate(),
2439 eval_time in select(vec![1u64, 5]),
2440 until in select(vec![None, Some(1u64), Some(5), Some(8), Some(100)]),
2441 )| {
2442 check(rows, predicate, eval_time, until)?;
2443 });
2444 }
2445
2446 #[mz_ore::test]
2452 #[cfg_attr(miri, ignore)] fn multi_part_decisions_are_independent() {
2454 fn check(
2455 parts: Vec<Vec<SourceData>>,
2456 predicate: MirScalarExpr,
2457 eval_time: u64,
2458 until: Option<u64>,
2459 ) -> Result<(), TestCaseError> {
2460 let desc = wide_desc();
2461 let Ok(plan) = MapFilterProject::new(desc.arity())
2462 .filter(std::iter::once(predicate))
2463 .into_plan()
2464 else {
2465 return Ok(());
2466 };
2467 let eval_time = Timestamp::from(eval_time);
2468 let until =
2469 until.map_or_else(Antichain::new, |t| Antichain::from_elem(Timestamp::from(t)));
2470 let upper = until.as_option().copied().unwrap_or(Timestamp::MAX);
2471 if eval_time > upper {
2472 return Ok(());
2473 }
2474 let metrics = PartStatsMetrics::new(&MetricsRegistry::new());
2475
2476 for rows in &parts {
2477 let part_stats = build_part_stats(&desc, rows);
2478 let stats = RelationPartStats::new("test", &metrics, &desc, &part_stats);
2479 let time_range = ResultSpec::value_between(
2480 Datum::MzTimestamp(eval_time),
2481 Datum::MzTimestamp(upper),
2482 );
2483 let decision = filter_result(&desc, time_range, stats, &plan);
2484 if part_yields_output(&plan, rows, eval_time, &until) {
2485 prop_assert!(
2486 !matches!(decision, FilterResult::Discard),
2487 "filter pushdown discarded a part whose MFP yields output on a \
2488 real row.\nrows={rows:?}\nplan={plan:?}\neval_time={eval_time}\n\
2489 until={until:?}",
2490 );
2491 }
2492 }
2493 Ok(())
2494 }
2495
2496 let default = ProptestConfig::default();
2497 let cases = if std::env::var_os("PROPTEST_CASES").is_some() {
2498 default.cases
2499 } else {
2500 1024
2501 };
2502 let config = ProptestConfig { cases, ..default };
2503 proptest!(config, |(
2504 parts in prop::collection::vec(arb_wide_rows(), 2..4),
2505 predicate in arb_wide_predicate(),
2506 eval_time in select(vec![1u64, 5]),
2507 until in select(vec![None, Some(5u64), Some(100)]),
2508 )| {
2509 check(parts, predicate, eval_time, until)?;
2510 });
2511 }
2512 }
2513}