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