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