Skip to main content

mz_storage_operators/
persist_source.rs

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