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::borrow::Cow;
14use std::collections::VecDeque;
15use std::convert::Infallible;
16use std::fmt::Debug;
17use std::future::Future;
18use std::hash::Hash;
19use std::sync::Arc;
20
21use differential_dataflow::lattice::Lattice;
22use futures::{StreamExt, future::Either};
23use mz_expr::{ColumnSpecs, EvalError, Interpreter, MfpPlan, ResultSpec, UnmaterializableFunc};
24use mz_ore::cast::CastFrom;
25use mz_ore::collections::CollectionExt;
26use mz_ore::str::redact;
27use mz_persist_client::cache::PersistClientCache;
28use mz_persist_client::cfg::{PersistConfig, RetryParameters};
29use mz_persist_client::fetch::{ExchangeableBatchPart, ShardSourcePart};
30use mz_persist_client::fetch::{FetchedBlob, FetchedPart};
31use mz_persist_client::operators::shard_source::{
32    ErrorHandler, FilterResult, SnapshotMode, shard_source,
33};
34use mz_persist_client::stats::STATS_AUDIT_PANIC;
35use mz_persist_types::Codec64;
36use mz_persist_types::codec_impls::UnitSchema;
37use mz_persist_types::columnar::{ColumnEncoder, Schema};
38use mz_repr::{
39    Datum, DatumVec, Diff, GlobalId, RelationDesc, ReprRelationType, Row, RowArena, Timestamp,
40};
41use mz_storage_types::StorageDiff;
42use mz_storage_types::controller::{CollectionMetadata, TxnsCodecRow};
43use mz_storage_types::errors::DataflowError;
44use mz_storage_types::sources::SourceData;
45use mz_storage_types::stats::RelationPartStats;
46use mz_timely_util::builder_async::{
47    Event, OperatorBuilder as AsyncOperatorBuilder, PressOnDropButton,
48};
49use mz_timely_util::probe::ProbeNotify;
50use mz_txn_wal::operator::{TxnsContext, TxnsProgress};
51use serde::{Deserialize, Serialize};
52use timely::container::{CapacityContainerBuilder, PushInto};
53use timely::dataflow::channels::pact::Pipeline;
54use timely::dataflow::operators::generic::OutputBuilder;
55use timely::dataflow::operators::generic::builder_rc::OperatorBuilder;
56use timely::dataflow::operators::{Capability, Leave};
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 timely::{ContainerBuilder, PartialOrder};
65use tokio::sync::mpsc::UnboundedSender;
66use tracing::{error, trace};
67
68use crate::metrics::BackpressureOperatorMetrics;
69
70/// This opaque token represents progress within a timestamp, allowing finer-grained frontier
71/// progress than would otherwise be possible.
72///
73/// This is "opaque" since we'd like to reserve the right to change the definition in the future
74/// without downstreams being able to rely on the precise representation. (At the moment, this
75/// is a simple batch counter, though we may change it to eg. reflect progress through the keyspace
76/// in the future.)
77#[derive(
78    Copy,
79    Clone,
80    PartialEq,
81    Default,
82    Eq,
83    PartialOrd,
84    Ord,
85    Debug,
86    Serialize,
87    Deserialize,
88    Hash,
89    columnar::Columnar
90)]
91// Derive ordering on the generated `SubtimeReference` too: the paged merge
92// batcher sorts the `(Timestamp, Subtime)` time and so requires `Ref: Ord`.
93#[columnar(derive(PartialEq, Eq, PartialOrd, Ord))]
94pub struct Subtime(u64);
95
96impl PartialOrder for Subtime {
97    fn less_equal(&self, other: &Self) -> bool {
98        self.0.less_equal(&other.0)
99    }
100}
101
102impl TotalOrder for Subtime {}
103
104impl PathSummary<Subtime> for Subtime {
105    fn results_in(&self, src: &Subtime) -> Option<Subtime> {
106        self.0.results_in(&src.0).map(Subtime)
107    }
108
109    fn followed_by(&self, other: &Self) -> Option<Self> {
110        self.0.followed_by(&other.0).map(Subtime)
111    }
112}
113
114impl TimelyTimestamp for Subtime {
115    type Summary = Subtime;
116
117    fn minimum() -> Self {
118        Subtime(0)
119    }
120}
121
122impl columnation::Columnation for Subtime {
123    type InnerRegion = columnation::CopyRegion<Subtime>;
124}
125
126impl differential_dataflow::lattice::Lattice for Subtime {
127    fn join(&self, other: &Self) -> Self {
128        Subtime(std::cmp::max(self.0, other.0))
129    }
130    fn meet(&self, other: &Self) -> Self {
131        Subtime(std::cmp::min(self.0, other.0))
132    }
133}
134
135impl differential_dataflow::lattice::Maximum for Subtime {
136    fn maximum() -> Self {
137        Subtime(u64::MAX)
138    }
139}
140
141impl Subtime {
142    /// The smallest non-zero summary for the opaque timestamp type.
143    pub const fn least_summary() -> Self {
144        Subtime(1)
145    }
146}
147
148/// Creates a new source that reads from a persist shard, distributing the work
149/// of reading data to all timely workers.
150///
151/// Returns the shard's rows in `CB`'s containers and its errors in a stream of their own.
152///
153/// All times emitted will have been [advanced by] the given `as_of` frontier.
154/// All updates at times greater or equal to `until` will be suppressed.
155/// The `map_filter_project` argument, if supplied, may be partially applied,
156/// and any un-applied part of the argument will be left behind in the argument.
157///
158/// Users of this function have the ability to apply flow control to the output
159/// to limit the in-flight data (measured in bytes) it can emit. The flow control
160/// input is a timely stream that communicates the frontier at which the data
161/// emitted from by this source have been dropped.
162///
163/// **Note:** Because this function is reading batches from `persist`, it is working
164/// at batch granularity. In practice, the source will be overshooting the target
165/// flow control upper by an amount that is related to the size of batches.
166///
167/// If no flow control is desired an empty stream whose frontier immediately advances
168/// to the empty antichain can be used. An easy easy of creating such stream is by
169/// using [`timely::dataflow::operators::generic::operator::empty`].
170///
171/// [advanced by]: differential_dataflow::lattice::Lattice::advance_by
172pub fn persist_source<'scope, E, CB>(
173    scope: Scope<'scope, mz_repr::Timestamp>,
174    source_id: GlobalId,
175    persist_clients: Arc<PersistClientCache>,
176    txns_ctx: &TxnsContext,
177    metadata: CollectionMetadata,
178    read_schema: Option<RelationDesc>,
179    as_of: Option<Antichain<Timestamp>>,
180    snapshot_mode: SnapshotMode,
181    until: Antichain<Timestamp>,
182    map_filter_project: Option<&mut MfpPlan>,
183    max_inflight_bytes: Option<usize>,
184    start_signal: impl Future<Output = ()> + Send + 'static,
185    error_handler: ErrorHandler,
186) -> (
187    Stream<'scope, mz_repr::Timestamp, CB::Container>,
188    StreamVec<'scope, mz_repr::Timestamp, (E, Timestamp, Diff)>,
189    Vec<PressOnDropButton>,
190)
191where
192    E: timely::ExchangeData + Ord + Clone + Debug + From<DataflowError> + From<EvalError>,
193    CB: ContainerBuilder + PushInto<(Row, mz_repr::Timestamp, Diff)>,
194    CB::Container: Clone,
195{
196    let mut tokens = vec![];
197    let name = source_id.to_string();
198
199    let outer = scope.clone();
200    let (ok_stream, err_stream) =
201        scope.scoped(&format!("granular_backpressure({})", source_id), |scope| {
202            let (flow_control, flow_control_probe) = match max_inflight_bytes {
203                Some(max_inflight_bytes) => {
204                    let series = &persist_clients.metrics().backpressure;
205                    let backpressure_metrics = BackpressureOperatorMetrics::new(
206                        series.emitted_bytes.clone(),
207                        series.last_backpressured_bytes.clone(),
208                        series.retired_bytes.clone(),
209                    );
210
211                    let probe = mz_timely_util::probe::Handle::default();
212                    let progress_stream = mz_timely_util::probe::source(
213                        scope.clone(),
214                        format!("decode_backpressure_probe({source_id})"),
215                        probe.clone(),
216                    );
217                    let flow_control = FlowControl {
218                        progress_stream,
219                        max_inflight_bytes,
220                        summary: (Default::default(), Subtime::least_summary()),
221                        metrics: Some(backpressure_metrics),
222                    };
223                    (Some(flow_control), Some(probe))
224                }
225                None => (None, None),
226            };
227
228            // Our default listen sleeps are tuned for the case of a shard that is
229            // written once a second, but txn-wal allows these to be lazy.
230            // Override the tuning to reduce crdb load. The pubsub fallback
231            // responsibility is then replaced by manual "one state" wakeups in the
232            // txns_progress operator.
233            let cfg = Arc::clone(&persist_clients.cfg().configs);
234            let subscribe_sleep = match metadata.txns_shard {
235                Some(_) => Some(move || mz_txn_wal::operator::txns_data_shard_retry_params(&cfg)),
236                None => None,
237            };
238
239            let filter_plan = map_filter_project.as_ref().map(|p| (*p).clone());
240            let (cfg, fetched, source_tokens) = fetch_parts(
241                outer,
242                scope,
243                source_id,
244                Arc::clone(&persist_clients),
245                metadata.clone(),
246                read_schema,
247                as_of.clone(),
248                snapshot_mode,
249                until.clone(),
250                filter_plan,
251                flow_control,
252                subscribe_sleep,
253                start_signal,
254                error_handler,
255            );
256            tokens.extend(source_tokens);
257
258            let (ok_stream, err_stream) = decode_and_mfp::<E, mz_repr::Timestamp, CB>(
259                cfg,
260                fetched,
261                &name,
262                until.clone(),
263                map_filter_project,
264                |time| time.0,
265            );
266
267            // The handle's frontier is the meet of what its clones report, so both streams
268            // have to feed it for backpressure to retire the right bytes.
269            let (ok_stream, err_stream) = match flow_control_probe {
270                Some(probe) => (
271                    ok_stream.probe_notify_with(vec![probe.clone()]),
272                    err_stream.probe_notify_with(vec![probe]),
273                ),
274                None => (ok_stream, err_stream),
275            };
276
277            (ok_stream.leave(outer), err_stream.leave(outer))
278        });
279
280    // If a txns_shard was provided, then this shard is in the txn-wal
281    // system. This means the "logical" upper may be ahead of the "physical"
282    // upper. Render dataflow operators that pass through the inputs and
283    // translate the progress frontiers as necessary.
284    let (ok_stream, err_stream) = match metadata.txns_shard {
285        Some(txns_shard) => {
286            let (progress, remap_token) = TxnsProgress::new::<SourceData, (), i64, TxnsCodecRow, _>(
287                outer,
288                &name,
289                txns_ctx,
290                move || {
291                    let (c, l) = (
292                        Arc::clone(&persist_clients),
293                        metadata.persist_location.clone(),
294                    );
295                    async move { c.open(l).await.expect("location is valid") }
296                },
297                txns_shard,
298                metadata.data_shard,
299                as_of
300                    .expect("as_of is provided for table sources")
301                    .into_option()
302                    .expect("shard is not closed"),
303                Arc::new(metadata.relation_desc),
304                Arc::new(UnitSchema),
305            );
306            let (ok_stream, ok_token) = progress.translate(ok_stream, until.clone());
307            let (err_stream, err_token) = progress.translate(err_stream, until);
308            tokens.extend([remap_token, ok_token, err_token]);
309            (ok_stream, err_stream)
310        }
311        None => (ok_stream, err_stream),
312    };
313
314    (ok_stream, err_stream, tokens)
315}
316
317type RefinedScope<'scope, T> = Scope<'scope, (T, Subtime)>;
318
319/// Creates a new source that reads from a persist shard, distributing the work
320/// of reading data to all timely workers.
321///
322/// Returns the ok and error sides separately, both carrying the refined time.
323///
324/// All times emitted will have been [advanced by] the given `as_of` frontier.
325///
326/// [advanced by]: differential_dataflow::lattice::Lattice::advance_by
327pub fn persist_source_core<'g, 'outer, E>(
328    outer: Scope<'outer, mz_repr::Timestamp>,
329    scope: RefinedScope<'g, mz_repr::Timestamp>,
330    source_id: GlobalId,
331    persist_clients: Arc<PersistClientCache>,
332    metadata: CollectionMetadata,
333    read_schema: Option<RelationDesc>,
334    as_of: Option<Antichain<Timestamp>>,
335    snapshot_mode: SnapshotMode,
336    until: Antichain<Timestamp>,
337    map_filter_project: Option<&mut MfpPlan>,
338    flow_control: Option<FlowControl<'g, RefinedTime>>,
339    // If Some, an override for the default listen sleep retry parameters.
340    listen_sleep: Option<impl Fn() -> RetryParameters + Send + 'static>,
341    start_signal: impl Future<Output = ()> + Send + 'static,
342    error_handler: ErrorHandler,
343) -> (
344    StreamVec<'g, RefinedTime, (Row, RefinedTime, Diff)>,
345    StreamVec<'g, RefinedTime, (E, RefinedTime, Diff)>,
346    Vec<PressOnDropButton>,
347)
348where
349    E: timely::ExchangeData + Ord + Clone + Debug + From<DataflowError> + From<EvalError>,
350{
351    let name = source_id.to_string();
352    let filter_plan = map_filter_project.as_ref().map(|p| (*p).clone());
353    let (cfg, fetched, token) = fetch_parts(
354        outer,
355        scope,
356        source_id,
357        persist_clients,
358        metadata,
359        read_schema,
360        as_of,
361        snapshot_mode,
362        until.clone(),
363        filter_plan,
364        flow_control,
365        listen_sleep,
366        start_signal,
367        error_handler,
368    );
369    let (oks, errs) = decode_and_mfp::<E, RefinedTime, RowVecBuilder<RefinedTime>>(
370        cfg,
371        fetched,
372        &name,
373        until,
374        map_filter_project,
375        |time| time,
376    );
377    (oks, errs, token)
378}
379
380/// Fetch the parts of a persist shard a dataflow needs, distributing the work of reading them
381/// across all timely workers.
382#[allow(clippy::needless_borrow)]
383fn fetch_parts<'g, 'outer>(
384    outer: Scope<'outer, mz_repr::Timestamp>,
385    scope: RefinedScope<'g, mz_repr::Timestamp>,
386    source_id: GlobalId,
387    persist_clients: Arc<PersistClientCache>,
388    metadata: CollectionMetadata,
389    read_schema: Option<RelationDesc>,
390    as_of: Option<Antichain<Timestamp>>,
391    snapshot_mode: SnapshotMode,
392    until: Antichain<Timestamp>,
393    // The MFP whose filter is pushed down into persist, if any.
394    filter_plan: Option<MfpPlan>,
395    flow_control: Option<FlowControl<'g, RefinedTime>>,
396    // If Some, an override for the default listen sleep retry parameters.
397    listen_sleep: Option<impl Fn() -> RetryParameters + Send + 'static>,
398    start_signal: impl Future<Output = ()> + Send + 'static,
399    error_handler: ErrorHandler,
400) -> (
401    PersistConfig,
402    StreamVec<'g, RefinedTime, FetchedBlob<SourceData, (), Timestamp, StorageDiff>>,
403    Vec<PressOnDropButton>,
404) {
405    let cfg = persist_clients.cfg().clone();
406    let name = source_id.to_string();
407
408    // N.B. `read_schema` may be a subset of the total columns for this shard.
409    let read_desc = match read_schema {
410        Some(desc) => desc,
411        None => metadata.relation_desc,
412    };
413
414    let desc_transformer = match flow_control {
415        Some(flow_control) => Some(move |scope, descs, chosen_worker| {
416            let (stream, token) = backpressure(
417                scope,
418                &format!("backpressure({source_id})"),
419                descs,
420                flow_control,
421                chosen_worker,
422                None,
423            );
424            (stream, vec![token])
425        }),
426        None => None,
427    };
428
429    let metrics = Arc::clone(persist_clients.metrics());
430    let filter_name = name.clone();
431    // The `until` gives us an upper bound on the possible values of `mz_now` this query may see.
432    // Ranges are inclusive, so it's safe to use the maximum timestamp as the upper bound when
433    // `until ` is the empty antichain.
434    let upper = until.as_option().cloned().unwrap_or(Timestamp::MAX);
435    let (fetched, token) = shard_source(
436        outer,
437        scope,
438        &name,
439        move || {
440            let (c, l) = (
441                Arc::clone(&persist_clients),
442                metadata.persist_location.clone(),
443            );
444            async move { c.open(l).await.unwrap() }
445        },
446        metadata.data_shard,
447        as_of,
448        snapshot_mode,
449        until.clone(),
450        desc_transformer,
451        Arc::new(read_desc.clone()),
452        Arc::new(UnitSchema),
453        move |stats, frontier| {
454            let Some(lower) = frontier.as_option().copied() else {
455                // If the frontier has advanced to the empty antichain,
456                // we'll never emit any rows from any part.
457                return FilterResult::Discard;
458            };
459
460            if lower > upper {
461                // The frontier timestamp is larger than the until of the dataflow:
462                // anything from this part will necessarily be filtered out.
463                return FilterResult::Discard;
464            }
465
466            let time_range =
467                ResultSpec::value_between(Datum::MzTimestamp(lower), Datum::MzTimestamp(upper));
468            if let Some(plan) = &filter_plan {
469                let metrics = &metrics.pushdown.part_stats;
470                let stats = RelationPartStats::new(&filter_name, metrics, &read_desc, stats);
471                filter_result(&read_desc, time_range, stats, plan)
472            } else {
473                FilterResult::Keep
474            }
475        },
476        listen_sleep,
477        start_signal,
478        error_handler,
479    );
480    (cfg, fetched, token)
481}
482
483fn filter_result(
484    relation_desc: &RelationDesc,
485    time_range: ResultSpec,
486    stats: RelationPartStats,
487    plan: &MfpPlan,
488) -> FilterResult {
489    let arena = RowArena::new();
490    let relation = ReprRelationType::from(relation_desc.typ());
491    let mut ranges = ColumnSpecs::new(&relation, &arena);
492    ranges.push_unmaterializable(UnmaterializableFunc::MzNow, time_range);
493
494    let may_error = stats.err_count().map_or(true, |count| count > 0);
495
496    // N.B. We may have pushed down column "demands" into Persist, so this
497    // relation desc may have a different set of columns than the stats.
498    for (pos, (idx, _, _)) in relation_desc.iter_all().enumerate() {
499        let result_spec = stats.col_stats(idx, &arena);
500        ranges.push_column(pos, result_spec);
501    }
502    let result = ranges.mfp_plan_filter(plan).range;
503    let may_error = may_error || result.may_fail();
504    let may_keep = result.may_contain(Datum::True);
505    let may_skip = result.may_contain(Datum::False) || result.may_contain(Datum::Null);
506    if relation_desc.len() == 0 && !may_error && !may_skip {
507        let Ok(mut key) = <RelationDesc as Schema<SourceData>>::encoder(relation_desc) else {
508            return FilterResult::Keep;
509        };
510        key.append(&SourceData(Ok(Row::default())));
511        let key = key.finish();
512        let Ok(mut val) = <UnitSchema as Schema<()>>::encoder(&UnitSchema) else {
513            return FilterResult::Keep;
514        };
515        val.append(&());
516        let val = val.finish();
517
518        FilterResult::ReplaceWith {
519            key: Arc::new(key),
520            val: Arc::new(val),
521        }
522    } else if may_error || may_keep {
523        FilterResult::Keep
524    } else {
525        FilterResult::Discard
526    }
527}
528
529/// The time a decode operator's capabilities carry, refined with a [`Subtime`] so flow
530/// control can pace parts within a millisecond.
531type RefinedTime = (mz_repr::Timestamp, Subtime);
532
533/// Ok-side container builder producing row vectors timestamped with `T`.
534pub type RowVecBuilder<T> = ConsolidatingContainerBuilder<Vec<(Row, T, Diff)>>;
535
536/// Err-side container builder.
537type ErrBuilder<E, RT> = ConsolidatingContainerBuilder<Vec<(E, RT, Diff)>>;
538
539/// Decode fetched parts and apply `map_filter_project`, writing ok records into `CB`'s
540/// containers and err records into a separate output.
541///
542/// `record_time` picks what a record stores for its time. A reader that does not distinguish
543/// times within a millisecond passes `|time| time.0`, which drops the [`Subtime`] coordinate
544/// the enclosing scope refines with: that coordinate exists to pace flow control and stays on
545/// the capabilities, and keeping it in the records would force a re-encode to strip it later.
546/// A reader that builds a collection in the refined scope needs it, and passes `|time| time`.
547fn decode_and_mfp<'scope, E, RT, CB>(
548    cfg: PersistConfig,
549    fetched: StreamVec<'scope, RefinedTime, FetchedBlob<SourceData, (), Timestamp, StorageDiff>>,
550    name: &str,
551    until: Antichain<Timestamp>,
552    mut map_filter_project: Option<&mut MfpPlan>,
553    record_time: fn(RefinedTime) -> RT,
554) -> (
555    Stream<'scope, RefinedTime, CB::Container>,
556    StreamVec<'scope, RefinedTime, (E, RT, Diff)>,
557)
558where
559    E: timely::ExchangeData + Ord + Clone + Debug + From<DataflowError> + From<EvalError>,
560    RT: Ord + Clone + Debug + 'static,
561    CB: ContainerBuilder + PushInto<(Row, RT, Diff)>,
562{
563    let scope = fetched.scope();
564    let mut builder = OperatorBuilder::new(
565        format!("persist_source::decode_and_mfp({})", name),
566        scope.clone(),
567    );
568    let operator_info = builder.operator_info();
569
570    let mut fetched_input = builder.new_input(fetched, Pipeline);
571    let (ok_output, ok_stream) = builder.new_output::<CB::Container>();
572    let mut ok_output: OutputBuilder<_, CB> = OutputBuilder::from(ok_output);
573    let (err_output, err_stream) = builder.new_output();
574    let mut err_output: OutputBuilder<_, ErrBuilder<E, RT>> = OutputBuilder::from(err_output);
575
576    let name = name.to_owned();
577    // Extract the MFP if it exists; leave behind an identity MFP in that case.
578    let map_filter_project = map_filter_project.as_mut().map(|mfp| mfp.take());
579
580    builder.build(move |_caps| {
581        // Acquire an activator to reschedule the operator when it has unfinished work.
582        let activator = Activator::new(operator_info.address, scope.activations());
583        let panic_on_audit_failure = STATS_AUDIT_PANIC.handle(&cfg);
584        let mut pending_work = VecDeque::new();
585        let mut datum_vec = DatumVec::new();
586        let mut row_builder = Row::default();
587
588        move |_frontier| {
589            fetched_input.for_each(|time, data| {
590                let capabilities = [time.retain(0), time.retain(1)];
591                let panic_on_audit_failure = panic_on_audit_failure.get();
592                for blob in data.drain(..) {
593                    pending_work.push_back(PendingWork {
594                        panic_on_audit_failure,
595                        capabilities: capabilities.clone(),
596                        part: PendingPart::Unparsed(blob),
597                    });
598                }
599            });
600
601            // Get dyncfg values once per schedule to amortize the cost of loading the atomics.
602            let yield_fuel = cfg.storage_source_decode_fuel();
603            let mut work = 0;
604            let mut ok_output = ok_output.activate();
605            let mut err_output = err_output.activate();
606            while let Some(front) = pending_work.front_mut() {
607                if work >= yield_fuel {
608                    break;
609                }
610                let cap_time = *front.capabilities[0].time();
611                // A session per part: dropping it flushes the container builder, so a
612                // container never mixes records from parts held at different capabilities.
613                let mut ok_session = ok_output.session_with_builder(&front.capabilities[0]);
614                let mut err_session = err_output.session_with_builder(&front.capabilities[1]);
615                let done = decode_part(
616                    &mut front.part,
617                    front.panic_on_audit_failure,
618                    cap_time,
619                    &name,
620                    &until,
621                    map_filter_project.as_ref(),
622                    &mut datum_vec,
623                    &mut row_builder,
624                    &mut work,
625                    yield_fuel,
626                    |record, time, diff| match record {
627                        Ok(row) => ok_session.give((row.into_owned(), record_time(time), diff)),
628                        Err(err) => err_session.give((err, record_time(time), diff)),
629                    },
630                );
631                drop(ok_session);
632                drop(err_session);
633                if done {
634                    pending_work.pop_front();
635                }
636            }
637            if !pending_work.is_empty() {
638                activator.activate();
639            }
640        }
641    });
642
643    (ok_stream, err_stream)
644}
645
646/// Pending work to read from fetched parts
647struct PendingWork {
648    /// Whether to panic if a part fails an audit, or to just pass along the audited data.
649    panic_on_audit_failure: bool,
650    /// The time at which the work should happen, one capability per operator output.
651    capabilities: [Capability<RefinedTime>; 2],
652    /// Pending fetched part.
653    part: PendingPart,
654}
655
656enum PendingPart {
657    Unparsed(FetchedBlob<SourceData, (), Timestamp, StorageDiff>),
658    Parsed {
659        part: ShardSourcePart<SourceData, (), Timestamp, StorageDiff>,
660    },
661}
662
663impl PendingPart {
664    /// Returns the contained `FetchedPart`, first parsing it from a
665    /// `FetchedBlob` if necessary.
666    ///
667    /// Also returns a bool, which is true if the part is known (from pushdown
668    /// stats) to be free of `SourceData(Err(_))`s. It will be false if the part
669    /// is known to contain errors or if it's unknown.
670    fn part_mut(&mut self) -> &mut FetchedPart<SourceData, (), Timestamp, StorageDiff> {
671        match self {
672            PendingPart::Unparsed(x) => {
673                *self = PendingPart::Parsed { part: x.parse() };
674                // Won't recurse any further.
675                self.part_mut()
676            }
677            PendingPart::Parsed { part } => &mut part.part,
678        }
679    }
680}
681
682/// Read `part`, apply the MFP, and hand every record to `give`, stopping once `work` reaches
683/// `yield_fuel`. Returns whether the part is exhausted.
684///
685/// Records carry `cap_time` with its millisecond replaced by the record's own time, so the
686/// caller must emit them at the capability `cap_time` came from.
687fn decode_part<E, F>(
688    part: &mut PendingPart,
689    panic_on_audit_failure: bool,
690    cap_time: RefinedTime,
691    name: &str,
692    until: &Antichain<Timestamp>,
693    map_filter_project: Option<&MfpPlan>,
694    datum_vec: &mut DatumVec,
695    row_builder: &mut Row,
696    work: &mut usize,
697    yield_fuel: usize,
698    mut give: F,
699) -> bool
700where
701    E: timely::ExchangeData + Ord + Clone + Debug + From<DataflowError> + From<EvalError>,
702    F: FnMut(Result<Cow<'_, Row>, E>, RefinedTime, Diff),
703{
704    let fetched_part = part.part_mut();
705    let is_filter_pushdown_audit = fetched_part.is_filter_pushdown_audit();
706    let mut row_buf = None;
707    while let Some(((key, val), time, diff)) =
708        fetched_part.next_with_storage(&mut row_buf, &mut None)
709    {
710        if until.less_equal(&time) {
711            continue;
712        }
713        match (key, val) {
714            (SourceData(Ok(row)), ()) => {
715                if let Some(mfp) = map_filter_project {
716                    // We originally accounted work as the number of outputs, to give downstream
717                    // operators a chance to reduce down anything we've emitted. This mfp call
718                    // might have a restrictive filter, which would have been counted as no
719                    // work. However, in practice, we've been decode_and_mfp be a source of
720                    // interactivity loss during rehydration, so we now also count each mfp
721                    // evaluation against our fuel.
722                    *work += 1;
723                    let arena = mz_repr::RowArena::new();
724                    let mut datums_local = datum_vec.borrow_with(&row);
725                    for result in mfp.evaluate(
726                        &mut datums_local,
727                        &arena,
728                        time,
729                        diff.into(),
730                        |time| !until.less_equal(time),
731                        row_builder,
732                    ) {
733                        // Earlier we decided this Part doesn't need to be fetched, but to
734                        // audit our logic we fetched it any way. If the MFP returned data it
735                        // means our earlier decision to not fetch this part was incorrect.
736                        if let Some(stats) = &is_filter_pushdown_audit {
737                            // NB: The tag added by this scope is used for alerting. The panic
738                            // message may be changed arbitrarily, but the tag key and val must
739                            // stay the same.
740                            sentry::with_scope(
741                                |scope| {
742                                    scope.set_tag("alert_id", "persist_pushdown_audit_violation")
743                                },
744                                || {
745                                    error!(
746                                        ?stats,
747                                        name,
748                                        mfp = ?redact(&mfp),
749                                        result = ?redact(&result),
750                                        "persist filter pushdown correctness violation!"
751                                    );
752                                    if panic_on_audit_failure {
753                                        panic!(
754                                            "persist filter pushdown correctness violation! {}",
755                                            name
756                                        );
757                                    }
758                                },
759                            );
760                        }
761                        match result {
762                            Ok((row, time, diff)) => {
763                                // Additional `until` filtering due to temporal filters.
764                                if !until.less_equal(&time) {
765                                    let mut emit_time = cap_time;
766                                    emit_time.0 = time;
767                                    give(Ok(Cow::Owned(row)), emit_time, diff);
768                                    *work += 1;
769                                }
770                            }
771                            Err((err, time, diff)) => {
772                                // Additional `until` filtering due to temporal filters.
773                                if !until.less_equal(&time) {
774                                    let mut emit_time = cap_time;
775                                    emit_time.0 = time;
776                                    give(Err(err), emit_time, diff);
777                                    *work += 1;
778                                }
779                            }
780                        }
781                    }
782                    // The MFP built its output into `row_builder`, so the decoded row's
783                    // allocation is free to go back to `row_buf`.
784                    drop(datums_local);
785                    row_buf.replace(SourceData(Ok(row)));
786                } else {
787                    let mut emit_time = cap_time;
788                    emit_time.0 = time;
789                    // The output copies the row out, so the allocation stays with `row_buf`.
790                    give(Ok(Cow::Borrowed(&row)), emit_time, diff.into());
791                    row_buf.replace(SourceData(Ok(row)));
792                    *work += 1;
793                }
794            }
795            (SourceData(Err(err)), ()) => {
796                // A discarded part that turns out to hold an error row is
797                // as much a pushdown violation as one whose MFP yields
798                // output: errors must surface regardless of any filter.
799                // Without this arm the audit was blind to exactly the
800                // undercounted-err-stats violation class.
801                if let Some(stats) = &is_filter_pushdown_audit {
802                    sentry::with_scope(
803                        |scope| scope.set_tag("alert_id", "persist_pushdown_audit_violation"),
804                        || {
805                            // `err` is redacted for the same reason the
806                            // `Ok`-row arm redacts its MFP output: these
807                            // events go to Sentry, and a `DecodeError`
808                            // carries the raw source record bytes while
809                            // several `EvalError`s embed user input.
810                            error!(
811                                ?stats,
812                                name,
813                                err = ?redact(&err),
814                                "persist filter pushdown correctness violation!"
815                            );
816                            if panic_on_audit_failure {
817                                panic!("persist filter pushdown correctness violation! {}", name);
818                            }
819                        },
820                    );
821                }
822                let mut emit_time = cap_time;
823                emit_time.0 = time;
824                give(Err(E::from(err)), emit_time, diff.into());
825                *work += 1;
826            }
827        }
828        if *work >= yield_fuel {
829            return false;
830        }
831    }
832    true
833}
834
835/// A trait representing a type that can be used in `backpressure`.
836pub trait Backpressureable: Clone + 'static {
837    /// Return the weight of the object, in bytes.
838    fn byte_size(&self) -> usize;
839}
840
841impl<T: Clone + 'static> Backpressureable for (usize, ExchangeableBatchPart<T>) {
842    fn byte_size(&self) -> usize {
843        self.1.encoded_size_bytes()
844    }
845}
846
847/// Flow control configuration.
848#[derive(Debug)]
849pub struct FlowControl<'scope, T: timely::progress::Timestamp> {
850    /// Stream providing in-flight frontier updates.
851    ///
852    /// As implied by its type, this stream never emits data, only progress updates.
853    ///
854    /// TODO: Replace `Infallible` with `!` once the latter is stabilized.
855    pub progress_stream: StreamVec<'scope, T, Infallible>,
856    /// Maximum number of in-flight bytes.
857    pub max_inflight_bytes: usize,
858    /// The minimum range of timestamps (be they granular or not) that must be emitted,
859    /// ignoring `max_inflight_bytes` to ensure forward progress is made.
860    pub summary: T::Summary,
861
862    /// Optional metrics for the `backpressure` operator to keep up-to-date.
863    pub metrics: Option<BackpressureOperatorMetrics>,
864}
865
866/// Apply flow control to the `data` input, based on the given `FlowControl`.
867///
868/// The `FlowControl` should have a `progress_stream` that is the pristine, unaltered
869/// frontier of the downstream operator we want to backpressure from, a `max_inflight_bytes`,
870/// and a `summary`. Note that the `data` input expects all the second part of the tuple
871/// timestamp to be 0, and all data to be on the `chosen_worker` worker.
872///
873/// The `summary` represents the _minimum_ range of timestamps that needs to be emitted before
874/// reasoning about `max_inflight_bytes`. In practice this means that we may overshoot
875/// `max_inflight_bytes`.
876///
877/// The implementation of this operator is very subtle. Many inline comments have been added.
878pub fn backpressure<'scope, T, O>(
879    scope: Scope<'scope, (T, Subtime)>,
880    name: &str,
881    data: StreamVec<'scope, (T, Subtime), O>,
882    flow_control: FlowControl<'scope, (T, Subtime)>,
883    chosen_worker: usize,
884    // A probe used to inspect this operator during unit-testing
885    probe: Option<UnboundedSender<(Antichain<(T, Subtime)>, usize, usize)>>,
886) -> (StreamVec<'scope, (T, Subtime), O>, PressOnDropButton)
887where
888    T: TimelyTimestamp + Lattice + Codec64 + TotalOrder,
889    O: Backpressureable + std::fmt::Debug,
890{
891    let worker_index = scope.index();
892
893    let (flow_control_stream, flow_control_max_bytes, metrics) = (
894        flow_control.progress_stream,
895        flow_control.max_inflight_bytes,
896        flow_control.metrics,
897    );
898
899    // Both the `flow_control` input and the data input are disconnected from the output. We manually
900    // manage the output's frontier using a `CapabilitySet`. Note that we also adjust the
901    // `flow_control` progress stream using the `summary` here, using a `feedback` operator in a
902    // non-circular fashion.
903    let (handle, summaried_flow) = scope.feedback(flow_control.summary.clone());
904    flow_control_stream.connect_loop(handle);
905
906    let mut builder = AsyncOperatorBuilder::new(
907        format!("persist_source_backpressure({})", name),
908        scope.clone(),
909    );
910    let (data_output, data_stream) = builder.new_output::<CapacityContainerBuilder<Vec<_>>>();
911
912    let mut data_input = builder.new_disconnected_input(data, Pipeline);
913    let mut flow_control_input = builder.new_disconnected_input(summaried_flow, Pipeline);
914
915    // Helper method used to synthesize current and next frontier for ordered times.
916    fn synthesize_frontiers<T: PartialOrder + Clone>(
917        mut frontier: Antichain<(T, Subtime)>,
918        mut time: (T, Subtime),
919        part_number: &mut u64,
920    ) -> (
921        (T, Subtime),
922        Antichain<(T, Subtime)>,
923        Antichain<(T, Subtime)>,
924    ) {
925        let mut next_frontier = frontier.clone();
926        time.1 = Subtime(*part_number);
927        frontier.insert(time.clone());
928        *part_number += 1;
929        let mut next_time = time.clone();
930        next_time.1 = Subtime(*part_number);
931        next_frontier.insert(next_time);
932        (time, frontier, next_frontier)
933    }
934
935    // _Refine_ the data stream by amending the second input with the part number. This also
936    // ensures that we order the parts by time.
937    let data_input = async_stream::stream!({
938        let mut part_number = 0;
939        let mut parts: Vec<((T, Subtime), O)> = Vec::new();
940        loop {
941            match data_input.next().await {
942                None => {
943                    let empty = Antichain::new();
944                    parts.sort_by_key(|val| val.0.clone());
945                    for (part_time, d) in parts.drain(..) {
946                        let (part_time, frontier, next_frontier) = synthesize_frontiers(
947                            empty.clone(),
948                            part_time.clone(),
949                            &mut part_number,
950                        );
951                        yield Either::Right((part_time, d, frontier, next_frontier))
952                    }
953                    break;
954                }
955                Some(Event::Data(time, data)) => {
956                    for d in data {
957                        parts.push((time.clone(), d));
958                    }
959                }
960                Some(Event::Progress(prog)) => {
961                    parts.sort_by_key(|val| val.0.clone());
962                    for (part_time, d) in parts.extract_if(.., |p| !prog.less_equal(&p.0)) {
963                        let (part_time, frontier, next_frontier) =
964                            synthesize_frontiers(prog.clone(), part_time.clone(), &mut part_number);
965                        yield Either::Right((part_time, d, frontier, next_frontier))
966                    }
967                    yield Either::Left(prog)
968                }
969            }
970        }
971    });
972    let shutdown_button = builder.build(move |caps| async move {
973        // The output capability.
974        let mut cap_set = CapabilitySet::from_elem(caps.into_element());
975
976        // The frontier of our output. This matches the `CapabilitySet` above.
977        let mut output_frontier = Antichain::from_elem(TimelyTimestamp::minimum());
978        // The frontier of the `flow_control` input.
979        let mut flow_control_frontier = Antichain::from_elem(TimelyTimestamp::minimum());
980
981        // Parts we have emitted, but have not yet retired (based on the `flow_control` edge).
982        let mut inflight_parts = Vec::new();
983        // Parts we have not yet emitted, but do participate in the `input_frontier`.
984        let mut pending_parts = std::collections::VecDeque::new();
985
986        // Only one worker is responsible for distributing parts
987        if worker_index != chosen_worker {
988            trace!(
989                "We are not the chosen worker ({}), exiting...",
990                chosen_worker
991            );
992            return;
993        }
994        tokio::pin!(data_input);
995        'emitting_parts: loop {
996            // At the beginning of our main loop, we determine the total size of
997            // inflight parts.
998            let inflight_bytes: usize = inflight_parts.iter().map(|(_, size)| size).sum();
999
1000            // There are 2 main cases where we can continue to emit parts:
1001            // - The total emitted bytes is less than `flow_control_max_bytes`.
1002            // - The output frontier is not beyond the `flow_control_frontier`
1003            //
1004            // SUBTLE: in the latter case, we may arbitrarily go into the backpressure `else`
1005            // block, as we wait for progress tracking to keep the `flow_control` frontier
1006            // up-to-date. This is tested in unit-tests.
1007            if inflight_bytes < flow_control_max_bytes
1008                || !PartialOrder::less_equal(&flow_control_frontier, &output_frontier)
1009            {
1010                let (time, part, next_frontier) =
1011                    if let Some((time, part, next_frontier)) = pending_parts.pop_front() {
1012                        (time, part, next_frontier)
1013                    } else {
1014                        match data_input.next().await {
1015                            Some(Either::Right((time, part, frontier, next_frontier))) => {
1016                                // Downgrade the output frontier to this part's time. This is useful
1017                                // "close" timestamp's from previous parts, even if we don't yet
1018                                // emit this part. Note that this is safe because `data_input` ensures
1019                                // time-ordering.
1020                                output_frontier = frontier;
1021                                cap_set.downgrade(output_frontier.iter());
1022
1023                                // If the most recent value's time is _beyond_ the
1024                                // `flow_control` frontier (which takes into account the `summary`), we
1025                                // have emitted an entire `summary` worth of data, and can store this
1026                                // value for later.
1027                                if inflight_bytes >= flow_control_max_bytes
1028                                    && !PartialOrder::less_than(
1029                                        &output_frontier,
1030                                        &flow_control_frontier,
1031                                    )
1032                                {
1033                                    pending_parts.push_back((time, part, next_frontier));
1034                                    continue 'emitting_parts;
1035                                }
1036                                (time, part, next_frontier)
1037                            }
1038                            Some(Either::Left(prog)) => {
1039                                output_frontier = prog;
1040                                cap_set.downgrade(output_frontier.iter());
1041                                continue 'emitting_parts;
1042                            }
1043                            None => {
1044                                if pending_parts.is_empty() {
1045                                    break 'emitting_parts;
1046                                } else {
1047                                    continue 'emitting_parts;
1048                                }
1049                            }
1050                        }
1051                    };
1052
1053                let byte_size = part.byte_size();
1054                // Store the value with the _frontier_ the `flow_control_input` must reach
1055                // to retire it. Note that if this `results_in` is `None`, then we
1056                // are at `T::MAX`, and give up on flow_control entirely.
1057                //
1058                // SUBTLE: If we stop storing these parts, we will likely never check the
1059                // `flow_control_input` ever again. This won't pile up data as that input
1060                // only has frontier updates. There may be spurious activations from it though.
1061                //
1062                // Also note that we don't attempt to handle overflowing the `u64` part counter.
1063                if let Some(emission_ts) = flow_control.summary.results_in(&time) {
1064                    inflight_parts.push((emission_ts, byte_size));
1065                }
1066
1067                // Emit the data at the given time, and update the frontier and capabilities
1068                // to just beyond the part.
1069                data_output.give(&cap_set.delayed(&time), part);
1070
1071                if let Some(metrics) = &metrics {
1072                    metrics.emitted_bytes.inc_by(u64::cast_from(byte_size))
1073                }
1074
1075                output_frontier = next_frontier;
1076                cap_set.downgrade(output_frontier.iter())
1077            } else {
1078                if let Some(metrics) = &metrics {
1079                    metrics
1080                        .last_backpressured_bytes
1081                        .set(u64::cast_from(inflight_bytes))
1082                }
1083                let parts_count = inflight_parts.len();
1084                // We've exhausted our budget, listen for updates to the flow_control
1085                // input's frontier until we free up new budget. If we don't interact with
1086                // with this side of the if statement, because the stream has no data, we
1087                // don't cause unbounded buffering in timely.
1088                let new_flow_control_frontier = match flow_control_input.next().await {
1089                    Some(Event::Progress(frontier)) => frontier,
1090                    Some(Event::Data(_, _)) => {
1091                        unreachable!("flow_control_input should not contain data")
1092                    }
1093                    None => Antichain::new(),
1094                };
1095
1096                // Update the `flow_control_frontier` if its advanced.
1097                flow_control_frontier.clone_from(&new_flow_control_frontier);
1098
1099                // Retire parts that are processed downstream.
1100                let retired_parts = inflight_parts
1101                    .extract_if(.., |(ts, _size)| !flow_control_frontier.less_equal(ts));
1102                let (retired_size, retired_count): (usize, usize) = retired_parts
1103                    .fold((0, 0), |(accum_size, accum_count), (_ts, size)| {
1104                        (accum_size + size, accum_count + 1)
1105                    });
1106                trace!(
1107                    "returning {} parts with {} bytes, frontier: {:?}",
1108                    retired_count, retired_size, flow_control_frontier,
1109                );
1110
1111                if let Some(metrics) = &metrics {
1112                    metrics.retired_bytes.inc_by(u64::cast_from(retired_size))
1113                }
1114
1115                // Optionally emit some information for tests to examine.
1116                if let Some(probe) = probe.as_ref() {
1117                    let _ = probe.send((new_flow_control_frontier, parts_count, retired_count));
1118                }
1119            }
1120        }
1121    });
1122    (data_stream, shutdown_button.press_on_drop())
1123}
1124
1125#[cfg(test)]
1126mod tests {
1127    use timely::container::CapacityContainerBuilder;
1128    use timely::dataflow::operators::{Enter, Probe};
1129    use tokio::sync::mpsc::unbounded_channel;
1130    use tokio::sync::oneshot;
1131
1132    use super::*;
1133
1134    #[mz_ore::test]
1135    fn test_backpressure_non_granular() {
1136        use Step::*;
1137        backpressure_runner(
1138            vec![(50, Part(101)), (50, Part(102)), (100, Part(1))],
1139            100,
1140            (1, Subtime(0)),
1141            vec![
1142                // Assert we backpressure only after we have emitted
1143                // the entire timestamp.
1144                AssertOutputFrontier((50, Subtime(2))),
1145                AssertBackpressured {
1146                    frontier: (1, Subtime(0)),
1147                    inflight_parts: 1,
1148                    retired_parts: 0,
1149                },
1150                AssertBackpressured {
1151                    frontier: (51, Subtime(0)),
1152                    inflight_parts: 1,
1153                    retired_parts: 0,
1154                },
1155                ProcessXParts(2),
1156                AssertBackpressured {
1157                    frontier: (101, Subtime(0)),
1158                    inflight_parts: 2,
1159                    retired_parts: 2,
1160                },
1161                // Assert we make later progress once processing
1162                // the parts.
1163                AssertOutputFrontier((100, Subtime(3))),
1164            ],
1165            true,
1166        );
1167
1168        backpressure_runner(
1169            vec![
1170                (50, Part(10)),
1171                (50, Part(10)),
1172                (51, Part(100)),
1173                (52, Part(1000)),
1174            ],
1175            50,
1176            (1, Subtime(0)),
1177            vec![
1178                // Assert we backpressure only after we emitted enough bytes
1179                AssertOutputFrontier((51, Subtime(3))),
1180                AssertBackpressured {
1181                    frontier: (1, Subtime(0)),
1182                    inflight_parts: 3,
1183                    retired_parts: 0,
1184                },
1185                ProcessXParts(3),
1186                AssertBackpressured {
1187                    frontier: (52, Subtime(0)),
1188                    inflight_parts: 3,
1189                    retired_parts: 2,
1190                },
1191                AssertBackpressured {
1192                    frontier: (53, Subtime(0)),
1193                    inflight_parts: 1,
1194                    retired_parts: 1,
1195                },
1196                // Assert we make later progress once processing
1197                // the parts.
1198                AssertOutputFrontier((52, Subtime(4))),
1199            ],
1200            true,
1201        );
1202
1203        backpressure_runner(
1204            vec![
1205                (50, Part(98)),
1206                (50, Part(1)),
1207                (51, Part(10)),
1208                (52, Part(100)),
1209                // Additional parts at the same timestamp
1210                (52, Part(10)),
1211                (52, Part(10)),
1212                (52, Part(10)),
1213                (52, Part(100)),
1214                // A later part with a later ts.
1215                (100, Part(100)),
1216            ],
1217            100,
1218            (1, Subtime(0)),
1219            vec![
1220                AssertOutputFrontier((51, Subtime(3))),
1221                // Assert we backpressure after we have emitted enough bytes.
1222                // We assert twice here because we get updates as
1223                // `flow_control` progresses from `(0, 0)`->`(0, 1)`-> a real frontier.
1224                AssertBackpressured {
1225                    frontier: (1, Subtime(0)),
1226                    inflight_parts: 3,
1227                    retired_parts: 0,
1228                },
1229                AssertBackpressured {
1230                    frontier: (51, Subtime(0)),
1231                    inflight_parts: 3,
1232                    retired_parts: 0,
1233                },
1234                ProcessXParts(1),
1235                // Our output frontier doesn't move, as the downstream frontier hasn't moved past
1236                // 50.
1237                AssertOutputFrontier((51, Subtime(3))),
1238                // After we process all of `50`, we can start emitting data at `52`, but only until
1239                // we exhaust out budget. We don't need to emit all of `52` because we have emitted
1240                // all of `51`.
1241                ProcessXParts(1),
1242                AssertOutputFrontier((52, Subtime(4))),
1243                AssertBackpressured {
1244                    frontier: (52, Subtime(0)),
1245                    inflight_parts: 3,
1246                    retired_parts: 2,
1247                },
1248                // After processing `50` and `51`, the minimum time is `52`, so we ensure that,
1249                // regardless of byte count, we emit the entire time (but do NOT emit the part at
1250                // time `100`.
1251                ProcessXParts(1),
1252                // Clear the previous `51` part, and start filling up `inflight_parts` with other
1253                // parts at `52`
1254                // This is an intermediate state.
1255                AssertBackpressured {
1256                    frontier: (53, Subtime(0)),
1257                    inflight_parts: 2,
1258                    retired_parts: 1,
1259                },
1260                // After we process all of `52`, we can continue to the next time.
1261                ProcessXParts(5),
1262                AssertBackpressured {
1263                    frontier: (101, Subtime(0)),
1264                    inflight_parts: 5,
1265                    retired_parts: 5,
1266                },
1267                AssertOutputFrontier((100, Subtime(9))),
1268            ],
1269            true,
1270        );
1271    }
1272
1273    #[mz_ore::test]
1274    fn test_backpressure_granular() {
1275        use Step::*;
1276        backpressure_runner(
1277            vec![(50, Part(101)), (50, Part(101))],
1278            100,
1279            (0, Subtime(1)),
1280            vec![
1281                // Advance our frontier to outputting a single part.
1282                AssertOutputFrontier((50, Subtime(1))),
1283                // Receive backpressure updates until our frontier is up-to-date but
1284                // not beyond the parts (while considering the summary).
1285                AssertBackpressured {
1286                    frontier: (0, Subtime(1)),
1287                    inflight_parts: 1,
1288                    retired_parts: 0,
1289                },
1290                AssertBackpressured {
1291                    frontier: (50, Subtime(1)),
1292                    inflight_parts: 1,
1293                    retired_parts: 0,
1294                },
1295                // Process that part.
1296                ProcessXParts(1),
1297                // Assert that we clear the backpressure status
1298                AssertBackpressured {
1299                    frontier: (50, Subtime(2)),
1300                    inflight_parts: 1,
1301                    retired_parts: 1,
1302                },
1303                // Ensure we make progress to the next part.
1304                AssertOutputFrontier((50, Subtime(2))),
1305            ],
1306            false,
1307        );
1308
1309        backpressure_runner(
1310            vec![
1311                (50, Part(10)),
1312                (50, Part(10)),
1313                (51, Part(35)),
1314                (52, Part(100)),
1315            ],
1316            50,
1317            (0, Subtime(1)),
1318            vec![
1319                // we can emit 3 parts before we hit the backpressure limit
1320                AssertOutputFrontier((51, Subtime(3))),
1321                AssertBackpressured {
1322                    frontier: (0, Subtime(1)),
1323                    inflight_parts: 3,
1324                    retired_parts: 0,
1325                },
1326                AssertBackpressured {
1327                    frontier: (50, Subtime(1)),
1328                    inflight_parts: 3,
1329                    retired_parts: 0,
1330                },
1331                // Retire the single part.
1332                ProcessXParts(1),
1333                AssertBackpressured {
1334                    frontier: (50, Subtime(2)),
1335                    inflight_parts: 3,
1336                    retired_parts: 1,
1337                },
1338                // Ensure we make progress, and then
1339                // can retire the next 2 parts.
1340                AssertOutputFrontier((52, Subtime(4))),
1341                ProcessXParts(2),
1342                AssertBackpressured {
1343                    frontier: (52, Subtime(4)),
1344                    inflight_parts: 3,
1345                    retired_parts: 2,
1346                },
1347            ],
1348            false,
1349        );
1350    }
1351
1352    type Time = (u64, Subtime);
1353    #[derive(Clone, Debug)]
1354    struct Part(usize);
1355    impl Backpressureable for Part {
1356        fn byte_size(&self) -> usize {
1357            self.0
1358        }
1359    }
1360
1361    /// Actions taken by `backpressure_runner`.
1362    enum Step {
1363        /// Assert that the output frontier of the `backpressure` operator has AT LEAST made it
1364        /// this far. This is a single time because we assume
1365        AssertOutputFrontier(Time),
1366        /// Assert that we have entered the backpressure flow in the `backpressure` operator. This
1367        /// allows us to assert what feedback frontier we got to, and how many inflight parts we
1368        /// retired.
1369        AssertBackpressured {
1370            frontier: Time,
1371            inflight_parts: usize,
1372            retired_parts: usize,
1373        },
1374        /// Process X parts in the downstream operator. This affects the feedback frontier.
1375        ProcessXParts(usize),
1376    }
1377
1378    /// A function that runs the `steps` to ensure that `backpressure` works as expected.
1379    fn backpressure_runner(
1380        // The input data to the `backpressure` operator
1381        input: Vec<(u64, Part)>,
1382        // The maximum inflight bytes the `backpressure` operator allows through.
1383        max_inflight_bytes: usize,
1384        // The feedback summary used by the `backpressure` operator.
1385        summary: Time,
1386        // List of steps to run through.
1387        steps: Vec<Step>,
1388        // Whether or not to consume records in the non-granular scope. This is useful when the
1389        // `summary` is something like `(1, 0)`.
1390        non_granular_consumer: bool,
1391    ) {
1392        timely::execute::execute_directly(move |worker| {
1393            let (
1394                backpressure_probe,
1395                consumer_tx,
1396                mut backpressure_status_rx,
1397                finalizer_tx,
1398                _token,
1399            ) =
1400                // Set up the top-level non-granular scope.
1401                worker.dataflow::<u64, _, _>(|outer_scope| {
1402                    let (non_granular_feedback_handle, non_granular_feedback) =
1403                        if non_granular_consumer {
1404                            let (h, f) = outer_scope.feedback(Default::default());
1405                            (Some(h), Some(f))
1406                        } else {
1407                            (None, None)
1408                        };
1409                    let (
1410                        backpressure_probe,
1411                        consumer_tx,
1412                        backpressure_status_rx,
1413                        token,
1414                        backpressured,
1415                        finalizer_tx,
1416                    ) = outer_scope.scoped::<(u64, Subtime), _, _>("hybrid", |scope| {
1417                        let (input, finalizer_tx) =
1418                            iterator_operator(scope.clone(), input.into_iter());
1419
1420                        let (flow_control, granular_feedback_handle) = if non_granular_consumer {
1421                            (
1422                                FlowControl {
1423                                    progress_stream: non_granular_feedback.unwrap().enter(scope),
1424                                    max_inflight_bytes,
1425                                    summary,
1426                                    metrics: None
1427                                },
1428                                None,
1429                            )
1430                        } else {
1431                            let (granular_feedback_handle, granular_feedback) =
1432                                scope.feedback(Default::default());
1433                            (
1434                                FlowControl {
1435                                    progress_stream: granular_feedback,
1436                                    max_inflight_bytes,
1437                                    summary,
1438                                    metrics: None,
1439                                },
1440                                Some(granular_feedback_handle),
1441                            )
1442                        };
1443
1444                        let (backpressure_status_tx, backpressure_status_rx) = unbounded_channel();
1445
1446                        let (backpressured, token) = backpressure(
1447                            scope,
1448                            "test",
1449                            input,
1450                            flow_control,
1451                            0,
1452                            Some(backpressure_status_tx),
1453                        );
1454
1455                        // If we want to granularly consume the output, we setup the consumer here.
1456                        let tx = if !non_granular_consumer {
1457                            Some(consumer_operator(
1458                                scope.clone(),
1459                                backpressured.clone(),
1460                                granular_feedback_handle.unwrap(),
1461                            ))
1462                        } else {
1463                            None
1464                        };
1465
1466                        let (probe_handle, backpressured) = backpressured.probe();
1467                        (
1468                            probe_handle,
1469                            tx,
1470                            backpressure_status_rx,
1471                            token,
1472                            backpressured.leave(outer_scope),
1473                            finalizer_tx,
1474                        )
1475                    });
1476
1477                    // If we want to non-granularly consume the output, we setup the consumer here.
1478                    let consumer_tx = if non_granular_consumer {
1479                        consumer_operator(
1480                            outer_scope.clone(),
1481                            backpressured,
1482                            non_granular_feedback_handle.unwrap(),
1483                        )
1484                    } else {
1485                        consumer_tx.unwrap()
1486                    };
1487
1488                    (
1489                        backpressure_probe,
1490                        consumer_tx,
1491                        backpressure_status_rx,
1492                        finalizer_tx,
1493                        token,
1494                    )
1495                });
1496
1497            use Step::*;
1498            for step in steps {
1499                match step {
1500                    AssertOutputFrontier(time) => {
1501                        eprintln!("checking advance to {time:?}");
1502                        backpressure_probe.with_frontier(|front| {
1503                            eprintln!("current backpressure output frontier: {front:?}");
1504                        });
1505                        while backpressure_probe.less_than(&time) {
1506                            worker.step();
1507                            backpressure_probe.with_frontier(|front| {
1508                                eprintln!("current backpressure output frontier: {front:?}");
1509                            });
1510                            std::thread::sleep(std::time::Duration::from_millis(25));
1511                        }
1512                    }
1513                    ProcessXParts(parts) => {
1514                        eprintln!("processing {parts:?} parts");
1515                        for _ in 0..parts {
1516                            consumer_tx.send(()).unwrap();
1517                        }
1518                    }
1519                    AssertBackpressured {
1520                        frontier,
1521                        inflight_parts,
1522                        retired_parts,
1523                    } => {
1524                        let frontier = Antichain::from_elem(frontier);
1525                        eprintln!(
1526                            "asserting backpressured at {frontier:?}, with {inflight_parts:?} inflight parts \
1527                            and {retired_parts:?} retired"
1528                        );
1529                        let (new_frontier, new_count, new_retired_count) = loop {
1530                            if let Ok(val) = backpressure_status_rx.try_recv() {
1531                                break val;
1532                            }
1533                            worker.step();
1534                            std::thread::sleep(std::time::Duration::from_millis(25));
1535                        };
1536                        assert_eq!(
1537                            (frontier, inflight_parts, retired_parts),
1538                            (new_frontier, new_count, new_retired_count)
1539                        );
1540                    }
1541                }
1542            }
1543            // Send the input to the empty frontier.
1544            let _ = finalizer_tx.send(());
1545        });
1546    }
1547
1548    /// An operator that emits `Part`'s at the specified timestamps. Does not
1549    /// drop its capability until it gets a signal from the `Sender` it returns.
1550    fn iterator_operator<'scope, I: Iterator<Item = (u64, Part)> + 'static>(
1551        scope: Scope<'scope, (u64, Subtime)>,
1552        mut input: I,
1553    ) -> (StreamVec<'scope, (u64, Subtime), Part>, oneshot::Sender<()>) {
1554        let (finalizer_tx, finalizer_rx) = oneshot::channel();
1555        let mut iterator = AsyncOperatorBuilder::new("iterator".to_string(), scope);
1556        let (output_handle, output) = iterator.new_output::<CapacityContainerBuilder<Vec<Part>>>();
1557
1558        iterator.build(|mut caps| async move {
1559            let mut capability = Some(caps.pop().unwrap());
1560            let mut last = None;
1561            while let Some(element) = input.next() {
1562                let time = element.0.clone();
1563                let part = element.1;
1564                last = Some((time, Subtime(0)));
1565                output_handle.give(&capability.as_ref().unwrap().delayed(&last.unwrap()), part);
1566            }
1567            if let Some(last) = last {
1568                capability
1569                    .as_mut()
1570                    .unwrap()
1571                    .downgrade(&(last.0 + 1, last.1));
1572            }
1573
1574            let _ = finalizer_rx.await;
1575            capability.take();
1576        });
1577
1578        (output, finalizer_tx)
1579    }
1580
1581    /// An operator that consumes its input ONLY when given a signal to do from
1582    /// the `UnboundedSender` it returns. Each `send` corresponds with 1 `Data` event
1583    /// being processed. Also connects the `feedback` handle to its output.
1584    fn consumer_operator<
1585        'scope,
1586        T: timely::progress::Timestamp,
1587        O: Backpressureable + std::fmt::Debug,
1588    >(
1589        scope: Scope<'scope, T>,
1590        input: StreamVec<'scope, T, O>,
1591        feedback: timely::dataflow::operators::feedback::Handle<
1592            'scope,
1593            T,
1594            Vec<std::convert::Infallible>,
1595        >,
1596    ) -> UnboundedSender<()> {
1597        let (tx, mut rx) = unbounded_channel::<()>();
1598        let mut consumer = AsyncOperatorBuilder::new("consumer".to_string(), scope);
1599        let (output_handle, output) =
1600            consumer.new_output::<CapacityContainerBuilder<Vec<std::convert::Infallible>>>();
1601        let mut input = consumer.new_input_for(input, Pipeline, &output_handle);
1602
1603        consumer.build(|_caps| async move {
1604            while let Some(()) = rx.recv().await {
1605                // Consume exactly one messages (unless the input is exhausted).
1606                while let Some(Event::Progress(_)) = input.next().await {}
1607            }
1608        });
1609        output.connect_loop(feedback);
1610
1611        tx
1612    }
1613
1614    /// End-to-end persist filter-pushdown soundness checks.
1615    ///
1616    /// These mirror the real audit contract in [`PendingWork::do_work`]: build
1617    /// a part from actual rows, compute the *real* production column statistics
1618    /// from it, then run [`filter_result`] and assert it never returns
1619    /// [`FilterResult::Discard`] for a part whose MFP would produce output (an
1620    /// `Ok` row or an error row) on some actual row. A `Discard` in that case is
1621    /// exactly the wrongly-skipped part the runtime audit panics on.
1622    ///
1623    /// Unlike the interpreter-level proptests in `mz_expr::interpret`, this
1624    /// exercises the full path stats derivation -> `col_stats` -> interpreter,
1625    /// so it catches both interpreter bugs and stats-derivation bugs (e.g. a
1626    /// column stat range that fails to contain a real value). See
1627    /// database-issues#9656 / PER-50.
1628    mod filter_pushdown_audit {
1629        use itertools::Itertools;
1630        use mz_expr::func::variadic::{And, Or};
1631        use mz_expr::func::{
1632            AddFloat32, AddTimestampInterval, CastNumericToFloat32, CastNumericToMzTimestamp, Eq,
1633            Gt, Gte, IsNull, JsonbGetString, JsonbGetStringStringify, Lt, Lte, MulFloat32,
1634            MulFloat64, Not, RoundNumericBinary, TryParseMonotonicIso8601Timestamp,
1635        };
1636        use mz_expr::{BinaryFunc, MapFilterProject, MirScalarExpr, UnaryFunc};
1637        use mz_ore::metrics::MetricsRegistry;
1638        use mz_persist_types::part::PartBuilder;
1639        use mz_persist_types::stats::{PartStats, PartStatsMetrics};
1640        use mz_repr::adt::interval::Interval;
1641        use mz_repr::adt::numeric::Numeric;
1642        use mz_repr::{Diff, ReprScalarType, SqlScalarType};
1643        use proptest::prelude::*;
1644        use proptest::sample::{Index, select};
1645        use proptest::strategy::Union;
1646
1647        use super::*;
1648
1649        fn f32_lit(x: f32) -> MirScalarExpr {
1650            MirScalarExpr::literal_ok(Datum::from(x), ReprScalarType::Float32)
1651        }
1652
1653        fn f64_lit(x: f64) -> MirScalarExpr {
1654            MirScalarExpr::literal_ok(Datum::from(x), ReprScalarType::Float64)
1655        }
1656
1657        fn numeric_datum(x: f64) -> Datum<'static> {
1658            Datum::from(Numeric::from(x))
1659        }
1660
1661        fn numeric_desc() -> RelationDesc {
1662            RelationDesc::builder()
1663                .with_column(
1664                    "c0",
1665                    SqlScalarType::Numeric { max_scale: None }.nullable(false),
1666                )
1667                .finish()
1668        }
1669
1670        /// Compute the real production `PartStats` from a set of rows, the same
1671        /// way the storage read path does.
1672        fn build_part_stats(desc: &RelationDesc, rows: &[SourceData]) -> PartStats {
1673            let mut builder = PartBuilder::new(desc, &UnitSchema);
1674            for row in rows {
1675                builder.push(row, &(), 1u64, 1i64);
1676            }
1677            let part = builder.finish();
1678            PartStats::new::<SourceData, RelationDesc>(&part, desc).expect("stats")
1679        }
1680
1681        /// Ground truth: does the MFP produce any output (an `Ok` row that
1682        /// passes the filter, or an error row) on at least one actual row? If
1683        /// so, the part must not be discarded.
1684        fn mfp_yields_output(plan: &MfpPlan, rows: &[Row]) -> bool {
1685            let arena = RowArena::new();
1686            let mut row_builder = Row::default();
1687            for row in rows {
1688                let mut datums: Vec<Datum> = row.iter().collect();
1689                let mut results = plan.evaluate::<DataflowError, _>(
1690                    &mut datums,
1691                    &arena,
1692                    Timestamp::MIN,
1693                    Diff::from(1),
1694                    |_| true,
1695                    &mut row_builder,
1696                );
1697                if results.next().is_some() {
1698                    return true;
1699                }
1700            }
1701            false
1702        }
1703
1704        fn comparison_funcs() -> impl Strategy<Value = BinaryFunc> {
1705            select(vec![
1706                BinaryFunc::Lte(Lte),
1707                BinaryFunc::Lt(Lt),
1708                BinaryFunc::Gte(Gte),
1709                BinaryFunc::Gt(Gt),
1710                BinaryFunc::Eq(Eq),
1711            ])
1712        }
1713
1714        fn f32_consts() -> impl Strategy<Value = f32> {
1715            select(vec![
1716                0.0f32,
1717                1.0,
1718                -1.0,
1719                0.0087531805,
1720                745213.56,
1721                76700000000.0,
1722                f32::MAX,
1723                1e30,
1724                -1e30,
1725            ])
1726        }
1727
1728        /// `(a::float4 + round(c0, scale)::float4) * b::float4 <cmp> c::float4`.
1729        /// `round` overflows and the float arithmetic can produce an error or a
1730        /// NaN on interior values of `c0` that the endpoints don't reveal.
1731        fn float_arith_predicate(
1732            scale: i32,
1733            a: f32,
1734            b: f32,
1735            c: f32,
1736            cmp: BinaryFunc,
1737        ) -> MirScalarExpr {
1738            let round = MirScalarExpr::CallBinary {
1739                func: BinaryFunc::RoundNumeric(RoundNumericBinary),
1740                expr1: Box::new(MirScalarExpr::column(0)),
1741                expr2: Box::new(MirScalarExpr::literal_ok(
1742                    Datum::from(scale),
1743                    ReprScalarType::Int32,
1744                )),
1745            };
1746            let cast = MirScalarExpr::CallUnary {
1747                func: UnaryFunc::CastNumericToFloat32(CastNumericToFloat32),
1748                expr: Box::new(round),
1749            };
1750            let add = MirScalarExpr::CallBinary {
1751                func: BinaryFunc::AddFloat32(AddFloat32),
1752                expr1: Box::new(f32_lit(a)),
1753                expr2: Box::new(cast),
1754            };
1755            let mul = MirScalarExpr::CallBinary {
1756                func: BinaryFunc::MulFloat32(MulFloat32),
1757                expr1: Box::new(add),
1758                expr2: Box::new(f32_lit(b)),
1759            };
1760            MirScalarExpr::CallBinary {
1761                func: cmp,
1762                expr1: Box::new(mul),
1763                expr2: Box::new(f32_lit(c)),
1764            }
1765        }
1766
1767        /// `c0::mz_timestamp <cmp> <ts>`. The cast errors on fractional or
1768        /// out-of-range `c0`, an interior-error condition the interpreter's
1769        /// endpoint sampling can miss.
1770        fn cast_mz_timestamp_predicate(ts: u64, cmp: BinaryFunc) -> MirScalarExpr {
1771            let cast = MirScalarExpr::CallUnary {
1772                func: UnaryFunc::CastNumericToMzTimestamp(CastNumericToMzTimestamp),
1773                expr: Box::new(MirScalarExpr::column(0)),
1774            };
1775            MirScalarExpr::CallBinary {
1776                func: cmp,
1777                expr1: Box::new(cast),
1778                expr2: Box::new(MirScalarExpr::literal_ok(
1779                    Datum::MzTimestamp(Timestamp::from(ts)),
1780                    ReprScalarType::MzTimestamp,
1781                )),
1782            }
1783        }
1784
1785        fn arb_numeric_rows() -> impl Strategy<Value = Vec<Row>> {
1786            let magnitudes = vec![
1787                0.0f64, 1.0, 2.0, 1.5, 2.5, 0.25, -1.0, 10.0, 100.0, 1e10, -1e10, 3.0e38, 3.4e38,
1788                3.5e38, 1e40, -1e40, 1e300,
1789            ];
1790            prop::collection::vec(
1791                select(magnitudes).prop_map(|x| Row::pack_slice(&[numeric_datum(x)])),
1792                1..8,
1793            )
1794        }
1795
1796        fn arb_predicate() -> impl Strategy<Value = MirScalarExpr> {
1797            let float_arith = (
1798                select(vec![0i32, 2, -5, 24699]),
1799                f32_consts(),
1800                f32_consts(),
1801                f32_consts(),
1802                comparison_funcs(),
1803            )
1804                .prop_map(|(scale, a, b, c, cmp)| float_arith_predicate(scale, a, b, c, cmp));
1805            let cast_ts = (select(vec![0u64, 1, 2, 100, u64::MAX]), comparison_funcs())
1806                .prop_map(|(ts, cmp)| cast_mz_timestamp_predicate(ts, cmp));
1807            proptest::strategy::Union::new(vec![float_arith.boxed(), cast_ts.boxed()])
1808        }
1809
1810        #[mz_ore::test]
1811        #[cfg_attr(miri, ignore)] // error: unsupported operation: can't call foreign function `decContextDefault`
1812        fn filter_result_never_discards_matching_part() {
1813            fn check(rows: Vec<Row>, predicate: MirScalarExpr) -> Result<(), TestCaseError> {
1814                let desc = numeric_desc();
1815                let plan = MapFilterProject::new(1)
1816                    .filter(std::iter::once(predicate))
1817                    .into_plan()
1818                    .expect("into_plan");
1819
1820                let source_rows: Vec<_> = rows.iter().map(|r| SourceData(Ok(r.clone()))).collect();
1821                let part_stats = build_part_stats(&desc, &source_rows);
1822                let metrics = PartStatsMetrics::new(&MetricsRegistry::new());
1823                let stats = RelationPartStats::new("test", &metrics, &desc, &part_stats);
1824
1825                let decision = filter_result(&desc, ResultSpec::anything(), stats, &plan);
1826
1827                if mfp_yields_output(&plan, &rows) {
1828                    prop_assert!(
1829                        !matches!(decision, FilterResult::Discard),
1830                        "filter pushdown discarded a part whose MFP yields output on a real \
1831                         row (wrongly-skipped part; the runtime audit would panic).\n\
1832                         rows={rows:?}\nplan={plan:?}",
1833                    );
1834                }
1835                Ok(())
1836            }
1837
1838            proptest!(|(rows in arb_numeric_rows(), predicate in arb_predicate())| {
1839                check(rows, predicate)?;
1840            });
1841        }
1842
1843        /// Assert that `filter_result` keeps a part it must keep, and that the
1844        /// case is not vacuous: the MFP has to yield output on a real row for
1845        /// "must keep" to mean anything.
1846        fn assert_part_kept(desc: &RelationDesc, rows: &[Row], predicate: MirScalarExpr) {
1847            let plan = MapFilterProject::new(1)
1848                .filter(std::iter::once(predicate))
1849                .into_plan()
1850                .expect("into_plan");
1851            assert!(
1852                mfp_yields_output(&plan, rows),
1853                "nothing to keep: the MFP yields no output on any of these rows.\n\
1854                 rows={rows:?}\nplan={plan:?}",
1855            );
1856
1857            let source_rows: Vec<_> = rows.iter().map(|r| SourceData(Ok(r.clone()))).collect();
1858            let part_stats = build_part_stats(desc, &source_rows);
1859            let metrics = PartStatsMetrics::new(&MetricsRegistry::new());
1860            let stats = RelationPartStats::new("test", &metrics, desc, &part_stats);
1861            let decision = filter_result(desc, ResultSpec::anything(), stats, &plan);
1862            assert!(
1863                !matches!(decision, FilterResult::Discard),
1864                "filter pushdown discarded a part whose MFP yields output on a real row.\n\
1865                 rows={rows:?}\nplan={plan:?}",
1866            );
1867        }
1868
1869        /// A part holding `-NaN` must not hide the rest of its rows from a
1870        /// filter on that column.
1871        ///
1872        /// `PrimitiveStats` takes a float column's bounds in arrow's *total*
1873        /// order, where `-NaN` sorts below `-Infinity`, so such a part records
1874        /// `lower = -NaN` against a finite `upper`. `OrderedFloat`, the `Datum`
1875        /// order the interpreter compares in, ranks every NaN *above* every
1876        /// finite value, so those bounds arrive unordered. Reading them as an
1877        /// empty range discarded the part, which lost every other row in it
1878        /// (PER-53). Both float widths take the same path.
1879        #[mz_ore::test]
1880        #[cfg_attr(miri, ignore)] // unsupported operation: can't call foreign function
1881        fn negative_nan_does_not_discard_matching_part() {
1882            // Both rows have to land in the same part. Split across parts each
1883            // one gets ordered bounds of its own and nothing is discarded.
1884            let desc = RelationDesc::builder()
1885                .with_column("c0", SqlScalarType::Float32.nullable(false))
1886                .finish();
1887            let rows = [
1888                Row::pack_slice(&[Datum::from(-f32::NAN)]),
1889                Row::pack_slice(&[Datum::from(0.0f32)]),
1890            ];
1891            assert_part_kept(
1892                &desc,
1893                &rows,
1894                MirScalarExpr::CallBinary {
1895                    func: BinaryFunc::Lt(Lt),
1896                    expr1: Box::new(MirScalarExpr::column(0)),
1897                    expr2: Box::new(f32_lit(1.0)),
1898                },
1899            );
1900
1901            let desc = RelationDesc::builder()
1902                .with_column("c0", SqlScalarType::Float64.nullable(false))
1903                .finish();
1904            let rows = [
1905                Row::pack_slice(&[Datum::from(-f64::NAN)]),
1906                Row::pack_slice(&[Datum::from(0.0f64)]),
1907            ];
1908            assert_part_kept(
1909                &desc,
1910                &rows,
1911                MirScalarExpr::CallBinary {
1912                    func: BinaryFunc::Lt(Lt),
1913                    expr1: Box::new(MirScalarExpr::column(0)),
1914                    expr2: Box::new(f64_lit(1.0)),
1915                },
1916            );
1917        }
1918
1919        /// A part holding NaNs of both signs must not hide its other rows.
1920        ///
1921        /// Arrow's total order puts `-NaN` below and `+NaN` above everything,
1922        /// so such a part records the bounds `(-NaN, +NaN)`. Under
1923        /// `OrderedFloat` those decode to `NaN == NaN`: not an inverted range
1924        /// but a seemingly valid one claiming the part holds nothing but NaN,
1925        /// so widening unordered bounds does not catch it. A filter that a
1926        /// finite row matches but NaN does not then discarded the part. Only
1927        /// the stats decode still sees the NaN signs, so the guard lives in
1928        /// `mz_repr::stats::col_values`.
1929        #[mz_ore::test]
1930        #[cfg_attr(miri, ignore)] // unsupported operation: can't call foreign function
1931        fn mixed_sign_nans_do_not_discard_matching_part() {
1932            let desc = RelationDesc::builder()
1933                .with_column("c0", SqlScalarType::Float32.nullable(false))
1934                .finish();
1935            let rows = [
1936                Row::pack_slice(&[Datum::from(-f32::NAN)]),
1937                Row::pack_slice(&[Datum::from(f32::NAN)]),
1938                Row::pack_slice(&[Datum::from(0.0f32)]),
1939            ];
1940            assert_part_kept(
1941                &desc,
1942                &rows,
1943                MirScalarExpr::CallBinary {
1944                    func: BinaryFunc::Eq(Eq),
1945                    expr1: Box::new(MirScalarExpr::column(0)),
1946                    expr2: Box::new(f32_lit(0.0)),
1947                },
1948            );
1949
1950            let desc = RelationDesc::builder()
1951                .with_column("c0", SqlScalarType::Float64.nullable(false))
1952                .finish();
1953            let rows = [
1954                Row::pack_slice(&[Datum::from(-f64::NAN)]),
1955                Row::pack_slice(&[Datum::from(f64::NAN)]),
1956                Row::pack_slice(&[Datum::from(0.0f64)]),
1957            ];
1958            assert_part_kept(
1959                &desc,
1960                &rows,
1961                MirScalarExpr::CallBinary {
1962                    func: BinaryFunc::Eq(Eq),
1963                    expr1: Box::new(MirScalarExpr::column(0)),
1964                    expr2: Box::new(f64_lit(0.0)),
1965                },
1966            );
1967        }
1968
1969        // Wide-schema variant: multiple column types populated from
1970        // `interesting_datums`, a predicate vocabulary that reaches the
1971        // interpreter's special cases (jsonb map specs and their unions,
1972        // `TryParseMonotonicIso8601Timestamp`, dynamically-monotone
1973        // timestamp+interval, the infinity guard on float multiplication),
1974        // Err rows, and real mz_now bounds instead of an unconstrained time
1975        // range.
1976
1977        const NUM: usize = 0;
1978        const F32: usize = 1;
1979        const F64: usize = 2;
1980        const STR: usize = 3;
1981        const J1: usize = 4;
1982        const J2: usize = 5;
1983        const BOOL: usize = 6;
1984        const TS: usize = 7;
1985        const MZTS: usize = 8;
1986        const WIDE_ARITY: usize = 9;
1987
1988        fn wide_scalar_type(col: usize) -> SqlScalarType {
1989            match col {
1990                NUM => SqlScalarType::Numeric { max_scale: None },
1991                F32 => SqlScalarType::Float32,
1992                F64 => SqlScalarType::Float64,
1993                STR => SqlScalarType::String,
1994                J1 | J2 => SqlScalarType::Jsonb,
1995                BOOL => SqlScalarType::Bool,
1996                TS => SqlScalarType::Timestamp { precision: None },
1997                MZTS => SqlScalarType::MzTimestamp,
1998                _ => unreachable!("no such column"),
1999            }
2000        }
2001
2002        fn wide_repr_type(col: usize) -> ReprScalarType {
2003            match col {
2004                NUM => ReprScalarType::Numeric,
2005                F32 => ReprScalarType::Float32,
2006                F64 => ReprScalarType::Float64,
2007                STR => ReprScalarType::String,
2008                J1 | J2 => ReprScalarType::Jsonb,
2009                BOOL => ReprScalarType::Bool,
2010                TS => ReprScalarType::Timestamp,
2011                MZTS => ReprScalarType::MzTimestamp,
2012                _ => unreachable!("no such column"),
2013            }
2014        }
2015
2016        fn wide_desc() -> RelationDesc {
2017            let mut builder = RelationDesc::builder();
2018            for col in 0..WIDE_ARITY {
2019                // The bool column stays non-nullable so an all-Err part
2020                // exercises the fabricated default bounds a non-nullable
2021                // column gets when no Ok row provides a value.
2022                let nullable = col != BOOL;
2023                builder = builder
2024                    .with_column(format!("c{col}"), wide_scalar_type(col).nullable(nullable));
2025            }
2026            builder.finish()
2027        }
2028
2029        fn wide_pool(col: usize) -> Vec<Datum<'static>> {
2030            let mut pool: Vec<_> = wide_scalar_type(col).interesting_datums().collect();
2031            if col != BOOL {
2032                pool.push(Datum::Null);
2033            }
2034            pool
2035        }
2036
2037        fn arb_wide_rows() -> impl Strategy<Value = Vec<SourceData>> {
2038            let pools: Vec<Vec<Datum<'static>>> = (0..WIDE_ARITY).map(wide_pool).collect();
2039            let ok_row = prop::collection::vec(any::<Index>(), WIDE_ARITY).prop_map(move |picks| {
2040                let datums = picks
2041                    .iter()
2042                    .zip_eq(&pools)
2043                    .map(|(pick, pool)| pool[pick.index(pool.len())]);
2044                SourceData(Ok(Row::pack(datums)))
2045            });
2046            let err_row = Just(SourceData(Err(DataflowError::from(
2047                EvalError::DivisionByZero,
2048            ))));
2049            let row = Union::new_weighted(vec![(9, ok_row.boxed()), (1, err_row.boxed())]);
2050            prop::collection::vec(row, 2..8)
2051        }
2052
2053        fn lit(datum: Datum<'static>, typ: ReprScalarType) -> MirScalarExpr {
2054            if datum.is_null() {
2055                MirScalarExpr::literal_null(typ)
2056            } else {
2057                MirScalarExpr::literal_ok(datum, typ)
2058            }
2059        }
2060
2061        fn is_null(expr: MirScalarExpr) -> MirScalarExpr {
2062            MirScalarExpr::CallUnary {
2063                func: UnaryFunc::IsNull(IsNull),
2064                expr: Box::new(expr),
2065            }
2066        }
2067
2068        fn not(expr: MirScalarExpr) -> MirScalarExpr {
2069            MirScalarExpr::CallUnary {
2070                func: UnaryFunc::Not(Not),
2071                expr: Box::new(expr),
2072            }
2073        }
2074
2075        fn binary(func: BinaryFunc, a: MirScalarExpr, b: MirScalarExpr) -> MirScalarExpr {
2076            MirScalarExpr::CallBinary {
2077                func,
2078                expr1: Box::new(a),
2079                expr2: Box::new(b),
2080            }
2081        }
2082
2083        /// `col <cmp> lit`, with the literal drawn from the same interesting
2084        /// pool as the row values, so poison values show up on both sides.
2085        fn arb_cmp_col_lit() -> impl Strategy<Value = MirScalarExpr> {
2086            (0..WIDE_ARITY, any::<Index>(), comparison_funcs()).prop_map(|(col, pick, cmp)| {
2087                let pool = wide_pool(col);
2088                let datum = pool[pick.index(pool.len())];
2089                binary(
2090                    cmp,
2091                    MirScalarExpr::column(col),
2092                    lit(datum, wide_repr_type(col)),
2093                )
2094            })
2095        }
2096
2097        fn arb_is_null_pred() -> impl Strategy<Value = MirScalarExpr> {
2098            (0..WIDE_ARITY, any::<bool>()).prop_map(|(col, negate)| {
2099                let expr = is_null(MirScalarExpr::column(col));
2100                if negate { not(expr) } else { expr }
2101            })
2102        }
2103
2104        fn jsonb_keys() -> impl Strategy<Value = &'static str> {
2105            select(vec!["x", "y", "nested", "absent"])
2106        }
2107
2108        fn jsonb_get(expr: MirScalarExpr, key: &'static str, stringify: bool) -> MirScalarExpr {
2109            let func = if stringify {
2110                BinaryFunc::JsonbGetStringStringify(JsonbGetStringStringify)
2111            } else {
2112                BinaryFunc::JsonbGetString(JsonbGetString)
2113            };
2114            binary(
2115                func,
2116                expr,
2117                MirScalarExpr::literal_ok(Datum::String(key), ReprScalarType::String),
2118            )
2119        }
2120
2121        /// `(jN -> 'key') IS NULL` or `(jN ->> 'key') = 'a'`, the shapes that
2122        /// consume the Nested specs built from real jsonb map stats.
2123        fn arb_jsonb_pred() -> impl Strategy<Value = MirScalarExpr> {
2124            (
2125                select(vec![J1, J2]),
2126                jsonb_keys(),
2127                any::<bool>(),
2128                any::<bool>(),
2129            )
2130                .prop_map(|(col, key, stringify, wrap_eq)| {
2131                    let get = jsonb_get(MirScalarExpr::column(col), key, stringify);
2132                    if wrap_eq {
2133                        let typ = if stringify {
2134                            ReprScalarType::String
2135                        } else {
2136                            ReprScalarType::Jsonb
2137                        };
2138                        binary(BinaryFunc::Eq(Eq), get, lit(Datum::String("a"), typ))
2139                    } else {
2140                        is_null(get)
2141                    }
2142                })
2143        }
2144
2145        /// `((CASE WHEN <cond> THEN j1 ELSE j2 END) ->> 'key') IS NULL`, the
2146        /// PER-6 shape: unioning the two columns' Nested specs.
2147        fn arb_case_jsonb_pred() -> impl Strategy<Value = MirScalarExpr> {
2148            (any::<bool>(), jsonb_keys(), any::<bool>()).prop_map(
2149                |(cond_is_col, key, stringify)| {
2150                    let cond = if cond_is_col {
2151                        MirScalarExpr::column(BOOL)
2152                    } else {
2153                        is_null(MirScalarExpr::column(STR))
2154                    };
2155                    let case = MirScalarExpr::If {
2156                        cond: Box::new(cond),
2157                        then: Box::new(MirScalarExpr::column(J1)),
2158                        els: Box::new(MirScalarExpr::column(J2)),
2159                    };
2160                    is_null(jsonb_get(case, key, stringify))
2161                },
2162            )
2163        }
2164
2165        /// `try_parse_monotonic_iso8601_timestamp(c_str) <cmp> <ts>`, the one
2166        /// SpecialUnary implementation in the interpreter.
2167        fn arb_iso_parse_pred() -> impl Strategy<Value = MirScalarExpr> {
2168            (comparison_funcs(), any::<Index>(), any::<bool>()).prop_map(
2169                |(cmp, pick, wrap_null)| {
2170                    let parse = MirScalarExpr::CallUnary {
2171                        func: UnaryFunc::TryParseMonotonicIso8601Timestamp(
2172                            TryParseMonotonicIso8601Timestamp,
2173                        ),
2174                        expr: Box::new(MirScalarExpr::column(STR)),
2175                    };
2176                    if wrap_null {
2177                        is_null(parse)
2178                    } else {
2179                        let pool: Vec<_> = SqlScalarType::Timestamp { precision: None }
2180                            .interesting_datums()
2181                            .collect();
2182                        let datum = pool[pick.index(pool.len())];
2183                        binary(cmp, parse, lit(datum, ReprScalarType::Timestamp))
2184                    }
2185                },
2186            )
2187        }
2188
2189        /// `(c_ts + <interval>) <cmp> <ts>`, the DynamicMonotone handler:
2190        /// day-only intervals are treated as monotone, month-bearing ones must
2191        /// stay conservative.
2192        fn arb_ts_interval_pred() -> impl Strategy<Value = MirScalarExpr> {
2193            let intervals = select(vec![
2194                Interval::new(0, 2, 0),
2195                Interval::new(0, 0, 3_600_000_000),
2196                Interval::new(1, 0, 0),
2197                Interval::new(-1, 0, 0),
2198            ]);
2199            (comparison_funcs(), intervals, any::<Index>()).prop_map(|(cmp, iv, pick)| {
2200                let add = binary(
2201                    BinaryFunc::AddTimestampInterval(AddTimestampInterval),
2202                    MirScalarExpr::column(TS),
2203                    lit(Datum::Interval(iv), ReprScalarType::Interval),
2204                );
2205                let pool: Vec<_> = SqlScalarType::Timestamp { precision: None }
2206                    .interesting_datums()
2207                    .collect();
2208                let datum = pool[pick.index(pool.len())];
2209                binary(cmp, add, lit(datum, ReprScalarType::Timestamp))
2210            })
2211        }
2212
2213        /// `(c_f64 * <const>) <cmp> <const>`, aimed at the interpreter's
2214        /// infinity guard: multiplication is monotone but not
2215        /// infinity-monotone.
2216        fn arb_float_mul_pred() -> impl Strategy<Value = MirScalarExpr> {
2217            let consts = || select(vec![0.0f64, 1.0, -1.0, 1e300, -1e300, f64::INFINITY]);
2218            (comparison_funcs(), consts(), consts()).prop_map(|(cmp, a, c)| {
2219                let mul = binary(
2220                    BinaryFunc::MulFloat64(MulFloat64),
2221                    MirScalarExpr::column(F64),
2222                    lit(Datum::from(a), ReprScalarType::Float64),
2223                );
2224                binary(cmp, mul, lit(Datum::from(c), ReprScalarType::Float64))
2225            })
2226        }
2227
2228        /// `mz_now() <cmp> <mz_timestamp expr>`, compiled by `into_plan` into
2229        /// the temporal lower/upper bounds that `filter_result` checks against
2230        /// the part's time range.
2231        fn arb_temporal_pred() -> impl Strategy<Value = MirScalarExpr> {
2232            let cmps = select(vec![
2233                BinaryFunc::Lte(Lte),
2234                BinaryFunc::Lt(Lt),
2235                BinaryFunc::Gte(Gte),
2236                BinaryFunc::Gt(Gt),
2237            ]);
2238            (cmps, any::<bool>(), any::<Index>()).prop_map(|(cmp, use_col, pick)| {
2239                let mz_now = MirScalarExpr::CallUnmaterializable(UnmaterializableFunc::MzNow);
2240                let rhs = if use_col {
2241                    MirScalarExpr::column(MZTS)
2242                } else {
2243                    let pool = wide_pool(MZTS);
2244                    lit(pool[pick.index(pool.len())], ReprScalarType::MzTimestamp)
2245                };
2246                binary(cmp, mz_now, rhs)
2247            })
2248        }
2249
2250        fn arb_wide_predicate() -> impl Strategy<Value = MirScalarExpr> {
2251            let leaf = Union::new(vec![
2252                arb_cmp_col_lit().boxed(),
2253                arb_is_null_pred().boxed(),
2254                arb_jsonb_pred().boxed(),
2255                arb_case_jsonb_pred().boxed(),
2256                arb_iso_parse_pred().boxed(),
2257                arb_ts_interval_pred().boxed(),
2258                arb_float_mul_pred().boxed(),
2259                arb_temporal_pred().boxed(),
2260                // The numeric shapes from the narrow test, aimed at the
2261                // fallible-interior mechanisms; both reference column 0.
2262                (
2263                    select(vec![0i32, 2, -5, 24699]),
2264                    f32_consts(),
2265                    f32_consts(),
2266                    f32_consts(),
2267                    comparison_funcs(),
2268                )
2269                    .prop_map(|(s, a, b, c, cmp)| float_arith_predicate(s, a, b, c, cmp))
2270                    .boxed(),
2271                (select(vec![0u64, 1, 2, 100, u64::MAX]), comparison_funcs())
2272                    .prop_map(|(ts, cmp)| cast_mz_timestamp_predicate(ts, cmp))
2273                    .boxed(),
2274            ])
2275            .boxed();
2276            Union::new_weighted(vec![
2277                (3, leaf.clone()),
2278                (
2279                    1,
2280                    (leaf.clone(), leaf.clone(), any::<bool>())
2281                        .prop_map(|(a, b, is_and)| {
2282                            let func = if is_and { And.into() } else { Or.into() };
2283                            MirScalarExpr::CallVariadic {
2284                                func,
2285                                exprs: vec![a, b],
2286                            }
2287                        })
2288                        .boxed(),
2289                ),
2290                (1, leaf.prop_map(not).boxed()),
2291            ])
2292        }
2293
2294        /// The zero-column count(*) path: when the read desc projects away
2295        /// every column and each row is known to pass, `filter_result`
2296        /// replaces the part with a synthesized single-row KV instead of
2297        /// keeping or discarding it. Errors and filters that can skip rows
2298        /// must suppress the substitution.
2299        #[mz_ore::test]
2300        #[cfg_attr(miri, ignore)] // too slow
2301        fn zero_column_relation_replace_with() {
2302            let desc = RelationDesc::empty();
2303            let metrics = PartStatsMetrics::new(&MetricsRegistry::new());
2304            let ok_rows = vec![
2305                SourceData(Ok(Row::default())),
2306                SourceData(Ok(Row::default())),
2307            ];
2308
2309            // No predicates, no errors: every row passes, so the part is
2310            // replaced with the synthesized KV.
2311            let plan = MapFilterProject::new(0).into_plan().expect("into_plan");
2312            let part_stats = build_part_stats(&desc, &ok_rows);
2313            let stats = RelationPartStats::new("test", &metrics, &desc, &part_stats);
2314            let decision = filter_result(&desc, ResultSpec::anything(), stats, &plan);
2315            assert!(
2316                matches!(decision, FilterResult::ReplaceWith { .. }),
2317                "expected ReplaceWith, got {decision:?}",
2318            );
2319
2320            // An error row must disable the substitution: the part has to be
2321            // fetched so the error surfaces.
2322            let mixed_rows = vec![
2323                SourceData(Ok(Row::default())),
2324                SourceData(Err(DataflowError::from(EvalError::DivisionByZero))),
2325            ];
2326            let part_stats = build_part_stats(&desc, &mixed_rows);
2327            let stats = RelationPartStats::new("test", &metrics, &desc, &part_stats);
2328            let decision = filter_result(&desc, ResultSpec::anything(), stats, &plan);
2329            assert!(
2330                matches!(decision, FilterResult::Keep),
2331                "expected Keep, got {decision:?}",
2332            );
2333
2334            // A constant-false filter never keeps anything: plain Discard.
2335            let plan = MapFilterProject::new(0)
2336                .filter(std::iter::once(MirScalarExpr::literal_ok(
2337                    Datum::False,
2338                    ReprScalarType::Bool,
2339                )))
2340                .into_plan()
2341                .expect("into_plan");
2342            let part_stats = build_part_stats(&desc, &ok_rows);
2343            let stats = RelationPartStats::new("test", &metrics, &desc, &part_stats);
2344            let decision = filter_result(&desc, ResultSpec::anything(), stats, &plan);
2345            assert!(
2346                matches!(decision, FilterResult::Discard),
2347                "expected Discard, got {decision:?}",
2348            );
2349        }
2350
2351        /// Schema drift between the stats and the read desc must degrade to
2352        /// "no stats", never to a narrower spec.
2353        ///
2354        /// Two real shapes: a column appended by `ALTER TABLE ... ADD COLUMN`
2355        /// after the part was written (present in the read desc, absent from
2356        /// the stats), and demand pushdown projecting the read desc down to a
2357        /// subset of the written columns (stats carry extra columns).
2358        #[mz_ore::test]
2359        #[cfg_attr(miri, ignore)] // too slow
2360        fn schema_drift_degrades_to_no_stats() {
2361            let metrics = PartStatsMetrics::new(&MetricsRegistry::new());
2362
2363            // Part written before ALTER TABLE ... ADD COLUMN b.
2364            let write_desc = RelationDesc::builder()
2365                .with_column("a", SqlScalarType::Int32.nullable(false))
2366                .finish();
2367            let rows = vec![SourceData(Ok(Row::pack_slice(&[Datum::Int32(1)])))];
2368            let part_stats = build_part_stats(&write_desc, &rows);
2369
2370            let read_desc = RelationDesc::builder()
2371                .with_column("a", SqlScalarType::Int32.nullable(false))
2372                .with_column("b", SqlScalarType::Float64.nullable(true))
2373                .finish();
2374            let stats = RelationPartStats::new("test", &metrics, &read_desc, &part_stats);
2375            // Old rows read the new column as null, so `b IS NULL` matches
2376            // them and the part must be kept.
2377            let plan = MapFilterProject::new(2)
2378                .filter(std::iter::once(is_null(MirScalarExpr::column(1))))
2379                .into_plan()
2380                .expect("into_plan");
2381            let decision = filter_result(&read_desc, ResultSpec::anything(), stats, &plan);
2382            assert!(
2383                !matches!(decision, FilterResult::Discard),
2384                "part written before ADD COLUMN was discarded: {decision:?}",
2385            );
2386
2387            // Demand pushdown: the read desc is a projection of the written
2388            // schema. The surviving column's stats must still line up with it
2389            // by name, so a matching filter keeps the part.
2390            let write_desc = RelationDesc::builder()
2391                .with_column("a", SqlScalarType::Int32.nullable(false))
2392                .with_column("b", SqlScalarType::Float64.nullable(true))
2393                .finish();
2394            let rows = vec![SourceData(Ok(Row::pack_slice(&[
2395                Datum::Int32(1),
2396                Datum::from(5.0f64),
2397            ])))];
2398            let part_stats = build_part_stats(&write_desc, &rows);
2399
2400            let read_desc = RelationDesc::builder()
2401                .with_column("b", SqlScalarType::Float64.nullable(true))
2402                .finish();
2403            let stats = RelationPartStats::new("test", &metrics, &read_desc, &part_stats);
2404            let plan = MapFilterProject::new(1)
2405                .filter(std::iter::once(binary(
2406                    BinaryFunc::Eq(Eq),
2407                    MirScalarExpr::column(0),
2408                    lit(Datum::from(5.0f64), ReprScalarType::Float64),
2409                )))
2410                .into_plan()
2411                .expect("into_plan");
2412            let decision = filter_result(&read_desc, ResultSpec::anything(), stats, &plan);
2413            assert!(
2414                !matches!(decision, FilterResult::Discard),
2415                "projected read desc discarded a matching part: {decision:?}",
2416            );
2417        }
2418
2419        /// Ground truth, mirroring [`PendingWork::do_work`]: a part yields
2420        /// output if any Err row survives `until`, or if the MFP applied to
2421        /// any Ok row at its effective time produces anything at all. The
2422        /// runtime audit fires on any result from `evaluate`, before the
2423        /// additional `until` filtering of the produced rows, so this must
2424        /// not post-filter either.
2425        fn part_yields_output(
2426            plan: &MfpPlan,
2427            rows: &[SourceData],
2428            eval_time: Timestamp,
2429            until: &Antichain<Timestamp>,
2430        ) -> bool {
2431            if until.less_equal(&eval_time) {
2432                return false;
2433            }
2434            let arena = RowArena::new();
2435            let mut row_builder = Row::default();
2436            for source_data in rows {
2437                match &source_data.0 {
2438                    Err(_) => return true,
2439                    Ok(row) => {
2440                        let mut datums: Vec<Datum> = row.iter().collect();
2441                        let mut results = plan.evaluate::<DataflowError, _>(
2442                            &mut datums,
2443                            &arena,
2444                            eval_time,
2445                            Diff::from(1),
2446                            |time| !until.less_equal(time),
2447                            &mut row_builder,
2448                        );
2449                        if results.next().is_some() {
2450                            return true;
2451                        }
2452                    }
2453                }
2454            }
2455            false
2456        }
2457
2458        #[mz_ore::test]
2459        #[cfg_attr(miri, ignore)] // too slow, and decNumber FFI is unsupported
2460        fn wide_filter_result_never_discards_matching_part() {
2461            fn check(
2462                rows: Vec<SourceData>,
2463                predicate: MirScalarExpr,
2464                eval_time: u64,
2465                until: Option<u64>,
2466            ) -> Result<(), TestCaseError> {
2467                let desc = wide_desc();
2468                // Predicate shapes that use mz_now in a way the temporal
2469                // filter machinery does not support fail to plan; there is
2470                // nothing to check for those.
2471                let Ok(plan) = MapFilterProject::new(desc.arity())
2472                    .filter(std::iter::once(predicate))
2473                    .into_plan()
2474                else {
2475                    return Ok(());
2476                };
2477                let eval_time = Timestamp::from(eval_time);
2478                let until =
2479                    until.map_or_else(Antichain::new, |t| Antichain::from_elem(Timestamp::from(t)));
2480
2481                let part_stats = build_part_stats(&desc, &rows);
2482                let metrics = PartStatsMetrics::new(&MetricsRegistry::new());
2483                let stats = RelationPartStats::new("test", &metrics, &desc, &part_stats);
2484
2485                // Mirror the read path: mz_now is bounded by the part's
2486                // frontier and the dataflow's until, both inclusive, with an
2487                // empty until standing in for MAX. A frontier past the until
2488                // is discarded before stats are consulted.
2489                let upper = until.as_option().copied().unwrap_or(Timestamp::MAX);
2490                if eval_time > upper {
2491                    return Ok(());
2492                }
2493                let time_range = ResultSpec::value_between(
2494                    Datum::MzTimestamp(eval_time),
2495                    Datum::MzTimestamp(upper),
2496                );
2497                let decision = filter_result(&desc, time_range, stats, &plan);
2498
2499                if part_yields_output(&plan, &rows, eval_time, &until) {
2500                    prop_assert!(
2501                        !matches!(decision, FilterResult::Discard),
2502                        "filter pushdown discarded a part whose MFP yields output on a real \
2503                         row (wrongly-skipped part; the runtime audit would panic).\n\
2504                         rows={rows:?}\nplan={plan:?}\neval_time={eval_time}\nuntil={until:?}",
2505                    );
2506                }
2507                Ok(())
2508            }
2509
2510            // The vocabulary is wide (9 columns, 10 predicate shapes), so a
2511            // specific poison-value-plus-predicate coincidence is rare per
2512            // case. The default 256 cases demonstrably miss known bugs; 4096
2513            // still runs in a couple of seconds because each case is cheap.
2514            // An explicit PROPTEST_CASES (already parsed into the default
2515            // config) wins, for long local or nightly runs.
2516            let default = ProptestConfig::default();
2517            let cases = if std::env::var_os("PROPTEST_CASES").is_some() {
2518                default.cases
2519            } else {
2520                4096
2521            };
2522            let config = ProptestConfig { cases, ..default };
2523            proptest!(config, |(
2524                rows in arb_wide_rows(),
2525                predicate in arb_wide_predicate(),
2526                eval_time in select(vec![1u64, 5]),
2527                until in select(vec![None, Some(1u64), Some(5), Some(8), Some(100)]),
2528            )| {
2529                check(rows, predicate, eval_time, until)?;
2530            });
2531        }
2532
2533        /// Filter decisions are made per part: rows split across several
2534        /// parts get an independent decision per part, and a part whose own
2535        /// rows yield output must never be discarded, regardless of what the
2536        /// sibling parts contain (e.g. a poison value in one part must not
2537        /// affect another part's decision, and vice versa).
2538        #[mz_ore::test]
2539        #[cfg_attr(miri, ignore)] // too slow, and decNumber FFI is unsupported
2540        fn multi_part_decisions_are_independent() {
2541            fn check(
2542                parts: Vec<Vec<SourceData>>,
2543                predicate: MirScalarExpr,
2544                eval_time: u64,
2545                until: Option<u64>,
2546            ) -> Result<(), TestCaseError> {
2547                let desc = wide_desc();
2548                let Ok(plan) = MapFilterProject::new(desc.arity())
2549                    .filter(std::iter::once(predicate))
2550                    .into_plan()
2551                else {
2552                    return Ok(());
2553                };
2554                let eval_time = Timestamp::from(eval_time);
2555                let until =
2556                    until.map_or_else(Antichain::new, |t| Antichain::from_elem(Timestamp::from(t)));
2557                let upper = until.as_option().copied().unwrap_or(Timestamp::MAX);
2558                if eval_time > upper {
2559                    return Ok(());
2560                }
2561                let metrics = PartStatsMetrics::new(&MetricsRegistry::new());
2562
2563                for rows in &parts {
2564                    let part_stats = build_part_stats(&desc, rows);
2565                    let stats = RelationPartStats::new("test", &metrics, &desc, &part_stats);
2566                    let time_range = ResultSpec::value_between(
2567                        Datum::MzTimestamp(eval_time),
2568                        Datum::MzTimestamp(upper),
2569                    );
2570                    let decision = filter_result(&desc, time_range, stats, &plan);
2571                    if part_yields_output(&plan, rows, eval_time, &until) {
2572                        prop_assert!(
2573                            !matches!(decision, FilterResult::Discard),
2574                            "filter pushdown discarded a part whose MFP yields output on a \
2575                             real row.\nrows={rows:?}\nplan={plan:?}\neval_time={eval_time}\n\
2576                             until={until:?}",
2577                        );
2578                    }
2579                }
2580                Ok(())
2581            }
2582
2583            let default = ProptestConfig::default();
2584            let cases = if std::env::var_os("PROPTEST_CASES").is_some() {
2585                default.cases
2586            } else {
2587                1024
2588            };
2589            let config = ProptestConfig { cases, ..default };
2590            proptest!(config, |(
2591                parts in prop::collection::vec(arb_wide_rows(), 2..4),
2592                predicate in arb_wide_predicate(),
2593                eval_time in select(vec![1u64, 5]),
2594                until in select(vec![None, Some(5u64), Some(100)]),
2595            )| {
2596                check(parts, predicate, eval_time, until)?;
2597            });
2598        }
2599    }
2600}