Skip to main content

mz_persist_client/
batch.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 handle to a batch of updates
11
12use std::borrow::Cow;
13use std::collections::{BTreeMap, BTreeSet};
14use std::fmt::Debug;
15use std::marker::PhantomData;
16use std::mem;
17use std::sync::Arc;
18use std::time::Instant;
19
20use arrow::array::{Array, Int64Array};
21use arrow::datatypes::DataType;
22use bytes::Bytes;
23use differential_dataflow::difference::Monoid;
24use differential_dataflow::lattice::Lattice;
25use differential_dataflow::trace::Description;
26use futures_util::stream::StreamExt;
27use futures_util::{FutureExt, stream};
28use mz_dyncfg::{Config, ParameterScope};
29use mz_ore::cast::CastFrom;
30use mz_ore::instrument;
31use mz_persist::indexed::encoding::{BatchColumnarFormat, BlobTraceBatchPart, BlobTraceUpdates};
32use mz_persist::location::Blob;
33use mz_persist_types::arrow::{ArrayBound, ArrayOrd};
34use mz_persist_types::columnar::{ColumnDecoder, Schema, data_type};
35use mz_persist_types::parquet::{CompressionFormat, EncodingConfig};
36use mz_persist_types::part::{Part, PartBuilder};
37use mz_persist_types::schema::SchemaId;
38use mz_persist_types::stats::{
39    PartStats, TRUNCATE_LEN, TruncateBound, trim_to_budget, truncate_bytes,
40};
41use mz_persist_types::{Codec, Codec64};
42use mz_proto::RustType;
43use mz_timely_util::order::Reverse;
44use proptest_derive::Arbitrary;
45use semver::Version;
46use timely::PartialOrder;
47use timely::order::TotalOrder;
48use timely::progress::{Antichain, Timestamp};
49use tracing::{Instrument, debug_span, trace_span, warn};
50
51use crate::async_runtime::IsolatedRuntime;
52use crate::cfg::{BATCH_BUILDER_MAX_OUTSTANDING_PARTS, MiB};
53use crate::error::InvalidUsage;
54use crate::internal::compact::{CompactConfig, Compactor};
55use crate::internal::encoding::{
56    LazyInlineBatchPart, LazyPartStats, LazyProto, MetadataMap, Schemas,
57};
58use crate::internal::machine::retry_external;
59use crate::internal::merge::{MergeTree, Pending};
60use crate::internal::metrics::{BatchWriteMetrics, Metrics, RetryMetrics, ShardMetrics};
61use crate::internal::paths::{PartId, PartialBatchKey, WriterKey};
62use crate::internal::state::{
63    BatchPart, ENABLE_INCREMENTAL_COMPACTION, HollowBatch, HollowBatchPart, HollowRun,
64    HollowRunRef, ProtoInlineBatchPart, RunId, RunMeta, RunOrder, RunPart,
65};
66use crate::stats::{STATS_BUDGET_BYTES, STATS_COLLECTION_ENABLED, untrimmable_columns};
67use crate::{PersistConfig, ShardId};
68
69include!(concat!(env!("OUT_DIR"), "/mz_persist_client.batch.rs"));
70
71/// A handle to a batch of updates that has been written to blob storage but
72/// which has not yet been appended to a shard.
73///
74/// A [Batch] needs to be marked as consumed or it needs to be deleted via [Self::delete].
75/// Otherwise, a dangling batch will leak and backing blobs will remain in blob storage.
76#[derive(Debug)]
77pub struct Batch<K, V, T, D> {
78    pub(crate) batch_delete_enabled: bool,
79    pub(crate) metrics: Arc<Metrics>,
80    pub(crate) shard_metrics: Arc<ShardMetrics>,
81
82    /// The version of Materialize which wrote this batch.
83    pub(crate) version: Version,
84
85    /// The encoded schemas of the data in the batch.
86    pub(crate) schemas: (Bytes, Bytes),
87
88    /// A handle to the data represented by this batch.
89    pub(crate) batch: HollowBatch<T>,
90
91    /// Handle to the [Blob] that the blobs of this batch were uploaded to.
92    pub(crate) blob: Arc<dyn Blob>,
93
94    // These provide a bit more safety against appending a batch with the wrong
95    // type to a shard.
96    pub(crate) _phantom: PhantomData<fn() -> (K, V, T, D)>,
97}
98
99impl<K, V, T, D> Drop for Batch<K, V, T, D> {
100    fn drop(&mut self) {
101        if self.batch.part_count() > 0 {
102            warn!(
103                "un-consumed Batch, with {} parts and dangling blob keys: {:?}",
104                self.batch.part_count(),
105                self.batch
106                    .parts
107                    .iter()
108                    .map(|x| x.printable_name())
109                    .collect::<Vec<_>>(),
110            );
111        }
112    }
113}
114
115impl<K, V, T, D> Batch<K, V, T, D>
116where
117    K: Debug + Codec,
118    V: Debug + Codec,
119    T: Timestamp + Lattice + Codec64,
120    D: Monoid + Codec64,
121{
122    pub(crate) fn new(
123        batch_delete_enabled: bool,
124        metrics: Arc<Metrics>,
125        blob: Arc<dyn Blob>,
126        shard_metrics: Arc<ShardMetrics>,
127        version: Version,
128        schemas: (Bytes, Bytes),
129        batch: HollowBatch<T>,
130    ) -> Self {
131        Self {
132            batch_delete_enabled,
133            metrics,
134            shard_metrics,
135            version,
136            schemas,
137            batch,
138            blob,
139            _phantom: PhantomData,
140        }
141    }
142
143    /// The `shard_id` of this [Batch].
144    pub fn shard_id(&self) -> ShardId {
145        self.shard_metrics.shard_id
146    }
147
148    /// The `upper` of this [Batch].
149    pub fn upper(&self) -> &Antichain<T> {
150        self.batch.desc.upper()
151    }
152
153    /// The `lower` of this [Batch].
154    pub fn lower(&self) -> &Antichain<T> {
155        self.batch.desc.lower()
156    }
157
158    /// Marks the blobs that this batch handle points to as consumed, likely
159    /// because they were appended to a shard.
160    ///
161    /// Consumers of a blob need to make this explicit, so that we can log
162    /// warnings in case a batch is not used.
163    pub(crate) fn mark_consumed(&mut self) {
164        self.batch.parts.clear();
165    }
166
167    /// Deletes the blobs that make up this batch from the given blob store and
168    /// marks them as deleted.
169    #[instrument(level = "debug", fields(shard = %self.shard_id()))]
170    pub async fn delete(mut self) {
171        if !self.batch_delete_enabled {
172            self.mark_consumed();
173            return;
174        }
175        let mut deletes = PartDeletes::default();
176        for part in self.batch.parts.drain(..) {
177            deletes.add(&part);
178        }
179        let () = deletes
180            .delete(
181                &*self.blob,
182                self.shard_id(),
183                usize::MAX,
184                &*self.metrics,
185                &*self.metrics.retries.external.batch_delete,
186            )
187            .await;
188    }
189
190    /// Returns the schemas of parts in this batch.
191    pub fn schemas(&self) -> impl Iterator<Item = SchemaId> + '_ {
192        self.batch.parts.iter().flat_map(|b| b.schema_id())
193    }
194
195    /// Turns this [`Batch`] into a `HollowBatch`.
196    ///
197    /// **NOTE**: If this batch is not eventually appended to a shard or
198    /// dropped, the data that it represents will have leaked.
199    pub fn into_hollow_batch(mut self) -> HollowBatch<T> {
200        let ret = self.batch.clone();
201        self.mark_consumed();
202        ret
203    }
204
205    /// Turns this [`Batch`] into a [`ProtoBatch`], which can be used to
206    /// transfer this batch across process boundaries, for example when
207    /// exchanging data between timely workers.
208    ///
209    /// **NOTE**: If this batch is not eventually appended to a shard or
210    /// dropped, the data that it represents will have leaked. The caller is
211    /// responsible for turning this back into a [`Batch`] using
212    /// [`WriteHandle::batch_from_transmittable_batch`](crate::write::WriteHandle::batch_from_transmittable_batch).
213    pub fn into_transmittable_batch(mut self) -> ProtoBatch {
214        let ret = ProtoBatch {
215            shard_id: self.shard_metrics.shard_id.into_proto(),
216            version: self.version.to_string(),
217            batch: Some(self.batch.into_proto()),
218            key_schema: self.schemas.0.clone(),
219            val_schema: self.schemas.1.clone(),
220        };
221        self.mark_consumed();
222        ret
223    }
224
225    pub(crate) async fn flush_to_blob(
226        &mut self,
227        cfg: &BatchBuilderConfig,
228        batch_metrics: &BatchWriteMetrics,
229        isolated_runtime: &Arc<IsolatedRuntime>,
230        write_schemas: &Schemas<K, V>,
231    ) {
232        // It's necessary for correctness to keep the parts in the same order.
233        // We could introduce concurrency here with FuturesOrdered, but it would
234        // be pretty unexpected to have inline writes in more than one part, so
235        // don't bother.
236        let mut parts = Vec::new();
237        for (run_meta, run_parts) in self.batch.runs() {
238            for part in run_parts {
239                let (updates, ts_rewrite, schema_id) = match part {
240                    RunPart::Single(BatchPart::Inline {
241                        updates,
242                        ts_rewrite,
243                        schema_id,
244                        deprecated_schema_id: _,
245                    }) => (updates, ts_rewrite, schema_id),
246                    other @ RunPart::Many(_) | other @ RunPart::Single(BatchPart::Hollow(_)) => {
247                        parts.push(other.clone());
248                        continue;
249                    }
250                };
251                let updates = updates
252                    .decode::<T>(&self.metrics.columnar)
253                    .expect("valid inline part");
254                let diffs_sum = diffs_sum::<D>(updates.updates.diffs());
255                let mut write_schemas = write_schemas.clone();
256                write_schemas.id = *schema_id;
257
258                let write_span =
259                    debug_span!("batch::flush_to_blob", shard = %self.shard_metrics.shard_id)
260                        .or_current();
261                let handle = mz_ore::task::spawn(
262                    || "batch::flush_to_blob",
263                    BatchParts::write_hollow_part(
264                        cfg.clone(),
265                        Arc::clone(&self.blob),
266                        Arc::clone(&self.metrics),
267                        Arc::clone(&self.shard_metrics),
268                        batch_metrics.clone(),
269                        Arc::clone(isolated_runtime),
270                        updates,
271                        run_meta.order.unwrap_or(RunOrder::Unordered),
272                        ts_rewrite.clone(),
273                        D::encode(&diffs_sum),
274                        write_schemas,
275                    )
276                    .instrument(write_span),
277                );
278                let part = handle.await;
279                parts.push(RunPart::Single(part));
280            }
281        }
282        self.batch.parts = parts;
283    }
284
285    /// The sum of the encoded sizes of all parts in the batch.
286    pub fn encoded_size_bytes(&self) -> usize {
287        self.batch.encoded_size_bytes()
288    }
289}
290
291impl<K, V, T, D> Batch<K, V, T, D>
292where
293    K: Debug + Codec,
294    V: Debug + Codec,
295    T: Timestamp + Lattice + Codec64 + TotalOrder,
296    D: Monoid + Codec64,
297{
298    /// Efficiently rewrites the timestamps in this not-yet-committed batch.
299    ///
300    /// This [Batch] represents potentially large amounts of data, which may
301    /// have partly or entirely been spilled to s3. This call bulk edits the
302    /// timestamps of all data in this batch in a metadata-only operation (i.e.
303    /// without network calls).
304    ///
305    /// Specifically, every timestamp in the batch is logically advanced_by the
306    /// provided `frontier`.
307    ///
308    /// This method may be called multiple times, with later calls overriding
309    /// previous ones, but the rewrite frontier may not regress across calls.
310    ///
311    /// When this batch was created, it was given an `upper`, which bounds the
312    /// staged data it represents. To allow rewrite past this original `upper`,
313    /// this call accepts a new `upper` which replaces the previous one. Like
314    /// the rewrite frontier, the upper may not regress across calls.
315    ///
316    /// Multiple batches with various rewrite frontiers may be used in a single
317    /// [crate::write::WriteHandle::compare_and_append_batch] call. This is an
318    /// expected usage.
319    ///
320    /// This feature requires that the timestamp impls `TotalOrder`. This is
321    /// because we need to be able to verify that the contained data, after the
322    /// rewrite forward operation, still respects the new upper. It turns out
323    /// that, given the metadata persist currently collects during batch
324    /// collection, this is possible for totally ordered times, but it's known
325    /// to be _not possible_ for partially ordered times. It is believed that we
326    /// could fix this by collecting different metadata in batch creation (e.g.
327    /// the join of or an antichain of the original contained timestamps), but
328    /// the experience of database-issues#7825 has shaken our confidence in our own abilities
329    /// to reason about partially ordered times and anyway all the initial uses
330    /// have totally ordered times.
331    pub fn rewrite_ts(
332        &mut self,
333        frontier: &Antichain<T>,
334        new_upper: Antichain<T>,
335    ) -> Result<(), InvalidUsage<T>> {
336        self.batch
337            .rewrite_ts(frontier, new_upper)
338            .map_err(InvalidUsage::InvalidRewrite)
339    }
340}
341
342/// Indicates what work was done in a call to [BatchBuilder::add]
343#[derive(Debug)]
344pub enum Added {
345    /// A record was inserted into a pending batch part
346    Record,
347    /// A record was inserted into a pending batch part
348    /// and the part was sent to blob storage
349    RecordAndParts,
350}
351
352/// A snapshot of dynamic configs to make it easier to reason about an individual
353/// run of BatchBuilder.
354#[derive(Debug, Clone)]
355pub struct BatchBuilderConfig {
356    writer_key: WriterKey,
357    pub(crate) blob_target_size: usize,
358    pub(crate) batch_delete_enabled: bool,
359    pub(crate) batch_builder_max_outstanding_parts: usize,
360    pub(crate) inline_writes_single_max_bytes: usize,
361    pub(crate) stats_collection_enabled: bool,
362    pub(crate) stats_budget: usize,
363    pub(crate) stats_untrimmable_columns: Arc<UntrimmableColumns>,
364    pub(crate) encoding_config: EncodingConfig,
365    pub(crate) preferred_order: RunOrder,
366    pub(crate) structured_key_lower_len: usize,
367    pub(crate) run_length_limit: usize,
368    pub(crate) enable_incremental_compaction: bool,
369    /// The number of runs to cap the built batch at, or None if we should
370    /// continue to generate one run per part for unordered batches.
371    /// See the config definition for details.
372    pub(crate) max_runs: Option<usize>,
373}
374
375// TODO: Remove this once we're comfortable that there aren't any bugs.
376pub(crate) const BATCH_DELETE_ENABLED: Config<bool> = Config::new(
377    "persist_batch_delete_enabled",
378    true,
379    "Whether to actually delete blobs when batch delete is called (Materialize).",
380    ParameterScope::Environment,
381);
382
383pub(crate) const ENCODING_ENABLE_DICTIONARY: Config<bool> = Config::new(
384    "persist_encoding_enable_dictionary",
385    true,
386    "A feature flag to enable dictionary encoding for Parquet data (Materialize).",
387    ParameterScope::Environment,
388);
389
390pub(crate) const ENCODING_COMPRESSION_FORMAT: Config<&'static str> = Config::new(
391    "persist_encoding_compression_format",
392    "none",
393    "A feature flag to enable compression of Parquet data (Materialize).",
394    ParameterScope::Environment,
395);
396
397pub(crate) const STRUCTURED_KEY_LOWER_LEN: Config<usize> = Config::new(
398    "persist_batch_structured_key_lower_len",
399    256,
400    "The maximum size in proto bytes of any structured key-lower metadata to preserve. \
401    (If we're unable to fit the lower in budget, or the budget is zero, no metadata is kept.)",
402    ParameterScope::Environment,
403);
404
405pub(crate) const MAX_RUN_LEN: Config<usize> = Config::new(
406    "persist_batch_max_run_len",
407    usize::MAX,
408    "The maximum length a run can have before it will be spilled as a hollow run \
409    into the blob store.",
410    ParameterScope::Environment,
411);
412
413pub(crate) const MAX_RUNS: Config<usize> = Config::new(
414    "persist_batch_max_runs",
415    1,
416    "The maximum number of runs a batch builder should generate for user batches. \
417    (Compaction outputs always generate a single run.) \
418    The minimum value is 2; below this, compaction is disabled.",
419    ParameterScope::Environment,
420);
421
422/// A target maximum size of blob payloads in bytes. If a logical "batch" is
423/// bigger than this, it will be broken up into smaller, independent pieces.
424/// This is best-effort, not a guarantee (though as of 2022-06-09, we happen to
425/// always respect it). This target size doesn't apply for an individual update
426/// that exceeds it in size, but that scenario is almost certainly a mis-use of
427/// the system.
428pub(crate) const BLOB_TARGET_SIZE: Config<usize> = Config::new(
429    "persist_blob_target_size",
430    128 * MiB,
431    "A target maximum size of persist blob payloads in bytes (Materialize).",
432    ParameterScope::Environment,
433);
434
435pub(crate) const INLINE_WRITES_SINGLE_MAX_BYTES: Config<usize> = Config::new(
436    "persist_inline_writes_single_max_bytes",
437    4096,
438    "The (exclusive) maximum size of a write that persist will inline in metadata.",
439    ParameterScope::Environment,
440);
441
442pub(crate) const INLINE_WRITES_TOTAL_MAX_BYTES: Config<usize> = Config::new(
443    "persist_inline_writes_total_max_bytes",
444    1 * MiB,
445    "\
446    The (exclusive) maximum total size of inline writes in metadata before \
447    persist will backpressure them by flushing out to s3.",
448    ParameterScope::Environment,
449);
450
451impl BatchBuilderConfig {
452    /// Initialize a batch builder config based on a snapshot of the Persist config.
453    pub fn new(value: &PersistConfig, _shard_id: ShardId) -> Self {
454        let writer_key = WriterKey::for_version(&value.build_version);
455
456        let preferred_order = RunOrder::Structured;
457
458        BatchBuilderConfig {
459            writer_key,
460            blob_target_size: BLOB_TARGET_SIZE.get(value).clamp(1, usize::MAX),
461            batch_delete_enabled: BATCH_DELETE_ENABLED.get(value),
462            batch_builder_max_outstanding_parts: BATCH_BUILDER_MAX_OUTSTANDING_PARTS.get(value),
463            inline_writes_single_max_bytes: INLINE_WRITES_SINGLE_MAX_BYTES.get(value),
464            stats_collection_enabled: STATS_COLLECTION_ENABLED.get(value),
465            stats_budget: STATS_BUDGET_BYTES.get(value),
466            stats_untrimmable_columns: Arc::new(untrimmable_columns(value)),
467            encoding_config: EncodingConfig {
468                use_dictionary: ENCODING_ENABLE_DICTIONARY.get(value),
469                compression: CompressionFormat::from_str(&ENCODING_COMPRESSION_FORMAT.get(value)),
470            },
471            preferred_order,
472            structured_key_lower_len: STRUCTURED_KEY_LOWER_LEN.get(value),
473            run_length_limit: MAX_RUN_LEN.get(value).clamp(2, usize::MAX),
474            max_runs: match MAX_RUNS.get(value) {
475                limit @ 2.. => Some(limit),
476                _ => None,
477            },
478            enable_incremental_compaction: ENABLE_INCREMENTAL_COMPACTION.get(value),
479        }
480    }
481}
482
483/// A list of (lowercase) column names that persist will always retain
484/// stats for, even if it means going over the stats budget.
485#[derive(
486    Debug,
487    Clone,
488    PartialEq,
489    Eq,
490    serde::Serialize,
491    serde::Deserialize,
492    Arbitrary
493)]
494pub(crate) struct UntrimmableColumns {
495    /// Always retain columns whose lowercased names exactly equal any of these strings.
496    pub equals: Vec<Cow<'static, str>>,
497    /// Always retain columns whose lowercased names start with any of these strings.
498    pub prefixes: Vec<Cow<'static, str>>,
499    /// Always retain columns whose lowercased names end with any of these strings.
500    pub suffixes: Vec<Cow<'static, str>>,
501}
502
503impl UntrimmableColumns {
504    pub(crate) fn should_retain(&self, name: &str) -> bool {
505        // TODO: see if there's a better way to match different formats than lowercasing
506        // https://github.com/MaterializeInc/database-issues/issues/6421#issue-1863623805
507        let name_lower = name.to_lowercase();
508        for s in &self.equals {
509            if *s == name_lower {
510                return true;
511            }
512        }
513        for s in &self.prefixes {
514            if name_lower.starts_with(s.as_ref()) {
515                return true;
516            }
517        }
518        for s in &self.suffixes {
519            if name_lower.ends_with(s.as_ref()) {
520                return true;
521            }
522        }
523        false
524    }
525}
526
527/// A builder for [Batches](Batch) that allows adding updates piece by piece and
528/// then finishing it.
529#[derive(Debug)]
530pub struct BatchBuilder<K, V, T, D>
531where
532    K: Codec,
533    V: Codec,
534    T: Timestamp + Lattice + Codec64,
535{
536    inline_desc: Description<T>,
537    inclusive_upper: Antichain<Reverse<T>>,
538
539    part_builder: PartBuilder<K, K::Schema, V, V::Schema>,
540    finished_parts: Vec<Part>,
541    /// The accumulated size of `finished_parts`.
542    ///
543    /// Tracked incrementally because `Part::goodbytes` rebuilds an `ArrayOrd` per key and value
544    /// column on every call, and parts accumulate until they reach the blob target size. Summing
545    /// the vector on each `add`/`add_part` would make a flush window quadratic in the number of
546    /// parts it holds.
547    finished_parts_goodbytes: usize,
548    /// The key and value data types of the write schemas, resolved on the first `add_part`.
549    part_data_types: Option<(DataType, DataType)>,
550    pub(crate) builder: BatchBuilderInternal<K, V, T, D>,
551}
552
553impl<K, V, T, D> BatchBuilder<K, V, T, D>
554where
555    K: Debug + Codec,
556    V: Debug + Codec,
557    T: Timestamp + Lattice + Codec64,
558    D: Monoid + Codec64,
559{
560    pub(crate) fn new(
561        builder: BatchBuilderInternal<K, V, T, D>,
562        inline_desc: Description<T>,
563    ) -> Self {
564        let records_builder = PartBuilder::new(
565            builder.write_schemas.key.as_ref(),
566            builder.write_schemas.val.as_ref(),
567        );
568        Self {
569            inline_desc,
570            inclusive_upper: Antichain::new(),
571            part_builder: records_builder,
572            finished_parts: vec![],
573            finished_parts_goodbytes: 0,
574            part_data_types: None,
575            builder,
576        }
577    }
578
579    fn in_progress_goodbytes(&self) -> usize {
580        self.part_builder.goodbytes() + self.finished_parts_goodbytes
581    }
582
583    fn push_finished_part(&mut self, part: Part) {
584        self.finished_parts_goodbytes += part.goodbytes();
585        self.finished_parts.push(part);
586    }
587
588    fn finish_part_builder(&mut self) {
589        if self.part_builder.len() > 0 {
590            let part = self.part_builder.finish_and_replace(
591                &*self.builder.write_schemas.key,
592                &*self.builder.write_schemas.val,
593            );
594            self.push_finished_part(part);
595        }
596    }
597
598    async fn flush_parts(&mut self) -> bool {
599        self.finish_part_builder();
600        if let Some(part) = Part::concat(&self.finished_parts).expect("type aligned") {
601            self.builder
602                .flush_part(self.inline_desc.clone(), part)
603                .await;
604            self.finished_parts.clear();
605            self.finished_parts_goodbytes = 0;
606            true
607        } else {
608            false
609        }
610    }
611
612    /// Finish writing this batch and return a handle to the written batch.
613    ///
614    /// This fails if any of the updates in this batch are beyond the given
615    /// `upper`.
616    pub async fn finish(
617        mut self,
618        registered_upper: Antichain<T>,
619    ) -> Result<Batch<K, V, T, D>, InvalidUsage<T>> {
620        if PartialOrder::less_than(&registered_upper, self.inline_desc.lower()) {
621            return Err(InvalidUsage::InvalidBounds {
622                lower: self.inline_desc.lower().clone(),
623                upper: registered_upper,
624            });
625        }
626
627        // When since is less than or equal to lower, the upper is a strict bound
628        // on the updates' timestamp because no advancement has been performed. Because user batches
629        // are always unadvanced, this ensures that new updates are recorded with valid timestamps.
630        // Otherwise, we can make no assumptions about the timestamps
631        if PartialOrder::less_equal(self.inline_desc.since(), self.inline_desc.lower()) {
632            for ts in self.inclusive_upper.iter() {
633                if registered_upper.less_equal(&ts.0) {
634                    return Err(InvalidUsage::UpdateBeyondUpper {
635                        ts: ts.0.clone(),
636                        expected_upper: registered_upper.clone(),
637                    });
638                }
639            }
640        }
641
642        self.flush_parts().await;
643
644        self.builder
645            .finish(Description::new(
646                self.inline_desc.lower().clone(),
647                registered_upper,
648                self.inline_desc.since().clone(),
649            ))
650            .await
651    }
652
653    /// Adds the given encoded part to the batch. Its key and value data types must match this
654    /// builder's write schemas.
655    ///
656    /// Every update timestamp in the part must be greater or equal to `lower` that was given when
657    /// creating this [BatchBuilder].
658    ///
659    /// Parts accumulate until they reach the configured blob target size and are then concatenated
660    /// into one flushed part, so an individual `part` must stay well below persist's
661    /// `KEY_VAL_DATA_MAX_LEN`: a single oversized part is flushed on its own, but nothing splits
662    /// it.
663    pub async fn add_part(&mut self, part: Part) -> Result<Added, InvalidUsage<T>> {
664        for time in part.time.values() {
665            let ts = T::decode(time.to_le_bytes());
666            if !self.inline_desc.lower().less_equal(&ts) {
667                return Err(InvalidUsage::UpdateNotBeyondLower {
668                    ts,
669                    lower: self.inline_desc.lower().clone(),
670                });
671            }
672            self.inclusive_upper.insert(Reverse(ts));
673        }
674
675        let schemas = &self.builder.write_schemas;
676        let (key_type, val_type) = self.part_data_types.get_or_insert_with(|| {
677            (
678                data_type::<K>(&schemas.key).expect("valid type"),
679                data_type::<V>(&schemas.val).expect("valid type"),
680            )
681        });
682        assert_eq!(*key_type, *part.key.data_type());
683        assert_eq!(*val_type, *part.val.data_type());
684
685        self.finish_part_builder();
686
687        let mut flushed = false;
688        if self.in_progress_goodbytes() + part.goodbytes() > self.builder.parts.cfg.blob_target_size
689        {
690            flushed |= self.flush_parts().await;
691        }
692        self.push_finished_part(part);
693        if self.in_progress_goodbytes() > self.builder.parts.cfg.blob_target_size {
694            flushed |= self.flush_parts().await;
695        }
696
697        let added = if flushed {
698            Added::RecordAndParts
699        } else {
700            Added::Record
701        };
702        Ok(added)
703    }
704
705    /// Adds the given update to the batch.
706    ///
707    /// The update timestamp must be greater or equal to `lower` that was given
708    /// when creating this [BatchBuilder].
709    pub async fn add(
710        &mut self,
711        key: &K,
712        val: &V,
713        ts: &T,
714        diff: &D,
715    ) -> Result<Added, InvalidUsage<T>> {
716        if !self.inline_desc.lower().less_equal(ts) {
717            return Err(InvalidUsage::UpdateNotBeyondLower {
718                ts: ts.clone(),
719                lower: self.inline_desc.lower().clone(),
720            });
721        }
722        self.inclusive_upper.insert(Reverse(ts.clone()));
723
724        let mut flushed = false;
725        self.part_builder.push(key, val, ts.clone(), diff.clone());
726        if self.in_progress_goodbytes() >= self.builder.parts.cfg.blob_target_size {
727            flushed |= self.flush_parts().await;
728        }
729
730        let added = if flushed {
731            Added::RecordAndParts
732        } else {
733            Added::Record
734        };
735        Ok(added)
736    }
737}
738
739#[derive(Debug)]
740pub(crate) struct BatchBuilderInternal<K, V, T, D>
741where
742    K: Codec,
743    V: Codec,
744    T: Timestamp + Lattice + Codec64,
745{
746    shard_id: ShardId,
747    version: Version,
748    blob: Arc<dyn Blob>,
749    metrics: Arc<Metrics>,
750
751    write_schemas: Schemas<K, V>,
752    parts: BatchParts<T>,
753
754    // These provide a bit more safety against appending a batch with the wrong
755    // type to a shard.
756    _phantom: PhantomData<fn(K, V, T, D)>,
757}
758
759impl<K, V, T, D> BatchBuilderInternal<K, V, T, D>
760where
761    K: Debug + Codec,
762    V: Debug + Codec,
763    T: Timestamp + Lattice + Codec64,
764    D: Monoid + Codec64,
765{
766    pub(crate) fn new(
767        _cfg: BatchBuilderConfig,
768        parts: BatchParts<T>,
769        metrics: Arc<Metrics>,
770        write_schemas: Schemas<K, V>,
771        blob: Arc<dyn Blob>,
772        shard_id: ShardId,
773        version: Version,
774    ) -> Self {
775        Self {
776            blob,
777            metrics,
778            write_schemas,
779            parts,
780            shard_id,
781            version,
782            _phantom: PhantomData,
783        }
784    }
785
786    /// Finish writing this batch and return a handle to the written batch.
787    ///
788    /// This fails if any of the updates in this batch are beyond the given
789    /// `upper`.
790    #[instrument(level = "debug", name = "batch::finish", fields(shard = %self.shard_id))]
791    pub async fn finish(
792        self,
793        registered_desc: Description<T>,
794    ) -> Result<Batch<K, V, T, D>, InvalidUsage<T>> {
795        let write_run_ids = self.parts.cfg.enable_incremental_compaction;
796        let batch_delete_enabled = self.parts.cfg.batch_delete_enabled;
797        let shard_metrics = Arc::clone(&self.parts.shard_metrics);
798        let runs = self.parts.finish().await;
799
800        let mut run_parts = vec![];
801        let mut run_splits = vec![];
802        let mut run_meta = vec![];
803        let total_updates = runs
804            .iter()
805            .map(|(_, _, num_updates)| num_updates)
806            .sum::<usize>();
807        for (order, parts, num_updates) in runs {
808            if parts.is_empty() {
809                continue;
810            }
811            if run_parts.len() != 0 {
812                run_splits.push(run_parts.len());
813            }
814            run_meta.push(RunMeta {
815                order: Some(order),
816                schema: self.write_schemas.id,
817                // Field has been deprecated but kept around to roundtrip state.
818                deprecated_schema: None,
819                id: if write_run_ids {
820                    Some(RunId::new())
821                } else {
822                    None
823                },
824                len: if write_run_ids {
825                    Some(num_updates)
826                } else {
827                    None
828                },
829                meta: MetadataMap::default(),
830            });
831            run_parts.extend(parts);
832        }
833        let desc = registered_desc;
834
835        let batch = Batch::new(
836            batch_delete_enabled,
837            Arc::clone(&self.metrics),
838            self.blob,
839            shard_metrics,
840            self.version,
841            (
842                K::encode_schema(&*self.write_schemas.key),
843                V::encode_schema(&*self.write_schemas.val),
844            ),
845            HollowBatch::new(desc, run_parts, total_updates, run_meta, run_splits),
846        );
847
848        Ok(batch)
849    }
850
851    /// Flushes the current part to Blob storage, first columnar encoding the updates.
852    /// It is the caller's responsibility to chunk `current_part` to be no greater than
853    /// [BatchBuilderConfig::blob_target_size], and must absolutely be less than
854    /// [mz_persist::indexed::columnar::KEY_VAL_DATA_MAX_LEN]
855    pub async fn flush_part(&mut self, part_desc: Description<T>, columnar: Part) {
856        let num_updates = columnar.len();
857        if num_updates == 0 {
858            return;
859        }
860        let diffs_sum = diffs_sum::<D>(&columnar.diff);
861
862        let start = Instant::now();
863        self.parts
864            .write(&self.write_schemas, part_desc, columnar, diffs_sum)
865            .await;
866        self.metrics
867            .compaction
868            .batch
869            .step_part_writing
870            .inc_by(start.elapsed().as_secs_f64());
871    }
872}
873
874#[derive(Debug, Clone)]
875pub(crate) struct RunWithMeta<T> {
876    pub parts: Vec<RunPart<T>>,
877    pub num_updates: usize,
878}
879
880impl<T> RunWithMeta<T> {
881    pub fn new(parts: Vec<RunPart<T>>, num_updates: usize) -> Self {
882        Self { parts, num_updates }
883    }
884
885    pub fn single(part: RunPart<T>, num_updates: usize) -> Self {
886        Self {
887            parts: vec![part],
888            num_updates,
889        }
890    }
891}
892
893#[derive(Debug)]
894enum WritingRuns<T> {
895    /// Building a single run with the specified ordering. Parts are expected to be internally
896    /// sorted and added in order. Merging a vec of parts will shift them out to a hollow run
897    /// in blob, bounding the total length of a run in memory.
898    Ordered(RunOrder, MergeTree<Pending<RunWithMeta<T>>>),
899    /// Building multiple runs which may have different orders. Merging a vec of runs will cause
900    /// them to be compacted together, bounding the total number of runs we generate.
901    Compacting(MergeTree<(RunOrder, Pending<RunWithMeta<T>>)>),
902}
903
904// TODO: If this is dropped, cancel (and delete?) any writing parts and delete
905// any finished ones.
906#[derive(Debug)]
907pub(crate) struct BatchParts<T> {
908    cfg: BatchBuilderConfig,
909    metrics: Arc<Metrics>,
910    shard_metrics: Arc<ShardMetrics>,
911    shard_id: ShardId,
912    blob: Arc<dyn Blob>,
913    isolated_runtime: Arc<IsolatedRuntime>,
914    next_index: u64,
915    writing_runs: WritingRuns<T>,
916    batch_metrics: BatchWriteMetrics,
917}
918
919impl<T: Timestamp + Codec64> BatchParts<T> {
920    pub(crate) fn new_compacting<K, V, D>(
921        cfg: CompactConfig,
922        desc: Description<T>,
923        runs_per_compaction: usize,
924        metrics: Arc<Metrics>,
925        shard_metrics: Arc<ShardMetrics>,
926        shard_id: ShardId,
927        blob: Arc<dyn Blob>,
928        isolated_runtime: Arc<IsolatedRuntime>,
929        batch_metrics: &BatchWriteMetrics,
930        schemas: Schemas<K, V>,
931    ) -> Self
932    where
933        K: Codec + Debug,
934        V: Codec + Debug,
935        T: Lattice + Send + Sync,
936        D: Monoid + Ord + Codec64 + Send + Sync,
937    {
938        let writing_runs = {
939            let cfg = cfg.clone();
940            let blob = Arc::clone(&blob);
941            let metrics = Arc::clone(&metrics);
942            let shard_metrics = Arc::clone(&shard_metrics);
943            let isolated_runtime = Arc::clone(&isolated_runtime);
944            // Clamping to prevent extreme values given weird configs.
945            let runs_per_compaction = runs_per_compaction.clamp(2, 1024);
946
947            let merge_fn = move |parts: Vec<(RunOrder, Pending<RunWithMeta<T>>)>| {
948                let blob = Arc::clone(&blob);
949                let metrics = Arc::clone(&metrics);
950                let shard_metrics = Arc::clone(&shard_metrics);
951                let cfg = cfg.clone();
952                let isolated_runtime = Arc::clone(&isolated_runtime);
953                let write_schemas = schemas.clone();
954                let compact_desc = desc.clone();
955                let handle = mz_ore::task::spawn(
956                    || "batch::compact_runs",
957                    async move {
958                        let runs: Vec<_> = stream::iter(parts)
959                            .then(|(order, parts)| async move {
960                                let completed_run = parts.into_result().await;
961                                (
962                                    RunMeta {
963                                        order: Some(order),
964                                        schema: schemas.id,
965                                        // Field has been deprecated but kept around to
966                                        // roundtrip state.
967                                        deprecated_schema: None,
968                                        id: if cfg.batch.enable_incremental_compaction {
969                                            Some(RunId::new())
970                                        } else {
971                                            None
972                                        },
973                                        len: if cfg.batch.enable_incremental_compaction {
974                                            Some(completed_run.num_updates)
975                                        } else {
976                                            None
977                                        },
978                                        meta: MetadataMap::default(),
979                                    },
980                                    completed_run.parts,
981                                )
982                            })
983                            .collect()
984                            .await;
985
986                        let run_refs: Vec<_> = runs
987                            .iter()
988                            .map(|(meta, run)| (&compact_desc, meta, run.as_slice()))
989                            .collect();
990
991                        let output_batch = Compactor::<K, V, T, D>::compact_runs(
992                            &cfg,
993                            &shard_id,
994                            &compact_desc,
995                            run_refs,
996                            blob,
997                            metrics,
998                            shard_metrics,
999                            isolated_runtime,
1000                            write_schemas,
1001                        )
1002                        .await
1003                        .expect("successful compaction");
1004
1005                        assert_eq!(
1006                            output_batch.run_meta.len(),
1007                            1,
1008                            "compaction is guaranteed to emit a single run"
1009                        );
1010                        let total_compacted_updates: usize = output_batch.len;
1011
1012                        RunWithMeta::new(output_batch.parts, total_compacted_updates)
1013                    }
1014                    .instrument(debug_span!("batch::compact_runs")),
1015                );
1016                (RunOrder::Structured, Pending::new(handle))
1017            };
1018            WritingRuns::Compacting(MergeTree::new(runs_per_compaction, merge_fn))
1019        };
1020        BatchParts {
1021            cfg: cfg.batch,
1022            metrics,
1023            shard_metrics,
1024            shard_id,
1025            blob,
1026            isolated_runtime,
1027            next_index: 0,
1028            writing_runs,
1029            batch_metrics: batch_metrics.clone(),
1030        }
1031    }
1032
1033    pub(crate) fn new_ordered<D: Monoid + Codec64>(
1034        cfg: BatchBuilderConfig,
1035        order: RunOrder,
1036        metrics: Arc<Metrics>,
1037        shard_metrics: Arc<ShardMetrics>,
1038        shard_id: ShardId,
1039        blob: Arc<dyn Blob>,
1040        isolated_runtime: Arc<IsolatedRuntime>,
1041        batch_metrics: &BatchWriteMetrics,
1042    ) -> Self {
1043        let writing_runs = {
1044            let cfg = cfg.clone();
1045            let blob = Arc::clone(&blob);
1046            let metrics = Arc::clone(&metrics);
1047            let writer_key = cfg.writer_key.clone();
1048            // Don't spill "unordered" runs to S3, since we'll split them up into many single-element
1049            // runs below.
1050            let run_length_limit = (order == RunOrder::Unordered)
1051                .then_some(usize::MAX)
1052                .unwrap_or(cfg.run_length_limit);
1053            let merge_fn = move |parts: Vec<Pending<RunWithMeta<T>>>| {
1054                let blob = Arc::clone(&blob);
1055                let writer_key = writer_key.clone();
1056                let metrics = Arc::clone(&metrics);
1057                let handle = mz_ore::task::spawn(
1058                    || "batch::spill_run",
1059                    async move {
1060                        let completed_runs: Vec<RunWithMeta<T>> = stream::iter(parts)
1061                            .then(|p| p.into_result())
1062                            .collect()
1063                            .await;
1064
1065                        let mut all_run_parts = Vec::new();
1066                        let mut total_updates = 0;
1067
1068                        for completed_run in completed_runs {
1069                            all_run_parts.extend(completed_run.parts);
1070                            total_updates += completed_run.num_updates;
1071                        }
1072
1073                        let run_ref = HollowRunRef::set::<D>(
1074                            shard_id,
1075                            blob.as_ref(),
1076                            &writer_key,
1077                            HollowRun {
1078                                parts: all_run_parts,
1079                            },
1080                            &*metrics,
1081                        )
1082                        .await;
1083
1084                        RunWithMeta::single(RunPart::Many(run_ref), total_updates)
1085                    }
1086                    .instrument(debug_span!("batch::spill_run")),
1087                );
1088                Pending::new(handle)
1089            };
1090            WritingRuns::Ordered(order, MergeTree::new(run_length_limit, merge_fn))
1091        };
1092        BatchParts {
1093            cfg,
1094            metrics,
1095            shard_metrics,
1096            shard_id,
1097            blob,
1098            isolated_runtime,
1099            next_index: 0,
1100            writing_runs,
1101            batch_metrics: batch_metrics.clone(),
1102        }
1103    }
1104
1105    pub(crate) fn expected_order(&self) -> RunOrder {
1106        match self.writing_runs {
1107            WritingRuns::Ordered(order, _) => order,
1108            WritingRuns::Compacting(_) => RunOrder::Unordered,
1109        }
1110    }
1111
1112    pub(crate) async fn write<K: Codec, V: Codec, D: Codec64>(
1113        &mut self,
1114        write_schemas: &Schemas<K, V>,
1115        desc: Description<T>,
1116        updates: Part,
1117        diffs_sum: D,
1118    ) {
1119        let batch_metrics = self.batch_metrics.clone();
1120        let index = self.next_index;
1121        self.next_index += 1;
1122        let num_updates = updates.len();
1123        let ts_rewrite = None;
1124        let schema_id = write_schemas.id;
1125
1126        // If we're going to encode structured data then halve our limit since we're storing
1127        // it twice, once as binary encoded and once as structured.
1128        let inline_threshold = self.cfg.inline_writes_single_max_bytes;
1129
1130        let updates = BlobTraceUpdates::from_part(updates);
1131        let (name, write_future) = if updates.goodbytes() < inline_threshold {
1132            let span = debug_span!("batch::inline_part", shard = %self.shard_id).or_current();
1133            (
1134                "batch::inline_part",
1135                async move {
1136                    let start = Instant::now();
1137                    let updates = LazyInlineBatchPart::from(&ProtoInlineBatchPart {
1138                        desc: Some(desc.into_proto()),
1139                        index: index.into_proto(),
1140                        updates: Some(updates.into_proto()),
1141                    });
1142                    batch_metrics
1143                        .step_inline
1144                        .inc_by(start.elapsed().as_secs_f64());
1145
1146                    RunWithMeta::single(
1147                        RunPart::Single(BatchPart::Inline {
1148                            updates,
1149                            ts_rewrite,
1150                            schema_id,
1151                            // Field has been deprecated but kept around to roundtrip state.
1152                            deprecated_schema_id: None,
1153                        }),
1154                        num_updates,
1155                    )
1156                }
1157                .instrument(span)
1158                .boxed(),
1159            )
1160        } else {
1161            let part = BlobTraceBatchPart {
1162                desc,
1163                updates,
1164                index,
1165            };
1166            let cfg = self.cfg.clone();
1167            let blob = Arc::clone(&self.blob);
1168            let metrics = Arc::clone(&self.metrics);
1169            let shard_metrics = Arc::clone(&self.shard_metrics);
1170            let isolated_runtime = Arc::clone(&self.isolated_runtime);
1171            let expected_order = self.expected_order();
1172            let encoded_diffs_sum = D::encode(&diffs_sum);
1173            let write_schemas_clone = write_schemas.clone();
1174            let write_span =
1175                debug_span!("batch::write_part", shard = %self.shard_metrics.shard_id).or_current();
1176            (
1177                "batch::write_part",
1178                async move {
1179                    let part = BatchParts::write_hollow_part(
1180                        cfg,
1181                        blob,
1182                        metrics,
1183                        shard_metrics,
1184                        batch_metrics,
1185                        isolated_runtime,
1186                        part,
1187                        expected_order,
1188                        ts_rewrite,
1189                        encoded_diffs_sum,
1190                        write_schemas_clone,
1191                    )
1192                    .await;
1193                    RunWithMeta::single(RunPart::Single(part), num_updates)
1194                }
1195                .instrument(write_span)
1196                .boxed(),
1197            )
1198        };
1199
1200        match &mut self.writing_runs {
1201            WritingRuns::Ordered(_order, run) => {
1202                let part = Pending::new(mz_ore::task::spawn(|| name, write_future));
1203                run.push(part);
1204
1205                // If there are more than the max outstanding parts, block on all but the
1206                //  most recent.
1207                for part in run
1208                    .iter_mut()
1209                    .rev()
1210                    .skip(self.cfg.batch_builder_max_outstanding_parts)
1211                    .take_while(|p| !p.is_finished())
1212                {
1213                    self.batch_metrics.write_stalls.inc();
1214                    part.block_until_ready().await;
1215                }
1216            }
1217            WritingRuns::Compacting(batches) => {
1218                let run = Pending::Writing(mz_ore::task::spawn(|| name, write_future));
1219                batches.push((RunOrder::Unordered, run));
1220
1221                // Allow up to `max_outstanding_parts` (or one compaction) to be pending, and block
1222                // on the rest.
1223                let mut part_budget = self.cfg.batch_builder_max_outstanding_parts;
1224                let mut compaction_budget = 1;
1225                for (_, part) in batches
1226                    .iter_mut()
1227                    .rev()
1228                    .skip_while(|(order, _)| match order {
1229                        RunOrder::Unordered if part_budget > 0 => {
1230                            part_budget -= 1;
1231                            true
1232                        }
1233                        RunOrder::Structured | RunOrder::Codec if compaction_budget > 0 => {
1234                            compaction_budget -= 1;
1235                            true
1236                        }
1237                        _ => false,
1238                    })
1239                    .take_while(|(_, p)| !p.is_finished())
1240                {
1241                    self.batch_metrics.write_stalls.inc();
1242                    part.block_until_ready().await;
1243                }
1244            }
1245        }
1246    }
1247
1248    async fn write_hollow_part<K: Codec, V: Codec>(
1249        cfg: BatchBuilderConfig,
1250        blob: Arc<dyn Blob>,
1251        metrics: Arc<Metrics>,
1252        shard_metrics: Arc<ShardMetrics>,
1253        batch_metrics: BatchWriteMetrics,
1254        isolated_runtime: Arc<IsolatedRuntime>,
1255        mut updates: BlobTraceBatchPart<T>,
1256        run_order: RunOrder,
1257        ts_rewrite: Option<Antichain<T>>,
1258        diffs_sum: [u8; 8],
1259        write_schemas: Schemas<K, V>,
1260    ) -> BatchPart<T> {
1261        let partial_key = PartialBatchKey::new(&cfg.writer_key, &PartId::new());
1262        let key = partial_key.complete(&shard_metrics.shard_id);
1263        let goodbytes = updates.updates.goodbytes();
1264        let metrics_ = Arc::clone(&metrics);
1265        let schema_id = write_schemas.id;
1266
1267        let (stats, key_lower, structured_key_lower, (buf, encode_time)) = isolated_runtime
1268            .spawn_named(|| "batch::encode_part", async move {
1269                // Measure the expensive steps of the part build - re-encoding and stats collection.
1270                let stats = metrics_.columnar.arrow().measure_part_build(|| {
1271                    let stats = if cfg.stats_collection_enabled {
1272                        let ext = updates.updates.get_or_make_structured::<K, V>(
1273                            write_schemas.key.as_ref(),
1274                            write_schemas.val.as_ref(),
1275                        );
1276
1277                        let key_stats = write_schemas
1278                            .key
1279                            .decoder_any(ext.key.as_ref())
1280                            .expect("decoding just-encoded data")
1281                            .stats();
1282
1283                        let part_stats = PartStats { key: key_stats };
1284
1285                        // Collect stats about the updates, if stats collection is enabled.
1286                        let trimmed_start = Instant::now();
1287                        let mut trimmed_bytes = 0;
1288                        let trimmed_stats = LazyPartStats::encode(&part_stats, |s| {
1289                            trimmed_bytes = trim_to_budget(s, cfg.stats_budget, |s| {
1290                                cfg.stats_untrimmable_columns.should_retain(s)
1291                            })
1292                        });
1293                        let trimmed_duration = trimmed_start.elapsed();
1294                        Some((trimmed_stats, trimmed_duration, trimmed_bytes))
1295                    } else {
1296                        None
1297                    };
1298
1299                    // Ensure the updates are in the specified columnar format before encoding.
1300                    updates.updates = updates.updates.as_structured::<K, V>(
1301                        write_schemas.key.as_ref(),
1302                        write_schemas.val.as_ref(),
1303                    );
1304
1305                    stats
1306                });
1307
1308                let key_lower = if let Some(records) = updates.updates.records() {
1309                    let key_bytes = records.keys();
1310                    if key_bytes.is_empty() {
1311                        &[]
1312                    } else if run_order == RunOrder::Codec {
1313                        key_bytes.value(0)
1314                    } else {
1315                        ::arrow::compute::min_binary(key_bytes).expect("min of nonempty array")
1316                    }
1317                } else {
1318                    &[]
1319                };
1320                let key_lower = truncate_bytes(key_lower, TRUNCATE_LEN, TruncateBound::Lower)
1321                    .expect("lower bound always exists");
1322
1323                let structured_key_lower = if cfg.structured_key_lower_len > 0 {
1324                    updates.updates.structured().and_then(|ext| {
1325                        let min_key = if run_order == RunOrder::Structured {
1326                            0
1327                        } else {
1328                            let ord = ArrayOrd::new(ext.key.as_ref());
1329                            (0..ext.key.len())
1330                                .min_by_key(|i| ord.at(*i))
1331                                .expect("non-empty batch")
1332                        };
1333                        let lower = ArrayBound::new(Arc::clone(&ext.key), min_key)
1334                            .to_proto_lower(cfg.structured_key_lower_len);
1335                        if lower.is_none() {
1336                            batch_metrics.key_lower_too_big.inc()
1337                        }
1338                        lower.map(|proto| LazyProto::from(&proto))
1339                    })
1340                } else {
1341                    None
1342                };
1343
1344                let encode_start = Instant::now();
1345                let mut buf = Vec::new();
1346                updates.encode(&mut buf, &metrics_.columnar, &cfg.encoding_config);
1347
1348                // Drop batch as soon as we can to reclaim its memory.
1349                drop(updates);
1350                (
1351                    stats,
1352                    key_lower,
1353                    structured_key_lower,
1354                    (Bytes::from(buf), encode_start.elapsed()),
1355                )
1356            })
1357            .instrument(debug_span!("batch::encode_part"))
1358            .await;
1359        // Can't use the `CodecMetrics::encode` helper because of async.
1360        metrics.codecs.batch.encode_count.inc();
1361        metrics
1362            .codecs
1363            .batch
1364            .encode_seconds
1365            .inc_by(encode_time.as_secs_f64());
1366
1367        let start = Instant::now();
1368        let payload_len = buf.len();
1369        let () = retry_external(&metrics.retries.external.batch_set, || async {
1370            shard_metrics.blob_sets.inc();
1371            blob.set(&key, Bytes::clone(&buf)).await
1372        })
1373        .instrument(trace_span!("batch::set", payload_len))
1374        .await;
1375        batch_metrics.seconds.inc_by(start.elapsed().as_secs_f64());
1376        batch_metrics.bytes.inc_by(u64::cast_from(payload_len));
1377        batch_metrics.goodbytes.inc_by(u64::cast_from(goodbytes));
1378        match run_order {
1379            RunOrder::Unordered => batch_metrics.unordered.inc(),
1380            RunOrder::Codec => batch_metrics.codec_order.inc(),
1381            RunOrder::Structured => batch_metrics.structured_order.inc(),
1382        }
1383        let stats = stats.map(|(stats, stats_step_timing, trimmed_bytes)| {
1384            batch_metrics
1385                .step_stats
1386                .inc_by(stats_step_timing.as_secs_f64());
1387            if trimmed_bytes > 0 {
1388                metrics.pushdown.parts_stats_trimmed_count.inc();
1389                metrics
1390                    .pushdown
1391                    .parts_stats_trimmed_bytes
1392                    .inc_by(u64::cast_from(trimmed_bytes));
1393            }
1394            stats
1395        });
1396
1397        let meta = MetadataMap::default();
1398        BatchPart::Hollow(HollowBatchPart {
1399            key: partial_key,
1400            meta,
1401            encoded_size_bytes: payload_len,
1402            key_lower,
1403            structured_key_lower,
1404            stats,
1405            ts_rewrite,
1406            diffs_sum: Some(diffs_sum),
1407            format: Some(BatchColumnarFormat::Structured),
1408            schema_id,
1409            // Field has been deprecated but kept around to roundtrip state.
1410            deprecated_schema_id: None,
1411        })
1412    }
1413
1414    #[instrument(level = "debug", name = "batch::finish_upload", fields(shard = %self.shard_id))]
1415    pub(crate) async fn finish(self) -> Vec<(RunOrder, Vec<RunPart<T>>, usize)> {
1416        match self.writing_runs {
1417            WritingRuns::Ordered(RunOrder::Unordered, run) => {
1418                let completed_runs = run.finish();
1419                let mut output = Vec::with_capacity(completed_runs.len());
1420                for completed_run in completed_runs {
1421                    let completed_run = completed_run.into_result().await;
1422                    // Each part becomes its own run for unordered case
1423                    for part in completed_run.parts {
1424                        output.push((RunOrder::Unordered, vec![part], completed_run.num_updates));
1425                    }
1426                }
1427                output
1428            }
1429            WritingRuns::Ordered(order, run) => {
1430                let completed_runs = run.finish();
1431                let mut all_parts = Vec::new();
1432                let mut all_update_counts = 0;
1433                for completed_run in completed_runs {
1434                    let completed_run = completed_run.into_result().await;
1435                    all_parts.extend(completed_run.parts);
1436                    all_update_counts += completed_run.num_updates;
1437                }
1438                vec![(order, all_parts, all_update_counts)]
1439            }
1440            WritingRuns::Compacting(batches) => {
1441                let runs = batches.finish();
1442                let mut output = Vec::new();
1443                for (order, run) in runs {
1444                    let completed_run = run.into_result().await;
1445                    output.push((order, completed_run.parts, completed_run.num_updates));
1446                }
1447                output
1448            }
1449        }
1450    }
1451}
1452
1453/// Validates that `batch` may be appended under the desc `truncate`.
1454///
1455/// Returns whether `truncate` cuts into `batch`'s own desc, i.e. whether the
1456/// batch's parts may hold updates outside the desc they are registered under.
1457/// Readers filter such updates out against the registered desc, but write-time
1458/// part statistics still count them, so callers must record the bit (see
1459/// `RunMeta::bounds_truncated`) to keep stats-based accounting from trusting
1460/// the statistics.
1461pub(crate) fn validate_truncate_batch<T: Timestamp>(
1462    batch: &HollowBatch<T>,
1463    truncate: &Description<T>,
1464    any_batch_rewrite: bool,
1465    validate_part_bounds_on_write: bool,
1466) -> Result<bool, InvalidUsage<T>> {
1467    // If rewrite_ts is used, we don't allow truncation, to keep things simpler
1468    // to reason about.
1469    if any_batch_rewrite {
1470        // We allow a new upper to be specified at rewrite time, so that's easy:
1471        // it must match exactly. This is both consistent with the upper
1472        // requirement below and proves that there is no data to truncate past
1473        // the upper.
1474        if truncate.upper() != batch.desc.upper() {
1475            return Err(InvalidUsage::InvalidRewrite(format!(
1476                "rewritten batch might have data past {:?} up to {:?}",
1477                truncate.upper().elements(),
1478                batch.desc.upper().elements(),
1479            )));
1480        }
1481        // To prove that there is no data to truncate below the lower, require
1482        // that the lower is <= the rewrite ts.
1483        for part in batch.parts.iter() {
1484            let part_lower_bound = part.ts_rewrite().unwrap_or_else(|| batch.desc.lower());
1485            if !PartialOrder::less_equal(truncate.lower(), part_lower_bound) {
1486                return Err(InvalidUsage::InvalidRewrite(format!(
1487                    "rewritten batch might have data below {:?} at {:?}",
1488                    truncate.lower().elements(),
1489                    part_lower_bound.elements(),
1490                )));
1491            }
1492        }
1493    }
1494
1495    // The rewrite checks above prove that no part holds data outside
1496    // `truncate`: the upper matches exactly and every part's effective lower
1497    // bound (its rewrite ts) is at or above `truncate.lower()`. The batch's
1498    // own desc being wider does not mean anything is filtered on read, so
1499    // comparing against it would spuriously mark every rewritten batch.
1500    let bounds_truncated = !any_batch_rewrite
1501        && (!PartialOrder::less_equal(truncate.lower(), batch.desc.lower())
1502            || !PartialOrder::less_equal(batch.desc.upper(), truncate.upper()));
1503
1504    if !validate_part_bounds_on_write {
1505        return Ok(bounds_truncated);
1506    }
1507
1508    let batch = &batch.desc;
1509    if !PartialOrder::less_equal(batch.lower(), truncate.lower())
1510        || PartialOrder::less_than(batch.upper(), truncate.upper())
1511    {
1512        return Err(InvalidUsage::InvalidBatchBounds {
1513            batch_lower: batch.lower().clone(),
1514            batch_upper: batch.upper().clone(),
1515            append_lower: truncate.lower().clone(),
1516            append_upper: truncate.upper().clone(),
1517        });
1518    }
1519
1520    Ok(bounds_truncated)
1521}
1522
1523#[derive(Debug)]
1524pub(crate) struct PartDeletes<T> {
1525    /// Keys to hollow parts or runs that we're ready to delete.
1526    blob_keys: BTreeSet<PartialBatchKey>,
1527    /// Keys to hollow runs that may not have had all their parts deleted (or added to blob_keys) yet.
1528    hollow_runs: BTreeMap<PartialBatchKey, HollowRunRef<T>>,
1529}
1530
1531impl<T> Default for PartDeletes<T> {
1532    fn default() -> Self {
1533        Self {
1534            blob_keys: Default::default(),
1535            hollow_runs: Default::default(),
1536        }
1537    }
1538}
1539
1540impl<T: Timestamp> PartDeletes<T> {
1541    // Adds the part to the set to be deleted and returns true if it was newly
1542    // inserted.
1543    pub fn add(&mut self, part: &RunPart<T>) -> bool {
1544        match part {
1545            RunPart::Many(r) => self.hollow_runs.insert(r.key.clone(), r.clone()).is_none(),
1546            RunPart::Single(BatchPart::Hollow(x)) => self.blob_keys.insert(x.key.clone()),
1547            RunPart::Single(BatchPart::Inline { .. }) => {
1548                // Nothing to delete.
1549                true
1550            }
1551        }
1552    }
1553
1554    pub fn contains(&self, part: &RunPart<T>) -> bool {
1555        match part {
1556            RunPart::Many(r) => self.hollow_runs.contains_key(&r.key),
1557            RunPart::Single(BatchPart::Hollow(x)) => self.blob_keys.contains(&x.key),
1558            RunPart::Single(BatchPart::Inline { .. }) => false,
1559        }
1560    }
1561
1562    pub fn is_empty(&self) -> bool {
1563        self.len() == 0
1564    }
1565
1566    pub fn len(&self) -> usize {
1567        match self {
1568            Self {
1569                blob_keys,
1570                hollow_runs,
1571            } => blob_keys.len() + hollow_runs.len(),
1572        }
1573    }
1574
1575    pub async fn delete(
1576        mut self,
1577        blob: &dyn Blob,
1578        shard_id: ShardId,
1579        concurrency: usize,
1580        metrics: &Metrics,
1581        delete_metrics: &RetryMetrics,
1582    ) where
1583        T: Codec64,
1584    {
1585        loop {
1586            let () = stream::iter(mem::take(&mut self.blob_keys))
1587                .map(|key| {
1588                    let key = key.complete(&shard_id);
1589                    async move {
1590                        retry_external(delete_metrics, || blob.delete(&key)).await;
1591                    }
1592                })
1593                .buffer_unordered(concurrency)
1594                .collect()
1595                .await;
1596
1597            let Some((run_key, run_ref)) = self.hollow_runs.pop_first() else {
1598                break;
1599            };
1600
1601            if let Some(run) = run_ref.get(shard_id, blob, metrics).await {
1602                // Queue up both all the individual parts and the run itself for deletion.
1603                for part in &run.parts {
1604                    self.add(part);
1605                }
1606                self.blob_keys.insert(run_key);
1607            };
1608        }
1609    }
1610}
1611
1612/// Returns the total sum of diffs or None if there were no updates.
1613fn diffs_sum<D: Monoid + Codec64>(updates: &Int64Array) -> D {
1614    let mut sum = D::zero();
1615    for d in updates.values().iter() {
1616        let d = D::decode(d.to_le_bytes());
1617        sum.plus_equals(&d);
1618    }
1619    sum
1620}
1621
1622#[cfg(test)]
1623mod tests {
1624    use mz_dyncfg::ConfigUpdates;
1625
1626    use super::*;
1627    use crate::PersistLocation;
1628    use crate::cache::PersistClientCache;
1629    use crate::cfg::BATCH_BUILDER_MAX_OUTSTANDING_PARTS;
1630    use crate::internal::paths::{BlobKey, PartialBlobKey};
1631    use crate::tests::{all_ok, new_test_client};
1632    use mz_persist_types::codec_impls::StringSchema;
1633
1634    #[mz_ore::test(tokio::test)]
1635    #[cfg_attr(miri, ignore)] // unsupported operation: returning ready events from epoll_wait is not yet implemented
1636    async fn batch_builder_flushing() {
1637        let data = vec![
1638            (("1".to_owned(), "one".to_owned()), 1, 1),
1639            (("2".to_owned(), "two".to_owned()), 2, 1),
1640            (("3".to_owned(), "three".to_owned()), 3, 1),
1641            (("4".to_owned(), "four".to_owned()), 4, 1),
1642        ];
1643
1644        let cache = PersistClientCache::new_no_metrics();
1645
1646        // Set blob_target_size to 0 so that each row gets forced into its own
1647        // batch. Set max_outstanding to a small value that's >1 to test various
1648        // edge cases below.
1649        cache.cfg.set_config(&BLOB_TARGET_SIZE, 0);
1650        cache.cfg.set_config(&MAX_RUNS, 3);
1651        cache
1652            .cfg
1653            .set_config(&BATCH_BUILDER_MAX_OUTSTANDING_PARTS, 2);
1654
1655        let client = cache
1656            .open(PersistLocation::new_in_mem())
1657            .await
1658            .expect("client construction failed");
1659        let (mut write, mut read) = client
1660            .expect_open::<String, String, u64, i64>(ShardId::new())
1661            .await;
1662
1663        // A new builder has no writing or finished parts.
1664        let mut builder = write.builder(Antichain::from_elem(0));
1665
1666        fn assert_writing(
1667            builder: &BatchBuilder<String, String, u64, i64>,
1668            expected_finished: &[bool],
1669        ) {
1670            let WritingRuns::Compacting(run) = &builder.builder.parts.writing_runs else {
1671                unreachable!("ordered run!")
1672            };
1673
1674            let actual: Vec<_> = run.iter().map(|(_, p)| p.is_finished()).collect();
1675            assert_eq!(*expected_finished, actual);
1676        }
1677
1678        assert_writing(&builder, &[]);
1679
1680        // We set blob_target_size to 0, so the first update gets forced out
1681        // into a run.
1682        let ((k, v), t, d) = &data[0];
1683        builder.add(k, v, t, d).await.expect("invalid usage");
1684        assert_writing(&builder, &[false]);
1685
1686        // We set batch_builder_max_outstanding_parts to 2, so we are allowed to
1687        // pipeline a second part.
1688        let ((k, v), t, d) = &data[1];
1689        builder.add(k, v, t, d).await.expect("invalid usage");
1690        assert_writing(&builder, &[false, false]);
1691
1692        // But now that we have 3 parts, the add call back-pressures until the
1693        // first one finishes.
1694        let ((k, v), t, d) = &data[2];
1695        builder.add(k, v, t, d).await.expect("invalid usage");
1696        assert_writing(&builder, &[true, false, false]);
1697
1698        // Finally, pushing a fourth part will cause the first three to spill out into
1699        // a new compacted run.
1700        let ((k, v), t, d) = &data[3];
1701        builder.add(k, v, t, d).await.expect("invalid usage");
1702        assert_writing(&builder, &[false, false]);
1703
1704        // Finish off the batch and verify that the keys and such get plumbed
1705        // correctly by reading the data back.
1706        let batch = builder
1707            .finish(Antichain::from_elem(5))
1708            .await
1709            .expect("invalid usage");
1710        assert_eq!(batch.batch.runs().count(), 2);
1711        assert_eq!(batch.batch.part_count(), 4);
1712        write
1713            .append_batch(batch, Antichain::from_elem(0), Antichain::from_elem(5))
1714            .await
1715            .expect("invalid usage")
1716            .expect("unexpected upper");
1717        assert_eq!(read.expect_snapshot_and_fetch(4).await, all_ok(&data, 4));
1718    }
1719
1720    #[mz_ore::test(tokio::test)]
1721    #[cfg_attr(miri, ignore)] // unsupported operation: returning ready events from epoll_wait is not yet implemented
1722    async fn batch_builder_keys() {
1723        let cache = PersistClientCache::new_no_metrics();
1724        // Set blob_target_size to 0 so that each row gets forced into its own batch part
1725        cache.cfg.set_config(&BLOB_TARGET_SIZE, 0);
1726        // Otherwise fails: expected hollow part!
1727        cache.cfg.set_config(&STRUCTURED_KEY_LOWER_LEN, 0);
1728        cache.cfg.set_config(&INLINE_WRITES_SINGLE_MAX_BYTES, 0);
1729        cache.cfg.set_config(&INLINE_WRITES_TOTAL_MAX_BYTES, 0);
1730        let client = cache
1731            .open(PersistLocation::new_in_mem())
1732            .await
1733            .expect("client construction failed");
1734        let shard_id = ShardId::new();
1735        let (mut write, _) = client
1736            .expect_open::<String, String, u64, i64>(shard_id)
1737            .await;
1738
1739        let batch = write
1740            .expect_batch(
1741                &[
1742                    (("1".into(), "one".into()), 1, 1),
1743                    (("2".into(), "two".into()), 2, 1),
1744                    (("3".into(), "three".into()), 3, 1),
1745                ],
1746                0,
1747                4,
1748            )
1749            .await;
1750
1751        assert_eq!(batch.batch.part_count(), 3);
1752        for part in &batch.batch.parts {
1753            let part = part.expect_hollow_part();
1754            match BlobKey::parse_ids(&part.key.complete(&shard_id)) {
1755                Ok((shard, PartialBlobKey::Batch(writer, _))) => {
1756                    assert_eq!(shard.to_string(), shard_id.to_string());
1757                    assert_eq!(writer, WriterKey::for_version(&cache.cfg.build_version));
1758                }
1759                _ => panic!("unparseable blob key"),
1760            }
1761        }
1762    }
1763
1764    fn string_part(updates: &[((&str, &str), u64, i64)]) -> Part {
1765        let mut builder = PartBuilder::new(&StringSchema, &StringSchema);
1766        for ((k, v), t, d) in updates {
1767            builder.push(&k.to_string(), &v.to_string(), *t, *d);
1768        }
1769        builder.finish()
1770    }
1771
1772    #[mz_ore::test(tokio::test)]
1773    #[cfg_attr(miri, ignore)] // unsupported operation: returning ready events from epoll_wait is not yet implemented
1774    async fn batch_builder_add_part_before_lower() {
1775        let client = new_test_client(&ConfigUpdates::default()).await;
1776        let (write, _) = client
1777            .expect_open::<String, String, u64, i64>(ShardId::new())
1778            .await;
1779        let mut builder = write.builder(Antichain::from_elem(2));
1780        let part = string_part(&[(("1", "one"), 2, 1), (("2", "two"), 1, 1)]);
1781        match builder.add_part(part).await {
1782            Err(InvalidUsage::UpdateNotBeyondLower { ts, .. }) => assert_eq!(ts, 1),
1783            other => panic!("expected UpdateNotBeyondLower, got {other:?}"),
1784        }
1785    }
1786
1787    #[mz_ore::test(tokio::test)]
1788    #[cfg_attr(miri, ignore)] // unsupported operation: returning ready events from epoll_wait is not yet implemented
1789    async fn batch_builder_add_part_flushing() {
1790        let small = || string_part(&[(("1", "one"), 1, 1)]);
1791        let big = string_part(&[
1792            (("2", "two"), 1, 1),
1793            (("3", "three"), 1, 1),
1794            (("4", "four"), 1, 1),
1795            (("5", "five"), 1, 1),
1796        ]);
1797        let small_bytes = small().goodbytes();
1798        assert!(
1799            big.goodbytes() > 2 * small_bytes,
1800            "big part must exceed the target"
1801        );
1802
1803        let cache = PersistClientCache::new_no_metrics();
1804        // Two small parts fit the target exactly. Disable inline writes so every flushed part is
1805        // a separate blob that `part_count` sees.
1806        cache.cfg.set_config(&BLOB_TARGET_SIZE, 2 * small_bytes);
1807        cache.cfg.set_config(&INLINE_WRITES_SINGLE_MAX_BYTES, 0);
1808        cache.cfg.set_config(&INLINE_WRITES_TOTAL_MAX_BYTES, 0);
1809        let client = cache
1810            .open(PersistLocation::new_in_mem())
1811            .await
1812            .expect("client construction failed");
1813        let (mut write, mut read) = client
1814            .expect_open::<String, String, u64, i64>(ShardId::new())
1815            .await;
1816
1817        let mut builder = write.builder(Antichain::from_elem(0));
1818        // Parts accumulate while they fit the target.
1819        let added = builder.add_part(small()).await.expect("valid usage");
1820        assert!(matches!(added, Added::Record), "first part accumulates");
1821        let added = builder.add_part(small()).await.expect("valid usage");
1822        assert!(
1823            matches!(added, Added::Record),
1824            "second part fits the target"
1825        );
1826        // The third would exceed the target, so the first two flush together.
1827        let added = builder.add_part(small()).await.expect("valid usage");
1828        assert!(matches!(added, Added::RecordAndParts), "third part flushes");
1829        // An oversized part flushes the accumulated part first, and then on its own.
1830        let added = builder.add_part(big).await.expect("valid usage");
1831        assert!(
1832            matches!(added, Added::RecordAndParts),
1833            "oversized part flushes"
1834        );
1835        let added = builder.add_part(small()).await.expect("valid usage");
1836        assert!(
1837            matches!(added, Added::Record),
1838            "accounting resets after a flush"
1839        );
1840
1841        let batch = builder
1842            .finish(Antichain::from_elem(2))
1843            .await
1844            .expect("valid usage");
1845        // [small, small], [small], [big], [small]
1846        assert_eq!(batch.batch.part_count(), 4);
1847        write
1848            .append_batch(batch, Antichain::from_elem(0), Antichain::from_elem(2))
1849            .await
1850            .expect("valid usage")
1851            .expect("unexpected upper");
1852        let mut expected = vec![
1853            (("1".to_owned(), "one".to_owned()), 1, 4),
1854            (("2".to_owned(), "two".to_owned()), 1, 1),
1855            (("3".to_owned(), "three".to_owned()), 1, 1),
1856            (("4".to_owned(), "four".to_owned()), 1, 1),
1857            (("5".to_owned(), "five".to_owned()), 1, 1),
1858        ];
1859        expected.sort();
1860        assert_eq!(
1861            read.expect_snapshot_and_fetch(1).await,
1862            all_ok(&expected, 1)
1863        );
1864    }
1865
1866    #[mz_ore::test(tokio::test)]
1867    #[cfg_attr(miri, ignore)] // unsupported operation: returning ready events from epoll_wait is not yet implemented
1868    async fn batch_delete() {
1869        let cache = PersistClientCache::new_no_metrics();
1870        cache.cfg.set_config(&INLINE_WRITES_SINGLE_MAX_BYTES, 0);
1871        cache.cfg.set_config(&INLINE_WRITES_TOTAL_MAX_BYTES, 0);
1872        cache.cfg.set_config(&BATCH_DELETE_ENABLED, true);
1873        let client = cache
1874            .open(PersistLocation::new_in_mem())
1875            .await
1876            .expect("client construction failed");
1877        let shard_id = ShardId::new();
1878        let (mut write, _) = client
1879            .expect_open::<String, String, u64, i64>(shard_id)
1880            .await;
1881
1882        let batch = write
1883            .expect_batch(
1884                &[
1885                    (("1".into(), "one".into()), 1, 1),
1886                    (("2".into(), "two".into()), 2, 1),
1887                    (("3".into(), "three".into()), 3, 1),
1888                ],
1889                0,
1890                4,
1891            )
1892            .await;
1893
1894        assert_eq!(batch.batch.part_count(), 1);
1895        let part_key = batch.batch.parts[0]
1896            .expect_hollow_part()
1897            .key
1898            .complete(&shard_id);
1899
1900        let part_bytes = client.blob.get(&part_key).await.expect("invalid usage");
1901        assert!(part_bytes.is_some());
1902
1903        batch.delete().await;
1904
1905        let part_bytes = client.blob.get(&part_key).await.expect("invalid usage");
1906        assert!(part_bytes.is_none());
1907    }
1908
1909    #[mz_ore::test]
1910    fn untrimmable_columns() {
1911        let untrimmable = UntrimmableColumns {
1912            equals: vec!["abc".into(), "def".into()],
1913            prefixes: vec!["123".into(), "234".into()],
1914            suffixes: vec!["xyz".into()],
1915        };
1916
1917        // equals
1918        assert!(untrimmable.should_retain("abc"));
1919        assert!(untrimmable.should_retain("ABC"));
1920        assert!(untrimmable.should_retain("aBc"));
1921        assert!(!untrimmable.should_retain("abcd"));
1922        assert!(untrimmable.should_retain("deF"));
1923        assert!(!untrimmable.should_retain("defg"));
1924
1925        // prefix
1926        assert!(untrimmable.should_retain("123"));
1927        assert!(untrimmable.should_retain("123-4"));
1928        assert!(untrimmable.should_retain("1234"));
1929        assert!(untrimmable.should_retain("234"));
1930        assert!(!untrimmable.should_retain("345"));
1931
1932        // suffix
1933        assert!(untrimmable.should_retain("ijk_xyZ"));
1934        assert!(untrimmable.should_retain("ww-XYZ"));
1935        assert!(!untrimmable.should_retain("xya"));
1936    }
1937
1938    // NB: Most edge cases are exercised in datadriven tests.
1939    #[mz_persist_proc::test(tokio::test)]
1940    #[cfg_attr(miri, ignore)] // too slow
1941    async fn rewrite_ts_example(dyncfgs: ConfigUpdates) {
1942        let client = new_test_client(&dyncfgs).await;
1943        let (mut write, read) = client
1944            .expect_open::<String, (), u64, i64>(ShardId::new())
1945            .await;
1946
1947        let mut batch = write.builder(Antichain::from_elem(0));
1948        batch.add(&"foo".to_owned(), &(), &0, &1).await.unwrap();
1949        let batch = batch.finish(Antichain::from_elem(1)).await.unwrap();
1950
1951        // Roundtrip through a transmittable batch.
1952        let batch = batch.into_transmittable_batch();
1953        let mut batch = write.batch_from_transmittable_batch(batch);
1954        batch
1955            .rewrite_ts(&Antichain::from_elem(2), Antichain::from_elem(3))
1956            .unwrap();
1957        write
1958            .expect_compare_and_append_batch(&mut [&mut batch], 0, 3)
1959            .await;
1960
1961        let (actual, _) = read.expect_listen(0).await.read_until(&3).await;
1962        let expected = vec![((("foo".to_owned()), ()), 2, 1)];
1963        assert_eq!(actual, expected);
1964    }
1965
1966    #[mz_ore::test(tokio::test)]
1967    #[cfg_attr(miri, ignore)] // unsupported operation: returning ready events from epoll_wait is not yet implemented
1968    async fn structured_lowers() {
1969        let cache = PersistClientCache::new_no_metrics();
1970        // Ensure structured data is calculated, and that we give some budget for a key lower.
1971        cache.cfg().set_config(&STRUCTURED_KEY_LOWER_LEN, 1024);
1972        // Otherwise fails: expected hollow part!
1973        cache.cfg().set_config(&INLINE_WRITES_SINGLE_MAX_BYTES, 0);
1974        cache.cfg().set_config(&INLINE_WRITES_TOTAL_MAX_BYTES, 0);
1975        let client = cache
1976            .open(PersistLocation::new_in_mem())
1977            .await
1978            .expect("client construction failed");
1979        let shard_id = ShardId::new();
1980        let (mut write, _) = client
1981            .expect_open::<String, String, u64, i64>(shard_id)
1982            .await;
1983
1984        let batch = write
1985            .expect_batch(
1986                &[
1987                    (("1".into(), "one".into()), 1, 1),
1988                    (("2".into(), "two".into()), 2, 1),
1989                    (("3".into(), "three".into()), 3, 1),
1990                ],
1991                0,
1992                4,
1993            )
1994            .await;
1995
1996        assert_eq!(batch.batch.part_count(), 1);
1997        let [part] = batch.batch.parts.as_slice() else {
1998            panic!("expected single part")
1999        };
2000        // Verifies that the structured key lower is stored and decoded.
2001        assert!(part.structured_key_lower().is_some());
2002    }
2003
2004    #[mz_ore::test]
2005    fn validate_truncate_batch_bounds() {
2006        use crate::internal::state::tests::hollow;
2007
2008        let desc = |lower: u64, upper: u64| {
2009            Description::new(
2010                Antichain::from_elem(lower),
2011                Antichain::from_elem(upper),
2012                Antichain::from_elem(0),
2013            )
2014        };
2015
2016        // Appending under the batch's own desc is not a truncation.
2017        let batch = hollow(0, 5, &["k"], 1);
2018        assert_eq!(
2019            validate_truncate_batch(&batch, &desc(0, 5), false, true).expect("valid"),
2020            false
2021        );
2022
2023        // A narrower append desc may leave part data outside the registered
2024        // bounds, on either side.
2025        assert_eq!(
2026            validate_truncate_batch(&batch, &desc(1, 5), false, true).expect("valid"),
2027            true
2028        );
2029        assert_eq!(
2030            validate_truncate_batch(&batch, &desc(0, 4), false, true).expect("valid"),
2031            true
2032        );
2033
2034        // A ts-rewritten batch keeps its write-time lower, but the rewrite
2035        // validation proves that no part holds data outside the append desc,
2036        // so it must not count as a truncation.
2037        let mut rewritten = hollow(0, 5, &["k"], 1);
2038        for part in rewritten.parts.iter_mut() {
2039            let RunPart::Single(BatchPart::Hollow(part)) = part else {
2040                panic!("expected hollow part");
2041            };
2042            part.ts_rewrite = Some(Antichain::from_elem(3));
2043        }
2044        assert_eq!(
2045            validate_truncate_batch(&rewritten, &desc(2, 5), true, true).expect("valid"),
2046            false
2047        );
2048    }
2049}