Skip to main content

mz_storage_operators/
persist_source.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//! A source that reads from an a persist shard.
11
12use 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/// This opaque token represents progress within a timestamp, allowing finer-grained frontier
70/// progress than would otherwise be possible.
71///
72/// This is "opaque" since we'd like to reserve the right to change the definition in the future
73/// without downstreams being able to rely on the precise representation. (At the moment, this
74/// is a simple batch counter, though we may change it to eg. reflect progress through the keyspace
75/// in the future.)
76#[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// Derive ordering on the generated `SubtimeReference` too: the paged merge
91// batcher sorts the `(Timestamp, Subtime)` time and so requires `Ref: Ord`.
92#[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    /// The smallest non-zero summary for the opaque timestamp type.
142    pub const fn least_summary() -> Self {
143        Subtime(1)
144    }
145}
146
147/// Creates a new source that reads from a persist shard, distributing the work
148/// of reading data to all timely workers.
149///
150/// All times emitted will have been [advanced by] the given `as_of` frontier.
151/// All updates at times greater or equal to `until` will be suppressed.
152/// The `map_filter_project` argument, if supplied, may be partially applied,
153/// and any un-applied part of the argument will be left behind in the argument.
154///
155/// Users of this function have the ability to apply flow control to the output
156/// to limit the in-flight data (measured in bytes) it can emit. The flow control
157/// input is a timely stream that communicates the frontier at which the data
158/// emitted from by this source have been dropped.
159///
160/// **Note:** Because this function is reading batches from `persist`, it is working
161/// at batch granularity. In practice, the source will be overshooting the target
162/// flow control upper by an amount that is related to the size of batches.
163///
164/// If no flow control is desired an empty stream whose frontier immediately advances
165/// to the empty antichain can be used. An easy easy of creating such stream is by
166/// using [`timely::dataflow::operators::generic::operator::empty`].
167///
168/// [advanced by]: differential_dataflow::lattice::Lattice::advance_by
169pub 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        // Our default listen sleeps are tuned for the case of a shard that is
225        // written once a second, but txn-wal allows these to be lazy.
226        // Override the tuning to reduce crdb load. The pubsub fallback
227        // responsibility is then replaced by manual "one state" wakeups in the
228        // txns_progress operator.
229        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    // If a txns_shard was provided, then this shard is in the txn-wal
262    // system. This means the "logical" upper may be ahead of the "physical"
263    // upper. Render a dataflow operator that passes through the input and
264    // translates the progress frontiers as necessary.
265    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/// Creates a new source that reads from a persist shard, distributing the work
300/// of reading data to all timely workers.
301///
302/// All times emitted will have been [advanced by] the given `as_of` frontier.
303///
304/// [advanced by]: differential_dataflow::lattice::Lattice::advance_by
305#[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    // If Some, an override for the default listen sleep retry parameters.
319    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    // N.B. `read_schema` may be a subset of the total columns for this shard.
338    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    // The `until` gives us an upper bound on the possible values of `mz_now` this query may see.
361    // Ranges are inclusive, so it's safe to use the maximum timestamp as the upper bound when
362    // `until ` is the empty antichain.
363    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                // If the frontier has advanced to the empty antichain,
385                // we'll never emit any rows from any part.
386                return FilterResult::Discard;
387            };
388
389            if lower > upper {
390                // The frontier timestamp is larger than the until of the dataflow:
391                // anything from this part will necessarily be filtered out.
392                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    // N.B. We may have pushed down column "demands" into Persist, so this
427    // relation desc may have a different set of columns than the stats.
428    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    // Re-used state for processing and building rows.
489    let mut datum_vec = mz_repr::DatumVec::new();
490    let mut row_builder = Row::default();
491
492    // Extract the MFP if it exists; leave behind an identity MFP in that case.
493    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        // Acquire an activator to reschedule the operator when it has unfinished work.
498        let activations = scope.activations();
499        let activator = Activator::new(operator_info.address, activations);
500        // Maintain a list of work to do
501        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            // Get dyncfg values once per schedule to amortize the cost of
517            // loading the atomics.
518            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
549/// Pending work to read from fetched parts
550struct PendingWork {
551    /// Whether to panic if a part fails an audit, or to just pass along the audited data.
552    panic_on_audit_failure: bool,
553    /// The time at which the work should happen.
554    capability: Capability<(mz_repr::Timestamp, Subtime)>,
555    /// Pending fetched part.
556    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    /// Returns the contained `FetchedPart`, first parsing it from a
568    /// `FetchedBlob` if necessary.
569    ///
570    /// Also returns a bool, which is true if the part is known (from pushdown
571    /// stats) to be free of `SourceData(Err(_))`s. It will be false if the part
572    /// is known to contain errors or if it's unknown.
573    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                // Won't recurse any further.
578                self.part_mut()
579            }
580            PendingPart::Parsed { part } => &mut part.part,
581        }
582    }
583}
584
585impl PendingWork {
586    /// Perform work, reading from the fetched part, decoding, and sending outputs, while checking
587    /// `yield_fn` whether more fuel is available.
588    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                        // We originally accounted work as the number of outputs, to give downstream
624                        // operators a chance to reduce down anything we've emitted. This mfp call
625                        // might have a restrictive filter, which would have been counted as no
626                        // work. However, in practice, we've been decode_and_mfp be a source of
627                        // interactivity loss during rehydration, so we now also count each mfp
628                        // evaluation against our fuel.
629                        *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                            // Earlier we decided this Part doesn't need to be fetched, but to
641                            // audit our logic we fetched it any way. If the MFP returned data it
642                            // means our earlier decision to not fetch this part was incorrect.
643                            if let Some(stats) = &is_filter_pushdown_audit {
644                                // NB: The tag added by this scope is used for alerting. The panic
645                                // message may be changed arbitrarily, but the tag key and val must
646                                // stay the same.
647                                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                                    // Additional `until` filtering due to temporal filters.
672                                    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                                    // Additional `until` filtering due to temporal filters.
681                                    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                        // At the moment, this is the only case where we can re-use the allocs for
691                        // the `SourceData`/`Row` we decoded. This could be improved if this timely
692                        // operator used a different container than `Vec<Row>`.
693                        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                        // Clone row so we retain our row allocation.
699                        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                    // A discarded part that turns out to hold an error row is
706                    // as much a pushdown violation as one whose MFP yields
707                    // output: errors must surface regardless of any filter.
708                    // Without this arm the audit was blind to exactly the
709                    // undercounted-err-stats violation class.
710                    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                                // `err` is redacted for the same reason the
715                                // `Ok`-row arm redacts its MFP output: these
716                                // events go to Sentry, and a `DecodeError`
717                                // carries the raw source record bytes while
718                                // several `EvalError`s embed user input.
719                                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
748/// A trait representing a type that can be used in `backpressure`.
749pub trait Backpressureable: Clone + 'static {
750    /// Return the weight of the object, in bytes.
751    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/// Flow control configuration.
761#[derive(Debug)]
762pub struct FlowControl<'scope, T: timely::progress::Timestamp> {
763    /// Stream providing in-flight frontier updates.
764    ///
765    /// As implied by its type, this stream never emits data, only progress updates.
766    ///
767    /// TODO: Replace `Infallible` with `!` once the latter is stabilized.
768    pub progress_stream: StreamVec<'scope, T, Infallible>,
769    /// Maximum number of in-flight bytes.
770    pub max_inflight_bytes: usize,
771    /// The minimum range of timestamps (be they granular or not) that must be emitted,
772    /// ignoring `max_inflight_bytes` to ensure forward progress is made.
773    pub summary: T::Summary,
774
775    /// Optional metrics for the `backpressure` operator to keep up-to-date.
776    pub metrics: Option<BackpressureMetrics>,
777}
778
779/// Apply flow control to the `data` input, based on the given `FlowControl`.
780///
781/// The `FlowControl` should have a `progress_stream` that is the pristine, unaltered
782/// frontier of the downstream operator we want to backpressure from, a `max_inflight_bytes`,
783/// and a `summary`. Note that the `data` input expects all the second part of the tuple
784/// timestamp to be 0, and all data to be on the `chosen_worker` worker.
785///
786/// The `summary` represents the _minimum_ range of timestamps that needs to be emitted before
787/// reasoning about `max_inflight_bytes`. In practice this means that we may overshoot
788/// `max_inflight_bytes`.
789///
790/// The implementation of this operator is very subtle. Many inline comments have been added.
791pub 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    // A probe used to inspect this operator during unit-testing
798    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    // Both the `flow_control` input and the data input are disconnected from the output. We manually
813    // manage the output's frontier using a `CapabilitySet`. Note that we also adjust the
814    // `flow_control` progress stream using the `summary` here, using a `feedback` operator in a
815    // non-circular fashion.
816    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    // Helper method used to synthesize current and next frontier for ordered times.
829    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    // _Refine_ the data stream by amending the second input with the part number. This also
849    // ensures that we order the parts by time.
850    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        // The output capability.
887        let mut cap_set = CapabilitySet::from_elem(caps.into_element());
888
889        // The frontier of our output. This matches the `CapabilitySet` above.
890        let mut output_frontier = Antichain::from_elem(TimelyTimestamp::minimum());
891        // The frontier of the `flow_control` input.
892        let mut flow_control_frontier = Antichain::from_elem(TimelyTimestamp::minimum());
893
894        // Parts we have emitted, but have not yet retired (based on the `flow_control` edge).
895        let mut inflight_parts = Vec::new();
896        // Parts we have not yet emitted, but do participate in the `input_frontier`.
897        let mut pending_parts = std::collections::VecDeque::new();
898
899        // Only one worker is responsible for distributing parts
900        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            // At the beginning of our main loop, we determine the total size of
910            // inflight parts.
911            let inflight_bytes: usize = inflight_parts.iter().map(|(_, size)| size).sum();
912
913            // There are 2 main cases where we can continue to emit parts:
914            // - The total emitted bytes is less than `flow_control_max_bytes`.
915            // - The output frontier is not beyond the `flow_control_frontier`
916            //
917            // SUBTLE: in the latter case, we may arbitrarily go into the backpressure `else`
918            // block, as we wait for progress tracking to keep the `flow_control` frontier
919            // up-to-date. This is tested in unit-tests.
920            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                                // Downgrade the output frontier to this part's time. This is useful
930                                // "close" timestamp's from previous parts, even if we don't yet
931                                // emit this part. Note that this is safe because `data_input` ensures
932                                // time-ordering.
933                                output_frontier = frontier;
934                                cap_set.downgrade(output_frontier.iter());
935
936                                // If the most recent value's time is _beyond_ the
937                                // `flow_control` frontier (which takes into account the `summary`), we
938                                // have emitted an entire `summary` worth of data, and can store this
939                                // value for later.
940                                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                // Store the value with the _frontier_ the `flow_control_input` must reach
968                // to retire it. Note that if this `results_in` is `None`, then we
969                // are at `T::MAX`, and give up on flow_control entirely.
970                //
971                // SUBTLE: If we stop storing these parts, we will likely never check the
972                // `flow_control_input` ever again. This won't pile up data as that input
973                // only has frontier updates. There may be spurious activations from it though.
974                //
975                // Also note that we don't attempt to handle overflowing the `u64` part counter.
976                if let Some(emission_ts) = flow_control.summary.results_in(&time) {
977                    inflight_parts.push((emission_ts, byte_size));
978                }
979
980                // Emit the data at the given time, and update the frontier and capabilities
981                // to just beyond the part.
982                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                // We've exhausted our budget, listen for updates to the flow_control
998                // input's frontier until we free up new budget. If we don't interact with
999                // with this side of the if statement, because the stream has no data, we
1000                // don't cause unbounded buffering in timely.
1001                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                // Update the `flow_control_frontier` if its advanced.
1010                flow_control_frontier.clone_from(&new_flow_control_frontier);
1011
1012                // Retire parts that are processed downstream.
1013                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                // Optionally emit some information for tests to examine.
1029                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                // Assert we backpressure only after we have emitted
1056                // the entire timestamp.
1057                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                // Assert we make later progress once processing
1075                // the parts.
1076                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                // Assert we backpressure only after we emitted enough bytes
1092                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                // Assert we make later progress once processing
1110                // the parts.
1111                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                // Additional parts at the same timestamp
1123                (52, Part(10)),
1124                (52, Part(10)),
1125                (52, Part(10)),
1126                (52, Part(100)),
1127                // A later part with a later ts.
1128                (100, Part(100)),
1129            ],
1130            100,
1131            (1, Subtime(0)),
1132            vec![
1133                AssertOutputFrontier((51, Subtime(3))),
1134                // Assert we backpressure after we have emitted enough bytes.
1135                // We assert twice here because we get updates as
1136                // `flow_control` progresses from `(0, 0)`->`(0, 1)`-> a real frontier.
1137                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                // Our output frontier doesn't move, as the downstream frontier hasn't moved past
1149                // 50.
1150                AssertOutputFrontier((51, Subtime(3))),
1151                // After we process all of `50`, we can start emitting data at `52`, but only until
1152                // we exhaust out budget. We don't need to emit all of `52` because we have emitted
1153                // all of `51`.
1154                ProcessXParts(1),
1155                AssertOutputFrontier((52, Subtime(4))),
1156                AssertBackpressured {
1157                    frontier: (52, Subtime(0)),
1158                    inflight_parts: 3,
1159                    retired_parts: 2,
1160                },
1161                // After processing `50` and `51`, the minimum time is `52`, so we ensure that,
1162                // regardless of byte count, we emit the entire time (but do NOT emit the part at
1163                // time `100`.
1164                ProcessXParts(1),
1165                // Clear the previous `51` part, and start filling up `inflight_parts` with other
1166                // parts at `52`
1167                // This is an intermediate state.
1168                AssertBackpressured {
1169                    frontier: (53, Subtime(0)),
1170                    inflight_parts: 2,
1171                    retired_parts: 1,
1172                },
1173                // After we process all of `52`, we can continue to the next time.
1174                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                // Advance our frontier to outputting a single part.
1195                AssertOutputFrontier((50, Subtime(1))),
1196                // Receive backpressure updates until our frontier is up-to-date but
1197                // not beyond the parts (while considering the summary).
1198                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                // Process that part.
1209                ProcessXParts(1),
1210                // Assert that we clear the backpressure status
1211                AssertBackpressured {
1212                    frontier: (50, Subtime(2)),
1213                    inflight_parts: 1,
1214                    retired_parts: 1,
1215                },
1216                // Ensure we make progress to the next part.
1217                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                // we can emit 3 parts before we hit the backpressure limit
1233                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                // Retire the single part.
1245                ProcessXParts(1),
1246                AssertBackpressured {
1247                    frontier: (50, Subtime(2)),
1248                    inflight_parts: 3,
1249                    retired_parts: 1,
1250                },
1251                // Ensure we make progress, and then
1252                // can retire the next 2 parts.
1253                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    /// Actions taken by `backpressure_runner`.
1275    enum Step {
1276        /// Assert that the output frontier of the `backpressure` operator has AT LEAST made it
1277        /// this far. This is a single time because we assume
1278        AssertOutputFrontier(Time),
1279        /// Assert that we have entered the backpressure flow in the `backpressure` operator. This
1280        /// allows us to assert what feedback frontier we got to, and how many inflight parts we
1281        /// retired.
1282        AssertBackpressured {
1283            frontier: Time,
1284            inflight_parts: usize,
1285            retired_parts: usize,
1286        },
1287        /// Process X parts in the downstream operator. This affects the feedback frontier.
1288        ProcessXParts(usize),
1289    }
1290
1291    /// A function that runs the `steps` to ensure that `backpressure` works as expected.
1292    fn backpressure_runner(
1293        // The input data to the `backpressure` operator
1294        input: Vec<(u64, Part)>,
1295        // The maximum inflight bytes the `backpressure` operator allows through.
1296        max_inflight_bytes: usize,
1297        // The feedback summary used by the `backpressure` operator.
1298        summary: Time,
1299        // List of steps to run through.
1300        steps: Vec<Step>,
1301        // Whether or not to consume records in the non-granular scope. This is useful when the
1302        // `summary` is something like `(1, 0)`.
1303        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                // Set up the top-level non-granular scope.
1314                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                        // If we want to granularly consume the output, we setup the consumer here.
1369                        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                    // If we want to non-granularly consume the output, we setup the consumer here.
1391                    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            // Send the input to the empty frontier.
1457            let _ = finalizer_tx.send(());
1458        });
1459    }
1460
1461    /// An operator that emits `Part`'s at the specified timestamps. Does not
1462    /// drop its capability until it gets a signal from the `Sender` it returns.
1463    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    /// An operator that consumes its input ONLY when given a signal to do from
1495    /// the `UnboundedSender` it returns. Each `send` corresponds with 1 `Data` event
1496    /// being processed. Also connects the `feedback` handle to its output.
1497    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                // Consume exactly one messages (unless the input is exhausted).
1519                while let Some(Event::Progress(_)) = input.next().await {}
1520            }
1521        });
1522        output.connect_loop(feedback);
1523
1524        tx
1525    }
1526
1527    /// End-to-end persist filter-pushdown soundness checks.
1528    ///
1529    /// These mirror the real audit contract in [`PendingWork::do_work`]: build
1530    /// a part from actual rows, compute the *real* production column statistics
1531    /// from it, then run [`filter_result`] and assert it never returns
1532    /// [`FilterResult::Discard`] for a part whose MFP would produce output (an
1533    /// `Ok` row or an error row) on some actual row. A `Discard` in that case is
1534    /// exactly the wrongly-skipped part the runtime audit panics on.
1535    ///
1536    /// Unlike the interpreter-level proptests in `mz_expr::interpret`, this
1537    /// exercises the full path stats derivation -> `col_stats` -> interpreter,
1538    /// so it catches both interpreter bugs and stats-derivation bugs (e.g. a
1539    /// column stat range that fails to contain a real value). See
1540    /// database-issues#9656 / PER-50.
1541    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        /// Compute the real production `PartStats` from a set of rows, the same
1584        /// way the storage read path does.
1585        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        /// Ground truth: does the MFP produce any output (an `Ok` row that
1595        /// passes the filter, or an error row) on at least one actual row? If
1596        /// so, the part must not be discarded.
1597        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        /// `(a::float4 + round(c0, scale)::float4) * b::float4 <cmp> c::float4`.
1642        /// `round` overflows and the float arithmetic can produce an error or a
1643        /// NaN on interior values of `c0` that the endpoints don't reveal.
1644        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        /// `c0::mz_timestamp <cmp> <ts>`. The cast errors on fractional or
1681        /// out-of-range `c0`, an interior-error condition the interpreter's
1682        /// endpoint sampling can miss.
1683        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)] // error: unsupported operation: can't call foreign function `decContextDefault`
1725        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        /// Assert that `filter_result` keeps a part it must keep, and that the
1757        /// case is not vacuous: the MFP has to yield output on a real row for
1758        /// "must keep" to mean anything.
1759        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        /// A part holding `-NaN` must not hide the rest of its rows from a
1783        /// filter on that column.
1784        ///
1785        /// `PrimitiveStats` takes a float column's bounds in arrow's *total*
1786        /// order, where `-NaN` sorts below `-Infinity`, so such a part records
1787        /// `lower = -NaN` against a finite `upper`. `OrderedFloat`, the `Datum`
1788        /// order the interpreter compares in, ranks every NaN *above* every
1789        /// finite value, so those bounds arrive unordered. Reading them as an
1790        /// empty range discarded the part, which lost every other row in it
1791        /// (PER-53). Both float widths take the same path.
1792        #[mz_ore::test]
1793        #[cfg_attr(miri, ignore)] // unsupported operation: can't call foreign function
1794        fn negative_nan_does_not_discard_matching_part() {
1795            // Both rows have to land in the same part. Split across parts each
1796            // one gets ordered bounds of its own and nothing is discarded.
1797            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        /// A part holding NaNs of both signs must not hide its other rows.
1833        ///
1834        /// Arrow's total order puts `-NaN` below and `+NaN` above everything,
1835        /// so such a part records the bounds `(-NaN, +NaN)`. Under
1836        /// `OrderedFloat` those decode to `NaN == NaN`: not an inverted range
1837        /// but a seemingly valid one claiming the part holds nothing but NaN,
1838        /// so widening unordered bounds does not catch it. A filter that a
1839        /// finite row matches but NaN does not then discarded the part. Only
1840        /// the stats decode still sees the NaN signs, so the guard lives in
1841        /// `mz_repr::stats::col_values`.
1842        #[mz_ore::test]
1843        #[cfg_attr(miri, ignore)] // unsupported operation: can't call foreign function
1844        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        // Wide-schema variant: multiple column types populated from
1883        // `interesting_datums`, a predicate vocabulary that reaches the
1884        // interpreter's special cases (jsonb map specs and their unions,
1885        // `TryParseMonotonicIso8601Timestamp`, dynamically-monotone
1886        // timestamp+interval, the infinity guard on float multiplication),
1887        // Err rows, and real mz_now bounds instead of an unconstrained time
1888        // range.
1889
1890        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                // The bool column stays non-nullable so an all-Err part
1933                // exercises the fabricated default bounds a non-nullable
1934                // column gets when no Ok row provides a value.
1935                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        /// `col <cmp> lit`, with the literal drawn from the same interesting
1997        /// pool as the row values, so poison values show up on both sides.
1998        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        /// `(jN -> 'key') IS NULL` or `(jN ->> 'key') = 'a'`, the shapes that
2035        /// consume the Nested specs built from real jsonb map stats.
2036        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        /// `((CASE WHEN <cond> THEN j1 ELSE j2 END) ->> 'key') IS NULL`, the
2059        /// PER-6 shape: unioning the two columns' Nested specs.
2060        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        /// `try_parse_monotonic_iso8601_timestamp(c_str) <cmp> <ts>`, the one
2079        /// SpecialUnary implementation in the interpreter.
2080        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        /// `(c_ts + <interval>) <cmp> <ts>`, the DynamicMonotone handler:
2103        /// day-only intervals are treated as monotone, month-bearing ones must
2104        /// stay conservative.
2105        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        /// `(c_f64 * <const>) <cmp> <const>`, aimed at the interpreter's
2127        /// infinity guard: multiplication is monotone but not
2128        /// infinity-monotone.
2129        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        /// `mz_now() <cmp> <mz_timestamp expr>`, compiled by `into_plan` into
2142        /// the temporal lower/upper bounds that `filter_result` checks against
2143        /// the part's time range.
2144        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                // The numeric shapes from the narrow test, aimed at the
2174                // fallible-interior mechanisms; both reference column 0.
2175                (
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        /// The zero-column count(*) path: when the read desc projects away
2208        /// every column and each row is known to pass, `filter_result`
2209        /// replaces the part with a synthesized single-row KV instead of
2210        /// keeping or discarding it. Errors and filters that can skip rows
2211        /// must suppress the substitution.
2212        #[mz_ore::test]
2213        #[cfg_attr(miri, ignore)] // too slow
2214        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            // No predicates, no errors: every row passes, so the part is
2223            // replaced with the synthesized KV.
2224            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            // An error row must disable the substitution: the part has to be
2234            // fetched so the error surfaces.
2235            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            // A constant-false filter never keeps anything: plain Discard.
2248            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        /// Schema drift between the stats and the read desc must degrade to
2265        /// "no stats", never to a narrower spec.
2266        ///
2267        /// Two real shapes: a column appended by `ALTER TABLE ... ADD COLUMN`
2268        /// after the part was written (present in the read desc, absent from
2269        /// the stats), and demand pushdown projecting the read desc down to a
2270        /// subset of the written columns (stats carry extra columns).
2271        #[mz_ore::test]
2272        #[cfg_attr(miri, ignore)] // too slow
2273        fn schema_drift_degrades_to_no_stats() {
2274            let metrics = PartStatsMetrics::new(&MetricsRegistry::new());
2275
2276            // Part written before ALTER TABLE ... ADD COLUMN b.
2277            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            // Old rows read the new column as null, so `b IS NULL` matches
2289            // them and the part must be kept.
2290            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            // Demand pushdown: the read desc is a projection of the written
2301            // schema. The surviving column's stats must still line up with it
2302            // by name, so a matching filter keeps the part.
2303            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        /// Ground truth, mirroring [`PendingWork::do_work`]: a part yields
2333        /// output if any Err row survives `until`, or if the MFP applied to
2334        /// any Ok row at its effective time produces anything at all. The
2335        /// runtime audit fires on any result from `evaluate`, before the
2336        /// additional `until` filtering of the produced rows, so this must
2337        /// not post-filter either.
2338        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)] // too slow, and decNumber FFI is unsupported
2373        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                // Predicate shapes that use mz_now in a way the temporal
2382                // filter machinery does not support fail to plan; there is
2383                // nothing to check for those.
2384                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                // Mirror the read path: mz_now is bounded by the part's
2399                // frontier and the dataflow's until, both inclusive, with an
2400                // empty until standing in for MAX. A frontier past the until
2401                // is discarded before stats are consulted.
2402                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            // The vocabulary is wide (9 columns, 10 predicate shapes), so a
2424            // specific poison-value-plus-predicate coincidence is rare per
2425            // case. The default 256 cases demonstrably miss known bugs; 4096
2426            // still runs in a couple of seconds because each case is cheap.
2427            // An explicit PROPTEST_CASES (already parsed into the default
2428            // config) wins, for long local or nightly runs.
2429            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        /// Filter decisions are made per part: rows split across several
2447        /// parts get an independent decision per part, and a part whose own
2448        /// rows yield output must never be discarded, regardless of what the
2449        /// sibling parts contain (e.g. a poison value in one part must not
2450        /// affect another part's decision, and vice versa).
2451        #[mz_ore::test]
2452        #[cfg_attr(miri, ignore)] // too slow, and decNumber FFI is unsupported
2453        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}