Skip to main content

mz_persist/indexed/
encoding.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//! Data structures stored in Blobs and Logs in serialized form.
11
12// NB: Everything starting with Blob* is directly serialized as a Blob value.
13// Ditto for Log* and the Log. The others are used internally in these top-level
14// structs.
15
16use std::fmt::{self, Debug};
17use std::sync::Arc;
18
19use arrow::array::{Array, ArrayRef, AsArray, BinaryArray, Int64Array};
20use arrow::buffer::OffsetBuffer;
21use arrow::datatypes::{DataType, Int64Type, ToByteSlice};
22use base64::Engine;
23use bytes::{BufMut, Bytes};
24use differential_dataflow::difference::Monoid;
25use differential_dataflow::trace::Description;
26use mz_ore::bytes::SegmentedBytes;
27use mz_ore::cast::CastFrom;
28use mz_ore::collections::CollectionExt;
29use mz_ore::soft_panic_or_log;
30use mz_persist_types::arrow::{ArrayBound, ArrayOrd};
31use mz_persist_types::columnar::{
32    ColumnEncoder, Schema, codec_to_schema, data_type, schema_to_codec,
33};
34use mz_persist_types::parquet::EncodingConfig;
35use mz_persist_types::part::Part;
36use mz_persist_types::schema::backward_compatible;
37use mz_persist_types::{Codec, Codec64};
38use mz_proto::{RustType, TryFromProtoError};
39use proptest::arbitrary::Arbitrary;
40use proptest::prelude::*;
41use proptest::strategy::{BoxedStrategy, Just};
42use prost::Message;
43use serde::Serialize;
44use timely::PartialOrder;
45use timely::progress::{Antichain, Timestamp};
46use tracing::error;
47
48use crate::error::Error;
49use crate::generated::persist::proto_batch_part_inline::FormatMetadata as ProtoFormatMetadata;
50use crate::generated::persist::{
51    ProtoBatchFormat, ProtoBatchPartInline, ProtoColumnarRecords, ProtoU64Antichain,
52    ProtoU64Description,
53};
54use crate::indexed::columnar::arrow::realloc_array;
55use crate::indexed::columnar::parquet::{decode_trace_parquet, encode_trace_parquet};
56use crate::indexed::columnar::{ColumnarRecords, ColumnarRecordsStructuredExt};
57use crate::location::Blob;
58use crate::metrics::ColumnarMetrics;
59
60/// Column format of a batch.
61#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
62pub enum BatchColumnarFormat {
63    /// Rows are encoded to `ProtoRow` and then a batch is written down as a Parquet with a schema
64    /// of `(k, v, t, d)`, where `k` are the serialized bytes.
65    Row,
66    /// Rows are encoded to `ProtoRow` and a columnar struct. The batch is written down as Parquet
67    /// with a schema of `(k, k_c, v, v_c, t, d)`, where `k` are the serialized bytes and `k_c` is
68    /// nested columnar data.
69    Both(usize),
70    /// Rows are encoded to a columnar struct. The batch is written down as Parquet
71    /// with a schema of `(t, d, k_s, v_s)`, where `k_s` is nested columnar data.
72    Structured,
73}
74
75impl BatchColumnarFormat {
76    /// Returns a default value for [`BatchColumnarFormat`].
77    pub const fn default() -> Self {
78        BatchColumnarFormat::Both(2)
79    }
80
81    /// Returns a [`BatchColumnarFormat`] for a given `&str`, falling back to a default value if
82    /// provided `&str` is invalid.
83    pub fn from_str(s: &str) -> Self {
84        match s {
85            "row" => BatchColumnarFormat::Row,
86            "both" => BatchColumnarFormat::Both(0),
87            "both_v2" => BatchColumnarFormat::Both(2),
88            "structured" => BatchColumnarFormat::Structured,
89            x => {
90                let default = BatchColumnarFormat::default();
91                soft_panic_or_log!("Invalid batch columnar type: {x}, falling back to {default}");
92                default
93            }
94        }
95    }
96
97    /// Returns a string representation for the [`BatchColumnarFormat`].
98    pub const fn as_str(&self) -> &'static str {
99        match self {
100            BatchColumnarFormat::Row => "row",
101            BatchColumnarFormat::Both(0 | 1) => "both",
102            BatchColumnarFormat::Both(2) => "both_v2",
103            BatchColumnarFormat::Structured => "structured",
104            BatchColumnarFormat::Both(_) => panic!("unknown batch columnar format"),
105        }
106    }
107
108    /// Returns if we should encode a Batch in a structured format.
109    pub const fn is_structured(&self) -> bool {
110        match self {
111            BatchColumnarFormat::Row => false,
112            // The V0 format has been deprecated and we ignore its structured columns.
113            BatchColumnarFormat::Both(0 | 1) => false,
114            BatchColumnarFormat::Both(_) => true,
115            BatchColumnarFormat::Structured => true,
116        }
117    }
118}
119
120impl fmt::Display for BatchColumnarFormat {
121    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
122        f.write_str(self.as_str())
123    }
124}
125
126impl Arbitrary for BatchColumnarFormat {
127    type Parameters = ();
128    type Strategy = BoxedStrategy<BatchColumnarFormat>;
129
130    fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
131        proptest::strategy::Union::new(vec![
132            Just(BatchColumnarFormat::Row).boxed(),
133            Just(BatchColumnarFormat::Both(0)).boxed(),
134            Just(BatchColumnarFormat::Both(1)).boxed(),
135            Just(BatchColumnarFormat::Both(2)).boxed(),
136            Just(BatchColumnarFormat::Structured).boxed(),
137        ])
138        .boxed()
139    }
140}
141
142/// The metadata necessary to reconstruct a list of [BlobTraceBatchPart]s.
143///
144/// Invariants:
145/// - The Description's time interval is non-empty.
146/// - Keys for all trace batch parts are unique.
147/// - Keys for all trace batch parts are stored in index order.
148/// - The data in all of the trace batch parts is sorted and consolidated.
149/// - All of the trace batch parts have the same desc as the metadata.
150#[derive(Clone, Debug, Eq, PartialEq)]
151pub struct TraceBatchMeta {
152    /// The keys to retrieve the batch's data from the blob store.
153    ///
154    /// The set of keys can be empty to denote an empty batch.
155    pub keys: Vec<String>,
156    /// The format of the stored batch data.
157    pub format: ProtoBatchFormat,
158    /// The half-open time interval `[lower, upper)` this batch contains data
159    /// for.
160    pub desc: Description<u64>,
161    /// The compaction level of each batch.
162    pub level: u64,
163    /// Size of the encoded batch.
164    pub size_bytes: u64,
165}
166
167/// The structure serialized and stored as a value in
168/// [crate::location::Blob] storage for data keys corresponding to trace
169/// data.
170///
171/// This batch represents the data that was originally written at some time in
172/// [lower, upper) (more precisely !< lower and < upper). The individual record
173/// times may have later been advanced by compaction to something <= since. This
174/// means the ability to reconstruct the state of the collection at times <
175/// since has been lost. However, there may still be records present in the
176/// batch whose times are < since. Users iterating through updates must take
177/// care to advance records with times < since to since in order to correctly
178/// answer queries at times >= since.
179///
180/// Invariants:
181/// - The [lower, upper) interval of times in desc is non-empty.
182/// - The timestamp of each update is >= to desc.lower().
183/// - The timestamp of each update is < desc.upper() iff desc.upper() >
184///   desc.since(). Otherwise the timestamp of each update is <= desc.since().
185/// - The values in updates are sorted by (key, value, time).
186/// - The values in updates are "consolidated", i.e. (key, value, time) is
187///   unique.
188/// - All entries have a non-zero diff.
189///
190/// TODO: disallow empty trace batch parts in the future so there is one unique
191/// way to represent an empty trace batch.
192#[derive(Clone, Debug, PartialEq)]
193pub struct BlobTraceBatchPart<T> {
194    /// Which updates are included in this batch.
195    ///
196    /// There may be other parts for the batch that also contain updates within
197    /// the specified [lower, upper) range.
198    pub desc: Description<T>,
199    /// Index of this part in the list of parts that form the batch.
200    pub index: u64,
201    /// The updates themselves.
202    pub updates: BlobTraceUpdates,
203}
204
205/// The set of updates that are part of a [`BlobTraceBatchPart`].
206#[derive(Clone, Debug, PartialEq)]
207pub enum BlobTraceUpdates {
208    /// Legacy format. Keys and Values are encoded into bytes via [`Codec`], then stored in our own
209    /// columnar-esque struct.
210    ///
211    /// [`Codec`]: mz_persist_types::Codec
212    Row(ColumnarRecords),
213    /// Migration format. Keys and Values are encoded into bytes via [`Codec`] and structured into
214    /// an Apache Arrow columnar format.
215    ///
216    /// [`Codec`]: mz_persist_types::Codec
217    Both(ColumnarRecords, ColumnarRecordsStructuredExt),
218    /// New-style structured format, including structured representations of the K/V columns and
219    /// the usual timestamp / diff encoding.
220    Structured {
221        /// Key-value data.
222        key_values: ColumnarRecordsStructuredExt,
223        /// Timestamp data.
224        timestamps: Int64Array,
225        /// Diffs.
226        diffs: Int64Array,
227    },
228}
229
230impl BlobTraceUpdates {
231    /// Convert from a [Part].
232    pub fn from_part(part: Part) -> Self {
233        Self::Structured {
234            key_values: ColumnarRecordsStructuredExt {
235                key: part.key,
236                val: part.val,
237            },
238            timestamps: part.time,
239            diffs: part.diff,
240        }
241    }
242
243    /// The number of updates.
244    pub fn len(&self) -> usize {
245        match self {
246            BlobTraceUpdates::Row(c) => c.len(),
247            BlobTraceUpdates::Both(c, _structured) => c.len(),
248            BlobTraceUpdates::Structured { timestamps, .. } => timestamps.len(),
249        }
250    }
251
252    /// The updates' timestamps as an integer array.
253    pub fn timestamps(&self) -> &Int64Array {
254        match self {
255            BlobTraceUpdates::Row(c) => c.timestamps(),
256            BlobTraceUpdates::Both(c, _structured) => c.timestamps(),
257            BlobTraceUpdates::Structured { timestamps, .. } => timestamps,
258        }
259    }
260
261    /// The updates' diffs as an integer array.
262    pub fn diffs(&self) -> &Int64Array {
263        match self {
264            BlobTraceUpdates::Row(c) => c.diffs(),
265            BlobTraceUpdates::Both(c, _structured) => c.diffs(),
266            BlobTraceUpdates::Structured { diffs, .. } => diffs,
267        }
268    }
269
270    /// Return the sum of the diffs in the blob.
271    pub fn diffs_sum<D: Codec64 + Monoid>(&self) -> D {
272        let mut sum = D::zero();
273        for d in self.diffs().values().iter() {
274            let d = D::decode(d.to_le_bytes());
275            sum.plus_equals(&d);
276        }
277        sum
278    }
279
280    /// Return the [`ColumnarRecords`] of the blob.
281    pub fn records(&self) -> Option<&ColumnarRecords> {
282        match self {
283            BlobTraceUpdates::Row(c) => Some(c),
284            BlobTraceUpdates::Both(c, _structured) => Some(c),
285            BlobTraceUpdates::Structured { .. } => None,
286        }
287    }
288
289    /// Return the [`ColumnarRecordsStructuredExt`] of the blob.
290    pub fn structured(&self) -> Option<&ColumnarRecordsStructuredExt> {
291        match self {
292            BlobTraceUpdates::Row(_) => None,
293            BlobTraceUpdates::Both(_, s) => Some(s),
294            BlobTraceUpdates::Structured { key_values, .. } => Some(key_values),
295        }
296    }
297
298    /// Return the estimated memory usage of the raw data.
299    pub fn goodbytes(&self) -> usize {
300        match self {
301            BlobTraceUpdates::Row(c) => c.goodbytes(),
302            // NB: we only report goodbytes for columnar records here, to avoid
303            // dual-counting the same records. (This means that our goodput % is much lower
304            // during the migration, which is an accurate reflection of reality.)
305            BlobTraceUpdates::Both(c, _) => c.goodbytes(),
306            BlobTraceUpdates::Structured {
307                key_values,
308                timestamps,
309                diffs,
310            } => {
311                key_values.goodbytes()
312                    + timestamps.values().to_byte_slice().len()
313                    + diffs.values().to_byte_slice().len()
314            }
315        }
316    }
317
318    /// Return the [ColumnarRecords] of the blob, generating it if it does not exist.
319    pub fn get_or_make_codec<K: Codec, V: Codec>(
320        &mut self,
321        key_schema: &K::Schema,
322        val_schema: &V::Schema,
323    ) -> &ColumnarRecords {
324        match self {
325            BlobTraceUpdates::Row(records) => records,
326            BlobTraceUpdates::Both(records, _) => records,
327            BlobTraceUpdates::Structured {
328                key_values,
329                timestamps,
330                diffs,
331            } => {
332                let key = schema_to_codec::<K>(key_schema, &*key_values.key).expect("valid keys");
333                let val = schema_to_codec::<V>(val_schema, &*key_values.val).expect("valid values");
334                let records = ColumnarRecords::new(key, val, timestamps.clone(), diffs.clone());
335
336                *self = BlobTraceUpdates::Both(records, key_values.clone());
337                let BlobTraceUpdates::Both(records, _) = self else {
338                    unreachable!("set to BlobTraceUpdates::Both in previous line")
339                };
340                records
341            }
342        }
343    }
344
345    /// Return the [`ColumnarRecordsStructuredExt`] of the blob.
346    pub fn get_or_make_structured<K: Codec, V: Codec>(
347        &mut self,
348        key_schema: &K::Schema,
349        val_schema: &V::Schema,
350    ) -> &ColumnarRecordsStructuredExt {
351        let structured = match self {
352            BlobTraceUpdates::Row(records) => {
353                let key = codec_to_schema::<K>(key_schema, records.keys()).expect("valid keys");
354                let val = codec_to_schema::<V>(val_schema, records.vals()).expect("valid values");
355
356                *self = BlobTraceUpdates::Both(
357                    records.clone(),
358                    ColumnarRecordsStructuredExt { key, val },
359                );
360                let BlobTraceUpdates::Both(_, structured) = self else {
361                    unreachable!("set to BlobTraceUpdates::Both in previous line")
362                };
363                structured
364            }
365            BlobTraceUpdates::Both(_, structured) => structured,
366            BlobTraceUpdates::Structured { key_values, .. } => key_values,
367        };
368
369        // If the types don't match, attempt to migrate the array to the new type.
370        // We expect this to succeed, since this should only be called with backwards-
371        // compatible schemas... but if it fails we only log, and let some higher-level
372        // code signal the error if it cares.
373        let migrate = |array: &mut ArrayRef, to_type: DataType| {
374            // TODO: Plumb down the SchemaCache and use it here for the array migrations.
375            let from_type = array.data_type().clone();
376            if from_type != to_type {
377                if let Some(migration) = backward_compatible(&from_type, &to_type) {
378                    *array = migration.migrate(Arc::clone(array));
379                    if array.data_type() != &to_type {
380                        error!(
381                            ?from_type,
382                            actual_type=?array.data_type(),
383                            ?to_type,
384                            "migration failed; returned non-matching data type!"
385                        );
386                    }
387                } else {
388                    error!(
389                        ?from_type,
390                        ?to_type,
391                        "failed to migrate array type; backwards-incompatible schema migration?"
392                    );
393                }
394            }
395        };
396        migrate(
397            &mut structured.key,
398            data_type::<K>(key_schema).expect("valid key schema"),
399        );
400        migrate(
401            &mut structured.val,
402            data_type::<V>(val_schema).expect("valid value schema"),
403        );
404
405        structured
406    }
407
408    /// If we have structured data, cast this data as a structured part.
409    pub fn as_part(&self) -> Option<Part> {
410        let ext = self.structured()?.clone();
411        Some(Part {
412            key: ext.key,
413            val: ext.val,
414            time: self.timestamps().clone(),
415            diff: self.diffs().clone(),
416        })
417    }
418
419    /// Convert this blob into a structured part, transforming the codec data if necessary.
420    pub fn into_part<K: Codec, V: Codec>(
421        &mut self,
422        key_schema: &K::Schema,
423        val_schema: &V::Schema,
424    ) -> Part {
425        let ext = self
426            .get_or_make_structured::<K, V>(key_schema, val_schema)
427            .clone();
428        Part {
429            key: ext.key,
430            val: ext.val,
431            time: self.timestamps().clone(),
432            diff: self.diffs().clone(),
433        }
434    }
435
436    /// Concatenate the given records together, column-by-column.
437    ///
438    /// If `ensure_codec` is true, then we'll ensure the returned [`BlobTraceUpdates`] includes
439    /// [`Codec`] data, re-encoding structured data if necessary.
440    pub fn concat<K: Codec, V: Codec>(
441        mut updates: Vec<BlobTraceUpdates>,
442        key_schema: &K::Schema,
443        val_schema: &V::Schema,
444        metrics: &ColumnarMetrics,
445    ) -> anyhow::Result<BlobTraceUpdates> {
446        match updates.len() {
447            0 => {
448                return Ok(BlobTraceUpdates::Structured {
449                    key_values: ColumnarRecordsStructuredExt {
450                        key: Arc::new(key_schema.encoder().expect("valid schema").finish()),
451                        val: Arc::new(val_schema.encoder().expect("valid schema").finish()),
452                    },
453                    timestamps: Int64Array::from_iter_values(vec![]),
454                    diffs: Int64Array::from_iter_values(vec![]),
455                });
456            }
457            1 => return Ok(updates.into_iter().into_element()),
458            _ => {}
459        }
460
461        // Always get or make our structured format.
462        let mut keys = Vec::with_capacity(updates.len());
463        let mut vals = Vec::with_capacity(updates.len());
464        for updates in &mut updates {
465            let structured = updates.get_or_make_structured::<K, V>(key_schema, val_schema);
466            keys.push(structured.key.as_ref());
467            vals.push(structured.val.as_ref());
468        }
469        let key_values = ColumnarRecordsStructuredExt {
470            key: ::arrow::compute::concat(&keys)?,
471            val: ::arrow::compute::concat(&vals)?,
472        };
473
474        // If necessary, ensure we have `Codec` data, which internally includes
475        // timestamps and diffs, otherwise get the timestamps and diffs separately.
476        let mut timestamps: Vec<&dyn Array> = Vec::with_capacity(updates.len());
477        let mut diffs: Vec<&dyn Array> = Vec::with_capacity(updates.len());
478
479        for update in &updates {
480            timestamps.push(update.timestamps());
481            diffs.push(update.diffs());
482        }
483        let timestamps = ::arrow::compute::concat(&timestamps)?
484            .as_primitive_opt::<Int64Type>()
485            .ok_or_else(|| anyhow::anyhow!("timestamps changed Array type"))?
486            .clone();
487        let diffs = ::arrow::compute::concat(&diffs)?
488            .as_primitive_opt::<Int64Type>()
489            .ok_or_else(|| anyhow::anyhow!("diffs changed Array type"))?
490            .clone();
491
492        let out = Self::Structured {
493            key_values,
494            timestamps,
495            diffs,
496        };
497        metrics
498            .arrow
499            .concat_bytes
500            .inc_by(u64::cast_from(out.goodbytes()));
501        Ok(out)
502    }
503
504    /// See [RustType::from_proto].
505    pub fn from_proto(
506        lgbytes: &ColumnarMetrics,
507        proto: ProtoColumnarRecords,
508    ) -> Result<Self, TryFromProtoError> {
509        let binary_array = |data: Bytes, offsets: Vec<i32>| {
510            if offsets.is_empty() && proto.len > 0 {
511                return Ok(None);
512            };
513            match BinaryArray::try_new(
514                OffsetBuffer::new(offsets.into()),
515                ::arrow::buffer::Buffer::from(data),
516                None,
517            ) {
518                Ok(data) => Ok(Some(realloc_array(&data, lgbytes))),
519                Err(e) => Err(TryFromProtoError::InvalidFieldError(format!(
520                    "Unable to decode binary array from repeated proto fields: {e:?}"
521                ))),
522            }
523        };
524
525        let codec_key = binary_array(proto.key_data, proto.key_offsets)?;
526        let codec_val = binary_array(proto.val_data, proto.val_offsets)?;
527
528        let timestamps = realloc_array(&proto.timestamps.into(), lgbytes);
529        let diffs = realloc_array(&proto.diffs.into(), lgbytes);
530        let ext =
531            ColumnarRecordsStructuredExt::from_proto(proto.key_structured, proto.val_structured)?;
532
533        let updates = match (codec_key, codec_val, ext) {
534            (Some(codec_key), Some(codec_val), Some(ext)) => BlobTraceUpdates::Both(
535                ColumnarRecords::new(codec_key, codec_val, timestamps, diffs),
536                ext,
537            ),
538            (Some(codec_key), Some(codec_val), None) => BlobTraceUpdates::Row(
539                ColumnarRecords::new(codec_key, codec_val, timestamps, diffs),
540            ),
541            (None, None, Some(ext)) => BlobTraceUpdates::Structured {
542                key_values: ext,
543                timestamps,
544                diffs,
545            },
546            (k, v, ext) => {
547                return Err(TryFromProtoError::InvalidPersistState(format!(
548                    "unexpected mix of key/value columns: k={:?}, v={}, ext={}",
549                    k.is_some(),
550                    v.is_some(),
551                    ext.is_some(),
552                )));
553            }
554        };
555
556        Ok(updates)
557    }
558
559    /// See [RustType::into_proto].
560    pub fn into_proto(&self) -> ProtoColumnarRecords {
561        let (key_offsets, key_data, val_offsets, val_data) = match self.records() {
562            None => (vec![], Bytes::new(), vec![], Bytes::new()),
563            Some(records) => (
564                records.keys().offsets().to_vec(),
565                Bytes::copy_from_slice(records.keys().value_data()),
566                records.vals().offsets().to_vec(),
567                Bytes::copy_from_slice(records.vals().value_data()),
568            ),
569        };
570        let (k_struct, v_struct) = match self.structured().map(|x| x.into_proto()) {
571            None => (None, None),
572            Some((k, v)) => (Some(k), Some(v)),
573        };
574
575        ProtoColumnarRecords {
576            len: self.len().into_proto(),
577            key_offsets,
578            key_data,
579            val_offsets,
580            val_data,
581            timestamps: self.timestamps().values().to_vec(),
582            diffs: self.diffs().values().to_vec(),
583            key_structured: k_struct,
584            val_structured: v_struct,
585        }
586    }
587
588    /// Convert these updates into the specified batch format, re-encoding or discarding key-value
589    /// data as necessary.
590    pub fn as_structured<K: Codec, V: Codec>(
591        &self,
592        key_schema: &K::Schema,
593        val_schema: &V::Schema,
594    ) -> Self {
595        let mut this = self.clone();
596        Self::Structured {
597            key_values: this
598                .get_or_make_structured::<K, V>(key_schema, val_schema)
599                .clone(),
600            timestamps: this.timestamps().clone(),
601            diffs: this.diffs().clone(),
602        }
603    }
604}
605
606impl TraceBatchMeta {
607    /// Asserts Self's documented invariants, returning an error if any are
608    /// violated.
609    pub fn validate(&self) -> Result<(), Error> {
610        // TODO: It's unclear if the equal case (an empty desc) is
611        // useful/harmful. Feel free to make this a less_than if empty descs end
612        // up making sense.
613        if PartialOrder::less_equal(self.desc.upper(), self.desc.lower()) {
614            return Err(format!("invalid desc: {:?}", self.desc).into());
615        }
616
617        Ok(())
618    }
619
620    /// Assert that all of the [BlobTraceBatchPart]'s obey the required invariants.
621    pub async fn validate_data(
622        &self,
623        blob: &dyn Blob,
624        metrics: &ColumnarMetrics,
625    ) -> Result<(), Error> {
626        let mut batches = vec![];
627        for (idx, key) in self.keys.iter().enumerate() {
628            let value = blob
629                .get(key)
630                .await?
631                .ok_or_else(|| Error::from(format!("no blob for trace batch at key: {}", key)))?;
632            let batch = BlobTraceBatchPart::decode(&value, metrics)?;
633            if batch.desc != self.desc {
634                return Err(format!(
635                    "invalid trace batch part desc expected {:?} got {:?}",
636                    self.desc, batch.desc
637                )
638                .into());
639            }
640
641            if batch.index != u64::cast_from(idx) {
642                return Err(format!(
643                    "invalid index for blob trace batch part at key {} expected {} got {}",
644                    key, idx, batch.index
645                )
646                .into());
647            }
648
649            batch.validate()?;
650            batches.push(batch);
651        }
652
653        for (batch_idx, batch) in batches.iter().enumerate() {
654            for (row_idx, diff) in batch.updates.diffs().values().iter().enumerate() {
655                // TODO: Don't assume diff is an i64, take a D type param instead.
656                let diff: u64 = Codec64::decode(diff.to_le_bytes());
657
658                // Check data invariants.
659                if diff == 0 {
660                    return Err(format!(
661                        "update with 0 diff in batch {batch_idx} at row {row_idx}",
662                    )
663                    .into());
664                }
665            }
666        }
667
668        Ok(())
669    }
670}
671
672impl<T: Timestamp + Codec64> BlobTraceBatchPart<T> {
673    /// Asserts the documented invariants, returning an error if any are
674    /// violated.
675    pub fn validate(&self) -> Result<(), Error> {
676        validate_trace_updates(&self.desc, &self.updates)
677    }
678
679    /// Encodes an BlobTraceBatchPart into the Parquet format.
680    pub fn encode<B>(&self, buf: &mut B, metrics: &ColumnarMetrics, cfg: &EncodingConfig)
681    where
682        B: BufMut + Send,
683    {
684        encode_trace_parquet(&mut buf.writer(), self, metrics, cfg).expect("batch was invalid");
685    }
686
687    /// Decodes a BlobTraceBatchPart from the Parquet format.
688    pub fn decode(buf: &SegmentedBytes, metrics: &ColumnarMetrics) -> Result<Self, Error> {
689        decode_trace_parquet(buf.clone(), metrics)
690    }
691
692    /// Scans the part and returns a lower bound on the contained keys.
693    pub fn key_lower(&self) -> &[u8] {
694        self.updates
695            .records()
696            .and_then(|r| r.keys().iter().flatten().min())
697            .unwrap_or(&[])
698    }
699
700    /// Scans the part and returns a lower bound on the contained keys.
701    pub fn structured_key_lower(&self) -> Option<ArrayBound> {
702        self.updates.structured().and_then(|r| {
703            let ord = ArrayOrd::new(&r.key);
704            (0..r.key.len())
705                .min_by_key(|i| ord.at(*i))
706                .map(|i| ArrayBound::new(Arc::clone(&r.key), i))
707        })
708    }
709}
710
711impl<T: Timestamp + Codec64> From<ProtoU64Description> for Description<T> {
712    fn from(x: ProtoU64Description) -> Self {
713        Description::new(
714            x.lower
715                .map_or_else(|| Antichain::from_elem(T::minimum()), |x| x.into()),
716            x.upper
717                .map_or_else(|| Antichain::from_elem(T::minimum()), |x| x.into()),
718            x.since
719                .map_or_else(|| Antichain::from_elem(T::minimum()), |x| x.into()),
720        )
721    }
722}
723
724impl<T: Timestamp + Codec64> From<ProtoU64Antichain> for Antichain<T> {
725    fn from(x: ProtoU64Antichain) -> Self {
726        Antichain::from(
727            x.elements
728                .into_iter()
729                .map(|x| T::decode(u64::to_le_bytes(x)))
730                .collect::<Vec<_>>(),
731        )
732    }
733}
734
735impl<T: Timestamp + Codec64> From<&Antichain<T>> for ProtoU64Antichain {
736    fn from(x: &Antichain<T>) -> Self {
737        ProtoU64Antichain {
738            elements: x
739                .elements()
740                .iter()
741                .map(|x| u64::from_le_bytes(T::encode(x)))
742                .collect(),
743        }
744    }
745}
746
747impl<T: Timestamp + Codec64> From<&Description<T>> for ProtoU64Description {
748    fn from(x: &Description<T>) -> Self {
749        ProtoU64Description {
750            lower: Some(x.lower().into()),
751            upper: Some(x.upper().into()),
752            since: Some(x.since().into()),
753        }
754    }
755}
756
757/// Asserts the invariants of [`BlobTraceBatchPart`] for `updates` under the
758/// part's inline `desc`. Applies equally to a whole part and to any subset of
759/// its rows.
760pub fn validate_trace_updates<T: Timestamp + Codec64>(
761    desc: &Description<T>,
762    updates: &BlobTraceUpdates,
763) -> Result<(), Error> {
764    // TODO: It's unclear if the equal case (an empty desc) is
765    // useful/harmful. Feel free to make this a less_than if empty descs end
766    // up making sense.
767    if PartialOrder::less_equal(desc.upper(), desc.lower()) {
768        return Err(format!("invalid desc: {:?}", desc).into());
769    }
770
771    let uncompacted = PartialOrder::less_equal(desc.since(), desc.lower());
772
773    for time in updates.timestamps().values() {
774        let ts = T::decode(time.to_le_bytes());
775        // Check ts against desc.
776        if !desc.lower().less_equal(&ts) {
777            return Err(format!(
778                "timestamp {:?} is less than the batch lower: {:?}",
779                ts, desc
780            )
781            .into());
782        }
783
784        // when since is less than or equal to lower, the upper is a strict bound on the updates'
785        // timestamp because no compaction has been performed. Because user batches are always
786        // uncompacted, this ensures that new updates are recorded with valid timestamps.
787        // Otherwise, we can make no assumptions about the timestamps
788        if uncompacted && desc.upper().less_equal(&ts) {
789            return Err(format!(
790                "timestamp {:?} is greater than or equal to the batch upper: {:?}",
791                ts, desc
792            )
793            .into());
794        }
795    }
796
797    for (row_idx, diff) in updates.diffs().values().iter().enumerate() {
798        // TODO: Don't assume diff is an i64, take a D type param instead.
799        let diff: u64 = Codec64::decode(diff.to_le_bytes());
800
801        // Check data invariants.
802        if diff == 0 {
803            return Err(format!("update with 0 diff at row {row_idx}",).into());
804        }
805    }
806
807    Ok(())
808}
809
810/// Encodes the inline metadata for a trace batch into a base64 string.
811pub fn encode_trace_inline_meta<T: Timestamp + Codec64>(batch: &BlobTraceBatchPart<T>) -> String {
812    let (format, format_metadata) = match &batch.updates {
813        // For the legacy Row format, we only write it as `ParquetKvtd`.
814        BlobTraceUpdates::Row(_) => (ProtoBatchFormat::ParquetKvtd, None),
815        // For the newer structured format we track some metadata about the version of the format.
816        BlobTraceUpdates::Both { .. } => {
817            let metadata = ProtoFormatMetadata::StructuredMigration(2);
818            (ProtoBatchFormat::ParquetStructured, Some(metadata))
819        }
820        BlobTraceUpdates::Structured { .. } => {
821            let metadata = ProtoFormatMetadata::StructuredMigration(3);
822            (ProtoBatchFormat::ParquetStructured, Some(metadata))
823        }
824    };
825
826    let inline = ProtoBatchPartInline {
827        format: format.into(),
828        desc: Some((&batch.desc).into()),
829        index: batch.index,
830        format_metadata,
831    };
832    let inline_encoded = inline.encode_to_vec();
833    base64::engine::general_purpose::STANDARD.encode(inline_encoded)
834}
835
836/// Decodes the inline metadata for a trace batch from a base64 string.
837pub fn decode_trace_inline_meta(
838    inline_base64: Option<&String>,
839) -> Result<(ProtoBatchFormat, ProtoBatchPartInline), Error> {
840    let inline_base64 = inline_base64.ok_or("missing batch metadata")?;
841    let inline_encoded = base64::engine::general_purpose::STANDARD
842        .decode(inline_base64)
843        .map_err(|err| err.to_string())?;
844    let inline = ProtoBatchPartInline::decode(&*inline_encoded).map_err(|err| err.to_string())?;
845    let format = ProtoBatchFormat::try_from(inline.format)
846        .map_err(|_| Error::from(format!("unknown format: {}", inline.format)))?;
847    Ok((format, inline))
848}
849
850#[cfg(test)]
851mod tests {
852    use std::sync::Arc;
853
854    use bytes::Bytes;
855
856    use crate::error::Error;
857    use crate::indexed::columnar::ColumnarRecordsBuilder;
858    use crate::mem::{MemBlob, MemBlobConfig};
859    use crate::metrics::ColumnarMetrics;
860    use crate::workload::DataGenerator;
861
862    use super::*;
863
864    fn update_with_key(ts: u64, key: &'static str) -> ((Vec<u8>, Vec<u8>), u64, i64) {
865        ((key.into(), "".into()), ts, 1)
866    }
867
868    fn u64_desc(lower: u64, upper: u64) -> Description<u64> {
869        Description::new(
870            Antichain::from_elem(lower),
871            Antichain::from_elem(upper),
872            Antichain::from_elem(0),
873        )
874    }
875
876    fn batch_meta(lower: u64, upper: u64) -> TraceBatchMeta {
877        TraceBatchMeta {
878            keys: vec![],
879            format: ProtoBatchFormat::Unknown,
880            desc: u64_desc(lower, upper),
881            level: 1,
882            size_bytes: 0,
883        }
884    }
885
886    fn u64_desc_since(lower: u64, upper: u64, since: u64) -> Description<u64> {
887        Description::new(
888            Antichain::from_elem(lower),
889            Antichain::from_elem(upper),
890            Antichain::from_elem(since),
891        )
892    }
893
894    fn columnar_records(updates: Vec<((Vec<u8>, Vec<u8>), u64, i64)>) -> BlobTraceUpdates {
895        let mut builder = ColumnarRecordsBuilder::default();
896        for ((k, v), t, d) in updates {
897            assert!(builder.push(((&k, &v), Codec64::encode(&t), Codec64::encode(&d))));
898        }
899        let updates = builder.finish(&ColumnarMetrics::disconnected());
900        BlobTraceUpdates::Row(updates)
901    }
902
903    #[mz_ore::test]
904    fn trace_batch_validate() {
905        // Normal case
906        let b = BlobTraceBatchPart {
907            desc: u64_desc(0, 2),
908            index: 0,
909            updates: columnar_records(vec![update_with_key(0, "0"), update_with_key(1, "1")]),
910        };
911        assert_eq!(b.validate(), Ok(()));
912
913        // Empty
914        let b = BlobTraceBatchPart {
915            desc: u64_desc(0, 2),
916            index: 0,
917            updates: columnar_records(vec![]),
918        };
919        assert_eq!(b.validate(), Ok(()));
920
921        // Invalid desc
922        let b = BlobTraceBatchPart {
923            desc: u64_desc(2, 0),
924            index: 0,
925            updates: columnar_records(vec![]),
926        };
927        assert_eq!(
928            b.validate(),
929            Err(Error::from(
930                "invalid desc: Description { lower: Antichain { elements: [2] }, upper: Antichain { elements: [0] }, since: Antichain { elements: [0] } }"
931            ))
932        );
933
934        // Empty desc
935        let b = BlobTraceBatchPart {
936            desc: u64_desc(0, 0),
937            index: 0,
938            updates: columnar_records(vec![]),
939        };
940        assert_eq!(
941            b.validate(),
942            Err(Error::from(
943                "invalid desc: Description { lower: Antichain { elements: [0] }, upper: Antichain { elements: [0] }, since: Antichain { elements: [0] } }"
944            ))
945        );
946
947        // Update "before" desc
948        let b = BlobTraceBatchPart {
949            desc: u64_desc(1, 2),
950            index: 0,
951            updates: columnar_records(vec![update_with_key(0, "0")]),
952        };
953        assert_eq!(
954            b.validate(),
955            Err(Error::from(
956                "timestamp 0 is less than the batch lower: Description { lower: Antichain { elements: [1] }, upper: Antichain { elements: [2] }, since: Antichain { elements: [0] } }"
957            ))
958        );
959
960        // Update "after" desc
961        let b = BlobTraceBatchPart {
962            desc: u64_desc(1, 2),
963            index: 0,
964            updates: columnar_records(vec![update_with_key(2, "0")]),
965        };
966        assert_eq!(
967            b.validate(),
968            Err(Error::from(
969                "timestamp 2 is greater than or equal to the batch upper: Description { lower: Antichain { elements: [1] }, upper: Antichain { elements: [2] }, since: Antichain { elements: [0] } }"
970            ))
971        );
972
973        // Normal case: update "after" desc and within since
974        let b = BlobTraceBatchPart {
975            desc: u64_desc_since(1, 2, 4),
976            index: 0,
977            updates: columnar_records(vec![update_with_key(2, "0")]),
978        };
979        assert_eq!(b.validate(), Ok(()));
980
981        // Normal case: update "after" desc and at since
982        let b = BlobTraceBatchPart {
983            desc: u64_desc_since(1, 2, 4),
984            index: 0,
985            updates: columnar_records(vec![update_with_key(4, "0")]),
986        };
987        assert_eq!(b.validate(), Ok(()));
988
989        // Update "after" desc since
990        let b = BlobTraceBatchPart {
991            desc: u64_desc_since(1, 2, 4),
992            index: 0,
993            updates: columnar_records(vec![update_with_key(5, "0")]),
994        };
995        assert_eq!(b.validate(), Ok(()));
996
997        // Invalid update
998        let b = BlobTraceBatchPart {
999            desc: u64_desc(0, 1),
1000            index: 0,
1001            updates: columnar_records(vec![(("0".into(), "0".into()), 0, 0)]),
1002        };
1003        assert_eq!(
1004            b.validate(),
1005            Err(Error::from("update with 0 diff at row 0"))
1006        );
1007    }
1008
1009    #[mz_ore::test]
1010    fn trace_batch_meta_validate() {
1011        // Normal case
1012        let b = batch_meta(0, 1);
1013        assert_eq!(b.validate(), Ok(()));
1014
1015        // Empty interval
1016        let b = batch_meta(0, 0);
1017        assert_eq!(
1018            b.validate(),
1019            Err(Error::from(
1020                "invalid desc: Description { lower: Antichain { elements: [0] }, upper: Antichain { elements: [0] }, since: Antichain { elements: [0] } }"
1021            )),
1022        );
1023
1024        // Invalid interval
1025        let b = batch_meta(2, 0);
1026        assert_eq!(
1027            b.validate(),
1028            Err(Error::from(
1029                "invalid desc: Description { lower: Antichain { elements: [2] }, upper: Antichain { elements: [0] }, since: Antichain { elements: [0] } }"
1030            )),
1031        );
1032    }
1033
1034    async fn expect_set_trace_batch<T: Timestamp + Codec64>(
1035        blob: &dyn Blob,
1036        key: &str,
1037        batch: &BlobTraceBatchPart<T>,
1038    ) -> u64 {
1039        let mut val = Vec::new();
1040        let metrics = ColumnarMetrics::disconnected();
1041        let config = EncodingConfig::default();
1042        batch.encode(&mut val, &metrics, &config);
1043        let val = Bytes::from(val);
1044        let val_len = u64::cast_from(val.len());
1045        blob.set(key, val).await.expect("failed to set trace batch");
1046        val_len
1047    }
1048
1049    #[mz_ore::test(tokio::test)]
1050    #[cfg_attr(miri, ignore)] // unsupported operation: returning ready events from epoll_wait is not yet implemented
1051    async fn trace_batch_meta_validate_data() -> Result<(), Error> {
1052        let metrics = ColumnarMetrics::disconnected();
1053        let blob = Arc::new(MemBlob::open(MemBlobConfig::default()));
1054        let format = ProtoBatchFormat::ParquetKvtd;
1055
1056        let batch_desc = u64_desc_since(0, 3, 0);
1057        let batch0 = BlobTraceBatchPart {
1058            desc: batch_desc.clone(),
1059            index: 0,
1060            updates: columnar_records(vec![
1061                (("k".as_bytes().to_vec(), "v".as_bytes().to_vec()), 2, 1),
1062                (("k3".as_bytes().to_vec(), "v3".as_bytes().to_vec()), 2, 1),
1063            ]),
1064        };
1065        let batch1 = BlobTraceBatchPart {
1066            desc: batch_desc.clone(),
1067            index: 1,
1068            updates: columnar_records(vec![
1069                (("k4".as_bytes().to_vec(), "v4".as_bytes().to_vec()), 2, 1),
1070                (("k5".as_bytes().to_vec(), "v5".as_bytes().to_vec()), 2, 1),
1071            ]),
1072        };
1073
1074        let batch0_size_bytes = expect_set_trace_batch(blob.as_ref(), "b0", &batch0).await;
1075        let batch1_size_bytes = expect_set_trace_batch(blob.as_ref(), "b1", &batch1).await;
1076        let size_bytes = batch0_size_bytes + batch1_size_bytes;
1077        let batch_meta = TraceBatchMeta {
1078            keys: vec!["b0".into(), "b1".into()],
1079            format,
1080            desc: batch_desc.clone(),
1081            level: 0,
1082            size_bytes,
1083        };
1084
1085        // Normal case:
1086        assert_eq!(
1087            batch_meta.validate_data(blob.as_ref(), &metrics).await,
1088            Ok(())
1089        );
1090
1091        // Incorrect desc
1092        let batch_meta = TraceBatchMeta {
1093            keys: vec!["b0".into(), "b1".into()],
1094            format,
1095            desc: u64_desc_since(1, 3, 0),
1096            level: 0,
1097            size_bytes,
1098        };
1099        assert_eq!(
1100            batch_meta.validate_data(blob.as_ref(), &metrics).await,
1101            Err(Error::from(
1102                "invalid trace batch part desc expected Description { lower: Antichain { elements: [1] }, upper: Antichain { elements: [3] }, since: Antichain { elements: [0] } } got Description { lower: Antichain { elements: [0] }, upper: Antichain { elements: [3] }, since: Antichain { elements: [0] } }"
1103            ))
1104        );
1105        // Key with no corresponding batch part
1106        let batch_meta = TraceBatchMeta {
1107            keys: vec!["b0".into(), "b1".into(), "b2".into()],
1108            format,
1109            desc: batch_desc.clone(),
1110            level: 0,
1111            size_bytes,
1112        };
1113        assert_eq!(
1114            batch_meta.validate_data(blob.as_ref(), &metrics).await,
1115            Err(Error::from("no blob for trace batch at key: b2"))
1116        );
1117        // Batch parts not in index order
1118        let batch_meta = TraceBatchMeta {
1119            keys: vec!["b1".into(), "b0".into()],
1120            format,
1121            desc: batch_desc,
1122            level: 0,
1123            size_bytes,
1124        };
1125        assert_eq!(
1126            batch_meta.validate_data(blob.as_ref(), &metrics).await,
1127            Err(Error::from(
1128                "invalid index for blob trace batch part at key b1 expected 0 got 1"
1129            ))
1130        );
1131
1132        Ok(())
1133    }
1134
1135    #[mz_ore::test]
1136    #[cfg_attr(miri, ignore)] // too slow
1137    fn encoded_batch_sizes() {
1138        fn sizes(data: DataGenerator) -> usize {
1139            let metrics = ColumnarMetrics::disconnected();
1140            let config = EncodingConfig::default();
1141            let updates: Vec<_> = data.batches().collect();
1142            let updates = BlobTraceUpdates::Row(ColumnarRecords::concat(&updates, &metrics));
1143            let trace = BlobTraceBatchPart {
1144                desc: Description::new(
1145                    Antichain::from_elem(0u64),
1146                    Antichain::new(),
1147                    Antichain::from_elem(0u64),
1148                ),
1149                index: 0,
1150                updates,
1151            };
1152            let mut trace_buf = Vec::new();
1153            trace.encode(&mut trace_buf, &metrics, &config);
1154            trace_buf.len()
1155        }
1156
1157        let record_size_bytes = DataGenerator::default().record_size_bytes;
1158        // Print all the sizes into one assert so we only have to update one
1159        // place if sizes change.
1160        assert_eq!(
1161            format!(
1162                "1/1={:?} 25/1={:?} 1000/1={:?} 1000/100={:?}",
1163                sizes(DataGenerator::new(1, record_size_bytes, 1)),
1164                sizes(DataGenerator::new(25, record_size_bytes, 25)),
1165                sizes(DataGenerator::new(1_000, record_size_bytes, 1_000)),
1166                sizes(DataGenerator::new(1_000, record_size_bytes, 1_000 / 100)),
1167            ),
1168            "1/1=903 25/1=2649 1000/1=72881 1000/100=72881"
1169        );
1170    }
1171}